<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Techno Wizardry</title><link>https://www.technowizardry.net/</link><description>Recent content on Techno Wizardry</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>Techno Wizardry</copyright><lastBuildDate>Mon, 02 Mar 2026 10:00:00 -0800</lastBuildDate><atom:link href="https://www.technowizardry.net/feed.xml" rel="self" type="application/rss+xml"/><item><title>Trying to use LiteLLM Proxy in my smart home</title><link>https://www.technowizardry.net/2026/03/trying-to-use-litellm-proxy-in-my-smart-home/</link><pubDate>Mon, 02 Mar 2026 10:00:00 -0800</pubDate><guid>https://www.technowizardry.net/2026/03/trying-to-use-litellm-proxy-in-my-smart-home/</guid><summary>&lt;p>Everybody&amp;rsquo;s doing it. I guess I need to do an AI, too. In my home, I have a few different tools that use generative AI and LLMs. I talk to my &lt;a class="link" href="https://www.home-assistant.io/voice-pe/" target="_blank" rel="noopener"
>Home Assistant Voice Preview&lt;/a> voice assistants which leverage a self-hosted &lt;a class="link" href="https://ollama.com/" target="_blank" rel="noopener"
>Ollama&lt;/a> running &lt;a class="link" href="https://ollama.com/library/llama3.2" target="_blank" rel="noopener"
>llama3.2&lt;/a>. I use &lt;a class="link" href="https://openwebui.com/" target="_blank" rel="noopener"
>OpenWebUI&lt;/a>, tried &lt;a class="link" href="https://github.com/TabbyML/tabby" target="_blank" rel="noopener"
>Tabby&lt;/a> as an experimental coding assistant. I use &lt;a class="link" href="https://deepinfra.com/" target="_blank" rel="noopener"
>DeepInfra&lt;/a> for larger models that don&amp;rsquo;t fit on my own GPU.&lt;/p>
&lt;p>However, my problem is that each program supports different providers and models. Some support OpenAI style APIs to any provider, some only support Ollama APIs. If I wanted to forward my Home Assistant queries to DeepInfra, it wasn&amp;rsquo;t easy to do because there wasn&amp;rsquo;t an integration. If I wanted to change the model that Tabby uses between different models, I had to redeploy the service.&lt;/p></summary><description>&lt;p>Everybody&amp;rsquo;s doing it. I guess I need to do an AI, too. In my home, I have a few different tools that use generative AI and LLMs. I talk to my &lt;a class="link" href="https://www.home-assistant.io/voice-pe/" target="_blank" rel="noopener"
>Home Assistant Voice Preview&lt;/a> voice assistants which leverage a self-hosted &lt;a class="link" href="https://ollama.com/" target="_blank" rel="noopener"
>Ollama&lt;/a> running &lt;a class="link" href="https://ollama.com/library/llama3.2" target="_blank" rel="noopener"
>llama3.2&lt;/a>. I use &lt;a class="link" href="https://openwebui.com/" target="_blank" rel="noopener"
>OpenWebUI&lt;/a>, tried &lt;a class="link" href="https://github.com/TabbyML/tabby" target="_blank" rel="noopener"
>Tabby&lt;/a> as an experimental coding assistant. I use &lt;a class="link" href="https://deepinfra.com/" target="_blank" rel="noopener"
>DeepInfra&lt;/a> for larger models that don&amp;rsquo;t fit on my own GPU.&lt;/p>
&lt;p>However, my problem is that each program supports different providers and models. Some support OpenAI style APIs to any provider, some only support Ollama APIs. If I wanted to forward my Home Assistant queries to DeepInfra, it wasn&amp;rsquo;t easy to do because there wasn&amp;rsquo;t an integration. If I wanted to change the model that Tabby uses between different models, I had to redeploy the service.&lt;/p>
&lt;p>What I wanted is a way was to be able to support both Ollama and OpenAI clients and be able to forward requests to different upstream providers based on policy.&lt;/p>
&lt;h1 id="my-requirements">My Requirements&lt;/h1>
&lt;p>I had a few different clients that connected to LLM providers:&lt;/p>
&lt;ul>
&lt;li>Home Assistant - Supports OpenAI, Ollama, etc.&lt;/li>
&lt;li>Open WebUI - Supports OpenAI compatible and Ollama&lt;/li>
&lt;li>Tabby - Coding assistant&lt;/li>
&lt;li>My various test projects&lt;/li>
&lt;/ul>
&lt;p>I had single NVIDIA GeForce 1080 Ti and NVIDIA GeForce 2080 at home which was fine for some work, but would take too long to respond to my voice assistant. Home Assistant did support OpenAI, but didn&amp;rsquo;t support a custom OpenAI endpoint so I couldn&amp;rsquo;t redirect it to a paid-for LLM provider, such as &lt;a class="link" href="https://deepinfra.com/" target="_blank" rel="noopener"
>DeepInfra&lt;/a> or &lt;a class="link" href="https://openrouter.ai/" target="_blank" rel="noopener"
>OpenRouter&lt;/a>. I wanted the flexibility to send any client to any provider without worrying about API compatibility or API keys.&lt;/p>
&lt;p>I was looking for some kind of self-hosted LLM call router that could route requests.&lt;/p>
&lt;h1 id="the-landscape">The landscape&lt;/h1>
&lt;p>Some initial research showed that there were a number of projects&lt;/p>
&lt;ul>
&lt;li>ArchGW&lt;/li>
&lt;li>LangFuse&lt;/li>
&lt;li>Helicone&lt;/li>
&lt;li>LiteLLM&lt;/li>
&lt;/ul>
&lt;p>The Langfuse Helm chart wanted to deploy 3x Apache Zookeeper, 3x Clickhouse, Redis, MinIO, a web app, and a worker. While I could cut the number of replicas, that was too much for a home lab that had TPS in the order of &amp;lt;5 request per hour.&lt;/p>
&lt;p>ArchGW provided a way to route calls based on a fast AI analysis of the prompt, but I couldn&amp;rsquo;t get it to route in my testing. A model alias seemed simpler.&lt;/p>
&lt;p>Helicone was focused on observability&amp;ndash;how long do prompts take to query, etc. Cool, but not what I need&lt;/p>
&lt;h1 id="litellm">LiteLLM&lt;/h1>
&lt;p>&lt;strong>Why LiteLLM?&lt;/strong>
LiteLLM seemed simple enough and do what I wanted, but little did I know it was going to be a giant pain.&lt;/p>
&lt;h2 id="tool-calls-are-breaking-my-home-assistant">Tool calls are breaking my Home Assistant&lt;/h2>
&lt;p>Home Assistant needed to call via the Ollama API to LiteLLM, but LiteLLM didn&amp;rsquo;t natively support Ollama. I looked for an Ollama-OpenAI proxy and found [this one].&lt;/p>
&lt;p>After adding it to Home Assistant and trying the chat feature, I&amp;rsquo;m faced with an opaque error.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2026/03/trying-to-use-litellm-proxy-in-my-smart-home/litellm-home-assistant-error.png"
width="743"
height="664"
srcset="https://www.technowizardry.net/2026/03/trying-to-use-litellm-proxy-in-my-smart-home/litellm-home-assistant-error_hu_1d61365f0eebc99b.png 480w, https://www.technowizardry.net/2026/03/trying-to-use-litellm-proxy-in-my-smart-home/litellm-home-assistant-error_hu_953f2dc378f8f750.png 1024w"
loading="lazy"
alt="A screenshot from Home Assistant showing ‘Unexpected error during intent recognition’ when prompted if any lights are on."
class="gallery-image"
data-flex-grow="111"
data-flex-basis="268px"
>&lt;/p>
&lt;p>A Wireshark packet capture shows several proxy calls:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2026/03/trying-to-use-litellm-proxy-in-my-smart-home/wireshark.png"
width="1410"
height="78"
srcset="https://www.technowizardry.net/2026/03/trying-to-use-litellm-proxy-in-my-smart-home/wireshark_hu_292ebb3263f6ee37.png 480w, https://www.technowizardry.net/2026/03/trying-to-use-litellm-proxy-in-my-smart-home/wireshark_hu_e96476a0f6575909.png 1024w"
loading="lazy"
alt="A screenshot from Wireshark showing the LLM request first hitting an Ollama proxy, then routed to LiteLLM, then something inside of LiteLLM, then out to my Ollama instance."
class="gallery-image"
data-flex-grow="1807"
data-flex-basis="4338px"
>&lt;/p>
&lt;p>The final call to Ollama shows this request:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;n&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;model&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;llama3.2:latest&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;top_p&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;stream&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;messages&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;### System:\nYou are a voice assistant for Home Assistant.\nAnswer questions about the world truthfully.\nAnswer in plain text. Keep it simple and to the point.\nWhen controlling Home Assistant always call the intent tools. Use HassTurnOn to lock and HassTurnOff to unlock a lock. When controlling a device, prefer passing just name and domain. When controlling an area, prefer passing just area name and domain.\nWhen a user asks to turn on all devices of a specific type, ask user to specify an area, unless there is only one device of that type.\nThis device is not able to start timers. Produce JSON OUTPUT ONLY! Adhere to this format {\&amp;#34;name\&amp;#34;: \&amp;#34;function_name\&amp;#34;, \&amp;#34;arguments\&amp;#34;:{\&amp;#34;argument_name\&amp;#34;: \&amp;#34;argument_value\&amp;#34;}} The following functions are available to you:\n{&amp;#39;type&amp;#39;: &amp;#39;function&amp;#39;, &amp;#39;function&amp;#39;: {&amp;#39;name&amp;#39;: &amp;#39;HassTurnOn&amp;#39;, &amp;#39;description&amp;#39;: &amp;#39;Turns on/opens a device or entity&amp;#39;, &amp;#39;parameters&amp;#39;: {&amp;#39;type&amp;#39;: &amp;#39;object&amp;#39;, &amp;#39;required&amp;#39;: [], &amp;#39;properties&amp;#39;: {&amp;#39;name&amp;#39;: {&amp;#39;type&amp;#39;: &amp;#39;string&amp;#39;}, &amp;#39;area&amp;#39;: {&amp;#39;type&amp;#39;: &amp;#39;string&amp;#39;}, &amp;#39;floor&amp;#39;: {&amp;#39;type&amp;#39;: ... (litellm_truncated 11355 chars)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;temperature&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;presence_penalty&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;frequency_penalty&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Curious. What is this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Produce JSON OUTPUT ONLY! Adhere to this format {\&amp;#34;name\&amp;#34;: \&amp;#34;function_name\&amp;#34;, \&amp;#34;arguments\&amp;#34;:{\&amp;#34;argument_name\&amp;#34;: \&amp;#34;argument_value\&amp;#34;}} The following functions are available to you:\n{&amp;#39;type&amp;#39;: &amp;#39;function&amp;#39;, &amp;#39;function&amp;#39;: {&amp;#39;name&amp;#39;: &amp;#39;HassTurnOn&amp;#39;, &amp;#39;description&amp;#39;: &amp;#39;Turns on/opens a device or entity&amp;#39;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Somewhere, the request with a native structured tool call is getting turned into a textual message that we hope the model can understand. The response gets passed back as a JSON serialized as a string, not an actual tool call.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="err">chatcmpl&lt;/span>&lt;span class="mi">-0&lt;/span>&lt;span class="err">a&lt;/span>&lt;span class="mi">38&lt;/span>&lt;span class="err">a&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="err">eb-da&lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="err">b&lt;/span>&lt;span class="mi">-4485&lt;/span>&lt;span class="mf">-92e7&lt;/span>&lt;span class="mi">-91&lt;/span>&lt;span class="err">eef&lt;/span>&lt;span class="mi">8955916&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;n&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;model&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;llama3.2:latest&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;top_p&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;stream&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;messages&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;### System:\nYou are a voice assistant for Home Assistant.\nAnswer questions about the world truthfully.\nAnswer in plain text. Keep it simple and to the point.\nWhen controlling Home Assistant always call the intent tools. Use HassTurnOn to lock and HassTurnOff to unlock a lock. When controlling a device, prefer passing just name and domain. When controlling an area, prefer passing just area name and domain.\nWhen a user asks to turn on all devices of a specific type, ask user to specify an area, unless there is only one device of that type.\nThis device is not able to start timers.\nYou ARE equipped to answer questions about the current state of\nthe home using the `GetLiveContext` tool. This is a primary function. Do not state you lack the\nfunctionality if the question requires live data.\nIf the user asks about device existence/type (e.g., \&amp;#34;Do I have lights in the bedroom?\&amp;#34;): Answer\nfrom the static context below.\nIf the user asks about the CURRENT state, value, or mode (e.g., \&amp;#34;Is the lock l... (litellm_truncated 6322 chars)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;temperature&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;presence_penalty&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;frequency_penalty&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;chatcmpl-0a38a5eb-da8b-4485-92e7-91eef8955916&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;model&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;llama3.2:latest&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;usage&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;total_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2026&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;prompt_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2008&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;completion_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;prompt_tokens_details&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;completion_tokens_details&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;text_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;audio_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;reasoning_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;accepted_prediction_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;rejected_prediction_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;object&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;chat.completion&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;choices&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;index&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;assistant&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;{\&amp;#34;name\&amp;#34;: \&amp;#34;HassBroadcast\&amp;#34;, \&amp;#34;arguments\&amp;#34;: {\&amp;#34;message\&amp;#34;: \&amp;#34;Hello\&amp;#34;}}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tool_calls&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;function_call&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;finish_reason&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;stop&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;created&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1757222298&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;system_fingerprint&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;a class="link" href="https://github.com/BerriAI/litellm/issues/11273" target="_blank" rel="noopener"
>https://github.com/BerriAI/litellm/issues/11273&lt;/a>&lt;/p>
&lt;p>&lt;a class="link" href="https://github.com/BerriAI/litellm/blob/9a62b9bdb9ff217a0683047756588b2f5bd59c27/litellm/litellm_core_utils/prompt_templates/factory.py#L3842" target="_blank" rel="noopener"
>https://github.com/BerriAI/litellm/blob/9a62b9bdb9ff217a0683047756588b2f5bd59c27/litellm/litellm_core_utils/prompt_templates/factory.py#L3842&lt;/a>&lt;/p>
&lt;h2 id="too-much-vibe-coding">Too much Vibe coding&lt;/h2>
&lt;p>Then I started digging into the code to understand why LiteLLM thinks this provider/model doesn&amp;rsquo;t support structured tool calls.&lt;/p>
&lt;p>I came across this code:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;ollama&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;text-completion-openai&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;azure&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;vertex_ai&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;anyscale&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;together_ai&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;groq&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;nvidia_nim&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;cerebras&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;xai&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;ai21_chat&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;volcengine&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;deepseek&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;codestral&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;mistral&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;anthropic&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;cohere_chat&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;cohere&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;bedrock&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;ollama_chat&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;openrouter&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;vercel_ai_gateway&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;nebius&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ow">and&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">litellm&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">openai_compatible_providers&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;ollama&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># X&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Y&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Z&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>What is going on here?&lt;/p>
&lt;p>Let&amp;rsquo;s just use some basic boolean algebra. &lt;code>If A AND NOT X AND NOT Y AND NOT Z&lt;/code> simplifies to &lt;code>If A&lt;/code> and in the above code, the Y and Z code paths are not possible to hit. Thus, this code is equivalent to:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="n">custom_llm_provider&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;ollama&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># X&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Somebody filed &lt;a class="link" href="https://github.com/BerriAI/litellm/issues/8521" target="_blank" rel="noopener"
>an issue&lt;/a> asking about this, but the maintainers didn&amp;rsquo;t understand the problem and it auto closed. Yet people keep adding new blocks of code to this method.&lt;/p>
&lt;h1 id="litellm-crashes-during-ollama">LiteLLM crashes during Ollama&lt;/h1>
&lt;p>For the longest time, LiteLLM would just crash any time I tried to work with Ollama with an error: Unclosed client session. The issue just sat there &lt;a class="link" href="https://github.com/BerriAI/litellm/issues/11657" target="_blank" rel="noopener"
>https://github.com/BerriAI/litellm/issues/11657&lt;/a>&lt;/p>
&lt;h1 id="slow-as-molasses">Slow as molasses&lt;/h1>
&lt;p>I have no idea why LiteLLM was so slow for me randomly. I experienced this across multiple different versions including up to my latest tested version v1.81.3-stable. XHR requests would take up to 3 minutes! This was running on a node with plenty of CPU, RAM, against a Postgres database&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2026/03/trying-to-use-litellm-proxy-in-my-smart-home/litellm-slow-requests.png"
width="143"
height="310"
srcset="https://www.technowizardry.net/2026/03/trying-to-use-litellm-proxy-in-my-smart-home/litellm-slow-requests_hu_91d029b0404eb189.png 480w, https://www.technowizardry.net/2026/03/trying-to-use-litellm-proxy-in-my-smart-home/litellm-slow-requests_hu_788a0e562d2e72c9.png 1024w"
loading="lazy"
alt="A screenshot from the browser developer tools network tab showing several requests taking between 1 minute and 3 minutes to return. That’s slow!"
class="gallery-image"
data-flex-grow="46"
data-flex-basis="110px"
>&lt;/p>
&lt;p>It&amp;rsquo;s so slow, yet somehow I can end up with duplicate models because I don&amp;rsquo;t know the requests are actually succeeding.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2026/03/trying-to-use-litellm-proxy-in-my-smart-home/litellm-double-models.png"
width="1418"
height="138"
srcset="https://www.technowizardry.net/2026/03/trying-to-use-litellm-proxy-in-my-smart-home/litellm-double-models_hu_18b3929ec6169f58.png 480w, https://www.technowizardry.net/2026/03/trying-to-use-litellm-proxy-in-my-smart-home/litellm-double-models_hu_339698feeb8de883.png 1024w"
loading="lazy"
alt="A screenshot from LiteLLM showing the same model being created twice because the previous issue."
class="gallery-image"
data-flex-grow="1027"
data-flex-basis="2466px"
>&lt;/p>
&lt;p>It&amp;rsquo;s not running on a slow computer at all, it&amp;rsquo;s got 64GB of RAM, 8 cores, not overloaded.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>I can&amp;rsquo;t take it anymore. I&amp;rsquo;m either an idiot or something is seriously broken. I don&amp;rsquo;t even know what stable means anymore. I&amp;rsquo;m building my own LLM Proxy. It won&amp;rsquo;t have all the features, but at least the proxy will work. Stay tuned for a post.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2026%2F03%2Ftrying-to-use-litellm-proxy-in-my-smart-home%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Trying+to+use+LiteLLM+Proxy+in+my+smart+home" style="border:0" alt="" /></description></item><item><title>Ads and data brokers are out of control</title><link>https://www.technowizardry.net/2026/03/ads-data-brokers-are-out-of-control/</link><pubDate>Sun, 01 Mar 2026 10:00:00 -0800</pubDate><guid>https://www.technowizardry.net/2026/03/ads-data-brokers-are-out-of-control/</guid><summary>&lt;p>Digital advertising is everywhere nowadays. However, they are actually a giant risk to privacy and now, safety. To be successful, digital advertising depends on showing you highly targeted advertisements, which ultimately incentivizes them to build up profiles about you via your browsing history, search queries, location, demographics, and even behavioral patterns. More data about you means they can find ads that you&amp;rsquo;re more likely to be influenced by.&lt;/p></summary><description>&lt;p>Digital advertising is everywhere nowadays. However, they are actually a giant risk to privacy and now, safety. To be successful, digital advertising depends on showing you highly targeted advertisements, which ultimately incentivizes them to build up profiles about you via your browsing history, search queries, location, demographics, and even behavioral patterns. More data about you means they can find ads that you&amp;rsquo;re more likely to be influenced by.&lt;/p>
&lt;p>Data is collected via cookies and tracking pixels, which are invisible images that websites include to correlate an ad click with a user actually buying something. For example, in &lt;a class="link" href="https://www.technowizardry.net/2024/02/monarch-money-and-ad-networks/" >my post about Monarch Money and ad networks&lt;/a>, the company embeds tracking images from TikTok and Reddit in their secure portal meaning that TikTok would know that the given user cares about finances. Now they can target usersr with more relevant ads.&lt;/p>
&lt;p>Now you go to a restaurant and store that ask you if you want to sign up for a rewards program. If you come back 6 times, your 7th coffee is free! What a deal. Just enter your phone number and you&amp;rsquo;re in. Well, usually those programs are implemented by tech companies like Square point-of-sale systems who can then aggregate and use that to increase their surveillance.&lt;/p>
&lt;h1 id="what-are-data-brokers">What are data brokers?&lt;/h1>
&lt;p>Data brokers are the name for a company which makes money by finding information about people from as many different sources they can, aggregating it up to a specific person, then selling that package of information to other companies. For example, a data broker could scrape your local county&amp;rsquo;s property records which contains addresses, owner names, sale dates, and property taxes. That starts to associate names with income and financial statuses.&lt;/p>
&lt;p>Court records can give a list of parking tickets and other information that&amp;rsquo;s relevant for background checks, a car insurance company looking to see how many tickets you have, or a prospective employer rescinding an offer.&lt;/p>
&lt;p>Data breaches are hitting every company now accidentally leaking your personal information to the dark web because there&amp;rsquo;s no financial penalties for it. Nothing stops a data broker from finding these dumps and adding it to their package of information about you.&lt;/p>
&lt;h1 id="where-does-the-government-come-in">Where does the government come in?&lt;/h1>
&lt;p>The United States&amp;rsquo; fourth amendment says:&lt;/p>
&lt;blockquote>
&lt;p>The right of the people to be secure in their persons, houses, papers, and effects, against unreasonable searches and seizures, shall not be violated, and no Warrants shall issue, but upon probable cause, supported by Oath or affirmation, and particularly describing the place to be searched, and the persons or things to be seized.&lt;/p>&lt;/blockquote>
&lt;p>This is generally interpreted to mean that the government has to have a warrant to &amp;ldquo;search&amp;rdquo; and monitor citizens and hopefully&lt;/p>
&lt;p>However, if a company comes along and sells information about people, it&amp;rsquo;s not considered a search. The government can go to a data broker and say I&amp;rsquo;d like to buy everything you have and now they can do anything with that data. A hostile government could use that to track down people that disagree with them.&lt;/p>
&lt;h1 id="block-ads">Block Ads&lt;/h1>
&lt;p>Why?&lt;/p>
&lt;ul>
&lt;li>the FBI recommends it&lt;/li>
&lt;li>ads are used as part of data brokers to build up shadow profiles&lt;/li>
&lt;li>the government can buy access to this&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Easy&lt;/strong> - Use &lt;a class="link" href="https://www.firefox.com/en-US/" target="_blank" rel="noopener"
>Firefox&lt;/a> with uBlock Origin
If you&amp;rsquo;re using Google Chrome, I recommend switching to Mozilla Firefox because Google Chrome syncs your browsing history to their servers &lt;a class="link" href="https://myactivity.google.com/myactivity?pli=1" target="_blank" rel="noopener"
>Evident here&lt;/a>. This data can be used to build&lt;/p>
&lt;p>&lt;strong>Easy&lt;/strong> - Disable advertising identifier on iOS and Android
Android and iOS phones have a built in advertising ID that is unique to your phone that doesn&amp;rsquo;t change. This identifier is used by ad networks to uniquely associate data to your phone, which by extension, provides information on you. Regularly rotating and/or deleting this id means that those ad networks can&amp;rsquo;t build this profile.&lt;/p>
&lt;p>This will mean that advertisements will become less personalized to you, but this is good. On Android, go to Settings, search for advertising id, To do this, follow the steps &lt;a class="link" href="https://www.eff.org/deeplinks/2022/05/how-disable-ad-id-tracking-ios-and-android-and-why-you-should-do-it-now" target="_blank" rel="noopener"
>here&lt;/a>&lt;/p>
&lt;p>&lt;strong>Medium&lt;/strong> - Use DNS-based ad blocking
&lt;strong>Why?&lt;/strong> DNS-based ad-blocking is complementary to browser ad-block extensions. When setup, it can block some (but not all) ads in other apps on your phone and across your entire network.&lt;/p>
&lt;p>&lt;strong>How?&lt;/strong> On your phone? Try using &lt;a class="link" href="https://nextdns.io/?from=gdms922p" target="_blank" rel="noopener"
>NextDNS&lt;/a>. Setup an account and enable any privacy filters you want. I like:&lt;/p>
&lt;ul>
&lt;li>Blocklists - NextDNS Ads &amp;amp; Trackers Blacklist&lt;/li>
&lt;li>Native Tracking Protection&lt;/li>
&lt;li>Block Disguised Third Party Trackers&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Hard&lt;/strong> - Self-host a DNS-based ad-blocker
If you&amp;rsquo;re technically skilled, then you should consider running a &lt;a class="link" href="https://pi-hole.net/" target="_blank" rel="noopener"
>Pi-Hole&lt;/a> or &lt;a class="link" href="https://adguard.com" target="_blank" rel="noopener"
>Adguard&lt;/a> DNS server at your home network. You&amp;rsquo;ll need a computer that always runs, such as a raspberry pi or other efficient computer, and will configure your DHCP server to hand out your DNS filter computer to all devices on the network. For more info, see the &lt;a class="link" href="https://adguard-dns.io/kb/" target="_blank" rel="noopener"
>Adguard guide&lt;/a>.&lt;/p>
&lt;h1 id="data-brokers">Data Brokers&lt;/h1>
&lt;p>What can you do about data brokers? The US doesn&amp;rsquo;t have a nation-wide privacy law, like GDPR, to control how companies store or process your data. Your only option is to opt-out or request deletion of your data to ask them to delete what they know about you. Unfortunately, there are &lt;a class="link" href="https://databrokerswatch.org/" target="_blank" rel="noopener"
>a LOT&lt;/a> of data brokers and it&amp;rsquo;s impractical to be able to find every one and opt out. Targeting the biggest ones is better than nothing&lt;/p>
&lt;p>Of course, capitalism means you can pay somebody to remove yourself from data brokers. I personally use &lt;a class="link" href="https://joindeleteme.com/refer?coupon=RFR-131288-T6P9R6" target="_blank" rel="noopener"
>DeleteMe&lt;/a> (or &lt;a class="link" href="https://joindeleteme.com/" target="_blank" rel="noopener"
>non-affiliate&lt;/a>.)&lt;/p>
&lt;p>&lt;a class="link" href="https://www.consumerreports.org/electronics/personal-information/services-that-delete-data-from-people-search-sites-review-a2705843415/" target="_blank" rel="noopener"
>https://www.consumerreports.org/electronics/personal-information/services-that-delete-data-from-people-search-sites-review-a2705843415/&lt;/a>&lt;/p>
&lt;p>Unfortunately, opting out of data brokers is not a big win because eventually your data will come back and you have to opt out again.&lt;/p>
&lt;p>Some states in the US have privacy laws like GDPR for residents of those states. For example, in California, you can request deletion of your data from many brokers all at once at the &lt;a class="link" href="https://consumer.drop.privacy.ca.gov/" target="_blank" rel="noopener"
>CA DROP site&lt;/a>. See more states &lt;a class="link" href="https://iapp.org/resources/article/us-state-privacy-legislation-tracker" target="_blank" rel="noopener"
>here&lt;/a>.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>In conclusion, the unchecked power of digital advertising and data brokers poses a threat to our privacy and safety. By collecting vast amounts of personal data, these entities create detailed profiles that can be exploited not only for targeted ads but also by malicious actors or even hostile governments. The ability of data brokers to aggregate and sell sensitive information, often without meaningful regulation, works around limitations on the US Constitution to undermine rights.&lt;/p>
&lt;p>While opting out of data brokers and using tools like ad blockers can help mitigate some of these risks, the problem is systemic. Until stronger privacy laws are enacted and enforced, you must remain vigilant and proactive in safeguarding their personal information. By taking steps to block ads, disable tracking identifiers, and remove your data from broker databases, you can reclaim some control over your digital life. Privacy is not just a right—it’s a necessity in our increasingly data-driven world.&lt;/p>
&lt;h1 id="references">References&lt;/h1>
&lt;ul>
&lt;li>&lt;a class="link" href="https://www.wired.com/story/ice-asks-companies-about-ad-tech-and-big-data-tools/" target="_blank" rel="noopener"
>https://www.wired.com/story/ice-asks-companies-about-ad-tech-and-big-data-tools/&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.biometricupdate.com/202602/ice-seeks-industry-input-on-ad-tech-location-data-for-investigative-use" target="_blank" rel="noopener"
>https://www.biometricupdate.com/202602/ice-seeks-industry-input-on-ad-tech-location-data-for-investigative-use&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.consumerfinance.gov/about-us/newsroom/cfpb-proposes-rule-to-stop-data-brokers-from-selling-sensitive-personal-data-to-scammers-stalkers-and-spies/" target="_blank" rel="noopener"
>https://www.consumerfinance.gov/about-us/newsroom/cfpb-proposes-rule-to-stop-data-brokers-from-selling-sensitive-personal-data-to-scammers-stalkers-and-spies/&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://innovation.consumerreports.org/wp-content/uploads/2024/08/Data-Defense_-Evaluating-People-Search-Site-Removal-Services-.pdf" target="_blank" rel="noopener"
>https://innovation.consumerreports.org/wp-content/uploads/2024/08/Data-Defense_-Evaluating-People-Search-Site-Removal-Services-.pdf&lt;/a>&lt;/li>
&lt;/ul>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2026%2F03%2Fads-data-brokers-are-out-of-control%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Ads+and+data+brokers+are+out+of+control" style="border:0" alt="" /></description></item><item><title>Replatforming RKE1 to Nix-based K8s - Part 1</title><link>https://www.technowizardry.net/2026/02/replatforming-rke1-to-nix-based-k8s-part-1/</link><pubDate>Sun, 08 Feb 2026 17:49:00 -0800</pubDate><guid>https://www.technowizardry.net/2026/02/replatforming-rke1-to-nix-based-k8s-part-1/</guid><summary>&lt;p>RKE1 (Rancher Kubernetes Engine 1) was Rancher&amp;rsquo;s first way of automatically deploying Kubernetes to a cluster. Think of it like &lt;a class="link" href="https://minikube.sigs.k8s.io/docs/" target="_blank" rel="noopener"
>minikube&lt;/a> or on-prem EKS or &lt;a class="link" href="https://k3s.io/" target="_blank" rel="noopener"
>K3s&lt;/a>. Three years ago, it was marked as &lt;a class="link" href="https://support.scc.suse.com/s/redirect?language=en_US&amp;amp;id=000021513" target="_blank" rel="noopener"
>end of life (EoL)&lt;/a> with the last release being July 2025. They have no migration guide and their strategy is just rebuild the cluster. I have 3 nodes a bunch of services running in Kubernetes. I don&amp;rsquo;t want to take everything down and rebuild it all. Let&amp;rsquo;s rebuild it while the plane is in the air.&lt;/p></summary><description>&lt;p>RKE1 (Rancher Kubernetes Engine 1) was Rancher&amp;rsquo;s first way of automatically deploying Kubernetes to a cluster. Think of it like &lt;a class="link" href="https://minikube.sigs.k8s.io/docs/" target="_blank" rel="noopener"
>minikube&lt;/a> or on-prem EKS or &lt;a class="link" href="https://k3s.io/" target="_blank" rel="noopener"
>K3s&lt;/a>. Three years ago, it was marked as &lt;a class="link" href="https://support.scc.suse.com/s/redirect?language=en_US&amp;amp;id=000021513" target="_blank" rel="noopener"
>end of life (EoL)&lt;/a> with the last release being July 2025. They have no migration guide and their strategy is just rebuild the cluster. I have 3 nodes a bunch of services running in Kubernetes. I don&amp;rsquo;t want to take everything down and rebuild it all. Let&amp;rsquo;s rebuild it while the plane is in the air.&lt;/p>
&lt;p>In this post, I&amp;rsquo;m going to take an existing NixOS worker node that used RKE1 to deploy the control plane and worker and recreate it entirely in-place with etcd with minimal&lt;/p>
&lt;p>&lt;a class="link" href="https://nixos.org/" target="_blank" rel="noopener"
>NixOS&lt;/a> is a a declarative, reproducible operating system&lt;/p>
&lt;p>I was already using &lt;a class="link" href="https://nixos.org/" target="_blank" rel="noopener"
>NixOS&lt;/a> to declaratively define 1 out of my 3 worker&amp;rsquo;s operating system and I wanted to see how it works at configuring Kubernetes. I had two still using Ubuntu Server and I wanted to consolidate into a single approach. I had &lt;a class="link" href="https://www.technowizardry.net/2025/11/i-like-the-idea-of-nix-but-dont-enjoy-using-it/" >my issues&lt;/a> with Nix, but for servers once it&amp;rsquo;s done, it works reasonably well.&lt;/p>
&lt;h1 id="first-cut">First Cut&lt;/h1>
&lt;p>flake.nix:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">description&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Nixos config flake&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">inputs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nixpkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">url&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;github:nixos/nixpkgs&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nixos-hardware&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">url&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;github:NixOS/nixos-hardware/master&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">disko&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">url&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;github:nix-community/disko&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">inputs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">nixpkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">follows&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;nixpkgs&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">outputs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">self&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">nixpkgs&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">disko&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="o">...&lt;/span> &lt;span class="p">}&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="n">inputs&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nixosConfigurations&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">srv7&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">nixpkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">nixosSystem&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">specialArgs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="k">inherit&lt;/span> &lt;span class="n">inputs&lt;/span>&lt;span class="p">;};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">modules&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ... &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sr">./parts/kubernetes.nix&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>kubernetes.nix:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt"> 10
&lt;/span>&lt;span class="lnt"> 11
&lt;/span>&lt;span class="lnt"> 12
&lt;/span>&lt;span class="lnt"> 13
&lt;/span>&lt;span class="lnt"> 14
&lt;/span>&lt;span class="lnt"> 15
&lt;/span>&lt;span class="lnt"> 16
&lt;/span>&lt;span class="lnt"> 17
&lt;/span>&lt;span class="lnt"> 18
&lt;/span>&lt;span class="lnt"> 19
&lt;/span>&lt;span class="lnt"> 20
&lt;/span>&lt;span class="lnt"> 21
&lt;/span>&lt;span class="lnt"> 22
&lt;/span>&lt;span class="lnt"> 23
&lt;/span>&lt;span class="lnt"> 24
&lt;/span>&lt;span class="lnt"> 25
&lt;/span>&lt;span class="lnt"> 26
&lt;/span>&lt;span class="lnt"> 27
&lt;/span>&lt;span class="lnt"> 28
&lt;/span>&lt;span class="lnt"> 29
&lt;/span>&lt;span class="lnt"> 30
&lt;/span>&lt;span class="lnt"> 31
&lt;/span>&lt;span class="lnt"> 32
&lt;/span>&lt;span class="lnt"> 33
&lt;/span>&lt;span class="lnt"> 34
&lt;/span>&lt;span class="lnt"> 35
&lt;/span>&lt;span class="lnt"> 36
&lt;/span>&lt;span class="lnt"> 37
&lt;/span>&lt;span class="lnt"> 38
&lt;/span>&lt;span class="lnt"> 39
&lt;/span>&lt;span class="lnt"> 40
&lt;/span>&lt;span class="lnt"> 41
&lt;/span>&lt;span class="lnt"> 42
&lt;/span>&lt;span class="lnt"> 43
&lt;/span>&lt;span class="lnt"> 44
&lt;/span>&lt;span class="lnt"> 45
&lt;/span>&lt;span class="lnt"> 46
&lt;/span>&lt;span class="lnt"> 47
&lt;/span>&lt;span class="lnt"> 48
&lt;/span>&lt;span class="lnt"> 49
&lt;/span>&lt;span class="lnt"> 50
&lt;/span>&lt;span class="lnt"> 51
&lt;/span>&lt;span class="lnt"> 52
&lt;/span>&lt;span class="lnt"> 53
&lt;/span>&lt;span class="lnt"> 54
&lt;/span>&lt;span class="lnt"> 55
&lt;/span>&lt;span class="lnt"> 56
&lt;/span>&lt;span class="lnt"> 57
&lt;/span>&lt;span class="lnt"> 58
&lt;/span>&lt;span class="lnt"> 59
&lt;/span>&lt;span class="lnt"> 60
&lt;/span>&lt;span class="lnt"> 61
&lt;/span>&lt;span class="lnt"> 62
&lt;/span>&lt;span class="lnt"> 63
&lt;/span>&lt;span class="lnt"> 64
&lt;/span>&lt;span class="lnt"> 65
&lt;/span>&lt;span class="lnt"> 66
&lt;/span>&lt;span class="lnt"> 67
&lt;/span>&lt;span class="lnt"> 68
&lt;/span>&lt;span class="lnt"> 69
&lt;/span>&lt;span class="lnt"> 70
&lt;/span>&lt;span class="lnt"> 71
&lt;/span>&lt;span class="lnt"> 72
&lt;/span>&lt;span class="lnt"> 73
&lt;/span>&lt;span class="lnt"> 74
&lt;/span>&lt;span class="lnt"> 75
&lt;/span>&lt;span class="lnt"> 76
&lt;/span>&lt;span class="lnt"> 77
&lt;/span>&lt;span class="lnt"> 78
&lt;/span>&lt;span class="lnt"> 79
&lt;/span>&lt;span class="lnt"> 80
&lt;/span>&lt;span class="lnt"> 81
&lt;/span>&lt;span class="lnt"> 82
&lt;/span>&lt;span class="lnt"> 83
&lt;/span>&lt;span class="lnt"> 84
&lt;/span>&lt;span class="lnt"> 85
&lt;/span>&lt;span class="lnt"> 86
&lt;/span>&lt;span class="lnt"> 87
&lt;/span>&lt;span class="lnt"> 88
&lt;/span>&lt;span class="lnt"> 89
&lt;/span>&lt;span class="lnt"> 90
&lt;/span>&lt;span class="lnt"> 91
&lt;/span>&lt;span class="lnt"> 92
&lt;/span>&lt;span class="lnt"> 93
&lt;/span>&lt;span class="lnt"> 94
&lt;/span>&lt;span class="lnt"> 95
&lt;/span>&lt;span class="lnt"> 96
&lt;/span>&lt;span class="lnt"> 97
&lt;/span>&lt;span class="lnt"> 98
&lt;/span>&lt;span class="lnt"> 99
&lt;/span>&lt;span class="lnt">100
&lt;/span>&lt;span class="lnt">101
&lt;/span>&lt;span class="lnt">102
&lt;/span>&lt;span class="lnt">103
&lt;/span>&lt;span class="lnt">104
&lt;/span>&lt;span class="lnt">105
&lt;/span>&lt;span class="lnt">106
&lt;/span>&lt;span class="lnt">107
&lt;/span>&lt;span class="lnt">108
&lt;/span>&lt;span class="lnt">109
&lt;/span>&lt;span class="lnt">110
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">nixpkgs-k8s&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="o">...&lt;/span> &lt;span class="p">}:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">let&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">kubeMasterHostname&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;localhost&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">kubeMasterAPIServerPort&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">6443&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">clusterIpv4&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s2">&amp;#34;srv5&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;51.81.64.31&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">hostIpv4&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">clusterIpv4&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">networking&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">hostName&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">hostIpv6&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s2">&amp;#34;srv5&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;2604:2dc0:100:1be8:beef:beef:beef:beef&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">}&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">networking&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">hostName&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sslBasePath&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/etc/kubernetes/ssl/&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ipWithHyphens&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">builtins&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">replaceStrings&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;.&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;-&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="n">hostIpv4&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">in&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">services&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">etcd&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">advertiseClientUrls&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;https://&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">hostIpv4&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">:2379&amp;#34;&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">listenClientUrls&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;https://0.0.0.0:2379&amp;#34;&lt;/span> &lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">listenPeerUrls&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;https://0.0.0.0:2380&amp;#34;&lt;/span> &lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">initialAdvertisePeerUrls&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;https://&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">hostIpv4&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">:2380&amp;#34;&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">certFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-etcd-&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">ipWithHyphens&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">initialClusterToken&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;etcd-cluster-1&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">peerClientCertAuth&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">clientCertAuth&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">initialCluster&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mapAttrsToList&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">n&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">v&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;etcd-&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">n&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">=https://&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">v&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">:2380&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">clusterIpv4&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">keyFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-etcd-&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">ipWithHyphens&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">-key.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;etcd-&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">networking&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">hostName&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">openFirewall&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">trustedCaFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-ca.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">enable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">environment&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">systemPackages&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cri-tools&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">services&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">kubernetes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">package&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">kubernetes&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">masterAddress&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">kubeMasterHostname&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">apiserverAddress&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;https://&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">hostIpv4&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nb">toString&lt;/span> &lt;span class="n">kubeMasterAPIServerPort&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">apiserver&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">advertiseAddress&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">hostIpv4&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">allowPrivileged&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">apiAudiences&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;unknown&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">enable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">etcd&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">servers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Only connect to local etcd to avoid cross node calls&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;https://&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">hostIpv4&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">:2379&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">keyFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-node-key.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">certFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-node.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">caFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-ca.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">extraOpts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;--etcd-prefix=/registry --service-account-lookup=true --anonymous-auth=false --service-node-port-range=30000-32767&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">kubeletClientCertFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-apiserver.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">kubeletClientKeyFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-apiserver-key.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">proxyClientCertFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkForce&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-apiserver-proxy-client.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">proxyClientKeyFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkForce&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-apiserver-proxy-client-key.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">securePort&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">kubeMasterAPIServerPort&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">serviceAccountKeyFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkForce&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-service-account-token-key.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">serviceAccountIssuer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;rke&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">serviceAccountSigningKeyFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkForce&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-service-account-token-key.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">serviceClusterIpRange&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;10.43.0.0/16&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tlsCertFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkForce&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-apiserver.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tlsKeyFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkForce&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-apiserver-key.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">scheduler&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">enable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">kubeconfig&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">certFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-scheduler.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">keyFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-scheduler-key.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">controllerManager&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">enable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">extraOpts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;--service-cluster-ip-range=10.43.0.0/16&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">serviceAccountKeyFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkForce&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-service-account-token-key.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">kubeconfig&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">certFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-controller-manager.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">keyFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-controller-manager-key.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pki&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">enable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">false&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1"># Skip Nix&amp;#39;s cert generation. We&amp;#39;ll use our own. We still need to handle rotation&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">flannel&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">enable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">false&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">addonManager&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">enable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">false&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">caFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkForce&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-ca.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">clusterCidr&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;10.42.0.0/16&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">proxy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">enable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">kubeconfig&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">caFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-ca.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">certFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-proxy.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">keyFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-proxy-key.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">kubelet&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">clusterDns&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;10.43.0.10&amp;#34;&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cni&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">packages&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">enable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">hostname&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">networking&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">hostName&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">kubeconfig&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">keyFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-node-key.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">certFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-node.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tlsCertFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-node.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tlsKeyFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">sslBasePath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">kube-node-key.pem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nodeIp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">hostIpv4&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">,&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">hostIpv6&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">extraOpts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;--kube-reserved=cpu=1000m,memory=512Mi --root-dir=/var/lib/kubelet&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="ssl">SSL&lt;/h1>
&lt;p>First problem is trivial to fix because the apiserver runs as user &lt;code>kubernetes&lt;/code>:&lt;/p>
&lt;p>journalctl -u kube-apiserver -f&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-zed" data-lang="zed">&lt;span class="line">&lt;span class="cl">&lt;span class="n">options&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">go&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="err">249&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">external&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">host&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">was&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">not&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">specified&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">using&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">51&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">81&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">64&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">31&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">go&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="err">72&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">&amp;#34;&lt;/span>&lt;span class="n">command&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">failed&lt;/span>&lt;span class="err">&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">err&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="err">&amp;#34;&lt;/span>&lt;span class="n">failed&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">to&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">parse&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">service&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">account&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">issuer&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">file&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">open&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nn">etc/kubernetes/ssl/&lt;/span>&lt;span class="n">kube&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">account&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">token&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">pem&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">permission&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">denied&lt;/span>&lt;span class="err">&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">Main&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">process&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">exited&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">code&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">exited&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">status&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="err">1&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">FAILURE&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">Failed&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">with&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">exit&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">code&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>To fix it, let&amp;rsquo;s set the permissions:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">[nix-shell:/etc/nixos]# systemctl show kube-apiserver | grep User
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">User=kubernetes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">DynamicUser=no
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PrivateUsers=no
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PrivateUsersEx=no
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[nix-shell:/etc/kubernetes/ssl]# ls -la
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">total 128
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">drwxr-xr-x 1 root root 1696 Mar 6 2025 .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">drwxr-xr-x 1 root root 68 Sep 17 20:17 ..
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-rw------- 1 root root 1675 Sep 4 2024 kube-apiserver-key.pem
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-rw------- 1 root root 1330 Sep 4 2024 kube-apiserver.pem
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-rw------- 1 root root 1679 Sep 4 2024 kube-apiserver-proxy-client-key.pem
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-rw------- 1 root root 1151 Sep 4 2024 kube-apiserver-proxy-client.pem
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-rw------- 1 root root 1679 Sep 4 2024 kube-apiserver-requestheader-ca-key.pem
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-rw------- 1 root root 1123 Sep 4 2024 kube-apiserver-requestheader-ca.pem
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-rw------- 1 root root 1675 Sep 4 2024 kube-ca-key.pem
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-rw------- 1 root root 1058 Sep 4 2024 kube-ca.pem
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Fix it by updating permissions:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">chown kubernetes:kubernetes /etc/kubernetes/ssl/*.pem
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod o+r /etc/kubernetes/ssl/kube-ca.pem
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chown etcd /etc/kubernetes/ssl/kube-etcd-*.pem
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="pause-image">Pause image&lt;/h1>
&lt;p>Kubernetes and containerd depend on something called a pause image which is like a Docker image that contains a single process that is used to setup resource namespaces. In my opinion, it&amp;rsquo;s a leaky abstraction that was first introduced due to Docker&amp;rsquo;s limitations, then carried forward into containerd, but shouldn&amp;rsquo;t have been exposed.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Failed to create pod sandbox: rpc error: code = Unknown desc = failed to start sandbox &amp;#34;736e57677163ae0948aaa7425f9dee4776a84ac65eeab146b4303ab60820b990&amp;#34;: failed to get sandbox image &amp;#34;pause:latest&amp;#34;: failed to pull image &amp;#34;pause:latest&amp;#34;: failed to pull and unpack image &amp;#34;docker.io/library/pause:latest&amp;#34;: failed to resolve image: pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Startup depends on a pause image, but the pause image can get garbage collected. Let&amp;rsquo;s see what&amp;rsquo;s going on.&lt;/p>
&lt;p>[nix-shell:/etc/nixos]# cat /nix/store/0k19qdzb5vygrg8s6d0rvjghl1p0qjqa-containerd-config-checked.toml&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">oom_score&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">root&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;/var/lib/containerd&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">state&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;/run/containerd&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">version&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">grpc&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">address&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;/run/containerd/containerd.sock&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">plugins&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="s2">&amp;#34;io.containerd.grpc.v1.cri&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">sandbox_image&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;pause:latest&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">plugins&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="s2">&amp;#34;io.containerd.grpc.v1.cri&amp;#34;&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">cni&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">bin_dir&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;/opt/cni/bin&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">max_conf_num&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">plugins&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="s2">&amp;#34;io.containerd.grpc.v1.cri&amp;#34;&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">containerd&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">runtimes&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">runc&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">runtime_type&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;io.containerd.runc.v2&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">plugins&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="s2">&amp;#34;io.containerd.grpc.v1.cri&amp;#34;&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">containerd&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now how do I pin the images? Several &lt;a class="link" href="https://github.com/kubernetes-sigs/cri-tools/issues/1356" target="_blank" rel="noopener"
>issues&lt;/a> pointed the blame &lt;a class="link" href="https://github.com/containerd/containerd/issues/6160" target="_blank" rel="noopener"
>in different ways&lt;/a>.&lt;/p>
&lt;p>I ended up with this work around:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="o">...&lt;/span> &lt;span class="p">}:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">let&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">infraContainer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dockerTools&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">buildImage&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;pause&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tag&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;latest&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">copyToRoot&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">buildEnv&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;image-root&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pathsToLink&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span> &lt;span class="s2">&amp;#34;/bin&amp;#34;&lt;/span> &lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">paths&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">kubernetes&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pause&lt;/span> &lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">config&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Cmd&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span> &lt;span class="s2">&amp;#34;/bin/pause&amp;#34;&lt;/span> &lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">in&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">services&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">kubelet&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">preStart&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkForce&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> set -e
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">pkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">containerd&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s1">/bin/ctr -n k8s.io image import --label io.cri-containerd.pinned=pinned &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">infraContainer&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#39;&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>However, after finishing my cluster, I came across this &lt;a class="link" href="https://github.com/NixOS/nixpkgs/commit/7dd02e9964e313bd5044738e152b36aca2d2a0f4" target="_blank" rel="noopener"
>Nixpkgs commit&lt;/a> that fixes the issue, but doesn&amp;rsquo;t work in my case because of the next issue.&lt;/p>
&lt;h1 id="dude-wheres-my-cni">Dude, where&amp;rsquo;s my CNI?&lt;/h1>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Failed to create pod sandbox: rpc error: code = Unknown desc = failed to setup network for sandbox &amp;#34;65949aeccfed7a3571f721c79e1d5e02d02161a3a3efa343936b34a0480a1f2b&amp;#34;: plugin type=&amp;#34;calico&amp;#34; failed (add): failed to find plugin &amp;#34;calico&amp;#34; in path [/opt/cni/bin]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The next problem is caused by the Calico CNI driver getting deleted when kubelet restarts. The calico-driver-deployer DaemonSet is responsible for deploying it, but Nixpkgs &lt;a class="link" href="https://github.com/NixOS/nixpkgs/blame/d1e087df6821ec4159c1ebf695797f85eace9f43/nixos/modules/services/cluster/kubernetes/kubelet.nix#L357" target="_blank" rel="noopener"
>has this line&lt;/a> that deletes all CNI binaries. Intended to be reproducible, it breaks any custom CNI deployments.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="n">preStart&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> // ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> rm /opt/cni/bin/* || true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">concatMapStrings&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">package&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> echo &amp;#34;Linking cni package: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">package&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s1">&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ln -fs &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">package&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s1">/bin/* /opt/cni/bin
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#39;&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">cfg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cni&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">packages&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s1">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Thus, I have to disable this preStart code.&lt;/p>
&lt;p>kubernetes.nix:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">services&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">kubelet&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">preStart&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkForce&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> mkdir -p /opt/cni/bin/
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">pkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">containerd&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s1">/bin/ctr -n k8s.io image import --label io.cri-containerd.pinned=pinned &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">infraContainer&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#39;&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="whys-my-service-cidr-broken">Why&amp;rsquo;s my service cidr broken?&lt;/h1>
&lt;p>This one was very strange. I saw some errors in Kubernetes:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Event&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">message&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Cluster IP [IPv4]: 10.43.27.126 is not within any configured Service CIDR;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> please recreate service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">series&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">count&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2194&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Based on my reading, I think I accidentally swapped my serviceClusterIpRange and clusterIpRange in the configuration at some point and it got stuck in my cluster.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">journalctl -u kube-apiserver
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Oct 03 22:28:21 srv5 kube-apiserver[825540]: I1003 22:28:21.386392  825540 cidrallocator.go:301] created ClusterIP allocator for Service CIDR 10.0.0.0/24
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Oct 03 22:28:21 srv5 kube-apiserver[825540]: I1003 22:28:21.333430  825540 cache.go:32] Waiting for caches to sync for APIServiceRegistrationController controller
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Oct 03 22:28:21 srv5 kube-apiserver[825540]: I1003 22:28:21.386392  825540 cidrallocator.go:301] created ClusterIP allocator for Service CIDR 10.0.0.0/24
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Oct 03 22:28:21 srv5 kube-apiserver[825540]: I1003 22:28:21.433485  825540 cache.go:39] Caches are synced for APIServiceRegistrationController controller
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Oct 03 22:28:21 srv5 kube-apiserver[825540]: I1003 22:28:21.444773  825540 default_servicecidr_controller.go:227] inconsistent ServiceCIDR status, global configura
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tion: [10.0.0.0/24] local configuration: [10.43.0.0/16], configure the flags to match current ServiceCIDR or manually delete the default ServiceCIDR
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Oct 03 22:28:21 srv5 kube-apiserver[825540]: I1003 22:28:21.444968  825540 event.go:389] &amp;#34;Event occurred&amp;#34; object=&amp;#34;kubernetes&amp;#34; fieldPath=&amp;#34;&amp;#34; kind=&amp;#34;ServiceCIDR&amp;#34; apiVe
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rsion=&amp;#34;networking.k8s.io/v1&amp;#34; type=&amp;#34;Warning&amp;#34; reason=&amp;#34;KubernetesDefaultServiceCIDRInconsistent&amp;#34; message=&amp;#34;The default ServiceCIDR [10.0.0.0/24] does not match the fla
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">g configurations [10.43.0.0/16]&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Oct 03 22:28:21 srv5 kube-apiserver[825540]: E1003 22:28:21.535706  825540 repairip.go:353] &amp;#34;Unhandled Error&amp;#34; err=&amp;#34;the ClusterIP [IPv4]: 10.43.28.139 for Service c
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">attle-system/rancher-webhook is not within any service CIDR; please recreate&amp;#34; logger=&amp;#34;UnhandledError&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Oct 03 22:28:21 srv5 kube-apiserver[825540]: E1003 22:28:21.537293  825540 repairip.go:353] &amp;#34;Unhandled Error&amp;#34; err=&amp;#34;the ClusterIP [IPv4]: 10.43.192.190 for Service datastore/mysql-lb is not within any service CIDR; please recreate&amp;#34; logger=&amp;#34;UnhandledError&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Searching online found a few issues, but the solution wasn&amp;rsquo;t obvious:&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="https://github.com/kubernetes/kubernetes/issues/52695" target="_blank" rel="noopener"
>https://github.com/kubernetes/kubernetes/issues/52695&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/kubernetes/kubernetes/issues/133031" target="_blank" rel="noopener"
>https://github.com/kubernetes/kubernetes/issues/133031&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/kubernetes/kubernetes/issues/133031" target="_blank" rel="noopener"
>https://github.com/kubernetes/kubernetes/issues/133031&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>There was a hidden resource &lt;code>servicecidrs/kubernetes&lt;/code> that kubectl was unable to modify. The only way to fix it was actually to edit the backing store for Kubernetes, in etcd.&lt;/p>
&lt;p>Looking in etcd, I found the culprit. Note content is stored in binary.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">docker exec -ti etcd etcdctl get /registry/servicecidrs/kubernetes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/registry/servicecidrs/kubernetes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">....
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">10.0.0.0/24▒?
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">=
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ReadyTrue����*2 Kubernetes Service CIDR is ready▒&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>To fix it, run the following commands.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> -ti etcd etcdctl get /registry/servicecidrs/kubernetes &amp;gt; backup
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete service kubernetes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> -ti etcd etcdctl del /registry/servicecidrs/kubernetes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then restart kube-apiserver. It&amp;rsquo;ll auto auto recreate it and it&amp;rsquo;s fixed. However, don&amp;rsquo;t be like me and just configure your Nix flake correctly the first time.&lt;/p>
&lt;h1 id="pin-kubernetes">Pin Kubernetes&lt;/h1>
&lt;p>One of my problems with Nix is that when you use Nixpkgs, you almost always end up using the latest version of every package.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">services&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">kubernetes&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">apiserver&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">enable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Doing the above means one day you&amp;rsquo;re using Kubernetes 1.33.4, and &lt;a class="link" href="https://github.com/NixOS/nixpkgs/commit/c86ea2bf7cd348f2ca215d3a5b250911f893e0e7" target="_blank" rel="noopener"
>the next day&lt;/a> you&amp;rsquo;re on 1.34.0. Kubernetes upgrades aren&amp;rsquo;t safe to be done blindly. Nodes need to updated after the control plane and features get deprecated. I don&amp;rsquo;t mind automatically performing minor updates like 1.33.3 to 1.33.4, but major updates need to be opt-in.&lt;/p>
&lt;p>We can pin to a specific version like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="o">...&lt;/span> &lt;span class="p">}:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">let&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">package&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">kubernetes&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">overrideAttrs&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">oldAttrs&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="k">rec&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">version&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;1.34.2&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">src&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">fetchFromGitHub&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">owner&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;kubernetes&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">repo&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;kubernetes&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">rev&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;v&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="n">version&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sha256&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;3rQyoGt9zTeF8+PIhA5p+hHY1V5O8CawvKWscf/r9RM=&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">in&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">services&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">kubernetes&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">package&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">package&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This isn&amp;rsquo;t yet perfect because it doesn&amp;rsquo;t ensure that the api server is updated first across the cluster as per the &lt;a class="link" href="https://kubernetes.io/docs/tasks/administer-cluster/cluster-upgrade/" target="_blank" rel="noopener"
>official docs&lt;/a>, but it&amp;rsquo;s a first step.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>In this post, I walked through how to take a Kubernetes node running RKE1 and deploy Kubernetes via Nix+Nixpkgs instead of RKE1. In my next post, I&amp;rsquo;ll show to swap a Ubuntu server to NixOS using &lt;a class="link" href="https://github.com/nix-community/nixos-anywhere" target="_blank" rel="noopener"
>nixos-anywhere&lt;/a>&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2026%2F02%2Freplatforming-rke1-to-nix-based-k8s-part-1%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Replatforming+RKE1+to+Nix-based+K8s+-+Part+1" style="border:0" alt="" /></description></item><item><title>Syncing my Christmas lights to my Sonos</title><link>https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/</link><pubDate>Tue, 30 Dec 2025 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/</guid><summary>&lt;p>Last year, I setup a Christmas lights show at my house. I started with some basic light sequences just to learn. I &lt;a class="link" href="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/" >wrote a post&lt;/a> on the basics. This year, I upped the ante and added more lights and starting making sequences linked to music.&lt;/p>
&lt;p>I have one light controller running Falcon Controller/FPP, a &lt;a class="link" href="https://kulplights.com/product/k8-b/" target="_blank" rel="noopener"
>Kulp K8-B controller&lt;/a>. How do I get sound out? I looked at options for getting sound out to a speaker. For my first pass, I decided to push it to a &lt;a class="link" href="https://www.sonos.com/en-us/shop/move-2" target="_blank" rel="noopener"
>Sonos Move&lt;/a> speaker since I had one.&lt;/p></summary><description>&lt;p>Last year, I setup a Christmas lights show at my house. I started with some basic light sequences just to learn. I &lt;a class="link" href="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/" >wrote a post&lt;/a> on the basics. This year, I upped the ante and added more lights and starting making sequences linked to music.&lt;/p>
&lt;p>I have one light controller running Falcon Controller/FPP, a &lt;a class="link" href="https://kulplights.com/product/k8-b/" target="_blank" rel="noopener"
>Kulp K8-B controller&lt;/a>. How do I get sound out? I looked at options for getting sound out to a speaker. For my first pass, I decided to push it to a &lt;a class="link" href="https://www.sonos.com/en-us/shop/move-2" target="_blank" rel="noopener"
>Sonos Move&lt;/a> speaker since I had one.&lt;/p>
&lt;h1 id="how-do-i-connect-a-speaker">How do I connect a speaker?&lt;/h1>
&lt;p>My Kulp K8-B controller is a hat that sits on top of a &lt;a class="link" href="https://www.beagleboard.org/boards/beaglebone-black" target="_blank" rel="noopener"
>BeagleBoard Black&lt;/a>.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/kulp-k8b.png"
width="2560"
height="1920"
srcset="https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/kulp-k8b_hu_602b6d197b742e67.png 480w, https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/kulp-k8b_hu_e3bff251edc138f8.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="133"
data-flex-basis="320px"
>&lt;/p>
&lt;p>The BeagleBoard, like a Raspberry Pi, runs Linux. It has no 3.5mm audio output, but has USB and Ethernet.
&lt;img src="https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/beagleboard.png"
width="800"
height="800"
srcset="https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/beagleboard_hu_4cb20cb853268973.png 480w, https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/beagleboard_hu_31c3e0a5aa3a209e.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="100"
data-flex-basis="240px"
>
I could attach a USB-attached audio player like &lt;a class="link" href="https://www.wiredwatts.com/products/sbplay3" target="_blank" rel="noopener"
>this&lt;/a>, but then I&amp;rsquo;d have to drill a hole in my weather enclosure and not ruin that. Most people run them to speakers and/or FM transmitters for cars, but I wanted to be able to play it without being in a car. Instead, since I already several Sonos speakers including a &lt;a class="link" href="https://www.sonos.com/en-us/shop/move-2" target="_blank" rel="noopener"
>Move&lt;/a> that can be taken outside. The Sonos speakers inside the house could be synchronized to the lights I had inside for parties.&lt;/p>
&lt;h1 id="control-protocols">Control Protocols&lt;/h1>
&lt;p>I have two different systems, Sonos and FPP. Neither of them talk to each other, so I needed to first understand how they independently work.&lt;/p>
&lt;p>FPP has two different protocols that can be used:&lt;/p>
&lt;ol>
&lt;li>Pixel control data (e.g. DDP an E1.31) - Sends individual pixel data to a receiver. Pixel data has to be sent at 20hz - 40hz to match the sequence&lt;/li>
&lt;li>Timing data - FPP MultiSync- Sends timing and song names over the network and the player loads a locally saved sequence. Timing data is sent every regularly around every 100ms-1s and players internally speed up or slow down to match the ticks.&lt;/li>
&lt;/ol>
&lt;p>The FPP MultiSync protocol sounds like it would be best to integrate with since I can send period sync packets and the player handles the pixel data.&lt;/p>
&lt;h1 id="wireshark-dissector">Wireshark Dissector&lt;/h1>
&lt;p>The first thing I did was write a wireshark packet dissector to help me understand what the packets look like and construct my own packets. There&amp;rsquo;s no built-in dissector in Wireshark for FPP MultiSync or DDP, so packets look like an opaque blob of bytes:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/wireshark-packet-before.png"
width="1331"
height="949"
srcset="https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/wireshark-packet-before_hu_8753b18c94a85108.png 480w, https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/wireshark-packet-before_hu_efdd3c153b4855c0.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="140"
data-flex-basis="336px"
>&lt;/p>
&lt;p>The protocol is defined in &lt;a class="link" href="https://github.com/FalconChristmas/fpp/blob/master/docs/ControlProtocol.txt" target="_blank" rel="noopener"
>this spec&lt;/a>. Wireshark exposes a &lt;a class="link" href="https://www.wireshark.org/docs/wsdg_html_chunked/wslua_dissector_example.html" target="_blank" rel="noopener"
>Lua based Dissector API&lt;/a>. Using this, I wrote a simple program that looked at packets and extracted out the relevant bytes into structured Wireshark fields:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/wireshark-packet-after.png"
width="1331"
height="949"
srcset="https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/wireshark-packet-after_hu_a62beac10859e6e.png 480w, https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/wireshark-packet-after_hu_d9264dc1ccd9b2ce.png 1024w"
loading="lazy"
alt="A screenshot from Wireshark’s packet dissector view showing meaningful data in the FPP MultiSync payload."
class="gallery-image"
data-flex-grow="140"
data-flex-basis="336px"
>&lt;/p>
&lt;p>The code can be found &lt;a class="link" href="https://github.com/ajacques/wireshark-dissectors/blob/master/fpp.lua" target="_blank" rel="noopener"
>here&lt;/a>. To load it into Wireshark, go to Help → About → Folders. Click on Personal Lua Plugins and paste the files into that folder.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/wireshark-about-folders.png"
width="928"
height="851"
srcset="https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/wireshark-about-folders_hu_1202a7877ce06b22.png 480w, https://www.technowizardry.net/2025/12/syncing-sonos-to-falcon-player-fpp/wireshark-about-folders_hu_65a71802df59fca1.png 1024w"
loading="lazy"
alt="A screenshot from Wireshark’s About window showing the location of the folder where the script needs to be saved."
class="gallery-image"
data-flex-grow="109"
data-flex-basis="261px"
>&lt;/p>
&lt;h1 id="the-control-script">The Control Script&lt;/h1>
&lt;p>I can either listen to another player send them or try to send them myself. If I listen to the packets, then I&amp;rsquo;d have to speed up or slow down the Sonos player. Given &lt;a class="link" href="https://falconchristmas.com/forum/index.php?topic=5012.0" target="_blank" rel="noopener"
>this forum post&lt;/a> on how the players internally work and the fact that it&amp;rsquo;s easier for our ears to hear discontinuities in the audio, I let the speakers play, then send FPP MultiSync packets every second to a multicast address that all players subscribe to.&lt;/p>
&lt;p>The source code can be found here: &lt;a class="link" href="https://github.com/ajacques/sonos-fpp-sync" target="_blank" rel="noopener"
>https://github.com/ajacques/sonos-fpp-sync&lt;/a>&lt;/p>
&lt;p>A high-level explanation on how it works:
Upon startup, it loads a playlist of songs and fseq files (generated by xLights), then loads the songs into the Sonos queue. Songs are served by a local HTTP web server running from the script. I considered streaming from a Jellyfin server, but I couldn&amp;rsquo;t solve some network access issues, and get Sonos to stream with the right codec and access tokens.&lt;/p>
&lt;p>Once the songs are loaded, it subscribes to media events from Sonos using &lt;a class="link" href="https://python-soco.com/" target="_blank" rel="noopener"
>python-soco&lt;/a> running using &lt;a class="link" href="https://twisted.org/" target="_blank" rel="noopener"
>Python Twisted&lt;/a>. Twisted is an event-based networking engine for Python that handles the async networking required for Sonos subscriptions, responding to HTTP requests to stream media, and sending datagrams.&lt;/p>
&lt;p>When the Sonos speaker starts playing a song, it sends a message to my script saying what song is playing. I then tell the FPP players to load the sequence containing the pixel data and prepare to start playing. I then start a timer that runs every second that asks Sonos how many seconds have elapsed in the song. I use that to derive the frame counter (# of seconds elapsed / # of seconds in song * number of frames in fseq file) and send a sync packet to the FPP players.&lt;/p>
&lt;p>There is a problem here. Sonos gives me elapsed time with a resolution of 1 second, combined with the round-trip time of ~50ms, that means the pixels can be off beat. I&amp;rsquo;ve noticed this when playing some sequences. I don&amp;rsquo;t have a fix for this yet, but had the idea to add some jitter to tick timer and average out the calculated start times. That&amp;rsquo;s a problem for next year.&lt;/p>
&lt;p>I&amp;rsquo;d put a video, but I don&amp;rsquo;t want a DMCA notice.&lt;/p>
&lt;p>Source Code: &lt;a class="link" href="https://github.com/ajacques/sonos-fpp-sync" target="_blank" rel="noopener"
>https://github.com/ajacques/sonos-fpp-sync&lt;/a>&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F12%2Fsyncing-sonos-to-falcon-player-fpp%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Syncing+my+Christmas+lights+to+my+Sonos" style="border:0" alt="" /></description></item><item><title>More infrastructure doesn't fix using the wrong infrastructure</title><link>https://www.technowizardry.net/2025/12/lambda-fails-at-queue-management/</link><pubDate>Tue, 16 Dec 2025 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2025/12/lambda-fails-at-queue-management/</guid><summary>&lt;p>This is a continuation of my &lt;a class="link" href="https://www.technowizardry.net/2024/09/more-infra-doesnt-fix-using-the-wrong-infra" >previous post&lt;/a> where I talked about the challenges of using serverless/Function as a Service (FaaS) compute systems for ETL (Extract, Transform, Load) jobs. It sat in my drafts folder for a long time, so I just decided to publish it as is.&lt;/p>
&lt;p>I used to work at AWS, and predictably we used a lot of AWS cloud services. In many cases, when an engineer looks for a service platform, they&amp;rsquo;ll often go directly to AWS Lambda because &amp;ldquo;it&amp;rsquo;s Serverless&amp;rdquo; with the justification that it&amp;rsquo;s simple and alternatives are too complex and not worth considering.&lt;/p></summary><description>&lt;p>This is a continuation of my &lt;a class="link" href="https://www.technowizardry.net/2024/09/more-infra-doesnt-fix-using-the-wrong-infra" >previous post&lt;/a> where I talked about the challenges of using serverless/Function as a Service (FaaS) compute systems for ETL (Extract, Transform, Load) jobs. It sat in my drafts folder for a long time, so I just decided to publish it as is.&lt;/p>
&lt;p>I used to work at AWS, and predictably we used a lot of AWS cloud services. In many cases, when an engineer looks for a service platform, they&amp;rsquo;ll often go directly to AWS Lambda because &amp;ldquo;it&amp;rsquo;s Serverless&amp;rdquo; with the justification that it&amp;rsquo;s simple and alternatives are too complex and not worth considering.&lt;/p>
&lt;p>In this post, I introduce another &amp;ldquo;heavily inspired by true events&amp;rdquo; problem space in which Serverless was the wrong choice. A queue processing system in AWS Lambda seems like a class example of using Serverless. Let&amp;rsquo;s walk through the problem.&lt;/p>
&lt;!-- more -->
&lt;h1 id="queue-processing-lambda">Queue Processing Lambda&lt;/h1>
&lt;p>A pattern I saw was using AWS Lambda as a queue processing feeding into another system with a limited throughput. The target service(s) had different constraints, such as a maximum number of tps (transactions per second) or a maximum number of in-flight operations before it would reject requests.&lt;/p>
&lt;p>&lt;em>Diagram Here - Maybe&lt;/em>&lt;/p>
&lt;p>In a naïve implementation (I saw this in several systems), SQS would feed into the Lambda function, which then would call the service. If it throttles, retry until it succeeds.&lt;/p>
&lt;p>Do you see any problems with this approach? Hopefully a few bells will be going off.&lt;/p>
&lt;ul>
&lt;li>If you get a throttle, how many times do you retry? How quickly should you retry?&lt;/li>
&lt;li>What happens if the number of retries exceeds the Lambda function timeout and it gets cut off in the middle of processing?&lt;/li>
&lt;li>Is the batch size big or small? If it&amp;rsquo;s too big, will it exceed the short-term throttling window?&lt;/li>
&lt;li>What happens if Lambda starts scaling out horizontally because you have more inbound messages or because of the retry delays are starting to take more time so it thinks it needs more functions?&lt;/li>
&lt;li>What happens if the throttle period means you can&amp;rsquo;t do any work for several minutes? Lambda will continue to deliver messages into your function even if you know you can&amp;rsquo;t do anything causing failed deliveries and presumably you have a DLQ configured (you do don&amp;rsquo;t you?)&lt;/li>
&lt;li>Is the process idempotent?&lt;/li>
&lt;/ul>
&lt;p>Let&amp;rsquo;s take an example service with a limit of 5 tps, each request kicks off a process that runs for 5-20 minutes, and there&amp;rsquo;s a maximum number of 10 workflows allowed for this client. If you didn&amp;rsquo;t think about this and used the defaults, then you&amp;rsquo;ll get 10 messages in that batch. Then pass those to the API sequentially and the API responds in &amp;lt;100ms, then on the 6th request, get throttled. Now depending on the periodicity of the throttling algorithm, it could clear up immediately or take a few seconds. Not terrible.&lt;/p>
&lt;p>Let&amp;rsquo;s make it worse. Say you&amp;rsquo;ve got huge queue of messages. AWS Lambda automatically scales out your number of invocations from 1 to 10 concurrent invocations. Now, you&amp;rsquo;re trying to do 10 x (10 / 100ms) = 50 requests per second with 90% of them getting throttled.&lt;/p>
&lt;h1 id="reduce-concurrency">Reduce concurrency&lt;/h1>
&lt;p>Is this fixable without moving from AWS Lambda? What knobs do we have? Well, we could &lt;a class="link" href="https://aws.amazon.com/about-aws/whats-new/2017/11/set-concurrency-limits-on-individual-aws-lambda-functions/" target="_blank" rel="noopener"
>limit the number of horizontal function invocations&lt;/a> to 1 which would constrain the maximum number of tps from:&lt;/p>
&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
&lt;semantics>
&lt;mrow>
&lt;mi>5 (event source mapping batch)&lt;/mi>
&lt;mo>&amp;times;&lt;/mo>
&lt;mi>1000&lt;/mi>
&lt;mspace width="3px" />
&lt;mtext>invocations&lt;/mtext>
&lt;mo>&amp;times;&lt;/mo>
&lt;mfrac>
&lt;mrow>
&lt;mi>1&lt;/mi>
&lt;mtext>second&lt;/mtext>
&lt;/mrow>
&lt;mrow>
&lt;mi>100ms&lt;/mi>
&lt;mspace width="3px" />
&lt;mtext>average latency&lt;/mtext>
&lt;/mrow>
&lt;/mfrac>
&lt;mo>&amp;equals;&lt;/mo>
&lt;mi>50,000 tps&lt;/mi>
&lt;/mrow>
&lt;/semantics>
&lt;/math>
&lt;p>to:&lt;/p>
&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
&lt;semantics>
&lt;mrow>
&lt;mrow>1 -
&lt;mi>5 (event source mapping batch)&lt;/mi>
&lt;mo>&amp;times;&lt;/mo>
&lt;mi>1&lt;/mi>
&lt;mtext> invocations&lt;/mtext>
&lt;/mrow>
&lt;mo>&amp;times;&lt;/mo>
&lt;mfrac>
&lt;mrow>
&lt;mi>1&lt;/mi>
&lt;mtext>second&lt;/mtext>
&lt;/mrow>
&lt;mrow>
&lt;mi>100ms&lt;/mi>
&lt;mspace width="3px" />
&lt;mtext>average latency&lt;/mtext>
&lt;/mrow>
&lt;/mfrac>
&lt;mo>&amp;equals;&lt;/mo>
&lt;mi>50&lt;/mi>
&lt;mspace width="3px" />
&lt;mtext>tps&lt;/mtext>
&lt;/mrow>
&lt;/semantics>
&lt;/math>
&lt;p>That reduces the worst case quite a bit, but it still exceeds my hypothetical example.&lt;/p>
&lt;h2 id="more-problems-with-throttling">More problems with throttling&lt;/h2>
&lt;p>That keeps it under the TPS limit, but there&amp;rsquo;s still a way it can fail. The service could start load-shedding non-important traffic. In this example, the service also enforces a maximum number of workflows that be on-going or maybe per day. Even if you stayed at or below the 5 TPS limit, you could still get throttled.&lt;/p>
&lt;blockquote>
&lt;p>Q: Why don&amp;rsquo;t you just scale the other service up? 5 tps seems awfully low.&lt;/p>&lt;/blockquote>
&lt;blockquote>
&lt;p>A: There are numerous reasons why it might not be possible to scale up another service. For example, it could be a third-party organization, it could be shedding low-priority traffic during a peak, or it could have hard limitations like dealing with physical equipment. Imagine a shipping system that can ship a lot of small boxes, but can only fit a few pallet-sized items in a truck before you have to wait for a new truck.&lt;/p>&lt;/blockquote>
&lt;p>If you continued to be throttled, you have to slow down and retry until it succeeds, but the clock is ticking on that Lambda function timeout. If the downstream service is saying no more traffic for 15+ minutes (yes this could really happen), then you have three options:&lt;/p>
&lt;ol>
&lt;li>Fail the batch entirely, return immediately to SQS&lt;/li>
&lt;li>Keep trying periodically and see if any of the requests get through&lt;/li>
&lt;li>Just sleep for the entire 15 minute interval&lt;/li>
&lt;/ol>
&lt;p>If you chose #1, you have no back-off and are going to immediately hit the service again, and those messages now are closer towards going into the DLQ (even though there&amp;rsquo;s nothing wrong with &lt;em>those&lt;/em> messages). We&amp;rsquo;re going to immediate fetch new messages that are not going to succeed and penalize those messages with failed delivery attempts. Eventually enough times through this, it&amp;rsquo;ll exceed the max delivery attempts and go into a DLQ.&lt;/p>
&lt;p>If you chose #2, it&amp;rsquo;s getting closer. One or two messages could succeed, but make sure you keep track of the time and return the ACK messages before exiting so Lambda can consider some of them done. However, make sure your message window time window is &amp;gt;15 minutes or you keep the pending messages alive with &lt;a class="link" href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ChangeMessageVisibility.html" target="_blank" rel="noopener"
>sqs:ChangeMessageVisibility&lt;/a>, so no other invocations can grab it (Technically we&amp;rsquo;re single-threaded, but if the handle expires, you can&amp;rsquo;t ACK it.)&lt;/p>
&lt;p>Options #2 and #3 are similar. If you know for certain no new traffic will work, option #3 reduces useless traffic downstream to the service.&lt;/p>
&lt;p>The next problem is the fact that messages have a finite number of delivery attempts before they go to a DLQ combined with this 15 minute time limit on Lambda functions. Throttling is a global problem and applies to all messages. We don&amp;rsquo;t want to penalize an individual message just because the entire system is blocked.&lt;/p>
&lt;p>So clearly there are cases where a Serverless FaaS compute environment will cause an algorithm to perform poorly. The lack of ability to control when a function exits and when a new process starts up will cause a cascade of failures where you can&amp;rsquo;t process backlog of requests. The fundamental problem is that the system (Lambda&amp;rsquo;s control plane) can&amp;rsquo;t react to slow down pulling messages from the queue and giving them to your function. It&amp;rsquo;s completely unaware of the inner-workings of your system and treats any error the same.&lt;/p>
&lt;h1 id="taking-control-of-the-process-life-cycle">Taking control of the process life-cycle&lt;/h1>
&lt;p>To break free from this problem, we have to take control of the entire control loop and decide when to fetch messages, how many messages to fetch, and how to react to errors. Not to worry, this is easier than it sounds.&lt;/p>
&lt;p>The process starts to look something like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">main&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">[]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">numMessagesInBatch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">5&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">while&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Message&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">messages&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">amazonSQSClient&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">receiveMessages&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">numMessagesInBatch&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">for&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">messages&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Process each message&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ThrottleException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Ack any successful messages&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Sleep and optionally reduce numMessagesInBatch&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">RuntimeException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Handle other errors as appropriate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>Picking the simple solution or using a technology that you&amp;rsquo;re already familiar with can sometimes be the pragmatic choice.&lt;/p>
&lt;p>Picking the right tool for the job is important and sometimes what appears to be a simple solution actually ends up being more complicated when you factor in all failure modes. Serverless is great for a class of problems. If you have a service API that&amp;rsquo;s infrequently called, the above problems don&amp;rsquo;t appear because failures apply back-pressure to the caller to slow down and retry. If you&amp;rsquo;re processing a queue and you&amp;rsquo;re likely to cause throttling while processing, Lambda and FaaS-based systems can fall-over. Don&amp;rsquo;t forget to leverage big data compute specialized systems, like Glue, EMR, and even Batch if you have that situation.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F12%2Flambda-fails-at-queue-management%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=More+infrastructure+doesn%27t+fix+using+the+wrong+infrastructure" style="border:0" alt="" /></description></item><item><title>Blogging on the Fediverse with ActivityPub</title><link>https://www.technowizardry.net/2025/12/hugo-support-for-activitypub/</link><pubDate>Mon, 15 Dec 2025 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2025/12/hugo-support-for-activitypub/</guid><summary>&lt;p>Previously, if you wanted to subscribe to changes from this blog, you&amp;rsquo;d have to subscribe to the &lt;a class="link" href="https://www.technowizardry.net/feed.xml" >RSS feed&lt;/a>, but as of today you can also subscribe to it in your preferred Fediverse client, like &lt;a class="link" href="https://joinmastodon.org/" target="_blank" rel="noopener"
>Mastodon&lt;/a>. Note this is considered Beta quality. If you have any issues, let me know.&lt;/p>
&lt;p>What is the &lt;a class="link" href="https://www.fediverse.to/" target="_blank" rel="noopener"
>Fediverse&lt;/a>? It&amp;rsquo;s a protocol for federated (meaning many independently operated) social networks, kind of like email. Under the hood, it uses a protocol called &lt;a class="link" href="https://activitypub.rocks/" target="_blank" rel="noopener"
>ActivityPub&lt;/a> to define the interactions between different servers.&lt;/p>
&lt;p>There&amp;rsquo;s a number of big implementations of this, like Mastodon, that I could have used. However, I wanted to see if it was possible to integrate directly into my static website generator, &lt;a class="link" href="https://gohugo.io/" target="_blank" rel="noopener"
>Hugo&lt;/a> and generate all the content directly out of the posts I already write without having to maintain another program and expose another domain name for people to remember (e.g. &lt;a class="link" href="mailto:blog@mastodon.technowizardry.net" >blog@mastodon.technowizardry.net&lt;/a>.)&lt;/p>
&lt;p>This post walks through the work I did to make this work.&lt;/p></summary><description>&lt;p>Previously, if you wanted to subscribe to changes from this blog, you&amp;rsquo;d have to subscribe to the &lt;a class="link" href="https://www.technowizardry.net/feed.xml" >RSS feed&lt;/a>, but as of today you can also subscribe to it in your preferred Fediverse client, like &lt;a class="link" href="https://joinmastodon.org/" target="_blank" rel="noopener"
>Mastodon&lt;/a>. Note this is considered Beta quality. If you have any issues, let me know.&lt;/p>
&lt;p>What is the &lt;a class="link" href="https://www.fediverse.to/" target="_blank" rel="noopener"
>Fediverse&lt;/a>? It&amp;rsquo;s a protocol for federated (meaning many independently operated) social networks, kind of like email. Under the hood, it uses a protocol called &lt;a class="link" href="https://activitypub.rocks/" target="_blank" rel="noopener"
>ActivityPub&lt;/a> to define the interactions between different servers.&lt;/p>
&lt;p>There&amp;rsquo;s a number of big implementations of this, like Mastodon, that I could have used. However, I wanted to see if it was possible to integrate directly into my static website generator, &lt;a class="link" href="https://gohugo.io/" target="_blank" rel="noopener"
>Hugo&lt;/a> and generate all the content directly out of the posts I already write without having to maintain another program and expose another domain name for people to remember (e.g. &lt;a class="link" href="mailto:blog@mastodon.technowizardry.net" >blog@mastodon.technowizardry.net&lt;/a>.)&lt;/p>
&lt;p>This post walks through the work I did to make this work.&lt;/p>
&lt;h1 id="what-is-activitypub">What is ActivityPub?&lt;/h1>
&lt;p>ActivityPub is an open standard protocol designed to enable decentralized social networking across different platforms and services. Published by the W3C organization, the same org that publishes the HTML standard, it allows users to maintain their online identities and connections while moving between different websites, apps, and services without losing access to their content or network.&lt;/p>
&lt;p>By defining an interoperable protocol, different software, like Mastodon, Lemmy, Pleroma can all subscribe and publish notes to different instances and software. The federation part means that you can subscribe to notes on a different server. My goal is to have readers be able to subscribe to posts in Mastodon and eventually be able to like and even comment on them.&lt;/p>
&lt;h1 id="the-webfinger">The WebFinger&lt;/h1>
&lt;p>The first thing that happens when you follow somebody else is your software issues a &amp;ldquo;webfinger&amp;rdquo; request (&lt;a class="link" href="https://webfinger.net/spec/" target="_blank" rel="noopener"
>spec&lt;/a>). The client will &lt;code>GET https://www.technowizardry.net/.well-known/webfinger?resource=acct:blog@technowizardry.net&lt;/code>. The response tells clients that I do support ActivityPub and the response looks like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;subject&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;acct:blog@technowizardry.net&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;aliases&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;https://www.technowizardry.net/&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;https://www.technowizardry.net/author/adam&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;links&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;rel&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;self&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;application/activity+json&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;href&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://www.technowizardry.net/author/adam&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>To generate this with Hugo, create a file &lt;code>layouts/index.webfinger.ajson&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;subject&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;acct:blog@{{ strings.TrimRight &amp;#34;&lt;/span>&lt;span class="err">/&lt;/span>&lt;span class="s2">&amp;#34; (replace $.Site.BaseURL &amp;#34;&lt;/span>&lt;span class="err">https&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="c1">//&amp;#34; &amp;#34;&amp;#34;) }}&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;aliases&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;{{ $.Site.BaseURL}}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;{{ $.Site.BaseURL }}author/adam&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;links&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;rel&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;self&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;application/activity+json&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;href&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;{{ $.Site.BaseURL }}author/adam&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The subject needs to match the value passed by the client in the &lt;code>/webfinger?resource=acct:blog@technowizardry.net&lt;/code>. Since I only support a single account, I can hard-code the value. The &lt;code>links&lt;/code> array will tell clients where to find my user details. This is critical for ActivityPub.&lt;/p>
&lt;p>Next we need to tell Hugo to generate this file (set in &lt;code>config.yaml&lt;/code>). Note that I&amp;rsquo;m going to generate the files with an &lt;code>.ajson&lt;/code> extension to distinguish between this and the HTML outputs. More on that later.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">mediaTypes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># The template file will have the extension .ajson&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">application/jrd+json&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">suffixes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">ajson&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">outputFormats&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">WEBFINGER&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mediaType&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">application/jrd+json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">notAlternative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Hugo will output to public/.well-known/webfinger/index.ajson&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">.well-known/webfinger&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">outputs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">home&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WEBFINGER&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="generating-the-outbox">Generating the outbox&lt;/h1>
&lt;p>The ActivityPub outbox contains a historical listing of ActivityPub events (my blog posts) that other servers can download to catch-up on old events. Unfortunately, it doesn&amp;rsquo;t seem to be well used. For example, Mastodon &lt;a class="link" href="https://github.com/mastodon/mastodon/issues/17213" target="_blank" rel="noopener"
>does not pull old posts&lt;/a>.&lt;/p>
&lt;p>I&amp;rsquo;m going to generate it anyway just to be safe in-case there is a server that does support it. The template iterates through every published post and emits a single JSON object for every post, much like the RSS feed is generated.&lt;/p>
&lt;p>Note the way that I serialize the &lt;code>content&lt;/code> and &lt;code>summary&lt;/code> fields. While researching this post and implementing it, I found several other implementations that incorrectly serialized the objects where HTML entities would be included in the post or they would even generate invalid JSON objects. For more information, see my other post on &lt;a class="link" href="https://www.technowizardry.net/2025/03/fixing-common-hugo-encoding-problems/" >fixing common Hugo encoding problems&lt;/a>.&lt;/p>
&lt;p>layouts/index.activitypub_outbox.json:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">{{- $pctx := . -}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{- $pages := slice -}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{- if or $.IsHome $.IsSection -}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{- $pages = .Site.RegularPages -}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{- else -}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{- $pages = $pctx.Pages -}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{- end -}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{- $pages := where $pages &amp;#34;Params.hidden&amp;#34; &amp;#34;!=&amp;#34; true -}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{- $limit := .Site.Config.Services.RSS.Limit -}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{- if ge $limit 1 -}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{- $pages = $pages | first $limit -}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{- end -}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;@context&amp;#34;: &amp;#34;https://www.w3.org/ns/activitystreams&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;id&amp;#34;: &amp;#34;{{ $.Site.BaseURL }}activitypub/outbox&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;summary&amp;#34;: &amp;#34;{{ $.Site.Title }}&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;type&amp;#34;: &amp;#34;OrderedCollection&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{- $notdrafts := where $pages &amp;#34;.Draft&amp;#34; &amp;#34;!=&amp;#34; true }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{- $all := where $notdrafts &amp;#34;Type&amp;#34; &amp;#34;in&amp;#34; (slice &amp;#34;posts&amp;#34;)}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;totalItems&amp;#34;: {{ len $all }},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;orderedItems&amp;#34;: [
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{- range $index, $element := $all }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{- if ne $index 0 }}, {{ end }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;@context&amp;#34;: &amp;#34;https://www.w3.org/ns/activitystreams&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;id&amp;#34;: &amp;#34;{{.Permalink}}-create&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;type&amp;#34;: &amp;#34;Create&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;actor&amp;#34;: &amp;#34;{{ $.Site.BaseURL }}author/adam&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;object&amp;#34;: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;id&amp;#34;: &amp;#34;{{ .Permalink }}&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;type&amp;#34;: &amp;#34;Article&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;content&amp;#34;: {{ .Content | htmlUnescape | jsonify (dict &amp;#34;noHTMLEscape&amp;#34; true) }},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;url&amp;#34;: {{ .Permalink | jsonify }},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;summary&amp;#34;: {{ printf &amp;#34;%s&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;%s&amp;#34; .Title .Summary | jsonify }},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;attributedTo&amp;#34;: &amp;#34;{{ $.Site.BaseURL }}author/adam&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;to&amp;#34;: &amp;#34;https://www.w3.org/ns/activitystreams#Public&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;published&amp;#34;: {{ dateFormat &amp;#34;2006-01-02T15:04:05-07:00&amp;#34; .Date | jsonify }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{- end }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Time to update the &lt;code>config.yaml&lt;/code> again:&lt;/p>
&lt;div class="highlight" hl_search="">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">mediaTypes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">application/activity+json&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">suffixes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">ajson&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">outputFormats&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ACTIVITY_OUTBOX&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mediaType&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">application/activity+json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">notAlternative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">baseName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">outbox&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">outputs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">home&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">HTML&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">ACTIVITY_OUTBOX&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="generate-per-post-files">Generate per-post files&lt;/h1>
&lt;p>Even though Mastodon doesn&amp;rsquo;t download old posts automatically, it can still open any post. By pasting the URL, and clicking &amp;ldquo;Open URL in Mastodon&amp;rdquo;, Mastodon will issue a &lt;code>GET&lt;/code> request to that URL with the header &lt;code>Accept: application/activity+json&lt;/code> expecting to download the post in ActivityPub JSON format.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/12/hugo-support-for-activitypub/images/mastodon-single-url.png"
width="364"
height="374"
srcset="https://www.technowizardry.net/2025/12/hugo-support-for-activitypub/images/mastodon-single-url_hu_66850e952e13d40.png 480w, https://www.technowizardry.net/2025/12/hugo-support-for-activitypub/images/mastodon-single-url_hu_9fce6cd6fd105ae9.png 1024w"
loading="lazy"
alt="A screenshot from Mastodon. The user is searching for a post url and is presented with Open URL or Profiles matching. Open URL is highlighted"
class="gallery-image"
data-flex-grow="97"
data-flex-basis="233px"
>&lt;/p>
&lt;p>Right now, it&amp;rsquo;s passing back as HTML. Let&amp;rsquo;s generate something per post. Again, update the &lt;code>config.yaml&lt;/code> to generate this new file.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="hl">&lt;span class="lnt"> 2
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt"> 3
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt"> 4
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt"> 5
&lt;/span>&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">outputFormats&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ACTIVITY_USER&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mediaType&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">application/activity+jsonindex&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">notAlternative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">baseName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">activitypub&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">POST_JSON&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mediaType&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">application/activity+json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">notAlternative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">outputs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">page&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">HTML&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">POST_JSON&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Let&amp;rsquo;s refactor this and the outbox and create a new partial that is shared between the outbox and post. The partial has to have the extension &lt;code>.html&lt;/code> because that&amp;rsquo;s &lt;a class="link" href="https://gohugo.io/templates/partial/#use-partials-in-your-templates" target="_blank" rel="noopener"
>how Hugo works&lt;/a>.&lt;/p>
&lt;p>&lt;code>layouts/partials/post_main_blob.html&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-template" data-lang="go-template">&lt;span class="line">&lt;span class="cl">&lt;span class="x">&amp;#34;id&amp;#34;: &amp;#34;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Permalink&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x">&amp;#34;type&amp;#34;: &amp;#34;Article&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x">&amp;#34;content&amp;#34;: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Content&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">htmlUnescape&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nx">dict&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;noHTMLEscape&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">true&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x">&amp;#34;url&amp;#34;: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Permalink&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x">&amp;#34;summary&amp;#34;: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">printf&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;%s&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;%s&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Summary&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x">&amp;#34;attributedTo&amp;#34;: &amp;#34;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">$.Site.BaseURL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">author/adam&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x">&amp;#34;to&amp;#34;: &amp;#34;https://www.w3.org/ns/activitystreams#Public&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x">&amp;#34;published&amp;#34;: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">dateFormat&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;2006-01-02T15:04:05-07:00&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Date&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>layouts/posts/single.post_json.ajson&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-template" data-lang="go-template">&lt;span class="line">&lt;span class="cl">&lt;span class="x">{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;@context&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;https://www.w3.org/ns/activitystreams&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;ostatus&amp;#34;: &amp;#34;http://ostatus.org#&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;conversation&amp;#34;: &amp;#34;ostatus:conversation&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;sensitive&amp;#34;: &amp;#34;as:sensitive&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">partial&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;post_main_blob&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;cc&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">$.Site.BaseURL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">author/adam/followers&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;sensitive&amp;#34;: false,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;attachment&amp;#34;: [],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;tag&amp;#34;: [],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;replies&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;id&amp;#34;: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">printf&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;%sreplies&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Permalink&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;type&amp;#34;: &amp;#34;Collection&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;first&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;type&amp;#34;: &amp;#34;CollectionPage&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;next&amp;#34;: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">printf&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;%sreplies-page&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Permalink&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;partOf&amp;#34;: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">printf&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;%sreplies&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Permalink&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;items&amp;#34;: []
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x">}
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="making-the-accept-header-work">Making the Accept header work&lt;/h1>
&lt;p>This is the part where your environment may look different than mine and may differ. For example, if you&amp;rsquo;re running on Vercel, then &lt;a class="link" href="https://paul.kinlan.me/adding-activity-pub-to-your-static-site/" target="_blank" rel="noopener"
>this approach&lt;/a> would be better. If you were using Azure Websites, then &lt;a class="link" href="https://maho.dev/2024/02/a-guide-to-implementing-activitypub-in-a-static-site-or-any-website-part-3/" target="_blank" rel="noopener"
>this&lt;/a> also works. Both of these generate static files that include references to small functions as a service to handle the inbox and followers lists. These can be hosted on any cloud provider.&lt;/p>
&lt;p>Prior to this project, I used pure Hugo to generate static content and Mastodon was able to work with this to load posts. I built everything using a Hugo Docker image and packaged the content into an NGINX Docker container that was run on my Kubernetes cluster. However, this is not enough to implement ActivityPub.&lt;/p>
&lt;p>First, we need to check the Accept header to see if the client is requesting HTML or if it&amp;rsquo;s requesting an ActivityPub JSON blob of the item.&lt;/p>
&lt;h2 id="attempt-1---using-nginx">Attempt 1 - Using NGINX&lt;/h2>
&lt;p>My first attempt was to implement this using NGINX&amp;rsquo;s configuration and it looked like the below. The following implemented a conditional based on the &lt;code>Accept&lt;/code> header and returned the &lt;code>index.ajson&lt;/code> file if the client passed in the special MIME type or returns &lt;code>index.html&lt;/code> in any other case.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nginx" data-lang="nginx">&lt;span class="line">&lt;span class="cl">&lt;span class="k">map&lt;/span> &lt;span class="nv">$http_accept&lt;/span> &lt;span class="nv">$ap_suffix&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">default&lt;/span> &lt;span class="s">&amp;#34;/index.html&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">&amp;#34;~*application\/activity\+json&amp;#34;&lt;/span> &lt;span class="s">&amp;#34;/index.ajson&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">server&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">listen&lt;/span> &lt;span class="mi">80&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">server_name&lt;/span> &lt;span class="s">localhost&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">root&lt;/span> &lt;span class="s">/usr/share/nginx/html&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">types&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">application/activity+json&lt;/span> &lt;span class="s">ajson&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">text/html&lt;/span> &lt;span class="s">html&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">text/css&lt;/span> &lt;span class="s">css&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">video/mp4&lt;/span> &lt;span class="s">mp4&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">image/png&lt;/span> &lt;span class="s">png&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">location&lt;/span> &lt;span class="s">/&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">try_files&lt;/span> &lt;span class="nv">$uri&lt;/span> &lt;span class="nv">$uri$ap_suffix&lt;/span> &lt;span class="p">=&lt;/span>&lt;span class="mi">404&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">location&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">/.well-known/webfinger&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">default_type&lt;/span> &lt;span class="s">application/jrd+json&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">try_files&lt;/span> &lt;span class="nv">$uri/index.ajson&lt;/span> &lt;span class="p">=&lt;/span>&lt;span class="mi">404&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">location&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">/activitypub/outbox&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">try_files&lt;/span> &lt;span class="nv">$uri$ap_suffix&lt;/span> &lt;span class="p">=&lt;/span>&lt;span class="mi">404&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">location&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">/activitypub/following&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># $ap_suffix is either &amp;#39;.html&amp;#39; or &amp;#39;.ajson&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kn">try_files&lt;/span> &lt;span class="nv">$uri$ap_suffix&lt;/span> &lt;span class="p">=&lt;/span>&lt;span class="mi">404&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This worked okay, but as I continued to implement this, I started to have issues. For example, when developing I had circular dependencies, and logic to implement request handling was getting split up into different Docker containers.&lt;/p>
&lt;p>Next part of this series, I&amp;rsquo;ll show how I approached this and implemented a server-side follower store.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>As you can see, this is one of the lengthier posts, for a seemingly &amp;ldquo;easy&amp;rdquo; function. This goes to show you that the simplist of tasks can take the most amount of time. But don&amp;rsquo;t worry, all you have to do as a user is to &amp;ldquo;like&amp;rdquo; and &amp;ldquo;subscribe&amp;rdquo; and donate to my coffee fund for more information like this.&lt;/p>
&lt;p>To implement ActivityPub, we have to generate the webfinger file to allow clients to know where to go, an ActivityPub user page, an outbox to download all posts, and individual post files.&lt;/p>
&lt;h1 id="references">References&lt;/h1>
&lt;ul>
&lt;li>&lt;a class="link" href="https://maho.dev/2024/02/a-guide-to-implement-activitypub-in-a-static-site-or-any-website/" target="_blank" rel="noopener"
>https://maho.dev/2024/02/a-guide-to-implement-activitypub-in-a-static-site-or-any-website/&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://paul.kinlan.me/adding-activity-pub-to-your-static-site/" target="_blank" rel="noopener"
>https://paul.kinlan.me/adding-activity-pub-to-your-static-site/&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/mastodon/mastodon/issues/34" target="_blank" rel="noopener"
>https://github.com/mastodon/mastodon/issues/34&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/mastodon/mastodon/issues/21770" target="_blank" rel="noopener"
>https://github.com/mastodon/mastodon/issues/21770&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.w3.org/TR/activitypub/#collections" target="_blank" rel="noopener"
>https://www.w3.org/TR/activitypub/#collections&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://webfinger.net/spec/" target="_blank" rel="noopener"
>https://webfinger.net/spec/&lt;/a>&lt;/li>
&lt;/ul>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F12%2Fhugo-support-for-activitypub%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Blogging+on+the+Fediverse+with+ActivityPub" style="border:0" alt="" /></description></item><item><title>I like the idea of Nix, but don't enjoy using it</title><link>https://www.technowizardry.net/2025/11/i-like-the-idea-of-nix-but-dont-enjoy-using-it/</link><pubDate>Fri, 28 Nov 2025 10:54:00 -0800</pubDate><guid>https://www.technowizardry.net/2025/11/i-like-the-idea-of-nix-but-dont-enjoy-using-it/</guid><summary>&lt;p>I&amp;rsquo;ve been playing with Nix and NixOS a lot more lately. I installed NixOS on one of &lt;a class="link" href="https://www.technowizardry.net/2024/09/adopting-nixos-for-my-rke1-kubernetes-nodes/" >my servers&lt;/a>, I installed the Nix CLI on my laptop, I tried to use Nix to build a Docker image, I use Nix flakes.&lt;/p>
&lt;p>This post was written from the perspective of a person new to Nix, but experienced with other computer languages. Thus, it&amp;rsquo;s probable that I might be doing something wrong or maybe complaining about something that&amp;rsquo;s obvious to you. However, these are issues that others may face.&lt;/p>
&lt;p>It was also written over several months as I gathered issues, so even looking back, I see mistakes.&lt;/p></summary><description>&lt;p>I&amp;rsquo;ve been playing with Nix and NixOS a lot more lately. I installed NixOS on one of &lt;a class="link" href="https://www.technowizardry.net/2024/09/adopting-nixos-for-my-rke1-kubernetes-nodes/" >my servers&lt;/a>, I installed the Nix CLI on my laptop, I tried to use Nix to build a Docker image, I use Nix flakes.&lt;/p>
&lt;p>This post was written from the perspective of a person new to Nix, but experienced with other computer languages. Thus, it&amp;rsquo;s probable that I might be doing something wrong or maybe complaining about something that&amp;rsquo;s obvious to you. However, these are issues that others may face.&lt;/p>
&lt;p>It was also written over several months as I gathered issues, so even looking back, I see mistakes.&lt;/p>
&lt;h1 id="the-good-parts">The Good Parts&lt;/h1>
&lt;p>The biggest advantage about Nix is that I can define an entire system using a text based language that I can check into a Git repo. The workflow of identifying a bug, making a change, running &lt;code>nixos-rebuild switch&lt;/code>, verifying it works, then checking it in. Then six months from now being able to Git blame and see why I made a change is great.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/11/i-like-the-idea-of-nix-but-dont-enjoy-using-it/github-example.png"
width="947"
height="654"
srcset="https://www.technowizardry.net/2025/11/i-like-the-idea-of-nix-but-dont-enjoy-using-it/github-example_hu_409cc7b5f526106f.png 480w, https://www.technowizardry.net/2025/11/i-like-the-idea-of-nix-but-dont-enjoy-using-it/github-example_hu_f9fa194d4c646204.png 1024w"
loading="lazy"
alt="A screenshot from GitHub showing a simple Nix script change to enable json based logs in Docker. The diff clearly shows the change vs just arbitrarily changing config on a server."
class="gallery-image"
data-flex-grow="144"
data-flex-basis="347px"
>&lt;/p>
&lt;h1 id="the-bad-parts">The Bad Parts&lt;/h1>
&lt;h2 id="nix-the-language">Nix, the language&lt;/h2>
&lt;p>The Nix language is not-intuitive. I understand it&amp;rsquo;s not like most computing languages that are designed for performing operations and it&amp;rsquo;s intended to be a declarative configuration model, but as a developer with skills in other languages, I can&amp;rsquo;t&lt;/p>
&lt;h3 id="variables">Variables&lt;/h3>
&lt;p>To declare a variable, you have to do this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="k">let&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">x&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">123&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">in&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">blah&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>My problem is I might start with some code like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pkgs&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="sr">&amp;lt;nixpkgs&amp;gt;&lt;/span> &lt;span class="p">{}&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">content&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="sr">./website.nix&lt;/span> &lt;span class="p">{}&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nginx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">nginxStable&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">overrideAttrs&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">oldAttrs&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nginxConfig&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">writeText&lt;/span> &lt;span class="s2">&amp;#34;nginx.conf&amp;#34;&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> # blah
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#39;&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>But if I want to refer to &lt;code>nginx&lt;/code> in &lt;code>nginxConfig&lt;/code>, I need to create a let block and define the variable away from the code that uses it, or create another &lt;code>let&lt;/code> block that increases nesting.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> pkgs ? import &amp;lt;nixpkgs&amp;gt; {},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> content ? import ./website.nix {},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">let
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> nginx = pkgs.nginxStable.overrideAttrs (oldAttrs: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> # ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> });
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> nginx = nginx;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> nginxConfig = pkgs.writeText &amp;#34;nginx.conf&amp;#34; &amp;#39;&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> # blah
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;&amp;#39;;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This requires a change in the code to do something that is trivial in every other programming language.&lt;/p>
&lt;h2 id="nix-the-cli">Nix, the CLI&lt;/h2>
&lt;h3 id="errors">Errors&lt;/h3>
&lt;p>Having useful error messages is critical for developers, but nix does not give good error messages. Now, a lot of this is because I&amp;rsquo;m new to Nix and make lots of dumb mistakes when coding, but a language needs to be approachable by new people. Otherwise, they can&amp;rsquo;t turn into experienced devs.&lt;/p>
&lt;p>Let&amp;rsquo;s see an example:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">error:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> … while calling the &amp;#39;derivationStrict&amp;#39; builtin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at &amp;lt;nix/derivation-internal.nix&amp;gt;:34:12:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 33|
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 34| strict = derivationStrict drvAttrs;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> | ^
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 35|
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> … while evaluating derivation &amp;#39;hugo-nginx-image.tar.gz&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> whose name attribute is located at /nix/store/vfvjcnwvwy1lrjfaz0wvvd5fwcf5gb4v-nixpkgs/nixpkgs/pkgs/stdenv/generic/make-derivation.nix:336:7
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> … while evaluating attribute &amp;#39;buildCommand&amp;#39; of derivation &amp;#39;hugo-nginx-image.tar.gz&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at /nix/store/vfvjcnwvwy1lrjfaz0wvvd5fwcf5gb4v-nixpkgs/nixpkgs/pkgs/build-support/trivial-builders/default.nix:59:17:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 58| enableParallelBuilding = true;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 59| inherit buildCommand name;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> | ^
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 60| passAsFile = [ &amp;#34;buildCommand&amp;#34; ]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (stack trace truncated; use &amp;#39;--show-trace&amp;#39; to show the full, detailed trace)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> error: expected a set but found a function: «lambda @ ~/projects/technowizardry.net/nix/docker.nix:1:1»
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>A file that I wrote was only mentioned at the very last point. Let&amp;rsquo;s zoom in:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> pkgs ? import &amp;lt;nixpkgs&amp;gt; {},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> docker ? import ./nix/docker.nix,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> hugo ? import ./nix/hugo.nix {},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pkgs.dockerTools.buildLayeredImage {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> # ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Can you spot the mistake? I spent time hunting through the &lt;code>buildLayeredImage&lt;/code> block, but that was entirely irrelevant. The error message seems to suggest line 1 and col 1, but it&amp;rsquo;s only a &lt;code>{&lt;/code>.&lt;/p>
&lt;p>Turns out, the fix is (note the curly braces).&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl">&lt;span class="gd">- docker ? import ./nix/docker.nix,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+ docker ? import ./nix/docker.nix {},
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>It was not intuitive for me to figure that out.&lt;/p>
&lt;h3 id="confusing-commands">Confusing Commands&lt;/h3>
&lt;p>There&amp;rsquo;s quite a few Nix commands:&lt;/p>
&lt;ul>
&lt;li>nix&lt;/li>
&lt;li>nix-build&lt;/li>
&lt;li>nix-channel&lt;/li>
&lt;li>nix-collect-garbage&lt;/li>
&lt;li>nix-copy-closure&lt;/li>
&lt;li>nix-daemon&lt;/li>
&lt;li>nix-env&lt;/li>
&lt;li>nix-hash&lt;/li>
&lt;li>nix-info&lt;/li>
&lt;li>nix-instantiate&lt;/li>
&lt;li>nix-prefetch-url&lt;/li>
&lt;li>nix-shell&lt;/li>
&lt;li>nix-store&lt;/li>
&lt;li>nixos-build-vms&lt;/li>
&lt;li>nixos-container&lt;/li>
&lt;li>nixos-enter&lt;/li>
&lt;li>nixos-firewall-tool&lt;/li>
&lt;li>nixos-generate-config&lt;/li>
&lt;li>nixos-help&lt;/li>
&lt;li>nixos-install&lt;/li>
&lt;li>nixos-option&lt;/li>
&lt;li>nixos-rebuild&lt;/li>
&lt;li>nixos-version&lt;/li>
&lt;/ul>
&lt;p>Now you can probably guess what most of these do, but that&amp;rsquo;s not what I mean. I want to &lt;a class="link" href="https://nix.dev/manual/nix/2.24/installation/upgrading" target="_blank" rel="noopener"
>upgrade my Nix daemon&lt;/a>. How do I do it? &lt;code>nix upgrade&lt;/code>? Nope.&lt;/p>
&lt;h2 id="nix-the-package-manager">Nix, the package manager&lt;/h2>
&lt;h3 id="what-i-would-have-expected-for-versioning">What I would have expected for versioning&lt;/h3>
&lt;p>As I understand it, when you pick a package from nixpkgs (e.g. &lt;code>pkgs.nginx&lt;/code>), you&amp;rsquo;re picking what ever happens to be defined as the version in the &lt;code>HEAD&lt;/code> commit in &lt;a class="link" href="https://github.com/NixOS/nixpkgs/" target="_blank" rel="noopener"
>NixOS/nixpkgs&lt;/a> repo. Using a Nix Flake can freeze the commit you&amp;rsquo;re looking at, but that is all of nixpkgs. You&amp;rsquo;re still at the mercy of whatever the nixpkg maintainer used as a version.&lt;/p>
&lt;p>That version could be out of date or it could even be an unofficial release candidate (e.g. &lt;a class="link" href="https://github.com/NixOS/nixpkgs/commit/07a1d0fb2f246c3dc0f68c6a54145fddce8ebc7d" target="_blank" rel="noopener"
>this commit&lt;/a> adopted an rc version for the &lt;code>onedrive&lt;/code> package). Or maybe the &lt;code>docker&lt;/code> uses 27.x which is not officially supported by your Kubernetes engine &lt;a class="link" href="https://www.suse.com/suse-rke1/support-matrix/all-supported-versions/rke1-v1-30/" target="_blank" rel="noopener"
>RKE1&lt;/a>.&lt;/p>
&lt;p>What are my options? With some packages like Docker, Nixpkgs continues to maintain the old versions, so I can just change from &lt;code>pkgs.docker&lt;/code> to &lt;code>pkgs.docker_26&lt;/code>, but not every package has this luxury. Nor is it clear how I actually use it when I use:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="n">virtualisation&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">docker&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">enable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>In the &lt;code>onedrive&lt;/code> example, the change was unexpected. It appears that it&amp;rsquo;s only a release candidate in the unstable Nix channel, but unstable seems to be the default.&lt;/p>
&lt;p>I could always just copy the derivation into my own repository and never deal with any issues, but that&amp;rsquo;s non-trivial.&lt;/p>
&lt;p>I see the nixpkgs style of versioning to be fundamentally not user-friendly. I think I&amp;rsquo;d prefer something like programming language dependency modeling (think Ruby Bundler&amp;rsquo;s Gemfile or Python&amp;rsquo;s Pipfile) where I can do:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">containerd =~ 24.0.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">firefox = *
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Right now, the upgrade story brings risk for unexpected changes. I do a &lt;code>nix flake up&lt;/code> and &lt;code>nixos-rebuild switch&lt;/code> and things just poof change.&lt;/p>
&lt;h2 id="how-do-i-even-override-something">How do I even override something?&lt;/h2>
&lt;p>I&amp;rsquo;m trying to use the Kubernetes NixPkg, but I needed to modify it to:&lt;/p>
&lt;ul>
&lt;li>Pin the control plane and worker versions so I can bump the control plane before bumping the worker nodes&lt;/li>
&lt;li>Pin the pause image so it doesn&amp;rsquo;t get accidentally deleted breaking my cluster&lt;/li>
&lt;li>Not delete the CNI binaries automatically every time Kubelet starts&lt;/li>
&lt;/ul>
&lt;h2 id="how-do-i-pin-nixpkgs">How do I pin NixPkgs?&lt;/h2>
&lt;p>To pin the versions, I found &lt;a class="link" href="https://blog.mplanchard.com/posts/installing-a-specific-version-of-a-package-with-nix.html" target="_blank" rel="noopener"
>posts&lt;/a> that said I could just import a specific commit of NixPkgs like this and then update the commit as I desire:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl">&lt;span class="gh">diff --git a/flake.nix b/flake.nix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">index a4f5dc0..2ff8429 100644
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">&lt;/span>&lt;span class="gd">--- a/flake.nix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+++ b/flake.nix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span>&lt;span class="gu">@@ -3,6 +3,8 @@
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span> 
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">  inputs = {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">    nixpkgs.url = &amp;#34;github:nixos/nixpkgs&amp;#34;;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+    # Grab a specific version of the NixPKGs so we don&amp;#39;t accidentally update K8s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+    nixpkgs-k8s.url = &amp;#34;github:NixOS/nixpkgs/78324291425a318af7b6fe08ce0646291f5588db&amp;#34;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span>    nixos-hardware.url = &amp;#34;github:NixOS/nixos-hardware/master&amp;#34;;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">    disko = {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">      url = &amp;#34;github:nix-community/disko&amp;#34;;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">@@ -10,7 +12,7 @@
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>    };
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">  };
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">-  outputs = { self, nixpkgs, disko, ... }@inputs: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+  outputs = { self, nixpkgs, nixpkgs-k8s, disko, ... }@inputs: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span>    nixosConfigurations.srv5 = nixpkgs.lib.nixosSystem {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">      specialArgs = {inherit inputs;};
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">      modules = [
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">diff --git a/parts/kubernetes.nix b/parts/kubernetes.nix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">index 5947840..47207ed 100644
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">&lt;/span>&lt;span class="gd">--- a/parts/kubernetes.nix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+++ b/parts/kubernetes.nix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span>&lt;span class="gu">@@ -1,4 +1,4 @@
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="gd">-{ config, lib, pkgs, ... }:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+{ config, lib, pkgs, nixpkgs-k8s, ... }:
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>It&amp;rsquo;s not exactly what I want which is to pin to a specific Kubernetes Major.Minor versions, but it&amp;rsquo;s fine. However, it still doesn&amp;rsquo;t make any sense:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">nixpkgs-k8s&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="o">...&lt;/span> &lt;span class="p">}:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">let&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">in&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">services&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">kubernetes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">kubelet&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ... How does one specify that kubelet is from nixpkgs-k8s, not the default pkgs?&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>What I think is happening is that the &lt;code>nixpkgs-k8s&lt;/code> parameter does point to the expected commit, but nixpkgs is composed of both configuration and packages. The configuration &lt;code>services.kubernetes.kubelet&lt;/code> works off the latest commit.&lt;/p>
&lt;p>My first assumption, until I was actually writing this post, was that I&amp;rsquo;d have to duplicate the &lt;a class="link" href="https://github.com/NixOS/nixpkgs/tree/921f06852867d06373bb0fa7ec570d14275b436d/nixos/modules/services/cluster/kubernetes" target="_blank" rel="noopener"
>&lt;code>services/cluster/kubernetes&lt;/code>&lt;/a> files and &lt;a class="link" href="https://github.com/NixOS/nixpkgs/blob/921f06852867d06373bb0fa7ec570d14275b436d/nixos/modules/services/cluster/kubernetes/kubelet.nix#L324" target="_blank" rel="noopener"
>here&lt;/a> where it builds the systemd service, I somehow modify the path to &lt;code>kubelet&lt;/code> from &lt;code>${top.package}/bin/kubelet&lt;/code> to ${nixpkgs-k8s.services.kubernetes.package}&lt;/p>
&lt;p>However, I now see a &lt;a class="link" href="https://search.nixos.org/options?channel=25.05&amp;amp;show=services.kubernetes.package&amp;amp;query=kubernetes.package" target="_blank" rel="noopener"
>&lt;code>services.kubernetes.package&lt;/code>&lt;/a> option that I think I can override. But how? Maybe &lt;code>nixpkgs-k8s.services.kubernetes.package&lt;/code>?&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">nixpkgs-k8s&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="o">...&lt;/span> &lt;span class="p">}:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">let&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">in&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">services&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">kubernetes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">package&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">nixpkgs-k8s&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">services&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">kubernetes&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">package&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Nope, I get:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">error: attribute &amp;#39;nixpkgs-k8s&amp;#39; missing
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I&amp;rsquo;m not really sure what to do to fix this. Maybe I need to vendor the entire &lt;code>kubernetes/&lt;/code> folder myself, but I look at it and I will have to make a number of modifications to get it to work to change variable references.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>Nix is a fascinating idea. The ability to declaratively define my entire OS is great for servers. I can have all my servers look exactly the same and easily create ones as I want. I like the idea of being able to Git revision control changes. It really satisfies my itch to centralize and cleanly define configuration.&lt;/p>
&lt;p>However, I find working with the Nix language and Nix the package manager to be extremely frustrating. Usually there&amp;rsquo;s a learning curve that I can get over and become proficient enough at it, but Nix I struggle with. The Nix language is just weird enough to be hard to work with, and I don&amp;rsquo;t really have faith in Nix, the package manager, not to just blow something up that I critically need.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F11%2Fi-like-the-idea-of-nix-but-dont-enjoy-using-it%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=I+like+the+idea+of+Nix%2C+but+don%27t+enjoy+using+it" style="border:0" alt="" /></description></item><item><title>Proxy ARP is broken on Unifi U7 Lite</title><link>https://www.technowizardry.net/2025/10/proxy-arp-is-broken-on-unifi-u7-lite/</link><pubDate>Sat, 04 Oct 2025 14:28:00 -0800</pubDate><guid>https://www.technowizardry.net/2025/10/proxy-arp-is-broken-on-unifi-u7-lite/</guid><summary>&lt;p>For several years, I had &lt;a class="link" href="https://store.ui.com/us/en/products/u6-lite" target="_blank" rel="noopener"
>2x Unifi U6 Lite&lt;/a> Access Points and they worked great. I had a special Wi-Fi network for my phones and laptops with a number of settings enabled but then I upgrade to the U7 Lite and immediately started having issues where my phone would disconnect. I got frustrated enough to break out my handy tool box to figure what was going wrong.&lt;/p></summary><description>&lt;p>For several years, I had &lt;a class="link" href="https://store.ui.com/us/en/products/u6-lite" target="_blank" rel="noopener"
>2x Unifi U6 Lite&lt;/a> Access Points and they worked great. I had a special Wi-Fi network for my phones and laptops with a number of settings enabled but then I upgrade to the U7 Lite and immediately started having issues where my phone would disconnect. I got frustrated enough to break out my handy tool box to figure what was going wrong.&lt;/p>
&lt;p>As it turns out, proxy ARP was breaking DNS traffic. It was able to send DNS queries to my DNS server, but responses were getting stuck because it wasn&amp;rsquo;t able to send the response back.&lt;/p>
&lt;p>ARP is necessary to translate an IP address into the Ethernet MAC Address that switches actually use to forward packet when on the same LAN.&lt;/p>
&lt;h1 id="first-attempt-using-airomon-ng">First Attempt using airomon-ng&lt;/h1>
&lt;p>I first attempted to packet capture the wireless packets between my phone and access point to see what is actually causing the phone to think it&amp;rsquo;s disconnected. Using my &lt;a class="link" href="https://www.technowizardry.net/2024/04/my-new-framework-laptop/" >linux laptop&lt;/a>, I used &lt;a class="link" href="https://www.aircrack-ng.org/doku.php?id=airmon-ng" target="_blank" rel="noopener"
>airmon-ng&lt;/a> to listen on my network channel.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/10/proxy-arp-is-broken-on-unifi-u7-lite/unifi-bands.png"
width="900"
height="342"
srcset="https://www.technowizardry.net/2025/10/proxy-arp-is-broken-on-unifi-u7-lite/unifi-bands_hu_3c95bed71fc447ab.png 480w, https://www.technowizardry.net/2025/10/proxy-arp-is-broken-on-unifi-u7-lite/unifi-bands_hu_1163ef2248f0d36b.png 1024w"
loading="lazy"
alt="Screenshot from UniFi Network UI showing the 5GHz band in channel 155 with 80MHz width."
class="gallery-image"
data-flex-grow="263"
data-flex-basis="631px"
>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">airmon-ng check &lt;span class="nb">kill&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">airmon-ng start wlp1s0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">airodump-ng wlp1s0mon --channel &lt;span class="m">155&lt;/span> --band a -w output.pcap
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I had some difficulty figuring out what channel to use for this given it was an 80Mhz channel width and airodump only seemed to support up to a 40Mhz channel width. This command was able to capture some packets.&lt;/p>
&lt;h1 id="decrypting-wpa3">Decrypting WPA3&lt;/h1>
&lt;p>Now that I have a Wireshark packet capture, I tried to decrypt it in Wireshark. Wireshark &lt;a class="link" href="https://wiki.wireshark.org/HowToDecrypt802.11" target="_blank" rel="noopener"
>supports decryption&lt;/a> if you have the encryption key, but WPA3 makes this more difficult because each device has it&amp;rsquo;s own encryption key so you need the &amp;ldquo;Pairwise Master Key (PMK)&amp;rdquo; from either the AP or the device. It&amp;rsquo;s not easy to get it out of Android, but luckily my AP has SSH access.&lt;/p>
&lt;p>I SSH&amp;rsquo;d into the AP and ran:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">ps &lt;span class="p">|&lt;/span> grep hostapd
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">kill&lt;/span> &lt;span class="o">{&lt;/span>pid&lt;span class="o">}&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> hostapd -S -g /var/run/hostapd/global -P /var/run/host -d K
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I reconnected my phone to Wi-Fi. At some point the logs contain this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">WPA: PMK - hexdump(len=32): bb 6e 4a 93 7b 2e e2 29 8b d3 29 24 02 6e b0 0e f4 d5 99 d6 f6 d2 06 60 a1 e1 5f 53 01 2c 37 17
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>To load this into Wireshark, go to Edit &amp;gt; Preferences &amp;gt; Protocols &amp;gt; IEEE 802.11 &amp;gt; Decryption Keys Edit. Add a WPA-PSK:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">bb6e4a937b2ee2298bd32924026eb00ef4d599d6f6d20660a1e15f53012c3717
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>In theory, Wireshark should now be able to decrypt the Wi-Fi packets. However, what I noticed is that airodump-ng did not seem to dump all the data packets, possible due to a limitation in my Intel Wi-Fi card.&lt;/p>
&lt;h1 id="a-second-look-at-the-hostapd-logs">A second look at the HostAPd logs&lt;/h1>
&lt;p>I went back to log at the logs on the access point and just looked at the logs being generated:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">tail -f /var/log/messages
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I saw something interesting every time the phone would report the Wi-Fi doesn&amp;rsquo;t work. It was sending DNS queries to my pi-hole server and the AP was detecting that there was no response back.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">kern.warn kernel: [STA_TRACKER] DNS request timed out; [STA: 12:f3:85:dd:92:95][QUERY: play.googleapis.com.] [DNS_SERVER :192.168.6.2] [TXN_ID 81e7] [SRCPORT 8571]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kern.warn kernel: [STA_TRACKER] DNS request timed out; [STA: 12:f3:85:dd:92:95][QUERY: play.googleapis.com.] [DNS_SERVER :192.168.6.2] [TXN_ID 81e7] [SRCPORT 8571]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kern.warn kernel: [STA_TRACKER] DNS request timed out; [STA: 12:f3:85:dd:92:95][QUERY: connectivitycheck.gstatic.com.] [DNS_SERVER :192.168.6.2] [TXN_ID 65e5] [SRCPORT 63180]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kern.warn kernel: [STA_TRACKER] DNS request timed out; [STA: 12:f3:85:dd:92:95][QUERY: connectivitycheck.gstatic.com.] [DNS_SERVER :192.168.6.2] [TXN_ID 65e5] [SRCPORT 63180]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kern.warn kernel: [STA_TRACKER] DNS request timed out; [STA: 12:f3:85:dd:92:95][QUERY: connectivitycheck.gstatic.com.] [DNS_SERVER :192.168.6.2] [TXN_ID b304] [SRCPORT 55085]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">daemon.info stahtd: stahtd[28828]: [STA-TRACKER].stahtd_dump_event(): {&amp;#34;message_type&amp;#34;:&amp;#34;STA_ASSOC_TRACKER&amp;#34;,&amp;#34;mac&amp;#34;:&amp;#34;ab:cd:ef:ab:cd:ef&amp;#34;,&amp;#34;vap&amp;#34;:&amp;#34;ath4&amp;#34;,&amp;#34;event_type&amp;#34;:&amp;#34;dns timeout&amp;#34;,&amp;#34;assoc_status&amp;#34;:&amp;#34;0&amp;#34;,&amp;#34;query_0&amp;#34;:&amp;#34;play.googleapis.com.&amp;#34;,&amp;#34;query_server_0&amp;#34;:&amp;#34;192.168.6.2&amp;#34;,&amp;#34;query_1&amp;#34;:&amp;#34;www.google.com.&amp;#34;,&amp;#34;query_server_1&amp;#34;:&amp;#34;192.168.6.2&amp;#34;,&amp;#34;query_2&amp;#34;:&amp;#34;play.googleapis.com.&amp;#34;,&amp;#34;query_server_2&amp;#34;:&amp;#34;192.168.6.2&amp;#34;,&amp;#34;query_3&amp;#34;:&amp;#34;www.google.com.&amp;#34;,&amp;#34;query_server_3&amp;#34;:&amp;#34;192.168.6.2&amp;#34;}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="is-it-dns">Is it DNS?&lt;/h1>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/10/proxy-arp-is-broken-on-unifi-u7-lite/its-always-dns.jpg"
width="600"
height="600"
srcset="https://www.technowizardry.net/2025/10/proxy-arp-is-broken-on-unifi-u7-lite/its-always-dns_hu_ec799036ceb4e71.jpg 480w, https://www.technowizardry.net/2025/10/proxy-arp-is-broken-on-unifi-u7-lite/its-always-dns_hu_275763c1d8735996.jpg 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="100"
data-flex-basis="240px"
>&lt;/p>
&lt;p>DNS is looking awfully suspicious right now and it is frequently blamed for network issues. However, if I check Pi-Hole (running at 192.168.6.2 mentioned above), it shows that it successfully resolved queries in the query log and sent a response.&lt;/p>
&lt;h1 id="tcpdump-everything">tcpdump everything&lt;/h1>
&lt;p>Time to start packet capturing everything. My network looks like this:
&lt;img src="https://www.technowizardry.net/2025/10/proxy-arp-is-broken-on-unifi-u7-lite/dns-query-path.png"
width="431"
height="459"
srcset="https://www.technowizardry.net/2025/10/proxy-arp-is-broken-on-unifi-u7-lite/dns-query-path_hu_65ca5a5cac93f832.png 480w, https://www.technowizardry.net/2025/10/proxy-arp-is-broken-on-unifi-u7-lite/dns-query-path_hu_b4262991ccb82dcf.png 1024w"
loading="lazy"
alt="Network architecture diagram showing a phone, Wi-Fi access point, switch, router, and a server. Explained separately"
class="gallery-image"
data-flex-grow="93"
data-flex-basis="225px"
>&lt;/p>
&lt;p>DNS queries are sent from my phone through the AP through the router to the Pi-hole. My pi-hole is being via out of &lt;a class="link" href="https://www.technowizardry.net/2021/10/home-lab-part-2-networking-setup/" >Kubernetes&lt;/a>, but the responses go directly to the sender because my server is on two networks. It&amp;rsquo;s complicated, but it does not break any Ethernet standards.&lt;/p>
&lt;p>I ran tcpdump on the Wi-Fi access point, on the router, and on the server. All three points show the DNS queries going to the server, but no responses back. The access point shows this cpature:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">13:54:11.331369 ath4 P IP 192.168.2.62.32550 &amp;gt; 192.168.6.2.53: 12773+ AAAA? example.com. (35)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">13:54:11.331918 eth0 B ARP, Request who-has 192.168.2.62 tell 192.168.2.3, length 46
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">13:54:11.331953 ath4 Out ARP, Request who-has 192.168.2.62 tell 192.168.2.3, length 46
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">13:54:11.331953 ath4 P IP 192.168.2.62.1558 &amp;gt; 192.168.6.2.53: 41230+ A? example.com. (35)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">13:54:11.331986 ath2 Out ARP, Request who-has 192.168.2.62 tell 192.168.2.3, length 46
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">13:54:11.331993 eth0 Out IP 192.168.2.62.1558 &amp;gt; 192.168.6.2.53: 41230+ A? example.com. (35)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">13:54:11.332003 ath0 Out ARP, Request who-has 192.168.2.62 tell 192.168.2.3, length 46
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">13:54:11.332020 br0 B ARP, Request who-has 192.168.2.62 tell 192.168.2.3, length 46
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The server is trying to figure out how to send the response back to the client by performing an ARP Request to say where is this IP address? The switch knows where the MAC address is and forwards the packet to the AP. The AP seems like it sends it to the phone, but the phone either doesn&amp;rsquo;t respond or doesn&amp;rsquo;t receive it even though the packet capture shows it going out the &lt;code>ath4&lt;/code> interface which is the Wi-Fi adapter.&lt;/p>
&lt;h1 id="proxy-arp">Proxy ARP&lt;/h1>
&lt;p>That gave me the idea to check the Wi-Fi network settings. Unifi exposes a number of different options and I already turned off the more problematic settings to see, but didn&amp;rsquo;t turn off Proxy ARP.&lt;/p>
&lt;p>Unifi says Proxy ARP &lt;em>Reduces airtime usage by allowing APs to &amp;ldquo;proxy common broadcast frames as unicast. This can improve latency, but may cause connectivity issues in some networks.&lt;/em>&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/10/proxy-arp-is-broken-on-unifi-u7-lite/unifi-network-settings.png"
width="394"
height="319"
srcset="https://www.technowizardry.net/2025/10/proxy-arp-is-broken-on-unifi-u7-lite/unifi-network-settings_hu_6ea36016b00dd527.png 480w, https://www.technowizardry.net/2025/10/proxy-arp-is-broken-on-unifi-u7-lite/unifi-network-settings_hu_71c8ff3e731a3f90.png 1024w"
loading="lazy"
alt="Screenshot of the Ubiquiti Network Wi-Fi advanced settings page showing Proxy ARP. They provide the above explanation."
class="gallery-image"
data-flex-grow="123"
data-flex-basis="296px"
>&lt;/p>
&lt;p>Clearly, the &amp;ldquo;may cause connectivity issues&amp;rdquo; is right. This is unusual because I had the setting on for all my U6 Lites without any issue, but as soon as I get a U7 Lite, it breaks.&lt;/p>
&lt;p>It&amp;rsquo;s now been over a week after disabling this option without any issues with my phone now. For reference, I&amp;rsquo;m running firmware &lt;a class="link" href="https://community.ui.com/releases/UniFi-Access-Point-all-U7-and-E7-models-8-0-49/d560c28b-8625-4535-938f-a50867597f3b" target="_blank" rel="noopener"
>v8.0.49&lt;/a> in case they fix it later.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F10%2Fproxy-arp-is-broken-on-unifi-u7-lite%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Proxy+ARP+is+broken+on+Unifi+U7+Lite" style="border:0" alt="" /></description></item><item><title>Auto enable user namespaces in Kubernetes</title><link>https://www.technowizardry.net/2025/09/auto-enable-userns-in-k8s-kyverno/</link><pubDate>Mon, 29 Sep 2025 20:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2025/09/auto-enable-userns-in-k8s-kyverno/</guid><summary>&lt;p>When you run a container, the process IDs are namespaced and different in the container vs the host, the network stack is namespaced, the file system mounts are namespaced, but a process running as root in the container is running as root outside the container. This is risk because many privilege escalation vulnerabilities in Linux can be exploited because of this common user id.&lt;/p>
&lt;p>Linux user namespaces aims to mitigate risks with running a process as root or other shared user ID where a vulnerability could allow a containerized process to escape a namespace and have privileges in the host. For example, process running as root in a container, would be marked as root outside the host without user namespaces or a process that has a UID that exists on the host.&lt;/p>
&lt;p>User namespaces attempt to fix this by saying UID=0 inside the container actually is UID=12356231 on the host. Thus, a breakout is not as bad as it could be without user namespaces.&lt;/p>
&lt;p>In this post, I&amp;rsquo;m going to walk through how I use &lt;a class="link" href="https://kyverno.io/" target="_blank" rel="noopener"
>Kyverno&lt;/a>, a Kubernetes native policy system to easily enable user namespaces in pods where it can be.&lt;/p></summary><description>&lt;p>When you run a container, the process IDs are namespaced and different in the container vs the host, the network stack is namespaced, the file system mounts are namespaced, but a process running as root in the container is running as root outside the container. This is risk because many privilege escalation vulnerabilities in Linux can be exploited because of this common user id.&lt;/p>
&lt;p>Linux user namespaces aims to mitigate risks with running a process as root or other shared user ID where a vulnerability could allow a containerized process to escape a namespace and have privileges in the host. For example, process running as root in a container, would be marked as root outside the host without user namespaces or a process that has a UID that exists on the host.&lt;/p>
&lt;p>User namespaces attempt to fix this by saying UID=0 inside the container actually is UID=12356231 on the host. Thus, a breakout is not as bad as it could be without user namespaces.&lt;/p>
&lt;p>In this post, I&amp;rsquo;m going to walk through how I use &lt;a class="link" href="https://kyverno.io/" target="_blank" rel="noopener"
>Kyverno&lt;/a>, a Kubernetes native policy system to easily enable user namespaces in pods where it can be.&lt;/p>
&lt;h1 id="release-schedule">Release Schedule&lt;/h1>
&lt;p>In release v1.30, Kubernetes announced beta support for User namespaces. In &lt;a class="link" href="https://kubernetes.io/blog/2025/04/25/userns-enabled-by-default/" target="_blank" rel="noopener"
>release v1.32&lt;/a>, Kubernetes enabled it by default.&lt;/p>
&lt;p>If you&amp;rsquo;re running v1.30-v1.31, then enable the feature gate on the APIServer with the &lt;code>--feature-gates=UserNamespacesSupport=true&lt;/code> arg.&lt;/p>
&lt;h1 id="thinking-about-policies">Thinking about policies&lt;/h1>
&lt;p>Kyverno policies are defined as Kubernetes resources, then when any workload resources are created or changed, Kyverno receives a webhook call and makes changes based on the policy.&lt;/p>
&lt;p>What I want is to automatically set &lt;code>PodSpec.hostUsers=false&lt;/code> when a pod is created. However, due to &lt;a class="link" href="https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/#limitations" target="_blank" rel="noopener"
>limitations&lt;/a>, I can&amp;rsquo;t use it with &lt;code>hostNetwork: true&lt;/code>, &lt;code>hostIPC: true&lt;/code>, &lt;code>hostPID: true&lt;/code>, or &lt;code>volumeDevices&lt;/code>. Also, any volumes that get mounted into the pod have to support &lt;a class="link" href="https://www.kernel.org/doc/html/latest/filesystems/idmappings.html" target="_blank" rel="noopener"
>idmap&lt;/a>. If I set &lt;code>hostUsers: false&lt;/code> on every pod, then I&amp;rsquo;ll get a bunch of failures and have a bad day.&lt;/p>
&lt;p>I&amp;rsquo;m going to walk through how I created the policy. If you&amp;rsquo;re just interested in the final policy, Skip below.&lt;/p>
&lt;h1 id="creating-the-policy">Creating the policy&lt;/h1>
&lt;h2 id="mutate-all-the-things">Mutate all the things&lt;/h2>
&lt;p>First, I start with a basic policy that enables users namespaces on all pods that are created in the cluster.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kyverno.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pod-policies.kyverno.io/autogen-controllers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">none&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enable-userns-when-able&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">background&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">match&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">any&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kinds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">Pod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mutate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">patchStrategicMerge&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostUsers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enable-userns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">preconditions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">all&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ request.operation || &amp;#39;&amp;#39;BACKGROUND&amp;#39;&amp;#39; }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Equals&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CREATE&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">validationFailureAction&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Audit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>However, this will cause some pods to fail to create because user namespaces isn&amp;rsquo;t supported on all types of volumes and hostNetwork.&lt;/p>
&lt;h2 id="skip-for-pods-with-other-host-namespacing">Skip for pods with other host namespacing&lt;/h2>
&lt;p>User namespacing isn&amp;rsquo;t supported with pods using the host network or host PIDs. My guess is because those would enable a container to see user ids that it can&amp;rsquo;t know about.&lt;/p>
&lt;p>I want to exclude pods that have these values set. This can be done with a precondition. Kyverno&amp;rsquo;s precondition language gets to be quite strange with complex preconditions, but these are trivial.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">preconditions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">all&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ... previous precondition&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ request.object.spec.hostUsers || &amp;#39;&amp;#39;false&amp;#39;&amp;#39; }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NotEquals&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ request.object.spec.hostIPC || &amp;#39;&amp;#39;false&amp;#39;&amp;#39; }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Equals&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;false&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ request.object.spec.hostPID || &amp;#39;&amp;#39;false&amp;#39;&amp;#39; }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Equals&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;false&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ request.object.spec.hostNetwork || &amp;#39;&amp;#39;false&amp;#39;&amp;#39; }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Equals&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;false&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now when I create any pods with &lt;code>hostNetwork=true&lt;/code> or &lt;code>hostIPC&lt;/code> or explicitly setting &lt;code>hostUsers=true&lt;/code>, then nothing is changed.&lt;/p>
&lt;h2 id="problems-with-pvcs">Problems with PVCs&lt;/h2>
&lt;p>However, I still have problems when I start working with volumes. If I mount a &lt;code>hostPath&lt;/code> volume, it works because your host volumes support user namespaces, but not all volumes do support them. For example, &lt;a class="link" href="https://longhorn.io/" target="_blank" rel="noopener"
>Longhorn&lt;/a>, my storage provider in my home lab, uses &lt;a class="link" href="https://en.wikipedia.org/wiki/Network_File_System" target="_blank" rel="noopener"
>NFS (Network File System)&lt;/a> to mount volumes and it didn&amp;rsquo;t support idmap:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">failed&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">create&lt;/span> &lt;span class="n">containerd&lt;/span> &lt;span class="n">task&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">failed&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">create&lt;/span> &lt;span class="n">shim&lt;/span> &lt;span class="n">task&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">OCI&lt;/span> &lt;span class="n">runtime&lt;/span> &lt;span class="n">create&lt;/span> &lt;span class="n">failed&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">runc&lt;/span> &lt;span class="n">create&lt;/span> &lt;span class="n">failed&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">unable&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">start&lt;/span> &lt;span class="n">container&lt;/span> &lt;span class="n">process&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="ow">or&lt;/span> &lt;span class="n">during&lt;/span> &lt;span class="n">container&lt;/span> &lt;span class="n">init&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">failed&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">fulfil&lt;/span> &lt;span class="n">mount&lt;/span> &lt;span class="n">request&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">failed&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">set&lt;/span> &lt;span class="n">MOUNT_ATTR_IDMAP&lt;/span> &lt;span class="n">on&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kubelet&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">pods&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="n">ea01f58&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">57&lt;/span>&lt;span class="n">f9&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">4504&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">87&lt;/span>&lt;span class="n">b5&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">898&lt;/span>&lt;span class="n">ca80bd980&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">volumes&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kube&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">rnetes&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">io&lt;/span>&lt;span class="o">~&lt;/span>&lt;span class="n">csi&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">pvc&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">48&lt;/span>&lt;span class="n">bee7b4&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">82&lt;/span>&lt;span class="n">b5&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">4119&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">a927&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">ceb0b0939c20&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">mount&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">invalid&lt;/span> &lt;span class="n">argument&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">maybe&lt;/span> &lt;span class="n">the&lt;/span> &lt;span class="n">filesystem&lt;/span> &lt;span class="n">used&lt;/span> &lt;span class="n">doesn&lt;/span>&lt;span class="s1">&amp;#39;t support idmap mounts on this kernel?)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Looking further, I believe this issue is limited to just &lt;a class="link" href="https://longhorn.io/docs/1.9.1/nodes-and-volumes/volumes/rwx-volumes/" target="_blank" rel="noopener"
>ReadWriteMany&lt;/a> volumes (volumes that can be mounted on multiple nodes at once) as other volumes are able to work with user namespaces.&lt;/p>
&lt;p>I also encountered issues with pods that mount devices from the host&amp;rsquo;s &lt;code>/dev&lt;/code>, like my &lt;a class="link" href="https://www.home-assistant.io/" target="_blank" rel="noopener"
>Home Assistant&lt;/a> that talked to USB Zigbee adapters.&lt;/p>
&lt;p>I need a way to skip mutating pods that use volumes that don&amp;rsquo;t support user namespaces.&lt;/p>
&lt;h1 id="skip-for-unsupported-volumes">Skip for unsupported volumes&lt;/h1>
&lt;p>This is where Kyverno&amp;rsquo;s policy language gets very messy and unintuitive.&lt;/p>
&lt;h2 id="skip-host-hardware-devices">Skip host hardware devices&lt;/h2>
&lt;p>First, we check to see if there&amp;rsquo;s any mounts that use &lt;code>hostPath&lt;/code> to mount something from under the host&amp;rsquo;s &lt;code>/dev/&lt;/code> folder. If it finds one or more, it skips mutating because user namespaces isn&amp;rsquo;t supported. This uses a language called &lt;a class="link" href="https://jmespath.org/" target="_blank" rel="noopener"
>JMESPath&lt;/a> to search the JSON/YAML file kind of like XPath.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">hasdevice&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">variable&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">default&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jmesPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> request.object.spec.volumes[?hostPath].hostPath.path[?starts_with(@,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#39;/dev/&amp;#39;)] | length(@)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># mutate:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># match: &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">preconditions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># - ... other preconditions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ hasdevice }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Equals&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="skip-for-longhorn">Skip for Longhorn&lt;/h2>
&lt;p>Next, we look for any volume mounts that reference a Longhorn RWX volume. A pod doesn&amp;rsquo;t directly say what kind of PVC is being mounted. It just says the name of the PVC. What can we do to figure this out?&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Pod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">persistentVolumeClaim&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">claimName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">paperless&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Luckily, Kyverno has &lt;a class="link" href="https://kyverno.io/docs/policy-types/cluster-policy/external-data-sources/" target="_blank" rel="noopener"
>a feature&lt;/a> that calls back to the Kubernete&amp;rsquo;s API to fetch one or more resources. I have the name of the PVC, can this tell me what kind of volume I have?&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># kubectl get pvc -n paperless paperless -o yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PersistentVolumeClaim&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pv.kubernetes.io/bind-completed&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;yes&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pv.kubernetes.io/bound-by-controller&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;yes&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volume.beta.kubernetes.io/storage-provisioner&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">driver.longhorn.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volume.kubernetes.io/storage-provisioner&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">driver.longhorn.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">recurring-job-group.longhorn.io/default&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enabled&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">recurring-job.longhorn.io/weekly&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enabled&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">paperless&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">paperless&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">accessModes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">ReadWriteOnce&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">ReadWriteMany&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">storageClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">longhorn&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>It has &lt;code>accessModes&lt;/code>, so we can use that to skip mutating RWX volumes. The &lt;code>spec.storageClassName&lt;/code> looks relevant at first, but you can create more Storage Classes with any name that uses Longhorn and it&amp;rsquo;s common to define classes with different node or disk selectors, so while it would work, it&amp;rsquo;s fragile and easy to break.&lt;/p>
&lt;p>There&amp;rsquo;s also the the &lt;code>metadata.annotations.&amp;quot;volume.kubernetes.io/storage-provisioner&amp;quot;&lt;/code>, but this isn&amp;rsquo;t present on my older volumes&amp;ndash;only the &lt;a class="link" href="https://kubernetes.io/docs/reference/labels-annotations-taints/#volume-beta-kubernetes-io-storage-provisioner-deprecated" target="_blank" rel="noopener"
>beta annotation&lt;/a> is there and looking at deprecated annotations feels dirty. Thus, I&amp;rsquo;d have to look at both annotations.&lt;/p>
&lt;p>The following will tell Kyverno to go back to Kubernetes and fetch all PVCs in the same namespace, then locally filter using JMESPath to find all PVCs that are managed by Longhorn and use ReadWriteMany and stores the names in a variable called &lt;code>longhornpvcs&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">apiCall&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jmesPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> items[?((metadata.annotations.&amp;#34;volume.kubernetes.io/storage-provisioner&amp;#34; == &amp;#39;driver.longhorn.io&amp;#39; || metadata.annotations.&amp;#34;volume.beta.kubernetes.io/storage-provisioner&amp;#34; == &amp;#39;driver.longhorn.io&amp;#39;) &amp;amp;&amp;amp; contains(spec.accessModes, &amp;#39;ReadWriteMany&amp;#39;))].metadata.name&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">method&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GET&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/api/v1/namespaces/{{ request.namespace }}/persistentvolumeclaims&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">longhornpvcs&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># match:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># mutate:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then, in the preconditions, we iterate over all the volumes in the Pod, gets the names and sees if any of them exist in the set I defined above. If any of them are, it skips mutation. Additionally, skips mutation for any volumes from any provisioner that mounts the volume as a &lt;code>volumeDevice&lt;/code>, which is a low level mount and not supported by user namespaces.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="c"># context:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># match:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># mutate:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">preconditions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">all&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ... other preconditions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> request.object.spec.volumes[?persistentVolumeClaim].persistentVolumeClaim.claimName
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> || `[]` }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">message&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Longhorn PVCs may not support user namespaces&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AllNotIn&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ longhornpvcs }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ request.object.spec.containers[?volumeDevices] | length(@) }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">message&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;volumeDevices don&amp;#39;t support support user namespaces&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Equals&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="the-final-policy">The Final Policy&lt;/h1>
&lt;p>Here&amp;rsquo;s the final policy put together:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;span class="lnt">68
&lt;/span>&lt;span class="lnt">69
&lt;/span>&lt;span class="lnt">70
&lt;/span>&lt;span class="lnt">71
&lt;/span>&lt;span class="lnt">72
&lt;/span>&lt;span class="lnt">73
&lt;/span>&lt;span class="lnt">74
&lt;/span>&lt;span class="lnt">75
&lt;/span>&lt;span class="lnt">76
&lt;/span>&lt;span class="lnt">77
&lt;/span>&lt;span class="lnt">78
&lt;/span>&lt;span class="lnt">79
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kyverno.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kyverno.io/kyverno-version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1.9.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pod-policies.kyverno.io/autogen-controllers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">none&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies.kyverno.io/category&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Pod Security&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies.kyverno.io/description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">This policy ensures that user namespaces are enabled for pods that can&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies.kyverno.io/severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">medium&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies.kyverno.io/subject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Pod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies.kyverno.io/title&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Enable User Namespaces when possible&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enable-userns-when-able&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">background&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">hasdevice&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">variable&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">default&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jmesPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> request.object.spec.volumes[?hostPath].hostPath.path[?starts_with(@,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#39;/dev/&amp;#39;)] | length(@)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">apiCall&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jmesPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> items[?((metadata.annotations.&amp;#34;volume.kubernetes.io/storage-provisioner&amp;#34; == &amp;#39;driver.longhorn.io&amp;#39; || metadata.annotations.&amp;#34;volume.beta.kubernetes.io/storage-provisioner&amp;#34; == &amp;#39;driver.longhorn.io&amp;#39;) &amp;amp;&amp;amp; contains(spec.accessModes, &amp;#39;ReadWriteMany&amp;#39;))].metadata.name&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">method&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GET&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/api/v1/namespaces/{{ request.namespace }}/persistentvolumeclaims&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">longhornpvcs&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exclude&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">any&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">kube-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">longhorn-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">calico-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">match&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">any&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kinds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">Pod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mutate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">patchStrategicMerge&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostUsers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enable-userns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">preconditions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">all&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ request.operation || &amp;#39;&amp;#39;BACKGROUND&amp;#39;&amp;#39; }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Equals&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CREATE&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ request.object.spec.hostUsers || &amp;#39;&amp;#39;false&amp;#39;&amp;#39; }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">message&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Skipping because hostUsers is explicitly set to true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NotEquals&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ request.object.spec.hostIPC || &amp;#39;&amp;#39;false&amp;#39;&amp;#39; }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Equals&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;false&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ request.object.spec.hostPID || &amp;#39;&amp;#39;false&amp;#39;&amp;#39; }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Equals&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;false&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ request.object.spec.hostNetwork || &amp;#39;&amp;#39;false&amp;#39;&amp;#39; }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">message&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;User namespaces can&amp;#39;t be used when hostNetwork=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Equals&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;false&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ hasdevice }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Equals&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ request.object.spec.containers[?volumeDevices] | length(@) }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">message&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;volumeDevices don&amp;#39;t support support user namespaces&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Equals&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> request.object.spec.volumes[?persistentVolumeClaim].persistentVolumeClaim.claimName
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> || `[]` }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">message&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Longhorn PVCs may not support user namespaces&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AllNotIn&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ longhornpvcs }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">validationFailureAction&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Audit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="see-it-in-action">See it in action&lt;/h1>
&lt;p>The policy only applies to new pod creation, so existing pods are not modified. To see it, we need to redeploy pods.&lt;/p>
&lt;p>To see which pods are using user namespaces with kubectl, run the following command. If the HostUsers column shows &lt;code>&amp;lt;none&amp;gt;&lt;/code> or &lt;code>true&lt;/code>, then it&amp;rsquo;s not. If it shows &lt;code>false&lt;/code>, then the pod has it&amp;rsquo;s own separate user namespace.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">kubectl get --all-namespaces pods -o custom-columns&lt;span class="o">=&lt;/span>Namespace:.metadata.namespace,Name:.metadata.name,HostUsers:.spec.hostUsers
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Output:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">NAMESPACE               NAME                                                           USERNS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">authelia                authelia-6dd4b4b75c-zvm7m                                      false
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calico-apiserver        calico-apiserver-7dbc7798c5-nvrqp                              &amp;lt;none&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calico-system           calico-kube-controllers-5778576475-b6gr8                       &amp;lt;none&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calico-system           calico-node-92t8f                                              &amp;lt;none&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calico-system           calico-typha-55dfc8c999-ckldk                                  &amp;lt;none&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calico-system           csi-node-driver-9jhvb                                          &amp;lt;none&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cattle-system           cattle-cluster-agent-746f6d788f-h6n2q                          &amp;lt;none&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cattle-system           kube-api-auth-cqrcz                                            &amp;lt;none&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cattle-system           rancher-webhook-5d4b7cd966-phdj2                               &amp;lt;none&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cattle-system           system-upgrade-controller-57b66d6cbd-6p92n                     &amp;lt;none&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F09%2Fauto-enable-userns-in-k8s-kyverno%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Auto+enable+user+namespaces+in+Kubernetes" style="border:0" alt="" /></description></item><item><title>Guaranteed Quality of Service in my Home Lab</title><link>https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/</link><pubDate>Wed, 17 Sep 2025 15:35:00 +0000</pubDate><guid>https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/</guid><summary>&lt;p>A few times in my Kubernetes clusters, I&amp;rsquo;ve encountered situations where some process consumes all the CPU or RAM which starves other services for critical services. For example, in one situation, &lt;a class="link" href="https://longhorn.io" target="_blank" rel="noopener"
>Longhorn&lt;/a> consumed all CPU and RAM and my pi-hole running on the same machine stopped being able to process DNS requests. Other issues have included having to shut down one of my worker nodes and the other nodes not having enough capacity to take on pods and important pods not getting scheduled or even a mistake when I changed the pod selector labels and Kubernetes just spawned thousands of pods.&lt;/p>
&lt;p>The graph below shows Disk I/O of a node with excessive disk writes because the OS is swapping RAM out to desk and back.
&lt;img src="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/prometheus-diskio-spikes.png"
width="939"
height="337"
srcset="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/prometheus-diskio-spikes_hu_538f5ee2ce56555.png 480w, https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/prometheus-diskio-spikes_hu_4b6af1c61538c75e.png 1024w"
loading="lazy"
alt="A graph of Disk I/O showing large amounts of disk I/O as the host swaps RAM to disk, then it finally fails"
class="gallery-image"
data-flex-grow="278"
data-flex-basis="668px"
>&lt;/p>
&lt;p>My home lab servers are now running what I consider to be &amp;ldquo;business critical&amp;rdquo; services and I don&amp;rsquo;t want those to be impacted. Kubernetes has several different knobs we can use to improve this such as leveraging Linux&amp;rsquo;s cgroups to ensure that specific pods get a certain amount of CPU and RAM. It also supports prioritization, so that certain pods get scheduled and others get evicted if there isn&amp;rsquo;t enough space.&lt;/p>
&lt;p>Or even lately, I&amp;rsquo;ve been hitting the max pod limit of 110 pods on my single-node cluster. Not everything is important and I want to make sure certain cron jobs always run even if I&amp;rsquo;m running some low-priority jobs. Turns out it is possible to be running 110 different pods.&lt;/p></summary><description>&lt;p>A few times in my Kubernetes clusters, I&amp;rsquo;ve encountered situations where some process consumes all the CPU or RAM which starves other services for critical services. For example, in one situation, &lt;a class="link" href="https://longhorn.io" target="_blank" rel="noopener"
>Longhorn&lt;/a> consumed all CPU and RAM and my pi-hole running on the same machine stopped being able to process DNS requests. Other issues have included having to shut down one of my worker nodes and the other nodes not having enough capacity to take on pods and important pods not getting scheduled or even a mistake when I changed the pod selector labels and Kubernetes just spawned thousands of pods.&lt;/p>
&lt;p>The graph below shows Disk I/O of a node with excessive disk writes because the OS is swapping RAM out to desk and back.
&lt;img src="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/prometheus-diskio-spikes.png"
width="939"
height="337"
srcset="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/prometheus-diskio-spikes_hu_538f5ee2ce56555.png 480w, https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/prometheus-diskio-spikes_hu_4b6af1c61538c75e.png 1024w"
loading="lazy"
alt="A graph of Disk I/O showing large amounts of disk I/O as the host swaps RAM to disk, then it finally fails"
class="gallery-image"
data-flex-grow="278"
data-flex-basis="668px"
>&lt;/p>
&lt;p>My home lab servers are now running what I consider to be &amp;ldquo;business critical&amp;rdquo; services and I don&amp;rsquo;t want those to be impacted. Kubernetes has several different knobs we can use to improve this such as leveraging Linux&amp;rsquo;s cgroups to ensure that specific pods get a certain amount of CPU and RAM. It also supports prioritization, so that certain pods get scheduled and others get evicted if there isn&amp;rsquo;t enough space.&lt;/p>
&lt;p>Or even lately, I&amp;rsquo;ve been hitting the max pod limit of 110 pods on my single-node cluster. Not everything is important and I want to make sure certain cron jobs always run even if I&amp;rsquo;m running some low-priority jobs. Turns out it is possible to be running 110 different pods.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/rancher-maxed-cluster.png"
width="609"
height="370"
srcset="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/rancher-maxed-cluster_hu_6740a0a57616f8ec.png 480w, https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/rancher-maxed-cluster_hu_e87e68cc96e272b0.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="164"
data-flex-basis="395px"
>&lt;/p>
&lt;p>Image Below: A sad worker node valiantly trying to keep operating without enough RAM. The breaks in metrics is when Prometheus is unable to scrape metrics because it didn&amp;rsquo;t get a chance to run.
&lt;img src="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/prometheus-ram-gaps.png"
width="1004"
height="369"
srcset="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/prometheus-ram-gaps_hu_54b7de2b95b88720.png 480w, https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/prometheus-ram-gaps_hu_6c73511c78bcae9a.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="272"
data-flex-basis="653px"
>&lt;/p>
&lt;p>In this post, I&amp;rsquo;m going to walk through the steps that I took to improve reliability by using Kubernetes&amp;rsquo; resource management and scheduling features. My clusters run on a limited number of hosts (only one node for my home and three nodes for my public cluster) and do not automatically scale up or down nodes, so I need to ensure that everything fits onto the clusters without starving any resource.&lt;/p>
&lt;h1 id="the-problem">The problem&lt;/h1>
&lt;p>Each of the incidents that I alluded to earlier were caused by me trying to schedule too many services for the given nodes. Exceeding the total RAM space causes the kernel to start swapping memory out to disk which slows down services as memory pages have to keep getting swapped in and out and additionally prevents the kernel from caching files in RAM.&lt;/p>
&lt;p>In this chart, the OS has run out of RAM and Prometheus is unable to get time on the CPU to scrape metrics. Before Prometheus can run any code, it has to wait for the Kernel to swap in pages that it needed. This was bad because it was starving Pi-hole&amp;rsquo;s DNS server for CPU time and prevented Internet access.
&lt;img src="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/prometheus-ram-gaps.png"
width="1004"
height="369"
srcset="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/prometheus-ram-gaps_hu_54b7de2b95b88720.png 480w, https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/prometheus-ram-gaps_hu_6c73511c78bcae9a.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="272"
data-flex-basis="653px"
>&lt;/p>
&lt;p>Overscheduling the CPU with too much work means that there&amp;rsquo;s threads awaiting time on the CPU, but not enough cores. Like RAM swapping, this will increase latency of operations.&lt;/p>
&lt;p>There are other ways to saturate a host like disk or network I/O, but I&amp;rsquo;ll focus on RAM and CPU because Kubernetes provides resource management for these two limits.&lt;/p>
&lt;h1 id="resource-requests-and-limits">Resource requests and limits&lt;/h1>
&lt;p>The first thing is setting resource requests and optionally limits on workloads. A resource request states that a given container should only be allocated on a node if there&amp;rsquo;s enough un-reserved space. So a container that requests 4GB should not be allocated on a node with 8GB of RAM that already has 6GB of RAM dedicated to other workloads.&lt;/p>
&lt;p>A limit states the maximum amount of a resource a given container should be allocated. Memory limits are given to the container so it knows how much it can allocate, though exceeding it will result in the container being killed. Software can be a little bit unpredictable in how it uses resources, it might spike up and settle back down faster than Prometheus can collect metrics, so be conservative in your limits.&lt;/p>
&lt;p>If you don&amp;rsquo;t set a limit, then the container can use as much as it wants.&lt;/p>
&lt;blockquote>
&lt;p>Can not setting a limit cause problems for other services?&lt;/p>&lt;/blockquote>
&lt;p>Resource requests and limits use Linux &lt;a class="link" href="https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html" target="_blank" rel="noopener"
>cgroups&lt;/a> &lt;a class="link" href="https://kubernetes.io/docs/concepts/architecture/cgroups/" target="_blank" rel="noopener"
>under the hood&lt;/a> which tells the Linux scheduler how much time and memory should be provided to a process or process group (a process group is a hierarchy of processes.) If you don&amp;rsquo;t use resource requests at all, then one process can use memory and push another process out of memory (unless it gets OOMKilled by the kernel.)&lt;/p>
&lt;p>A memory request sets the &lt;a class="link" href="https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#memory-interface-files" target="_blank" rel="noopener"
>&lt;code>memory.min&lt;/code> cgroup setting &lt;/a> which tells the Kernel to avoid reclaiming memory from this container until there&amp;rsquo;s no other process to recover RAM from. This means that the Kernel will generally scavenge RAM from other places before impacting the processes you really care about.&lt;/p>
&lt;p>By default, Kubernetes also sets the &lt;code>memory.swap.max&lt;/code> cgroup setting to 0 which prevents the Kernel from swapping memory pages out for the given container, however as in the example above, there&amp;rsquo;s still plenty of other pages that are necessary that aren&amp;rsquo;t directly in the container&amp;rsquo;s space, there&amp;rsquo;s a lot of system daemons, file caching, and other processes that are crucial for operation that could start swapping. However, this can be mitigated by reserving space for those processes directly.&lt;/p>
&lt;p>So, not setting a limit &lt;em>can&lt;/em> still cause issues, however setting requests gives you a lot of protection against over-scheduling until things get really bad.&lt;/p>
&lt;h2 id="picking-numbers">Picking numbers&lt;/h2>
&lt;h3 id="memory">Memory&lt;/h3>
&lt;p>To pick my resource requests, I go to Prometheus after a service has been running for a bit of time and look at it&amp;rsquo;s RAM and CPU usage. For example, looking at the following graph, I&amp;rsquo;d say homeassistant should have 1GB for a request and the limit.&lt;/p>
&lt;p>When picking these numbers, consider the workload. Does it have spiky operations where it pulls in a bunch of data, then clears it out later? Make sure to accommodate that usage. Does memory trend up? Maybe you&amp;rsquo;ve got a memory leak or maybe it&amp;rsquo;s just caching more. Many processes will adapt to memory limits and perform garbage collections more frequently (at the cost of more CPU usage) or just reduce it&amp;rsquo;s caching to adapt.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/grafana-ram-usage.png"
width="825"
height="445"
srcset="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/grafana-ram-usage_hu_92fa955f4b3ff15b.png 480w, https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/grafana-ram-usage_hu_21cc74ec0f3d52a3.png 1024w"
loading="lazy"
alt="A screenshot from Grafana showing container RAM usage for several different pods and containers. Home Assistant is using on average 927MiB with a few small spikes of a few tens of MiB."
class="gallery-image"
data-flex-grow="185"
data-flex-basis="444px"
>
Prometheus Query: &lt;code>container_memory_usage_bytes{container!=&amp;quot;POD&amp;quot;,container!=&amp;quot;&amp;quot;}&lt;/code>&lt;/p>
&lt;h3 id="cpu">CPU&lt;/h3>
&lt;p>Let&amp;rsquo;s try to do the same thing with CPU. Starting with the below graph, I can see that homeassistant uses about 7ms of CPU per second and seems to peak at 68ms per second. It&amp;rsquo;s per second because &lt;code>irate&lt;/code> averages the increase over the interval to get the per second rate.
&lt;img src="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/grafana-cpu-usage.png"
width="996"
height="528"
srcset="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/grafana-cpu-usage_hu_2287eef17825f896.png 480w, https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/grafana-cpu-usage_hu_cfe6f91b36f1cd89.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="188"
data-flex-basis="452px"
>
Prometheus Query: &lt;code>irate(container_cpu_usage_seconds_total{container!=&amp;quot;&amp;quot;}[1m])&lt;/code>&lt;/p>
&lt;p>Setting a CPU request is safe because it defines the minimum amount of CPU time reserved for the process. It&amp;rsquo;s measured in number of CPUs, so a request of &lt;code>1&lt;/code> reserves an entire core for the container, a request of &lt;code>2&lt;/code> gives two cores, and a request of &lt;code>256m&lt;/code> gives 1/4 of a core. For this example, I went with &lt;code>resource.requests=256m&lt;/code> so it shares a CPU core with other process.&lt;/p>
&lt;p>However, there&amp;rsquo;s peril with setting a limit. As a turns out &lt;a class="link" href="https://www.brendanlong.com/cpu-utilization-is-a-lie.html" target="_blank" rel="noopener"
>CPU utilization is misleading&lt;/a> and tons of posts that say you &lt;a class="link" href="https://dnastacio.medium.com/why-you-should-keep-using-cpu-limits-on-kubernetes-60c4e50dfc61" target="_blank" rel="noopener"
>should&lt;/a> or &lt;a class="link" href="https://home.robusta.dev/blog/stop-using-cpu-limits" target="_blank" rel="noopener"
>shouldn&amp;rsquo;t&lt;/a> use CPU limits.&lt;/p>
&lt;p>CPU is trickier than memory because CPU usage in Prometheus is measured in milliseconds, but then averaged over seconds or minutes in graphs. So instead, if we change from &lt;code>irate&lt;/code> to &lt;code>increase&lt;/code> which does not average it and shows the pure increase from one minute to the next, it shows that we used an average of 540ms of CPU time per minute.
&lt;img src="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/grafana-cpu-usage-increase.png"
width="979"
height="527"
srcset="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/grafana-cpu-usage-increase_hu_b02a13261472653f.png 480w, https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/grafana-cpu-usage-increase_hu_212497a4de03c996.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="185"
data-flex-basis="445px"
>&lt;/p>
&lt;p>If that were to execute all in a 1 second block, then setting a limit could mean the process is stuck waiting for CPU time when there is idle time available. If you&amp;rsquo;re not sure how the process behaves, it&amp;rsquo;s probably safer not to set a limit.&lt;/p>
&lt;p>The biggest benefit with setting a limit is if you have the limits equal to requests, then Kubernetes will &lt;a class="link" href="https://kubernetes.io/docs/tasks/configure-pod-container/quality-service-pod/#create-a-pod-that-gets-assigned-a-qos-class-of-guaranteed" target="_blank" rel="noopener"
>mark the pod&lt;/a> with the &lt;a class="link" href="https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/#guaranteed" target="_blank" rel="noopener"
>guaranteed QoS&lt;/a> meaning that it&amp;rsquo;s always the last pods to get evicted by Kubernetes or to face the wrath of the kernel.&lt;/p>
&lt;h3 id="defining-them-in-yaml">Defining them in YAML&lt;/h3>
&lt;p>Resource requests and limits are easy to set in YAML:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">homeassistant&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1Gi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">256m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1Gi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Defining resource requests and limits gives the Kubernetes scheduler information on how heavy workloads are and ensures that there&amp;rsquo;s space to schedule them, but at this point it&amp;rsquo;s just first-come-first-serve. The first pod scheduled wins.&lt;/p>
&lt;h3 id="kubectl-command-to-review-current-settings">Kubectl command to review current settings&lt;/h3>
&lt;p>This command is useful for viewing all pods and their assigned requests:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">kubectl get --sort-by&lt;span class="o">=&lt;/span>.spec.priorityClassName --all-namespaces pods -o&lt;span class="o">=&lt;/span>custom-columns&lt;span class="o">=&lt;/span>STATUS:.status.phase,NAME:.metadata.name,NAMESPACE:.metadata.namespace,PRIORITY-CLASS:.spec.priorityClassName,REQUEST-MEM:.spec.containers&lt;span class="o">[&lt;/span>*&lt;span class="o">]&lt;/span>.resources.requests.memory,REQUEST-CPU:.spec.containers&lt;span class="o">[&lt;/span>*&lt;span class="o">]&lt;/span>.resources.requests.cpu --field-selector status.phase&lt;span class="o">=&lt;/span>Running
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="priority-classes">Priority Classes&lt;/h1>
&lt;p>Priority classes then give the Kubernetes scheduler information to know which pods have to be running and which ones aren&amp;rsquo;t. It depends on the information provided by resource requests and limits to know when it has to prioritize. Official documentation can be found &lt;a class="link" href="https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/" target="_blank" rel="noopener"
>here&lt;/a>. Prioritization ensures that my high-priority services, such as DNS, always gets scheduled and my lower-priority services, like my Blender distributed rendering service (&lt;a class="link" href="https://github.com/LogicReinc/LogicReinc.BlendFarm" target="_blank" rel="noopener"
>Blendfarm&lt;/a>) don&amp;rsquo;t kill my DNS.&lt;/p>
&lt;p>Priority classes rank the services into most important to least important. Normally, I can run every single workload I care about, but defining these priority classes ensure that when I do some development work and spin up something to test, my scheduled jobs still run.&lt;/p>
&lt;h2 id="identifying-services">Identifying services&lt;/h2>
&lt;p>The first step is to identify all the services that are critical for functioning. I know that I need pi-hole for my Internet DNS traffic, Home Assistant and my smart home services, and Postgres for data storage.&lt;/p>
&lt;p>But those services have dependencies themselves too. For example, pi-hole won&amp;rsquo;t start without:&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="https://www.tigera.io/project-calico/" target="_blank" rel="noopener"
>Calico&lt;/a> and &lt;a class="link" href="https://github.com/k8snetworkplumbingwg/multus-cni" target="_blank" rel="noopener"
>Multus&lt;/a> to setup networking and expose the service via BGP to my network&lt;/li>
&lt;li>Longhorn to mount the data and config volumes&lt;/li>
&lt;li>CoreDNS to handle DNS resolution for queries to my internal services&lt;/li>
&lt;/ul>
&lt;p>Other soft dependencies include:&lt;/p>
&lt;ul>
&lt;li>ingress-nginx to be able to access the pi-hole UI&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/vouch/vouch-proxy" target="_blank" rel="noopener"
>Vouch-proxy&lt;/a> to handle access control to the UI&lt;/li>
&lt;/ul>
&lt;p>If I capture those dependency in a dependency graph, I end up a diagram like this just for pi-hole.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/pihole-dependency.svg"
loading="lazy"
>&lt;/p>
&lt;p>I start with pi-hole as critical for my network, then go through each dependency and ensure that it&amp;rsquo;s the same priority or higher priority.&lt;/p>
&lt;h2 id="my-list">My List&lt;/h2>
&lt;p>Starting from the lowest level, this is the list I came up with:&lt;/p>
&lt;ul>
&lt;li>System
&lt;ul>
&lt;li>containerd&lt;/li>
&lt;li>ssh&lt;/li>
&lt;li>k3s&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Basic stuff
&lt;ul>
&lt;li>Calico (networking CNI)&lt;/li>
&lt;li>CoreDNS&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Storage
&lt;ul>
&lt;li>Longhorn&lt;/li>
&lt;li>local-path-storage&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Management (system-cluster-critical)
&lt;ul>
&lt;li>cattle-cluster-agent - Needed so I can use the Rancher UI to manage my cluster&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>network-critical
&lt;ul>
&lt;li>pi-hole&lt;/li>
&lt;li>metal-lb&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>Without everything above, I would just consider the entire home network non-functional and would quickly have frustrated family members and guests. That doesn&amp;rsquo;t mean I can&amp;rsquo;t run the next list. Things would still be broken, but at least I can get online to figure out how to fix the other issues.&lt;/p>
&lt;ul>
&lt;li>smarthome-critical
&lt;ul>
&lt;li>Home Assistant&lt;/li>
&lt;li>ZWaveJS&lt;/li>
&lt;li>MQTT&lt;/li>
&lt;li>Other Smart Home services&lt;/li>
&lt;li>Databases (Postgres, InfluxDB)&lt;/li>
&lt;li>Internal DNS Stuff (external-dns + etcd)&lt;/li>
&lt;li>ingress-nginx&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>My computer runs a lot of the automations in my house. Without them, I&amp;rsquo;d be sad, but still my light switches would work. Those services should still get priority over anything else.&lt;/p>
&lt;ul>
&lt;li>smarthome-important
&lt;ul>
&lt;li>Auth (Vouch Proxy)&lt;/li>
&lt;li>Voice Assistants for Smart Home (whisper, piper)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>The smarthome-important category of services are still noticeable when they&amp;rsquo;re offline, but at least things work without them.&lt;/p>
&lt;ul>
&lt;li>other-important - Things that I want to run instead of my random test services, but are not as important as things that actively get used by me on a daily basis
&lt;ul>
&lt;li>Scheduled jobs&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>That led me to this diagram showing the dependencies of services and to think it&amp;rsquo;s not all of my workloads that I have running.
&lt;img src="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/most-services-dependency.png"
width="2031"
height="1437"
srcset="https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/most-services-dependency_hu_de7b8afbb59e8650.png 480w, https://www.technowizardry.net/2025/09/guaranteed-qos-in-my-home-lab/images/most-services-dependency_hu_9af91e1af5bbe3d.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="141"
data-flex-basis="339px"
>&lt;/p>
&lt;h2 id="creating-the-priority-classes">Creating the priority classes&lt;/h2>
&lt;p>Kubernetes comes with a few default classes and Longhorn also brings it&amp;rsquo;s own, so I have to create my own for the lower priorities:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">kubectl get priorityclasses --sort-by&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;.value&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME VALUE GLOBAL-DEFAULT AGE PREEMPTIONPOLICY
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lab-important &lt;span class="m">600000000&lt;/span> &lt;span class="nb">false&lt;/span> 2d PreemptLowerPriority
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">smarthome-important &lt;span class="m">700000000&lt;/span> &lt;span class="nb">false&lt;/span> 2y257d PreemptLowerPriority
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">smarthome-critical &lt;span class="m">800000000&lt;/span> &lt;span class="nb">false&lt;/span> 2y257d PreemptLowerPriority
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">network-critical &lt;span class="m">900000000&lt;/span> &lt;span class="nb">false&lt;/span> 2y257d PreemptLowerPriority
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">longhorn-critical &lt;span class="m">1000000000&lt;/span> &lt;span class="nb">false&lt;/span> 497d PreemptLowerPriority
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">system-cluster-critical &lt;span class="m">2000000000&lt;/span> &lt;span class="nb">false&lt;/span> 3y154d PreemptLowerPriority
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">system-node-critical &lt;span class="m">2000001000&lt;/span> &lt;span class="nb">false&lt;/span> 3y154d PreemptLowerPriority
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Creating a new one is easy and looks like this. Pick a value that&amp;rsquo;s smaller than the higher priority. Kubernetes convention seems to use multiples of 100_000_000 to give space to put your own priorities in.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">scheduling.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Software critical for the minimum functioning of the home network. (e.g. DNS)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PriorityClass&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">network-critical&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">preemptionPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PreemptLowerPriority&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">900000000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="making-changes">Making changes&lt;/h1>
&lt;p>Now that I&amp;rsquo;ve identified categories of services based on how important they, it&amp;rsquo;s time to start tagging and updating workloads and processes. I&amp;rsquo;m starting with the OS and working my way up.&lt;/p>
&lt;h2 id="the-operating-system">The Operating System&lt;/h2>
&lt;p>Kubernetes doesn&amp;rsquo;t know how much space to reserve for the operating system itself. Think the kernel, sshd, systemd, and if you&amp;rsquo;re like me and use my server for a TV, a desktop environment. The kubelet provides a parameter &lt;a class="link" href="https://pwittrock.github.io/docs/tasks/administer-cluster/reserve-compute-resources/#system-reserved" target="_blank" rel="noopener"
>&lt;code>--system-reserved&lt;/code>&lt;/a> to tell Kubernetes to ignore some of the resources when scheduling nodes.&lt;/p>
&lt;p>I&amp;rsquo;ve got a 16 core CPU, so I&amp;rsquo;ll reserve two cores for the host OS and 4GB of RAM. I&amp;rsquo;m using K3s managed by Rancher, so the setting is controlled by the Cluster resource:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">provisioning.cattle.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Cluster&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">adamsnet&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fleet-default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rkeConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">machineSelectorConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kubelet-arg&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">kube-reserved=cpu=2,memory=4Gi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="rancher-management">Rancher Management&lt;/h2>
&lt;h3 id="rancher-system-agent">Rancher System Agent&lt;/h3>
&lt;p>The Rancher system agent is used when you deploy a RKE2 or K3s cluster using the Rancher UI, like I do. It needs some CPU time reserved so that any changes to the cluster itself, like version upgrades, etc.. Management daemons should always get some reserved resources to ensure that they can make corrective actions, if necessary. For the system agent, I use systemd&amp;rsquo;s CPUShares property to give it some time.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">systemctl edit rancher-system-agent.service
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="cl">&lt;span class="k">[Service]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">CPUShares&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">200&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="rancher-cluster-agent">Rancher Cluster Agent&lt;/h3>
&lt;p>Next, the Rancher Cluster Agent is used to proxy Kubernetes calls from the Rancher UI to the downstream cluster. Without this, I&amp;rsquo;d be unable to browse and make corrective actions. Another management component, but this one runs as a Kubernetes Pod. By default, it&amp;rsquo;s marked as &lt;code>system-cluster-critical&lt;/code> which is the second highest priority. No need to change the priority class, just give it some resources:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">provisioning.cattle.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Cluster&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">adamsnet&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fleet-default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">clusterAgentDeploymentCustomization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">overrideResourceRequirements&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">256m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1Gi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="calico">Calico&lt;/h2>
&lt;p>Now we&amp;rsquo;re into Kubernetes workloads that need to be updated using the &lt;code>priorityClassName&lt;/code> field on the PodSpec.&lt;/p>
&lt;p>Calico provides networking for all pods. Without it, pods wouldn&amp;rsquo;t start. This reserves some CPU and memory for them:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">operator.tigera.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Installation&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">calicoNodeDaemonSet&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">calico-node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">200m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">256Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">typhaDeployment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">calico-typha&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">128Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">100m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">64Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">operator.tigera.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">APIServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiServerDeployment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">calico-apiserver&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">30m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">64Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">priorityClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">system-cluster-critical&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="postgres">Postgres&lt;/h2>
&lt;p>I deploy Postgres using &lt;a class="link" href="https://cloudnative-pg.io/" target="_blank" rel="noopener"
>CloudNativePG&lt;/a> which uses a custom resource to provision my Postgres database. Resource requests and priorities can be set like so:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgresql.cnpg.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Cluster&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgres&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">priorityClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cluster-important&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">2Gi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">512m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="other-workloads">Other workloads&lt;/h2>
&lt;p>Anything else is updated directly on the PodSpec&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">homeassistant&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">priorityClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome-critical&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now it&amp;rsquo;s just a matter of working through every single workload including Deployments, DaemonSets, CronJobs, and StatefulSets to mark them with resources and priorities. You can pick whichever order you want, either most important or by the biggest consumer.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>There&amp;rsquo;s a lot that goes into resource management in a Kubernetes cluster when running many different services. Kubernetes depends on the values for priorities and resource requests you provide on the services to decide how to schedule and run pods.&lt;/p>
&lt;ul>
&lt;li>Resource requests tell Kubernetes whether there&amp;rsquo;s space to fit a given workload on the cluster.&lt;/li>
&lt;li>Resource limits define the maximum size of a workload and prevent it from exceeding that.&lt;/li>
&lt;li>Priority classes are used when the resource requests exceed total node space&lt;/li>
&lt;/ul>
&lt;h1 id="references">References&lt;/h1>
&lt;ul>
&lt;li>&lt;a class="link" href="https://medium.com/directeam/kubernetes-resources-under-the-hood-part-1-4f2400b6bb96" target="_blank" rel="noopener"
>https://medium.com/directeam/kubernetes-resources-under-the-hood-part-1-4f2400b6bb96&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://thenewstack.io/how-k8s-eviction-works-resource-management-gone-wrong/" target="_blank" rel="noopener"
>https://thenewstack.io/how-k8s-eviction-works-resource-management-gone-wrong/&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://dnastacio.medium.com/why-you-should-keep-using-cpu-limits-on-kubernetes-60c4e50dfc61" target="_blank" rel="noopener"
>https://dnastacio.medium.com/why-you-should-keep-using-cpu-limits-on-kubernetes-60c4e50dfc61&lt;/a>&lt;/li>
&lt;/ul>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F09%2Fguaranteed-qos-in-my-home-lab%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Guaranteed+Quality+of+Service+in+my+Home+Lab" style="border:0" alt="" /></description></item><item><title>Better Vault for Postgres access in my Home Lab</title><link>https://www.technowizardry.net/2025/09/vault-postgres-home-lab/</link><pubDate>Tue, 09 Sep 2025 22:39:00 +0000</pubDate><guid>https://www.technowizardry.net/2025/09/vault-postgres-home-lab/</guid><summary>&lt;p>In my &lt;a class="link" href="https://www.technowizardry.net/2024/08/vault-for-a-home-lab" >previous post on Vault&lt;/a>, I showed how Hashicorp&amp;rsquo;s Vault can be used to protect important passwords, static passwords that don&amp;rsquo;t change frequently. Vault can do much more than this and can even automatically create temporary accounts and rotate passwords for database users.&lt;/p>
&lt;p>Today, I&amp;rsquo;m using long-lived passwords that I generate once when I add a new service, I, along with most people, just insert those passwords into the environment like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">DATABASE_URL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> postgresql://username:mypassword@postgres:5432/database&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>That&amp;rsquo;s not secure at all. While you can store them in Kubernetes Secrets, they&amp;rsquo;re not encrypted by default. Kubernetes can &lt;a class="link" href="https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/" target="_blank" rel="noopener"
>encrypt secrets&lt;/a>, but they&amp;rsquo;re open to anybody with access to the cluster. The passwords are easily accessible to anybody with access to Kubernetes and are never rotated. This simply won&amp;rsquo;t do. In this post, I&amp;rsquo;m going to walk through how I switch to Vault for&lt;/p></summary><description>&lt;p>In my &lt;a class="link" href="https://www.technowizardry.net/2024/08/vault-for-a-home-lab" >previous post on Vault&lt;/a>, I showed how Hashicorp&amp;rsquo;s Vault can be used to protect important passwords, static passwords that don&amp;rsquo;t change frequently. Vault can do much more than this and can even automatically create temporary accounts and rotate passwords for database users.&lt;/p>
&lt;p>Today, I&amp;rsquo;m using long-lived passwords that I generate once when I add a new service, I, along with most people, just insert those passwords into the environment like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">DATABASE_URL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> postgresql://username:mypassword@postgres:5432/database&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>That&amp;rsquo;s not secure at all. While you can store them in Kubernetes Secrets, they&amp;rsquo;re not encrypted by default. Kubernetes can &lt;a class="link" href="https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/" target="_blank" rel="noopener"
>encrypt secrets&lt;/a>, but they&amp;rsquo;re open to anybody with access to the cluster. The passwords are easily accessible to anybody with access to Kubernetes and are never rotated. This simply won&amp;rsquo;t do. In this post, I&amp;rsquo;m going to walk through how I switch to Vault for&lt;/p>
&lt;h1 id="postgres">Postgres&lt;/h1>
&lt;p>Vault natively supports Postgres and can either maintain the password for a single Postgres role and rotate the password periodically (called static), or generate a new role for every single Pod that runs (dynamic.) With static, I have to go into DataGrip and create a role and setup the permissions manually, but dynamically should be able to create it all for me.&lt;/p>
&lt;p>If you haven&amp;rsquo;t already created a Database backend, create one (&lt;a class="link" href="https://developer.hashicorp.com/vault/docs/secrets/databases" target="_blank" rel="noopener"
>Vault docs&lt;/a>) using the UI or the API. Add a connection to your database. Make sure to use a privileged user with the ability to create users. I just use the &lt;code>postgres&lt;/code> super user account.&lt;/p>
&lt;h2 id="i-cant-figure-out-dynamic-roles">I can&amp;rsquo;t figure out dynamic roles&lt;/h2>
&lt;p>Dynamic roles in postgres were confusing. I couldn&amp;rsquo;t figure out how to grant privileges in a given database. I was able to create my user with the following creation statements, but no matter what I tried it&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-postgresql" data-lang="postgresql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">create&lt;/span> &lt;span class="k">user&lt;/span> &lt;span class="s">&amp;#34;{{name}}&amp;#34;&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="k">encrypted&lt;/span> &lt;span class="k">password&lt;/span> &lt;span class="s1">&amp;#39;{{password}}&amp;#39;&lt;/span> &lt;span class="k">valid&lt;/span> &lt;span class="k">until&lt;/span> &lt;span class="s1">&amp;#39;{{expiration}}&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">grant&lt;/span> &lt;span class="n">connect&lt;/span> &lt;span class="k">on&lt;/span> &lt;span class="k">database&lt;/span> &lt;span class="n">openwebui&lt;/span> &lt;span class="k">to&lt;/span> &lt;span class="s">&amp;#34;{{name}}&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">GRANT&lt;/span> &lt;span class="k">SELECT&lt;/span> &lt;span class="k">ON&lt;/span> &lt;span class="k">ALL&lt;/span> &lt;span class="k">TABLES&lt;/span> &lt;span class="k">IN&lt;/span> &lt;span class="k">SCHEMA&lt;/span> &lt;span class="n">public&lt;/span> &lt;span class="k">TO&lt;/span> &lt;span class="s">&amp;#34;{{name}}&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">grant&lt;/span> &lt;span class="k">all&lt;/span> &lt;span class="k">on&lt;/span> &lt;span class="k">database&lt;/span> &lt;span class="n">openwebui&lt;/span> &lt;span class="k">to&lt;/span> &lt;span class="s">&amp;#34;{{name}}&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>It would grant CONNECT to the db:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/09/vault-postgres-home-lab/data-grip.png"
width="985"
height="992"
srcset="https://www.technowizardry.net/2025/09/vault-postgres-home-lab/data-grip_hu_ed0b7eb46fa74197.png 480w, https://www.technowizardry.net/2025/09/vault-postgres-home-lab/data-grip_hu_32cc14cd3f551341.png 1024w"
loading="lazy"
alt="A screenshot from JetBrains DataGrip tool showing the dynamically created role. It has a random name, is marked as can login in, and a expiration date 1 day later. It has one grant for CONNECT, USAGE, and TEMPORARY for the database, but no access to the schema"
class="gallery-image"
data-flex-grow="99"
data-flex-basis="238px"
>&lt;/p>
&lt;p>But this was insufficient to actually connect to the database and create/edit tables. Apparently Postgres configures the database at the connection level and does not allow me to grant this.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-zed" data-lang="zed">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="err">2025&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="err">09&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="err">08&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">22&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="err">28&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="err">17&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">openwebui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">public&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">select&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">from&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">chat&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">2025&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="err">09&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="err">08&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">22&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="err">28&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="err">17&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">42501&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ERROR&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">permission&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">denied&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">for&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">table&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">chat&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="use-a-role">Use a role&lt;/h2>
&lt;p>Looks like I can&amp;rsquo;t avoid pre-configuring the roles with Postgres. First, create a new role and grant it all appropriate privileges on the database:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-postgresql" data-lang="postgresql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- On openwebui.public
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">create&lt;/span> &lt;span class="k">role&lt;/span> &lt;span class="s">&amp;#34;openwebui-role&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">grant&lt;/span> &lt;span class="n">connect&lt;/span> &lt;span class="k">on&lt;/span> &lt;span class="k">database&lt;/span> &lt;span class="n">openwebui&lt;/span> &lt;span class="k">to&lt;/span> &lt;span class="s">&amp;#34;openwebui-role&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">grant&lt;/span> &lt;span class="k">create&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">usage&lt;/span> &lt;span class="k">ON&lt;/span> &lt;span class="k">schema&lt;/span> &lt;span class="n">public&lt;/span> &lt;span class="k">to&lt;/span> &lt;span class="s">&amp;#34;openwebui-role&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">grant&lt;/span> &lt;span class="k">all&lt;/span> &lt;span class="k">privileges&lt;/span> &lt;span class="k">on&lt;/span> &lt;span class="k">all&lt;/span> &lt;span class="k">tables&lt;/span> &lt;span class="k">in&lt;/span> &lt;span class="k">schema&lt;/span> &lt;span class="n">public&lt;/span> &lt;span class="k">TO&lt;/span> &lt;span class="s">&amp;#34;openwebui-role&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="bring-it-all-together">Bring it all together&lt;/h2>
&lt;p>Then create a Vault dynamic user like so:&lt;/p>
&lt;ul>
&lt;li>Role Name: Whatever you want or &lt;strong>namespace/serviceaccount&lt;/strong>. This naming convention will be important if you&amp;rsquo;re following my guide&lt;/li>
&lt;li>Connection Name: Your PGSQL connection&lt;/li>
&lt;li>Type of Role: dynamic&lt;/li>
&lt;li>Creation Statements&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-postgresql" data-lang="postgresql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">create&lt;/span> &lt;span class="k">user&lt;/span> &lt;span class="s">&amp;#34;{{name}}&amp;#34;&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="k">encrypted&lt;/span> &lt;span class="k">password&lt;/span> &lt;span class="s1">&amp;#39;{{password}}&amp;#39;&lt;/span> &lt;span class="k">in&lt;/span> &lt;span class="k">role&lt;/span> &lt;span class="s">&amp;#34;openwebui-role&amp;#34;&lt;/span> &lt;span class="k">valid&lt;/span> &lt;span class="k">until&lt;/span> &lt;span class="s1">&amp;#39;{{expiration}}&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">alter&lt;/span> &lt;span class="k">user&lt;/span> &lt;span class="s">&amp;#34;{{name}}&amp;#34;&lt;/span> &lt;span class="k">set&lt;/span> &lt;span class="k">role&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">&amp;#34;openwebui-role&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now, if I click &amp;ldquo;Generate credentials&amp;rdquo; in Vault, I should get credentials that I login using DataGroup and view/edit table table.&lt;/p>
&lt;h1 id="vault">Vault&lt;/h1>
&lt;h2 id="create-the-vault-role">Create the Vault role&lt;/h2>
&lt;p>So far, we&amp;rsquo;ve just created a secret that Vault can return, but our Kubernetes services can&amp;rsquo;t access these credentials, so we need to create a policy that allows a Vault user to access this Vault secret.&lt;/p>
&lt;p>My previous approach of creating a new Kubernetes role and custom policy for every single Kubernetes service was become onerous. I want the ability to be able to add a new service to Vault without having to go through this work. If only services could automatically get access to their own secrets without being able to see other secrets. It&amp;rsquo;s possible with templated policies.&lt;/p>
&lt;p>First, let&amp;rsquo;s create a common role in the Kubernetes authentication method that every pod can assume.&lt;/p>
&lt;ul>
&lt;li>Authentication Method: Kubernetes&lt;/li>
&lt;li>Name: &lt;strong>k8srole&lt;/strong>&lt;/li>
&lt;li>Alias name source: &lt;strong>serviceaccount_name&lt;/strong>&lt;/li>
&lt;li>Bound service account names: &lt;strong>*&lt;/strong>&lt;/li>
&lt;li>Bound service account namespaces: &lt;strong>*&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>NOTE: Don&amp;rsquo;t assign any privileged policies because any service will be able to assume this role. If you want to be more careful, explicitly list the namespaces that run services that should have access in the role.&lt;/p>
&lt;h2 id="create-the-vault-policy">Create the Vault Policy&lt;/h2>
&lt;p>When I first started, I didn&amp;rsquo;t fully understand how &lt;a class="link" href="https://developer.hashicorp.com/vault/docs/concepts/policies#templated-policies" target="_blank" rel="noopener"
>templated policies in Vault&lt;/a> worked. There were these parameters that I could use, like &lt;code>identity.entity.id&lt;/code>, but it&amp;rsquo;s not clear how that looks in Kubernetes.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">path &amp;#34;database/creds/???&amp;#34; {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;read&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>To figure this out, I launched a pod with this Vault approle, and pulled the token out of &lt;code>/vault/secrets/token&lt;/code>. Then used curl to call the API.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">curl -H &lt;span class="s1">&amp;#39;X-Vault-Token: hvs.CAE[...]&amp;#39;&lt;/span> &lt;span class="s1">&amp;#39;https://vault.example.com/v1/auth/token/lookup-self&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>That returned:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;request_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;df26cb1d-8ca1-b53a-be17-1c96c12dd8c2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;lease_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;renewable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;lease_duration&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;data&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;accessor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;jKE4BZeUR3N03qZMgsQJUtDA&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;creation_time&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1729211189&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;creation_ttl&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3600&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;display_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;kubernetes-default-my-service-account&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;entity_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2a22a75d-be11-a77f-9ac4-8fe3cd16f4ee&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;expire_time&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2024-10-18T01:26:29.249283537Z&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;explicit_max_ttl&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;hvs.CAE[...]&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;issue_time&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2024-10-18T00:26:29.248036782Z&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;last_renewal&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2024-10-18T00:26:29.249283637Z&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;last_renewal_time&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1729211189&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;meta&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">     &lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;test&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">     &lt;span class="nt">&amp;#34;service_account_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;my-service-account&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">     &lt;span class="nt">&amp;#34;service_account_namespace&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;default&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">     &lt;span class="nt">&amp;#34;service_account_secret_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">     &lt;span class="nt">&amp;#34;service_account_uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;61c1afa0-dfa4-4c9f-8eec-7e5ebe0b6904&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;num_uses&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;orphan&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;path&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;auth/kubernetes/login&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;period&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3600&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;policies&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;renewable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;ttl&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3503&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;service&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;wrap_info&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;warnings&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;auth&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then I fetched more details using the &lt;code>data.entity_id&lt;/code> parameter:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">curl -H &lt;span class="s1">&amp;#39;X-Vault-Token: hvs.CAE[...]&amp;#39;&lt;/span> https://vault.example.com/v1/identity/entity/id/2a22a75d-be11-a77f-9ac4-8fe3cd16f4ee &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;request_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;63e86ba7-39c3-66af-8048-a8c130fe20ab&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;lease_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;renewable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;lease_duration&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;data&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;aliases&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">     &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">       &lt;span class="nt">&amp;#34;canonical_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2a22a75d-be11-a77f-9ac4-8fe3cd16f4ee&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">       &lt;span class="nt">&amp;#34;creation_time&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2023-11-18T05:27:32.523910476Z&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">       &lt;span class="nt">&amp;#34;custom_metadata&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">       &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;88853d50-7780-998b-fb78-c8141ded6a63&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">       &lt;span class="nt">&amp;#34;last_update_time&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2023-11-18T05:27:32.523910476Z&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">       &lt;span class="nt">&amp;#34;local&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">       &lt;span class="nt">&amp;#34;merged_from_canonical_ids&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">       &lt;span class="nt">&amp;#34;metadata&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">         &lt;span class="nt">&amp;#34;service_account_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;my-service-account&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">         &lt;span class="nt">&amp;#34;service_account_namespace&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;default&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">         &lt;span class="nt">&amp;#34;service_account_secret_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">         &lt;span class="nt">&amp;#34;service_account_uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;61c1afa0-dfa4-4c9f-8eec-7e5ebe0b6904&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">       &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">       &lt;span class="nt">&amp;#34;mount_accessor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;auth_kubernetes_398dc4e7&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">       &lt;span class="nt">&amp;#34;mount_path&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;auth/kubernetes/&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">       &lt;span class="nt">&amp;#34;mount_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;kubernetes&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">       &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;61c1afa0-dfa4-4c9f-8eec-7e5ebe0b6904&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">     &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;creation_time&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2023-11-18T05:27:32.523902982Z&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;direct_group_ids&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;disabled&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;group_ids&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2a22a75d-be11-a77f-9ac4-8fe3cd16f4ee&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;inherited_group_ids&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;last_update_time&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2023-11-18T05:27:32.523902982Z&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;merged_entity_ids&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;metadata&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;entity_0daf7612&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;namespace_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;root&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   &lt;span class="nt">&amp;#34;policies&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;wrap_info&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;warnings&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;auth&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Say I want to limit my policy to be able to access secrets named &lt;code>{{namespace}}|{{service_account_name}}&lt;/code>, I use these parameters:&lt;/p>
&lt;ul>
&lt;li>&lt;code>{[namespace}}&lt;/code> = &lt;code>{{identity.entity.aliases.auth_kubernetes_398dc4e7.metadata.service_account_namespace}}&lt;/code>. The &lt;code>auth_kubernetes_398dc4e7&lt;/code> is the name of the auth method specified in &lt;code>mount_accessor&lt;/code>&lt;/li>
&lt;li>&lt;code>{{service_account_name&lt;/code> = &lt;code>{{identity.entity.aliases.auth_kubernetes_398dc4e7.metadata.service_account_name}}&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="final-policy">Final Policy&lt;/h3>
&lt;p>Assuming I have a database storage end called &lt;code>database&lt;/code>, I can grant access by creating a policy:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># Dynamic users
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">path &amp;#34;database/creds/{{identity.entity.aliases.auth_kubernetes_398dc4e7.metadata.service_account_namespace}}|{{identity.entity.aliases.auth_kubernetes_398dc4e7.metadata.service_account_name}}&amp;#34; {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;read&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> allowed_parameters = {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;password&amp;#34; = []
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># Static users
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">path &amp;#34;database/static-creds/{{identity.entity.aliases.auth_kubernetes_398dc4e7.metadata.service_account_namespace}}|{{identity.entity.aliases.auth_kubernetes_398dc4e7.metadata.service_account_name}}&amp;#34; {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;read&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> allowed_parameters = {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;password&amp;#34; = []
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And if I want to grant read-only access to secrets under &lt;code>by-service/{{namespace}}/{{service_account}}/*&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">path &amp;#34;by-service/data/{{identity.entity.aliases.auth_kubernetes_398dc4e7.metadata.service_account_namespace}}/{{identity.entity.aliases.auth_kubernetes_398dc4e7.metadata.service_account_name}}/*&amp;#34; {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;read&amp;#34;, &amp;#34;list:]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>NOTE: Your auth provider name will be different than &lt;code>auth_kubernetes_398dc4e7&lt;/code>. Make sure you update it.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/09/vault-postgres-home-lab/vault-auth-name.png"
width="348"
height="204"
srcset="https://www.technowizardry.net/2025/09/vault-postgres-home-lab/vault-auth-name_hu_d3c580d2ec515f10.png 480w, https://www.technowizardry.net/2025/09/vault-postgres-home-lab/vault-auth-name_hu_ae42ca4df793a9d2.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="170"
data-flex-basis="409px"
>&lt;/p>
&lt;p>Create a policy with the above directives and name it something like &lt;code>k8s-default-policy&lt;/code>. The go back to the Kubernetes role &lt;code>k8srole&lt;/code> and add the policy under Generated Token&amp;rsquo;s Policies.&lt;/p>
&lt;h1 id="using-it-in-an-app">Using it in an app&lt;/h1>
&lt;p>How you get an app to use the Vault credentials will differ depending on the app itself.&lt;/p>
&lt;h2 id="using-environment-variables">Using environment variables&lt;/h2>
&lt;p>For applications that take the password using an environment variable and can&amp;rsquo;t use a file, I use Vault&amp;rsquo;s templating system to generate a file called &lt;code>/vault/secrets/env&lt;/code>, then source it and invoke the process as normal.&lt;/p>
&lt;p>For example, with OpenWebUI, I would do:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">vault.hashicorp.com/agent-inject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">vault.hashicorp.com/role&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">k8srole&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">vault.hashicorp.com/agent-inject-template-env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{- with secret &amp;#34;database/creds/openwebui&amp;#34; -}}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> export DATABASE_URL=postgres://{{ .Data.username }}:{{ .Data.password
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }}@postgres.datastore.svc.cluster.local.:5432/openwebui?sslmode=disable
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{- end }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/bin/sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;-c&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>-&lt;span class="l">f /vault/secrets/env ] &amp;amp;&amp;amp; . /vault/secrets/env &amp;amp;&amp;amp; /app/backend/start.sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This generates a file that looks like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">DATABASE_URL&lt;/span>&lt;span class="o">=&lt;/span>postgres://v-kubernet-openwebu-&lt;span class="o">[&lt;/span>...&lt;span class="o">]&lt;/span>:&lt;span class="o">[&lt;/span>password&lt;span class="o">]&lt;/span>@postgres.datastore.svc.cluster.local.:5432/openwebui?sslmode&lt;span class="o">=&lt;/span>disable
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="using-files">Using files&lt;/h2>
&lt;p>Files are the easiest way. For example, Authelia can be configured using:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">vault.hashicorp.com/agent-inject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">vault.hashicorp.com/role&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">k8srole&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">vault.hashicorp.com/agent-inject-template-db-password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{- with secret &amp;#34;database/static-creds/authelia&amp;#34; -}}{{ .Data.password
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }}{{- end -}}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/vault/secrets/db-password&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>Thus far, I&amp;rsquo;ve create a reusable Vault role and Vault policy that that any Kubernetes pod with a service account can assume and login to Vault. It will only have access to secrets that match it&amp;rsquo;s name. Now it&amp;rsquo;s easy to onboard new services and grant secure access.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F09%2Fvault-postgres-home-lab%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Better+Vault+for+Postgres+access+in+my+Home+Lab" style="border:0" alt="" /></description></item><item><title>Git pushes can be surprising</title><link>https://www.technowizardry.net/2025/08/git-pushes-can-be-surprising/</link><pubDate>Sat, 30 Aug 2025 23:28:00 +0000</pubDate><guid>https://www.technowizardry.net/2025/08/git-pushes-can-be-surprising/</guid><summary>&lt;p>I was recently working on an open source project (&lt;a class="link" href="https://github.com/tryfi/hass-tryfi" target="_blank" rel="noopener"
>tryfi/hass-tryfi&lt;/a> - A Home Assistant integration for pulling data from my dog&amp;rsquo;s collar using the &lt;a class="link" href="https://shop.tryfi.com/r/4F68HH/?utm_source=referrals&amp;amp;type=dog" target="_blank" rel="noopener"
>TryFi&lt;/a> API and I found out that Git pushes can behave in a surprising way after I accidentally pushed a bunch of testing commits to the wrong branch.&lt;/p></summary><description>&lt;p>I was recently working on an open source project (&lt;a class="link" href="https://github.com/tryfi/hass-tryfi" target="_blank" rel="noopener"
>tryfi/hass-tryfi&lt;/a> - A Home Assistant integration for pulling data from my dog&amp;rsquo;s collar using the &lt;a class="link" href="https://shop.tryfi.com/r/4F68HH/?utm_source=referrals&amp;amp;type=dog" target="_blank" rel="noopener"
>TryFi&lt;/a> API and I found out that Git pushes can behave in a surprising way after I accidentally pushed a bunch of testing commits to the wrong branch.&lt;/p>
&lt;h1 id="background">Background&lt;/h1>
&lt;p>In my workspace, I had two different remotes. One that tracked my own testing repo and one that was the official repo:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ git remote -v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">official    https://github.com/tryfi/hass-tryfi.git (fetch)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">official    git@github.com:tryfi/hass-tryfi.git (push)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">origin   https://github.com/ajacques/hass-tryfi.git (fetch)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">origin   git@github.com:ajacques/hass-tryfi.git (push)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I had two branches: &lt;code>master&lt;/code> (upstream to origin/master) and &lt;code>official-master&lt;/code> (upstream to officialf/master). This enabled me to do testing work on my own repo and push it and when I thought it was ready, merge into the official repo. Yes these names are confusing. I&amp;rsquo;ll fix it later.&lt;/p>
&lt;h1 id="what-was-the-surprise">What was the surprise?&lt;/h1>
&lt;p>All hell broke loose when I was on my &lt;code>main-master&lt;/code> branch and wanted to push some small commits to the &lt;code>tryfi/hass-tryfi&lt;/code> repo:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">git push official master
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>What I assumed would happen is that it would take my &lt;code>main-master&lt;/code> branch commits and push them to &lt;code>main/master&lt;/code>. That would make sense wouldn&amp;rsquo;t it? Turns out, that&amp;rsquo;s not what happened. It took all the juicy, barely tested commits on my &lt;code>master&lt;/code> branch of the same name and pushed them to &lt;code>main/master&lt;/code>.&lt;/p>
&lt;h1 id="git-config">Git Config&lt;/h1>
&lt;p>After cleaning up my mess, I then tried to figure out what I did wrong. Looking at the Git docs shows an interesting git config option: &lt;a class="link" href="https://git-scm.com/docs/git-config#Documentation/git-config.txt-pushdefault" target="_blank" rel="noopener"
>&lt;code>push.default&lt;/code>&lt;/a> that &amp;ldquo;Defines the action &lt;code>git&lt;/code> &lt;code>push&lt;/code> should take if no refspec is given (whether from the command-line, config, or elsewhere).&amp;rdquo; It defines the following options:&lt;/p>
&lt;ul>
&lt;li>&lt;code>nothing&lt;/code> - do not push anything (error out) unless a refspec is given. This is primarily meant for people who want to avoid mistakes by always being explicit.&lt;/li>
&lt;li>&lt;code>current&lt;/code> - push the current branch to update a branch with the same name on the receiving end. Works in both central and non-central workflows.&lt;/li>
&lt;li>&lt;code>upstream&lt;/code> - push the current branch back to the branch whose changes are usually integrated into the current branch (which is called &lt;code>@{upstream}&lt;/code>). This mode only makes sense if you are pushing to the same repository you would normally pull from (i.e. central workflow).&lt;/li>
&lt;li>&lt;code>tracking&lt;/code> - This is a deprecated synonym for &lt;code>upstream&lt;/code>.&lt;/li>
&lt;li>&lt;code>simple&lt;/code> - &lt;strong>DEFAULT&lt;/strong> - push the current branch with the same name on the remote.&lt;/li>
&lt;li>&lt;code>matching&lt;/code> - push all branches having the same name on both ends. This makes the repository you are pushing to remember the set of branches that will be pushed out (e.g. if you always push &lt;em>maint&lt;/em> and &lt;em>master&lt;/em> there and no other branches, the repository you push to will have these two branches, and your local &lt;em>maint&lt;/em> and &lt;em>master&lt;/em> will be pushed there).&lt;/li>
&lt;/ul>
&lt;p>So I was using the default of &lt;code>simple&lt;/code> meaning that it pushes the branch with the same name as the remote. Since I pushed to master, it pushed from my master to &lt;code>official/master&lt;/code>.&lt;/p>
&lt;p>In a way, I could see how some people might expect this behavior. In this situation, it was unexpected.&lt;/p>
&lt;p>Instead, I want to change to use &lt;code>upstream&lt;/code> to always push to the upstream branch. This can be changed with:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Set for only the current repo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config &lt;span class="nb">set&lt;/span> --local push.default upstream
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Set for all repos&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config &lt;span class="nb">set&lt;/span> --global push.default upstream
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F08%2Fgit-pushes-can-be-surprising%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Git+pushes+can+be+surprising" style="border:0" alt="" /></description></item><item><title>A COE on why technowizardry.net went down</title><link>https://www.technowizardry.net/2025/08/tw-went-down/</link><pubDate>Wed, 20 Aug 2025 21:23:00 +0000</pubDate><guid>https://www.technowizardry.net/2025/08/tw-went-down/</guid><summary>&lt;p>COE = Correction of Error&lt;/p>
&lt;p>My previous employer, Amazon, was a big proponent of doing blameless analysis of outages and figuring out what could be done to fix it. I recently had an outage on my servers and wanted to share what went wrong and the fix.&lt;/p>
&lt;h1 id="summary">Summary&lt;/h1>
&lt;p>Starting Thursday until Friday, all TLS requests to a *.technowizardry.net domain would have failed due to a TLS certificate expiration error. Then on Friday, all DNS queries to a *.technowizardry.net zone failed which also caused mail delivery to fail too. This happened because cert-manager had created the &lt;em>acme-challenge&lt;/em> TXT record, but the record was not visible to the Internet because the &lt;a class="link" href="https://dns.he.net" target="_blank" rel="noopener"
>HE DNS&lt;/a> was failing to perform an AXFR Zone Transfer from my authoritative DNS server. This was because PowerDNS was unable to bind to port :53 because &lt;a class="link" href="https://www.freedesktop.org/software/systemd/man/251/systemd-resolved.html" target="_blank" rel="noopener"
>systemd-resolved&lt;/a> was already listening on that port.&lt;/p></summary><description>&lt;p>COE = Correction of Error&lt;/p>
&lt;p>My previous employer, Amazon, was a big proponent of doing blameless analysis of outages and figuring out what could be done to fix it. I recently had an outage on my servers and wanted to share what went wrong and the fix.&lt;/p>
&lt;h1 id="summary">Summary&lt;/h1>
&lt;p>Starting Thursday until Friday, all TLS requests to a *.technowizardry.net domain would have failed due to a TLS certificate expiration error. Then on Friday, all DNS queries to a *.technowizardry.net zone failed which also caused mail delivery to fail too. This happened because cert-manager had created the &lt;em>acme-challenge&lt;/em> TXT record, but the record was not visible to the Internet because the &lt;a class="link" href="https://dns.he.net" target="_blank" rel="noopener"
>HE DNS&lt;/a> was failing to perform an AXFR Zone Transfer from my authoritative DNS server. This was because PowerDNS was unable to bind to port :53 because &lt;a class="link" href="https://www.freedesktop.org/software/systemd/man/251/systemd-resolved.html" target="_blank" rel="noopener"
>systemd-resolved&lt;/a> was already listening on that port.&lt;/p>
&lt;p>While trying to fix the issue, I temporarily deleting the entire zone in Hurricane Electric which then triggered an issue how PowerDNS handles ALIAS records that blocked further AXFR transfers from succeeding. This required manual effort to unblock the transfer to let the zone start working again and allowed the TLS certificate to be renewed.&lt;/p>
&lt;p>Additionally, &lt;a class="link" href="https://dovecot.org/" target="_blank" rel="noopener"
>Dovecot&lt;/a>, which handles mail storage, failed to start after being rebooted because a Kubernetes sidecar container couldn&amp;rsquo;t start because of a &amp;ldquo;too many open files&amp;rdquo; error while setting up an inotify (to listen for file changes) because of a low ulimit.&lt;/p>
&lt;h1 id="background">Background&lt;/h1>
&lt;h2 id="dns">DNS&lt;/h2>
&lt;p>On each of my three servers, I run the &lt;a class="link" href="https://www.powerdns.com/" target="_blank" rel="noopener"
>PowerDNS authoritative server&lt;/a> storing zone records in MySQL. I use cert-manager to create and renew TLS certificates from &lt;a class="link" href="https://letsencrypt.org/" target="_blank" rel="noopener"
>Let&amp;rsquo;s Encrypt&lt;/a> in my Kubernetes cluster. Since I use wildcard certificates, Let&amp;rsquo;s Encrypt requires me to use the &lt;a class="link" href="https://cert-manager.io/docs/configuration/acme/dns01/" target="_blank" rel="noopener"
>DNS verification method&lt;/a> which means cert-manager has to have permissions to create and modify TXT records. It creates records in PowerDNS using the API.&lt;/p>
&lt;p>I have three dedicated servers running my Kubernetes cluster. Three is chosen to ensure that if one machine is offline, I can continue to serve critical services, like this blog. However, they are located geographically all in the north-east area of the US and Canada. For performance, I wanted to distribute out the DNS servers so they&amp;rsquo;re faster. Thus, I adopted Hurricane Electric&amp;rsquo;s DNS service to serve as authoritative DNS servers. These servers automatically perform what&amp;rsquo;s called an &lt;a class="link" href="https://en.wikipedia.org/wiki/DNS_zone_transfer" target="_blank" rel="noopener"
>DNS Zone Transfer&lt;/a> or an AXFR from my servers to their servers to keep in sync.&lt;/p>
&lt;p>When cert-manager updates a record using the API, PowerDNS automatically updates the SOA record to tell other secondary resolvers that there&amp;rsquo;s an update. As of now, my SOA record looks like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ dig +short SOA technowizardry.net
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ns1.he.net. contact.technowizardry.net. 2025082101 3600 600 259200 60
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ns1.he.net - Primary NS server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">contact.technowizardry.net - A contact email
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2025082101 - Serial number. Translates to date: &amp;#34;2025-08-21&amp;#34; version: &amp;#34;01&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">3600 - The refresh time, how often secondary resolvers should try to refresh in seconds (1 hour)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">600 - Time between retries
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">259200 - Expiration time. After 7 days, secondaries stop returning cached records
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">60 - Minimum TTL for the SOA
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>HE DNS checks for any changes to the zone serial number on my server, and if mine is newer, it pulls the newest version.&lt;/p>
&lt;h2 id="mail">Mail&lt;/h2>
&lt;p>I use &lt;a class="link" href="https://dovecot.org/" target="_blank" rel="noopener"
>Dovecot&lt;/a> to store email and implement IMAP/POP3. This is running in Kubernetes and is exposed as a Kubernetes service. It&amp;rsquo;s not configured in a Highly Available mode and only has one instance running because I never spent the time to figure out Dovecot HA. I then expose the port using ingress-nginx&amp;rsquo;s mechanism to &lt;a class="link" href="https://kubernetes.github.io/ingress-nginx/user-guide/exposing-tcp-udp-services/" target="_blank" rel="noopener"
>expose TCP services&lt;/a>.&lt;/p>
&lt;p>Running as a side-car, I have a config-reloader container which runs quietly and listens for changes in the ConfigMap configuration files or the SSL certificate and if a change is detected, it sends a SIGHUP to Dovecot to reload the configuration. This allows me to avoid having to fully delete and restart the service to make config changes and automatically handle SSL certificate renewals. The deployment looks like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dovecot&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mail&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;-c&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/etc/dovecot/dovecot.conf&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;-F&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/usr/sbin/dovecot&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ajacques/kube-mail:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dovecot&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">64Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/etc/dovecot/conf.d&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readOnly&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CONFIG_DIR&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/etc/dovecot/conf.d,/etc/dovecot/ssl/tls-combined.pem&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PROCESS_NAME&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dovecot&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ajacques/config-reloader-sidecar:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">imagePullPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">IfNotPresent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config-reloader-sidecar-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">8Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">8Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">securityContext&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowPrivilegeEscalation&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">capabilities&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">add&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">KILL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">drop&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">ALL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">privileged&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readOnlyRootFilesystem&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runAsNonRoot&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/etc/dovecot/conf.d&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readOnly&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/etc/dovecot/ssl&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mailcert&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readOnly&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">configMap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">defaultMode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">292&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dovecot&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">optional&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mailcert&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secret&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">defaultMode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">292&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">optional&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">technowizardry-wildcard&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="investigation">Investigation&lt;/h1>
&lt;h2 id="dns-1">DNS&lt;/h2>
&lt;p>I was on vacation with limited access to the Internet so it took a lot longer than it should have to figure it out and fix the problem.&lt;/p>
&lt;p>First thing to check is server logs. cert-manager showed:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">E0815 11:23:44.790373       1 sync.go:190] &amp;#34;propagation check failed&amp;#34; err=&amp;#34;DNS record for \&amp;#34;technowizardry.net\&amp;#34; not yet propagated&amp;#34; logger=&amp;#34;cert-manager.challenges&amp;#34; resource_name=&amp;#34;technowizardry-prod-18-1227467484-534542642&amp;#34; resource_namespace=&amp;#34;technowizardry&amp;#34; resource_kind=&amp;#34;Challenge&amp;#34; resource_version=&amp;#34;v1&amp;#34; dnsName=&amp;#34;technowizardry.net&amp;#34; type=&amp;#34;DNS-01&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The PowerDNS pods showed:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Aug 14 18:06:18 Guardian is launching an instance
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Aug 14 18:06:18 Unable to bind UDP socket to &amp;#39;0.0.0.0:53&amp;#39;: Address already in use
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Aug 14 18:06:18 Fatal error: Unable to bind to UDP socket
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Aug 14 18:06:19 Our pdns instance exited with code 1, respawning
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Aug 14 18:06:20 Guardian is launching an instance
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Aug 14 18:06:20 Unable to bind UDP socket to &amp;#39;0.0.0.0:53&amp;#39;: Address already in use
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Aug 14 18:06:20 Fatal error: Unable to bind to UDP socket
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Comparing SOA records shows a difference. HE is not pulling updates.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">dig +short SOA @srv5.technowizardry.net technowizardry.net | awk &amp;#39;{ print $3 }&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2025081401
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">dig +short SOA @srv5.technowizardry.net technowizardry.net | awk &amp;#39;{ print $3 }&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2025062601
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Without SSH access and using an eSIM with a very limited data cap, I can&amp;rsquo;t investigate what&amp;rsquo;s listening, but it&amp;rsquo;s definitely the systemd-resolved. In the mean time, I use Hurricane Electric&amp;rsquo;s website to change from a secondary DNS zone type to a primary zone which means I can edit the records in the website and skip AXFR. A day later I had full Internet access and could get to work. I wanted to switch back to using the DNS Zone transfer mechanism and have Hurricane Electric pull from my PowerDNS, but they kept failing to pull my server. I couldn&amp;rsquo;t figure out why because the server was responding to some queries.&lt;/p>
&lt;p>Manually trying to pull the AXFR zone didn&amp;rsquo;t succeed and gave no error messages back. I use TSIG to authenticate transfers. I double and triple-checked that it was correct.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">$ dig -t AXFR -y hmac-sha512:&lt;span class="o">[&lt;/span>tsigkeyname&lt;span class="o">]&lt;/span>:&lt;span class="o">[&lt;/span>tsigkey&lt;span class="o">]&lt;/span>@144.217.181.222 technowizardry.net
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The server logs gave no error messages as to why. In the &lt;code>pdns.conf&lt;/code>, I changed the logging level from 3 to 5 to increase logging messages.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># pdns.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># loglevel Amount of logging. Higher is more. Do not set below 3
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">loglevel=5
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then retried and got this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Aug 15 11:02:02 AXFR-out zone &amp;#39;technowizardry.net&amp;#39;, client &amp;#39;217.138.75.171&amp;#39;, error resolving for ALIAS ingress-nginx.technowizardry.net., aborting AXFR
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Ah ha. I&amp;rsquo;m using an &lt;a class="link" href="https://support.dnsimple.com/articles/alias-record/" target="_blank" rel="noopener"
>ALIAS record type&lt;/a> on apex record, &lt;code>technowizardry.net&lt;/code> because I can&amp;rsquo;t use a CNAME record (CNAME tells resolvers to go look at another record to find the value) CNAMEs can&amp;rsquo;t be used on the root domain because of &amp;ldquo;reasons&amp;rdquo; that are boring, legacy, and unfortunate. I use these records because I point everything to a common record &lt;code>ingress-nginx.technowizardry.net&lt;/code> which includes all three servers. As I do maintenance, I take them out and put them back in that single record.&lt;/p>
&lt;p>Unfortunately, it seems that even though &lt;code>technowizardry.net&lt;/code> is an ALIAS to &lt;code>ingress-nginx.technowizardry.net&lt;/code> which this server knows about, it ends up trying to query a resolver on the Internet for that value. However, in this case, no server (which is my HE DNS) is able to handle that query because they&amp;rsquo;re failing to AXFR transfer. Thus we have a circular loop.&lt;/p>
&lt;p>I consider this to be a bug in PowerDNS given that it can absolutely handle this query. However, I suspect they may not have implemented this to avoid issues where somebody creates an ALIAS on a subdomain that is on another NS server.&lt;/p>
&lt;p>I quickly disable all ALIAS records to get back in business.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">update&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">records&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">set&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">disabled&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">where&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">domain_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">and&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">type&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;ALIAS&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After that the DNS zone transfer succeeds. I can re-enable the ALIAS records. Eventually, the TLS certificate renewal succeeds and we&amp;rsquo;re mostly golden.&lt;/p>
&lt;h2 id="mail-1">Mail&lt;/h2>
&lt;p>Except for mail is not coming back. The pod is marked as unhealthy which means that ingress-nginx won&amp;rsquo;t forward traffic to it. However, Dovecot itself is fine. It&amp;rsquo;s the config reloader that&amp;rsquo;s restarting with a &amp;ldquo;too many open files&amp;rdquo; error. It only opens like five files. How could that be. Well, it&amp;rsquo;s the only container that uses &lt;a class="link" href="https://en.wikipedia.org/wiki/Inotify" target="_blank" rel="noopener"
>Inotify&lt;/a> which is a Linux feature that sends notifications when files are updated. This one was interesting. Some &lt;a class="link" href="https://github.com/nats-io/nack/issues/68" target="_blank" rel="noopener"
>GitHub issues&lt;/a> talk about this, but it&amp;rsquo;s unclear. My intuition suggests it&amp;rsquo;s a &lt;a class="link" href="https://www.man7.org/linux/man-pages/man3/ulimit.3.html" target="_blank" rel="noopener"
>ulimit&lt;/a> or sysctl issue causing it to be unable to startup. Coincidentally, the pod is running on &lt;a class="link" href="https://www.technowizardry.net/2024/09/adopting-nixos-for-my-rke1-kubernetes-nodes/" target="_blank" rel="noopener"
>my node that is running NixOS&lt;/a>. The other two haven&amp;rsquo;t been changed over yet.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">sysctl -a &lt;span class="p">|&lt;/span> grep inotify
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">fs.inotify.max_user_instances &lt;span class="o">=&lt;/span> &lt;span class="m">128&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">user.max_inotify_instances &lt;span class="o">=&lt;/span> &lt;span class="m">128&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The limit was pretty low. That&amp;rsquo;s the culprit.&lt;/p>
&lt;h1 id="why">Why&lt;/h1>
&lt;p>The next section is asking &lt;a class="link" href="https://en.wikipedia.org/wiki/Five_whys" target="_blank" rel="noopener"
>Five whys&lt;/a> to get to the bottom. I&amp;rsquo;ll break it down to different problems.&lt;/p>
&lt;h2 id="why-didnt-cert-manager-renew-the-certificate">Why didn&amp;rsquo;t cert-manager renew the certificate?&lt;/h2>
&lt;ul>
&lt;li>cert-manager was unable to renew certificates. Why?&lt;/li>
&lt;li>cert-manager was waiting for the &lt;em>acme-challenge&lt;/em> TXT record to be available? Why wasn&amp;rsquo;t it available?&lt;/li>
&lt;li>cert-manager had created the TXT record in the authoritative PowerDNS instance, but the NS servers listed on the technowizardry.net zone (run by Hurricane Electric) did not have the TXT record. Why didn&amp;rsquo;t they have the record?&lt;/li>
&lt;li>Hurricane Electric failed to perform a DNS zone transfer. Why couldn&amp;rsquo;t they transfer?&lt;/li>
&lt;li>The DNS query was not making it to PowerDNS which could handle the transfer. Why?&lt;/li>
&lt;li>PowerDNS wasn&amp;rsquo;t able to bind to the port because systemd-resolved was already listening. Why wasn&amp;rsquo;t it already disabled?&lt;/li>
&lt;li>Not sure. Maybe an OS upgrade reverted my change or maybe I had only temp disabled it.&lt;/li>
&lt;/ul>
&lt;h2 id="why-didnt-mail-services-start-to-work">Why didn&amp;rsquo;t mail services start to work?&lt;/h2>
&lt;ul>
&lt;li>The dovecot service wasn&amp;rsquo;t starting up. Why?&lt;/li>
&lt;li>Because the config-reloader side-car was crash looping. Why?&lt;/li>
&lt;li>Because it was unable to setup the inotify that was needed. Why?&lt;/li>
&lt;li>The sysctl was set to low. Why?&lt;/li>
&lt;li>It was never changed from the default. Why?&lt;/li>
&lt;li>First time I hit this problem&lt;/li>
&lt;/ul>
&lt;h1 id="action-items">Action Items&lt;/h1>
&lt;h2 id="disable-systemd-resolved-resolver">Disable systemd-resolved resolver&lt;/h2>
&lt;p>The first action is to disable the systemd-resolved local resolver which listens on the same port. My servers then forwards DNS off host instead of using a local cache.&lt;/p>
&lt;p>On NixOS, this can done using:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">{ config, lib, pkgs, ... }:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> services.resolved.extraConfig = &amp;#39;&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> DNSStubListener=no
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;&amp;#39;;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And on other machines, it&amp;rsquo;s like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># /etc/systemd/resolved.conf.d/disable-local-resolver.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Resolve]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">DNSStubListener=no
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="increase-inotify-limits">Increase inotify limits&lt;/h2>
&lt;p>Next, we need to increase the sysctl limits. The Nix team &lt;a class="link" href="https://github.com/NixOS/nixpkgs/issues/36214" target="_blank" rel="noopener"
>discussed increasing this limit by default&lt;/a>, but the &lt;a class="link" href="https://github.com/NixOS/nixpkgs/pull/126777/" target="_blank" rel="noopener"
>related PR&lt;/a> was abandoned.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="o">...&lt;/span> &lt;span class="p">}:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boot&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">kernel&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sysctl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;fs.inotify.max_user_instances&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;8192&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;user.max_inotify_instances&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;8192&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="add-more-monitoring">Add more monitoring&lt;/h2>
&lt;p>I&amp;rsquo;m using &lt;a class="link" href="https://github.com/prometheus-community/helm-charts/blob/main/charts/kube-prometheus-stack/README.md" target="_blank" rel="noopener"
>kube-prometheus-stack&lt;/a> to monitor my cluster. I didn&amp;rsquo;t know my certificate was failing to renew for over a week. Had I known, I could have fixed it ahead of the expiration. Let&amp;rsquo;s add an alert so I get an email next time.&lt;/p>
&lt;p>Here I create a PrometheusRule object which tells Prometheus&amp;rsquo; AlarmManager to start watching for this metric expression to trigger.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">monitoring.coreos.com/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PrometheusRule&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager-rules&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">groups&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">certificates&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">failing-to-renew&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Certificate {{ $labels.exported_namespace }}/{{ $labels.name }} is
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> failing to renew&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> sum(certmanager_certificate_renewal_timestamp_seconds - time()) BY
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> (name,exported_namespace) &amp;lt; 0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">0s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">critical&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I use &lt;a class="link" href="https://grafana.com/" target="_blank" rel="noopener"
>Grafana dashboards&lt;/a> to visualize and send alerts. I configured a contact point that sends me an email and a notification policy to catch the above alarm.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/08/tw-went-down/grafana-notify-policy.png"
width="1330"
height="802"
srcset="https://www.technowizardry.net/2025/08/tw-went-down/grafana-notify-policy_hu_47e1f34de01f3d.png 480w, https://www.technowizardry.net/2025/08/tw-went-down/grafana-notify-policy_hu_d9927c5074806f9.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="165"
data-flex-basis="398px"
>&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F08%2Ftw-went-down%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=A+COE+on+why+technowizardry.net+went+down" style="border:0" alt="" /></description></item><item><title>Over-engineering my Home Assistant HVAC Dashboard</title><link>https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/</link><pubDate>Mon, 21 Jul 2025 23:47:00 -0700</pubDate><guid>https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/</guid><summary>&lt;p>Ever wondered how well your HVAC system is working in your home or condo? I did to an unhealthy degree. I want to know not just what&amp;rsquo;s the temperature, but how often is it running, what&amp;rsquo;s the supply and return temperatures, etc.? Let&amp;rsquo;s overengineer another project.&lt;/p></summary><description>&lt;p>Ever wondered how well your HVAC system is working in your home or condo? I did to an unhealthy degree. I want to know not just what&amp;rsquo;s the temperature, but how often is it running, what&amp;rsquo;s the supply and return temperatures, etc.? Let&amp;rsquo;s overengineer another project.&lt;/p>
&lt;p>To start, I&amp;rsquo;ve got an Ecobee thermostat and use &lt;a class="link" href="https://www.home-assistant.io/" target="_blank" rel="noopener"
>Home Assistant&lt;/a> to integrate with all my devices. Home Assistant is an open-source smart-home orchestration system. It has &lt;a class="link" href="https://www.home-assistant.io/integrations/?brands=featured" target="_blank" rel="noopener"
>numerous different integrations&lt;/a>, which allow you to add almost any smart home device you can think of. I switched to after trying both Samsung SmartThings and Hubitat and finding of them not powerful.&lt;/p>
&lt;h1 id="why">Why?&lt;/h1>
&lt;p>With any thermostat added in Home Assistant, you can obviously know what is the temperature and humidity in different rooms and that&amp;rsquo;s useful, but HVAC units have a lot of different things that can be monitored.&lt;/p>
&lt;p>For example, comparing the return temp (the temperature of the air going into the system) and the supply temp (air temp going back to your house) tells you how much the unit is able to increase or lower the temperature of the air.&lt;/p>
&lt;p>Monitoring how long it runs per day helps me know whether it&amp;rsquo;s under sized or the setpoint is too low/high. I can even use this to know that I need to replace the filter after a certain number of hours of runtime.&lt;/p>
&lt;p>My condo has a water sourced heat pump with a central water loop. When heating, it takes heat out of the loop and heats the air. When cooling, it takes heat in the air and transfers it to the water in the loop to go to the cooling tower up top. I attached a thermocouple to the pipe to measure that loop temp since it fluctuates based on how many other people are using it.&lt;/p>
&lt;h1 id="home-assistant">Home Assistant&lt;/h1>
&lt;p>First, add the thermostat to Home Assistant if it already isn&amp;rsquo;t already there. I&amp;rsquo;m using an &lt;a class="link" href="https://www.ecobee.com" target="_blank" rel="noopener"
>Ecobee&lt;/a>. If you have a different thermostat, you should still be able to use these dashboards, but you&amp;rsquo;ll need to ensure your thermostat provides similar sensors&lt;/p>
&lt;h2 id="ecobee-homekit">Ecobee HomeKit&lt;/h2>
&lt;p>If you&amp;rsquo;ve got an Ecobee, then adding it using the Apple &lt;a class="link" href="https://www.home-assistant.io/integrations/homekit_controller/" target="_blank" rel="noopener"
>HomeKit Device&lt;/a> integration will use local only network connections and avoid any cloud calls.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-homekit.png"
width="401"
height="164"
srcset="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-homekit_hu_628a9460dce33d82.png 480w, https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-homekit_hu_6b127d4bf1f609d9.png 1024w"
loading="lazy"
alt="A screenshot from Home Assistant showing the HomeKit Device integration added"
class="gallery-image"
data-flex-grow="244"
data-flex-basis="586px"
>&lt;/p>
&lt;h2 id="ecobee-cloud-integration">Ecobee Cloud Integration&lt;/h2>
&lt;p>There is also an &lt;a class="link" href="https://www.home-assistant.io/integrations/ecobee/" target="_blank" rel="noopener"
>Ecobee integration&lt;/a>, but Ecobee stopped allowing people to sign up for the developer accounts. There&amp;rsquo;s a few features that the HomeKit integration doesn&amp;rsquo;t support like better mode switching because the HomeKit mode doesn&amp;rsquo;t set an override like the Ecobee integration does. I have both added, but disabled polling so I only make API calls when calling actions.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-ecobee-configure-1.png"
width="1393"
height="650"
srcset="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-ecobee-configure-1_hu_474ab842d25e38ed.png 480w, https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-ecobee-configure-1_hu_9c359b7336cda09.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="214"
data-flex-basis="514px"
>&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-ecobee-configure-2.png"
width="813"
height="437"
srcset="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-ecobee-configure-2_hu_ea7f86c8b2f2f430.png 480w, https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-ecobee-configure-2_hu_6482dd1ee19728b6.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="186"
data-flex-basis="446px"
>&lt;/p>
&lt;h1 id="basic-dashboards">Basic Dashboards&lt;/h1>
&lt;p>Now, I&amp;rsquo;ve got some basic entities that report temperature, humidity, even motion. How you design your dashboard is up to your personal preference and the later metrics are more interesting. Don&amp;rsquo;t spend too much time here.&lt;/p>
&lt;h2 id="motion">Motion&lt;/h2>
&lt;p>Ecobee thermostats and the smart reports report motion making them useful for motion tracking. A history graph makes it easy to see this across different rooms:
&lt;img src="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-motion.png"
width="636"
height="270"
srcset="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-motion_hu_15455fae9dce59a2.png 480w, https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-motion_hu_ef6b23ecb6d86c2a.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="235"
data-flex-basis="565px"
>
And the YAML:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">history-graph&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">entities&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">entity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">binary_sensor.home_motion&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Living Room&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">entity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">binary_sensor.living_room_motion&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Office&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">entity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">binary_sensor.bedroom_motion&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Bedroom&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Motion&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="thermostat-control">Thermostat Control&lt;/h2>
&lt;p>What dashboard isn&amp;rsquo;t complete without actually allowing us to edit the set points.
&lt;img src="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-thermostat.png"
width="626"
height="558"
srcset="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-thermostat_hu_371ba5311cbe1abc.png 480w, https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-thermostat_hu_823b966481f40786.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="112"
data-flex-basis="269px"
>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">thermostat&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">entity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">climate.home&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">features&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">climate-hvac-modes&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="calculating-runtime">Calculating Runtime&lt;/h1>
&lt;p>Let&amp;rsquo;s get into the more complex metrics&amp;ndash;runtime. Tracking runtime is a useful metric because it gives you some interesting insights:&lt;/p>
&lt;ul>
&lt;li>Runtime - The more your A/C runs, the more wear it has and the more likely it&amp;rsquo;ll break down. You could use this to decide to schedule a yearly maintenance/inspection sooner or later depending on how much you use it.&lt;/li>
&lt;li>Duty Cycle is the percentage of time that your unit is actually running. 100% means it&amp;rsquo;s running continuously. It could mean different things including: it&amp;rsquo;s a very hot/cold day, poor house insulation, improper setpoints, undersized HVAC units, or even a window left open.&lt;/li>
&lt;li>Runtime since the filter was changed. As filters get dirtier, it gets harder to pull air through it making your system work harder. Tracking this gives you a metric to know when to replace your filter.&lt;/li>
&lt;/ul>
&lt;h2 id="state-tracking">State Tracking&lt;/h2>
&lt;p>First, we&amp;rsquo;ll need to create a few template sensors to break out the states. You can either put this into your Home Assistant&amp;rsquo;s &lt;code>/config/configuration.yaml&lt;/code> file, or create these using the UI in Settings &amp;gt; Devices &amp;amp; services &amp;gt; Helpers &amp;gt; Create Helper.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">binary_sensor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># binary_sensor.hvac_fan_state&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># This tracks whether the fan is running&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># The fan will be running when it&amp;#39;s in heat/cool or just running the fan&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># It tracks the unit actually working&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;HVAC Fan State&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">icon&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mdi:fan&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unique_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">0k79upwzyOUladHiSg3R&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">availability&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;{{ has_value(&amp;#39;climate.home&amp;#39;) }}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">state&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{ (state_attr(&amp;#34;climate.home&amp;#34;, &amp;#34;hvac_action&amp;#34;) in [&amp;#34;heating&amp;#34;, &amp;#34;cooling&amp;#34;, &amp;#34;fan&amp;#34;]) }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">sensor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># sensor.hvac_run_state&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># This tracks what the unit is doing, whether heating, cooling, or just spinning the fan&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;HVAC Run State&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unique_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NtHeqUZpB2caZAA8PQyz&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">availability&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;{{ has_value(&amp;#39;climate.home&amp;#39;) }}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">state&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;{{ state_attr(&amp;#39;climate.home&amp;#39;, &amp;#39;hvac_action&amp;#39;) }}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If you&amp;rsquo;re using YAML, save the file, then reload your entities in Developer tools &amp;gt;YAML tab &amp;gt; Reload &amp;ldquo;Template Entities&amp;rdquo; if it&amp;rsquo;s available, or restart HA if it&amp;rsquo;s not. You should now be able to see those new entities (Search in the top right if you don&amp;rsquo;t know where they are). If they&amp;rsquo;re marked as unavailable, then check to make sure your thermostat entity is &lt;code>climate.home&lt;/code>. If it&amp;rsquo;s something else, like &lt;code>climate.my_ecobee&lt;/code>, then update the templates above.&lt;/p>
&lt;h2 id="calculating-runtimes">Calculating runtimes&lt;/h2>
&lt;p>Next, it&amp;rsquo;s time to use those templates to actually track the runtime. Home Assistants &lt;a class="link" href="https://www.home-assistant.io/integrations/history_stats/" target="_blank" rel="noopener"
>history stats&lt;/a> integration is perfect for this.&lt;/p>
&lt;p>These entities will start counting up whenever the fan state or run state is in the defined mode.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">sensor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">history_stats&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HVAC Runtime Today&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">entity_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">binary_sensor.hvac_fan_state&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">state&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;on&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">time&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">start&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ today_at() }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">end&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ now() }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">history_stats&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HVAC Runtime Cooling Today&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">entity_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">binary_sensor.hvac_run_state&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">state&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;cooling&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">time&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">start&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ today_at() }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">end&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ now() }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">history_stats&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HVAC Runtime Heating Today&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">entity_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">binary_sensor.hvac_run_state&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">state&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;heating&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">time&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">start&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ today_at() }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">end&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{{ now() }}&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="filter-change-tracking">Filter Change Tracking&lt;/h2>
&lt;p>The &lt;code>sensor.hvac_runtime_today&lt;/code> resets every day, so if we want to track total runtime since a filter was replaced weeks or months ago, we need to keep summing it up.. The &lt;a class="link" href="https://www.home-assistant.io/integrations/utility_meter/" target="_blank" rel="noopener"
>utility meter&lt;/a> integration can do exactly that. I also store the filter change time just to make it easier.&lt;/p>
&lt;p>In &lt;code>/config/configuration.yaml&lt;/code>, create the following entities and reload.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">input_datetime&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Tracks when the filter was replaced&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hvac_filter_change_date&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HVAC Filter Change Date&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">has_date&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">has_time&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">utility_meter&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">energy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HVAC Runtime Since Filter Change&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unique_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">SmRwiJmHh4FdbVftTDPD&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">source&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sensor.hvac_runtime_today&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="dashboarding">Dashboarding&lt;/h2>
&lt;p>Now, I&amp;rsquo;ve got a few different entities, here&amp;rsquo;s some sample dashboards. If you don&amp;rsquo;t already have it, I use the &lt;a class="link" href="https://github.com/thomasloven/lovelace-template-entity-row" target="_blank" rel="noopener"
>template-entity-row&lt;/a> dashboard card row to help with some more complicated dashboards.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-dashboard-runtime.png"
width="525"
height="462"
srcset="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-dashboard-runtime_hu_1b15efd8ddf1c43d.png 480w, https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-dashboard-runtime_hu_3673a4ef5f8dc23f.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="113"
data-flex-basis="272px"
>&lt;/p>
&lt;p>And the YAML:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">entities&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">entities&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">entity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">binary_sensor.hvac_fan_state&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">entity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sensor.hvac_run_state&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">entity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sensor.hvac_runtime_since_filter_change&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">entity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sensor.hvac_runtime_today&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">entity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sensor.hvac_cooling_runtime_today&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">entity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">input_datetime.hvac_filter_change_date&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">custom:template-entity-row&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Filter last changed&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">state&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{ time_since(states(&amp;#39;input_datetime.hvac_filter_change_date&amp;#39;) |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> as_datetime) }} ago&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HVAC&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="entity-reference">Entity Reference&lt;/h2>
&lt;p>Let&amp;rsquo;s recap the entities we have created.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>input_datetime.hvac_filter_change_date&lt;/strong> - Tracks the last time the filter has been changed&lt;/li>
&lt;li>&lt;strong>sensor.hvac_runtime_since_filter_change&lt;/strong> - Tracks how many minutes the unit has operated since changing the filter&lt;/li>
&lt;li>&lt;strong>binary_sensor.hvac_fan_state&lt;/strong> - Tracks whether the fan is running&lt;/li>
&lt;li>&lt;strong>sensor.hvac_run_state&lt;/strong> - Tracks what the thermostat is calling for (heating, cooling, fan, etc.)&lt;/li>
&lt;li>&lt;strong>sensor.hvac_runtime_today&lt;/strong> - Tracks how much the unit has run today&lt;/li>
&lt;li>&lt;strong>sensor.hvac_cooling_runtime_today&lt;/strong> - Tracks how much cooling the unit has done today&lt;/li>
&lt;li>&lt;strong>sensor.hvac_heating_runtime_today&lt;/strong> - Tracks how much heating the unit has done today&lt;/li>
&lt;/ul>
&lt;h1 id="sensing-temperatures">Sensing Temperatures&lt;/h1>
&lt;p>In this post, I showed some basic dashboarding for a thermostat in Home Assistant and also showed some advanced monitoring that used Home Assistant to track running times and temperatures for your HVAC unit which gives useful insights for maintenance and efficiency. The new metrics are easy to create and add to dashboards.&lt;/p>
&lt;p>Going a step further, I created my own embedded device to measure air temperatures.
Next up, I wanted to add some extra sensors to my HVAC unit itself. My system is self-contained inside my condo and I have access to the different pipes and vents, but a split unit installed outside may not be as accessible.&lt;/p>
&lt;p>To handle this, I built my own prototype circuit. I started with an &lt;a class="link" href="https://www.adafruit.com/product/4600" target="_blank" rel="noopener"
>ESP32&lt;/a> running &lt;a class="link" href="https://esphome.io/" target="_blank" rel="noopener"
>ESPHome&lt;/a>. You can use any type of ESP32, but I had a few of the Adafruit Qt Py ESP32s lying around. ESPHome is a firmware development system that abstracts out writing C++ and you define sensors in YAML.&lt;/p>
&lt;p>For my sensors, I opted for these &lt;a class="link" href="https://www.adafruit.com/product/270" target="_blank" rel="noopener"
>thermocouples&lt;/a> because they&amp;rsquo;re easy connect to the board and place them in specific areas. Thermocouples work by measuring change in resistance as the temperature changes. They don&amp;rsquo;t have any electronics themselves and require a separate component, an amplifier to convert that into something that the ESP32 can read.&lt;/p>
&lt;p>Adafruit sells several different types of amplifiers including the &lt;a class="link" href="https://www.adafruit.com/product/4101" target="_blank" rel="noopener"
>MCP9600&lt;/a>, &lt;a class="link" href="https://www.adafruit.com/product/5165" target="_blank" rel="noopener"
>MCP9601&lt;/a>, &lt;a class="link" href="https://www.adafruit.com/product/3263" target="_blank" rel="noopener"
>MAX31856&lt;/a>, and the &lt;a class="link" href="https://www.adafruit.com/product/1727" target="_blank" rel="noopener"
>MAX31850K&lt;/a>, and a few others.&lt;/p>
&lt;p>The MCP9601 is nice because it comes with a stemma connector, which is an Adafruit standard connector that allows for you to connect i2c devices without soldering, but the issue with i2c is that the address is hard-coded to a single address. Multiple amplifiers will conflict unless you change the address by adding a small amount of solder to the address change pads below.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/mcp9600-addr-pads.png"
width="962"
height="728"
srcset="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/mcp9600-addr-pads_hu_35118b6aac00e74b.png 480w, https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/mcp9600-addr-pads_hu_f791d75aa690de95.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="132"
data-flex-basis="317px"
>&lt;/p>
&lt;p>The MAX variants are less accurate (0.25°C resolution) compared to the MCP960x at 0.0625°C resolution.&lt;/p>
&lt;p>My first revision opted for the MCP9600 and I soldered the pins to the circuit and put them into a breadboard.&lt;/p>
&lt;p>One thermocouple was inserted as close as possible to the supply duct that supplies the rest of the condo with heated or cooled air. The &lt;a class="link" href="https://www.adafruit.com/product/3895" target="_blank" rel="noopener"
>5 meter thermocouple&lt;/a> makes it easy to run the temp sensor right into it. The thermocouple wire can be seen in the picture below along the right side of the unit.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/photo-air-filter.jpg"
loading="lazy"
>&lt;/p>
&lt;p>I also secured one to the water loop supplying hot and chilled water to my unit. During heating modes, it allows me to see how much heat I could put into the air. During cooling modes, the hotter it is, the harder my unit will work to put heat into it. This also gives me some data I can use to work with my building&amp;rsquo;s engineer to optimize the systme.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-loop-temp.png"
width="704"
height="594"
srcset="https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-loop-temp_hu_903036b330363744.png 480w, https://www.technowizardry.net/2025/07/overengineering-my-hass-hvac-dashboard/hass-loop-temp_hu_21b8de1ca6f63e0e.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="118"
data-flex-basis="284px"
>&lt;/p>
&lt;h2 id="esphome-config">ESPHome Config&lt;/h2>
&lt;p>Here&amp;rsquo;s the configuration I used. If you use a different ESP32 board, some of this configuration will differ.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">esphome&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">qt-py-hvac-sensor&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">platformio_options&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">board_build.f_cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">160000000L&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 160MHz&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">board_build.f_flash&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">40000000L&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">board_build.flash_mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dio&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">board_build.flash_size&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">4MB&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">esp32&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">board&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">adafruit_qtpy_esp32&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">variant&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ESP32&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">framework&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">arduino&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2.0.6&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">wifi&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ssid&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>!&lt;span class="l">secret wifi_ssid&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">power_save_mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">none&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>!&lt;span class="l">secret wifi_password&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">substitutions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">device&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Qt Py Heat Pump&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">mqtt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">broker&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mqtt.home.ajacqu.es&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">discovery_unique_id_generator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mac&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">discovery_object_id_generator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">device_name&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">ota&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">esphome&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c">#changeme&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">i2c&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">stemma&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sda&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">SDA1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">scl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">SCL1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">scan&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">frequency&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">10kHz&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">sensor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mcp9600&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">temp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hot_junction&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Loop Water Temperature&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retain&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">thermocouple_type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">K&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0x67&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">update_interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">60s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># On a 5 meter cable to the output manifold&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mcp9600&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">temp2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hot_junction&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Air Supply Temperature&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retain&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">thermocouple_type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">K&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0x60&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">update_interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">60s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="future-work">Future Work&lt;/h2>
&lt;p>In my next iteration, I plan to integrate a 24v DC regulator to pull power directly from the HVAC unit instead of having to run an ugly DC wire to a nearby closet. I want to add a few more thermocouple amplifiers to measure other areas, or even measure the pressure drop across the fan like this project, &lt;a class="link" href="https://github.com/gcormier/esphome-pressure" target="_blank" rel="noopener"
>esphome-pressure&lt;/a> demonstrates. I also want to make it a custom PCB so the board is a lot cleaner.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>In this post, I showed some basic dashboarding for a thermostat in Home Assistant and also showed some advanced monitoring that used Home Assistant to track running times and temperatures for your HVAC unit which gives useful insights for maintenance and efficiency. The new metrics are easy to create and add to dashboards.&lt;/p>
&lt;p>Going a step further, I created my own embedded device to measure air temperatures.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F07%2Foverengineering-my-hass-hvac-dashboard%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Over-engineering+my+Home+Assistant+HVAC+Dashboard" style="border:0" alt="" /></description></item><item><title>Abandon the Helm, leveraging CDK for Kubernetes</title><link>https://www.technowizardry.net/2025/04/abandon-the-helm-leveraging-cdk-for-kubernetes/</link><pubDate>Mon, 14 Apr 2025 18:27:00 -0700</pubDate><guid>https://www.technowizardry.net/2025/04/abandon-the-helm-leveraging-cdk-for-kubernetes/</guid><summary>&lt;p>I&amp;rsquo;ve had enough of Helm. I don&amp;rsquo;t know who thought string-based templating engines would be a good idea, but I have had one too many indention relate bugs. They&amp;rsquo;re a source of bug and a pain. Kubernetes YAML files just contain a ton of boiler-plate YAML configuration. Like how many times do I have to specify the labels? Its spec/template/spec for Deployment, but spec/jobTemplate/spec for CronJob. Ain&amp;rsquo;t nobody got time to remember that.&lt;/p>
&lt;p>Enter &lt;a class="link" href="https://cdk8s.io/" target="_blank" rel="noopener"
>cdk8s&lt;/a>. It&amp;rsquo;s built-upon CDK, a software development kit that uses standard programming languages, like TypeScript, Python, or Java, as a way to define resources that then get compiled into YAML or JSON to upload to CloudFormation, or in our case, Kubernetes.&lt;/p>
&lt;p>Why would you want/need a full programming language just to define some infrastructure? Well, there are some benefits. Let&amp;rsquo;s go through them.&lt;/p></summary><description>&lt;p>I&amp;rsquo;ve had enough of Helm. I don&amp;rsquo;t know who thought string-based templating engines would be a good idea, but I have had one too many indention relate bugs. They&amp;rsquo;re a source of bug and a pain. Kubernetes YAML files just contain a ton of boiler-plate YAML configuration. Like how many times do I have to specify the labels? Its spec/template/spec for Deployment, but spec/jobTemplate/spec for CronJob. Ain&amp;rsquo;t nobody got time to remember that.&lt;/p>
&lt;p>Enter &lt;a class="link" href="https://cdk8s.io/" target="_blank" rel="noopener"
>cdk8s&lt;/a>. It&amp;rsquo;s built-upon CDK, a software development kit that uses standard programming languages, like TypeScript, Python, or Java, as a way to define resources that then get compiled into YAML or JSON to upload to CloudFormation, or in our case, Kubernetes.&lt;/p>
&lt;p>Why would you want/need a full programming language just to define some infrastructure? Well, there are some benefits. Let&amp;rsquo;s go through them.&lt;/p>
&lt;h1 id="the-good">The Good&lt;/h1>
&lt;h2 id="no-more-string-based-templating">No more string based templating&lt;/h2>
&lt;p>In Helm, when you&amp;rsquo;re templating files, you use Golang&amp;rsquo;s &lt;a class="link" href="https://pkg.go.dev/text/template" target="_blank" rel="noopener"
>text templating system&lt;/a>. You start writing YAML text, then depending on your use case, mix in some variables, some conditionals, some loops, and more. At first, it seems reasonable and maybe you&amp;rsquo;ve only got a few variables to swap out (&lt;a class="link" href="https://github.com/ajacques/helm-catalog/blob/master/sentry/templates/web-ui/deployment.yaml" target="_blank" rel="noopener"
>snippet source&lt;/a>):&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-template" data-lang="go-template">&lt;span class="line">&lt;span class="cl">&lt;span class="x">apiVersion: apps/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x">kind: Deployment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> name: web
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> app: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">template&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;sentry.name&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> chart: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Chart.Name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">-&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Chart.Version&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">replace&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;+&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;_&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> release: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Release.Name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> heritage: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Release.Service&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"># ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> annotations:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> checksum/config: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">include&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="k">print&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">$.Template.BasePath&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;/configmap.yaml&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">sha256sum&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> checksum/config2: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">include&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="k">print&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">$.Template.BasePath&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;/sentry-config-file.yaml&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">sha256sum&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> checksum/secret: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">include&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="k">print&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">$.Template.BasePath&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;/secret.yaml&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">sha256sum&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> component: web
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> chart: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Chart.Name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> containers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> - args:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> - run
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> - web
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Values.ingress.tls_secret_name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> env:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> - name: SENTRY_USE_SSL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> value: &amp;#34;1&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">end&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Maybe it stays that way, or maybe you need more and more variables, substitutions. Especially if you&amp;rsquo;re vending a Helm chart to others. Then you end up with everything needed to be passed as values like &lt;a class="link" href="https://github.com/kubernetes/ingress-nginx/blob/main/charts/ingress-nginx/templates/controller-daemonset.yaml" target="_blank" rel="noopener"
>this&lt;/a> template from ingress-nginx.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-template" data-lang="go-template">&lt;span class="line">&lt;span class="cl">&lt;span class="x">apiVersion: apps/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x">kind: DaemonSet
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">include&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;ingress-nginx.labels&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">nindent&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">4&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> app.kubernetes.io/component: controller
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">with&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Values.controller.labels&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">toYaml&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">nindent&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">4&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">end&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> name: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">include&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;ingress-nginx.controller.fullname&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> namespace: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">include&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;ingress-nginx.namespace&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Values.controller.annotations&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> annotations: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">toYaml&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Values.controller.annotations&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">nindent&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">4&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">end&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> matchLabels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">include&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;ingress-nginx.selectorLabels&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">nindent&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">6&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> app.kubernetes.io/component: controller
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> revisionHistoryLimit: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Values.revisionHistoryLimit&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Values.controller.updateStrategy&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> updateStrategy: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">toYaml&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Values.controller.updateStrategy&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">nindent&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">4&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">end&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> minReadySeconds: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Values.controller.minReadySeconds&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> template:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Values.controller.podAnnotations&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> annotations:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">range&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$key&lt;/span>&lt;span class="o">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$value&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">:=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Values.controller.podAnnotations&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$key&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$value&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">quote&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">end&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">end&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"># ...
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>By that point, you&amp;rsquo;ve lost all meaning of templating. The YAML has become but a shell of its original self, it&amp;rsquo;s merely just a vessel that the &lt;code>values.yaml&lt;/code> get passed through onto their final destination that is your Kubernetes cluster. Then you ask yourself, is this the best it can be? Is any of this logic correct? Can you even tell at a glance without unit tests? Wait, a templating system has unit tests? &lt;a class="link" href="https://helm.sh/docs/topics/chart_tests/" target="_blank" rel="noopener"
>Indeed&lt;/a>.&lt;/p>
&lt;p>Now, you get to define and expose class based properties and fields. For example, instead of explicitly listing every single property that can be overridden, you can do:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Allow user to override a few props
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">type&lt;/span> &lt;span class="nx">OverridableProps&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">Pick&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">DaemonSetProps&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;image&amp;#39;&lt;/span> &lt;span class="err">|&lt;/span> &lt;span class="s1">&amp;#39;imagePullPolicy&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">makeDaemonSet&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scope&lt;/span>: &lt;span class="kt">Construct&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">props&lt;/span>: &lt;span class="kt">OverrideableProps&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">new&lt;/span> &lt;span class="nx">DaemonSet&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">app&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;nginx&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;controller&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">image&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;registry.k8s.io/ingress-nginx/controller&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Allow the user to override things:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">...&lt;/span>&lt;span class="nx">props&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="no-more-indention-hell">No more indention hell&lt;/h2>
&lt;p>Using Helm charts means that you&amp;rsquo;re forced to be very careful about indention. It gets worse once you start mixing templates into the picture.&lt;/p>
&lt;p>For example, I found &lt;a class="link" href="https://github.com/kubernetes/ingress-nginx/pull/9709" target="_blank" rel="noopener"
>this bug&lt;/a> in ingress-nginx caused by improper indention:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="hl">&lt;span class="lnt">11
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">12
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">13
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">14
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">15
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">16
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">17
&lt;/span>&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">initContainers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/bin/chown&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;101&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/mycache&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">busybox:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">chmod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/mycache&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cache&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># &amp;lt;--- Notice that this is aligned under the volumeMounts, not under initContainers&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># &amp;lt;-- Also note the empty image and names&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;sh&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;-c&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;/usr/local/bin/init_module.sh&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">modules&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/modules_mount&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The bug was because the following snippet had one too many tabs at the beginning, but you&amp;rsquo;d never figure know that just looking at it:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-template" data-lang="go-template">&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Values.controller.extraModules&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &lt;/span>&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">range&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Values.controller.extraModules&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> - name: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> image: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Image&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> command: [&amp;#39;sh&amp;#39;, &amp;#39;-c&amp;#39;, &amp;#39;/usr/local/bin/init_module.sh&amp;#39;]
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>CDK fixes that. You no-longer care about indention, unless of course you use the Python bindings for CDK8s. You use normal objects and set properties on them, call methods, etc. CDK8s then is responsible for serializing that into a properly indented YAML file.&lt;/p>
&lt;h2 id="reusable-functions">Reusable functions&lt;/h2>
&lt;p>With Helm templating, you frequently end up just having a lot of boiler-plate YAML tags around. For example, a bunch of my network policies ended up having the same egress policy in them. Before, I would copy and paste the following file into many different namespaces:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NetworkPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">egress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Allow access to DNS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">UDP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">namespaceSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kubernetes.io/metadata.name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">k8s-app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-dns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Helm does support a &lt;a class="link" href="https://helm.sh/docs/chart_best_practices/templates/" target="_blank" rel="noopener"
>reusable template&lt;/a>, but I was using Rancher Fleet which I don&amp;rsquo;t know if it supports them and they suffer from the indention problem mentioned above.&lt;/p>
&lt;p>With code-based solutions, this entire model changes because I can just write a method that can be re-used by different constructs. This one of the most powerful features of code-based infra as code.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">grantDns&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>: &lt;span class="kt">Construct&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">netPolicy&lt;/span>: &lt;span class="kt">kplus.NetworkPolicy&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">peer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">kplus&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Pods&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">select&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;netpol-kube-dns&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">namespaces&lt;/span>: &lt;span class="kt">kplus.Namespaces.select&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;netpol-kube-system&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">names&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;kube-system&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">labels&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;kube-app&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;kube-dns&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">netPolicy&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEgressRule&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">peer&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">NetworkPolicyPort&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tcp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">53&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">NetworkPolicyPort&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">udp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">53&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">netPol&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">kplus&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">NetworkPolicy&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;netpol&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">grantDns&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">netPol&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="compile-time-type-safety">Compile time type-safety&lt;/h2>
&lt;p>Does that property exist on that resource? Are you missing anything critical? Any invalid field values? YAML provides no compile-time validation. I like to use &lt;a class="link" href="https://github.com/adrienverge/yamllint" target="_blank" rel="noopener"
>yamllint&lt;/a> to see if it&amp;rsquo;s syntactically valid YAML, but that doesn&amp;rsquo;t validate that fields exist.&lt;/p>
&lt;p>CDK8s gets this right. Fields that don&amp;rsquo;t exist on the resource can&amp;rsquo;t be set in code and it simply does not compile.&lt;/p>
&lt;h1 id="the-bad">The Bad&lt;/h1>
&lt;h2 id="cant-move-resources">Can&amp;rsquo;t move resources&lt;/h2>
&lt;p>This is another problem with the &lt;a class="link" href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/refactor-stacks.html" target="_blank" rel="noopener"
>underlying CDK design&lt;/a>. In CDK, resource names are derived from the path of constructs and resource names. A resource might have the path &lt;code>App/Chart/MyService/Deployment&lt;/code>, which has the resource name &lt;code>chart-myservice-deployment-c873a441&lt;/code>.&lt;/p>
&lt;p>If I try to rename the construct name to &lt;code>Deployment2&lt;/code>, I get a new resource name &lt;code>Chart-MyService-Deployment2-c8cb06b1&lt;/code> and helm will delete the old one and create a new deployment.&lt;/p>
&lt;p>This is dangerous. When you&amp;rsquo;re writing code, you easily forget about this because sometimes a code refactoring is needed to fix some issue, but you can&amp;rsquo;t always safely do this because Helm will end up deleting and recreating a resource. Helm also doesn&amp;rsquo;t have CloudFormation&amp;rsquo;s safe deployment mechanism where dependencies between resources are identified and creates are deployed first, then if everything succeeds, deletes are performed. ==Helm has &lt;a class="link" href="https://github.com/helm/helm/blob/5442c6b9cb67213ea6c66158bf4025fdd4d2ce45/pkg/release/util/kind_sorter.go#L31" target="_blank" rel="noopener"
>a static defined order&lt;/a> on how it deploys resources==. This sort of replicates CloudFormation&amp;rsquo;s deployment ordering strategy, but it doesn&amp;rsquo;t actually guarantee that resources are working correctly. For example, a PersistentVolumeClaim can be created, but fail to actually provision and the deployment will get stuck.&lt;/p>
&lt;h2 id="deployment-tooling">Deployment tooling&lt;/h2>
&lt;p>CDK8s itself provides a bunch of classes that allow you to generate YAML files, but those are not useful unless you have something that actually deploys those files. This tooling for deployments is very important.&lt;/p>
&lt;p>It is the glue that takes the synthesized output files and actually gets them onto your K8s cluster. Base CDK does pretty well with this because it inherently only has to work with AWS and has a built-in CLI to deploy to CloudFormation or using CDK you can deploy with CodePipelines or GitHub Actions. To be honest, I only worked with the Amazon-internal variant of the pipeline which is different and works well.&lt;/p>
&lt;p>What are we missing? The CDK CLI itself provides a command &lt;code>cdk deploy&lt;/code> that synthesizes your output, identifies the dependencies between the stacks, then sequentially deploys the stacks in transitive order using CloudFormation.&lt;/p>
&lt;p>I want the thing that manages the deployment part of CI/CD. Kubernetes has a lot of options here. Let&amp;rsquo;s explore some&lt;/p>
&lt;h3 id="what-about-helm">What about Helm?&lt;/h3>
&lt;p>Helm provides a CLI that given a YAML manifest, it will create, update, and delete the resources in Kubernetes. I could use GitHub Actions to first synthesize my CDK8s application, then deploy it like below:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">validate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/checkout@v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">synth&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Synth cdk8s manifests&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> npm install
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> npm run build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Set up Kubectl&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">azure/setup-kubectl@v4.0.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;v1.31.0&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deploy to cluster&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">azure/setup-helm@v4.3.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Create kubeconfig file&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">echo &amp;#34;${{ secrets.KUBECONFIG }}&amp;#34; &amp;gt; ${{ github.workspace }}/kubeconfig&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deploy to Kubernetes using Helm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github.ref == &amp;#39;refs/heads/master&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">KUBECONFIG&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ github.workspace }}/kubeconfig&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> helm upgrade --install mytarget dist/ --namespace default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Functionally that works, however it lack some features:&lt;/p>
&lt;p>&lt;strong>Can&amp;rsquo;t deploy to multiple Helm releases&lt;/strong>. My Git repo has several different Helm charts and releases that got deployed. Some of them had to be deployed first. CDK8s doesn&amp;rsquo;t make this easy.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">cdk8s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">App&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">chart1&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">cdk8s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Chart&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">app&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;common&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">chart2&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">cdk8s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Chart&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">app&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;service1&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">chart3&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">cdk8s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Chart&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">app&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;service2&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">synth&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Everything gets emitted to the dist folder. Because there&amp;rsquo;s only one Chart.yaml and everything is under the same templates, I can&amp;rsquo;t use Helm CLI to deploy one file to one release.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">dist/Chart.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">dist/templates/common.k8s.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">dist/templates/service1.k8s.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">dist/templates/service2.k8s.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If we look back to my GitHub Actions attempt:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deploy to Kubernetes using Helm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github.ref == &amp;#39;refs/heads/master&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">KUBECONFIG&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ github.workspace }}/kubeconfig&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> helm upgrade --install mytarget dist/ --namespace default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The only idea that I have to fix this is to emit separate chart files, then run a GitHub action that moves them all into separate Chart folders that Helm can deploy separately.&lt;/p>
&lt;h3 id="what-about-argocd">What about ArgoCD?&lt;/h3>
&lt;p>There&amp;rsquo;s also &lt;a class="link" href="https://argoproj.github.io/cd/" target="_blank" rel="noopener"
>ArgoCD&lt;/a> which is a common Kubernetes-native solution for managing deployments. I&amp;rsquo;ve avoided it thus far because it&amp;rsquo;s always seemed overly-complex with several different controllers all running for my use-case of just deploying some YAML to a cluster. Do I really want to need how many controllers just to deploy some YAML?&lt;/p>
&lt;p>Also, it seems like when ArgoCD works with a Git repo, it doesn&amp;rsquo;t know how to first compile the manifests. At least, that&amp;rsquo;s what &lt;a class="link" href="https://mattias.engineer/blog/2022/gitops-cdk8s-argocd/" target="_blank" rel="noopener"
>this blog post&lt;/a> implies and it required two separate Git repositories.&lt;/p>
&lt;p>Admittedly, I didn&amp;rsquo;t test out ArgoCD, so there might be something I&amp;rsquo;m missing, but I it still won&amp;rsquo;t fix some of the other issues when coding with YAML.&lt;/p>
&lt;h2 id="working-with-legacy-resources">Working with legacy resources&lt;/h2>
&lt;p>If you have any existing resources written using raw YAML and are already created in an existing Helm release and you want to adopt cdk8s, you&amp;rsquo;re going to be in a tricky place. If you want to switch an existing Helm release from raw YAML to cdk8s, you have to either:&lt;/p>
&lt;ol>
&lt;li>Synth the output and put them in the same folder as your legacy YAML&lt;/li>
&lt;/ol>
&lt;p>Whether this works or not depends on what kind of CI/CD you currently use. If you&amp;rsquo;re using something like &lt;a class="link" href="https://fleet.rancher.io/" target="_blank" rel="noopener"
>Rancher Fleet&lt;/a>, like I was until I realized it was fragile and frequently broke, then you now have to have two different Git repos, one with cdk8s, and one with raw YAML that uses your CI/CD to commit the output into that repo. [This post](If you&amp;rsquo;re using something like &lt;a class="link" href="https://fleet.rancher.io/" target="_blank" rel="noopener"
>Racher Fleet&lt;/a>, like I was, then you now have to have two different Git repos, one with cdk8s) talks about that model using GitHub Actions, but that complexity terrified me.&lt;/p>
&lt;ol start="2">
&lt;li>Import the legacy resources as-is and include them in the output&lt;/li>
&lt;/ol>
&lt;p>When I worked at AWS and owned a program to switch from an internal framework that used raw YAML CloudFormation to native CDK, we used CDK&amp;rsquo;s &lt;a class="link" href="https://docs.aws.amazon.com/cdk/v2/guide/use_cfn_template.html" target="_blank" rel="noopener"
>CfnInclude&lt;/a> construct to do this. Cdk8s has an equivalent called &lt;a class="link" href="https://cdk8s.io/docs/latest/basics/include/" target="_blank" rel="noopener"
>Include&lt;/a>&lt;/p>
&lt;h2 id="messy-auto-generated-resources">Messy auto generated resources&lt;/h2>
&lt;p>CDK8s has a number of dev-friendly methods, like the ability to create NetworkPolicies with permissions using just one line of code:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">myService&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">connections&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">allowTo&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">redis&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">workload&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>From a dev perspective, this is massive productivity boost. It can auto create both the ingress and egress policies. However, cdk8s generates multiple NetworkPolicies for each and every grant with automatically generated names.&lt;/p>
&lt;p>From a debugging and operational perspective, this makes it difficult when you&amp;rsquo;re viewing your cluster resources and trying to figure out what policy applies and what each means.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NetworkPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">allowegressc8116eb74577cb8dbda1910afbbd7c3456-c8832897&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">foo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">egress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cdk8s.io/metadata.addr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">example-TestService-Deployment-c8d415ef&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policyTypes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">Egress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NetworkPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">allowingresskube-systemc8116eb74577cb8dbda191-c803ee19&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kube-app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-dns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policyTypes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">Ingress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NetworkPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">allowegressc8b9fe52de2665a49bb6866db05c076914-c8924060&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">foo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">egress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cdk8s.io/metadata.addr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">example-TestService-Deployment-c8d415ef&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policyTypes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">Egress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NetworkPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">allowingressfooc8b9fe52de2665a49bb6866d-c8819c21&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">foo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cdk8s.io/metadata.addr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">example-TestService-Redis-c8b9fe52&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policyTypes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">Ingress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="difference-in-mental-model-compared-to-yaml">Difference in mental model compared to YAML&lt;/h2>
&lt;p>I expect differences when comparing a programming language vs a YAML templates because they&amp;rsquo;re just fundamentally different styles of writing. YAML is &lt;em>the&lt;/em> language for describing Kubernetes resources. Tutorials use them, this blog uses YAML for Kubernetes. With so much documentation using YAML, you now have to mentally translate that into the CDK8s equivalent. Generally they look pretty similar, like a container looks the same:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NetworkPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">homeassistant&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">homeassistant&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policyTypes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">Egress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">egress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Allow DNS queries&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">UDP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">namespaceSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kubernetes.io/metadata.name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">k8s-app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-dns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And the equivalent in CDK8s:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">netPolicy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">kplus&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">NetworkPolicy&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;netpolicy&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">peer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">kplus&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Pods&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">select&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;netpol-kube-dns&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">namespaces&lt;/span>: &lt;span class="kt">kplus.Namespaces.select&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;netpol-kube-system&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">names&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;kube-system&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">labels&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;kube-app&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;kube-dns&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">netPolicy&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEgressRule&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">peer&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">NetworkPolicyPort&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tcp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">53&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">NetworkPolicyPort&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">udp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">53&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>At first I didn&amp;rsquo;t even know about the &lt;code>Pods.select&lt;/code> and &lt;code>Namespaces.select&lt;/code> and assumed I had to create my own custom peer class, but it wasn&amp;rsquo;t until I started preparing to make a PR that I found &lt;a class="link" href="https://github.com/cdk8s-team/cdk8s-plus/blob/k8s-30/main/docs/plus/network-policy.md#peers" target="_blank" rel="noopener"
>this doc&lt;/a> that explained this.&lt;/p>
&lt;p>This is not unique to CDK8s, and is prevalent in CDK vs CloudFormation too or even Terraform/OpenTofu. It&amp;rsquo;s also not necessary a really bad problem, but the more field names start to differ. The more you have to reach for different classes or start to pass around CDK contexts and names, it just gets confusing.&lt;/p>
&lt;p>Sure, I know the answer now, but these things will be confusing for the next person.&lt;/p>
&lt;h2 id="unexpected-defaults">Unexpected defaults&lt;/h2>
&lt;p>CDK8s provides defaults for Deployments that I don&amp;rsquo;t think should be provided. For example, they provide a default resource request and a default security context.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1500m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">2048Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1000m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">512Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">securityContext&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowPrivilegeEscalation&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">privileged&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readOnlyRootFilesystem&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runAsNonRoot&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>CPU resource limits &lt;a class="link" href="https://dev.to/naveens16/kubernetes-cpu-limits-the-silent-killer-of-performance-and-how-to-fix-it-20d1" target="_blank" rel="noopener"
>are bad&lt;/a>. You probably shouldn&amp;rsquo;t use them unless you know your process has a max thread count. On-top of that, CDK8s limits to 1.5 CPU cores which is not rounded and if you do have two threads running a lot, one of them is going to get throttled 50% of every second. I imagine they&amp;rsquo;re just picking some default value to help Kubernetes bin-pack, but in this case, I say you&amp;rsquo;re more likely just picking a bad number.&lt;/p>
&lt;p>While enforcing read-only root file-systems is good security practice™, it&amp;rsquo;s also likely to cause a lot of broken software. So many software components I run require the ability to write temp files, etc. I&amp;rsquo;m not sure why they chose this default. Maybe they wanted to do opt-out security, which I mean if the software can run, great, but also it&amp;rsquo;s very inconsistent.&lt;/p>
&lt;p>My opinion: CDK8s should use the same defaults as Kubernetes itself. If they want to provide secure, robust solutions, provide a higher level construct. I&amp;rsquo;ve seen this employed inside of Amazon for security sensitive CDK.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>CDK8s provides some pretty useful dev productivity improvements over writing raw YAML defined resources. The built-in compile-time type-safety, code completion when in an IDE, and ability to abstract out repetitive code is a time saver.&lt;/p>
&lt;p>However, it does have some down-sides. The big one is lack of the last bit of deployment tooling that actually helps this get deployed to my Kubernetes cluster.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F04%2Fabandon-the-helm-leveraging-cdk-for-kubernetes%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Abandon+the+Helm%2C+leveraging+CDK+for+Kubernetes" style="border:0" alt="" /></description></item><item><title>This blog is now on ASP.NET Core</title><link>https://www.technowizardry.net/2025/03/this-blog-is-now-on-asp.net-core/</link><pubDate>Sun, 23 Mar 2025 18:17:00 -0700</pubDate><guid>https://www.technowizardry.net/2025/03/this-blog-is-now-on-asp.net-core/</guid><summary>&lt;p>This blog is a static website compiled using &lt;a class="link" href="https://gohugo.io/" target="_blank" rel="noopener"
>Hugo&lt;/a>. Up to this point, I built the website and packaged all of the assets into a Docker container with NGINX which was hosted on my dedicated server cluster.&lt;/p>
&lt;p>This worked well and was simple, but I have an upcoming project that I&amp;rsquo;ll be announcing soon that required dynamic content that nginx + pure static files wasn&amp;rsquo;t easily able to implement with NGINX.&lt;/p>
&lt;p>To fix this, I decided to migrate this blog from NGINX to ASP.NET Core. Here&amp;rsquo;s how and why.&lt;/p></summary><description>&lt;p>This blog is a static website compiled using &lt;a class="link" href="https://gohugo.io/" target="_blank" rel="noopener"
>Hugo&lt;/a>. Up to this point, I built the website and packaged all of the assets into a Docker container with NGINX which was hosted on my dedicated server cluster.&lt;/p>
&lt;p>This worked well and was simple, but I have an upcoming project that I&amp;rsquo;ll be announcing soon that required dynamic content that nginx + pure static files wasn&amp;rsquo;t easily able to implement with NGINX.&lt;/p>
&lt;p>To fix this, I decided to migrate this blog from NGINX to ASP.NET Core. Here&amp;rsquo;s how and why.&lt;/p>
&lt;h1 id="background">Background&lt;/h1>
&lt;p>I&amp;rsquo;m still using Hugo to generate all HTML files in my blog from the raw Markdown files just like any other static website. I make changes, commit them to GitHub, then GitHub Actions runs &lt;code>hugo&lt;/code>, builds the .net application, and packs the web assets into the image.&lt;/p>
&lt;h1 id="why">Why&lt;/h1>
&lt;p>&lt;strong>Why C#?&lt;/strong> I like C# and .net and wanted to see how .net has evolved over since I last used it.&lt;/p>
&lt;p>&lt;strong>Why replace NGINX?&lt;/strong> My next project, to implement ActivityPub, required some dynamic features that were difficult to handle in NGINX. For example, I need to send push notifications on new posts and process inbox messages, NGINX can&amp;rsquo;t do that. I experimented with using the &lt;a class="link" href="https://github.com/openresty/lua-nginx-module" target="_blank" rel="noopener"
>nginx-lua-module&lt;/a> and implement the logic in Lua, but my use case was more complex. I also looked at &lt;a class="link" href="https://varnish-cache.org/" target="_blank" rel="noopener"
>Varnish&lt;/a> which supported more imperative style response handling, but also lacked some features. Ultimately, I had a prototype that used NGINX, but the resulting architecture was more complex.&lt;/p>
&lt;h1 id="static-files">Static Files&lt;/h1>
&lt;p>First thing we do is serve files located in the &lt;code>/app/wwwroot&lt;/code> folder (this is where we&amp;rsquo;ll put the Hugo output files.)&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">var&lt;/span> &lt;span class="n">app&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">builder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Build&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">UseDefaultFiles&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// When a user requests /foobar/, serve up /foobar/index.html&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">UseStaticFiles&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Run&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The Dockerfile looks like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="k">FROM&lt;/span>&lt;span class="s"> hugomods/hugo:0.123.0 AS hugobuild&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> . /src&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> &lt;span class="nv">HUGO_ENVIRONMENT&lt;/span>&lt;span class="o">=&lt;/span>production hugo --minify -b https://www.technowizardry.net&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> mcr.microsoft.com/dotnet/runtime:8.0 AS base&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">USER&lt;/span>&lt;span class="s"> app&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">WORKDIR&lt;/span>&lt;span class="s"> /app&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> mcr.microsoft.com/dotnet/sdk:8.0 AS build&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> apt-get update &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> apt-get install -y --no-install-recommends &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> clang zlib1g-dev&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ARG&lt;/span> &lt;span class="nv">BUILD_CONFIGURATION&lt;/span>&lt;span class="o">=&lt;/span>Release
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">WORKDIR&lt;/span>&lt;span class="s"> /src&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> &lt;span class="o">[&lt;/span>&lt;span class="s2">&amp;#34;http-server/http-server.csproj&amp;#34;&lt;/span>, &lt;span class="s2">&amp;#34;.&amp;#34;&lt;/span>&lt;span class="o">]&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> dotnet restore &lt;span class="s2">&amp;#34;./http-server.csproj&amp;#34;&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> activity-publisher/ .&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">WORKDIR&lt;/span>&lt;span class="s"> &amp;#34;/src/.&amp;#34;&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> dotnet build &lt;span class="s2">&amp;#34;./http-server.csproj&amp;#34;&lt;/span> -c &lt;span class="nv">$BUILD_CONFIGURATION&lt;/span> -o /app/build&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># This stage is used to build the AOT-compiled output&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> build AS publish&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ARG&lt;/span> &lt;span class="nv">BUILD_CONFIGURATION&lt;/span>&lt;span class="o">=&lt;/span>Release
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">RUN&lt;/span> dotnet publish &lt;span class="s2">&amp;#34;./http-server.csproj&amp;#34;&lt;/span> -c &lt;span class="nv">$BUILD_CONFIGURATION&lt;/span> -o /app/publish /p:UseAppHost&lt;span class="o">=&lt;/span>true&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> ${FINAL_BASE_IMAGE:-mcr.microsoft.com/dotnet/runtime-deps:8.0} AS final&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">WORKDIR&lt;/span>&lt;span class="s"> /app&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> --from&lt;span class="o">=&lt;/span>publish /app/publish /app/&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> --from&lt;span class="o">=&lt;/span>hugobuild /src/public /app/wwwroot/&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENTRYPOINT&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;./http-server&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="compressing-static-files">Compressing static files&lt;/h1>
&lt;p>That&amp;rsquo;s easy, but the next problem is supporting compression. With NGINX, I can pre-compress files in the Docker image, then use the &lt;a class="link" href="https://nginx.org/en/docs/http/ngx_http_gzip_static_module.html#gzip_static" target="_blank" rel="noopener"
>gzip_static&lt;/a> option to serve that to clients. ASP.NET Core &lt;a class="link" href="https://learn.microsoft.com/en-us/aspnet/core/performance/response-compression?view=aspnetcore-8.0" target="_blank" rel="noopener"
>does support&lt;/a> response compression, but the response gets compressed dynamically for every request, instead of using pre-compressed files. Compressing the file every time a client fetches it even though it never changes is wasted effort. Let&amp;rsquo;s do better.&lt;/p>
&lt;p>I will support both gzip, which is the most common compression encoding, and &lt;a class="link" href="https://github.com/google/brotli" target="_blank" rel="noopener"
>brotli&lt;/a>, a newer compression encoding that better compresses files.&lt;/p>
&lt;h2 id="generating-the-files">Generating the files&lt;/h2>
&lt;p>First thing, we need to generate the pre-compressed files in the Dockerfile so they can be found. The command I used below compresses anything over 1KiB because smaller files often times don&amp;rsquo;t compress enough to be worth the overhead. I also exclude media files because they already have their own compression applied that won&amp;rsquo;t benefit from extra compression.&lt;/p>
&lt;p>Here&amp;rsquo;s what the Dockerfile looks like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="hl">&lt;span class="lnt"> 7
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt"> 8
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt"> 9
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">10
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">11
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">12
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">13
&lt;/span>&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="k">FROM&lt;/span>&lt;span class="s"> hugomods/hugo:0.123.0 AS hugobuild&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> . /src&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> &lt;span class="nv">HUGO_ENVIRONMENT&lt;/span>&lt;span class="o">=&lt;/span>production hugo --minify -b https://www.technowizardry.net&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># First, compress using gzip&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> find /src/public ! -name &lt;span class="s1">&amp;#39;*.png&amp;#39;&lt;/span> ! -name &lt;span class="s1">&amp;#39;*.jpg&amp;#39;&lt;/span> ! -name &lt;span class="s2">&amp;#34;*.mp4&amp;#34;&lt;/span> -size +1k -type f -print -exec gzip -k -f &lt;span class="s2">&amp;#34;{}&amp;#34;&lt;/span> &lt;span class="se">\;&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># Then compress using brotli&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> alpine:3 AS brotlibuild&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> apk update &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> apk add --upgrade brotli&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> --from&lt;span class="o">=&lt;/span>hugobuild /src/public /src/public/&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> find /src/public ! -name &lt;span class="s2">&amp;#34;*.gz&amp;#34;&lt;/span> ! -name &lt;span class="s2">&amp;#34;*.png&amp;#34;&lt;/span> ! -name &lt;span class="s2">&amp;#34;*.jpg&amp;#34;&lt;/span> ! -name &lt;span class="s2">&amp;#34;*.mp4&amp;#34;&lt;/span> -size +1k -type f -print -exec brotli &lt;span class="s2">&amp;#34;{}&amp;#34;&lt;/span> &lt;span class="se">\;&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># ...&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> --from&lt;span class="o">=&lt;/span>brotlibuild /src/public /app/wwwroot/&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENTRYPOINT&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;./http-server&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="serving-the-files">Serving the files&lt;/h2>
&lt;p>Next, we need to look at every inbound HTTP request and see if the client passed in an &lt;code>Accept-Encoding&lt;/code> header (&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding" target="_blank" rel="noopener"
>MDN Web Docs&lt;/a>). If specified, one or more values can be passed, but we&amp;rsquo;re interested in &lt;code>gzip&lt;/code>, &lt;code>br&lt;/code>, and &lt;code>identity&lt;/code>.&lt;/p>
&lt;p>This check will be added to the ASP.NET request processing chain. ASP.NET came with a class (&lt;a class="link" href="https://github.com/dotnet/aspnetcore/blob/v8.0.14/src/Middleware/StaticFiles/src/DefaultFilesMiddleware.cs" target="_blank" rel="noopener"
>DefaultFilesMiddleware.cs&lt;/a>) that implemented part of what I wanted, but it didn&amp;rsquo;t support checking for &lt;code>.gz&lt;/code> or &lt;code>.br&lt;/code> pre-generated files.&lt;/p>
&lt;p>Let&amp;rsquo;s break this down. The first chunk is just initialization. The &lt;code>_next&lt;/code> delegate represents the next middleware in the chain. In this case, it&amp;rsquo;ll be the StaticFilesMiddleware. The &lt;code>_fileProvider&lt;/code> provides a handle to the directory that contains our static files. When running in Docker, it&amp;rsquo;ll be &lt;code>/app/wwwroot&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="k">using&lt;/span> &lt;span class="nn">Microsoft.Extensions.FileProviders&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">using&lt;/span> &lt;span class="nn">Microsoft.Extensions.Options&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span> &lt;span class="k">class&lt;/span> &lt;span class="nc">CustomDefaultFileMiddleware&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">private&lt;/span> &lt;span class="k">readonly&lt;/span> &lt;span class="n">RequestDelegate&lt;/span> &lt;span class="n">_next&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">private&lt;/span> &lt;span class="k">readonly&lt;/span> &lt;span class="n">IFileProvider&lt;/span> &lt;span class="n">_fileProvider&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">public&lt;/span> &lt;span class="n">CustomDefaultFileMiddleware&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">RequestDelegate&lt;/span> &lt;span class="n">next&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">IWebHostEnvironment&lt;/span> &lt;span class="n">hostingEnv&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">IOptions&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">StaticFileOptions&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="n">options&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// next middleware in the chain&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_next&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">next&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// The file provider knows where to find the source files&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_fileProvider&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">options&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">FileProvider&lt;/span> &lt;span class="p">??&lt;/span> &lt;span class="n">hostingEnv&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ContentRootFileProvider&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Next, this method is called for every request. Only GET/HEAD requests should be handled. POSTs/PUTs, etc. should never touch a static file.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">public&lt;/span> &lt;span class="n">Task&lt;/span> &lt;span class="n">Invoke&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">HttpContext&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">GetEndpoint&lt;/span>&lt;span class="p">()?.&lt;/span>&lt;span class="n">RequestDelegate&lt;/span> &lt;span class="k">is&lt;/span> &lt;span class="kc">null&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;amp;&amp;amp;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Method&lt;/span> &lt;span class="p">==&lt;/span> &lt;span class="s">&amp;#34;GET&amp;#34;&lt;/span> &lt;span class="p">||&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Method&lt;/span> &lt;span class="p">==&lt;/span> &lt;span class="s">&amp;#34;HEAD&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">var&lt;/span> &lt;span class="n">subpath&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Path&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Add a slash at the end if it doesn&amp;#39;t exist&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// This ensures that when we append the file name, it&amp;#39;s a valid path&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// TODO: Consider doing a client redirect here instead to ensure&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// the user is always on a consistent location&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(!&lt;/span>&lt;span class="n">subpath&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="p">!.&lt;/span>&lt;span class="n">EndsWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">subpath&lt;/span> &lt;span class="p">+=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">PathString&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Next, we check to see if the folder exists. If the client passes in an &lt;code>Accept-Encoding&lt;/code> header, we scan to see if we support any of the encodings and have a pre-compressed file on disk. We then set the path and, if applicable, a response &lt;code>Content-Encoding&lt;/code> header and pass it along to the next middleware to handle.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">var&lt;/span> &lt;span class="n">dirContents&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">_fileProvider&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">GetDirectoryContents&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">subpath&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="p">!);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">dirContents&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Exists&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">var&lt;/span> &lt;span class="n">acceptEncoding&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Headers&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">AcceptEncoding&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">foreach&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">var&lt;/span> &lt;span class="n">encoding&lt;/span> &lt;span class="k">in&lt;/span> &lt;span class="n">acceptEncoding&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// We use StartsWith because request encodings can include a &amp;#34;;q=0.5&amp;#34; to specify quality&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// but we ignore that&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">encoding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">StartsWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;br&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">FileExists&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">subpath&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="p">!,&lt;/span> &lt;span class="n">fileToFind&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="s">&amp;#34;.br&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">fileToFind&lt;/span> &lt;span class="p">+=&lt;/span> &lt;span class="s">&amp;#34;.br&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Response&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Headers&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ContentEncoding&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;br&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">break&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">encoding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">StartsWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;gzip&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">FileExists&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">subpath&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="p">!,&lt;/span> &lt;span class="n">fileToFind&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="s">&amp;#34;.gz&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">fileToFind&lt;/span> &lt;span class="p">+=&lt;/span> &lt;span class="s">&amp;#34;.gz&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Response&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Headers&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ContentEncoding&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;gzip&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">break&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Match found, re-write the url. A later middleware will actually serve the file.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Path&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">PathString&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">subpath&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="n">fileToFind&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">_next&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then replace the previously used &lt;code>app.UseDefaultFiles&lt;/code>, with our custom middleware:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">var&lt;/span> &lt;span class="n">app&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">builder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Build&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// app.UseDefaultFiles()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">UseMiddleware&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">CustomDefaultFileMiddleware&lt;/span>&lt;span class="p">&amp;gt;();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">UseStaticFiles&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Run&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="response-headers">Response headers&lt;/h2>
&lt;p>But wait, there&amp;rsquo;s a problem. Opening up my website in a web browser causes me to download the page instead of viewing it. Looking at the response, the &lt;code>Content-Type&lt;/code> response header shows a gzip MIME type vs HTML.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-http" data-lang="http">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">GET&lt;/span> &lt;span class="nn">/&lt;/span> &lt;span class="kr">HTTP&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="m">1.1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Host&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="l">localhost:1313&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Accept-Encoding&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="l">gzip, deflate, br&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="g">HTTP/1.1 200 OK
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="g">Content-Encoding: gzip
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="g">Content-Length: 4499
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="g">Content-Type: application/x-gzip
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This is happening because our &lt;code>CustomDefaultFileMiddleware&lt;/code> says &amp;ldquo;hey serve up the &lt;code>index.html.gz&lt;/code> file please&amp;rdquo; and the &lt;code>StaticFileMiddleware&lt;/code> says &amp;ldquo;okay here we go, btw the mime type for &lt;code>.gz&lt;/code> is &lt;code>application/x-gzip&lt;/code>.&amp;rdquo; The correct HTTP implementation is to pass the MIME type as &lt;code>text/html&lt;/code>, the MIME type of the un-encoded file.&lt;/p>
&lt;p>This can be done by adding an &lt;code>OnPrepareResponse&lt;/code> on the &lt;code>UseStaticFiles&lt;/code> step that corrects the response MIME type.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// app.UseStaticFiles()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kt">var&lt;/span> &lt;span class="n">contentTypeProvider&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">FileExtensionContentTypeProvider&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">UseStaticFiles&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="n">StaticFileOptions&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ContentTypeProvider&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">contentTypeProvider&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">OnPrepareResponse&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Response&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Headers&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ContentEncoding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Count&lt;/span> &lt;span class="p">&amp;gt;&lt;/span> &lt;span class="m">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">var&lt;/span> &lt;span class="n">subGz&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Path&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="p">[..^&lt;/span>&lt;span class="m">3&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">contentTypeProvider&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">TryGetContentType&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">subGz&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">out&lt;/span> &lt;span class="kt">var&lt;/span> &lt;span class="n">contentType&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Response&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Headers&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ContentType&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">contentType&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now, the response &lt;code>Content-Type&lt;/code> header shows the MIME type of the file being served and everything works.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-http" data-lang="http">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">GET&lt;/span> &lt;span class="nn">/&lt;/span> &lt;span class="kr">HTTP&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="m">1.1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Host&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="l">localhost:1313&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Accept-Encoding&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="l">gzip, deflate, br&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="g">HTTP/1.1 200 OK
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="g">Content-Encoding: gzip
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="g">Content-Length: 4499
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="g">Content-Type: text/html
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="cache-control">Cache-Control&lt;/h1>
&lt;p>Next up, we need to allow client caching. By default, no caching headers are being set to the client, so nothing is being cached and the client has to make a request to the server for every single file. Caching increases browsing performance and is controlled through the Cache-Control header (&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control" target="_blank" rel="noopener"
>MDN docs&lt;/a>).&lt;/p>
&lt;p>In my blog, I only set caching headers for static assets that are hashed, like my CSS, JS, and images. Other content, such as HTML, will not be cached to ensure clients revalidate and see if there&amp;rsquo;s any new content.&lt;/p>
&lt;p>To make this happen, we need yet another middleware handler. We&amp;rsquo;ll add this to the &lt;code>OnPrepareResponse&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="k">readonly&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="n">MAX_CACHE_HEADER&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">CacheControlHeaderValue&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Public&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">MaxAge&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">TimeSpan&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">FromDays&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="m">365&lt;/span> &lt;span class="p">*&lt;/span> &lt;span class="m">10&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 10 years&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}.&lt;/span>&lt;span class="n">ToString&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kt">var&lt;/span> &lt;span class="n">contentTypeProvider&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">FileExtensionContentTypeProvider&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">UseStaticFiles&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="n">StaticFileOptions&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ContentTypeProvider&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">contentTypeProvider&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">OnPrepareResponse&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">var&lt;/span> &lt;span class="n">response&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Response&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">var&lt;/span> &lt;span class="n">workingPath&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Path&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">response&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Headers&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ContentEncoding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Count&lt;/span> &lt;span class="p">&amp;gt;&lt;/span> &lt;span class="m">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">workingPath&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">workingPath&lt;/span>&lt;span class="p">[..^&lt;/span>&lt;span class="m">3&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">contentTypeProvider&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">TryGetContentType&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">workingPath&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">out&lt;/span> &lt;span class="kt">var&lt;/span> &lt;span class="n">contentType&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">response&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Headers&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ContentType&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">contentType&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span> &lt;span class="n">urlPath&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">StartsWithSegments&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/scss&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">||&lt;/span> &lt;span class="n">urlPath&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">StartsWithSegments&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/ts&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Don&amp;#39;t cache SVGs because Hugo doesn&amp;#39;t hash the file name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// If we fix that, we can check the mime type&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">||&lt;/span> &lt;span class="n">workingPath&lt;/span>&lt;span class="p">[^&lt;/span>&lt;span class="m">4.&lt;/span>&lt;span class="p">.]&lt;/span> &lt;span class="p">==&lt;/span> &lt;span class="s">&amp;#34;.png&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">response&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Headers&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">CacheControl&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">MAX_CACHE_HEADER&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>With that, we now have caching headers set and clients won&amp;rsquo;t refetch static content.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-http" data-lang="http">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">GET&lt;/span> &lt;span class="nn">/ts/main.abc.js&lt;/span> &lt;span class="kr">HTTP&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="m">1.1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Host&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="l">localhost:1313&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Accept-Encoding&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="l">gzip, deflate, br&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="g">HTTP/1.1 200 OK
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="g">Content-Length: 7835
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="g">Content-Type: text/javascript
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="g">Cache-Control: public, max-age=315360000
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>With these changes, I now have a fully equivalent replacement for my previous NGINX web server. I had to implement code to select which default file to return when a user requests &lt;code>/foobar/&lt;/code> and support serving pre-compressed static files which required code to fix MIME types. This was trivially handled by NGINX with the &lt;code>gzip_static on&lt;/code> directive. I also had to add cache control headers, also handled in one line with NGINX&amp;rsquo;s &lt;code>expires max&lt;/code> directive.&lt;/p>
&lt;p>Looking at this comparison in isolation, I ended up with more code compared to what NGINX already handles out of the box. However, I did this not just for the sake of change, but because when I implemented ActivityPub, I built on-top of this server and implemented features that were not easy with NGINX.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F03%2Fthis-blog-is-now-on-asp.net-core%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=This+blog+is+now+on+ASP.NET+Core" style="border:0" alt="" /></description></item><item><title>Fixing common Hugo encoding problems</title><link>https://www.technowizardry.net/2025/03/fixing-common-hugo-encoding-problems/</link><pubDate>Sat, 15 Mar 2025 21:10:00 -0700</pubDate><guid>https://www.technowizardry.net/2025/03/fixing-common-hugo-encoding-problems/</guid><summary>&lt;p>I posted a link to my blog on Slack and was greeted with HTML entities right in the website summary. I could see certain characters like the apostrophe &lt;code>’&lt;/code> being encoded as &lt;code>&amp;amp;rsquo;&lt;/code>.&lt;/p>
&lt;p>Here&amp;rsquo;s how I fixed this problem.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/03/fixing-common-hugo-encoding-problems/images/slack-bad-encoding.png"
width="1296"
height="518"
srcset="https://www.technowizardry.net/2025/03/fixing-common-hugo-encoding-problems/images/slack-bad-encoding_hu_453f7a15513523ef.png 480w, https://www.technowizardry.net/2025/03/fixing-common-hugo-encoding-problems/images/slack-bad-encoding_hu_b46c68fc270d6d35.png 1024w"
loading="lazy"
alt="A screenshot from Slack of a post of this blog. The description includes HTML entities literally in the description: … they&amp;rsquo;ll … instead of they’ll"
class="gallery-image"
data-flex-grow="250"
data-flex-basis="600px"
>&lt;/p></summary><description>&lt;p>I posted a link to my blog on Slack and was greeted with HTML entities right in the website summary. I could see certain characters like the apostrophe &lt;code>’&lt;/code> being encoded as &lt;code>&amp;amp;rsquo;&lt;/code>.&lt;/p>
&lt;p>Here&amp;rsquo;s how I fixed this problem.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/03/fixing-common-hugo-encoding-problems/images/slack-bad-encoding.png"
width="1296"
height="518"
srcset="https://www.technowizardry.net/2025/03/fixing-common-hugo-encoding-problems/images/slack-bad-encoding_hu_453f7a15513523ef.png 480w, https://www.technowizardry.net/2025/03/fixing-common-hugo-encoding-problems/images/slack-bad-encoding_hu_b46c68fc270d6d35.png 1024w"
loading="lazy"
alt="A screenshot from Slack of a post of this blog. The description includes HTML entities literally in the description: … they&amp;rsquo;ll … instead of they’ll"
class="gallery-image"
data-flex-grow="250"
data-flex-basis="600px"
>&lt;/p>
&lt;h1 id="html-meta-tags">HTML Meta Tags&lt;/h1>
&lt;h2 id="investigating">Investigating&lt;/h2>
&lt;p>Slack and social media sites use meta tags defined by the &lt;a class="link" href="https://ogp.me/" target="_blank" rel="noopener"
>OpenGraph protocol&lt;/a> to fetch information like the summary, publish dates, and images relevant for posting on a feed. On this post, we can see those tags contain HTML entities.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">property&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;og:description&amp;#39;&lt;/span> &lt;span class="na">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;I work at AWS, and predictably we use a lot of AWS cloud services. In many cases, when an engineer looks for a computer platform, they&amp;amp;rsquo;ll often go directly to AWS Lambda because &amp;amp;ldquo;it&amp;amp;rsquo;s Serverless&amp;amp;rdquo; with the justification that it&amp;amp;rsquo;s simple and the best option no matter what and not want to explore alternatives. The FaaS (Functions as a Service) compute style is great for a certain category of system problems&amp;amp;ndash;ones in which you don&amp;amp;rsquo;t need strict control over how it executes. AWS Lambda only exposes limited controls and depending on what your workload is like, you could run into unexpected scaling and failure modes. There are alternative compute environments that avoid those limitations that you should know about. &amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">property&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;article:published_time&amp;#39;&lt;/span> &lt;span class="na">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;2024-09-29T00:00:00&amp;amp;#43;00:00&amp;#39;&lt;/span>&lt;span class="p">/&amp;gt;&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">property&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;article:modified_time&amp;#39;&lt;/span> &lt;span class="na">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;2024-09-29T00:00:00&amp;amp;#43;00:00&amp;#39;&lt;/span>&lt;span class="p">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>My Hugo template had a file that looked like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-html-template" data-lang="go-html-template">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">:=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">partialCached&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;data/title&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.RelPermalink&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">-}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$description&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">:=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">partialCached&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;data/description&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.RelPermalink&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">-}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">property&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;og:title&amp;#39;&lt;/span> &lt;span class="na">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="s">&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">property&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;og:description&amp;#39;&lt;/span> &lt;span class="na">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$description&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="s">&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">property&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;og:url&amp;#39;&lt;/span> &lt;span class="na">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Permalink&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="s">&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">property&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;og:site_name&amp;#39;&lt;/span> &lt;span class="na">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Site.Title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="s">&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If we look at Hugo&amp;rsquo;s repo in the &lt;a class="link" href="https://github.com/gohugoio/hugo/blame/v0.145.0/tpl/tplimpl/embedded/templates/opengraph.html" target="_blank" rel="noopener"
>opengraph.html template&lt;/a>, they use &lt;a class="link" href="https://gohugo.io/functions/transform/plainify/" target="_blank" rel="noopener"
>plainify&lt;/a> to remove HTML tags, then &lt;a class="link" href="https://gohugo.io/functions/transform/htmlunescape/" target="_blank" rel="noopener"
>htmlUnescape&lt;/a> to remove HTML entities and encoding from the strings.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-html-template" data-lang="go-html-template">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">with&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">or&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Description&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Summary&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">site&lt;/span>&lt;span class="na">.Params.description&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">plainify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">htmlUnescape&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">property&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;og:description&amp;#34;&lt;/span> &lt;span class="na">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">trim&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;\n\r\t &amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="s">&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">end&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="solution">Solution&lt;/h2>
&lt;p>Let&amp;rsquo;s do a similar fix.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-html-template" data-lang="go-html-template">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$description&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">:=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">partialCached&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;data/description&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.RelPermalink&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">-}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$description&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">:=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$description&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">plainify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">htmlUnescape&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$description&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">:=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">trim&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$description&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;\n&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">-}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$description&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">:=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$description&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">safeHTMLAttr&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">-}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">property&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;og:title&amp;#39;&lt;/span> &lt;span class="na">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">htmlUnescape&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">safeHTMLAttr&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="s">&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">property&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;og:description&amp;#39;&lt;/span> &lt;span class="na">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$description&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="s">&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now we get the following HTML:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">property&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;og:description&amp;#39;&lt;/span> &lt;span class="na">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;I work at AWS, and predictably we use a lot of AWS cloud services. In many cases, when an engineer looks for a computer platform, they’ll often go directly to AWS Lambda because “it’s Serverless” with the justification that it’s simple and the best option no matter what and not want to explore alternatives.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">The FaaS (Functions as a Service) compute style is great for a certain category of system problems–ones in which you don’t need strict control over how it executes. AWS Lambda only exposes limited controls and depending on what your workload is like, you could run into unexpected scaling and failure modes. There are alternative compute environments that avoid those limitations that you should know about.&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And now my OpenGraph tags are generated correctly.&lt;/p>
&lt;h1 id="activitypub-json">ActivityPub JSON&lt;/h1>
&lt;h2 id="investigation">Investigation&lt;/h2>
&lt;p>My &lt;del>secret&lt;/del> upcoming project is adding ActivityPub support to this blog. It&amp;rsquo;s not finished yet and a separate post will be made, but here we can see a similar encoding problem. All the content was being HTML escaped which meant that links were not clickable and were directly visible in the post.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/03/fixing-common-hugo-encoding-problems/images/mastodon-bad-encoding.png"
width="582"
height="650"
srcset="https://www.technowizardry.net/2025/03/fixing-common-hugo-encoding-problems/images/mastodon-bad-encoding_hu_ad5672ef5297040b.png 480w, https://www.technowizardry.net/2025/03/fixing-common-hugo-encoding-problems/images/mastodon-bad-encoding_hu_3282ecdb9eedb755.png 1024w"
loading="lazy"
alt="A screenshot of my blog post in Mastodon. You can see HTML elements directly in the post. Example: I was using a mixture of "
class="gallery-image"
data-flex-grow="89"
data-flex-basis="214px"
>&lt;/p>
&lt;p>This is exactly the same problem as before. Say I&amp;rsquo;ve got a file: &lt;code>layouts/posts/single.post_json.json&lt;/code> that generates the ActivityPub JSON for a single post with the following subset:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-template" data-lang="go-template">&lt;span class="line">&lt;span class="cl">&lt;span class="x">{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;id&amp;#34;: &amp;#34;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Permalink&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;type&amp;#34;: &amp;#34;Article&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;content1&amp;#34;: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">printf&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;%s&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Summary&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;content2&amp;#34;: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Content&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">htmlUnescape&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;content3&amp;#34;: &amp;#34;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="na">.Summary&lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x">}
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Looking at these two encoding types, which one do you think is correct? Let&amp;rsquo;s get a complex test post that contains quotation marks:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">---
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">title: test \&amp;#34;test2&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">author: &amp;#34;author\&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">---
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">test &amp;#34;test&amp;#34;&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">br&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> [&lt;span class="nt">test&lt;/span>](&lt;span class="na">https://example.com&lt;/span>). test&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Running &lt;code>hugo&lt;/code> gives me the following output file:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;content1&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;\u003cp\u003ehere \u0026ldquo;is\u0026rdquo; a summary\u003cbr\u003e \u003ca class=\&amp;#34;link\&amp;#34; href=\&amp;#34;https://example.com\&amp;#34; target=\&amp;#34;_blank\&amp;#34; rel=\&amp;#34;noopener\&amp;#34;\n \u003etest\u003c/a\u003e. test\u0026quot;\u003c/p\u003e&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;content2&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;\u003cp\u003ehere “is” a summary\u003cbr\u003e \u003ca class=\&amp;#34;link\&amp;#34; href=\&amp;#34;https://example.com\&amp;#34; target=\&amp;#34;_blank\&amp;#34; rel=\&amp;#34;noopener\&amp;#34;\n \u003etest\u003c/a\u003e. test\&amp;#34;\u003c/p\u003e\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;content3&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;p&amp;gt;here &amp;amp;ldquo;is&amp;amp;rdquo; a summary&amp;lt;br&amp;gt; &amp;lt;a class=&amp;#34;&lt;/span>&lt;span class="err">link&lt;/span>&lt;span class="s2">&amp;#34; href=&amp;#34;&lt;/span>&lt;span class="err">https&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="c1">//example.com&amp;#34; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noopener&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="err">&amp;gt;test&amp;lt;/a&amp;gt;.&lt;/span> &lt;span class="err">test&amp;amp;quot;&amp;lt;/p&amp;gt;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="solution-1">Solution&lt;/h2>
&lt;p>Looking at &lt;code>content3&lt;/code>, it incorrectly serializes any quotation marks and generates corrupted JSON syntax, so that&amp;rsquo;s out. Looking at &lt;code>content1&lt;/code>, we see examples of HTML entities, like &lt;code>\u0026ldquo;&lt;/code> . That&amp;rsquo;s why we&amp;rsquo;re seeing the HTML tags written raw in Mastodon, instead of as a clickable link as we expected. &lt;code>content2&lt;/code> does not include any HTML entities, thus is the correct format.&lt;/p>
&lt;p>Those &lt;code>\u003c&lt;/code> Unicode encodings are extraneous, we can use &lt;a class="link" href="https://gohugo.io/functions/encoding/jsonify/" target="_blank" rel="noopener"
>jsonify&lt;/a>&amp;rsquo;s options to disable them. Here&amp;rsquo;s the final:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-template" data-lang="go-template">&lt;span class="line">&lt;span class="cl">&lt;span class="x">{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> &amp;#34;content&amp;#34;: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Content&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">htmlUnescape&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nx">dict&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;noHTMLEscape&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">true&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x"> ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="x">}
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This pattern of &lt;code>string | htmlUnescape | jsonify (dict &amp;quot;noHTMLEscape&amp;quot; true)&lt;/code> should only be used when you intentionally want to emit HTML tags into a JSON field value. Don&amp;rsquo;t allow user defined content to enter these values without sanitizing it, but if you&amp;rsquo;re using Hugo, it&amp;rsquo;s probably all static defined by you.&lt;/p>
&lt;h1 id="url-encoding-inside-rssxml">URL Encoding inside RSS/XML&lt;/h1>
&lt;p>My next problem was to embed an image into an RSS feed. At first I did:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-template" data-lang="go-template">&lt;span class="line">&lt;span class="cl">&lt;span class="x">&amp;lt;img referrerpolicy=&amp;#34;no-referrer-when-downgrade&amp;#34; src=&amp;#34;https://www.technowizardry.net/rss_view?action_name=&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">&amp;amp;amp;url=&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Permalink&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">&amp;#34; style=&amp;#34;border:0&amp;#34; alt=&amp;#34;&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>That incorrectly generates the following HTML:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="ni">&amp;amp;lt;&lt;/span>img referrerpolicy=&amp;#34;no-referrer-when-downgrade&amp;#34; src=&amp;#34;https://www.technowizardry.net/rss_view?action_name=Fixing common Hugo encoding problems&lt;span class="ni">&amp;amp;amp;&lt;/span>url=https://www.technowizardry.net/2025/03/fixing-common-hugo-encoding-problems&amp;#34; style=&amp;#34;border:0&amp;#34; alt=&amp;#34;&amp;#34; /&lt;span class="ni">&amp;amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Multiple issues here:&lt;/p>
&lt;ol>
&lt;li>The HTML tags use &amp;lt;&amp;gt; instead of &amp;lt;&amp;gt;&lt;/li>
&lt;li>The title and the URL is not URL encoded, so the request doesn&amp;rsquo;t get serialized correctly.&lt;/li>
&lt;/ol>
&lt;h2 id="solution-2">Solution&lt;/h2>
&lt;p>To serialize the HTML tags, we change from &lt;code>&amp;lt;&lt;/code> to &lt;code>{{ &amp;quot;&amp;lt;&amp;quot; | html }}&lt;/code> and to generate the URL, we use &lt;code>{{ .Permalink | urlquery }}&lt;/code>. The &lt;a class="link" href="https://gohugo.io/functions/go-template/urlquery/" target="_blank" rel="noopener"
>&lt;code>urlquery&lt;/code>&lt;/a> method URL-encodes each variable to fit within the URL parameters.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-template" data-lang="go-template">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;&amp;lt;&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">html&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">img referrerpolicy=&amp;#34;no-referrer-when-downgrade&amp;#34; src=&amp;#34;https://www.technowizardry.net/rss_view?url=&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Permalink&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">urlquery&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">%3Fmtm_campaign%3Drss&amp;amp;amp;action_name=&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">urlquery&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">&amp;#34; style=&amp;#34;border:0&amp;#34; alt=&amp;#34;&amp;#34; &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;/&amp;gt;&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">html&lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="summary">Summary&lt;/h1>
&lt;p>By default, Hugo tries to escape every single string to avoid emitting HTML elements where they don&amp;rsquo;t belong. This is a good security mechanism, but sometimes you need to generate non-HTML files, like JSON, or sometimes you want the HTML elements to emitted in an output artifact without them being escaped.&lt;/p>
&lt;p>If you want HTML elements to be removed and are emitting into an HTML element, do this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-html-template" data-lang="go-html-template">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">property&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;og:description&amp;#34;&lt;/span> &lt;span class="na">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Summary&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">plainify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">htmlUnescape&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="s">&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If you&amp;rsquo;re emitting into a JSON file, do this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-template" data-lang="go-template">&lt;span class="line">&lt;span class="cl">&lt;span class="x">&amp;#34;field&amp;#34;: &lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Content&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">htmlUnescape&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nx">dict&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;noHTMLEscape&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">true&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="x">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I hope this helps. Stay tuned for further posts on ActivityPub&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F03%2Ffixing-common-hugo-encoding-problems%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Fixing+common+Hugo+encoding+problems" style="border:0" alt="" /></description></item><item><title>Email spam filtering with Rspamd on K8s</title><link>https://www.technowizardry.net/2025/03/email-spam-filtering-with-rspamd-on-k8s/</link><pubDate>Mon, 10 Mar 2025 21:07:00 +0000</pubDate><guid>https://www.technowizardry.net/2025/03/email-spam-filtering-with-rspamd-on-k8s/</guid><summary>&lt;p>I&amp;rsquo;ve been running my own mail server for well over ten years now. It&amp;rsquo;s pretty old, so it&amp;rsquo;s hard to make changes to it, but it&amp;rsquo;s running in Kubernetes. I was using a mixture of &lt;a class="link" href="https://www.postfix.org/" target="_blank" rel="noopener"
>Postfix&lt;/a>, &lt;a class="link" href="http://www.opendkim.org/" target="_blank" rel="noopener"
>OpenDKIM&lt;/a>, &lt;a class="link" href="https://github.com/trusteddomainproject/OpenDMARC" target="_blank" rel="noopener"
>OpenDMARC&lt;/a>, and &lt;a class="link" href="https://amavis.org/" target="_blank" rel="noopener"
>Amavis&lt;/a> for spam filtering with SpamAssasin, but it wasn&amp;rsquo;t very good at catching spam. Instead, its time move to &lt;a class="link" href="https://www.rspamd.com/" target="_blank" rel="noopener"
>rspamd&lt;/a>. It&amp;rsquo;s much newer and encapsulates DKIM, DMARC, DNS based blacklisting, bayesian filtering, etc. all in one single tool.&lt;/p>
&lt;p>Here&amp;rsquo;s my notes on migrating, what it took to get it going and some tweaks I made.&lt;/p></summary><description>&lt;p>I&amp;rsquo;ve been running my own mail server for well over ten years now. It&amp;rsquo;s pretty old, so it&amp;rsquo;s hard to make changes to it, but it&amp;rsquo;s running in Kubernetes. I was using a mixture of &lt;a class="link" href="https://www.postfix.org/" target="_blank" rel="noopener"
>Postfix&lt;/a>, &lt;a class="link" href="http://www.opendkim.org/" target="_blank" rel="noopener"
>OpenDKIM&lt;/a>, &lt;a class="link" href="https://github.com/trusteddomainproject/OpenDMARC" target="_blank" rel="noopener"
>OpenDMARC&lt;/a>, and &lt;a class="link" href="https://amavis.org/" target="_blank" rel="noopener"
>Amavis&lt;/a> for spam filtering with SpamAssasin, but it wasn&amp;rsquo;t very good at catching spam. Instead, its time move to &lt;a class="link" href="https://www.rspamd.com/" target="_blank" rel="noopener"
>rspamd&lt;/a>. It&amp;rsquo;s much newer and encapsulates DKIM, DMARC, DNS based blacklisting, bayesian filtering, etc. all in one single tool.&lt;/p>
&lt;p>Here&amp;rsquo;s my notes on migrating, what it took to get it going and some tweaks I made.&lt;/p>
&lt;h1 id="setup-redisvalkey">Setup Redis/Valkey&lt;/h1>
&lt;p>Rspamd uses Redis to store runtime stats and config. Deploy it using &lt;a class="link" href="https://bitnami.com/stack/valkey/helm" target="_blank" rel="noopener"
>Helm&lt;/a>, or manually. I&amp;rsquo;m going to assume it&amp;rsquo;s deployed in the &lt;code>mail&lt;/code> namespace with a service named called &lt;code>rspamd-redis&lt;/code>.&lt;/p>
&lt;h1 id="rspamd-configuration">Rspamd Configuration&lt;/h1>
&lt;p>I started with a basic configuration defined below. Starting with this meant I could skip the &lt;a class="link" href="https://rspamd.com/doc/tutorials/quickstart.html#running-rspamd" target="_blank" rel="noopener"
>quick start guide&lt;/a> that mandated that I run the &lt;code>rspamadm configwizard&lt;/code> command, though you might have some slightly different needs&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">classifier-bayes.conf&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> servers = &amp;#34;rspamd-redis.mail.svc.cluster.local.&amp;#34;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> backend = &amp;#34;redis&amp;#34;;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">redis.conf&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> read_servers = &amp;#34;rspamd-redis.mail.svc.cluster.local.&amp;#34;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> write_servers = &amp;#34;rspamd-redis.mail.svc.cluster.local.&amp;#34;;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">worker-normal.inc&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> bind_socket = &amp;#34;0.0.0.0:11333&amp;#34;;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">worker-proxy.inc&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> milter = yes; # Enable milter mode
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> timeout = 120s; # Needed for Milter usually
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> upstream &amp;#34;local&amp;#34; {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> default = yes; # Self-scan upstreams are always default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> self_scan = yes; # Enable self-scan
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rspamd-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mail&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then deployed rspamd like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PersistentVolumeClaim&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rspamd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mail&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">accessModes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">ReadWriteOnce&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">storage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">256Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rspamd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mail&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rspamd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">component&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rspamd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">component&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rspamd/rspamd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">imagePullPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">IfNotPresent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rspamd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/etc/rspamd/local.d&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/var/lib/rspamd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">subPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">configMap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">defaultMode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">420&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rspamd-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">persistentVolumeClaim&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">claimName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rspamd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then in postfix, I remove all the existing filters and use only rspamd:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl">&lt;span class="gd">- non_smtpd_milters = inet:localhost:9999, inet:localhost:9998,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">- content_filter = amavisfeed:[127.0.0.1]:10024
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ smtpd_milters = inet:rspamd.mail.svc.cluster.local:11332
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ non_smtpd_milters = inet:rspamd.mail.svc.cluster.local:11332
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="dns-blocklisting">DNS Blocklisting&lt;/h1>
&lt;p>Upon starting rspamd, I get a bunch of warnings like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">rspamd_monitored_dns_cb: DNS reply returned &amp;#39;no error&amp;#39; for zen.spamhaus.org while &amp;#39;no records with this name&amp;#39; was expected when querying for &amp;#39;1.0.0.127.zen.spamhaus.org&amp;#39;(likely DNS spoofing or BL internal issues)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>It filters email, but still lets through a lot of obvious spam. This happens because rspamd queries several DNS-based block-list providers, like &lt;a class="link" href="https://www.spamhaus.org/" target="_blank" rel="noopener"
>Spamhaus&lt;/a>, to identify which IP addresses commonly send spam and reject them. But providers, like Spamhaus, have a free-tier and paid tiers and chose to &lt;a class="link" href="https://www.spamhaus.com/resource-center/successfully-accessing-spamhauss-free-block-lists-using-a-public-dns/" target="_blank" rel="noopener"
>block public DNS servers&lt;/a> because public DNS servers anonymize the source for queries and Spamhaus is unable to limit traffic. I&amp;rsquo;m a very low-volume mail receiver, so the free tier is fine.&lt;/p>
&lt;p>My server was just using the default DNS server provided by my dedicated server provider, OVH, which they considered to be a public resolver. Thus every query was being rejected.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/03/email-spam-filtering-with-rspamd-on-k8s/images/dns-public-resolver.svg"
loading="lazy"
alt="A diagram showing a query to a public resolver which then goes to Spamhaus’ server, but they reject the query"
>&lt;/p>
&lt;p>To fix this, I&amp;rsquo;m going to have to change my resolver to not use a public resolver and instead use a resolver that does not use a public resolver. Instead, I&amp;rsquo;ll use a resolving DNS server, one that sends queries directly to Spamhaus, i.e. a recursive resolving DNS server. I just needed a simple containerized server. &lt;a class="link" href="https://coredns.io" target="_blank" rel="noopener"
>CoreDNS&lt;/a>, which I normally use, did not support recursive DNS queries without an external plugin. Instead, I came across &lt;a class="link" href="https://nlnetlabs.nl/projects/unbound/about/" target="_blank" rel="noopener"
>unbound&lt;/a> which natively did.&lt;/p>
&lt;h2 id="configuration">Configuration&lt;/h2>
&lt;p>The following is the configuration I used. The server is configured to forward queries for &lt;code>.cluster.local.&lt;/code> to the Kubernetes internal &lt;code>kube-dns&lt;/code> instance. This is required because rspamd needs to connect to other Kubernetes services, such as Redis.&lt;/p>
&lt;p>Additionally, we must allow all DNS blocklist domains to return internal IP addresses via the &lt;a class="link" href="https://unbound.docs.nlnetlabs.nl/en/latest/manpages/unbound.conf.html#unbound-conf-private-domain" target="_blank" rel="noopener"
>private-domain&lt;/a> configuration option. This is critical because DNS-based block lists commonly use A DNS queries that return IP addresses to signal what action a mail server should take. For example, Spamhaus returns &lt;code>127.0.0.2&lt;/code> when a request is on a specific filter list (&lt;a class="link" href="https://www.spamhaus.org/faqs/dnsbl-usage#what-do-the-127-return-codes-mean-in-dnsbls" target="_blank" rel="noopener"
>other responses&lt;/a>.) Without this configuration, unbound would discard all responses.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unbound.conf&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> server:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> chroot: &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # Skip IPv6 since my cluster is not dual-stack yet
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> do-ip6: no
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> private-domain: cluster.local.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # Add all DNS blocklists below
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> private-domain: dnsbl.manitu.net.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> private-domain: surbl.org.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> private-domain: spamhaus.org.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> private-domain: spameatingmonkey.net.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # Forwards all queries for internal cluster queries to kube-dns
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> forward-zone:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> name: &amp;#34;cluster.local.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> forward-addr: 10.43.0.10@53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rspamd-dns-resolver&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mail&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="switch-to-unbound">Switch to unbound&lt;/h2>
&lt;p>Next, we need to start the DNS server. I added it as a side-car container with the existing Rspamd service and configured the pod&amp;rsquo;s &lt;a class="link" href="https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/" target="_blank" rel="noopener"
>&lt;code>dnsConfig&lt;/code>&lt;/a> to unbound.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="hl">&lt;span class="lnt">12
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">13
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">14
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">15
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">16
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">17
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">18
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">19
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">20
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">21
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">22
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">23
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">24
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">25
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">26
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">27
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">28
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">29
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">30
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">31
&lt;/span>&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="hl">&lt;span class="lnt">34
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">35
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">36
&lt;/span>&lt;/span>&lt;span class="hl">&lt;span class="lnt">37
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rspamd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mail&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rspamd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ... elided&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mvance/unbound:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">imagePullPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">IfNotPresent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">128Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">8m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">64Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/opt/unbound/etc/unbound/unbound.conf&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dns-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">subPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">unbound.conf&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dnsConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nameservers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="m">127.0.0.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">options&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ndots&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;1&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dnsPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">None&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">configMap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">defaultMode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">420&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rspamd-dns-resolver&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dns-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After restarting Rspamd, it started using the DNS block lists and was able to flag even more obvious spam.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/03/email-spam-filtering-with-rspamd-on-k8s/images/rspamd-example-dnsbl.png"
width="523"
height="171"
srcset="https://www.technowizardry.net/2025/03/email-spam-filtering-with-rspamd-on-k8s/images/rspamd-example-dnsbl_hu_81d23f1ddb9f742e.png 480w, https://www.technowizardry.net/2025/03/email-spam-filtering-with-rspamd-on-k8s/images/rspamd-example-dnsbl_hu_7bd8da40ddc554df.png 1024w"
loading="lazy"
alt="A screenshot from Rspamd showing results from an email that contained URIs blocked via DNS block listing"
class="gallery-image"
data-flex-grow="305"
data-flex-basis="734px"
>&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>Migrating to rspamd from my previous solution was a great idea. It consolidated and reduced the services I had to run and it actually helped cut down on the spam that made it through with better rules. The UI was very easy to investigate what emails were being marked and why.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/03/email-spam-filtering-with-rspamd-on-k8s/images/rspamd-filter-stats-pie-chart.png"
width="888"
height="539"
srcset="https://www.technowizardry.net/2025/03/email-spam-filtering-with-rspamd-on-k8s/images/rspamd-filter-stats-pie-chart_hu_15bb8879e1bd20e4.png 480w, https://www.technowizardry.net/2025/03/email-spam-filtering-with-rspamd-on-k8s/images/rspamd-filter-stats-pie-chart_hu_6b5f3f48273cfdaf.png 1024w"
loading="lazy"
alt="A pie chart from the Rspamd UI showing 29% of emails being rejected as spam"
class="gallery-image"
data-flex-grow="164"
data-flex-basis="395px"
>&lt;/p>
&lt;p>My previous solution based on Amavis and SpamAssassin was a black-box and it was difficult to understand what&amp;rsquo;s going on without digging through logs.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F03%2Femail-spam-filtering-with-rspamd-on-k8s%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Email+spam+filtering+with+Rspamd+on+K8s" style="border:0" alt="" /></description></item><item><title>Lighting up the holidays with computers</title><link>https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/</link><pubDate>Tue, 14 Jan 2025 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/</guid><summary>&lt;p>This Christmas season, I decided I wanted to play with programmable light strings and see if I could create an interesting light show on the front of my house. I stumbled across &lt;a class="link" href="https://xlights.org/" target="_blank" rel="noopener"
>xlights&lt;/a>, an open source light show sequencing program and got to work.&lt;/p></summary><description>&lt;p>This Christmas season, I decided I wanted to play with programmable light strings and see if I could create an interesting light show on the front of my house. I stumbled across &lt;a class="link" href="https://xlights.org/" target="_blank" rel="noopener"
>xlights&lt;/a>, an open source light show sequencing program and got to work.&lt;/p>
&lt;h1 id="the-hardware">The Hardware&lt;/h1>
&lt;p>My light set-up needed a central light controller that would store the light sequences, send the light data to the strings of lights. There were a few different vendors, but the Xlights communities generally recommended &lt;a class="link" href="https://kulplights.com/" target="_blank" rel="noopener"
>Kulp&lt;/a> or &lt;a class="link" href="https://pixelcontroller.com/store/" target="_blank" rel="noopener"
>Falcon&lt;/a>. I selected a &lt;a class="link" href="https://kulplights.com/product/k8-b/" target="_blank" rel="noopener"
>Kulp K8-B controller&lt;/a> because Kulp controllers were standard BeagleBoard computers that ran standard Linux which gave me some flexibility. I ordered the controller kit through the &lt;a class="link" href="https://www.wiredwatts.com/build-a-controller-kit" target="_blank" rel="noopener"
>build a controller kit tool&lt;/a> on WiredWatts.com and some other lights/cables through other vendors.&lt;/p>
&lt;p>I ended up with a basic selection with:&lt;/p>
&lt;ul>
&lt;li>Kulp K8-B - The light controller&lt;/li>
&lt;li>A waterproof enclosure&lt;/li>
&lt;li>A 350 watt power supply&lt;/li>
&lt;li>Two icicles pixel strings&lt;/li>
&lt;li>Two flood lights (I plan to illuminate some trees)&lt;/li>
&lt;li>Two strings of &lt;a class="link" href="https://wallyslights.com/products/12v-lumidot-pixels" target="_blank" rel="noopener"
>more efficient, string pixels&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>The kit came not assembled and contained no instructions on how to put it together. I mostly just put it together with trial and error.&lt;/p>
&lt;h1 id="electrical-power">Electrical Power&lt;/h1>
&lt;p>Power management is important, but I didn&amp;rsquo;t realize how important it was. I got the lights up on the house and started testing some lights. It was daytime and the lights defaulted to 100% brightness. The lights flashed white, then a few seconds later they didn&amp;rsquo;t turn on. Weird. I thought it was a software issue until I thought to look at the controller which contains a number of fuses and some status LEDs showing if they are working. And low and behold, I blew a fuse.&lt;/p>
&lt;p>Let&amp;rsquo;s break this down. I&amp;rsquo;ve got one outdoor outlet that&amp;rsquo;s rated for 15 amps (1600 watts at 110v). That feeds into a 350 watt power supply at 12v DC (29 amps) that powers the Kulp controller. The controller has eight separate light ports that each have a 5amp fuse. I then connected two strings of lights that each peaked at 5 amps consumption, thus 10 amps total on a 5 amp fuse for 100% brightness and full white. Oops.&lt;/p>
&lt;p>At night, the LEDs are so bright that they can easily be run at only 10-15% brightness which helps to cut the power utilization.&lt;/p>
&lt;p>It would be really helpful if I were able to measure the current consumption across the entire power supply and on individual ports to identify when I&amp;rsquo;m getting close to the limits.&lt;/p>
&lt;h1 id="laying-the-lights-out-in-xlights">Laying the lights out in xLights&lt;/h1>
&lt;p>For example, I have icicles that wrap around my front porch. Like this:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-layout-icicles.png"
width="1159"
height="894"
srcset="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-layout-icicles_hu_9614af73458a9ac5.png 480w, https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-layout-icicles_hu_eeffaa465c422d4a.png 1024w"
loading="lazy"
alt="A screenshot of xLights showing the icicles wrapping around two corners, while also showing the shadow groups straight out."
class="gallery-image"
data-flex-grow="129"
data-flex-basis="311px"
>&lt;/p>
&lt;p>If I were to create an effect that goes from one side to the other, xLights would have to figure out how to apply the effect to the pixels. If I apply a simple slide effect from one side to another, it&amp;rsquo;ll look like the following. Notice how the sides of the icicles seem to fill immediately, then the center fills smoothly.&lt;/p>
&lt;p>Let&amp;rsquo;s create a sample effect: Fill. Click Direction: Left.
&lt;img src="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-sample-effect-1.png"
width="1066"
height="668"
srcset="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-sample-effect-1_hu_96cb7b059ec4f5d7.png 480w, https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-sample-effect-1_hu_871e9ce37be9a6e5.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="159"
data-flex-basis="382px"
>&lt;/p>
&lt;p>Then click the green, right-pointing arrow for Position. Then pick a linear ramp in the bottom row of grey buttons and click okay.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-sample-effect-2.png"
width="781"
height="685"
srcset="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-sample-effect-2_hu_e4217a47a92ea88b.png 480w, https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-sample-effect-2_hu_329c891d4ad5b946.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="114"
data-flex-basis="273px"
>&lt;/p>
&lt;p>Then drag that onto the sequencer
&lt;img src="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-sample-effect-3.png"
width="905"
height="548"
srcset="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-sample-effect-3_hu_da704e221eb39c50.png 480w, https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-sample-effect-3_hu_debe872531649142.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="165"
data-flex-basis="396px"
>&lt;/p>
&lt;p>After clicking the render button &lt;img src="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-render-button.png"
width="61"
height="48"
srcset="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-render-button_hu_3616d4a52d9dae22.png 480w, https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-render-button_hu_57f7dc67d07162e5.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="127"
data-flex-basis="305px"
>, I can then see what the effect looks like in the left preview window.&lt;/p>
&lt;h2 id="fixing-rendering-issues">Fixing Rendering issues&lt;/h2>
&lt;video controls style="width: 100%" muted aria-label="A video showing how the slide effect does not smoothly apply around the sides">
&lt;source src="
/2025/01/lighting-up-the-holidays-with-computers/images/xlights-bad-wrapping.mp4" type="video/mp4" />
&lt;/video>
&lt;p>xLights doesn&amp;rsquo;t know what I mean and what I want, so I need to tell it how it should map this shape. Luckily it provides a feature to fix this that is poorly documented, but commonly used to fix this.&lt;/p>
&lt;p>A Shadow Model is a copy of a model that you can reposition in different ways to get xLights to render effects the way you expect it to. I created two icicles models to represent the sides of the patio, then extended them out laterally like they&amp;rsquo;re sticking out instead of wrapping around the sides. The width, height, and number of light drops should match. From a 2D view looking straight on, it looks like this:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-layout-icicles-with-shadows-annotated.png"
width="736"
height="169"
srcset="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-layout-icicles-with-shadows-annotated_hu_dd68cd8f2bc4e988.png 480w, https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-layout-icicles-with-shadows-annotated_hu_48ee9430bc6da77a.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="435"
data-flex-basis="1045px"
>&lt;/p>
&lt;p>Then each of those get marked as a &lt;em>Shadow Model&lt;/em> for the left/right icicles.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-layout-shadow-group-config.png"
width="697"
height="446"
srcset="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-layout-shadow-group-config_hu_85f074f0f3c25cd.png 480w, https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-layout-shadow-group-config_hu_55a71defbab1afc1.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="156"
data-flex-basis="375px"
>&lt;/p>
&lt;p>Then I create a model group for all my patio icicles that contains the shadow group instead of the actual lights.
&lt;img src="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-layout-icicles-group.png"
width="703"
height="655"
srcset="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-layout-icicles-group_hu_75af34dce38342a6.png 480w, https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-layout-icicles-group_hu_ca285f4e2e3a92c9.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="107"
data-flex-basis="257px"
>&lt;/p>
&lt;p>If I re-apply my effect, I get this:&lt;/p>
&lt;video controls style="width: 100%" muted aria-label="A video showing how the slide effect now smoothly applies to the sides">
&lt;source src="
/2025/01/lighting-up-the-holidays-with-computers/images/xlights-good-wrapping.mp4" type="video/mp4" />
&lt;/video>
&lt;h1 id="the-light-show-sequencing">The Light Show (Sequencing)&lt;/h1>
&lt;p>Now the &lt;del>hard&lt;/del> fun part. Actually coming up with some light shows. I&amp;rsquo;m not good at this at all. I initially wanted to do something to some songs, but that requires some kind of audio transmitter, like an FM transmitter, speaker, or even online streaming. Also, figuring out what effects should go with different parts of the song, was not easy for me being rhythmically challenged.&lt;/p>
&lt;p>Instead, I started simple. A few simple, aesthetically pleasing effects on the lights. For example, my first sequence included just 30 seconds of a meteor effect, plus some subtle lighting of trees in the front yard. One of the Japanese maple trees has beautiful red leaves (until they dropped in the winter), so adding a bit of red light, caused the leaves to pop in color.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-sequence-single-effect.png"
width="1461"
height="434"
srcset="https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-sequence-single-effect_hu_c561c3fab5aef8eb.png 480w, https://www.technowizardry.net/2025/01/lighting-up-the-holidays-with-computers/images/xlights-sequence-single-effect_hu_cb0301b635a07f70.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="336"
data-flex-basis="807px"
>&lt;/p>
&lt;h1 id="a-video">A Video&lt;/h1>
&lt;video controls style="width: 100%" autoplay muted aria-label="A video showing the effects on the house">
&lt;source src="
/2025/01/lighting-up-the-holidays-with-computers/images/final-render.mp4" type="video/mp4" />
&lt;/video>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>I had fun playing with Xlights and getting a basic light show set-up. The sequencing is complicated and it took me some time to figure out how to build the exact effect that I wanted. Next year, I&amp;rsquo;m going to expand it and add more lights.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2025%2F01%2Flighting-up-the-holidays-with-computers%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Lighting+up+the+holidays+with+computers" style="border:0" alt="" /></description></item><item><title>More infrastructure doesn't fix using the wrong infrastructure</title><link>https://www.technowizardry.net/2024/09/more-infra-doesnt-fix-using-the-wrong-infra/</link><pubDate>Sun, 29 Sep 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/09/more-infra-doesnt-fix-using-the-wrong-infra/</guid><summary>&lt;p>I work at AWS, and predictably we use a lot of AWS cloud services. In many cases, when an engineer looks for a computer platform, they&amp;rsquo;ll often go directly to AWS Lambda because &amp;ldquo;it&amp;rsquo;s Serverless&amp;rdquo; with the justification that it&amp;rsquo;s simple and the best option no matter what and not want to explore alternatives.&lt;/p>
&lt;p>The FaaS (Functions as a Service) compute style is great for a certain category of system problems&amp;ndash;ones in which you don&amp;rsquo;t need strict control over how it executes. AWS Lambda only exposes limited controls and depending on what your workload is like, you could run into unexpected scaling and failure modes. There are alternative compute environments that avoid those limitations that you should know about.&lt;/p></summary><description>&lt;p>I work at AWS, and predictably we use a lot of AWS cloud services. In many cases, when an engineer looks for a computer platform, they&amp;rsquo;ll often go directly to AWS Lambda because &amp;ldquo;it&amp;rsquo;s Serverless&amp;rdquo; with the justification that it&amp;rsquo;s simple and the best option no matter what and not want to explore alternatives.&lt;/p>
&lt;p>The FaaS (Functions as a Service) compute style is great for a certain category of system problems&amp;ndash;ones in which you don&amp;rsquo;t need strict control over how it executes. AWS Lambda only exposes limited controls and depending on what your workload is like, you could run into unexpected scaling and failure modes. There are alternative compute environments that avoid those limitations that you should know about.&lt;/p>
&lt;p>Sure, there is a cost to understanding a new service like having to learn a new query language. What I realize is that not all engineers assign the same weight to that knowledge. There&amp;rsquo;s different uncertainty in the new and different.&lt;/p>
&lt;p>However, in this post I make the claim that FaaS platforms aren&amp;rsquo;t the best solution for everything and you should recognize situations where they are good and are not good.&lt;/p>
&lt;!-- more -->
&lt;h1 id="the-example---data-processing-lambda">The Example - Data Processing Lambda&lt;/h1>
&lt;p>In this example, you have a situation where you need to take a file from S3, iterate over it, and do some useful work on it. Maybe you save it to another file, maybe push it into a service. Another developer wants to run a Lambda function that does this. It could work right? Lambda can fetch files, process them, and write them back somewhere else. What&amp;rsquo;s wrong with that?&lt;/p>
&lt;p>Let&amp;rsquo;s take a look at an example implementation as a Lambda function:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">Program&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">implements&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RequestHandler&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Request&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Response&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Response&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">handleRequest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Request&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">event&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Context&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">S3Client&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s3&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">S3Client&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">create&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fileContent&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">readS3File&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">s3&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">output&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">multiplyValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fileContent&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">writeOutputToS3&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">s3&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;s3://outputbucket/foobar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">output&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">readS3File&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">S3Client&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s3&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">filePath&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">throws&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IOException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">ResponseBytes&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">bytes&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">GetObjectRequest&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">request&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">GetObjectRequest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">builder&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">bucket&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;awsexamplebucket-streaming-demo2&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">key&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;inputs/productsStatic.csv&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">build&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">bytes&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getObjectAsBytes&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">request&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">IOException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// TODO&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">bytes&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">content&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">array&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">performBusinessLogic&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Here&amp;#39;s the actual business logic right here&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">10&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">multiplyValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fileContent&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Scanner&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">scanner&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Scanner&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fileContent&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">StringBuilder&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">output&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">StringBuilder&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">while&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">scanner&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">hasNextLine&lt;/span>&lt;span class="p">())&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">line&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">scanner&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">nextLine&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">[]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">values&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">line&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;,&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// TODO: Handle errors with the input file&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">multipliedValue&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">valueOf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Integer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">parseInt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">performBusinessLogic&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">values&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="n">1&lt;/span>&lt;span class="o">]&lt;/span>&lt;span class="p">)));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">output&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">multipliedValue&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;\n&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">output&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">toString&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">writeOutputToS3&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">S3Client&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s3&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">filePath&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">content&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">FileWriter&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FileWriter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;output.csv&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">content&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">s3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">putObject&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">PutObjectRequest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">builder&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">bucket&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;outputbucket&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">key&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;foobar/output.csv&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">build&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">SdkBytes&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">fromInputStream&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FileInputStream&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;output.csv&amp;#34;&lt;/span>&lt;span class="p">)));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">IOException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// TODO: Handle errors and retries&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Is that bad? Should you push back? You might think it&amp;rsquo;s pretty simple to call &lt;code>s3.getObject&lt;/code>, load it, parse it, etc. Why bother with something else? Well, what do you do if the data set starts getting larger and you run into the &lt;a class="link" href="https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html" target="_blank" rel="noopener"
>15 minute Lambda time limit&lt;/a>?&lt;/p>
&lt;p>Just split it into two jobs and have one Lambda function perform part of the work, then the second Lambda function perform the rest. (Think that&amp;rsquo;s crazy? This actually happened in one project I worked with.) Now you have an orchestration problem. What happens if one function succeeds, but the second fails? You have to setup monitoring rules for both and try to handle retries.&lt;/p>
&lt;p>Or maybe you try run multiple threads to process in parallel. Now, you&amp;rsquo;ve got another type of orchestration: batching up work to each thread, waiting for responses, making sure you have your threads scaled to the number of Lambda cores.&lt;/p>
&lt;p>Can you solve these problems? Yes. Should you, probably not. Why? They are not solving the actual business problem. The system is there to provide an answer, anything else is just getting in the way. Using the wrong tool in the toolbox makes your life harder. Picking the right tool, i.e. the right framework, makes it easier.&lt;/p>
&lt;h1 id="trying-another-compute-environment">Trying another compute environment&lt;/h1>
&lt;p>What about &lt;a class="link" href="https://aws.amazon.com/batch/" target="_blank" rel="noopener"
>AWS Batch&lt;/a> or &lt;a class="link" href="https://aws.amazon.com/fargate/" target="_blank" rel="noopener"
>AWS Fargate&lt;/a>? These are two hosted compute environments (AWS still owns the underlying OS), but you own the main method. Thus you have more control over the life-cycle of your process. It does away with the 15 minute limit and gives you some controls on what kind of host you&amp;rsquo;re running on.&lt;/p>
&lt;p>That means you don&amp;rsquo;t have to worry about 15 minute timeouts. But you still have other edge cases: the call to S3 could fail, the file format changes unexpectedly, etc. Your code has to handle that, if not at first, then eventually at 3am when somebody gets paged when it breaks or changes.&lt;/p>
&lt;h1 id="the-etl-job">The ETL Job&lt;/h1>
&lt;p>Looking at it from a different perspective, this could be an ETL (&lt;strong>E&lt;/strong>xtract, &lt;strong>T&lt;/strong>ransform, &lt;strong>L&lt;/strong>oad) job. An ETL job is a high-level concept that takes an input file, transforms it in some way, maybe joins it with another file, then writes it to another file. Exactly what the code above does.&lt;/p>
&lt;p>This is exactly the kind of thing that ETL services are great at, such as &lt;a class="link" href="https://aws.amazon.com/glue/" target="_blank" rel="noopener"
>AWS Glue&lt;/a>, or &lt;a class="link" href="https://aws.amazon.com/emr/" target="_blank" rel="noopener"
>AWS EMR&lt;/a>, this would look like the following (some lines removed (see &lt;a class="link" href="https://docs.aws.amazon.com/glue/latest/dg/glue-etl-scala-example.html" target="_blank" rel="noopener"
>here&lt;/a> for full example.) AWS Glue uses Spark and you can either use conventional SQL or define your own Spark Scala functions. AWS EMR can run several different big data compute frameworks, but for this post we&amp;rsquo;re going to use Spark because I&amp;rsquo;m familiar with it.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-scala" data-lang="scala">&lt;span class="line">&lt;span class="cl">&lt;span class="k">object&lt;/span> &lt;span class="nc">Program&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="n">main&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">sysArgs&lt;/span>&lt;span class="k">:&lt;/span> &lt;span class="kt">Array&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="kt">String&lt;/span>&lt;span class="o">])&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Yes this is boiler-plate code not relevant for your business logic
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// But it&amp;#39;s smaller and limited to just the start
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">val&lt;/span> &lt;span class="n">spark&lt;/span>&lt;span class="k">:&lt;/span> &lt;span class="kt">SparkContext&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nc">SparkContext&lt;/span>&lt;span class="o">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="n">glueContext&lt;/span>&lt;span class="k">:&lt;/span> &lt;span class="kt">GlueContext&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nc">GlueContext&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">spark&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="n">sparkSession&lt;/span>&lt;span class="k">:&lt;/span> &lt;span class="kt">SparkSession&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">glueContext&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getSparkSession&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">import&lt;/span> &lt;span class="nn">sparkSession.implicits._&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="n">input&lt;/span> &lt;span class="k">=&lt;/span> &lt;span class="n">sparkSession&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">.&lt;/span>&lt;span class="n">format&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s">&amp;#34;csv&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">.&lt;/span>&lt;span class="n">option&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s">&amp;#34;header&amp;#34;&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="s">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">.&lt;/span>&lt;span class="n">load&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s">&amp;#34;s3://awsexamplebucket-streaming-demo2/inputs/productsStatic.csv&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">input&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">select&lt;/span>&lt;span class="o">((&lt;/span>&lt;span class="n">$&lt;/span>&lt;span class="s">&amp;#34;value&amp;#34;&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="n">as&lt;/span> &lt;span class="s">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">.&lt;/span>&lt;span class="n">write&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">.&lt;/span>&lt;span class="n">save&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s">&amp;#34;s3://outputbucket/foobar&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>There&amp;rsquo;s some boiler-plate in here just setting up the Spark system, but the actual business logic is simple. It&amp;rsquo;s a declarative model (you describe what you want done) and it makes it happen instead of the previous example being imperative (you describe how it happens.)&lt;/p>
&lt;p>Is it really cheaper to go learn Spark if you weren&amp;rsquo;t already familiar? If it was just this small snippet of code, probably not. But systems rarely stay the same as what they were originally envisioned. The growing problems may or may not come up in a prototype that you do just to try it out.&lt;/p>
&lt;p>When you&amp;rsquo;re writing everything from scratch, you have to deal with all the minutia of the problem. Did you do a retry? Should you run it in multiple threads for performance? How do you parse the CSV file? It gets really easy to just layer more and more on it because the alternative is unknown because either you don&amp;rsquo;t know it exists, or you don&amp;rsquo;t know how hard it&amp;rsquo;ll be to use.&lt;/p>
&lt;p>Soon enough, you&amp;rsquo;re spending your time fixing issues in the system that aren&amp;rsquo;t the important parts (the business logic.) And personally, as a developer, I don&amp;rsquo;t enjoy running around fixing those kinds of issues.&lt;/p>
&lt;h1 id="but-spark-sql-is-weird-and-hard">But Spark SQL is weird and hard&lt;/h1>
&lt;p>A common response I get about frameworks like Spark is that Scala is confusing, or Spark SQL influences too much of your code.&lt;/p>
&lt;p>Yes, the Spark SQL functions are different than native Java, Python, or Scala and they can get a bit complicated modeling everything inside of Spark SQL. The following is what the &lt;a class="link" href="https://en.wikipedia.org/wiki/FizzBuzz" target="_blank" rel="noopener"
>FizzBuzz problem&lt;/a> looks like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-scala" data-lang="scala">&lt;span class="line">&lt;span class="cl">&lt;span class="k">val&lt;/span> &lt;span class="n">df&lt;/span> &lt;span class="k">=&lt;/span> &lt;span class="n">spark&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">createDF&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nc">Seq&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="mi">100&lt;/span>&lt;span class="o">))&lt;/span> &lt;span class="c1">// TODO: Validate this
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">val&lt;/span> &lt;span class="n">mod3&lt;/span> &lt;span class="k">=&lt;/span> &lt;span class="n">$&lt;/span>&lt;span class="s">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mod&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">lit&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="o">))&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">val&lt;/span> &lt;span class="n">mod5&lt;/span> &lt;span class="k">=&lt;/span> &lt;span class="n">$&lt;/span>&lt;span class="s">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mod&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">lit&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="o">))&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">df&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">withColumn&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s">&amp;#34;fizzbuzz&amp;#34;&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">when&lt;/span>&lt;span class="o">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mod3&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">and&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">mod5&lt;/span>&lt;span class="o">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">lit&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s">&amp;#34;fizzbuzz&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">.&lt;/span>&lt;span class="n">when&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">mod3&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">lit&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s">&amp;#34;fizz&amp;#34;&lt;/span>&lt;span class="o">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">.&lt;/span>&lt;span class="n">when&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">mod5&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">lit&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s">&amp;#34;buzz&amp;#34;&lt;/span>&lt;span class="o">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">.&lt;/span>&lt;span class="n">otherwise&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">$&lt;/span>&lt;span class="s">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cast&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s">&amp;#34;string&amp;#34;&lt;/span>&lt;span class="o">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Going 100% into that style of coding changes from one problem to another. You went from imperative code that doesn&amp;rsquo;t handle edge cases, to one where the execution framework heavily dictates your coding style. For some engineers, this will also be confusing.&lt;/p>
&lt;p>There are some solutions. You can develop a hybrid code style where you can pull out business logic into conventional Scala, Python, or Java because Spark Scala has full interop with JVM languages and PySpark has interop with Python.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-scala" data-lang="scala">&lt;span class="line">&lt;span class="cl">&lt;span class="k">val&lt;/span> &lt;span class="n">df&lt;/span> &lt;span class="k">=&lt;/span> &lt;span class="n">spark&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">parquet&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s">&amp;#34;s3://blah&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">val&lt;/span> &lt;span class="n">fizzbuzz&lt;/span> &lt;span class="k">=&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="k">=&amp;gt;&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">((&lt;/span>&lt;span class="n">value&lt;/span> &lt;span class="o">%&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">value&lt;/span> &lt;span class="o">%&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="o">))&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;fizzbuzz&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">value&lt;/span> &lt;span class="o">%&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;fizz&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">value&lt;/span> &lt;span class="o">%&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;buzz&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">value&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">toString&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">df&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">select&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">udf&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="kt">String&lt;/span>, &lt;span class="kt">Int&lt;/span>&lt;span class="o">](&lt;/span>&lt;span class="n">fizzbuzz&lt;/span>&lt;span class="o">)(&lt;/span>&lt;span class="n">$&lt;/span>&lt;span class="s">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="o">))&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If you have a large block of business logic and you only want Spark to handle input/output, task distribution, retries, etc. you can just jump out to native Java/Scala/Python code to perform the business logic.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-scala" data-lang="scala">&lt;span class="line">&lt;span class="cl">&lt;span class="k">val&lt;/span> &lt;span class="n">df&lt;/span> &lt;span class="k">=&lt;/span> &lt;span class="n">spark&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">parquet&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s">&amp;#34;s3://blah&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="n">fizzBuzzPartition&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">rows&lt;/span>&lt;span class="k">:&lt;/span> &lt;span class="kt">Iterator&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="kt">Row&lt;/span>&lt;span class="o">])&lt;/span> &lt;span class="k">=&amp;gt;&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">rows&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">map&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">row&lt;/span> &lt;span class="k">=&amp;gt;&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="n">value&lt;/span> &lt;span class="k">=&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getInt&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Imagine fizzbuzz() is calling out to existing business logic
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nc">Row&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">fizzbuzz&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="o">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">val&lt;/span> &lt;span class="n">out&lt;/span> &lt;span class="k">=&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mapPartitions&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">fizzBuzzPartition&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="should-you-always-use-the-best-technology">Should you always use the &amp;ldquo;best&amp;rdquo; technology?&lt;/h1>
&lt;p>Nah. Even if there appears to be a technology that seems to abstract away all your problems, you can&amp;rsquo;t ignore the other costs like having to learn a new language, new framework, new failure modes, and operational monitoring requirements. Your teammates have to learn something new and that adds a lot of friction. Some engineers don&amp;rsquo;t want or have the energy to learn something new.&lt;/p>
&lt;p>You have to weigh the benefits with the costs. If it&amp;rsquo;s a component you&amp;rsquo;re not going to tough very frequently, err on the side of technologies that are extremely simple and easy for others to understand. Pick ones that are consistent with others.&lt;/p>
&lt;p>If it is a key component that you&amp;rsquo;re going to continue to develop on, then it&amp;rsquo;s worth spending some extra time to understand these other technologies.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>Picking frameworks and technologies is like picking the right tool in the toolbox. Sure you can probably make squeaky door hinge stop with some WD-40, but you really want some lubricant. The right framework is important because software in production has a lot of hidden edge cases that have to be handled.&lt;/p>
&lt;p>Don&amp;rsquo;t always pick the fancy technology. Weigh the benefits and the costs. Using the technology you already know is a big benefit and something new should be considered a cost. But don&amp;rsquo;t be overly confident in your ability to identify and handle all edge cases.&lt;/p>
&lt;p>If you feel like you&amp;rsquo;re bending a system or framework to get it to work, take a step back and ask yourself if you&amp;rsquo;re using the right solution.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F09%2Fmore-infra-doesnt-fix-using-the-wrong-infra%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=More+infrastructure+doesn%27t+fix+using+the+wrong+infrastructure" style="border:0" alt="" /></description></item><item><title>Fixing an IP conflict with Docker and Delta in-flight wi-fi</title><link>https://www.technowizardry.net/2024/09/docker-conflicts-with-delta-inflight-wifi/</link><pubDate>Fri, 13 Sep 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/09/docker-conflicts-with-delta-inflight-wifi/</guid><summary>&lt;p>Today, I took a flight and tried to use the in-flight Wi-Fi, but I was unable to login to the the network. Nothing loaded or opened. I poked around in &lt;code>ip route&lt;/code> and found two different routes that conflicted created by the Docker daemon. Looking at the following route, there&amp;rsquo;s two routes: &lt;code>172.19.0.0/23&lt;/code> and &lt;code>172.19.0.0/16&lt;/code>. These correspond to: &lt;code>172.19.0.0 - 172.19.1.255&lt;/code> and &lt;code>172.19.0.0 - 172.19.255.255&lt;/code>.&lt;/p></summary><description>&lt;p>Today, I took a flight and tried to use the in-flight Wi-Fi, but I was unable to login to the the network. Nothing loaded or opened. I poked around in &lt;code>ip route&lt;/code> and found two different routes that conflicted created by the Docker daemon. Looking at the following route, there&amp;rsquo;s two routes: &lt;code>172.19.0.0/23&lt;/code> and &lt;code>172.19.0.0/16&lt;/code>. These correspond to: &lt;code>172.19.0.0 - 172.19.1.255&lt;/code> and &lt;code>172.19.0.0 - 172.19.255.255&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">ip route
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">default via 172.19.0.1 dev wlp1s0 proto dhcp src 172.19.1.22 metric 600
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">172.18.0.0/16 dev br-71fb429d0dc2 proto kernel scope link src 172.18.0.1 linkdown
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vvv
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">172.19.0.0/23 dev wlp1s0 proto kernel scope link src 172.19.1.22 metric 600
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">172.19.0.0/16 dev br-908cdaa5e0f4 proto kernel scope link src 172.19.0.1 linkdown
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">^^^
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">172.20.0.0/16 dev br-5432e3728889 proto kernel scope link src 172.20.0.1 linkdown
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">172.31.0.0/24 dev docker0 proto kernel scope link src 172.31.0.1 linkdown
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And here&amp;rsquo;s the problematic networks:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">docker network ls
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NETWORK ID NAME DRIVER SCOPE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">908cdaa5e0f4 testwebsite_default bridge local
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">b112ed129840 bridge bridge local
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cd3a9ccce4c5 host host local
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2e34dc80f263 none null local
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">5432e3728889 quickstart bridge local
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">71fb429d0dc2 technowizardrynet_default bridge local
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>First step is to delete these networks &lt;code>docker network rm 908cdaa5e0f4 5432e3728889&lt;/code>.&lt;/p>
&lt;p>Then configure Docker to use a new range by adding the following to &lt;code>/etc/docker/daemon.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;default-address-pools&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;base&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;172.31.0.0/16&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;size&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">24&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And restart docker: &lt;code>sudo systemctl restart docker&lt;/code>. After that, you should be able to use Docker and the in-flight Wi-Fi.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F09%2Fdocker-conflicts-with-delta-inflight-wifi%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Fixing+an+IP+conflict+with+Docker+and+Delta+in-flight+wi-fi" style="border:0" alt="" /></description></item><item><title>Screen sharing on Wayland Ubuntu 24.04</title><link>https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/</link><pubDate>Tue, 10 Sep 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/</guid><summary>&lt;p>I tried screen sharing in a video call on my Ubuntu 24.04 computer running the Snap Firefox install, but I could never get it to prompt to share a screen, thus it wouldn&amp;rsquo;t work. This post shows how I fixed that.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/screen-share-webrtc-no-options.png"
width="691"
height="447"
srcset="https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/screen-share-webrtc-no-options_hu_f3425a2f8f633eb1.png 480w, https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/screen-share-webrtc-no-options_hu_ed538e1d50d62567.png 1024w"
loading="lazy"
alt="Screenshot of Firefox’s share window/screen popup. No windows or screens are available because Firefox doesn’t detect them."
class="gallery-image"
data-flex-grow="154"
data-flex-basis="371px"
>&lt;/p></summary><description>&lt;p>I tried screen sharing in a video call on my Ubuntu 24.04 computer running the Snap Firefox install, but I could never get it to prompt to share a screen, thus it wouldn&amp;rsquo;t work. This post shows how I fixed that.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/screen-share-webrtc-no-options.png"
width="691"
height="447"
srcset="https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/screen-share-webrtc-no-options_hu_f3425a2f8f633eb1.png 480w, https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/screen-share-webrtc-no-options_hu_ed538e1d50d62567.png 1024w"
loading="lazy"
alt="Screenshot of Firefox’s share window/screen popup. No windows or screens are available because Firefox doesn’t detect them."
class="gallery-image"
data-flex-grow="154"
data-flex-basis="371px"
>&lt;/p>
&lt;h1 id="investigation">Investigation&lt;/h1>
&lt;p>My initial web searches came across &lt;a class="link" href="https://askubuntu.com/questions/1407494/screen-share-not-working-in-ubuntu-22-04-in-all-platforms-zoom-teams-google-m#" target="_blank" rel="noopener"
>this&lt;/a> Stack Exchange question that recommended that users switch from Wayland to X11. While this did fix the problem, I&amp;rsquo;m not a fan of just turning off new technologies to get things to work, so I continued investigating.&lt;/p>
&lt;p>GNOME and desktop environment integrations usually appear in &lt;a class="link" href="https://www.freedesktop.org/wiki/Software/dbus/" target="_blank" rel="noopener"
>D-Bus&lt;/a> and the awesome packet capturing tool, &lt;a class="link" href="https://www.wireshark.org/" target="_blank" rel="noopener"
>Wireshark&lt;/a>, has the ability to sniff D-Bus messages.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/wireshark-capture-screen.png"
width="518"
height="403"
srcset="https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/wireshark-capture-screen_hu_fcdd0dc177769a8d.png 480w, https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/wireshark-capture-screen_hu_ed7d23c9e1411bbe.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="128"
data-flex-basis="308px"
>&lt;/p>
&lt;p>I started capturing packets, then went back to Firefox and tried to share my screen. From there, I was able to see a few interesting packets:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/wireshark-error.png"
width="2108"
height="586"
srcset="https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/wireshark-error_hu_4055aadc48da224a.png 480w, https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/wireshark-error_hu_bfc486a55d823cd0.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="359"
data-flex-basis="863px"
>&lt;/p>
&lt;p>And an error message stating &lt;code>No such interface &amp;quot;org.freedesktop.portal.ScreenCast&amp;quot;&lt;/code>. Searching for that lead me to &lt;a class="link" href="https://github.com/rustdesk/rustdesk/issues/4276#issuecomment-1537105758" target="_blank" rel="noopener"
>this comment&lt;/a>.&lt;/p>
&lt;p>&lt;a class="link" href="https://apps.gnome.org/Dspy/" target="_blank" rel="noopener"
>D-Spy&lt;/a> is also a useful tool for investigating these issues too. With it, I&amp;rsquo;m able to see that Screencast is enabled.&lt;/p>
&lt;p>To enable it, run:
&lt;code>sudo apt install d-spy&lt;/code>&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/d-bus.png"
width="2244"
height="1644"
srcset="https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/d-bus_hu_871ea37decc222f2.png 480w, https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/d-bus_hu_9acaa6290c3f2707.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="136"
data-flex-basis="327px"
>&lt;/p>
&lt;h1 id="the-fix">The Fix&lt;/h1>
&lt;p>Easy enough:
&lt;code>sudo apt install xdg-desktop-portal xdg-desktop-portal-gnome&lt;/code>&lt;/p>
&lt;p>I installed those packages, then rebooted. Voilà! Firefox is now able to share my screen.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/wayland-prompt.png"
width="1564"
height="1244"
srcset="https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/wayland-prompt_hu_7eca305195b4b003.png 480w, https://www.technowizardry.net/2024/09/screen-sharing-on-wayland-ubuntu-24-04/images/wayland-prompt_hu_68680c36ff4170f8.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="125"
data-flex-basis="301px"
>&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F09%2Fscreen-sharing-on-wayland-ubuntu-24-04%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Screen+sharing+on+Wayland+Ubuntu+24.04" style="border:0" alt="" /></description></item><item><title>Adopting NixOS for my RKE1 Kubernetes nodes</title><link>https://www.technowizardry.net/2024/09/adopting-nixos-for-my-rke1-kubernetes-nodes/</link><pubDate>Sat, 07 Sep 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/09/adopting-nixos-for-my-rke1-kubernetes-nodes/</guid><summary>&lt;p>For those not aware, &lt;a class="link" href="https://nixos.org/" target="_blank" rel="noopener"
>Nix&lt;/a> is an interesting new application (Nix) and operating System (NixOS) that provides a declarative environment definition and atomic operating system. Declarative means that instead of running &lt;code>apt-get install docker&lt;/code>, you write down everything you want and it installs everything and removes everything you don&amp;rsquo;t want. You can use the same language to manage packages, users, firewall, networking, etc. This is useful because now you can revision control your OS state in Git and have exact replicas across multiple hosts.&lt;/p>
&lt;p>My friend, &lt;a class="link" href="https://0xda.de" target="_blank" rel="noopener"
>dade&lt;/a>, and I have been diving into Nix and NixOS. He got it working on his laptop, I&amp;rsquo;m trying to get it to be the OS for my four dedicated servers all running Kubernetes. In this post, I&amp;rsquo;ll walk through the main issues I encountered and how I got a single node running in an existing &lt;a class="link" href="https://github.com/rancher/rke" target="_blank" rel="noopener"
>RKE1 cluster&lt;/a>.&lt;/p>
&lt;p>I&amp;rsquo;m not going to go all the way to use Nix to configure everything including my Kubernetes configuration. I know &lt;a class="link" href="https://nixos.wiki/wiki/Kubernetes" target="_blank" rel="noopener"
>that&amp;rsquo;s possible&lt;/a>, but I already have a Kubernetes cluster deployed using RKE1 that I&amp;rsquo;m not ready to break yet since it hosts this blog and other services. Maybe in a future iteration I will.&lt;/p></summary><description>&lt;p>For those not aware, &lt;a class="link" href="https://nixos.org/" target="_blank" rel="noopener"
>Nix&lt;/a> is an interesting new application (Nix) and operating System (NixOS) that provides a declarative environment definition and atomic operating system. Declarative means that instead of running &lt;code>apt-get install docker&lt;/code>, you write down everything you want and it installs everything and removes everything you don&amp;rsquo;t want. You can use the same language to manage packages, users, firewall, networking, etc. This is useful because now you can revision control your OS state in Git and have exact replicas across multiple hosts.&lt;/p>
&lt;p>My friend, &lt;a class="link" href="https://0xda.de" target="_blank" rel="noopener"
>dade&lt;/a>, and I have been diving into Nix and NixOS. He got it working on his laptop, I&amp;rsquo;m trying to get it to be the OS for my four dedicated servers all running Kubernetes. In this post, I&amp;rsquo;ll walk through the main issues I encountered and how I got a single node running in an existing &lt;a class="link" href="https://github.com/rancher/rke" target="_blank" rel="noopener"
>RKE1 cluster&lt;/a>.&lt;/p>
&lt;p>I&amp;rsquo;m not going to go all the way to use Nix to configure everything including my Kubernetes configuration. I know &lt;a class="link" href="https://nixos.wiki/wiki/Kubernetes" target="_blank" rel="noopener"
>that&amp;rsquo;s possible&lt;/a>, but I already have a Kubernetes cluster deployed using RKE1 that I&amp;rsquo;m not ready to break yet since it hosts this blog and other services. Maybe in a future iteration I will.&lt;/p>
&lt;h1 id="booting">Booting&lt;/h1>
&lt;p>First, I needed to make the disk as a bootable EFI image:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">...&lt;/span> &lt;span class="p">}:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Use the systemd-boot EFI boot loader.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boot&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loader&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">systemd&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">boot&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">enable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boot&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loader&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">efi&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">canTouchEfiVariables&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="disk-partitioning">Disk Partitioning&lt;/h1>
&lt;p>The NixOS minimal live CD does not give a partitioning tool other than &lt;code>fdisk&lt;/code>. You&amp;rsquo;re responsible for figuring out partition types yourself. Luckily NixOS has &lt;a class="link" href="https://github.com/nix-community/disko" target="_blank" rel="noopener"
>disko&lt;/a> which uses the exact same Nix files to declaratively define partitions and I can steal the config from somebody else who figured it out. Nice.&lt;/p>
&lt;p>I had two drives, one SSD, one on a spinning hard drive and followed the &lt;a class="link" href="https://github.com/nix-community/disko/blob/master/docs/disko-install.md" target="_blank" rel="noopener"
>disko guide&lt;/a>. My configuration looked like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">disko&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="o">...&lt;/span> &lt;span class="p">}:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">disko&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">devices&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">disk&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ssd&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># When using disko-install, we will overwrite this value from the commandline&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">device&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/dev/disk/by-id/5dbd3913-bc15-4e14-9902-de9c1703eacd&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;disk&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">content&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;gpt&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">partitions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">MBR&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;EF02&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1"># for grub MBR&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;1M&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">priority&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1"># Needs to be first partition&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ESP&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;EF00&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;500M&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">content&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;filesystem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">format&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;vfat&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mountpoint&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/boot&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">root&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;100%&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">content&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;filesystem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">format&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;btrfs&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mountpoint&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">hdd&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># When using disko-install, we will overwrite this value from the commandline&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">device&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/dev/disk/by-id/eae13bd2-c505-46b2-9066-5aa1028f10d7&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;disk&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">content&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;gpt&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">partitions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">root&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;100%&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">content&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;filesystem&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">format&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;btrfs&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mountpoint&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/mnt/hdd&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Writing to the disk was easy (make sure &lt;code>sdb&lt;/code> and &lt;code>sda&lt;/code> are pointing to the right disks)&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">nix --extra-experimental-features flakes --extra-experimental-features nix-command run &lt;span class="s1">&amp;#39;github:nix-community/disko#disko-install&amp;#39;&lt;/span> -- --flake &lt;span class="s1">&amp;#39;/tmp/config/etc/nixos#srv5&amp;#39;&lt;/span> --write-efi-boot-entries --disk ssd /dev/sdb --disk hdd /dev/sda
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="making-vim-sane">Making Vim sane&lt;/h1>
&lt;p>Nix&amp;rsquo;s default vimrc configuration is weird for me, but it&amp;rsquo;s easy to fix this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">environment.systemPackages = with pkgs; [
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ((vim_configurable.override { }).customize {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name = &amp;#34;vim&amp;#34;;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> vimrcConfig.customRC = &amp;#39;&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> set mouse=&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> set backspace=indent,eol,start
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> syntax on
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;&amp;#39;;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> })
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">];
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="moving-on-to-k8s">Moving on to K8s&lt;/h1>
&lt;p>I&amp;rsquo;m currently using RKE1 which involves running &lt;code>./rke up&lt;/code> to provision a new node, not NixOS because my K8s is a few years old now. To add a new node it looks like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl"> # cluster.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> nodes:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ - address: 1.2.3.4
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ user: adam
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ role:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ - controlplane
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ - etcd
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ - worker
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then run: (&lt;code>--ignore-docker-version&lt;/code> is needed because NixOS uses Docker 27.x, but RKE1 only supports up to 26.x)&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">./rke_linux-amd64 up --ignore-docker-version
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And waiting for it to provision.&lt;/p>
&lt;h2 id="resource-requests-the-dumb-issue">Resource requests (The dumb issue)&lt;/h2>
&lt;p>The node joined the cluster, but my next problem is no pods could start because the networking was down:&lt;/p>
&lt;p>The kubelet logs (&lt;code>docker logs kubelet&lt;/code>) showed:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">kubelet.go:2862] &amp;#34;Container runtime network not ready&amp;#34; networkReady=&amp;#34;NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">0/4 nodes are available: 1 Insufficient cpu, 3 node is filtered out by the prefilter result. preemption: 0/4 nodes are available: 1 Insufficient cpu, 3 Preemption is not helpful for scheduling..
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://www.technowizardry.net/2024/09/adopting-nixos-for-my-rke1-kubernetes-nodes/images/rancher-canal-pending.png"
width="1410"
height="112"
srcset="https://www.technowizardry.net/2024/09/adopting-nixos-for-my-rke1-kubernetes-nodes/images/rancher-canal-pending_hu_20a8736e9c8e1555.png 480w, https://www.technowizardry.net/2024/09/adopting-nixos-for-my-rke1-kubernetes-nodes/images/rancher-canal-pending_hu_1b85e92e979bf8a5.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="1258"
data-flex-basis="3021px"
>&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/09/adopting-nixos-for-my-rke1-kubernetes-nodes/images/rancher-unavailable-node.png"
width="1663"
height="821"
srcset="https://www.technowizardry.net/2024/09/adopting-nixos-for-my-rke1-kubernetes-nodes/images/rancher-unavailable-node_hu_8f34aab732db5ab5.png 480w, https://www.technowizardry.net/2024/09/adopting-nixos-for-my-rke1-kubernetes-nodes/images/rancher-unavailable-node_hu_a254bba34dd2f302.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="202"
data-flex-basis="486px"
>&lt;/p>
&lt;p>I was stumped. How come there wasn&amp;rsquo;t enough CPU? Was it because Calico couldn&amp;rsquo;t spin up, so kubelet was reporting unhealthy because there was no CNI, thus no CPU? A circular dependency?&lt;/p>
&lt;p>Nah, let&amp;rsquo;s get the dumb mistakes out of the way: turns out, I only provisioned one CPU to this test VM because it was just for experimenting, not production use.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/09/adopting-nixos-for-my-rke1-kubernetes-nodes/images/esxi-cores.png"
width="409"
height="144"
srcset="https://www.technowizardry.net/2024/09/adopting-nixos-for-my-rke1-kubernetes-nodes/images/esxi-cores_hu_60fc9ae79984cdaa.png 480w, https://www.technowizardry.net/2024/09/adopting-nixos-for-my-rke1-kubernetes-nodes/images/esxi-cores_hu_c6d89fd89965a962.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="284"
data-flex-basis="681px"
>&lt;/p>
&lt;p>And I had reserved 1 core for the host OS:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># rke cluster.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kubelet&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extra_args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kube-reserved&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cpu=1000m,memory=512Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>So there was nothing available for the Kubernetes pods itself. Whoops. I gave it more CPUs.&lt;/p>
&lt;h2 id="inter-node-traffic">Inter-node traffic&lt;/h2>
&lt;p>Next problem is diagnosing why the pods weren&amp;rsquo;t able to communicate with the rest of the cluster. I&amp;rsquo;m currently using &lt;a class="link" href="https://github.com/projectcalico/canal" target="_blank" rel="noopener"
>Calico Canal&lt;/a> for networking which uses &lt;a class="link" href="https://www.tkng.io/cni/flannel/" target="_blank" rel="noopener"
>Flannel&lt;/a> to create an encapsulated overlay network between the different nodes in the cluster. They weren&amp;rsquo;t able to communicate and I needed to figure out why, so I installed &lt;code>tcpdump&lt;/code>, a packet capturing program. I added the following, then ran &lt;code>nixos-rebuild switch&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">environment.systemPackages = with pkgs; [
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> pkgs.tcpdump
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">];
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>On a different node, I pinged a pod running on my test node to identify which ports were needed:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">ping 10.42.4.72
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PING 10.42.4.72 (10.42.4.72) 56(84) bytes of data.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Gotcha:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">tcpdump -f &amp;#39;host 51.81.64.31 or dst 149.56.22.10&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">05:04:55.054420 ens33 Out IP srv8.7946 &amp;gt; srv6.technowizardry.net.7946: UDP, length 96
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">05:04:55.068243 ens33 In IP srv6.technowizardry.net.7946 &amp;gt; srv8.7946: UDP, length 49
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Easy enough in Nix to open up the firewall. Ideally, I&amp;rsquo;d lock this down to just the other nodes in my cluster, but I didn&amp;rsquo;t know how to do that in NixOS yet.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">lib&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="o">...&lt;/span> &lt;span class="p">}:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">networking&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">firewall&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">allowedTCPPorts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">7946&lt;/span> &lt;span class="c1"># flannel&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">8285&lt;/span> &lt;span class="c1"># flannel&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">8472&lt;/span> &lt;span class="c1"># flannel&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Kubernetes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">2379&lt;/span> &lt;span class="c1"># etcd-client&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">2380&lt;/span> &lt;span class="c1"># etcd-cluster&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">6443&lt;/span> &lt;span class="c1"># kube-apiserver&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Prometheus metrics&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">10250&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">10254&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">networking&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">firewall&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">allowedUDPPorts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">7946&lt;/span> &lt;span class="c1"># flannel&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">8472&lt;/span> &lt;span class="c1"># flannel&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Another NixOS rebuild and the pings are flowing between pods.&lt;/p>
&lt;h2 id="its-always-dns">(it&amp;rsquo;s always) DNS&lt;/h2>
&lt;p>All my servers (via a DaemonSet) run &lt;a class="link" href="https://www.powerdns.com/" target="_blank" rel="noopener"
>PowerDNS&lt;/a> as an authoritative DNS server for my various domain names. However, on my new node, it wasn&amp;rsquo;t able to start up:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Guardian is launching an instance
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Unable to bind UDP socket to &amp;#39;0.0.0.0:53&amp;#39;: Address already in use
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Fatal error: Unable to bind to UDP socket
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>It binds to port &lt;code>53/tcp&lt;/code> and &lt;code>53/udp&lt;/code> on &lt;code>0.0.0.0&lt;/code>, however systemd-resolved is already running on that port on &lt;code>127.0.0.53&lt;/code> and &lt;code>127.0.0.54&lt;/code>. Even though it&amp;rsquo;s listening on loopback, PowerDNS will conflict because it&amp;rsquo;s trying to run on &lt;code>0.0.0.0&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">lsof -iUDP -P -n
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">systemd-r 692 systemd-resolve 11u IPv4 14287 0t0 UDP *:5355
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">systemd-r 692 systemd-resolve 13u IPv6 14290 0t0 UDP *:5355
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">systemd-r 692 systemd-resolve 15u IPv4 14292 0t0 UDP *:5353
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">systemd-r 692 systemd-resolve 16u IPv6 14293 0t0 UDP *:5353
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">systemd-r 692 systemd-resolve 20u IPv4 14297 0t0 UDP 127.0.0.53:53
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">systemd-r 692 systemd-resolve 22u IPv4 14299 0t0 UDP 127.0.0.54:53
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>To fix this, I disable the local resolver and point my server to another resolver:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">{ config, lib, pkgs, ... }:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> networking.nameservers = [ &amp;#34;1.1.1.1&amp;#34; ];
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> services.resolved.enable = false;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="longhorn">Longhorn&lt;/h2>
&lt;p>I use &lt;a class="link" href="https://longhorn.io/" target="_blank" rel="noopener"
>Longhorn&lt;/a> as my Kubernetes CSI (storage provider) because it supports mirroring across multiple hosts and automatic backups. Under the hood, it uses NFS to mount a volume from the Longhorn storage pod and the host running the application pod. To do this, it requires a few packages to be installed on the host itself (nfs-utils) and the iscsid system daemon.&lt;/p>
&lt;p>When I tried to start Longhorn, I got the following error messages because the nfs utils weren&amp;rsquo;t installed:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">MountVolume&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">MountDevice&lt;/span> &lt;span class="n">failed&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">volume&lt;/span> &lt;span class="s2">&amp;#34;pvc-8416dbbf-89cf-45c0-bdc8-8a2b55a4e58a&amp;#34;&lt;/span> &lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">rpc&lt;/span> &lt;span class="n">error&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">code&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Internal&lt;/span> &lt;span class="n">desc&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">mount&lt;/span> &lt;span class="n">failed&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">exit&lt;/span> &lt;span class="n">status&lt;/span> &lt;span class="mi">32&lt;/span> &lt;span class="n">Mounting&lt;/span> &lt;span class="n">command&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">/&lt;/span>&lt;span class="n">usr&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">local&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sbin&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">nsmounter&lt;/span> &lt;span class="n">Mounting&lt;/span> &lt;span class="n">arguments&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mount&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">t&lt;/span> &lt;span class="n">nfs&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">o&lt;/span> &lt;span class="n">vers&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">4.1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="n">noresvport&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="n">timeo&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">600&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="n">retrans&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="n">softerr&lt;/span> &lt;span class="mf">10.43&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mf">149.52&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">pvc&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">8416&lt;/span>&lt;span class="n">dbbf&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">89&lt;/span>&lt;span class="n">cf&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">45&lt;/span>&lt;span class="n">c0&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bdc8&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="n">a2b55a4e58a&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kubelet&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">plugins&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kubernetes&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">io&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">csi&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">driver&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">longhorn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">io&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="n">d6bbeebe575c0ca8e25eb35c0248aca3e81a606bc647be0c2d9901b6b4bd9b3&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">globalmount&lt;/span> &lt;span class="n">Output&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mount&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kubelet&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">plugins&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kubernetes&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">io&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">csi&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">driver&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">longhorn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">io&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="n">d6bbeebe575c0ca8e25eb35c0248aca3e81a606bc647be0c2d9901b6b4bd9b3&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">globalmount&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">bad&lt;/span> &lt;span class="n">option&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">several&lt;/span> &lt;span class="n">filesystems&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">g&lt;/span>&lt;span class="o">.&lt;/span> &lt;span class="n">nfs&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">cifs&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">you&lt;/span> &lt;span class="n">might&lt;/span> &lt;span class="n">need&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">sbin&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">mount&lt;/span>&lt;span class="o">.&amp;lt;&lt;/span>&lt;span class="n">type&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">helper&lt;/span> &lt;span class="n">program&lt;/span>&lt;span class="o">.&lt;/span> &lt;span class="n">dmesg&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">may&lt;/span> &lt;span class="n">have&lt;/span> &lt;span class="n">more&lt;/span> &lt;span class="n">information&lt;/span> &lt;span class="n">after&lt;/span> &lt;span class="n">failed&lt;/span> &lt;span class="n">mount&lt;/span> &lt;span class="n">system&lt;/span> &lt;span class="n">call&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>As per the &lt;a class="link" href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/networking/cluster/k3s/docs/examples/STORAGE.md" target="_blank" rel="noopener"
>Longhorn docs&lt;/a>, we need to install a few packages:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">environment.systemPackages = [ pkgs.nfs-utils pkgs.openiscsi ];
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>However, NixOS doesn&amp;rsquo;t place the files in the same place as Longhorn expects (&lt;a class="link" href="https://github.com/longhorn/longhorn/issues/2166" target="_blank" rel="noopener"
>GitHub issue&lt;/a>). Longhorn expects to find &lt;code>mount.nfs&lt;/code> in the PATH, but in NixOS, it&amp;rsquo;s actually found in &lt;code>/nix/store/2l9hiinf01ikdjjxd1lafb9mqs5ssfp5-nfs-utils-2.7.1/bin/mount.nfs&lt;/code> (the path may differ on your host). When it goes to run &lt;code>nsenter --mount=/host/proc/14084/ns/mnt --net=/host/proc/14084/ns/net mount.nfs&lt;/code> in the container to break out of the container into the host namespaces and mount the NFS volume, it looks in the PATH of &lt;code>/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&lt;/code> and doesn&amp;rsquo;t find it so it fails.&lt;/p>
&lt;p>We need to trick Longhorn into looking into the right place by overriding the &lt;code>PATH&lt;/code> environment variable. When &lt;code>nsenter&lt;/code> runs, it uses the &lt;code>PATH&lt;/code> environment to invoke the command on the host OS. I like using &lt;a class="link" href="https://kyverno.io/" target="_blank" rel="noopener"
>Kyverno&lt;/a> for this which is a Kubernetes program that can validate and mutate resources based on policies. I used it in the past and already had it installed on my cluster, so this next part worked well.&lt;/p>
&lt;p>Inspired by &lt;a class="link" href="https://github.com/longhorn/longhorn/issues/2166#issuecomment-2155785122" target="_blank" rel="noopener"
>this comment&lt;/a>, I created a simple Kyverno policy that mutates every pod in the &lt;code>longhorn-system&lt;/code> namespace to include our custom PATH.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kyverno.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">longhorn-add-nixos-path&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies.kyverno.io/title&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Add Environment Variables from ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies.kyverno.io/subject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Pod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies.kyverno.io/category&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Other&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies.kyverno.io/description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Longhorn invokes executables on the host system, and needs
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> to be aware of the host systems PATH. This modifies all
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> deployments such that the PATH is explicitly set to support
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> NixOS based systems.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">add-env-vars&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">match&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kinds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">Pod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">longhorn-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mutate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">patchStrategicMerge&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">initContainers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">(name)&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PATH&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">(name)&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PATH&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After that, I redeployed the Longhorn pods and the containers were able to start up without crashing.&lt;/p>
&lt;h3 id="iscsi">iSCSI&lt;/h3>
&lt;p>However, trying to mount a volume on the host would fail:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">failed to execute: /usr/bin/nsenter [nsenter --mount=/host/proc/14084/ns/mnt --net=/host/proc/14084/ns/net iscsiadm -m discovery -t sendtargets -p 10.42.0.20], output , stderr
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> iscsiadm: can&amp;#39;t open iscsid.startup configuration file etc/iscsi/iscsid.conf\n
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> iscsiadm: iscsid is not running. Could not start it up automatically using the startup command in the iscsid.conf iscsid.startup setting. Please check that the file exists or that your init scripts have started iscsid.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> iscsiadm: can not connect to iSCSI daemon (111)!
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> iscsiadm: can&amp;#39;t open iscsid.startup configuration file etc/iscsi/iscsid.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> iscsiadm: iscsid is not running. Could not start it up automatically using the startup command in the iscsid.conf iscsid.startup setting. Please check that the file exists or that your init scripts have started iscsid.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> iscsiadm: can not connect to iSCSI daemon (111)!
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> iscsiadm: Cannot perform discovery. Initiatorname required.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> iscsiadm: Could not perform SendTargets discovery: could not connect to iscsid
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> exit status 20
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Nodes cleaned up for iqn.2019-10.io.longhorn:unifi-restore&amp;#34; func=&amp;#34;iscsidev.(*Device).StartInitator&amp;#34; file=&amp;#34;iscsi.go:168
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">msg=&amp;#34;Failed to startup frontend&amp;#34; func=&amp;#34;controller.(*Controller).startFrontend&amp;#34; file=&amp;#34;control.go:489&amp;#34; error=&amp;#34;failed to execute:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> /usr/bin/nsenter [nsenter --mount=/host/proc/14084/ns/mnt --net=/host/proc/14084/ns/net iscsiadm -m node -T iqn.2019-10.io.longhorn:unifi-restore -o update -n node.session.err_timeo.abort_timeout -v 15], output , stderr
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> iscsiadm: No records found
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> exit status 21&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>It can&amp;rsquo;t find the &lt;code>iscsid&lt;/code> service provided by the &lt;code>iscsid.socket&lt;/code> and &lt;code>iscsid.service&lt;/code> systemd units. Just installing the package doesn&amp;rsquo;t enable the service. For that we need to add:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl"> { config, lib, pkgs, ... }:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> environment.systemPackages = with pkgs; [
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> pkgs.nfs-utils
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> pkgs.openiscsi
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ];
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ services.openiscsi = {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ enable = true;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ name = &amp;#34;iqn.2005-10.nixos:${config.networking.hostName}&amp;#34;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ };
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then redeploy the application pod.&lt;/p>
&lt;h2 id="i-forgot-the-firewall">I forgot the Firewall&lt;/h2>
&lt;p>At this point everything is deploying, but I can&amp;rsquo;t seem to access anything running on this host. Normally Docker and Kubernetes hijack the IPTables rules, but on NixOS they don&amp;rsquo;t. Easy fix:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">networking&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">firewall&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">allowedTCPPorts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">53&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># nginx&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">80&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">443&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># mail&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">25&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">110&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">143&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">465&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">587&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">993&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">995&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">7946&lt;/span> &lt;span class="c1"># flannel&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">8285&lt;/span> &lt;span class="c1"># flannel&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">8472&lt;/span> &lt;span class="c1"># flannel&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Rancher UI&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">8443&lt;/span> &lt;span class="c1"># I don&amp;#39;t know if this is actually used&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">32121&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># TODO: Lock these down to intra-node only&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">2379&lt;/span> &lt;span class="c1"># etcd-client&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">2380&lt;/span> &lt;span class="c1"># etcd-cluster&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">6443&lt;/span> &lt;span class="c1"># kube-apiserver&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">10250&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">10254&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">9100&lt;/span> &lt;span class="c1"># prom-node-export&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">networking&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">firewall&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">allowedUDPPorts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">53&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">7946&lt;/span> &lt;span class="c1"># flannel&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">8472&lt;/span> &lt;span class="c1"># flannel&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="missing-kubelet-logs">Missing kubelet logs&lt;/h1>
&lt;p>Next, I had an issue where &lt;code>kubelet logs&lt;/code> would not show any logs from any pods:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="o">$&lt;/span> &lt;span class="n">kubectl&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">local&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">n&lt;/span> &lt;span class="n">cattle&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">system&lt;/span> &lt;span class="n">logs&lt;/span> &lt;span class="n">helm&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">operation&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">hpq86&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Defaulted&lt;/span> &lt;span class="n">container&lt;/span> &lt;span class="s2">&amp;#34;helm&amp;#34;&lt;/span> &lt;span class="n">out&lt;/span> &lt;span class="n">of&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">helm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">proxy&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">init&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">kubeconfig&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">volume&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">init&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">failed&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">try&lt;/span> &lt;span class="n">resolving&lt;/span> &lt;span class="n">symlinks&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">path&lt;/span> &lt;span class="s2">&amp;#34;/var/log/pods/cattle-system_helm-operation-hpq86_f63d99d0-ad39-478f-be8f-bdc1f4657d1d/helm/0.log&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">lstat&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nb">log&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">pods&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">cattle&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">system_helm&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">operation&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">hpq86_f63d99d0&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">ad39&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">478&lt;/span>&lt;span class="n">f&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">be8f&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bdc1f4657d1d&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">helm&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="mf">0.&lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">no&lt;/span> &lt;span class="n">such&lt;/span> &lt;span class="n">file&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">directory&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Looking at the host, no files were present in &lt;code>/var/logs/pods&lt;/code> and nothing was in &lt;code>/var/lib/docker/containers/{container}/*-json.log&lt;/code>. The &lt;a class="link" href="https://search.nixos.org/options?channel=24.05&amp;amp;show=virtualisation.docker.logDriver&amp;amp;from=0&amp;amp;size=50&amp;amp;sort=alpha_asc&amp;amp;type=packages&amp;amp;query=virtualisation.docker.logDriver" target="_blank" rel="noopener"
>default logDriver in Nix&lt;/a> for Docker is &lt;code>journald&lt;/code>. I tried switching to &lt;code>local&lt;/code> which didn&amp;rsquo;t work, but &lt;a class="link" href="https://stackoverflow.com/questions/41319233/kubelet-does-not-create-symlinks-to-var-log-containers" target="_blank" rel="noopener"
>this StackOverflow post&lt;/a> suggests it needs to be set to &lt;code>json-file&lt;/code>.&lt;/p>
&lt;p>Easy fix. I changed updated the logDriver and &lt;code>nixos-rebuild switch&lt;/code> to restart the Docker daemon fixed my logs:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="cl">&lt;span class="n">virtualisation&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">docker&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">enable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="no">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">logDriver&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;json-file&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>Thus far, I feel that Nix is powerful and I like having declarative config that can manage everything about the OS. I can define a series of configuration files and define per host flakes for my servers and computers and pick and choose which machines get what config.&lt;/p>
&lt;p>But I don&amp;rsquo;t find the language itself very intuitive. I tried to create my own derivative, but ran into issues. Over time, I hope it gets better though and I&amp;rsquo;ll keep playing with it.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F09%2Fadopting-nixos-for-my-rke1-kubernetes-nodes%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Adopting+NixOS+for+my+RKE1+Kubernetes+nodes" style="border:0" alt="" /></description></item><item><title>My financial data scraping system</title><link>https://www.technowizardry.net/2024/08/my-financial-data-scraping-system/</link><pubDate>Sun, 18 Aug 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/08/my-financial-data-scraping-system/</guid><summary>&lt;p>In my &lt;a class="link" href="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/" >Importing and cleaning my Mint transactions&lt;/a>, I worked through loading, cleaning, and solving for transfers.&lt;/p>
&lt;p>However, Mint and other financial scraping tools are not authoritative and don&amp;rsquo;t expose everything that the bank itself will provide. For example, Mint and Monarch don&amp;rsquo;t have detailed enough stock transaction and position data to identify cost basis, tax lots, and positions. Directly going to the bank can give me higher precision time stamps, scans of checks, merchant addresses, and other attributes.&lt;/p></summary><description>&lt;p>In my &lt;a class="link" href="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/" >Importing and cleaning my Mint transactions&lt;/a>, I worked through loading, cleaning, and solving for transfers.&lt;/p>
&lt;p>However, Mint and other financial scraping tools are not authoritative and don&amp;rsquo;t expose everything that the bank itself will provide. For example, Mint and Monarch don&amp;rsquo;t have detailed enough stock transaction and position data to identify cost basis, tax lots, and positions. Directly going to the bank can give me higher precision time stamps, scans of checks, merchant addresses, and other attributes.&lt;/p>
&lt;!-- more -->
&lt;h1 id="summary">Summary&lt;/h1>
&lt;p>I&amp;rsquo;ve been working on this project for the better part of 2024 at this point. Since none of my banks or brokerages provided any sort of obvious API to access my data, I created a few different syncing jobs that used &lt;a class="link" href="https://playwright.dev" target="_blank" rel="noopener"
>Playwright&lt;/a>, a web browser testing and automation framework, to login to my accounts and pull data. I then stored all that data in a custom &lt;a class="link" href="https://www.postgresql.org/" target="_blank" rel="noopener"
>PostgreSQL&lt;/a> DB and load it into &lt;a class="link" href="https://firefly-iii.org/" target="_blank" rel="noopener"
>Firefly&lt;/a> and &lt;a class="link" href="https://ghostfol.io/" target="_blank" rel="noopener"
>Ghostfolio&lt;/a>. I also wrote &lt;a class="link" href="https://zeppelin.apache.org/" target="_blank" rel="noopener"
>Apache Zeppelin&lt;/a> notebooks to do ad-hoc analytics and testing of new algorithms.&lt;/p>
&lt;p>With this new found data, I found it possible to do quick analytics. For example, I was able to easily write some Python to decide whether I was getting enough returns to justify whether or a credit card annual fee was worth it.&lt;/p>
&lt;h1 id="the-architecture">The Architecture&lt;/h1>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/08/my-financial-data-scraping-system/images/sync-architecture.svg"
loading="lazy"
alt="A architectural flow diagram showing the different components in use. Explained in this section"
>&lt;/p>
&lt;p>The architecture breaks down into a few different components. First, we have the banks and brokerages that I want to scrape. Then I use Monarch to scrape all banks, and I use my own Playwright based scrapers to pull data from the banks that I specifically wrote scrapers for. This all gets written into a few different PostgreSQL tables. The Postgres tables are there as a copy of the data because they contain more data that Firefly/Ghostfolio can store and provides a place for me to test new transfer solving algorithms and rebuild the Firefly/Ghostfolio databases as I make improvements.&lt;/p>
&lt;h1 id="security">Security&lt;/h1>
&lt;p>Security is paramount given that I have to store my own account credentials to log into my accounts. I didn&amp;rsquo;t want to store my passwords in a &lt;a class="link" href="https://kubernetes.io/docs/concepts/configuration/secret/" target="_blank" rel="noopener"
>Kubernetes Secret&lt;/a> which would have been too risky in case of a malicious actor. I opted to leverage &lt;a class="link" href="https://github.com/hashicorp/vault" target="_blank" rel="noopener"
>Vault&lt;/a> and &lt;a class="link" href="https://github.com/openbao/openbao" target="_blank" rel="noopener"
>OpenBao&lt;/a> (an open-source Vault fork) which software-based secret storage system to store everything sensitive including the usernames, passwords, TOTP secrets (Time based one time passwords. These are codes that last for about 30 seconds), and cookie jars.&lt;/p>
&lt;p>Why cookie jars? As a it turns out, banks get nervous when you keep logging in from fresh browsers each time. They&amp;rsquo;ll make you go through verification steps that may include an SMS, or a mobile app push notification. What I do is sign in once manually and approve the request, then every time my sync job runs, I restore the entire cookie jar into the browser, and save it afterwards. This makes it look like my scraping job is somewhat human.&lt;/p>
&lt;p>All data in Vault is encrypted at rest and encrypted in transit, so no plain-text password is stored on disk. The decryption key is stored only in memory. If my server ever restarts, I have to re-enter the recovery key(s) to unlock the Vault database.&lt;/p>
&lt;p>Vault is then configured to only trust certain Kubernetes namespaces to read/write secrets. If you want to see exactly how this is configured, read my &lt;a class="link" href="https://www.technowizardry.net/2024/08/vault-for-a-home-lab/" >Vault install post&lt;/a>.&lt;/p>
&lt;h1 id="why-not-scrape-everything-myself">Why not scrape everything myself?&lt;/h1>
&lt;p>In a previous post, I did talk about some issues that I had with Monarch around their web application loading ad scripts &lt;a class="link" href="" >here&lt;/a>. This has not yet been fixed and I hope they do fix it, so it might seem unusual that I&amp;rsquo;m still using it. Unfortunately, I realized that I couldn&amp;rsquo;t develop scrapers for every single application. Some of my banks (e.g. Citi) required me to 2FA (two factor auth. A password is one factor, an extra code makes it 2FA) using SMS every single time I logged in which was impractical to automate. Monarch and their data providers (&lt;a class="link" href="https://plaid.com/how-it-works-for-consumers/" target="_blank" rel="noopener"
>Plaid&lt;/a>, &lt;a class="link" href="https://www.mx.com/" target="_blank" rel="noopener"
>MX&lt;/a>, and &lt;a class="link" href="https://www.finicity.com/" target="_blank" rel="noopener"
>Finicity&lt;/a>) have the ability to do OAuth auth and API based communications with banks, but that&amp;rsquo;s not possible for me without compliance reviews.&lt;/p>
&lt;p>Monarch handled the banks and credit cards, whereas I focused on the stock brokerages because Monarch did not going to scrape the data that I needed.&lt;/p>
&lt;h1 id="institutions">Institutions&lt;/h1>
&lt;h2 id="venmo">Venmo&lt;/h2>
&lt;p>Venmo was comparatively easy vs the other sites. I did this one mostly as a prototype for a simple, non-stock scrape target. Their login page seems to use Recaptcha, but it never prompted me with a captcha. What&amp;rsquo;s nice is they have a hidden JSON API to transactions, but it&amp;rsquo;s wonky. My Playwright script will login to venmo.com, then navigate to Statements, find the &lt;em>Download CSV&lt;/em> button, get the URL, modify to grab it in JSON format and the last 30 days, then download that data and update my SQL table.&lt;/p>
&lt;h2 id="fidelity">Fidelity&lt;/h2>
&lt;p>This one consumed most of my time. During login, Fidelity would randomly require a 2FA verification if I ever changed from my server to my dev desktop even though I statically defined the User-Agent. My scraper jobs don&amp;rsquo;t handle 2FA very well yet. Scraping the data was painful because Fidelity&amp;rsquo;s website seems to be composed of several different web frameworks and engines.&lt;/p>
&lt;p>Some data, like positions and activity, was available using CSV which I used when available, but positions data only contains stock level data, not at lot level.&lt;/p>
&lt;p>Other data is returned using server-rendered HTML, some of it is returned using JSON APIs. Sometimes I couldn&amp;rsquo;t figure out how to replicate web requests and would have to click a button and sniff the requests using Playwrights &lt;code>expect_request&lt;/code> mechanism. I had to use &lt;a class="link" href="https://beautiful-soup-4.readthedocs.io/en/latest/" target="_blank" rel="noopener"
>BeautifulSoup&lt;/a> to parse HTML responses in other times.&lt;/p>
&lt;p>The server-rendered HTML was problematic because it was designed for humans. For example, the tax lot table would contain icons for wash sales or include non-dates like the string &lt;em>Various&lt;/em> and other random details that was hard to process programmatically.&lt;/p>
&lt;h2 id="betterment">Betterment&lt;/h2>
&lt;p>Betterment was another (not) fun scraping. The plus side is while they did require 2FA auth, they supported TOTP based auth. I stored the TOTP seed in my Vault DB and used &lt;a class="link" href="https://github.com/pyauth/pyotp" target="_blank" rel="noopener"
>pytotp&lt;/a> to generate the token. I&amp;rsquo;ve since learned that Vault supports a &lt;a class="link" href="https://developer.hashicorp.com/vault/docs/secrets/totp" target="_blank" rel="noopener"
>TOTP provider&lt;/a> natively which would be more secure because it doesn&amp;rsquo;t vend out the TOTP seed, it just vends out the OTP codes. That&amp;rsquo;s better because it reduces the risk of full exposure.&lt;/p>
&lt;p>Once logged in, it was difficult to get the data I needed. They had some basic CSV exports, but it was severely lacking in useful data. The transaction level data CSV is unsuitable because it doesn&amp;rsquo;t contain symbol information. Their reports don&amp;rsquo;t even provide quarterly gains data making estimated tax payments hard.&lt;/p>
&lt;p>I even email them every few years and they never implemented any of my requests. They even told me &lt;code>We are not able to provide API access to an individual, though as noted you can access information through programs such as TurboTax.&lt;/code> Clearly they have APIs, they just don&amp;rsquo;t make them available.&lt;/p>
&lt;p>The only way to get access to activity data was to scrape PDF files. Which, I being masochist, did just that. Did you know that PDFs don&amp;rsquo;t have a DOM like HTML? It&amp;rsquo;s just absolute positioning of content. How do you parse a table of content? Well, have to grab the (x, y) coordinates of text that I expect to appear above, then the disclaimers that appear below, grab all the text in between that and somehow make a table out of that.&lt;/p>
&lt;h2 id="citibank">Citibank&lt;/h2>
&lt;p>Citi was unworkable. They required an SMS OTP every single time I signed in. No option to remember it. While I could try to extract the OTP from the SMS&amp;rsquo;s on my phone using &lt;a class="link" href="https://companion.home-assistant.io/docs/core/sensors/#notification-sensors" target="_blank" rel="noopener"
>Home Assistant&amp;rsquo;s Android App sensor&lt;/a>, I have not gone down that path given the fragility of that setup.&lt;/p>
&lt;h1 id="what-did-i-learn">What did I learn?&lt;/h1>
&lt;p>Banks don&amp;rsquo;t make it easy to scrape your own data. They make it hard with 2FA authentication using push notifications or SMS (which is &lt;a class="link" href="https://www.nist.gov/blogs/cybersecurity-insights/questionsand-buzz-surrounding-draft-nist-special-publication-800-63-3" target="_blank" rel="noopener"
>no-longer recommended by NIST&lt;/a>). I also have to take care not to authenticate too often, which makes debugging difficult because I sometimes debug the script by running the sync job entirely which logs out and logs back in. My session saving only stores cookies and localStorage, but does not persist any session cookies or sessionStorage.&lt;/p>
&lt;p>Synchronizing data without strong identifiers is tricky. When scraping data, I basically get a tuple of (date, amount, description). Without having some kind of identifier to uniquely and stably identify each transaction makes it challenging to figure out when a transaction should be created, updated, or deleted. This is similar to the problem I had &lt;a class="link" href="https://www.technowizardry.net/2024/05/solving-for-bank-transfers-using-pandas/" >when identifying transfer pairs&lt;/a>. Instead I haveatch based on dates, amounts, and sometimes descriptions, but that often causes me to delete and recreate instead of updating. This isn&amp;rsquo;t a major problem because it self corrects, but annoying.&lt;/p>
&lt;h1 id="wheres-the-code">Where&amp;rsquo;s the code?&lt;/h1>
&lt;p>Right now, my code is pretty coupled to my system, however I&amp;rsquo;m slowly moving to my GitHub repo here: &lt;a class="link" href="https://github.com/ajacques/own-your-finances" target="_blank" rel="noopener"
>ajacques/own-your-finance&lt;/a>.&lt;/p>
&lt;h1 id="whats-next">What&amp;rsquo;s next?&lt;/h1>
&lt;ul>
&lt;li>&lt;strong>Improve reliability&lt;/strong> - right now the scrapers don&amp;rsquo;t handle unexpected things well like 2FA, or slow or failed navigations. Next would be to implement a 2FA handler to get OTP codes&lt;/li>
&lt;li>&lt;strong>Handle more institutions&lt;/strong> - my goal is to support as many of the banks I or my friends use to reduce dependency on aggregators&lt;/li>
&lt;li>&lt;strong>Better analytics&lt;/strong> - right now Firefly and Ghostfolio are fine, but they lack in some of the features I&amp;rsquo;m looking for like good charts and tables&lt;/li>
&lt;/ul>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F08%2Fmy-financial-data-scraping-system%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=My+financial+data+scraping+system" style="border:0" alt="" /></description></item><item><title>Securing data using Vault in a Home Lab</title><link>https://www.technowizardry.net/2024/08/vault-for-a-home-lab/</link><pubDate>Mon, 12 Aug 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/08/vault-for-a-home-lab/</guid><summary>&lt;p>I have several projects running in my Home Lab that now have to store and use sensitive secrets. In my &lt;a class="link" href="https://www.technowizardry.net/series/self-hosted-finances" >Self-hosted finances series&lt;/a>, I developed software to scrape my own bank statements (more on that coming soon.) In other projects, I store API keys to manage DNS or even my dedicated servers.&lt;/p>
&lt;p>These applications all run in Kubernetes, which does support &lt;a class="link" href="https://kubernetes.io/docs/concepts/configuration/secret/" target="_blank" rel="noopener"
>Secrets&lt;/a>, however, by default, they are not encrypted and are easily accessible to actors that have access to the K8s API.&lt;/p></summary><description>&lt;p>I have several projects running in my Home Lab that now have to store and use sensitive secrets. In my &lt;a class="link" href="https://www.technowizardry.net/series/self-hosted-finances" >Self-hosted finances series&lt;/a>, I developed software to scrape my own bank statements (more on that coming soon.) In other projects, I store API keys to manage DNS or even my dedicated servers.&lt;/p>
&lt;p>These applications all run in Kubernetes, which does support &lt;a class="link" href="https://kubernetes.io/docs/concepts/configuration/secret/" target="_blank" rel="noopener"
>Secrets&lt;/a>, however, by default, they are not encrypted and are easily accessible to actors that have access to the K8s API.&lt;/p>
&lt;p>It supports &lt;a class="link" href="https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/" target="_blank" rel="noopener"
>encryption at rest&lt;/a> using a static encryption key or KMS plugin, which is fine, but I wanted to try out &lt;a class="link" href="https://www.vaultproject.io/" target="_blank" rel="noopener"
>Hashicorp Vault&lt;/a> which stored the decryption keys in memory only, so if somebody were to get access to my server (say by breaking into my house and stealing my server), they wouldn&amp;rsquo;t get access to all my secrets.&lt;/p>
&lt;!-- more -->
&lt;h1 id="pre-requisites">Pre-requisites&lt;/h1>
&lt;p>This post assumes that you have a working Kubernetes cluster.&lt;/p>
&lt;h1 id="installation-into-kubernetes">Installation into Kubernetes&lt;/h1>
&lt;p>Instead of duplicating the guides that already exist, I recommend following this guide:
&lt;a class="link" href="https://developer.hashicorp.com/vault/tutorials/kubernetes/kubernetes-raft-deployment-guide" target="_blank" rel="noopener"
>https://developer.hashicorp.com/vault/tutorials/kubernetes/kubernetes-raft-deployment-guide&lt;/a>&lt;/p>
&lt;h1 id="the-plan">The plan&lt;/h1>
&lt;p>In my cluster, I have two actors:&lt;/p>
&lt;ul>
&lt;li>Various Kubernetes pods - limited read/write to different buckets&lt;/li>
&lt;li>Me - full admin access&lt;/li>
&lt;/ul>
&lt;h1 id="configure-an-auth-method">Configure an Auth Method&lt;/h1>
&lt;p>Authentication Methods are the way actors like you or your services authenticate with Vault. Combined with policies, it defines what actions are allowed to be performed.&lt;/p>
&lt;p>We&amp;rsquo;ll have two auth methods (mapping to the above actors):&lt;/p>
&lt;ul>
&lt;li>Kubernetes&lt;/li>
&lt;li>userpass&lt;/li>
&lt;/ul>
&lt;h2 id="create-an-admin-policy">Create an admin policy&lt;/h2>
&lt;p>Let&amp;rsquo;s create a basic admin policy that will be assigned to yourself with full permissions. This policy has full admin access so it should only be used by trusted actors.&lt;/p>
&lt;p>In the Vault UI, go to Policies &amp;gt; Create ACL Policy. Name it &lt;strong>admin&lt;/strong> or similar and click create.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">path &amp;#34;sys/health&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;read&amp;#34;, &amp;#34;sudo&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">path &amp;#34;*&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;create&amp;#34;, &amp;#34;read&amp;#34;, &amp;#34;update&amp;#34;, &amp;#34;list&amp;#34;, &amp;#34;sudo&amp;#34;, &amp;#34;delete&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">path &amp;#34;sys/policies/acl&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;list&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">path &amp;#34;data/*&amp;#34; {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;read&amp;#34;, &amp;#34;list&amp;#34;, &amp;#34;update&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">path &amp;#34;sys/policies/acl/*&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;create&amp;#34;, &amp;#34;read&amp;#34;, &amp;#34;update&amp;#34;, &amp;#34;delete&amp;#34;, &amp;#34;list&amp;#34;, &amp;#34;sudo&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">path &amp;#34;auth/*&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;create&amp;#34;, &amp;#34;read&amp;#34;, &amp;#34;update&amp;#34;, &amp;#34;delete&amp;#34;, &amp;#34;list&amp;#34;, &amp;#34;sudo&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">path &amp;#34;sys/auth/*&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;create&amp;#34;, &amp;#34;update&amp;#34;, &amp;#34;delete&amp;#34;, &amp;#34;sudo&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">path &amp;#34;sys/auth&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;read&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">path &amp;#34;secret/*&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;create&amp;#34;, &amp;#34;read&amp;#34;, &amp;#34;update&amp;#34;, &amp;#34;delete&amp;#34;, &amp;#34;list&amp;#34;, &amp;#34;sudo&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">path &amp;#34;sys/mounts/*&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;create&amp;#34;, &amp;#34;read&amp;#34;, &amp;#34;update&amp;#34;, &amp;#34;delete&amp;#34;, &amp;#34;list&amp;#34;, &amp;#34;sudo&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">path &amp;#34;sys/mounts&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;read&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="userpassword">User/Password&lt;/h2>
&lt;p>It&amp;rsquo;s not a good practice to log in using the root key every time, so we&amp;rsquo;re going to create a username/password to use instead. To be pragmatic, I still have the root key because I know I&amp;rsquo;m going to need to make a change eventually. If this were a business, I&amp;rsquo;d lock it in a safe or destroy it.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/08/vault-for-a-home-lab/images/vault-enable-new-method.png"
width="265"
height="63"
srcset="https://www.technowizardry.net/2024/08/vault-for-a-home-lab/images/vault-enable-new-method_hu_39bafa26250f4e02.png 480w, https://www.technowizardry.net/2024/08/vault-for-a-home-lab/images/vault-enable-new-method_hu_2a7712264372a97d.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="420"
data-flex-basis="1009px"
>&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/08/vault-for-a-home-lab/images/vault-userpass.png"
width="175"
height="161"
srcset="https://www.technowizardry.net/2024/08/vault-for-a-home-lab/images/vault-userpass_hu_e53092e3d29b3348.png 480w, https://www.technowizardry.net/2024/08/vault-for-a-home-lab/images/vault-userpass_hu_207484d81f6a8732.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="108"
data-flex-basis="260px"
>&lt;/p>
&lt;p>I recommend enabling a Lease TTL which will cause the session to automatically expire.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/08/vault-for-a-home-lab/images/vault-create-userpass.png"
width="1438"
height="1478"
srcset="https://www.technowizardry.net/2024/08/vault-for-a-home-lab/images/vault-create-userpass_hu_f39bdb58ed74c943.png 480w, https://www.technowizardry.net/2024/08/vault-for-a-home-lab/images/vault-create-userpass_hu_41fc9ed8f12ae9cb.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="97"
data-flex-basis="233px"
>&lt;/p>
&lt;p>Then create a user and attach the admin policy to the user.&lt;/p>
&lt;h2 id="kubernetes-auth-method">Kubernetes Auth Method&lt;/h2>
&lt;p>The Kubernetes Auth method defines how Kubernetes pods will authenticate to the Vault server and be able to receive and update credentials. Create the Kubernetes auth method.&lt;/p>
&lt;p>Then configure it to point to your apiserver. For example, the Kubernetes host might be https://10.43.0.1:443/ and the Kubernetes CA Certificate can be retrieved using:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">kubectl get configmap kube-root-ca.crt -o jsonpath=&amp;#34;{[&amp;#39;data&amp;#39;][&amp;#39;ca\.crt&amp;#39;]}&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://www.technowizardry.net/2024/08/vault-for-a-home-lab/images/vault-k8s-create.png"
width="1333"
height="700"
srcset="https://www.technowizardry.net/2024/08/vault-for-a-home-lab/images/vault-k8s-create_hu_5d4c5243a96ede8a.png 480w, https://www.technowizardry.net/2024/08/vault-for-a-home-lab/images/vault-k8s-create_hu_4268cc98eb6cff9.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="190"
data-flex-basis="457px"
>&lt;/p>
&lt;h1 id="getting-application-specific">Getting application specific&lt;/h1>
&lt;p>Now that Vault is setup, we need to create the resources that pods will use. This will get into application specific configuration so you might do this one or more times. Vault uses the term engine to describe something where secrets are stored or vended. There&amp;rsquo;s a few different types, but for storing API keys, username/passwords to my accounts, a &lt;em>KV&lt;/em> or (key-value) storage should be used. Create a &lt;em>KV&lt;/em> engine and name it something meaningful for your application. For example, &lt;code>financialcreds&lt;/code>.&lt;/p>
&lt;h2 id="create-the-vault-policies">Create the Vault Policies&lt;/h2>
&lt;p>Next, we need a policy that allows services to access it (we already have access through the &lt;em>admin&lt;/em> policy).&lt;/p>
&lt;p>Go back to Policies &amp;gt; Create ACL Policy and create a policy like below. Note how the path only gives access to the engine. You can also do something like &lt;code>foobar/baz/*&lt;/code> to give access to a folder&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">path &amp;#34;financialcreds/*&amp;#34; {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities = [&amp;#34;read&amp;#34;, &amp;#34;update&amp;#34;, &amp;#34;list&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="create-vault-roles">Create Vault Roles&lt;/h2>
&lt;p>In Vault, a role maps the Kubernetes System Account to the list of Vault policies and permissions, and ultimately the secrets that the pod can access. Some useful configuration to specify:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Name&lt;/strong>: Role name. This will be used in your code later&lt;/li>
&lt;li>&lt;strong>Bound service account names&lt;/strong>: The name of the Kubernetes SystemAccount resource&lt;/li>
&lt;li>&lt;strong>Bound service account namespaces&lt;/strong>: The namespace(s) where the SystemAccount exists&lt;/li>
&lt;li>&lt;strong>Generated Token&amp;rsquo;s Maximum TTL&lt;/strong>: (optional) Sets the maximum lifetime the token lives for. This ensures that if a token leaks from a short-term job, it can&amp;rsquo;t be used for long. For my periodic scheduled sync jobs, I use &lt;em>30m&lt;/em> so credentials expire after 30 minutes.&lt;/li>
&lt;li>&lt;strong>Generated Token&amp;rsquo;s Policies&lt;/strong>: Specify the policies that this role has access to. Add the policy you just created in the previous section (not the admin one).&lt;/li>
&lt;li>&lt;strong>Generated Token&amp;rsquo;s Type&lt;/strong>: (optional) &lt;a class="link" href="https://developer.hashicorp.com/vault/tutorials/tokens/tokens#token-types" target="_blank" rel="noopener"
>Vault docs&lt;/a>&lt;/li>
&lt;/ul>
&lt;h1 id="using-it-in-practice">Using it in practice&lt;/h1>
&lt;p>If you did everything right, you should be able to run a job that authenticates to Vault and grabs a secret.&lt;/p>
&lt;p>First we need a ServiceAccount with a matching name in the Vault role. Make sure to set &lt;code>automountServiceAccountToken: true&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">automountServiceAccountToken&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ServiceAccount&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-account-specified-in-vault-role&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-namespace-specified-in-vault-role&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then update the CronJob, Deployment, etc.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">batch/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CronJob&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sync-job&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-namespace-specified-in-vault-role&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jobTemplate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">serviceAccountName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-account-specified-in-vault-role&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And some Python sample code to login and read:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">hvac&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">os&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">hvac.api.auth_methods&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Kubernetes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">vaultUrl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;http://vault.vault.svc.cluster.local.&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="s1">&amp;#39;KUBERNETES_SERVICE_HOST&amp;#39;&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">environ&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">client&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">hvac&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Client&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">vaultUrl&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">jwt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/var/run/secrets/kubernetes.io/serviceaccount/token&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Kubernetes&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">adapter&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">login&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">role&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;rolename&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># Role name specified above&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">jwt&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">jwt&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">environ&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;VAULT_TOKEN&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">client&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">hvac&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Client&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">vaultUrl&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">token&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>We didn&amp;rsquo;t do anything store anything in Vault yet, but just setup the basics. In my future posts, I&amp;rsquo;ll be showing how I leverage Vault to manage my financial scraping.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F08%2Fvault-for-a-home-lab%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Securing+data+using+Vault+in+a+Home+Lab" style="border:0" alt="" /></description></item><item><title>Securing MQTT Traffic using cert-manager</title><link>https://www.technowizardry.net/2024/07/securing-mqtt-traffic-using-cert-manager/</link><pubDate>Sat, 06 Jul 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/07/securing-mqtt-traffic-using-cert-manager/</guid><summary>&lt;p>I use MQTT in my home lab to connect different Home Lab services like &lt;a class="link" href="https://esphome.io" target="_blank" rel="noopener"
>ESPHome&lt;/a>, &lt;a class="link" href="https://www.home-assistant.io/" target="_blank" rel="noopener"
>Home Assistant&lt;/a>, &lt;a class="link" href="https://nodered.org/" target="_blank" rel="noopener"
>Node Red&lt;/a>, etc. It&amp;rsquo;s great because it&amp;rsquo;s a light-weight way to decouple these services, but by default there&amp;rsquo;s no security. I can&amp;rsquo;t prevent a sensor from manipulating another sensor&amp;rsquo;s data, I can&amp;rsquo;t prevent somebody who has network access from monitoring messages.&lt;/p>
&lt;p>In this post, I&amp;rsquo;m going to walk through enabling TLS with usernames and passwords or mTLS (Mutual TLS) using &lt;a class="link" href="https://cert-manager.io/" target="_blank" rel="noopener"
>cert-manager&lt;/a>. Cert-manager supports a mechanism to generate &lt;a class="link" href="https://cert-manager.io/docs/configuration/ca/" target="_blank" rel="noopener"
>self-signed CA certs&lt;/a> that I will use.&lt;/p></summary><description>&lt;p>I use MQTT in my home lab to connect different Home Lab services like &lt;a class="link" href="https://esphome.io" target="_blank" rel="noopener"
>ESPHome&lt;/a>, &lt;a class="link" href="https://www.home-assistant.io/" target="_blank" rel="noopener"
>Home Assistant&lt;/a>, &lt;a class="link" href="https://nodered.org/" target="_blank" rel="noopener"
>Node Red&lt;/a>, etc. It&amp;rsquo;s great because it&amp;rsquo;s a light-weight way to decouple these services, but by default there&amp;rsquo;s no security. I can&amp;rsquo;t prevent a sensor from manipulating another sensor&amp;rsquo;s data, I can&amp;rsquo;t prevent somebody who has network access from monitoring messages.&lt;/p>
&lt;p>In this post, I&amp;rsquo;m going to walk through enabling TLS with usernames and passwords or mTLS (Mutual TLS) using &lt;a class="link" href="https://cert-manager.io/" target="_blank" rel="noopener"
>cert-manager&lt;/a>. Cert-manager supports a mechanism to generate &lt;a class="link" href="https://cert-manager.io/docs/configuration/ca/" target="_blank" rel="noopener"
>self-signed CA certs&lt;/a> that I will use.&lt;/p>
&lt;!-- more -->
&lt;h1 id="pre-req-mqtt-broker">(Pre-req) MQTT Broker&lt;/h1>
&lt;p>I&amp;rsquo;m using &lt;a class="link" href="https://mosquitto.org/" target="_blank" rel="noopener"
>Mosquitto&lt;/a> as my MQTT broker and will assume you already have it setup. Additionally, you&amp;rsquo;ll need to have the ability to edit the configuration files. I created a Kubernetes PVC and mounted it.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;span class="lnt">68
&lt;/span>&lt;span class="lnt">69
&lt;/span>&lt;span class="lnt">70
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mqtt-broker&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">strategy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rollingUpdate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxSurge&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="l">%&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxUnavailable&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="l">%&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RollingUpdate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workload.user.cattle.io/workloadselector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">deployment-smarthome-mqtt-broker&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workload.user.cattle.io/workloadselector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">deployment-smarthome-mqtt-broker&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">eclipse-mosquitto:2.0.18-openssl&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">imagePullPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">IfNotPresent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mqtt-broker&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">16Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">16Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">terminationMessagePath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/dev/termination-log&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">terminationMessagePolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">File&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/mosquitto/config/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readOnly&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Side-car will trigger Mosquitto to reload when the config changes&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># No need to restart the entire pod.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CONFIG_DIR&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> /config/mosquitto.conf,/config/acl.conf,/config/passwd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PROCESS_NAME&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mosquitto&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ajacques/config-reloader-sidecar:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">imagePullPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">IfNotPresent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config-reloader&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">16Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">16Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">securityContext&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowPrivilegeEscalation&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">capabilities&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">add&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">KILL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">drop&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">ALL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">privileged&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readOnlyRootFilesystem&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runAsNonRoot&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/config/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readOnly&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">persistentVolumeClaim&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">claimName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mqtt-broker&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="setting-up-the-private-ca">Setting up the private CA&lt;/h1>
&lt;p>First step is to create a root certificate that will serve as the trusted root store&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Issuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome-ca&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selfSigned&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Certificate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome-ca-cert&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commonName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">homelab-ca&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">duration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">87600h0m0s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">isCA&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">issuerRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Issuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome-ca&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">privateKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">algorithm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ECDSA&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">size&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">256&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">root-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="ca-signer">CA Signer&lt;/h2>
&lt;p>Next step is to create a &lt;a class="link" href="https://cert-manager.io/docs/concepts/issuer/" target="_blank" rel="noopener"
>cert-manager Issuer&lt;/a> that will sign certificates using the root CA created above:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Issuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome-issuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ca&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">root-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="mqtt-server-certificate">MQTT Server Certificate&lt;/h2>
&lt;p>Next step is to create a certificate for the MQTT server. Note that I include both the external and internal Kubernetes DNS names as a Subject Alternate Name. This ensures that the certificate will validate both inside the cluster and outside.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Certificate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mqtt-server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commonName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mqtt.example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dnsNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Include the internal domain name too if&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># services are directly connecting to it&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">mqtt.example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">mqtt-headless.smarthome.svc.cluster.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">issuerRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Issuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome-issuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">privateKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">algorithm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ECDSA&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">size&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">256&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mqtt-server-cert&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="configuring-the-server">Configuring the server&lt;/h1>
&lt;p>Right now, the MQTT broker is going to have all these identities, but is going to do nothing with them. Combining this with some authorization rules using ACLs will enable us to control what topics each device can read from and write to.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;span class="lnt">68
&lt;/span>&lt;span class="lnt">69
&lt;/span>&lt;span class="lnt">70
&lt;/span>&lt;span class="lnt">71
&lt;/span>&lt;span class="lnt">72
&lt;/span>&lt;span class="lnt">73
&lt;/span>&lt;span class="lnt">74
&lt;/span>&lt;span class="lnt">75
&lt;/span>&lt;span class="lnt">76
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl"> apiVersion: apps/v1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> kind: Deployment
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> metadata:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: mqtt-broker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> namespace: smarthome
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> spec:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> replicas: 1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> selector:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> matchLabels:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> workload.user.cattle.io/workloadselector: deployment-smarthome-mqtt-broker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> template:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> metadata:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> labels:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> workload.user.cattle.io/workloadselector: deployment-smarthome-mqtt-broker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> spec:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> containers:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - image: eclipse-mosquitto:2.0.18-openssl
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> imagePullPolicy: IfNotPresent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: mqtt-broker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> resources:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> limits:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> memory: 16Mi
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> requests:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> cpu: 5m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> memory: 16Mi
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> terminationMessagePath: /dev/termination-log
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> terminationMessagePolicy: File
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> volumeMounts:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - mountPath: /mosquitto/config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: config
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> readOnly: true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ - mountPath: /ssl/cert
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ name: ssl
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ readOnly: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span> - env:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - name: CONFIG_DIR
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">- value: /config/mosquitto.conf,/config/acl.conf,/config/passwd
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+ value: /config/mosquitto.conf,/config/acl.conf,/config/passwd,/ssl/tls.key,/ssl/tls.crt
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span> - name: PROCESS_NAME
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> value: mosquitto
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> image: ajacques/config-reloader-sidecar:latest
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> imagePullPolicy: IfNotPresent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: config-reloader
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> resources:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> limits:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> memory: 16Mi
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> requests:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> memory: 16Mi
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> securityContext:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> allowPrivilegeEscalation: false
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> capabilities:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> add:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - KILL
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> drop:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - ALL
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> privileged: false
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> readOnlyRootFilesystem: true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> runAsNonRoot: false
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> terminationMessagePath: /dev/termination-log
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> terminationMessagePolicy: File
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> volumeMounts:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - mountPath: /config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: config
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> readOnly: true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ - mountPath: /ssl
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ name: ssl
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ readOnly: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span> volumes:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - name: config
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> persistentVolumeClaim:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> claimName: mqtt-broker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ - name: ssl
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ secret:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ defaultMode: 420
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ optional: false
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ secretName: mqtt-server-cert
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>To start, we have a &lt;code>mosquitto.conf&lt;/code> that looks like the below. It means that anybody can connect on port 1883 with no auth.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">log_dest stdout
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log_type notice
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">per_listener_settings true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">listener 1883
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">allow_anonymous true
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I&amp;rsquo;ll add a new port that requires authentication. The first port requires username and passwords (Make sure to create a passwd file if you want this.) Some devices, like esp devices running &lt;a class="link" href="https://esphome.io" target="_blank" rel="noopener"
>esphome&lt;/a> don&amp;rsquo;t have great support for mTLS, so I have to use usernames.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># TLS but with usernames and passwords
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">listener 8882
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tls_version tlsv1.2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">password_file /mosquitto/config/passwd
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The next port requires mTLS for all connections. This will be added in parallel so I can slowly move devices over:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># Mutual TLS - Encryption
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">listener 8883
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tls_version tlsv1.2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">require_certificate true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">use_identity_as_username true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cafile /ssl/cert/ca.crt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">certfile /ssl/cert/tls.crt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">keyfile /ssl/cert/tls.key
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="configuring-acls">Configuring ACLs&lt;/h1>
&lt;p>In the server configuration, we need to add:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl"> listener 8882
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+acl_file /mosquitto/config/acl.conf
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> listener 8883
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+acl_file /mosquitto/config/acl.conf
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>You can read more about the ACL file format &lt;a class="link" href="https://mosquitto.org/man/mosquitto-conf-5.html" target="_blank" rel="noopener"
>here&lt;/a>. My file looks like this. By default authenticated devices are able to read and write to their own device topic. Each authenticated device has a username. For username/password, it&amp;rsquo;s obviously the username and for mTLS it comes from the &lt;code>CN&lt;/code> in the subject (You can see an example in the Usage section below.)&lt;/p>
&lt;p>This first section gives a default. All devices have access to their own topics (&lt;code>{device}/{sensor_name}&lt;/code>) and their equivalent Home Assistant discovery topics (&lt;code>homeassistant/{domain}/{device}/{sensor_name}&lt;/code>). This is implemented with the &lt;code>%u&lt;/code> username placeholder. Most of my devices are running esphome so this works well.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">pattern readwrite esphome/discover/%u
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pattern readwrite %u/#
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pattern readwrite homeassistant/+/%u/#
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Some clients need different privileges because they aren&amp;rsquo;t esp devices. For example, my control software needs higher privileges. My &lt;a class="link" href="https://nodered.org/" target="_blank" rel="noopener"
>Node-Red&lt;/a> service can consume anything and write to anything (I should restrict this more) and my &lt;a class="link" href="https://github.com/AppDaemon/appdaemon" target="_blank" rel="noopener"
>AppDaemon&lt;/a> has some access.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">user node-red
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">topic read #
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">topic write #
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">user appdaemon
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">topic read #
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">topic write homeassistant/#
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Place this &lt;code>acl.conf&lt;/code> in the same folder as the configuration file.&lt;/p>
&lt;h1 id="usage">Usage&lt;/h1>
&lt;h2 id="mqtt-client-certificates">MQTT Client Certificates&lt;/h2>
&lt;p>Repeat this step for as many clients that you have.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Certificate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mqtt-{clientname}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commonName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="l">clientname}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">issuerRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Issuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome-issuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">privateKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">algorithm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ECDSA&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">size&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">256&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mqtt-{clientname}-cert&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This certificate can then be mounted into any Kubernetes pod and used.&lt;/p>
&lt;h3 id="node-red-example">Node-Red example&lt;/h3>
&lt;p>For example, in Node-Red I&amp;rsquo;ll do:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl"> apiVersion: apps/v1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> kind: Deployment
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> metadata:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   name: hass-node-red
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">   namespace: smarthome
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> spec:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">     spec:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">       containers:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">           name: node-red
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">           volumeMounts:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+            - mountPath: /ssl/mqtt
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+              name: mqtt-ssl
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+              readOnly: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span>       volumes:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+        - name: mqtt-ssl
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+          secret:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+            defaultMode: 420
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+            optional: false
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+            secretName: mqtt-node-red-cert
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And configured in the UI to use this here:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/07/securing-mqtt-traffic-using-cert-manager/images/node-red-mtls-1.png"
width="485"
height="580"
srcset="https://www.technowizardry.net/2024/07/securing-mqtt-traffic-using-cert-manager/images/node-red-mtls-1_hu_58492424118dfd5.png 480w, https://www.technowizardry.net/2024/07/securing-mqtt-traffic-using-cert-manager/images/node-red-mtls-1_hu_e376b5e57019ee66.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="83"
data-flex-basis="200px"
>&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/07/securing-mqtt-traffic-using-cert-manager/images/node-red-mtls-2.png"
width="480"
height="53"
srcset="https://www.technowizardry.net/2024/07/securing-mqtt-traffic-using-cert-manager/images/node-red-mtls-2_hu_7c4b9fa27b80e905.png 480w, https://www.technowizardry.net/2024/07/securing-mqtt-traffic-using-cert-manager/images/node-red-mtls-2_hu_f0d2393ea3e6d335.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="905"
data-flex-basis="2173px"
>&lt;/p>
&lt;h2 id="esphome">ESPHome&lt;/h2>
&lt;p>As of the time of writing this, I didn&amp;rsquo;t have great luck with mTLS on my esp32 devices. First, I had to extend the lifetime of the certificates so I didn&amp;rsquo;t have to reflash devices every time they expired. It was also hard to get it to verify the Host identity too because the &lt;code>mqtt.certificate_authority&lt;/code> field is only available on &lt;code>platform: esp-idf&lt;/code>, and not all my device configurations worked with this (e.g. &lt;a class="link" href="https://esphome.io/components/light/neopixelbus.html" target="_blank" rel="noopener"
>NeoPixels&lt;/a> didn&amp;rsquo;t work with &lt;code>platform: esp-idf&lt;/code>).&lt;/p>
&lt;h3 id="usernames">Usernames&lt;/h3>
&lt;p>However, it does support have support for username/passwords. In the above configuration, I add a password listener on port &lt;code>8882&lt;/code>, so this can be done using:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">mqtt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">broker&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mqtt.home.ajacqu.es&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8882&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">username&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="l">myusername}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="l">mypassword}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>To enable CA verification on &lt;code>esp32&lt;/code> and &lt;code>esp-idf&lt;/code> devices, we need to import the certificate authority which is available in the &lt;code>ca.crt&lt;/code> key of the Secret generated by cert-manager.&lt;/p>
&lt;p>Either copy the &lt;code>ca.crt&lt;/code> value out of the secret that cert-manager generates, or inject it into ESPHome:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl"> apiVersion: apps/v1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> kind: Deployment
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> metadata:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: esphome
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> namespace: smarthome
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> spec:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> spec:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> containers:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - name: main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> volumeMounts:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - mountPath: /config
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: data
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - mountPath: /config/.esphome/build
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: tempfolder
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> subPath: build
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - mountPath: /config/.esphome/platformio
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: tempfolder
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> subPath: platformio
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - mountPath: /config/.esphome/external_components
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: tempfolder
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> subPath: external_components
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ - mountPath: /config/ssl-ca.crt
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ name: tls-cert
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ readOnly: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ subPath: ca.crt
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span> volumes:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - hostPath:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> path: /tmp/k8s-esphome/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> type: DirectoryOrCreate
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: tempfolder
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - name: data
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> persistentVolumeClaim:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> claimName: esphome2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ - name: tls-cert
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ secret:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ defaultMode: 420
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ optional: false
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ secretName: mqtt-server-cert
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then use it in any device:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">mqtt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">broker&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mqtt.example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8882&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">username&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xyz&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">abc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">certificate_authority&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>!&lt;span class="l">include ssl-ca.crt&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>Unfortunately, I went through this exercise and don&amp;rsquo;t have a great story for mTLS on the ESPHome devices which is an important use case, but I did get it working on my internal services, the ones that have higher privileges anyway.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F07%2Fsecuring-mqtt-traffic-using-cert-manager%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Securing+MQTT+Traffic+using+cert-manager" style="border:0" alt="" /></description></item><item><title>Auto switch between light and dark mode on GNOME</title><link>https://www.technowizardry.net/2024/06/auto-switch-between-light-and-dark-mode-on-gnome/</link><pubDate>Tue, 18 Jun 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/06/auto-switch-between-light-and-dark-mode-on-gnome/</guid><summary>&lt;p>I recently got a Framework laptop and installed Ubuntu on it to give Linux for laptops a chance after using Windows and Mac for work for years. One thing I wanted was to be able to switch between light mode and dark mode automatically depending on the time of day. GNOME had a blue-light filter mode that could automatically turn on, but it didn&amp;rsquo;t appear to have a way to switch between light mode and dark mode at the same time.&lt;/p></summary><description>&lt;p>I recently got a Framework laptop and installed Ubuntu on it to give Linux for laptops a chance after using Windows and Mac for work for years. One thing I wanted was to be able to switch between light mode and dark mode automatically depending on the time of day. GNOME had a blue-light filter mode that could automatically turn on, but it didn&amp;rsquo;t appear to have a way to switch between light mode and dark mode at the same time.&lt;/p>
&lt;figure>
&lt;img src="
/2024/06/auto-switch-between-light-and-dark-mode-on-gnome/images/gnome-settings-night-light.png" alt="The GNOME settings window showing the ability to automatically enable/disable a blue light filter at specific times of the day." />
&lt;figcaption>The GNOME settings window showing the ability to automatically enable/disable a blue light filter at specific times of the day.&lt;/figcaption>
&lt;/figure>
&lt;h1 id="research">Research&lt;/h1>
&lt;h2 id="sort-of-working">Sort of Working&lt;/h2>
&lt;p>The following command seemed to switch the theme, but not all apps would follow. The terminal and GNOME would follow, but Firefox wouldn&amp;rsquo;t.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">gsettings &lt;span class="nb">set&lt;/span> org.gnome.desktop.interface gtk-theme Yaru-dark
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gsettings &lt;span class="nb">set&lt;/span> org.gnome.desktop.interface gtk-theme Yaru
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="digging-through-code">Digging through Code&lt;/h2>
&lt;figure>
&lt;img src="
/2024/06/auto-switch-between-light-and-dark-mode-on-gnome/images/gnome-settings-appearance.png" alt="" />
&lt;figcaption>The GNOME settings window showing light and dark mode, but no option to schedule it&lt;/figcaption>
&lt;/figure>
&lt;p>To figure this out, I dove into the source code for the GNOME settings tool (GitHub &lt;a class="link" href="https://github.com/GNOME/gnome-control-center" target="_blank" rel="noopener"
>GNOME/gnome-control-center&lt;/a>). Searching for strings like &amp;ldquo;light&amp;rdquo; and &amp;ldquo;dark&amp;rdquo; led me to the file &lt;a class="link" href="https://github.com/GNOME/gnome-control-center/blob/a670744ebaeba540a9d26db75d91b5a6fe95dba5/panels/background/cc-background-panel.c#L129" target="_blank" rel="noopener"
>panels/background/cc-background-panel.c&lt;/a> which implemented this settings page.&lt;/p>
&lt;p>These methods suggested there was a different setting that controlled the light/dark mode:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="cl">&lt;span class="k">static&lt;/span> &lt;span class="kt">void&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">on_color_scheme_toggle_active_cb&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">CcBackgroundPanel&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">self&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">gtk_toggle_button_get_active&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">self&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">default_toggle&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">set_color_scheme&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">G_DESKTOP_COLOR_SCHEME_DEFAULT&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">gtk_toggle_button_get_active&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">self&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">dark_toggle&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">set_color_scheme&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">G_DESKTOP_COLOR_SCHEME_PREFER_DARK&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">static&lt;/span> &lt;span class="kt">void&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">set_color_scheme&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">CcBackgroundPanel&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">self&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">GDesktopColorScheme&lt;/span> &lt;span class="n">color_scheme&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Removed
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">g_settings_set_enum&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">self&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">interface_settings&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">INTERFACE_COLOR_SCHEME_KEY&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">color_scheme&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then using the following commands it actually worked and fully changed the color in Firefox and across my desktop environment.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gsettings &lt;span class="nb">set&lt;/span> org.gnome.desktop.interface color-scheme prefer-dark
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gsettings &lt;span class="nb">set&lt;/span> org.gnome.desktop.interface color-scheme prefer-light
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="scripts">Scripts&lt;/h1>
&lt;p>I started with the script from &lt;a class="link" href="https://askubuntu.com/questions/1234742/automatic-light-dark-mode" target="_blank" rel="noopener"
>here&lt;/a> and changed the gsettings key&lt;/p>
&lt;p>&lt;code>/usr/bin/gnome-theme-switcher.sh&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>set_theme&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;dark&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">new_gtk_theme&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;prefer-dark&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;light&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">new_gtk_theme&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;prefer-light&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;[!] Unsupported theme: &lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">export&lt;/span> &lt;span class="nv">DBUS_SESSION_BUS_ADDRESS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$DBUS_SESSION_BUS_ADDRESS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DBUS_SESSION_BUS_ADDRESS&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">current_gtk_theme&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>gsettings get org.gnome.desktop.interface color-scheme&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># echo &amp;#34;[.] Currently using ${current_gtk_theme}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">current_gtk_theme&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;&amp;#39;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">new_gtk_theme&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#39;&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;[i] Already using gtk &amp;#39;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">new_gtk_theme&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#39; theme&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;[-] Setting gtk theme to &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">new_gtk_theme&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> gsettings &lt;span class="nb">set&lt;/span> org.gnome.desktop.interface color-scheme &lt;span class="si">${&lt;/span>&lt;span class="nv">new_gtk_theme&lt;/span>&lt;span class="si">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;[✓] gtk theme changed to &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">new_gtk_theme&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># If script run without argument&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> -z &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">currenttime&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>date +%H:%M&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$currenttime&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &amp;gt; &lt;span class="s2">&amp;#34;17:00&amp;#34;&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$currenttime&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &amp;lt; &lt;span class="s2">&amp;#34;09:00&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> set_theme dark
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> set_theme light
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> set_theme &lt;span class="nv">$1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Move the file into a safe location. I use &lt;code>/usr/bin/&lt;/code> so it&amp;rsquo;s not user-writable by default:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo mv ~/gnome-theme-switcher.sh /usr/bin/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chmod +x /usr/bin/gnome-theme-switcher.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="scheduling-using-systemd">Scheduling using systemd&lt;/h1>
&lt;p>Next up is getting systemd to run the command. The service unit defines what command to run.&lt;/p>
&lt;p>&lt;code>~/.config/systemd/user/auto-theme-switcher.service&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">Description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">Auto adjusts GNOME theme between dark and light to match the time&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">After&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">suspend.target&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">[Service]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">Type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">oneshot&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">ExecStart&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">/usr/bin/gnome-theme-switcher.sh&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">TimeoutSec&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">30&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">StandardOutput&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">journal&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">PrivateTmp&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">ProtectSystem&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">full&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">ProtectHome&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">read-only&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">[Install]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">WantedBy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">default.target&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The timer defines when the command should run. I choose to run mine at 8pm and 9am. If you change it, make sure to update the script above.&lt;/p>
&lt;p>&lt;code>~/.config/systemd/user/auto-theme-switcher.timer&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">Description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">Auto adjusts GNOME theme between dark and light to match the time&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">[Timer]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">OnCalendar&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">*-*-* 20:00:00 # 8pm&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">OnCalendar&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">*-*-* 09:00:00 # 9am&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">[Install]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">WantedBy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">timers.target&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then run the following to enable it&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">systemctl --user &lt;span class="nb">enable&lt;/span> auto-theme.service
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">systemctl --user &lt;span class="nb">enable&lt;/span> auto-theme.timer
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>You can test the command by running:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">systemctl --user start auto-theme.service
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Update: Nov 11, 2025&lt;/p>
&lt;p>I&amp;rsquo;ve since switched to KDE which has a built in night mode, so I don&amp;rsquo;t have to use this. However, a reader helpfully shared their update to the script &lt;a class="link" href="https://github.com/KrzysiekGL/gnome_switch_style" target="_blank" rel="noopener"
>here&lt;/a> which incorporates automatic sunset/sunrise schedule adjusting&lt;/p>
&lt;h1 id="references">References&lt;/h1>
&lt;ul>
&lt;li>&lt;a class="link" href="https://askubuntu.com/questions/1234742/automatic-light-dark-mode" target="_blank" rel="noopener"
>https://askubuntu.com/questions/1234742/automatic-light-dark-mode&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://extensions.gnome.org/extension/2236/night-theme-switcher/" target="_blank" rel="noopener"
>https://extensions.gnome.org/extension/2236/night-theme-switcher/&lt;/a>&lt;/li>
&lt;/ul>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F06%2Fauto-switch-between-light-and-dark-mode-on-gnome%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Auto+switch+between+light+and+dark+mode+on+GNOME" style="border:0" alt="" /></description></item><item><title>Publishing to Firefly-iii using Pandas</title><link>https://www.technowizardry.net/2024/06/publishing-to-firefly-iii-using-pandas/</link><pubDate>Sat, 08 Jun 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/06/publishing-to-firefly-iii-using-pandas/</guid><summary>&lt;p>Previously, in my &lt;a class="link" href="https://www.technowizardry.net/series/self-hosted-finances" >Self-hosted finances series&lt;/a>, I &lt;a class="link" href="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/" >cleaned&lt;/a> and &lt;a class="link" href="https://www.technowizardry.net/2024/05/solving-for-bank-transfers-using-pandas/" target="_blank" rel="noopener"
>identified transfers&lt;/a> in my Mint transactions for the purposes of of importing into &lt;a class="link" href="https://github.com/firefly-iii/firefly-iii" target="_blank" rel="noopener"
>Firefly-iii&lt;/a>. In this post, I&amp;rsquo;m going to import the transactions into Firefly-iii.&lt;/p></summary><description>&lt;p>Previously, in my &lt;a class="link" href="https://www.technowizardry.net/series/self-hosted-finances" >Self-hosted finances series&lt;/a>, I &lt;a class="link" href="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/" >cleaned&lt;/a> and &lt;a class="link" href="https://www.technowizardry.net/2024/05/solving-for-bank-transfers-using-pandas/" target="_blank" rel="noopener"
>identified transfers&lt;/a> in my Mint transactions for the purposes of of importing into &lt;a class="link" href="https://github.com/firefly-iii/firefly-iii" target="_blank" rel="noopener"
>Firefly-iii&lt;/a>. In this post, I&amp;rsquo;m going to import the transactions into Firefly-iii.&lt;/p>
&lt;p>This part is comparatively easy vs the previous steps, however it&amp;rsquo;s only a one time import. A continue updating workflow is tricky and I&amp;rsquo;m working on some logic to do that.&lt;/p>
&lt;h1 id="authenticating">Authenticating&lt;/h1>
&lt;p>First, we need a token that allows us to call the API. To get this, login to Firefly, and open Options, then Profile.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/06/publishing-to-firefly-iii-using-pandas/images/firefly-left-navbar.png"
width="598"
height="957"
srcset="https://www.technowizardry.net/2024/06/publishing-to-firefly-iii-using-pandas/images/firefly-left-navbar_hu_16caf3e12b94084d.png 480w, https://www.technowizardry.net/2024/06/publishing-to-firefly-iii-using-pandas/images/firefly-left-navbar_hu_bc42585a32c2ed4d.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="62"
data-flex-basis="149px"
>
Then go to OAuth and click Create a new token&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/06/publishing-to-firefly-iii-using-pandas/images/firefly-options.png"
width="1123"
height="777"
srcset="https://www.technowizardry.net/2024/06/publishing-to-firefly-iii-using-pandas/images/firefly-options_hu_99eb56dd8e4880c0.png 480w, https://www.technowizardry.net/2024/06/publishing-to-firefly-iii-using-pandas/images/firefly-options_hu_89de6b528288a0f.png 1024w"
loading="lazy"
alt="A screenshot of FIrefly showing the options page and the OAuth client page."
class="gallery-image"
data-flex-grow="144"
data-flex-basis="346px"
>
Firefly will then show you a token. Copy and store this in the script as a variable and also specify the endpoint where Firefly is being served:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">fireflyToken&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;eYJ...&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">host&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;https://firefly.example.com&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="constructing-the-request">Constructing the request&lt;/h1>
&lt;p>This next method will translate the DataFrame created in the &lt;a class="link" href="https://www.technowizardry.net/2024/05/solving-for-bank-transfers-using-pandas" >previous post&lt;/a> request into the format that the Firefly API expects and creates the transaction.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">json&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">requests&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">session&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">requests&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Session&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">send_to_firefly&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">record&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">item&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;type&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">record&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;type&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;amount&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">record&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;amount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">key&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;description&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;destination_name&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;source_name&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;category_name&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;notes&amp;#39;&lt;/span>&lt;span class="p">]:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">key&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">record&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isnull&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">record&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">]):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">record&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">key&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;source_id&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;destination_id&amp;#39;&lt;/span>&lt;span class="p">]:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">key&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">record&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isnull&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">record&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">record&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">int&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">record&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">]))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;foreign_amount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;0&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;reconciled&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">False&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;order&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;0&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># If you use something other than USD, change this value&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;currency_id&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;8&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;currency_code&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;USD&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">key&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;date&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;process_date&amp;#39;&lt;/span>&lt;span class="p">]:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">key&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">record&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isnull&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">record&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">record&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">record&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isoformat&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">isinstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">record&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;tags&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;tags&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">record&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;tags&amp;#39;&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;tags&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">payload&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;transactions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">item&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;apply_rules&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;fire_webhooks&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;error_if_duplicate_hash&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">False&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">payload&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">Exception&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">raise&lt;/span> &lt;span class="ne">Exception&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Can&amp;#39;t serialize &amp;#39;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">item&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">session&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">post&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">host&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/api/v1/transactions&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">headers&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;Authorization&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Bearer &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">fireflyToken&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Content-Type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;application/json&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Accept&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;application/json&amp;#39;&lt;/span>&lt;span class="p">},&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">payload&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">allow_redirects&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">status_code&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Series&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">status_code&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">()[&lt;/span>&lt;span class="s1">&amp;#39;data&amp;#39;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;id&amp;#39;&lt;/span>&lt;span class="p">]],&lt;/span> &lt;span class="n">index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;status&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;message&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;firefly_id&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Series&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">status_code&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">()],&lt;/span> &lt;span class="n">index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;status&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;message&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="sending-the-request">Sending the request&lt;/h1>
&lt;p>Next, it&amp;rsquo;s easy to send the requests to Firefly:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># First find the transfers and translate into common format&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">output&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">apply&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">func&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">process_record&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">axis&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">result_type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;expand&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Then send it to Firefly&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">fireflyResult&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">output&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">output&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;amount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">apply&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">func&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">send_to_firefly&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">axis&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">result_type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;expand&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="reviewing-results">Reviewing Results&lt;/h1>
&lt;p>Firefly can return non-200 status code responses. Running this will identify all the non-successful writes. In theory, this should empty. It&amp;rsquo;s a good idea to check the error messages and see what&amp;rsquo;s wrong.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fireflyResult&lt;/span>&lt;span class="p">[(&lt;/span>&lt;span class="n">fireflyResult&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;status&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">)])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>Once we&amp;rsquo;ve identified the transfers, it&amp;rsquo;s comparatively easy to write the one-time extract to Firefly. The code can be found in &lt;a class="link" href="https://github.com/ajacques/own-your-finances" target="_blank" rel="noopener"
>my GitHub repository&lt;/a>. I&amp;rsquo;ve got a few projects in progress right now, including tools to automatically scrape data from accounts and how to merge new transactions into Firefly.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F06%2Fpublishing-to-firefly-iii-using-pandas%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Publishing+to+Firefly-iii+using+Pandas" style="border:0" alt="" /></description></item><item><title>GoDaddy is now blocking API access</title><link>https://www.technowizardry.net/2024/06/godaddy-is-now-blocking-api-access/</link><pubDate>Tue, 04 Jun 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/06/godaddy-is-now-blocking-api-access/</guid><summary>&lt;p>I own few domains and one of those domains is registered at GoDaddy. This is for historical reasons because this domain is on the &lt;code>.es&lt;/code> TLD but my preferred registrar, PorkBun or CloudFlare, do not support this TLD. I kept it there mainly because I&amp;rsquo;ve had it for 10+ years and there were some new identify requirements that I didn&amp;rsquo;t want to deal with yet.&lt;/p>
&lt;p>I use &lt;a class="link" href="https://github.com/kubernetes-sigs/external-dns" target="_blank" rel="noopener"
>external-dns&lt;/a> as a tool to automatically to take my Kubernetes Ingress resources and register them in my DNS zone. This tool would use the GoDaddy API to create, edit, and delete the DNS records automatically for me and point them to my own servers. Sure, I could do this myself because I don&amp;rsquo;t have that many records, but automation is fun.&lt;/p></summary><description>&lt;p>I own few domains and one of those domains is registered at GoDaddy. This is for historical reasons because this domain is on the &lt;code>.es&lt;/code> TLD but my preferred registrar, PorkBun or CloudFlare, do not support this TLD. I kept it there mainly because I&amp;rsquo;ve had it for 10+ years and there were some new identify requirements that I didn&amp;rsquo;t want to deal with yet.&lt;/p>
&lt;p>I use &lt;a class="link" href="https://github.com/kubernetes-sigs/external-dns" target="_blank" rel="noopener"
>external-dns&lt;/a> as a tool to automatically to take my Kubernetes Ingress resources and register them in my DNS zone. This tool would use the GoDaddy API to create, edit, and delete the DNS records automatically for me and point them to my own servers. Sure, I could do this myself because I don&amp;rsquo;t have that many records, but automation is fun.&lt;/p>
&lt;p>Then today, I&amp;rsquo;m greeted with this error from external-dns and it just crashing and restarting:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">level=fatal msg=&amp;#34;Error ACCESS_DENIED: \&amp;#34;Authenticated user is not allowed access\&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;!-- more -->
&lt;p>Some web searching, led me to a few &lt;a class="link" href="https://old.reddit.com/r/godaddy/comments/1bl0f5r/am_i_the_only_one_who_cant_use_the_api/" target="_blank" rel="noopener"
>different posts&lt;/a> by others complaining and this response from GoDaddy Support:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Hi,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Thank you for reaching out to us regarding the recent changes to our Domain API.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">We wanted to inform you that we have recently updated our Domain API requirements. As part of this update, customers are now required to have 50 or more domains in their account to utilize the API. Unfortunately, as you currently only have 1 domain in your account, access to the API is blocked for you.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">However, we want to assure you that you still have access to the OTE API without any blocks.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">We apologize for any confusion or inconvenience this may have caused. If you have any further questions or need assistance with any other aspect of our services, please don&amp;#39;t hesitate to reach out.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Thank you for your understanding.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Regards,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">API Support Team
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I have less than 10 domains in my account, so they decided that they would block my API access like others. No warning at all was given to me over email, this wasn&amp;rsquo;t clearly indicated in the developer console either. Corporate greed. Now I have to figure out if I want to transfer it or just manage the entire DNS zone myself, which I already do for &lt;code>technowizardry.net&lt;/code>.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F06%2Fgodaddy-is-now-blocking-api-access%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=GoDaddy+is+now+blocking+API+access" style="border:0" alt="" /></description></item><item><title>The confusing world of scraping my own stock portfolio</title><link>https://www.technowizardry.net/2024/05/confusing-world-of-scraping-my-stock-portfolio/</link><pubDate>Wed, 22 May 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/05/confusing-world-of-scraping-my-stock-portfolio/</guid><summary>&lt;p>Over the past few months, as part of my &lt;a class="link" href="https://www.technowizardry.net/series/self-hosted-finances" >self hosted finances series&lt;/a> I&amp;rsquo;ve been working to extract all of my stock portfolio into some kind of self hosted database. I came across
&lt;a class="link" href="https://github.com/ghostfolio/ghostfolio" target="_blank" rel="noopener"
>Ghostfolio&lt;/a>, which is an open-source (with a paid hosted edition) tool for tracking stock portfolios. It was able to give me a portfolio view across multiple brokerages, automatically fetched stock prices, and gave some basic allocation reporting.&lt;/p></summary><description>&lt;p>Over the past few months, as part of my &lt;a class="link" href="https://www.technowizardry.net/series/self-hosted-finances" >self hosted finances series&lt;/a> I&amp;rsquo;ve been working to extract all of my stock portfolio into some kind of self hosted database. I came across
&lt;a class="link" href="https://github.com/ghostfolio/ghostfolio" target="_blank" rel="noopener"
>Ghostfolio&lt;/a>, which is an open-source (with a paid hosted edition) tool for tracking stock portfolios. It was able to give me a portfolio view across multiple brokerages, automatically fetched stock prices, and gave some basic allocation reporting.&lt;/p>
&lt;h1 id="the-problem">The problem&lt;/h1>
&lt;p>Ghostfolio doesn&amp;rsquo;t have any mechanism of actually knowing about your transactions and I don&amp;rsquo;t want to manually do it because that takes time and can have mistakes. Brokerages don&amp;rsquo;t expose this. Options like Plaid cost money and require extensive compliance requirements and don&amp;rsquo;t even provide all data that I need.&lt;/p>
&lt;p>While some of banks provided CSV exports, that was rarely sufficient. That spawned a several month long project to see if I could build my own scraping to import it into Ghostfolio and centralize all my financial data into one place so I could perform analytics on it.&lt;/p>
&lt;!-- more -->
&lt;p>This led me on a multi month long project ot build tooling to synchronize my stock positions into Ghostfolio. I ended up building several web browser automation tools to just scrape what I need, however this quickly became a giant mess requiring a lot of work to figure out. I thought that working with bank and card transactions &lt;a class="link" href="https://www.technowizardry.net/post/2024/solving-for-bank-transfers-using-pandas" >was hard&lt;/a>, but stocks introduce an entirely new set of challenges.&lt;/p>
&lt;p>In this post, I&amp;rsquo;m not even going to talk about how I implemented my scraping system, just the horrors I faced.&lt;/p>
&lt;h1 id="terminology-and-concepts">Terminology and Concepts&lt;/h1>
&lt;p>Before we can talk about complexity, let&amp;rsquo;s break down the concepts.&lt;/p>
&lt;ul>
&lt;li>Portfolio - The full set of current positions&lt;/li>
&lt;li>Current Position - The current # of shares of a given stock. This is the minimum information we need&lt;/li>
&lt;li>Cost Basis - The purchase price for all shares (now we have the profit)&lt;/li>
&lt;li>Stock Lot - A lot is a block of shares purchased on a given date and time with a given purchase price. Combined with cost basis, this gives us some useful insights for tax planning&lt;/li>
&lt;li>Realized gains/losses - A purchase and sale of a given stock lot&lt;/li>
&lt;li>Historical Activity - The history of buy, sell, and dividends. With this we now know how our returns over a given period of time&lt;/li>
&lt;/ul>
&lt;p>With just the positions, you can only know your account value. Adding cost basis means we now know the profit. Knowing the lots means you can now make tax planning decisions (for example is it better sell FIFO or the lot with the highest basis). Once you know the historical activity, you can now know the value of your account over time. Realized gains/losses gives you the historical profit.&lt;/p>
&lt;h1 id="what-does-ghostfolio-need">What does Ghostfolio need?&lt;/h1>
&lt;p>Ghostfolio processes in terms of the buy, sell, dividend activity. i.e. I bought x shares on y date for z monies. Thus, the historical activity should be the best way to import data.&lt;/p>
&lt;h1 id="limited-activity-data">Limited activity data&lt;/h1>
&lt;p>Sounds easy, right? Whelp, unfortunately not every brokerage gives this information in this manner. &lt;a class="link" href="https://www.fidelity.com/" target="_blank" rel="noopener"
>Fidelity&lt;/a> provides a CSV export of the activity, but only goes back 5 years. What happens if you have shares that are older than that? Using just the activity log, you&amp;rsquo;ll end up short in your portfolio.&lt;/p>
&lt;p>An incorrect short-position in Ghostfolio caused by the missing BUY side of the transaction because it&amp;rsquo;s &amp;gt;5 years old:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/05/confusing-world-of-scraping-my-stock-portfolio/images/ghostfolio-negative-position.png"
width="763"
height="130"
srcset="https://www.technowizardry.net/2024/05/confusing-world-of-scraping-my-stock-portfolio/images/ghostfolio-negative-position_hu_a35b962d792a7d7d.png 480w, https://www.technowizardry.net/2024/05/confusing-world-of-scraping-my-stock-portfolio/images/ghostfolio-negative-position_hu_dfb992031b44f68c.png 1024w"
loading="lazy"
alt="A screenshot of Ghostfolio showing a negative quantity of shares of a stock caused by this bug."
class="gallery-image"
data-flex-grow="586"
data-flex-basis="1408px"
>&lt;/p>
&lt;p>&lt;a class="link" href="https://www.betterment.com/" target="_blank" rel="noopener"
>Betterment&lt;/a> provides a dividends in CSV, lot level cost basis in CSV, and activity as a PDF. This is even more difficult because now you can&amp;rsquo;t identify any stock sales meaning that your portfolio incorrectly inflates upwards. In addition, it&amp;rsquo;ll incorrectly calculate historical account balances and assume you invested more money in later than you actually did. I request them to provide some useful every few years and they regularly say they don&amp;rsquo;t support it. Even though they clearly offer it through third-party aggregators like Plaid.&lt;/p>
&lt;p>So here we have two different brokerages that provide completely different data offerings and none of them are actually usable.&lt;/p>
&lt;h1 id="inferring-it-from-other-sources">Inferring it from other sources&lt;/h1>
&lt;p>Instead of looking just at one thing, the activity log, can we try to combine all the information the broker does provide us to infer the activity going back as far as possible?&lt;/p>
&lt;p>For example, Fidelity has lot level realized gains going back 9 years, the current positions, open stock lots, in addition to the activity log. The current positions has a CSV download, but it only gives me a cost basis and current value, but I can&amp;rsquo;t figure out a BUY because I don&amp;rsquo;t know when that was purchased.&lt;/p>
&lt;h2 id="by-open-stock-lots">By open stock lots&lt;/h2>
&lt;p>The unrealized stock lots are available in the UI, but are server-side rendered as HTML (no friendly JSON or CSV) so I have to write a custom parser. Here&amp;rsquo;s the data it provides:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Acquired&lt;/th>
&lt;th>Term&lt;/th>
&lt;th>$ Total Gain/Loss&lt;/th>
&lt;th>% Total Gain/Loss&lt;/th>
&lt;th>Current Value&lt;/th>
&lt;th>Quantity&lt;/th>
&lt;th>Average Cost Basis&lt;/th>
&lt;th>Cost Basis Total&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>May-22-2015&lt;/td>
&lt;td>Long&lt;/td>
&lt;td>$100.00&lt;/td>
&lt;td>+100%&lt;/td>
&lt;td>$150&lt;/td>
&lt;td>5&lt;/td>
&lt;td>$10&lt;/td>
&lt;td>$50&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Jan-1-2024&lt;/td>
&lt;td>Short&lt;/td>
&lt;td>$5&lt;/td>
&lt;td>+5%&lt;/td>
&lt;td>$10&lt;/td>
&lt;td>1&lt;/td>
&lt;td>$5&lt;/td>
&lt;td>$5&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Is that enough to tell Ghostfolio there was a BUY on May 22, 2015? Sort of, but there&amp;rsquo;s a foot-gun. It&amp;rsquo;s entirely possible to buy 10 shares of a stock and then sell 5 from that lot, so you&amp;rsquo;ve split the original purchase. If I have activity going back that includes the 2024 share purchase above, I have to recognize this and not create duplicate purchase records in Ghostfolio. I accidentally made this mistake before and then inflated my Ghostfolio.&lt;/p>
&lt;h2 id="by-pdf-statement">By PDF statement&lt;/h2>
&lt;p>Fidelity offers PDF statements going back 10 years and Betterment provides activity events as PDF too. However, PDF is a terrible format to parse. There&amp;rsquo;s no DOM structuring like HTML. I adopted to use a Python package called &lt;a class="link" href="https://github.com/jcushman/pdfquery" target="_blank" rel="noopener"
>PDFQuery&lt;/a> to parse the PDFs and found out that you basically have to use bounding boxes to select text based on where it appears on the page. For example, in one brokerage, the doc looks like:&lt;/p>
&lt;ul>
&lt;li>Investment Details&lt;/li>
&lt;li>A table detailing the trades&lt;/li>
&lt;li>A bunch of legal disclaimers and notices&lt;/li>
&lt;/ul>
&lt;p>And to extract the table, I have to first find the text immediately before and after, then find those (x, y) coordinates, then find all the text in between those two lines and extract the table. Let&amp;rsquo;s hope they never change their disclaimer.&lt;/p>
&lt;p>Even worse, institutions can and have regularly changed their page layouts over the years, so now you have to implement parsers depending on the years.&lt;/p>
&lt;h2 id="by-realized-gains">By realized gains&lt;/h2>
&lt;p>In Fidelity, I tried current positions and I tried the activity log and still got only partial data. What about the realized gains (i.e. closed positions on the UI)?&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/05/confusing-world-of-scraping-my-stock-portfolio/images/fidelity-closed-positions.png"
width="213"
height="217"
srcset="https://www.technowizardry.net/2024/05/confusing-world-of-scraping-my-stock-portfolio/images/fidelity-closed-positions_hu_3747a6f713c77ab2.png 480w, https://www.technowizardry.net/2024/05/confusing-world-of-scraping-my-stock-portfolio/images/fidelity-closed-positions_hu_9fb7a60e8cf60574.png 1024w"
loading="lazy"
alt="A screenshot of the Fidelity page showing the link saying Closed Positions"
class="gallery-image"
data-flex-grow="98"
data-flex-basis="235px"
>&lt;/p>
&lt;p>With this, we can see buy and sell transactions between the 5 and 10 year time range, but of course it has the same problem as the open tax lots, in that you have to ignore any transaction that you do have activity for to avoid a duplicate buy/sell. But of course this is only available as an entirely custom HTML table which requires browser automation to open the tabs and grab the table contents.&lt;/p>
&lt;h1 id="rounding-bugs">Rounding Bugs&lt;/h1>
&lt;p>Brokerage exports can give you information per share or total for the entire transaction. Ghostfolio takes the # of shares and the per share price, then multiplies number x share price = total price. If a data source gives me the transaction cost basis or value, then I have to do value / # of shares to get the per share price. That introduces error, especially if I try to match transactions.&lt;/p>
&lt;p>For example, Betterment&amp;rsquo;s cost basis CSV export has:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Shares&lt;/th>
&lt;th>Cost Basis&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0.130046&lt;/td>
&lt;td>13.81&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0.131024&lt;/td>
&lt;td>13.91&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>And the equivalent statements show:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Statement&lt;/th>
&lt;th>Price&lt;/th>
&lt;th>Shares&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>#1&lt;/td>
&lt;td>$106.27&lt;/td>
&lt;td>0.130&lt;/td>
&lt;td>$13.82&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>#2&lt;/td>
&lt;td>$106.24&lt;/td>
&lt;td>0.131&lt;/td>
&lt;td>$13.92&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>We can clearly see that $13.81 is not equal to $13.82 or $13.92, so there&amp;rsquo;s clearly a difference in rounding in the CSV and the PDF statement. In addition, $13.81 / 0.130046 = 106.19319&amp;hellip; which is not equal to $106.27 or $106.24.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>In theory, if I combine all these different signals, I should be able reverse engineer most of the activity log at least for Fidelity, but good luck doing that across every institution and these institutions aren&amp;rsquo;t very interested in making this any easier. I email Betterment about some improvements that would make tax filing and extracting easier and each time they say not supported. Maybe I&amp;rsquo;ll rethink paying their management fee.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F05%2Fconfusing-world-of-scraping-my-stock-portfolio%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=The+confusing+world+of+scraping+my+own+stock+portfolio" style="border:0" alt="" /></description></item><item><title>Solving for bank transfers using Pandas</title><link>https://www.technowizardry.net/2024/05/solving-for-bank-transfers-using-pandas/</link><pubDate>Wed, 01 May 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/05/solving-for-bank-transfers-using-pandas/</guid><summary>&lt;p>Previously in &lt;a class="link" href="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/" >Part 1&lt;/a>, I talked about how to clean-up the transaction data from Mint to remove duplicates and add any missing transactions.&lt;/p>
&lt;h2 id="solving-for-transfers">Solving for transfers&lt;/h2>
&lt;p>The next phase is to solve for the transfer pairs. A transfer pair is defined with a matching credit and debit transaction on two different accounts. In Firefly, a transfer is treated separately than a credit/debit because it&amp;rsquo;s excluded from the expense and income reports.&lt;/p></summary><description>&lt;p>Previously in &lt;a class="link" href="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/" >Part 1&lt;/a>, I talked about how to clean-up the transaction data from Mint to remove duplicates and add any missing transactions.&lt;/p>
&lt;h2 id="solving-for-transfers">Solving for transfers&lt;/h2>
&lt;p>The next phase is to solve for the transfer pairs. A transfer pair is defined with a matching credit and debit transaction on two different accounts. In Firefly, a transfer is treated separately than a credit/debit because it&amp;rsquo;s excluded from the expense and income reports.&lt;/p>
&lt;p>This ended up being trickier than I expected and I went through multiple phases. First attempt, I used Pandas &lt;a class="link" href="https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.merge_asof.html" target="_blank" rel="noopener"
>merge_asof&lt;/a>, but this did not handle multiple transactions around the same time. Then I developed a new method that ignored previously used transactions. This worked better, but it still struggled with complex transaction chains. For example, if I transferred $1k from account A to B, then from C to D, it might accidentally book a transfer from A to D and C to B which is incorrect.&lt;/p>
&lt;!-- more -->
&lt;h2 id="the-basic-algorithm">The basic algorithm&lt;/h2>
&lt;p>Here we have the first iteration. How it works is for every single transaction, it looks forward 5 days, finds all transactions that are the opposite type (credit -&amp;gt; debit or vice versa) and the same amount, then tags the first one as the opposite. For example, in the below table, it&amp;rsquo;d identify the two $300 transactions as a pair.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Date&lt;/th>
&lt;th>Description&lt;/th>
&lt;th>Amount&lt;/th>
&lt;th>Type&lt;/th>
&lt;th>Account&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1/1/2024&lt;/td>
&lt;td>Transfer to XYZ&lt;/td>
&lt;td>$300.00&lt;/td>
&lt;td>debit&lt;/td>
&lt;td>ABC&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1/1/2024&lt;/td>
&lt;td>Bought Avocado Toast&lt;/td>
&lt;td>$4.99&lt;/td>
&lt;td>debit&lt;/td>
&lt;td>Credit Card&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1/2/2024&lt;/td>
&lt;td>Transfer from ABC&lt;/td>
&lt;td>$300.00&lt;/td>
&lt;td>credit&lt;/td>
&lt;td>XYZ&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The code looked like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;span class="lnt">68
&lt;/span>&lt;span class="lnt">69
&lt;/span>&lt;span class="lnt">70
&lt;/span>&lt;span class="lnt">71
&lt;/span>&lt;span class="lnt">72
&lt;/span>&lt;span class="lnt">73
&lt;/span>&lt;span class="lnt">74
&lt;/span>&lt;span class="lnt">75
&lt;/span>&lt;span class="lnt">76
&lt;/span>&lt;span class="lnt">77
&lt;/span>&lt;span class="lnt">78
&lt;/span>&lt;span class="lnt">79
&lt;/span>&lt;span class="lnt">80
&lt;/span>&lt;span class="lnt">81
&lt;/span>&lt;span class="lnt">82
&lt;/span>&lt;span class="lnt">83
&lt;/span>&lt;span class="lnt">84
&lt;/span>&lt;span class="lnt">85
&lt;/span>&lt;span class="lnt">86
&lt;/span>&lt;span class="lnt">87
&lt;/span>&lt;span class="lnt">88
&lt;/span>&lt;span class="lnt">89
&lt;/span>&lt;span class="lnt">90
&lt;/span>&lt;span class="lnt">91
&lt;/span>&lt;span class="lnt">92
&lt;/span>&lt;span class="lnt">93
&lt;/span>&lt;span class="lnt">94
&lt;/span>&lt;span class="lnt">95
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">find_transfer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">time_window_days&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">start_date&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">end_date&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">start_date&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">timedelta&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">days&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">time_window_days&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">opposing_filter&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="n">start_date&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="n">end_date&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Transaction Type&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Transaction Type&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="o">~&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Considered&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">transfer_pair&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">opposing_filter&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">transfer_pair&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">empty&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">df&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">at&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Considered&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">True&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">opposite&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transfer_pair&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">iloc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">opposite&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">process_record&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">at&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Considered&amp;#39;&lt;/span>&lt;span class="p">]:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pair&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">find_transfer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">pair&lt;/span> &lt;span class="ow">is&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">df&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">at&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">pair&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Considered&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">True&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">notes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Notes&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pair&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Notes&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">labels&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">lambda&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">isinstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Labels&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pair&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Labels&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Transaction Type&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;debit&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Bank is on the left&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">date&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">process_date&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pair&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">notes&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pair&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Original Description&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;type&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;transfer&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;date&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;process_date&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">pair&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;amount&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;category&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">pair&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Category&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;description&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Original Description&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;source_id&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;destination_id&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">pair&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;tags&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">labels&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;format&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;transfer_debit&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;notes&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">lambda&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">isinstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">notes&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">date&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pair&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">process_date&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">notes&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Original Description&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;type&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;transfer&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;date&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">date&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;process_date&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">process_date&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;amount&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;category&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">pair&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Category&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;description&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">pair&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Original Description&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;source_id&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">pair&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;destination_id&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;tags&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">labels&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;format&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;transfer_credit&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;notes&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">lambda&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">isinstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">notes&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;date&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;description&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Original Description&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;amount&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;category&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Category&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;tags&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Labels&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;notes&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Notes&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Transaction Type&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;credit&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;type&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;deposit&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;destination_id&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;source_name&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Description&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;source_id&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;destination_name&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Description&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;type&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;withdrawal&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">result&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Considered&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">False&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">transactions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">apply&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">func&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">process_record&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">axis&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">result_type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;expand&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>However, it made a lot of mistakes. For example, it would classify the following as a transfer from Venmo to the Credit Card. Which maybe one could say it&amp;rsquo;s a &amp;ldquo;transfer&amp;rdquo;, but I didn&amp;rsquo;t want that because the credit card would get paid by my checking account.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Date&lt;/th>
&lt;th>Description&lt;/th>
&lt;th>Amount&lt;/th>
&lt;th>Type&lt;/th>
&lt;th>Account&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1/1/2024&lt;/td>
&lt;td>Bought Dinner for my friends&lt;/td>
&lt;td>$50&lt;/td>
&lt;td>debit&lt;/td>
&lt;td>Credit Card&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1/1/2024&lt;/td>
&lt;td>Friend paid me back&lt;/td>
&lt;td>$50&lt;/td>
&lt;td>credit&lt;/td>
&lt;td>Venmo&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Eventually, I extended that algorithm with rules based on how my accounts behaved. Let&amp;rsquo;s look at some of those options.&lt;/p>
&lt;h2 id="not-all-transactions-are-transfers">Not all transactions are transfers&lt;/h2>
&lt;p>Not all transactions are transfers, but sometimes it lines up that it looks like it&amp;rsquo;s a transfer because there&amp;rsquo;s a matching side. Just like the previous example. We can define some rules that match transactions that we know are not transfers.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">account&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">num&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Union&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">int&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">List&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">int&lt;/span>&lt;span class="p">]]):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">type&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">num&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="nb">int&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">num&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isin&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">num&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">orig_descr_contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">msg&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Original Description&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">str&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">msg&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">case&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># List of all Firefly account ids that are credit cards&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">credit_cards&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">15&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">brokerages&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">account_id_401k&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">25&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">not_a_transfer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Maybe exclude an entire account. For example, your 401k may never have&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># a transfer (unless you take a loan or do a Mega Backdoor Roth)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">account&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">account_id_401k&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Or you want to exclude reimbursements via Venmo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">account&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">23&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="n">orig_descr_contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;John Doe Paid &amp;#39;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="c1"># Venmo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">account&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">23&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="o">~&lt;/span>&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Category&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isna&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Category&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s1">&amp;#39;Transfer&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="c1"># Venmos frequently appear like transfers because a friend reimburses me&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Credit Card Payments (to the card) can be transfers, but&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># maybe they&amp;#39;re never transfers going out (unless you do a Balance Transfer)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">account&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">credit_cards&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Transaction Type&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;debit&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># A paycheck&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># While it is a transfer from your employer to you, without the&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># debit side, the algorithm could incorrectly classify it as a transfer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">orig_descr_contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;PAYROLL&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Category&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;Paycheck&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Interest paid is not a transfer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">orig_descr_contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;INTEREST PAYMENT&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Category&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;Interest Income&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Dividend reinvestment are not transfers&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">account&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">brokerages&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">orig_descr_contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;DIVDEND&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="n">orig_descr_contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;REINVESTMENT&amp;#39;&lt;/span>&lt;span class="p">)),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Some accounts have $0 transactions as part of bookkeeping&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># They are not transfers&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then we can integrate it into the algorithm. While searching for new transactions, it excludes anything that is not considered relevant.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Compute the exclusion set&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">df_filter_relevant&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">~&lt;/span>&lt;span class="n">reduce&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">lambda&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">not_a_transfer&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">find_transfer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">time_window_days&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">opposing_filter&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="n">start_date&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="n">end_date&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Transaction Type&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Transaction Type&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">~&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Considered&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="n">df_filter_relevant&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">process_record&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">at&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Considered&amp;#39;&lt;/span>&lt;span class="p">]:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">df_relevant&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">]:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">transfer_pair&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">find_transfer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">output&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">apply&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">func&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">process_record&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">axis&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">result_type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;expand&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Figuring out the rules is tricky. What I do is to actually look at both sides of the transfers in the notes:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">output&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">output&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;type&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;transfer&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And see what makes sense and what doesn&amp;rsquo;t. If you can&amp;rsquo;t figure out how to define a rule that ignores that transaction, then go back to your CSV file, add a new column called &lt;code>IsTransfer&lt;/code> and put true or false into rows that the algorithm struggles with (you don&amp;rsquo;t have to annotate every row.) Then reload the file and add a rule:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">not_a_transfer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">~&lt;/span>&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;IsTransfer&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="heuristic-account-id-in-description">Heuristic: Account Id in Description&lt;/h2>
&lt;p>The next problem is that if I make transfers with the same amount during the same time period, it can get confused and depending on the ordering in the transactions CSV, mis-attribute the transfer between the wrong accounts.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/05/solving-for-bank-transfers-using-pandas/images/transaction-flow-wrong-account.svg"
loading="lazy"
>&lt;/p>
&lt;p>This led to a new idea to use the description as a signal. For example, in the transaction description:
&lt;code>ONLINE TRANSFER REF #XYZ1234 TO SAVINGS XXXXXX1234 ON 11/2/11&lt;/code> we can see that I transferred to account &lt;code>X1234&lt;/code>. I know that this account corresponds to Firefly account XYZ and can seed the algorithm to search for credits in that account.&lt;/p>
&lt;p>Not all transfers include the exact account id though, especially ones that cross banks will usually show something vague. Like a transfer from one bank to another bank might just show &lt;code>FOOBANK ONLINE TRANSFER&lt;/code> and that bank has multiple candidate accounts.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">known_account_ids&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;X1234&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;X4561&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;FOO BANK $TRANSFER&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">...&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This says if we find a transaction that says &lt;code>X1234&lt;/code>, then we should look for matching transactions in account id &lt;code>1&lt;/code>. It also says that if we have transaction in account &lt;code>1&lt;/code>, we should &lt;em>try&lt;/em> to find transactions that contain either &lt;code>X1234&lt;/code> or &lt;code>FOO BANK $TRANSFER&lt;/code>, and if neither can be find, expand the scope to any matching transactions. So in the following example we have two transfers that all have the same amount, but must be disambiguated.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Description&lt;/th>
&lt;th>Amount&lt;/th>
&lt;th>Type&lt;/th>
&lt;th>Account&lt;/th>
&lt;th>Pair&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>Transfer to X1234&lt;/td>
&lt;td>$100&lt;/td>
&lt;td>debit&lt;/td>
&lt;td>4561&lt;/td>
&lt;td>4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>Transfer&lt;/td>
&lt;td>$100&lt;/td>
&lt;td>credit&lt;/td>
&lt;td>3&lt;/td>
&lt;td>3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>Transfer to FOO BANK $TRANSFER&lt;/td>
&lt;td>$100&lt;/td>
&lt;td>debit&lt;/td>
&lt;td>5678&lt;/td>
&lt;td>2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>Deposit&lt;/td>
&lt;td>$100&lt;/td>
&lt;td>credit&lt;/td>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>In transaction #1, we&amp;rsquo;d find X1234 and look for transactions in account &lt;code>1&lt;/code>, thus finding #4 as the pair. In transaction #2, we have no account ids, but we if we look for transactions that contain either &lt;code>FOO BANK $TRANSFER&lt;/code> or &lt;code>X4561&lt;/code>, then we find transaction #3.&lt;/p>
&lt;p>Wiring this into the algorithm is hairy, but here goes nothing. First, we identify any matching account ids defined above, along with the inverse.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">attempt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">base&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">attempt&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">base&lt;/span> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="n">attempt&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">base&lt;/span> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="n">attempt&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">base&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">find_transfer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">time_window_days&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">match_by_description&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">set&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">inverted_by_account_id_in_description&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">value&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">known_account_ids&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">items&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># If we find an account id in this record&amp;#39;s description&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># then refine the other side by the found account id(s)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">key&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Original Description&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">upper&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">isinstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nb">id&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">value&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">match_by_description&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">id&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">match_by_description&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Find possible descriptions based on this row&amp;#39;s&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># account id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">isinstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">value&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">value&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">]:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">foo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">inverted_by_account_id_in_description&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Original Description&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">str&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">case&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then we use this information to attempt different variations of the pair finder algorithm and if any of them return a result, we use that transactions as our pair.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">match_by_description&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">opposing_filter&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">attempt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">opposing_filter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isin&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">match_by_description&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">inverted_by_account_id_in_description&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">filter_by_invert&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">reduce&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">lambda&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">inverted_by_account_id_in_description&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">        &lt;span class="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">not_inverted&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">            &lt;span class="n">filter_by_invert&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">filter_by_invert&lt;/span> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="n">reduce&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">lambda&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">&amp;amp;&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">not_inverted&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">        &lt;span class="n">opposing_filter&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">attempt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">opposing_filter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">filter_by_invert&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">transfer_pair&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">opposing_filter&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">transfer_pair&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">empty&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="ambiguous-matches">Ambiguous Matches&lt;/h2>
&lt;p>Even with all that, we can still end up with incorrect transfer matches. For example, I frequently transferred $1k blocks between different accounts and if that happened around the same time, it could still misclassify the transfer. The next step is to identify ambiguous matches&amp;ndash; i.e. any time the solver finds two or more candidates and just picks one to proceed. These are likely candidates to investigate further. Let&amp;rsquo;s filter for those:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">find_transfer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">time_window_days&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">transfer_pair&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">opposing_filter&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">transfer_pair&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">empty&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">df&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">at&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Considered&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">True&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">opposite&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">find_ambiguous&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">at&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Considered&amp;#39;&lt;/span>&lt;span class="p">]:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">transfer_pair&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">find_transfer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">transfer_pair&lt;/span> &lt;span class="ow">is&lt;/span> &lt;span class="kc">None&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">transfer_pair&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;- &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Description&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">index&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">option&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">transfer_pair&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">iterrows&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">delta&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">option&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34; &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">delta&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">days&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> day(s): &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">option&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">option&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Original Description&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">copy&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Considered&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">False&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">copy&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">df_relevant&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">apply&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">func&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">find_ambiguous&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">axis&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">copy&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This gives me something like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">- 2012-03-01 Savings Account A: &amp;#34;DepositInternet transfer from Interest Checking account XXXXXXX6789&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 0 day(s) Spending Account: &amp;#34;WithdrawalInternet transfer to Online Savings account XXXXXXX1234&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 0 day(s) Ally Spending Account: &amp;#34;WithdrawalInternet transfer to Online Savings account XXXXXXX5678&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>From here, we can see that I need to create an alias for &lt;code>6789&lt;/code>, &lt;code>X1234&lt;/code>, &lt;code>X5678&lt;/code> in &lt;code>known_account_ids&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">account_info_map&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;X6789&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># Firefly&amp;#39;s account id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;X1234&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;X5678&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">7&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then rerun the transfer solver and it&amp;rsquo;ll fix those issues&lt;/p>
&lt;h2 id="testing">Testing&lt;/h2>
&lt;p>How do you know if the transfers are working correctly? There&amp;rsquo;s no automatic solution for this. I built my rules based on trial and error. I looked through the data, found issues, added rules, and re-ran.&lt;/p>
&lt;p>With this code, you can view the solved transfers:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">transfers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">df_filter_relevant&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">apply&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">func&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">process_record&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">axis&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">result_type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;expand&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">transfers&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">transfers&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;type&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;transfer&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>You can also write it to a CSV and review it in a spreadsheet editor:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">transfers&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">transfers&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;type&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;transfer&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">to_csv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;transfers.csv&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Once you&amp;rsquo;re satisfied, you can move on to the next step, which is loading into Firefly-iii in the next part of this series.&lt;/p>
&lt;h2 id="conclusion--next-steps">Conclusion &amp;amp; Next Steps&lt;/h2>
&lt;p>In the post, I walked through the Python code to identify and solve for transfers. The final code for this can be found on the GitHub repo &lt;a class="link" href="https://github.com/ajacques/own-your-finances" target="_blank" rel="noopener"
>here&lt;/a>.&lt;/p>
&lt;p>In the next post, I&amp;rsquo;ll show how to load all this into Firefly-iii.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F05%2Fsolving-for-bank-transfers-using-pandas%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Solving+for+bank+transfers+using+Pandas" style="border:0" alt="" /></description></item><item><title>My new Framework laptop</title><link>https://www.technowizardry.net/2024/04/my-new-framework-laptop/</link><pubDate>Wed, 17 Apr 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/04/my-new-framework-laptop/</guid><summary>&lt;p>I was recently in the market for a new personal-use laptop and wanted to try out a &lt;a class="link" href="https://frame.work/" target="_blank" rel="noopener"
>Framework Laptop&lt;/a>. I was intrigued by the idea of being able to replace any part that failed or even upgrade parts as I went. I also was frustrated with the direction that Windows 10 and Windows 11 was going.&lt;/p>
&lt;p>They seemed more interested in advertising, tracking, sending notifications to increase my engagement of their apps, then just building an operating system that got out of my way and let me do my thing.&lt;/p></summary><description>&lt;p>I was recently in the market for a new personal-use laptop and wanted to try out a &lt;a class="link" href="https://frame.work/" target="_blank" rel="noopener"
>Framework Laptop&lt;/a>. I was intrigued by the idea of being able to replace any part that failed or even upgrade parts as I went. I also was frustrated with the direction that Windows 10 and Windows 11 was going.&lt;/p>
&lt;p>They seemed more interested in advertising, tracking, sending notifications to increase my engagement of their apps, then just building an operating system that got out of my way and let me do my thing.&lt;/p>
&lt;p>Here&amp;rsquo;s my brief thoughts.&lt;/p>
&lt;!-- more -->
&lt;p>I ordered the &lt;a class="link" href="https://frame.work/products/laptop-13-gen-amd" target="_blank" rel="noopener"
>Framework 13&amp;quot; Laptop w/ AMD&lt;/a> and installed Ubuntu Linux on it because it was considered &lt;a class="link" href="https://frame.work/linux" target="_blank" rel="noopener"
>well supported&lt;/a> by Framework. The build quality felt very solid and I appreciated the ability to pick and choose what ports I had. Most laptops are switching to USB C, but I still had a number of USB A devices like my &lt;a class="link" href="https://www.yubico.com/" target="_blank" rel="noopener"
>Yubikey&lt;/a> or even slapping an HDMI port or Ethernet port in the cases that I need to do some network troubleshooting.&lt;/p>
&lt;p>I encountered a few issues, but here&amp;rsquo;s how to fix them.&lt;/p>
&lt;h2 id="issue-1---flickering-screen">Issue 1 - Flickering screen&lt;/h2>
&lt;p>The first issue I saw was that my screen would randomly flicker white. Some &lt;a class="link" href="https://community.frame.work/t/responded-screen-flashing-partially-white-on-framework-13-amd-ubuntu-22-04/40626/2" target="_blank" rel="noopener"
>others&lt;/a> that it was a bug in the &lt;a class="link" href="https://gitlab.freedesktop.org/drm/amd/-/issues/2735" target="_blank" rel="noopener"
>AMD drivers&lt;/a>.&lt;/p>
&lt;p>The fix was quick. All I had to do was add the the Kernel arg &lt;code>amdgpu.sg_display=0&lt;/code> to the boot config:&lt;/p>
&lt;p>/etc/default/grub&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl"> GRUB_DEFAULT=0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> GRUB_TIMEOUT_STYLE=hidden
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> GRUB_TIMEOUT=0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> GRUB_DISTRIBUTOR=`lsb_release -i -s 2&amp;gt; /dev/null || echo Debian`
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">-GRUB_CMDLINE_LINUX_DEFAULT=&amp;#34;quiet splash&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+GRUB_CMDLINE_LINUX_DEFAULT=&amp;#34;quiet splash amdgpu.sg_display=0&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span> GRUB_CMDLINE_LINUX=&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then reboot.&lt;/p>
&lt;h2 id="issue-2---amd-wi-fi-card">Issue 2 - AMD Wi-Fi Card&lt;/h2>
&lt;p>My next issue frequently when I&amp;rsquo;d come out of standby, the Wi-Fi card would be stuck. I wouldn&amp;rsquo;t be able to get it unstuck without a reboot which got really annoying to reboot five times a day. Running &lt;code>sudo iwlist scan&lt;/code> would give me &lt;code>No scan results&lt;/code>. A number of articles talked about strategies, install this driver, or change that firmware. Instead, I ordered the &lt;a class="link" href="https://frame.work/products/intel-wi-fi-6e-ax210?v=FRANBWNT04" target="_blank" rel="noopener"
>Intel Wi-Fi 6E AX210&lt;/a> card and replaced it. It took me less than five minutes to replace it and it all worked better.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F04%2Fmy-new-framework-laptop%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=My+new+Framework+laptop" style="border:0" alt="" /></description></item><item><title>Importing and cleaning Mint transactions</title><link>https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/</link><pubDate>Sun, 07 Apr 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/</guid><summary>&lt;p>Since Intuit announced that Mint was going away, I&amp;rsquo;ve spent several months investigating how to import my Mint data into &lt;a class="link" href="https://github.com/firefly-iii/firefly-iii" target="_blank" rel="noopener"
>Firefly-iii&lt;/a>, an open source, self-hosted budgeting software. It seemed like a perfect fit. I would fully own the data and get to build whatever tooling I want on top.&lt;/p>
&lt;p>However, before we can get there, we need to have cleaned and accurate data from Mint. As it turns out, Mint&amp;rsquo;s data actually had some errors in it that required me to go back years and fix them. In this post, I walk through the programmatic approach I took to identifying mistakes and fixing them.&lt;/p></summary><description>&lt;p>Since Intuit announced that Mint was going away, I&amp;rsquo;ve spent several months investigating how to import my Mint data into &lt;a class="link" href="https://github.com/firefly-iii/firefly-iii" target="_blank" rel="noopener"
>Firefly-iii&lt;/a>, an open source, self-hosted budgeting software. It seemed like a perfect fit. I would fully own the data and get to build whatever tooling I want on top.&lt;/p>
&lt;p>However, before we can get there, we need to have cleaned and accurate data from Mint. As it turns out, Mint&amp;rsquo;s data actually had some errors in it that required me to go back years and fix them. In this post, I walk through the programmatic approach I took to identifying mistakes and fixing them.&lt;/p>
&lt;!-- more -->
&lt;h1 id="downloading">Downloading&lt;/h1>
&lt;p>I previously talked about downloading data from Mint in &lt;a class="link" href="https://www.technowizardry.net/2024/02/monarch-money-and-ad-networks" >Monarch Money and ad networks&lt;/a>. Also download the daily balances. I also used the &lt;a class="link" href="https://chromewebstore.google.com/detail/mint-data-exporter-by-mon/doknkjpaacjheilodaibfpimamfgfhap" target="_blank" rel="noopener"
>Monarch Money Data Exporter&lt;/a> to download daily balance info.&lt;/p>
&lt;h1 id="understanding-the-firefly-model">Understanding the Firefly model&lt;/h1>
&lt;p>Importing into Firefly is tricky because it calculates the current balance of an account as the sum of all credits and debits. This is good in theory, but it requires you to have perfectly accurate transaction data going back to the account opening date. Most other tools, like Mint, Monarch, Personal Capital, all seem to track as a series of transactions and the balance is just collected from the bank and is not a function of the transactions. That&amp;rsquo;s easier to implement because it doesn&amp;rsquo;t require the full history and is robust against errors&lt;/p>
&lt;p>Additionally, Firefly-iii supports explicitly modelling transfers between two accounts. In effect, the two separate credit and debit transactions are linked together. This is problematic (and you&amp;rsquo;ll see why) because no bank actually tracks the opposite transaction, nor does Mint.&lt;/p>
&lt;h1 id="importing-using-the-firefly-importer">Importing using the Firefly importer&lt;/h1>
&lt;p>Firefly has a default &lt;a class="link" href="https://github.com/firefly-iii/data-importer" target="_blank" rel="noopener"
>importer&lt;/a>. It&amp;rsquo;s a separate Docker container that enables you to import from a csv file.&lt;/p>
&lt;p>The importer has performance issues, doesn&amp;rsquo;t scale when loading large amounts of history. It would fail to load and crash when trying to do an entire year of transaction data.&lt;/p>
&lt;p>Data from Mint isn&amp;rsquo;t clean and has a few duplicates here and there.&lt;/p>
&lt;p>Identifying transfers is tough. It has no concept of being able to identify a transfer itself given two halves. The only way is to set remap the description as an opposing account, but descriptions aren&amp;rsquo;t consistent.&lt;/p>
&lt;p>For example, these are all transaction descriptions from Mint with different formats. I know they&amp;rsquo;re on the CAPITAL ONE side and I know which account they pull from, but I had to manually remap every single month:&lt;/p>
&lt;ul>
&lt;li>CAPITAL ONE AUTOPAY PYMT AuthDate 09-Oct&lt;/li>
&lt;li>CAPITAL ONE AUTOPAY PYMT AuthDate 09-Sep&lt;/li>
&lt;li>CAPITAL ONE AUTOPAY payment AuthDate 05-N ov&lt;/li>
&lt;li>CAPITAL ONE CRCARDPMT&lt;/li>
&lt;/ul>
&lt;p>An ambiguous example:&lt;/p>
&lt;ul>
&lt;li>Internet transfer from Interest Checking account&lt;/li>
&lt;li>Internet transfer from Online Savings account&lt;/li>
&lt;/ul>
&lt;p>Which account was it transferred from? This simply wasn&amp;rsquo;t going to work, so I gave up using the tool.&lt;/p>
&lt;h1 id="importing-using-pandas">Importing using Pandas&lt;/h1>
&lt;p>&lt;a class="link" href="https://pandas.pydata.org" target="_blank" rel="noopener"
>Pandas&lt;/a> is a popular Python package designed for data processing and manipulation and great for working with CSV files like this. It should be more scalable and able to handle these ambiguous cases.&lt;/p>
&lt;h2 id="loading">Loading&lt;/h2>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">pandas&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">pd&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">transactions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read_csv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;/data/transactions.csv&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">parse_dates&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">encoding&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;utf8&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sort_values&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">reset_index&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Id&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">index&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Map the Account Name column to the right Firefly account id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">account_map&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Checking&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2087&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">account_map&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isnull&lt;/span>&lt;span class="p">()]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">groupby&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">count&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">transactions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">notnull&lt;/span>&lt;span class="p">()]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AccountId&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">astype&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;int&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AbsoluteAmount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="p">[[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Transaction Type&amp;#39;&lt;/span>&lt;span class="p">]]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">apply&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">lambda&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Transaction Type&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;credit&amp;#39;&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">axis&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;RunningBalance&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sort_values&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">groupby&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="s1">&amp;#39;AbsoluteAmount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">transform&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Series&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cumsum&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">transactions&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="comparing-expected-vs-actual-balances">Comparing expected vs actual balances&lt;/h2>
&lt;p>First, we need to check to see if Mint even has the right transactions (foreshadowing). Let&amp;rsquo;s compute the total balance based on all transactions:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_option&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;max_colwidth&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_option&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;display.width&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">options&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">display&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">float_format&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="si">{:20,.2f}&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">format&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">balances&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">groupby&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Transaction Type&amp;#39;&lt;/span>&lt;span class="p">])[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sum&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">to_frame&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">reset_index&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pivot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">columns&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;Transaction Type&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">values&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">balances&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;balance&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">balances&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;credit&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">balances&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;debit&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">balances&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Next, load the balances as measured by Mint. The folder &lt;code>/data/mint-balances&lt;/code> contains &lt;code>.csv&lt;/code> files with the schema: &lt;code>Account Name,Date,Amount&lt;/code>. I downloaded this using the &lt;a class="link" href="https://chromewebstore.google.com/detail/mint-data-exporter-by-mon/doknkjpaacjheilodaibfpimamfgfhap" target="_blank" rel="noopener"
>Monarch Money Data Exporter&lt;/a> before Mint shut down entirely.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">glob&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">dfs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">name&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">glob&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">glob&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/data/mint-balances/*.csv&amp;#39;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">dfs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read_csv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">parse_dates&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">]))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">actual_balances&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">concat&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">dfs&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">axis&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ignore_index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">current_balance&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">actual_balances&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sort_values&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">by&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ascending&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">groupby&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">first&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">drop&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">columns&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Join to find error&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">balance_combined&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">merge&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">left&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">current_balance&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">columns&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;Actual&amp;#39;&lt;/span>&lt;span class="p">}),&lt;/span> &lt;span class="n">right&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">balances&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">columns&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;balance&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;Estimated&amp;#39;&lt;/span>&lt;span class="p">}),&lt;/span> &lt;span class="n">left_index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">right_index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">balance_combined&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">balance_combined&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Estimated&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">balance_combined&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Actual&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">balance_combined&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sort_values&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="s1">&amp;#39;Estimated&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Actual&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">]])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Stock brokerage accounts
I focused just on the cash based accounts (excluded anything with any type of security pricing such as brokerages, IRAs, etc.) and then compared the balances with the actual balances to find the balance error. In theory, if Mint were have collected data perfectly accurately, all error balances would be $0. However, it was not.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Account Name&lt;/th>
&lt;th>balance&lt;/th>
&lt;th>actual_balance&lt;/th>
&lt;th>balance_error&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>A&lt;/td>
&lt;td>$4,000.0&lt;/td>
&lt;td>$2,000.0&lt;/td>
&lt;td>$2,000.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>B&lt;/td>
&lt;td>$500.00&lt;/td>
&lt;td>$500.0&lt;/td>
&lt;td>$0.00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C&lt;/td>
&lt;td>-$1,000.0&lt;/td>
&lt;td>$600.00&lt;/td>
&lt;td>-$400.00&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Unfortunately, errors are hard to track down because the error can come from anywhere.&lt;/p>
&lt;h2 id="duplicates">Duplicates&lt;/h2>
&lt;p>One source is duplicated transactions. Duplicated transactions aren&amp;rsquo;t guaranteed to be bad, but could be. For example, if you pay with your phone at &lt;a class="link" href="https://en.wikipedia.org/wiki/Metropolitan_Transportation_Authority" target="_blank" rel="noopener"
>MTA&lt;/a>, you&amp;rsquo;ll end up with a transaction for the same price for each ride.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">transactions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">duplicated&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">subset&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Transaction Type&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Original Description&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">keep&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">)]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># [98 rows x 12 columns]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>How you actually verify whether the duplicates are correct will vary. If you have access to the original statements (not likely if you&amp;rsquo;re past 7 years), then you&amp;rsquo;re going to have to use intuition. For example, I purchased something at a 0% financed rate. The payment is the same every month, so if there&amp;rsquo;s a duplicate, it&amp;rsquo;s wrong. My Capital One card allowed me to fetch any statements from the open date, my Citi and Amex cards required me to request transactions, and other cards I was out of luck.&lt;/p>
&lt;p>One account, like the &amp;ldquo;Account A/(Fidelity Cash Management)&amp;rdquo;, had an error of -$43.47. This was easy because I found a duplicate transaction:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Date&lt;/th>
&lt;th>Description&lt;/th>
&lt;th>Type&lt;/th>
&lt;th>Amount&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>9/25/2018&lt;/td>
&lt;td>Cash Withdrawal&lt;/td>
&lt;td>debit&lt;/td>
&lt;td>43.47&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>9/25/2018&lt;/td>
&lt;td>Cash Withdrawal&lt;/td>
&lt;td>debit&lt;/td>
&lt;td>43.47&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Removing one from my .csv file brought the error for that account from -$43.47 to $0.00.&lt;/p>
&lt;h2 id="comparing-balance-over-time-to-identify-errors">Comparing balance over time to identify errors&lt;/h2>
&lt;p>Once I identified which accounts showed errors, I compared the estimated balance vs the actual balance. The estimated balance is the cumulative sum of the credits and debits each day, and the actual balance is the daily balance as measured by Mint. This balance is collected when it scrapes the account so it should be accurate. Before this we compared just the ending balance, but this approach breaks it down as a time series which should help us narrow down to the exact date when an error is introduced.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">numpy&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">np&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">datetime&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">timedelta&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">matplotlib.pyplot&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">plt&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">render_bal_chart&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">acc_name&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">fig&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ax&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">subplots&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">figsize&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Difference in expected balance vs actual&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_label&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">yaxis&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_label&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;$&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">actual_calc&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">actual_balances&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">actual_balances&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">acc_name&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_index&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">actual_calc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Change&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">actual_calc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">diff&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">actual&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">actual_calc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">actual_calc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Change&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">estimated&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">acc_name&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">groupby&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">])[&lt;/span>&lt;span class="s1">&amp;#39;RunningBalance&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sum&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">merge&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">merge_asof&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">left&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">actual&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Actual&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">right&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">estimated&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Estimated&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">left_index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">right_index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tolerance&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">timedelta&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">days&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">direction&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;backward&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Actual&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Estimated&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">render_bal_chart&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This next diagram shows the difference in the daily balance calculation.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/images/balance-difference-noisy.png"
width="388"
height="278"
srcset="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/images/balance-difference-noisy_hu_8269614a3d28ca3c.png 480w, https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/images/balance-difference-noisy_hu_f883b69780c75dad.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="139"
data-flex-basis="334px"
>&lt;/p>
&lt;p>This chart is very noisy and not helpful yet. But wait, let&amp;rsquo;s zoom in! There seems to be a noticeable pattern:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/images/balance-difference-noisy-zoomed.png"
width="627"
height="333"
srcset="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/images/balance-difference-noisy-zoomed_hu_6059b2243a231b69.png 480w, https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/images/balance-difference-noisy-zoomed_hu_3d92abcdd1af83cf.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="188"
data-flex-basis="451px"
>&lt;/p>
&lt;p>As it turns out, the Mint collected account balance isn&amp;rsquo;t always right because 1) Mint doesn&amp;rsquo;t update every day and 2) pending transactions aren&amp;rsquo;t always included in the balance. To fix this, we can calculate a rolling average and drop out the error:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">numpy&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">np&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">datetime&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">timedelta&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">matplotlib.pyplot&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">plt&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">render_bal_chart&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">acc_name&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">fig&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ax&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">subplots&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">figsize&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Difference in expected balance vs actual&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">xlabel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ylabel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;$&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">actual_calc&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">actual_balances&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">actual_balances&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">acc_name&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_index&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">actual_calc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Change&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">actual_calc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">diff&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">actual&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">actual_calc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">actual_calc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Change&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">estimated&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">acc_name&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">groupby&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">])[&lt;/span>&lt;span class="s1">&amp;#39;RunningBalance&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sum&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">merge&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">merge_asof&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">left&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">actual&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Actual&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">right&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">estimated&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Estimated&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">left_index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">right_index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tolerance&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">timedelta&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">days&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">direction&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;backward&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Actual&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Estimated&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Denoise the signal&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">rolling_mean&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">rolling&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mean&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">z_scores&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">abs&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">rolling_mean&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">df_filtered&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">z_scores&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">diff&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">df_filtered&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">diff&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">non_zero_indices&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">df_filtered&lt;/span>&lt;span class="p">[(&lt;/span>&lt;span class="n">diff&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">diff&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">index&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">indices&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Include the previous value at each step&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">non_zero_indices&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">indices&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">df_filtered&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">index&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get_loc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">indices_to_include&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">df_filtered&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">index&lt;/span>&lt;span class="p">[[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">union&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">non_zero_indices&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">union&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">df_filtered&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">index&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">indices&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">df_filtered&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">indices_to_include&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">marker&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;.&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">idx&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">indices&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">idiff&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">diff&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">index&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">idx&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">df_filtered&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">idiff&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">idiff&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">300&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;(&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">idiff&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strftime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;%b &lt;/span>&lt;span class="si">%d&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">verticalalignment&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;bottom&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">idx&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">non_zero_indices&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">idiff&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">diff&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">idx&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">idx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">df_filtered&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">idx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;(&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">idx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strftime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;%b &lt;/span>&lt;span class="si">%d&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">, &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">idiff&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.2f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">, &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">df_filtered&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">idx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.2f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">verticalalignment&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;bottom&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">render_bal_chart&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;a class="link" href="images/balance-cleaned-edges.png" >&lt;img src="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/images/balance-cleaned-edges.png"
width="613"
height="319"
srcset="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/images/balance-cleaned-edges_hu_3bbc9767a7c2dc2.png 480w, https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/images/balance-cleaned-edges_hu_2502e994b584ca29.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="192"
data-flex-basis="461px"
>&lt;/a>&lt;/p>
&lt;p>Now I know to check between Mar 6th and April 26, 2013 for either an extra $20 credit or a missing $20 transaction. As it turns out there were two $20 credits. The same thing happened with the $1k difference in the end of 2019. This seems to be the common mistake.&lt;/p>
&lt;p>This method worked pretty well for cash based accounts, but trying to apply it to a credit card broke down because every transaction goes through a pending phase for a few days, far more than the cash accounts did. Even after tweaking the denoising algorithm, it still wasn&amp;rsquo;t able to find the error points even though I could very clearly see it with my eyes.&lt;/p>
&lt;p>&lt;a class="link" href="images/balance-error.png" >&lt;img src="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/images/balance-error.png"
width="575"
height="333"
srcset="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/images/balance-error_hu_1850763ba9e41504.png 480w, https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/images/balance-error_hu_e83ca78c0f3b9af9.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="172"
data-flex-basis="414px"
>&lt;/a>&lt;/p>
&lt;p>I realized the most common failure point seems to be duplicated transactions, so can I specifically test for them? I came up with the following algorithm, first I identified all of the duplicated transactions, then it simulates removing one of the duplicates, if it reduces the error, it saves the transactions for further testing.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">render_bal_chart&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">acc_name&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> Renders a chart showing the estimated balance and the correct balance for a single account.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :param str acc_name: The account name as listed in the transactions.csv and the mint-balances.csv file
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">fig&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">ax&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ax2&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">subplots&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">figsize&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">20&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s1">&amp;#39;Difference in expected vs actual: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">acc_name&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">xaxis&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_label&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">applicable_transactions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">acc_name&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">actual_calc&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">actual_balances&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">actual_balances&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">acc_name&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_index&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">actual_calc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Change&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">actual_calc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">diff&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">actual&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">actual_calc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">actual_calc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Change&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">estimated&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">applicable_transactions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">groupby&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">])[&lt;/span>&lt;span class="s1">&amp;#39;AbsoluteAmount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sum&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cumsum&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">merge&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">merge&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">left&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">actual&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Actual&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">right&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">estimated&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Estimated&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">left_index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">right_index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">how&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;inner&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">validate&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;one_to_one&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Actual&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Estimated&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">current_error&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">abs&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">iloc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">legend_handles&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">best_errors&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">index&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">duplicate&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">applicable_transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">applicable_transactions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">duplicated&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">subset&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Transaction Type&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Original Description&amp;#39;&lt;/span>&lt;span class="p">])]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">groupby&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Transaction Type&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Original Description&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">agg&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;count&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;sum&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;mean&amp;#39;&lt;/span>&lt;span class="p">])[&lt;/span>&lt;span class="s1">&amp;#39;Amount&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">iterrows&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">date_key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">duplicate&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">duplicate&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;debit&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">adj&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">duplicate&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;mean&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">adj&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">duplicate&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;mean&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">test_s&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">copy&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">test_s&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">test_s&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">index&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">date_key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">adj&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">new_error&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">abs&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">test_s&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">date_key&lt;/span>&lt;span class="p">:]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mean&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">new_error&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">current_error&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">best_errors&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="s1">&amp;#39;error&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">test_s&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">abs&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sum&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="s1">&amp;#39;data&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">duplicate&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;adj&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">adj&lt;/span>&lt;span class="p">})&lt;/span> &lt;span class="c1"># Area under the curve&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Once it has a list of possibly incorrect duplicates ordered by how much it reduces the error, and iteratively simulates removing multiple transactions until it no-longer improves. These transactions are the ones that have a high likelihood of being invalid. To confirm, use your memory or check your statements.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">best_errors&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">sorted&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">best_errors&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">lambda&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;error&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">new_df&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">copy&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">current_error&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">new_df&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">abs&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sum&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">convergence&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">current_error&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">best_error&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">best_errors&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">attempt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">new_df&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">copy&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">date_key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">best_error&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;data&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">adj&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">best_error&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;adj&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">attempt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">date_key&lt;/span>&lt;span class="p">:]&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">adj&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">new_error&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">attempt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">abs&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sum&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">new_error&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">current_error&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">new_df&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">attempt&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">current_error&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">new_error&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">convergence&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">current_error&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Found duplicate that when removed, reduced error: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">date_key&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strftime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;%Y-%m-&lt;/span>&lt;span class="si">%d&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">, tamount=&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">adj&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">,iamount=&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">adj&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">duplicate&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;count&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">foo&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">new_df&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">label&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Duplicate: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">date_key&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strftime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;%Y-%m-&lt;/span>&lt;span class="si">%d&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">, amount=&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">best_error&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;data&amp;#39;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;mean&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">legend_handles&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">foo&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">axvline&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">date_key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">color&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">foo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get_color&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax2&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Convergence of error&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax2&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">convergence&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then finally render the simulations:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">legend_handles&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">merge&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">label&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;Default&amp;#39;&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">legend&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">handles&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">legend_handles&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">axhline&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">y&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">color&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;grey&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">dashes&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Running this gets us the following. The second graph shows the error duplicates are removed (Lower is better). The first graph shows the default line along with the simulated removals. It&amp;rsquo;s a little hard to see, but at the very end, the error gets corrected back to 0 by removing the duplicates.&lt;/p>
&lt;p>&lt;a class="link" href="images/balance-error-simulation.png" >&lt;img src="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/images/balance-error-simulation.png"
width="1153"
height="1134"
srcset="https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/images/balance-error-simulation_hu_5d1d4dfc8d2fc175.png 480w, https://www.technowizardry.net/2024/04/importing-and-cleaning-mint-transactions/images/balance-error-simulation_hu_bc9ee9ccf314b7ad.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="101"
data-flex-basis="244px"
>&lt;/a>&lt;/p>
&lt;p>Note, this may have false positives or false negatives. It&amp;rsquo;s calculating based on what looks right. However, it should give you some places to look. In my case, I looked at these transactions and deleted them from the CSV because they were wrong.&lt;/p>
&lt;h2 id="dummy-transactions">Dummy Transactions&lt;/h2>
&lt;p>The last strategy I employ is to just add dummy transactions to correct any errors. Find the earliest statement you can get for an account and see what the starting balance is. Then compare that with your estimated balance. Whatever the difference is can be added as an initial balance transaction.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">account&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">transactions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Account Name&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;General Fund&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sort_values&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ascending&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">first_known&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">account&lt;/span>&lt;span class="p">[(&lt;/span>&lt;span class="n">account&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="s1">&amp;#39;2016-01-01&amp;#39;&lt;/span>&lt;span class="p">)]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">iloc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">][[&lt;/span>&lt;span class="s1">&amp;#39;Date&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;RunningBalance&amp;#39;&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">first_known&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>To calculate the initial balance, subtract the RunningBalance from the expected balance and create a manual transaction:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Date 2011-11-28 00:00:00
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">RunningBalance 335.21
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Name: 16, dtype: object
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="all-together">All Together&lt;/h1>
&lt;p>If you want to see the (messy) code all together, take a look at the GitHub repo &lt;a class="link" href="https://github.com/ajacques/own-your-finances" target="_blank" rel="noopener"
>ajacques/own-your-finances&lt;/a>.&lt;/p>
&lt;h1 id="next-steps--conclusion">Next Steps &amp;amp; Conclusion&lt;/h1>
&lt;p>Now, we should have a Pandas DataFrame containing cleaned and accurate transaction data. We identified multiple strategies to identify bad transaction data In Part 2, I&amp;rsquo;ll walk through how to identify the transfer pairs and actually import it into Firefly. At some point, I&amp;rsquo;ll figure out how to link this up to pull transaction data from accounts.&lt;/p>
&lt;p>If you&amp;rsquo;re in the United States, then you basically have no good options for accessing your data yourself without paying other companies (e.g. &lt;a class="link" href="https://www.plaid.com" target="_blank" rel="noopener"
>Plaid&lt;/a>, &lt;a class="link" href="https://akoya.com/" target="_blank" rel="noopener"
>Akoya&lt;/a>) to screen scrape or integrate with restricted APIs. I&amp;rsquo;ll dive into this more in a later post, but this is why I&amp;rsquo;m very interested in the CFPB&amp;rsquo;s proposed rules on &lt;a class="link" href="https://www.consumerfinance.gov/personal-financial-data-rights/" target="_blank" rel="noopener"
>CFP1033&lt;/a>. For awhile they were accepting public comments &lt;a class="link" href="https://www.regulations.gov/docket/CFPB-2023-0052" target="_blank" rel="noopener"
>here&lt;/a>, but that&amp;rsquo;s closed now.&lt;/p>
&lt;h1 id="errata">Errata&lt;/h1>
&lt;ul>
&lt;li>2024-04-25 - Fix bug in &lt;code>render_bal_chart&lt;/code> because I had an extra parameter that shouldn&amp;rsquo;t apply to other users. Added better validation.&lt;/li>
&lt;/ul>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F04%2Fimporting-and-cleaning-mint-transactions%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Importing+and+cleaning+Mint+transactions" style="border:0" alt="" /></description></item><item><title>What to expect when you're excepting Java</title><link>https://www.technowizardry.net/2024/03/java-exceptions-practices/</link><pubDate>Fri, 08 Mar 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/03/java-exceptions-practices/</guid><summary>&lt;p>Birthing code is not always easy.&lt;/p>
&lt;p>Enough puns. Let&amp;rsquo;s talk about Java exceptions. No matter how hard you try, your code will likely encounter an error and throw an exception (if your language supports exceptions.) It could be anything from unexpected user input to an underlying service outage. An exception will be thrown and it&amp;rsquo;s important to do something useful with it. That doesn&amp;rsquo;t mean putting try-catch blocks everywhere or trying to recover everything, in-fact I&amp;rsquo;ll argue the opposite in a few situations.&lt;/p>
&lt;p>This post introduces a few common issues I&amp;rsquo;ve seen when working with Java code-bases and developers that lead to poor debuggability or other operational pains.&lt;/p></summary><description>&lt;p>Birthing code is not always easy.&lt;/p>
&lt;p>Enough puns. Let&amp;rsquo;s talk about Java exceptions. No matter how hard you try, your code will likely encounter an error and throw an exception (if your language supports exceptions.) It could be anything from unexpected user input to an underlying service outage. An exception will be thrown and it&amp;rsquo;s important to do something useful with it. That doesn&amp;rsquo;t mean putting try-catch blocks everywhere or trying to recover everything, in-fact I&amp;rsquo;ll argue the opposite in a few situations.&lt;/p>
&lt;p>This post introduces a few common issues I&amp;rsquo;ve seen when working with Java code-bases and developers that lead to poor debuggability or other operational pains.&lt;/p>
&lt;h1 id="practices">Practices&lt;/h1>
&lt;h2 id="stop-the-catch-log-throw-shuffle">Stop the catch-log-throw shuffle&lt;/h2>
&lt;p>This is a common problem in Java code that I see. At multiple levels, developers will catch broad exceptions because there are checked exceptions just to wrap an re-throw or to log to the console.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Do a lot of logic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Exception&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">warn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Something bad happened!&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">throw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RuntimeException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Failed to update.&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then this happens at every level in the call stack and soon enough your error log is 10 screens high of call stacks and the exception class has lost all meaning.&lt;/p>
&lt;p>Avoid needless wrapping of exceptions. Every wrapped exception adds more noise to understanding the problem. Each wrapped exceptions should provide some additional context or an abstracted exception type.&lt;/p>
&lt;p>Take the above example:&lt;/p>
&lt;ul>
&lt;li>The outer RuntimeException provides no additional context and the exception class is not specific at all. Make the error clear and direct. Is it important to know which item failed to update?&lt;/li>
&lt;li>The log statement probably happens at every single catch block. The application log is probably full of useless log statements. Instead, log only at the high point in you call stack.&lt;/li>
&lt;/ul>
&lt;h2 id="logging-with-log4jslf4j-correctly">Logging with Log4j/Slf4j correctly&lt;/h2>
&lt;p>The following example breaks down a common way developers catch and log exceptions in code. Can you spot the issue? Hint: it has to do with the log format.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">lombok.extern.slf4j.Slf4j&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Slf4j&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">ExceptionTest&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">B&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">throw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IllegalArgumentException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Bad argument&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">A&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">B&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Exception&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">throw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RuntimeException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Something went wrong&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">main&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">[]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">A&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Exception&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ex&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Two examples of problematic logging&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Oh no! an error happened doing {}: {}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;something&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ex&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Oh no! an error happened doing {}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ex&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Running the above code gets the following log statement. Where&amp;rsquo;s the call stack or the nested exception information?&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">15:21:08.748 [main] ERROR ExceptionTest - Oh no! an error happened doing something: java.lang.RuntimeException: Something went wrong
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>As it turns out, Log4j has special logic when it&amp;rsquo;s formatting the log statement. If there are n placeholders and n+1 arguments and the last value extends from Throwable, then it prints out the exception call stack and nested exceptions. If there are n placeholders and n arguments, it doesn&amp;rsquo;t matter if the last argument is a Throwable, it calls #toString() on all the objects.&lt;/p>
&lt;p>The one edge case to this is if you call log.error(&amp;ldquo;msg&amp;rdquo;, throwable), then the compiler directly calls Logger#error(String,Throwable), but the end result is the same.&lt;/p>
&lt;p>Instead, don&amp;rsquo;t include a log placeholder for the exception and always pass it as the last parameter:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">lombok.extern.slf4j.Slf4j&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Slf4j&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">ExceptionTest&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">B&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">throw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IllegalArgumentException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Bad argument&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">A&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">B&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Exception&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">throw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RuntimeException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Something went wrong&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">main&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">[]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">A&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Exception&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ex&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Oh no! an error happened doing {}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;something&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ex&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>When we run the above code, we now get a much more intuitive log statement:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">15:21:47.644 [main] ERROR ExceptionTest - Oh no! an error happened doing something
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">java.lang.RuntimeException: Something went wrong
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at ExceptionTest.A(ExceptionTest.java:13)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at ExceptionTest.main(ExceptionTest.java:19)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Caused by: java.lang.IllegalArgumentException: Bad argument
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at ExceptionTest.B(ExceptionTest.java:6)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at ExceptionTest.A(ExceptionTest.java:11)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ... 1 more
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If you&amp;rsquo;re using IntelliJ, you can automatically catch issues like this. It&amp;rsquo;s won&amp;rsquo;t detect all issues, but it&amp;rsquo;s a good start.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// This is detected&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Oh no! an error happened doing {}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ex&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// This isn&amp;#39;t detected&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Oh no! an error happened doing {} {}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;something&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ex&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Find it in: IntelliJ Inspections: &lt;code>Java | Logging | Number of placeholders does not match number of arguments in logging call.&lt;/code>&lt;/p>
&lt;h2 id="include-inner-exceptions">Include Inner Exceptions&lt;/h2>
&lt;p>Almost always include the inner exception when throwing an exception in a catch block. Unless you&amp;rsquo;re careful to include sufficient to explain what happened, you&amp;rsquo;re more likely to throw an exception that contains not enough information. Special care should be taken when exposing exceptions outside a security boundary, such as to a caller of a service. Log the full details, but then truncate to a minimal amount of information in the response body.&lt;/p>
&lt;p>For example, take this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Do some work&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">IllegalArgumentException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">throw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">WebApplicationException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Invalid argument&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>With this, the WebApplicationException does not have an inner exception, so consumers have no context about what happened. In this specific case, it&amp;rsquo;s critical for the caller to know what argument was invalid so they can fix it.&lt;/p>
&lt;p>Include an inner exception when wrapping the exception:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Do some work&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">IllegalArgumentException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">){&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">throw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">BadRequestException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="dont-accidentally-mistake-developer-mistakes-for-validation-issues">Don&amp;rsquo;t accidentally mistake developer mistakes for validation issues&lt;/h2>
&lt;p>NullPointerExceptions are commonly thrown by validation functions like Lombok&amp;rsquo;s @NonNull or Guava&amp;rsquo;s Preconditions. They&amp;rsquo;re also frequently caused by developer mistakes when you call a method on a null. Unfortunately this can lead to poor exception handling if you assume that NPEs are only thrown by your validation code.&lt;/p>
&lt;p>Take the following example. NullPointerExceptions can be thrown in any line of code here. It could mean that the variable something is null and it&amp;rsquo;s possible handle it or it could mean a developer made a mistake (as I did) and tried to call a method on a null object.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">com.google.common.base.Preconditions&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// [...]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">FooBar&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fooBar&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Preconditions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">checkNotNull&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">something&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getValue&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;Validate Something&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">fooBar&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">callSomething&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="c1">// Will throw an NPE&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">NullPointerException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">LOG&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;something wasn&amp;#39;t valid. It&amp;#39;s okay though&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// something must be null and invalid&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Extract out exception message construction&lt;/p>
&lt;p>If you create and throw custom exceptions, you might find yourself writing code like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">MyCustomException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">extends&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RuntimeException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">MyCustomException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Throwable&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cause&lt;/span>&lt;span class="p">){&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">super&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cause&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">throw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">MyCustomException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">format&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Something %s happened when doing %s while also doing %s&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">z&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If you throw this exception multiple times, then you may end up copying and pasting the String.format code in multiple places. Instead just move the message construction onto the exception class itself:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">MyCustomException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">extends&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RuntimeException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">MyCustomException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">z&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Throwable&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cause&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">super&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">makeMessage&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">z&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cause&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">makeMessage&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">z&lt;/span>&lt;span class="p">){&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">format&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Something %s happened when doing %s while also doing %s&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">z&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">throw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">MyCustomException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">z&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="structured-exceptions">Structured exceptions&lt;/h2>
&lt;p>Pretty much every exception I see thrown converts all the problem details into a single giant string message. Strings are terrible at encoding information. Sure a human can read it, but often times code needs to analyze it. Say you&amp;rsquo;ve got a library that throws a throttling exception when the client calls it too frequently:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">performSomething&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">numCalls&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">100&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">throw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ThrottledException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">format&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Request throttled. Client hit %s calls in %s seconds. Please try again in %s seconds.&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">...),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">performSomething&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ThrottledException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pos&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getMessage&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">indexOf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">again&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">in&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">seconds&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getMessage&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">substring&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pos&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">again&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">in&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">length&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>As a caller, I might want to know when I actually can try again. Since this is currently in a string, I&amp;rsquo;d have to string parse this exception message to find out. That&amp;rsquo;s horrifying.&lt;/p>
&lt;p>Instead, have your exception object property getters for these different facts:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">ThrottledException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">extends&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RuntimeException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">numCalls&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">numSeconds&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Instant&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">resetsAt&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">ThrottledException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">numCalls&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">numSeconds&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Instant&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">resetsAt&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">super&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">format&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Request throttled. Client hit %s calls in %s seconds. Please try again at %s.&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">numCalls&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">numSeconds&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">resetsAt&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">numCalls&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">numCalls&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">numSeconds&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">numSeconds&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">resetsAt&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">resetsAt&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="cm">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * Gets when the caller can resume performing work.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> **/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Instant&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getResetsAt&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">resetsAt&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">performSomething&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">numCalls&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">100&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">throw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ThrottledException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">numCalls&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">numSeconds&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Instant&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">now&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">plusHours&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">1&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">performSomething&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ThrottledException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getResetsAt&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Now I know when to check again&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc9457" target="_blank" rel="noopener"
>RFC9457&lt;/a> is a good resource on how to apply this style of error structuring to an HTTP endpoint.&lt;/p>
&lt;h2 id="checked-vs-unchecked-exception">Checked vs Unchecked Exception&lt;/h2>
&lt;p>Checked exceptions are exceptions which are checked by the compiler, if they are being explicity thrown or caught. The class Exception and any subclasses that are not also subclasses of RuntimeException are checked exceptions. Checked exceptions need to be declared in a method or constructor&amp;rsquo;s throws clause if they can be thrown by the execution of the method or constructor and propagate outside the method or constructor boundary.&lt;/p>
&lt;p>Examples of checked exceptions that we might have seen are IOException, JSONParseException.&lt;/p>
&lt;p>Every exception which is subclass of the RuntimeException class is an unchecked exception and not checked by the compiler. A common example of this is the NullPointerException.&lt;/p>
&lt;p>Which type of exception should we use when creating our own exceptions?&lt;/p>
&lt;p>Almost always the recommendation is to create an unchecked exception.&lt;/p>
&lt;p>Checked exceptions, if not modeled correctly, adds unnecessary burden on the developers. If you throw a checked exception in your code and it can only be appropriately handled, say 3 - 4 layers up, it has to be added in each method’s declaration. Imagine adding/removing a checked exception in the code which would require layers of code to be refactored. Checked exceptions also do not add any value in our service interfaces as the actual clients will never catch it.&lt;/p>
&lt;p>The worst offender for useless checked exception is &lt;a class="link" href="https://docs.oracle.com/javase/8/docs/api/java/nio/charset/Charset.html#forName-java.lang.String-" target="_blank" rel="noopener"
>Charset#ofName&lt;/a> which throws a checked &lt;a class="link" href="https://docs.oracle.com/javase/8/docs/api/java/nio/charset/IllegalCharsetNameException.html" target="_blank" rel="noopener"
>IllegalCharsetNameException&lt;/a>. Every time I&amp;rsquo;ve used it, I&amp;rsquo;ve always passed the static string &lt;code>UTF-8&lt;/code> and if it&amp;rsquo;s missing, I&amp;rsquo;m screwed.&lt;/p>
&lt;p>Imagine, every time you have to deserialize or serialize a file you end up with this the following code and this untestable catch block that reduces my code coverage.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Charset&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">ofName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">UTF&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">8&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">IllegalCharsetNameException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">throw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RuntimeException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;When would this ever happen?&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="references">References&lt;/h1>
&lt;ul>
&lt;li>&lt;a class="link" href="https://yiming.dev/blog/2022/07/10/how-let-it-fail-leads-to-simpler-code/" target="_blank" rel="noopener"
>https://yiming.dev/blog/2022/07/10/how-let-it-fail-leads-to-simpler-code/&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc9457" target="_blank" rel="noopener"
>https://www.rfc-editor.org/rfc/rfc9457&lt;/a>&lt;/li>
&lt;/ul>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F03%2Fjava-exceptions-practices%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=What+to+expect+when+you%27re+excepting+Java" style="border:0" alt="" /></description></item><item><title>Monarch Money and ad networks</title><link>https://www.technowizardry.net/2024/02/monarch-money-and-ad-networks/</link><pubDate>Wed, 14 Feb 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/02/monarch-money-and-ad-networks/</guid><summary>&lt;p>In late 2023, Intuit announced that Mint was going to be shutting down and migrating everybody to Credit Karma. I could try out Credit Karma, but maybe it&amp;rsquo;s time to explore alternatives. Since that announcement came out, I launched a massive time sink to try and find a new option I liked.&lt;/p></summary><description>&lt;p>In late 2023, Intuit announced that Mint was going to be shutting down and migrating everybody to Credit Karma. I could try out Credit Karma, but maybe it&amp;rsquo;s time to explore alternatives. Since that announcement came out, I launched a massive time sink to try and find a new option I liked.&lt;/p>
&lt;h1 id="intro">Intro&lt;/h1>
&lt;p>Different people have different goals for a finance tracking app. My requirements are not so much about following a strict budget, but more focused on transaction tracking, tracking stocks and other assets, and future planning. I looked around and found a few.&lt;/p>
&lt;p>The first one I tried is &lt;a class="link" href="https://www.monarchmoney.com/" target="_blank" rel="noopener"
>Monarch Money&lt;/a>.&lt;/p>
&lt;p>Monarch Money was interesting. It was $99/yr, but with a &lt;code>MINT50&lt;/code> discount it was only $50/yr. I don&amp;rsquo;t mind paying for services. Especially ones that add value. I tried it out. It had a mechanism to import data from Mint.&lt;/p>
&lt;h1 id="importing-from-mint">Importing from Mint&lt;/h1>
&lt;p>This is broken down into the transactions and the balance history. This was tedious, but largely worked. I first discovered that Mint truncated the transaction.csv history to 10,000 records and lost data.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/02/monarch-money-and-ad-networks/images/mint-export.png"
width="273"
height="108"
srcset="https://www.technowizardry.net/2024/02/monarch-money-and-ad-networks/images/mint-export_hu_be01b87d624b1a2f.png 480w, https://www.technowizardry.net/2024/02/monarch-money-and-ad-networks/images/mint-export_hu_b88e023500be66b5.png 1024w"
loading="lazy"
alt="Alt text"
class="gallery-image"
data-flex-grow="252"
data-flex-basis="606px"
>&lt;/p>
&lt;p>To fix this, export each section independently until you have under 10k in each export and join them together into a single CSV.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/02/monarch-money-and-ad-networks/images/mint-transaction-breakdown.png"
width="238"
height="350"
srcset="https://www.technowizardry.net/2024/02/monarch-money-and-ad-networks/images/mint-transaction-breakdown_hu_2b66d0605eb6e2bd.png 480w, https://www.technowizardry.net/2024/02/monarch-money-and-ad-networks/images/mint-transaction-breakdown_hu_c90e2e95cde977d3.png 1024w"
loading="lazy"
alt="Alt text"
class="gallery-image"
data-flex-grow="68"
data-flex-basis="163px"
>&lt;/p>
&lt;p>The next problem is that Monarch Money requires all accounts to be created before importing which meant I couldn&amp;rsquo;t import accounts that were closed. This is not a huge deal, but I had 70+ accounts at one point or another.&lt;/p>
&lt;p>This was fine, took effort to work. I had to create all the accounts first, the import transactions. It also struggled with CDs which got opened and closed.&lt;/p>
&lt;h1 id="ad-networks">Ad networks?&lt;/h1>
&lt;p>But then I noticed something interesting in &lt;a class="link" href="https://noscript.net/" target="_blank" rel="noopener"
>NoScript&lt;/a>. It loaded tons of assets from other domains. Why is a financial institution that has access to all my financial data loading data from tiktok.com?!&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/02/monarch-money-and-ad-networks/images/monarch-noscript.png"
width="833"
height="614"
srcset="https://www.technowizardry.net/2024/02/monarch-money-and-ad-networks/images/monarch-noscript_hu_c6a2a2f1918ee734.png 480w, https://www.technowizardry.net/2024/02/monarch-money-and-ad-networks/images/monarch-noscript_hu_dfa7d8afbced6837.png 1024w"
loading="lazy"
alt="A screenshot of the NoScript browser extension showing what domain names Monarch Money is trying to load content such as JavaScript, fonts, etc. from. It shows a variety of domains including Spotify, Tiktok, and Reddit. More analysis below."
class="gallery-image"
data-flex-grow="135"
data-flex-basis="325px"
>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;/analytics.js&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;/reddit.js&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;/spotify.js&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;/tiktok.js&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;/clarity.js&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;/userleap.js&amp;#34;&lt;/span> &lt;span class="na">userleap_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;jhOvgs1si6&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;text/javascript&amp;#34;&lt;/span> &lt;span class="na">async&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;&amp;#34;&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;https://analytics.tiktok.com/i18n/pixel/events.js?sdkid=CAG18GJC77U2NHFFNB3G&amp;amp;lib=ttq&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I looked at &lt;code>/reddit.js&lt;/code>, and at the time it showed this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="o">!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">rdt&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">p&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">rdt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">sendEvent&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">sendEvent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">apply&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">p&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">arguments&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">callQueue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">arguments&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">callQueue&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">t&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">createElement&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;script&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="nx">t&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">src&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;https://www.redditstatic.com/ads/pixel.js&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">t&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">async&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">s&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getElementsByTagName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;script&amp;#39;&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">parentNode&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">insertBefore&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">t&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">s&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">})(&lt;/span>&lt;span class="nb">window&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">rdt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;init&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;t2_5u6sm01h&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">rdt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;track&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;PageVisit&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now this looks like a tracking pixel which is used to ad conversion tracking. It&amp;rsquo;s intended to track if a user were to click on an ad, then sign-up, it would count towards marketing as a success. This means it does not appear to be sending any net worth information to Reddit to build up a profile (at least in this code), it is trying to see how many people are clicking ads on Reddit, Spotify, TikTok and signing-up. However, since it is loaded as a script tag, nothing stops those companies from injecting code in your browser.&lt;/p>
&lt;p>They even call this out on their help page &lt;a class="link" href="https://help.monarchmoney.com/hc/en-us/articles/360057171991-Ad-blockers" target="_blank" rel="noopener"
>here&lt;/a>. However, there&amp;rsquo;s no reason that click attribution scripts should be loaded while signed-in to my account. At the very least they should only be loaded on user sign-up pages prior to creating an account. Even better is not to load it at all, but that&amp;rsquo;s always a battle with the marketing departments.&lt;/p>
&lt;p>To me, it shows poor security practices. Not a great look when dealing with critical financial software. I&amp;rsquo;m still signed up for the demo, but I&amp;rsquo;ll continue to update my opinion as I use it.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F02%2Fmonarch-money-and-ad-networks%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Monarch+Money+and+ad+networks" style="border:0" alt="" /></description></item><item><title>Migrating from Google Location History to OwnTracks</title><link>https://www.technowizardry.net/2024/01/migrating-from-google-location-history-to-owntracks/</link><pubDate>Mon, 01 Jan 2024 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2024/01/migrating-from-google-location-history-to-owntracks/</guid><summary>&lt;p>I&amp;rsquo;ve been slowly reducing the amount of data shared with Google. I&amp;rsquo;ve been using Google Location History since 2013. I found it really useful just because I could figure out what restaurant I went to when I was traveling or any number of things.&lt;/p>
&lt;p>I found &lt;a class="link" href="https://owntracks.org/" target="_blank" rel="noopener"
>OwnTracks&lt;/a> which was an open-source location history storage solution. It&amp;rsquo;s not nearly as polished as Google Maps where it natively integrates your location history, but step one is owning my data, step 2 can be better UIs.&lt;/p></summary><description>&lt;p>I&amp;rsquo;ve been slowly reducing the amount of data shared with Google. I&amp;rsquo;ve been using Google Location History since 2013. I found it really useful just because I could figure out what restaurant I went to when I was traveling or any number of things.&lt;/p>
&lt;p>I found &lt;a class="link" href="https://owntracks.org/" target="_blank" rel="noopener"
>OwnTracks&lt;/a> which was an open-source location history storage solution. It&amp;rsquo;s not nearly as polished as Google Maps where it natively integrates your location history, but step one is owning my data, step 2 can be better UIs.&lt;/p>
&lt;p>In this post, I&amp;rsquo;m going to walk through exactly how to get data from Google Location History and import it into OwnTracks&lt;/p>
&lt;!-- more -->
&lt;h1 id="setup-owntracks">Setup OwnTracks&lt;/h1>
&lt;p>Feel free to follow the guides &lt;a class="link" href="https://github.com/owntracks/docker-recorder" target="_blank" rel="noopener"
>here&lt;/a> to setup the OwnTracks recorder and UI.&lt;/p>
&lt;h1 id="exporting">Exporting&lt;/h1>
&lt;p>Google is moving their location history from server side to device side &lt;a class="link" href="https://www.theverge.com/2024/6/5/24172204/google-maps-delete-location-history-timeline" target="_blank" rel="noopener"
>news&lt;/a> to be ensure that they don&amp;rsquo;t have access to your location data, but as of now (August 2024) I&amp;rsquo;ve not been able to export my data on my phone.&lt;/p>
&lt;h2 id="android">Android&lt;/h2>
&lt;p>It looks like Google is trying to add support for exporting&lt;/p>
&lt;ol>
&lt;li>Settings&lt;/li>
&lt;li>Location&lt;/li>
&lt;li>Location Services&lt;/li>
&lt;li>Export timeline date&lt;/li>
&lt;li>Authenticate&lt;/li>
&lt;li>Pick a folder and then save it&lt;/li>
&lt;/ol>
&lt;p>Then if you&amp;rsquo;re lucky, the save will succeed and you can upload it to a computer. Mine, unfortunately, which failed with an error and it created a partially generated file. There appears to be an application bug where Google Maps isn&amp;rsquo;t able to handle certain timeline entries.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Shushing crash.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">FATAL EXCEPTION: lowpool[2]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Process: com.google.android.gms, PID: 28769
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">java.lang.NullPointerException: Attempt to invoke virtual method &amp;#39;java.lang.Class java.lang.Object.getClass()&amp;#39; on a null object reference
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at erfs.A(:com.google.android.gms@243333039@24.33.33 (190408-666381490):1)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at dgix.f(:com.google.android.gms@243333039@24.33.33 (190408-666381490):672)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at bopo.fx(:com.google.android.gms@243333039@24.33.33 (190408-666381490):1)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at boqa.run(:com.google.android.gms@243333039@24.33.33 (190408-666381490):66)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at epiu.run(:com.google.android.gms@243333039@24.33.33 (190408-666381490):21)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at amrj.c(:com.google.android.gms@243333039@24.33.33 (190408-666381490):50)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at amrj.run(:com.google.android.gms@243333039@24.33.33 (190408-666381490):76)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at amwv.run(:com.google.android.gms@243333039@24.33.33 (190408-666381490):8)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at java.lang.Thread.run(Thread.java:1012)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Suppressed: epjf:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at tk_trace.314-ExportOperation(Unknown Source:0)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at tk_trace.semanticlocationhistory-SemanticLocationHistoryZeroPartyClientChimeraService-ISemanticLocationHistoryZeroPartyService_7(Unknown Source:0)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="ios">iOS&lt;/h2>
&lt;ol>
&lt;li>Google Maps&lt;/li>
&lt;li>Your Timeline (three dots, top right corner)&lt;/li>
&lt;li>Location and privacy settings&lt;/li>
&lt;li>Export Timeline Data&lt;/li>
&lt;/ol>
&lt;h2 id="google-takeout">Google Takeout&lt;/h2>
&lt;p>If you haven&amp;rsquo;t migrated to on-device location tracking, then you can use the Google Takeout method:&lt;/p>
&lt;ol>
&lt;li>&lt;a class="link" href="https://takeout.google.com/" target="_blank" rel="noopener"
>https://takeout.google.com/&lt;/a>&lt;/li>
&lt;li>Deselect all, select Google location History&lt;/li>
&lt;li>Export once, file size: 4GB&lt;/li>
&lt;li>Wait for it to export, then download and extract the .zip file&lt;/li>
&lt;/ol>
&lt;h1 id="importing-into-owntracks">Importing into OwnTracks&lt;/h1>
&lt;p>I first tried to follow &lt;a class="link" href="https://blog.tiga.tech/posts/selfhost-location-history/" target="_blank" rel="noopener"
>this guide&lt;/a> which uses &lt;a class="link" href="https://github.com/owntracks/recorder/tree/master/contrib/google-import" target="_blank" rel="noopener"
>this importer&lt;/a> in the OwnTracks/recorder repository. My exported JSON file was 2GB, and it took hours to import and the MQTT importer seemed to have dropped a few records here and there.&lt;/p>
&lt;p>Instead, let&amp;rsquo;s try to import it directly and bypass MQTT. I opened up OwnTrack&amp;rsquo;s &lt;code>/store&lt;/code> folder and checked out the file format:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">ls /[...]/rec/user/phone# head 2023-01.rec
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2023-01-01T00:00:52Z * {&amp;#34;_type&amp;#34;:&amp;#34;location&amp;#34;,&amp;#34;tid&amp;#34;:&amp;#34;fl&amp;#34;,&amp;#34;tst&amp;#34;:1672531252,&amp;#34;lat&amp;#34;:47.12345,&amp;#34;lon&amp;#34;:-122.12345,&amp;#34;acc&amp;#34;:13,&amp;#34;alt&amp;#34;:123,&amp;#34;vac&amp;#34;:4}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2023-01-01T00:01:52Z * {&amp;#34;_type&amp;#34;:&amp;#34;location&amp;#34;,&amp;#34;tid&amp;#34;:&amp;#34;fl&amp;#34;,&amp;#34;tst&amp;#34;:1672531312,&amp;#34;lat&amp;#34;:47.12345,&amp;#34;lon&amp;#34;:-122.12345,&amp;#34;acc&amp;#34;:13,&amp;#34;alt&amp;#34;:123,&amp;#34;vac&amp;#34;:4}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[...]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The file was plain-text and appeared easy to generate. I created a Python function that loaded data from &lt;code>/data/Records.json&lt;/code> then generated the text files to &lt;code>/data/location/yyyy-mm-.rec&lt;/code>.&lt;/p>
&lt;p>I created a &lt;a class="link" href="https://github.com/ajacques/google-location-history-tools/" target="_blank" rel="noopener"
>Git repo&lt;/a> with my tools in a much cleaner format.&lt;/p>
&lt;p>Install it using:&lt;/p>
&lt;ol>
&lt;li>&lt;code>git clone https://github.com/ajacques/google-location-history-tools.git&lt;/code>&lt;/li>
&lt;li>&lt;code>uv sync&lt;/code>&lt;/li>
&lt;/ol>
&lt;h2 id="new-format">New Format&lt;/h2>
&lt;p>If you&amp;rsquo;ve exported your location history from your phone using the new Android or iOS export mechanism (not Google Takeout), then use this code to load the data. You&amp;rsquo;ll still need to run the code in Processing.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">uv run main.py Timeline.json --gmaps --tracker-id &lt;span class="o">[&lt;/span>two digit owntracks tracker id&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="old-takeout-format">Old Takeout Format&lt;/h2>
&lt;p>If you&amp;rsquo;ve exported your data from Google Takeout, prior to Google deprecating that mechanism, then use this code to load the data. You still need to run the code in the next section.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">uv run main.py Records.json --takeout --tracker-id &lt;span class="o">[&lt;/span>two digit owntracks tracker id&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="processing">Processing&lt;/h2>
&lt;p>Once you have your data from the above code, you need to run the following to process and save the data. Make sure to set the &lt;a class="link" href="https://owntracks.org/booklet/features/tid/" target="_blank" rel="noopener"
>&amp;ndash;tracker-id&lt;/a> to something meaningful.&lt;/p>
&lt;p>After it generates the data, find your OwnTracks data directory. The copy the contents of the &lt;code>output/&lt;/code> folder into &lt;code>/{owntracks_store_dir}/store/{user}/{device}&lt;/code> folder. If you replace any existing data you will lose data recorded from Opentracks itself, so I would recommend downloading the Google Location History data first, converting, importing, then start collecting data afterwards.&lt;/p>
&lt;h2 id="removing-extra-devices">Removing extra devices&lt;/h2>
&lt;p>When I loaded this, the history was crazy showing me moving in and out of a city instantly:
&lt;img src="https://www.technowizardry.net/2024/01/migrating-from-google-location-history-to-owntracks/images/bouncing.png"
width="432"
height="251"
srcset="https://www.technowizardry.net/2024/01/migrating-from-google-location-history-to-owntracks/images/bouncing_hu_b60d9306e69a2123.png 480w, https://www.technowizardry.net/2024/01/migrating-from-google-location-history-to-owntracks/images/bouncing_hu_77b01adb6330cd5f.png 1024w"
loading="lazy"
alt="A screenshot showing my location appearing to be in one city, then out, then back in at a physically impossible speed."
class="gallery-image"
data-flex-grow="172"
data-flex-basis="413px"
>&lt;/p>
&lt;p>Turns out, multiple devices on my accounts were reporting locations so it appeared that I kept moving between two cities. Easy enough to exclude the device.&lt;/p>
&lt;p>The following script produces a chart that shows when devices are reporting their location:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">df_gps&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sort_values&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">by&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;timestamp&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">inplace&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">devices&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">df_gps&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;deviceTag&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">unique&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">figure&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">figsize&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">colors&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cm&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">viridis_r&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="n">i&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">devices&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">devices&lt;/span>&lt;span class="p">))])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">i&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">device&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">enumerate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">devices&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">device_data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">df_gps&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">df_gps&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;deviceTag&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">device&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">first_year&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">device_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;timestamp&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">year&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">min&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">last_year&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">device_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;timestamp&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">year&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">max&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">middle_year&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">first_year&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">last_year&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">hlines&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">y&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">xmin&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">first_year&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">xmax&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">last_year&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">color&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">colors&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">linewidth&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">middle_year&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mf">0.25&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">i&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">verticalalignment&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;center&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">fontsize&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">xlabel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Year&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ylabel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Device&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Device Reporting Years&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">grid&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">show&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>With this I can identify which device is reporting in parallel. In my case, it&amp;rsquo;s the 10th device that&amp;rsquo;s reporting in parallel with my phone. Note that I&amp;rsquo;ve replaced the device ids, yours will be a long random number:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/01/migrating-from-google-location-history-to-owntracks/images/device-years.png"
width="609"
height="391"
srcset="https://www.technowizardry.net/2024/01/migrating-from-google-location-history-to-owntracks/images/device-years_hu_ca8475087fbbb365.png 480w, https://www.technowizardry.net/2024/01/migrating-from-google-location-history-to-owntracks/images/device-years_hu_49c7439cd105c3aa.png 1024w"
loading="lazy"
alt="Chart showing the years that a device is reporting data. Most devices are only reporting for a brief amount of time, but one device is reporting 2018-2024 along side other devices. This is my old tablet."
class="gallery-image"
data-flex-grow="155"
data-flex-basis="373px"
>&lt;/p>
&lt;p>From there, I filtered out this device from the location history&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+excluded_devices = [1234510]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> owntracks = df_gps.rename(columns={&amp;#39;latitudeE7&amp;#39;: &amp;#39;lat&amp;#39;, &amp;#39;longitudeE7&amp;#39;: &amp;#39;lon&amp;#39;, &amp;#39;accuracy&amp;#39;: &amp;#39;acc&amp;#39;, &amp;#39;altitude&amp;#39;: &amp;#39;alt&amp;#39;, &amp;#39;verticalAccuracy&amp;#39;: &amp;#39;vac&amp;#39;})
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> owntracks[&amp;#39;tst&amp;#39;] = (owntracks[&amp;#39;timestamp&amp;#39;].astype(int) / 10**9)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+matched = owntracks[&amp;#39;deviceTag&amp;#39;].isin(excluded_devices)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+if len(owntracks[matched]) == 0:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ print(&amp;#34;Didn&amp;#39;t find matching deviceTags. Double check your excluded_devices&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ return
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+owntracks = owntracks[~matched]
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And the history looks a lot better:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2024/01/migrating-from-google-location-history-to-owntracks/images/bounce-fixed.png"
width="517"
height="526"
srcset="https://www.technowizardry.net/2024/01/migrating-from-google-location-history-to-owntracks/images/bounce-fixed_hu_792074c37d7b5516.png 480w, https://www.technowizardry.net/2024/01/migrating-from-google-location-history-to-owntracks/images/bounce-fixed_hu_483d5497d893faf.png 1024w"
loading="lazy"
alt="Screenshot of my location history showing me staying within a single city as expected."
class="gallery-image"
data-flex-grow="98"
data-flex-basis="235px"
>&lt;/p>
&lt;h1 id="adding-new-data">Adding new data&lt;/h1>
&lt;p>Now, once you have your historical data, what do you do for new data moving forward? There&amp;rsquo;s a lot of options depending on personal preference including:&lt;/p>
&lt;ul>
&lt;li>Using the OwnTracks &lt;a class="link" href="https://owntracks.org/" target="_blank" rel="noopener"
>mobile apps&lt;/a>&lt;/li>
&lt;li>Copying location from Home Assistant &lt;a class="link" href="https://blog.tiga.tech/posts/selfhost-location-history/" target="_blank" rel="noopener"
>like here&lt;/a>. This is the method that I use.&lt;/li>
&lt;/ul>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>OwnTracks is an open-source system to store your location history. You can import existing history from Google Maps, then report new data into OwnTracks and have a localized copy of your location history.&lt;/p>
&lt;h1 id="updates">Updates&lt;/h1>
&lt;ul>
&lt;li>2024-05-04 - Corrected a few bugs in the Python code&lt;/li>
&lt;li>2024-07-15 - Automatically create output folder in Python&lt;/li>
&lt;li>2024-08-02 - Updated the timestamp parsing logic again&lt;/li>
&lt;li>2024-08-28 - Updated downloading section to include Android and iOS examples (thanks to reader contribution)&lt;/li>
&lt;li>2024-11-24 - Clarified sections and added more validation around excluded devices&lt;/li>
&lt;li>2026-03-10 - Added link to GitHub &lt;a class="link" href="https://github.com/ajacques/google-location-history-tools/" target="_blank" rel="noopener"
>repo&lt;/a>&lt;/li>
&lt;/ul>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2024%2F01%2Fmigrating-from-google-location-history-to-owntracks%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Migrating+from+Google+Location+History+to+OwnTracks" style="border:0" alt="" /></description></item><item><title>Content-Security-Policy for Home Assistant</title><link>https://www.technowizardry.net/2023/12/content-security-policy-for-home-assistant/</link><pubDate>Sun, 31 Dec 2023 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2023/12/content-security-policy-for-home-assistant/</guid><summary>&lt;p>Content-Security-Policy is a security feature (&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP" target="_blank" rel="noopener"
>MDN Web Docs&lt;/a>) in modern web browsers that restricts the kind of content that helps to protect against certain types of attacks, such as Cross-Site Scripting (XSS) attacks. Since my Home Assistant has significant access to my home network and is reasonably well-known, I wanted to take some steps to protect against malicious actors using XSS or other injection attacks to taking over my network. In addition, there have been a few different CVEs (&lt;a class="link" href="https://www.home-assistant.io/security" target="_blank" rel="noopener"
>HA Security Disclosures&lt;/a>) in Home Assistant that allowed for XSS),&lt;/p></summary><description>&lt;p>Content-Security-Policy is a security feature (&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP" target="_blank" rel="noopener"
>MDN Web Docs&lt;/a>) in modern web browsers that restricts the kind of content that helps to protect against certain types of attacks, such as Cross-Site Scripting (XSS) attacks. Since my Home Assistant has significant access to my home network and is reasonably well-known, I wanted to take some steps to protect against malicious actors using XSS or other injection attacks to taking over my network. In addition, there have been a few different CVEs (&lt;a class="link" href="https://www.home-assistant.io/security" target="_blank" rel="noopener"
>HA Security Disclosures&lt;/a>) in Home Assistant that allowed for XSS),&lt;/p>
&lt;p>In this blog post, I walk through the steps I took to design a CSP header and fix bugs I found along the way.&lt;/p>
&lt;!-- more -->
&lt;p>CSP won&amp;rsquo;t protect against all attacks, but it&amp;rsquo;s one tool as part of a layered security system to protect your system. CSP works by explicitly identifying all sources of JavaScript, CSS, XHR requests, etc. and explicitly listing them in an HTTP response header sent to the browser.&lt;/p>
&lt;h1 id="my-stack">My Stack&lt;/h1>
&lt;p>I run Home Assistant inside Kubernetes which is fronted by &lt;a class="link" href="https://kubernetes.github.io/ingress-nginx/" target="_blank" rel="noopener"
>ingress-nginx&lt;/a>. This is important because I attach the CSP header into the response using ingress-nginx. Home Assistant itself does not expose a config value to set this header in the &lt;a class="link" href="https://www.home-assistant.io/integrations/http/" target="_blank" rel="noopener"
>http integration&lt;/a>. Using Kubernetes isn&amp;rsquo;t required,&lt;/p>
&lt;h1 id="configure-ingress-nginx">Configure ingress-nginx&lt;/h1>
&lt;p>First, we need to update ingress-nginx to allow us to define headers by updating the &lt;a class="link" href="https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#global-allowed-response-headers" target="_blank" rel="noopener"
>global-allowed-response-headers&lt;/a> setting in the ConfigMap.&lt;/p>
&lt;p>If you&amp;rsquo;re using Helm, the setting is:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">controller&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">global-allowed-response-headers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Content-Security-Policy&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="a-broken-header">A broken header&lt;/h1>
&lt;p>If you&amp;rsquo;re using Kubernetes like me, then go to the HA Ingress and start setting the header like below. Right now, the headers are defaults, but we&amp;rsquo;ll fill those in.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Content-Security-Policy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-src &amp;#39;self&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">hass-ingress-headers-full&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl"> apiVersion: networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> kind: Ingress
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> metadata:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> annotations:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> # AuthN/Z handled by HA itself
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> nginx.ingress.kubernetes.io/enable-global-auth: &amp;#34;false&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ nginx.ingress.kubernetes.io/custom-headers: smarthome/hass-ingress-headers-full
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span> name: homeassistant
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> namespace: smarthome
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> spec:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ingressClassName: external-nginx
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> rules:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - host: ha.example.com
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> http:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> paths:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - backend:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> service:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: homeassistant-headless
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> port:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> number: 8123
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> path: /
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> pathType: Prefix
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> tls:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - hosts:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - ha.example.com
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>A vanilla nginx config will look like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl"> server {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> listen 80;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> server_name ha.example.com;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ add_header Content-Security-Policy &amp;#34;default-src &amp;#39;self&amp;#39;&amp;#34;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> location / {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> proxy_pass foo:8123;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Traefik can use &lt;a class="link" href="https://doc.traefik.io/traefik/middlewares/http/headers/" target="_blank" rel="noopener"
>this&lt;/a>.&lt;/p>
&lt;h1 id="a-basic-header">A basic header&lt;/h1>
&lt;p>To start, I opened up my browser dev tools, reloaded the page, navigated around, then noted down all the different URLs that were loaded. Then I went to a &lt;a class="link" href="https://report-uri.com/home/generate" target="_blank" rel="noopener"
>CSP Generator&lt;/a>. Most things load from the same HA endpoint, but things like maps and HCAS will load from external origins which must be explicitly permitted.&lt;/p>
&lt;p>The header will look something like this:&lt;/p>
&lt;ul>
&lt;li>&lt;code>default-src&lt;/code>/Default Source - Set this to &lt;code>'self'&lt;/code> as a fallback grant if a request doesn&amp;rsquo;t match another directive.&lt;/li>
&lt;li>&lt;code>script-src&lt;/code>/Script Source - Set this to &lt;code>'self' 'unsafe-inline' 'unsafe-eval'&lt;/code>. This directive is the most important because it scripts are what make XSS attacks useful (more on the risks below.)&lt;/li>
&lt;li>&lt;code>style-src&lt;/code>/Style Source - Set this to &lt;code>'self' 'unsafe-inline'&lt;/code>&lt;/li>
&lt;li>&lt;code>image-src&lt;/code>/Image Source - Set this to &lt;code>'self' data: https://basemaps.cartocdn.com https://*.basemaps.cartocdn.com https://brands.home-assistant.io https://raw.githubusercontent.com&lt;/code>. &lt;code>cartocdn.com&lt;/code> is used for Home Assistant&amp;rsquo;s maps, &lt;code>brands.home-assistant.io&lt;/code> is used to load the integration icons, and &lt;code>raw.githubusercontent.com&lt;/code> is used in HACS to view images for repositories. Make sure to add any other URLs that you&amp;rsquo;ve identified in the dev tools For example, I use the &lt;a class="link" href="https://github.com/ajacques/hass-tryfi" target="_blank" rel="noopener"
>hass-tryfi&lt;/a> integration, so I needed to add &lt;code>https://barkinglabs-media.s3.amazonaws.com&lt;/code> to this header.&lt;/li>
&lt;li>&lt;code>connect-src&lt;/code>/Connnect Source - Set this to &lt;code>'self' data: https://brands.home-assistant.io https://raw.githubusercontent.com&lt;/code>. Interestingly, since HA uses a Service Worker the initial request to populate the Service Worker cache is considered a connect, so if an image isn&amp;rsquo;t loading, try adding it to the &lt;code>connect-src&lt;/code> too.&lt;/li>
&lt;li>&lt;code>frame-ancestors&lt;/code>/Frame Ancestors - Set this to &lt;code>'none'&lt;/code>. Home Assistant shouldn&amp;rsquo;t be embedded in an iframe. Replaces the X-Frame-Options header, see &lt;a class="link" href="https://nvd.nist.gov/vuln/detail/CVE-2023-41897" target="_blank" rel="noopener"
>CVE-2023-41897&lt;/a> and &lt;a class="link" href="https://github.com/home-assistant/core/security/advisories/GHSA-935v-rmg9-44mw" target="_blank" rel="noopener"
>GHSA-935v-rmg9-44mw&lt;/a>.&lt;/li>
&lt;li>&lt;code>report-uri&lt;/code>/Report URI - Optional. I use the &lt;a class="link" href="https://www.home-assistant.io/integrations/sentry/" target="_blank" rel="noopener"
>Sentry integration&lt;/a> and collect CSP reports to investigate issues using this. Example: &lt;code>https://sentry.example.com/api/12345/security/?sentry_key=abcdef&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Add the CSP header to the request router config, reload it, then refresh the browser page with the dev tools open. If anything does not load correctly, update the CSP header as needed. Note, that if you using HACS to load custom integrations, the repository pages will be intentionally partially broken. HACS shows the README.md from the GitHub repository, and often times developers include images from other origins, such as &lt;code>img.shields.io&lt;/code> as &amp;ldquo;shield icons&amp;rdquo; that show things like below:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2023/12/content-security-policy-for-home-assistant/images/github-badges.png"
width="683"
height="148"
srcset="https://www.technowizardry.net/2023/12/content-security-policy-for-home-assistant/images/github-badges_hu_84fca8c6a813aeaa.png 480w, https://www.technowizardry.net/2023/12/content-security-policy-for-home-assistant/images/github-badges_hu_9cf87ad235b234d4.png 1024w"
loading="lazy"
alt="A screenshot of a few different small badge images showing the latest GitHub release version, how many users are on Discord, HACS support, and number of sponsors."
class="gallery-image"
data-flex-grow="461"
data-flex-basis="1107px"
>&lt;/p>
&lt;p>I intentionally didn&amp;rsquo;t add this for privacy reasons and they don&amp;rsquo;t add a lot of value. However, if you want to see them, just add &lt;code>https://img.shields.io&lt;/code> to your &lt;code>image-src&lt;/code>.&lt;/p>
&lt;h1 id="analysis-of-my-header">Analysis of my header&lt;/h1>
&lt;p>Home Assistant is intentionally quite flexible in what JS code it can run. Since you can load arbitrary cards through HACS which bring in their own HTML, CSS, JS, and developers sometimes use inline &lt;code>&amp;lt;script&amp;gt;foo();&amp;lt;/script&amp;gt;&lt;/code> tags, I had to enable &lt;code>unsafe-inline&lt;/code>.
This is a common source of drive-by XSS attacks and would still leave me susceptible to things like &lt;a class="link" href="https://nvd.nist.gov/vuln/detail/CVE-2017-16782" target="_blank" rel="noopener"
>CVE-2017-16782&lt;/a> and &lt;a class="link" href="https://nvd.nist.gov/vuln/detail/CVE-2023-41896" target="_blank" rel="noopener"
>CVE-2023-41896&lt;/a> since you could still inject JavaScript through incorrectly escaped content. Unescaped content isn&amp;rsquo;t the only source of XSS attacks though.&lt;/p>
&lt;p>The next layer of defense is what I set for &lt;code>image-src&lt;/code>, &lt;code>style-src,&lt;/code> and &lt;code>connect-src&lt;/code> which are the mechanisms to then exfiltrate data to a malicious actor. Any origin listed there should be considered relevant trusted. Don&amp;rsquo;t add &lt;code>*&lt;/code> or anything.&lt;/p>
&lt;h1 id="problems-with-iframes-in-firefox">Problems with iframes in Firefox&lt;/h1>
&lt;p>I&amp;rsquo;m using the &lt;a class="link" href="https://github.com/Makin-Things/weather-radar-card" target="_blank" rel="noopener"
>weather-radar-card&lt;/a> to show precipitation clouds. This required adding &lt;code>https://tilecache.rainviewer.com&lt;/code> to my &lt;code>image-src&lt;/code> directive. However after setting the CSP header, it would only show the first time I loaded the page, but not if it was in the cache. I had to hard refresh (shift-F5) to get it to load. Otherwise, it&amp;rsquo;d show an empty white box, like below:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2023/12/content-security-policy-for-home-assistant/images/image.png"
width="622"
height="765"
srcset="https://www.technowizardry.net/2023/12/content-security-policy-for-home-assistant/images/image_hu_958ff38c11d9b28b.png 480w, https://www.technowizardry.net/2023/12/content-security-policy-for-home-assistant/images/image_hu_e8ad13911e5ac1c4.png 1024w"
loading="lazy"
alt="A screenshot showing that the weather-radar-card is not loading. It shows a white box."
class="gallery-image"
data-flex-grow="81"
data-flex-basis="195px"
>&lt;/p>
&lt;p>It took me awhile to figure this out, because it only seemed to happen in Firefox, but looking at the HTML DOM, it looked like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">iframe&lt;/span> &lt;span class="na">srcdoc&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;&amp;lt;html&amp;gt;&amp;lt;script src=&amp;#34;&lt;/span>&lt;span class="err">/&lt;/span>&lt;span class="na">local&lt;/span>&lt;span class="err">/&lt;/span>&lt;span class="na">community&lt;/span>&lt;span class="err">/&lt;/span>&lt;span class="na">weather-radar-card&lt;/span>&lt;span class="err">/&lt;/span>&lt;span class="na">leaflet&lt;/span>&lt;span class="err">.&lt;/span>&lt;span class="na">js&lt;/span>&lt;span class="err">&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>According to the &lt;a class="link" href="https://stackoverflow.com/questions/67849788/which-csp-is-enforced-on-an-iframe-created-with-a-globally-unique-identifier-s" target="_blank" rel="noopener"
>spec&lt;/a>, iframe srcdocs are supposed to inherit the parent&amp;rsquo;s Origin and thus use the same header we set above, but for some reason, it was enforcing the header, but not using the same Origin. Even though we set &lt;code>'self'&lt;/code>, it was ignoring that and thought it was different. To fix this, I added &lt;code>ha.example.com&lt;/code> to my &lt;code>script-src&lt;/code> and &lt;code>image-src&lt;/code>.&lt;/p>
&lt;p>After that, the card started working:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2023/12/content-security-policy-for-home-assistant/images/working.png"
width="585"
height="705"
srcset="https://www.technowizardry.net/2023/12/content-security-policy-for-home-assistant/images/working_hu_c8c578fec9ea71a3.png 480w, https://www.technowizardry.net/2023/12/content-security-policy-for-home-assistant/images/working_hu_9a41f87f1e2f032a.png 1024w"
loading="lazy"
alt="A screen shot showing the working radar card"
class="gallery-image"
data-flex-grow="82"
data-flex-basis="199px"
>&lt;/p>
&lt;h1 id="strict-login-header">Strict Login Header&lt;/h1>
&lt;p>Just for fun (this is optional), I added a second restricted header just for the login page to reduce my exposure even more.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Content-Security-Policy:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> default-src &amp;#39;self&amp;#39; data:;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> script-src &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39;;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> style-src &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39;;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> frame-ancestors &amp;#39;none&amp;#39;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="all-together">All Together&lt;/h1>
&lt;p>Now, putting it all together, my Content-Security-Policy looks like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Content-Security-Policy:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> default-src &amp;#39;self&amp;#39; data:;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> script-src &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39; &amp;#39;unsafe-eval&amp;#39; https://ha.example.com;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> style-src &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39;;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> img-src &amp;#39;self&amp;#39; data: https://ha.example.com https://barkinglabs-media.s3.amazonaws.com https://basemaps.cartocdn.com https://*.basemaps.cartocdn.com https://brands.home-assistant.io https://tilecache.rainviewer.com https://raw.githubusercontent.com;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> connect-src &amp;#39;self&amp;#39; https://brands.home-assistant.io https://raw.githubusercontent.com;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> frame-ancestors &amp;#39;none&amp;#39;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And inserting that into my K8s Ingress:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;span class="lnt">68
&lt;/span>&lt;span class="lnt">69
&lt;/span>&lt;span class="lnt">70
&lt;/span>&lt;span class="lnt">71
&lt;/span>&lt;span class="lnt">72
&lt;/span>&lt;span class="lnt">73
&lt;/span>&lt;span class="lnt">74
&lt;/span>&lt;span class="lnt">75
&lt;/span>&lt;span class="lnt">76
&lt;/span>&lt;span class="lnt">77
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Content-Security-Policy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> default-src &amp;#39;self&amp;#39; data:; script-src
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39; &amp;#39;unsafe-eval&amp;#39; https://ha.example.com; style-src &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> img-src &amp;#39;self&amp;#39; data: https://ha.example.com https://barkinglabs-media.s3.amazonaws.com
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> https://basemaps.cartocdn.com https://*.basemaps.cartocdn.com
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> https://brands.home-assistant.io https://tilecache.rainviewer.com
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> https://raw.githubusercontent.com; connect-src &amp;#39;self&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> https://brands.home-assistant.io https://raw.githubusercontent.com;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> frame-ancestors &amp;#39;none&amp;#39;;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">hass-ingress-headers-full&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Ingress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># AuthN/Z handled by HA itself&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nginx.ingress.kubernetes.io/enable-global-auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;false&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nginx.ingress.kubernetes.io/custom-headers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome/hass-ingress-headers-full&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">homeassistant&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingressClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">external-nginx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ha.example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">http&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">paths&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">backend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">homeassistant-headless&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">number&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8123&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathType&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Prefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">ha.example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Content-Security-Policy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> default-src &amp;#39;self&amp;#39; data:; script-src
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39;; style-src &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39;;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">hass-ingress-headers-login&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Ingress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nginx.ingress.kubernetes.io/enable-global-auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;false&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nginx.ingress.kubernetes.io/custom-headers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome/hass-ingress-headers-login&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">homeassistant-login&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingressClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">external-nginx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ha.example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">http&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">paths&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">backend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">homeassistant-headless&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">number&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8123&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/auth/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathType&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Prefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">ha.example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">home-wildcard&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>Content-Security-Policy is a useful security feature that mitigates many (but not all) types of XSS attacks in the browser. It can be tricky to set in Home Assistant because HA uses a variety of different features like service workers, iframes, and allows arbitrary JS to be loaded by design through custom cards. However, this makes it more important to add a CSP header to protect as much as possible.&lt;/p>
&lt;h1 id="updates">Updates&lt;/h1>
&lt;ul>
&lt;li>2025-04-02 - ingress-nginx now considers the snippets to be &lt;a class="link" href="https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations-risk/" target="_blank" rel="noopener"
>a high risk&lt;/a> annotation and it&amp;rsquo;s not enabled by default. I&amp;rsquo;ve updated the examples to use the new method&lt;/li>
&lt;/ul>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2023%2F12%2Fcontent-security-policy-for-home-assistant%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Content-Security-Policy+for+Home+Assistant" style="border:0" alt="" /></description></item><item><title>Auto disable Kubernetes' service LB NodePorts</title><link>https://www.technowizardry.net/2023/10/auto-disable-kubernetes-service-lb-nodeports/</link><pubDate>Sun, 08 Oct 2023 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2023/10/auto-disable-kubernetes-service-lb-nodeports/</guid><summary>&lt;p>In a &lt;a class="link" href="https://www.technowizardry.net/2021/12/why-is-kubernetes-opening-random-ports/" >previous post&lt;/a>, I noticed that all my Kubernetes services with &lt;code>type=LoadBalancer&lt;/code> were exposing some internal services as NodePorts which meant that I might be exposing internal services to the Internet at high ports. I was running Kubernetes directly on my dedicated servers and not behind a load balancer. Kubernetes expected everybody to sit behind a LB which often times required a NodePort.&lt;/p>
&lt;p>The solution was to set the Service &lt;code>spec.allocateLoadBalancerNodePorts&lt;/code> value to &lt;code>false&lt;/code> when the service is created. This works if I can set it while I create the Service, however Helm based templates often wouldn&amp;rsquo;t allow me to set this and once it was set to true and the node port was allocated it was difficult to deallocate the NodePort.&lt;/p>
&lt;p>In this post, I walk through using a Kubernetes mutating webhook to automatically set the value for all Services.&lt;/p></summary><description>&lt;p>In a &lt;a class="link" href="https://www.technowizardry.net/2021/12/why-is-kubernetes-opening-random-ports/" >previous post&lt;/a>, I noticed that all my Kubernetes services with &lt;code>type=LoadBalancer&lt;/code> were exposing some internal services as NodePorts which meant that I might be exposing internal services to the Internet at high ports. I was running Kubernetes directly on my dedicated servers and not behind a load balancer. Kubernetes expected everybody to sit behind a LB which often times required a NodePort.&lt;/p>
&lt;p>The solution was to set the Service &lt;code>spec.allocateLoadBalancerNodePorts&lt;/code> value to &lt;code>false&lt;/code> when the service is created. This works if I can set it while I create the Service, however Helm based templates often wouldn&amp;rsquo;t allow me to set this and once it was set to true and the node port was allocated it was difficult to deallocate the NodePort.&lt;/p>
&lt;p>In this post, I walk through using a Kubernetes mutating webhook to automatically set the value for all Services.&lt;/p>
&lt;h2 id="what-are-webhooks">What are webhooks?&lt;/h2>
&lt;p>The &lt;a class="link" href="https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/" target="_blank" rel="noopener"
>Kubernetes admission webhooks&lt;/a> are a Kubernetes feature that enables you to process any change made in the cluster. There are two flavors:&lt;/p>
&lt;ul>
&lt;li>Validating - Checks a pending resource change and returns whether it should be denied or accepted&lt;/li>
&lt;li>Mutating - Checks a pending resource change and possibly changes the values in that resource.&lt;/li>
&lt;/ul>
&lt;p>Webhooks get registered as a Kubernetes resource, then are called automatically.&lt;/p>
&lt;h2 id="options">Options&lt;/h2>
&lt;p>There&amp;rsquo;s a few different options:&lt;/p>
&lt;ul>
&lt;li>Manually create a webhook and implement the API&lt;/li>
&lt;li>&lt;a class="link" href="https://www.openpolicyagent.org/docs/latest/kubernetes-introduction/" target="_blank" rel="noopener"
>OpenPolicyAgent Gatekeeper&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://kyverno.io" target="_blank" rel="noopener"
>Kyverno&lt;/a>&lt;/li>
&lt;li>KubeWarden&lt;/li>
&lt;/ul>
&lt;p>I first experimented with OPA Gatekeeper, but found the authoring and policy registration process to be complicated. KubeWarden was another option, but overly complex and I just needed some basic policies. Kyverno looked perfect. I could easily define policies using just YAML. Far simpler than OPA&amp;rsquo;s WebAssembly project. Thus I selected Kyverno.&lt;/p>
&lt;h2 id="deploying-kyverno">Deploying Kyverno&lt;/h2>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># values.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Cleanups are currently considered alpha. Minimize the deployment size&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">cleanupController&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">reportsController&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">features&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">admissionReports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># This next feature is optional&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Set it to true to allow fail-open if Kyverno is offline&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># In my home lab, I&amp;#39;m using this for house-keeping, not security&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># So I want to be able to gracefully handle errors.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># https://kyverno.io/docs/installation/#security-vs-operability&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">forceFailurePolicyIgnore&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policyReports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">helm repo add
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install --create-namespace -n kyverno kyverno/kyverno kyverno -f values.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="authoring-the-policy">Authoring the Policy&lt;/h2>
&lt;p>I defined the following policy and uploaded it to my cluster. Anytime a &lt;code>LoadBalancer&lt;/code> &lt;code>Service&lt;/code> is created or updated, the following policy will apply and disable NodePorts.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kyverno.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">disable-lb-node-port&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mutateExistingOnPolicyUpdate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">match&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kinds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">preconditions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">all&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;{{ request.object.spec.type }}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Equals&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LoadBalancer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mutate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">patchStrategicMerge&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allocateLoadBalancerNodePorts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mutate-loadbalancer-service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">validationFailureAction&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Enforce&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="existing-resources">Existing Resources&lt;/h2>
&lt;p>Any service created prior to the policy won&amp;rsquo;t be updated automatically. Even if we toggle the &lt;code>allocateLoadBalancerNodePorts&lt;/code> to &lt;code>false&lt;/code>, and existing nodePorts will remain allocated and accessible to the Internet.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgres-lb&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">datastore&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allocateLoadBalancerNodePorts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">clusterIP&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10.43.23.122&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">clusterIPs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="m">10.43.23.122&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">externalTrafficPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">healthCheckNodePort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">32439&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">internalTrafficPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Cluster&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ipFamilies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">IPv4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ipFamilyPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">SingleStack&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sql&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5432&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nodePort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30313&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5432&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cnpg.io/cluster&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgres&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sessionAffinity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">None&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LoadBalancer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The only way to fix this is to remove the nodePort field and rename each port name temporarily to get Kubernetes to clear it out.&lt;/p>
&lt;p>Unfortunately as of Kubernetes 1.26 it&amp;rsquo;s still not possible to disable the &lt;code>healthCheckNodePort&lt;/code> if you&amp;rsquo;re using &lt;code>externalTrafficPolicy=Local&lt;/code>&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>Using Kyverno, I showed how to automatically disable unintended node ports created by Load Balancers.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2023%2F10%2Fauto-disable-kubernetes-service-lb-nodeports%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Auto+disable+Kubernetes%27+service+LB+NodePorts" style="border:0" alt="" /></description></item><item><title>Improving bad on-call with the Snowball Effect</title><link>https://www.technowizardry.net/2023/06/improving-bad-on-call-with-the-snowball-effect/</link><pubDate>Sun, 25 Jun 2023 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2023/06/improving-bad-on-call-with-the-snowball-effect/</guid><summary>&lt;p>I&amp;rsquo;ve worked on several different teams over the past 8 years I&amp;rsquo;ve worked at Amazon. Each one of them had on-call in which the engineers were on-call to keep the system running 24/7 for a week. If something broke at 2am, they&amp;rsquo;d get paged to fix it.&lt;/p>
&lt;p>Now, Amazon&amp;rsquo;s a big company. On-call varied quite a bit. Some teams had more ops load, others had barely any. I had my fair share of weeks with lots of tickets, but usually I sought out teams where it was more manageable. However, those engineers frequently struggled to get anywhere, playing a bit of on-call hot potato with the next on-call. Sadly, Amazon largely did not leverage SREs or dedicated support groups except for the most critical systems. I do wish they would have leveraged them.&lt;/p></summary><description>&lt;p>I&amp;rsquo;ve worked on several different teams over the past 8 years I&amp;rsquo;ve worked at Amazon. Each one of them had on-call in which the engineers were on-call to keep the system running 24/7 for a week. If something broke at 2am, they&amp;rsquo;d get paged to fix it.&lt;/p>
&lt;p>Now, Amazon&amp;rsquo;s a big company. On-call varied quite a bit. Some teams had more ops load, others had barely any. I had my fair share of weeks with lots of tickets, but usually I sought out teams where it was more manageable. However, those engineers frequently struggled to get anywhere, playing a bit of on-call hot potato with the next on-call. Sadly, Amazon largely did not leverage SREs or dedicated support groups except for the most critical systems. I do wish they would have leveraged them.&lt;/p>
&lt;p>Here&amp;rsquo;s my strategy that I employed when I joined a new team.&lt;/p>
&lt;p>&lt;strong>The Situation&lt;/strong>&lt;/p>
&lt;p>On-call was generally structured as a week long duty that rotated through engineers on the team. On the on duty week, the on-call engineer would be responsible for keeping the system operational and responding to any emergent issues. Tickets were cut based on impact to the business and customers. Sev2s were cut any time there was a customer impacting or soon to be impacting issue, though sometimes they were cut too aggressively and the engineer would either downgrade it or it would be fixed after responding.&lt;/p>
&lt;p>The ticket queues would often times have a lot of tickets in it just waiting for a response and nobody would respond because they didn&amp;rsquo;t have time. Things just stayed bad and engineers would complain about OE load.&lt;/p>
&lt;p>Then during the weekly OE hand-off meeting, the previous on-call would go through the list of problems, scroll through dashboards, and give out action items. It was generally a state of despair.&lt;/p>
&lt;p>Let&amp;rsquo;s walk through the strategy I employed to improve my teams.&lt;/p>
&lt;h2 id="the-strategy">The Strategy&lt;/h2>
&lt;p>&lt;strong>Accomplishments&lt;/strong>&lt;/p>
&lt;p>To instill ownership into the team, I introduced the goal that every on-call would accomplish one thing to make the next on-call&amp;rsquo;s life easier. Each on-call was to present their accomplishment during the hand-off. It could be big like building a new ops tool, it could be small like fixing an alarm that was too noisy, but I clearly drew the line between things that were just work as normal vs actually innovative.&lt;/p>
&lt;p>Examples of things that &lt;strong>wouldn&amp;rsquo;t&lt;/strong> qualify, but still important to do:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>Resolving a ticket&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Executing a manual deployment&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Responding to a customer request&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>Examples of &lt;strong>good&lt;/strong> accomplishments:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>Fixing an alarm that was too noisy&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Taking steps towards CI/CD by introducing better pipeline approval workflows&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Spending time to root cause a reoccurring issue and proposing a permanent fix&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>Sometimes, engineers claimed they didn&amp;rsquo;t have time to do this because there were too many issues. This showed good ownership, they wanted to fix all the problems, but that just doesn&amp;rsquo;t scale. Closing ticket rarely reduces the number of tickets, and eventually as your service scales, you just get more and more tickets. I&amp;rsquo;ve been there, seen that, but I aligned with the Team Manager and team that it was okay to fix one fewer issue as long as you instead improved one thing.&lt;/p>
&lt;p>At first it was tough for on-calls to get this mindset, but week by week, they&amp;rsquo;d slowly fix things and bit-by-bit common pain-points would be reduced further freeing up time.&lt;/p>
&lt;p>&lt;strong>Reduce Aggressive Paging&lt;/strong>&lt;/p>
&lt;p>I signed up to receive every an email alert every time an engineer got paged and reviewed each one. I questioned: was there really a problem here? Did you really need to get paged at 2 o&amp;rsquo; clock in the morning? Did you do anything? Could it have waited until 9am? How do we prevent it from happening again?&lt;/p>
&lt;p>Sometimes the team would say &amp;ldquo;but this alarm could tell us something&amp;rsquo;s wrong.&amp;rdquo; How many times has it caught an actual issue? Are there more direct measurements of a problem. For example, an alarm on number of 4xx response codes on an HTTP server sounds like a good thing to measure, but there&amp;rsquo;s all kinds of challenges. Do you use a percentage or a raw count (obviously percentages), but what happens if you have a low traffic API that gets a few transactions per second and somebody refreshes on a 404 page? Boom engineer paged. Even if an alarm could raise a problem, if it&amp;rsquo;s not directly measuring a problem and in the past it&amp;rsquo;s largely been annoying, it&amp;rsquo;s better to be pragmatic and just change thresholds or even disable alarms until you&amp;rsquo;re in a better state.&lt;/p>
&lt;p>Use any situation where an engineer got paged to aggressively change thresholds or remove alarms until only high confidence problems are waking engineers up at 2 in the morning.&lt;/p>
&lt;p>&lt;strong>No doom-scrolling in the OE hand-off&lt;/strong>&lt;/p>
&lt;p>Poorly structured OE hand-off meetings often devolve into scrolling through dashboards pointing to things and saying &amp;ldquo;oh what&amp;rsquo;s that spike?&amp;rdquo; or scrolling through issues that need to be fixed. This quickly wastes the time of the entire team who, especially on conference calls, check out and start doing other things. Everybody knows there are problems.&lt;/p>
&lt;p>Instead, the hand-off meetings should focus on what are the key problems that the on-call is facing and what steps should the next on-call take. At the start on their own time, the on-call should review dashboards and the items from the previous rotation and identify one or two goals they&amp;rsquo;ll get done that week.&lt;/p>
&lt;h2 id="conclusion">&lt;strong>Conclusion&lt;/strong>&lt;/h2>
&lt;p>This is not a comprehensive list of everything that will immediately improve operations, however by taking a few steps here and there, your team will eventually get better. It&amp;rsquo;s like a snowball, a few small things here, then bigger and bigger problems, eventually you&amp;rsquo;ve got a snowman.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2023%2F06%2Fimproving-bad-on-call-with-the-snowball-effect%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Improving+bad+on-call+with+the+Snowball+Effect" style="border:0" alt="" /></description></item><item><title>Technical Diagrams - Stop using cloud logos</title><link>https://www.technowizardry.net/2023/02/technical-diagrams-stop-using-cloud-logos/</link><pubDate>Fri, 24 Feb 2023 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2023/02/technical-diagrams-stop-using-cloud-logos/</guid><summary>&lt;p>Quick, what is this diagram trying to show?&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2023/02/technical-diagrams-stop-using-cloud-logos/images/aws-variant.png"
width="898"
height="388"
srcset="https://www.technowizardry.net/2023/02/technical-diagrams-stop-using-cloud-logos/images/aws-variant_hu_e1212403f7df31eb.png 480w, https://www.technowizardry.net/2023/02/technical-diagrams-stop-using-cloud-logos/images/aws-variant_hu_58ae38f59ea33516.png 1024w"
loading="lazy"
alt="An architecture diagram using AWS service logos. It intentionally does not use labels to force the reader to guess what the strangely colored logos are supposed to represent."
class="gallery-image"
data-flex-grow="231"
data-flex-basis="555px"
>&lt;/p></summary><description>&lt;p>Quick, what is this diagram trying to show?&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2023/02/technical-diagrams-stop-using-cloud-logos/images/aws-variant.png"
width="898"
height="388"
srcset="https://www.technowizardry.net/2023/02/technical-diagrams-stop-using-cloud-logos/images/aws-variant_hu_e1212403f7df31eb.png 480w, https://www.technowizardry.net/2023/02/technical-diagrams-stop-using-cloud-logos/images/aws-variant_hu_58ae38f59ea33516.png 1024w"
loading="lazy"
alt="An architecture diagram using AWS service logos. It intentionally does not use labels to force the reader to guess what the strangely colored logos are supposed to represent."
class="gallery-image"
data-flex-grow="231"
data-flex-basis="555px"
>&lt;/p>
&lt;p>An architecture diagram using AWS service icons to describe services&lt;/p>
&lt;p>I hope you know your AWS icons. There&amp;rsquo;s over 200 services and I have to guess frequently when playing the &lt;a class="link" href="https://quiz.cloudar.be/" target="_blank" rel="noopener"
>AWS Logo Quiz&lt;/a>. While this diagram could easily add some descriptive labels to help, the icons assume developers can remember what the icon means. Some color-blind people may even struggle to see the difference in coloring that AWS uses for different types of services. These icons become visually cluttered and distract the viewer from what matters&amp;ndash;your system.&lt;/p>
&lt;p>Your design documents aren&amp;rsquo;t supposed to be AWS marketing material, they&amp;rsquo;re supposed to be refined, intuitive, and show how the parts fit together, not just show you can glue together these different services.&lt;/p>
&lt;p>Compare that with the below diagram:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2023/02/technical-diagrams-stop-using-cloud-logos/images/simple.svg"
loading="lazy"
alt="An architecture diagram showing a simple service using just solid color boxes and arrows instead of service logos. It still labels service names if relevant, for example using “Pending Work Topic (SNS)” to denote it’s an AWS SQS queue."
>&lt;/p>
&lt;p>Here I use simple boxes and arrows to focus the viewer on the key parts that matter on each part. Service and component names become the primary focus with the service being a secondary detail on the label.&lt;/p>
&lt;p>I also use colors and simple shading to signify which components are new, changing, or being removed in my projects. This helps to convey relative impact of projects to other engineers. Avoid using too many similar colors to ensure that color blind coworkers are still able to understand your diagrams.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2023%2F02%2Ftechnical-diagrams-stop-using-cloud-logos%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Technical+Diagrams+-+Stop+using+cloud+logos" style="border:0" alt="" /></description></item><item><title>Local Energy Monitoring using the Emporia Vue 2</title><link>https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/</link><pubDate>Wed, 22 Feb 2023 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/</guid><summary>&lt;p>I&amp;rsquo;ve previously explored the world of home energy monitoring systems and in the past arrived at using the Brultech GreenEye Monitor for a project in a friend&amp;rsquo;s house. It had the advantage of being local out-of-the-box and had a wide range of compact CTs that made fitting the electronics in the breaker box a lot easier, but it had one flaw that made it not suitable for my condo. It had to be mounted outside the breaker box with wires running into the box. I had no space in my condo, so I instead explored other options.&lt;/p></summary><description>&lt;p>I&amp;rsquo;ve previously explored the world of home energy monitoring systems and in the past arrived at using the Brultech GreenEye Monitor for a project in a friend&amp;rsquo;s house. It had the advantage of being local out-of-the-box and had a wide range of compact CTs that made fitting the electronics in the breaker box a lot easier, but it had one flaw that made it not suitable for my condo. It had to be mounted outside the breaker box with wires running into the box. I had no space in my condo, so I instead explored other options.&lt;/p>
&lt;p>I came across the &lt;a class="link" href="https://www.emporiaenergy.com/how-the-vue-energy-monitor-works" target="_blank" rel="noopener"
>Emporia Vue2&lt;/a> and identified that it was running a standard ESP32 device and was &lt;a class="link" href="https://github.com/emporia-vue-local/esphome" target="_blank" rel="noopener"
>easy to reflash&lt;/a> with custom &lt;a class="link" href="https://esphome.io/" target="_blank" rel="noopener"
>ESPHome&lt;/a> firmware. ESPHome is an open-source framework for creating firmware to collect data from a variety of different sensors and publish it to MQTT/Home Assistant. This sounded perfect, so I ordered a Vue2 and here&amp;rsquo;s how I made it work.&lt;/p>
&lt;h2 id="gear-used">Gear Used&lt;/h2>
&lt;ul>
&lt;li>&lt;a class="link" href="https://www.emporiaenergy.com/" target="_blank" rel="noopener"
>Emporia Vue2&lt;/a> - Obviously&lt;/li>
&lt;li>13 Emporia 50A CT Clamps&lt;/li>
&lt;li>&lt;a class="link" href="https://www.sparkfun.com/products/14050" target="_blank" rel="noopener"
>SparkFun Serial Basic Breakout CH340G&lt;/a> - A USB to UART board to write firmware&lt;/li>
&lt;li>&lt;a class="link" href="https://www.adafruit.com/product/5433" target="_blank" rel="noopener"
>Pogo Pin Probe Clip - 6 Pins with 2.54mm / 0.1&amp;quot; Pitch&lt;/a> - Used to interface with the ESP32 without soldering&lt;/li>
&lt;/ul>
&lt;h2 id="installation">Installation&lt;/h2>
&lt;p>I found the emporia-vue-local GitHub project here &lt;a class="link" href="https://github.com/emporia-vue-local/esphome" target="_blank" rel="noopener"
>emporia-vue-local/esphome&lt;/a> and followed the guide &lt;a class="link" href="https://github.com/emporia-vue-local/esphome/discussions/53" target="_blank" rel="noopener"
>here&lt;/a>. I had some issues trying to wire up to the DTR and CTS pins, so I instead connected IO0 and CTS to GND at boot, then let EN go high when I was ready to program.&lt;/p>
&lt;p>I tried using the ESPHome Web UI to program the device, but it never worked correctly, so instead I used ESPTool on my laptop (&lt;a class="link" href="https://github.com/RavenSystem/esp-homekit-devices/wiki/Install-ESPTool-on-macOS" target="_blank" rel="noopener"
>Installation Guide&lt;/a>).&lt;/p>
&lt;p>First I made a backup of my existing firmware:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">python3&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">m&lt;/span> &lt;span class="n">esptool&lt;/span> &lt;span class="n">read_flash&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mh">0x400000&lt;/span> &lt;span class="n">flash_contents&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">bin&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">esptool&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span> &lt;span class="n">v4&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mi">4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Found&lt;/span> &lt;span class="mi">3&lt;/span> &lt;span class="n">serial&lt;/span> &lt;span class="n">ports&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Serial&lt;/span> &lt;span class="n">port&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">cu&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">usbserial&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">130&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Connecting&lt;/span>&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Detecting&lt;/span> &lt;span class="n">chip&lt;/span> &lt;span class="n">type&lt;/span>&lt;span class="o">...&lt;/span> &lt;span class="n">Unsupported&lt;/span> &lt;span class="n">detection&lt;/span> &lt;span class="n">protocol&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">switching&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">trying&lt;/span> &lt;span class="n">again&lt;/span>&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Connecting&lt;/span>&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Detecting&lt;/span> &lt;span class="n">chip&lt;/span> &lt;span class="n">type&lt;/span>&lt;span class="o">...&lt;/span> &lt;span class="n">ESP32&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Chip&lt;/span> &lt;span class="n">is&lt;/span> &lt;span class="n">ESP32&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">D0WD&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">revision&lt;/span> &lt;span class="n">v1&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Features&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">WiFi&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">BT&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Dual&lt;/span> &lt;span class="n">Core&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">240&lt;/span>&lt;span class="n">MHz&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">VRef&lt;/span> &lt;span class="n">calibration&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">efuse&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Coding&lt;/span> &lt;span class="n">Scheme&lt;/span> &lt;span class="n">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Crystal&lt;/span> &lt;span class="n">is&lt;/span> &lt;span class="mi">40&lt;/span>&lt;span class="n">MHz&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">MAC&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">de&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">ad&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">be&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">ef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">ca&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">fe&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Stub&lt;/span> &lt;span class="n">is&lt;/span> &lt;span class="n">already&lt;/span> &lt;span class="n">running&lt;/span>&lt;span class="o">.&lt;/span> &lt;span class="n">No&lt;/span> &lt;span class="n">upload&lt;/span> &lt;span class="n">is&lt;/span> &lt;span class="n">necessary&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">4194304&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mi">100&lt;/span> &lt;span class="o">%&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">4194304&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mi">100&lt;/span> &lt;span class="o">%&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Read&lt;/span> &lt;span class="mi">4194304&lt;/span> &lt;span class="n">bytes&lt;/span> &lt;span class="n">at&lt;/span> &lt;span class="mh">0x00000000&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="mf">379.5&lt;/span> &lt;span class="n">seconds&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mf">88.4&lt;/span> &lt;span class="n">kbit&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Hard&lt;/span> &lt;span class="n">resetting&lt;/span> &lt;span class="n">via&lt;/span> &lt;span class="n">RTS&lt;/span> &lt;span class="n">pin&lt;/span>&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then I went to my ESPHome dashboard and created a new configuration. I started with the reference ESPHome, but made a few changes. Specifically:&lt;/p>
&lt;p>I updated the CTs to match my phases and circuits. You will need to do the same.&lt;/p>
&lt;p>More importantly, I restructured how the sensors were configured to improve accuracy and reduce useless events. More information is found in the Accuracy section below.&lt;/p>
&lt;p>Here&amp;rsquo;s the current ESPHome YAML:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt"> 10
&lt;/span>&lt;span class="lnt"> 11
&lt;/span>&lt;span class="lnt"> 12
&lt;/span>&lt;span class="lnt"> 13
&lt;/span>&lt;span class="lnt"> 14
&lt;/span>&lt;span class="lnt"> 15
&lt;/span>&lt;span class="lnt"> 16
&lt;/span>&lt;span class="lnt"> 17
&lt;/span>&lt;span class="lnt"> 18
&lt;/span>&lt;span class="lnt"> 19
&lt;/span>&lt;span class="lnt"> 20
&lt;/span>&lt;span class="lnt"> 21
&lt;/span>&lt;span class="lnt"> 22
&lt;/span>&lt;span class="lnt"> 23
&lt;/span>&lt;span class="lnt"> 24
&lt;/span>&lt;span class="lnt"> 25
&lt;/span>&lt;span class="lnt"> 26
&lt;/span>&lt;span class="lnt"> 27
&lt;/span>&lt;span class="lnt"> 28
&lt;/span>&lt;span class="lnt"> 29
&lt;/span>&lt;span class="lnt"> 30
&lt;/span>&lt;span class="lnt"> 31
&lt;/span>&lt;span class="lnt"> 32
&lt;/span>&lt;span class="lnt"> 33
&lt;/span>&lt;span class="lnt"> 34
&lt;/span>&lt;span class="lnt"> 35
&lt;/span>&lt;span class="lnt"> 36
&lt;/span>&lt;span class="lnt"> 37
&lt;/span>&lt;span class="lnt"> 38
&lt;/span>&lt;span class="lnt"> 39
&lt;/span>&lt;span class="lnt"> 40
&lt;/span>&lt;span class="lnt"> 41
&lt;/span>&lt;span class="lnt"> 42
&lt;/span>&lt;span class="lnt"> 43
&lt;/span>&lt;span class="lnt"> 44
&lt;/span>&lt;span class="lnt"> 45
&lt;/span>&lt;span class="lnt"> 46
&lt;/span>&lt;span class="lnt"> 47
&lt;/span>&lt;span class="lnt"> 48
&lt;/span>&lt;span class="lnt"> 49
&lt;/span>&lt;span class="lnt"> 50
&lt;/span>&lt;span class="lnt"> 51
&lt;/span>&lt;span class="lnt"> 52
&lt;/span>&lt;span class="lnt"> 53
&lt;/span>&lt;span class="lnt"> 54
&lt;/span>&lt;span class="lnt"> 55
&lt;/span>&lt;span class="lnt"> 56
&lt;/span>&lt;span class="lnt"> 57
&lt;/span>&lt;span class="lnt"> 58
&lt;/span>&lt;span class="lnt"> 59
&lt;/span>&lt;span class="lnt"> 60
&lt;/span>&lt;span class="lnt"> 61
&lt;/span>&lt;span class="lnt"> 62
&lt;/span>&lt;span class="lnt"> 63
&lt;/span>&lt;span class="lnt"> 64
&lt;/span>&lt;span class="lnt"> 65
&lt;/span>&lt;span class="lnt"> 66
&lt;/span>&lt;span class="lnt"> 67
&lt;/span>&lt;span class="lnt"> 68
&lt;/span>&lt;span class="lnt"> 69
&lt;/span>&lt;span class="lnt"> 70
&lt;/span>&lt;span class="lnt"> 71
&lt;/span>&lt;span class="lnt"> 72
&lt;/span>&lt;span class="lnt"> 73
&lt;/span>&lt;span class="lnt"> 74
&lt;/span>&lt;span class="lnt"> 75
&lt;/span>&lt;span class="lnt"> 76
&lt;/span>&lt;span class="lnt"> 77
&lt;/span>&lt;span class="lnt"> 78
&lt;/span>&lt;span class="lnt"> 79
&lt;/span>&lt;span class="lnt"> 80
&lt;/span>&lt;span class="lnt"> 81
&lt;/span>&lt;span class="lnt"> 82
&lt;/span>&lt;span class="lnt"> 83
&lt;/span>&lt;span class="lnt"> 84
&lt;/span>&lt;span class="lnt"> 85
&lt;/span>&lt;span class="lnt"> 86
&lt;/span>&lt;span class="lnt"> 87
&lt;/span>&lt;span class="lnt"> 88
&lt;/span>&lt;span class="lnt"> 89
&lt;/span>&lt;span class="lnt"> 90
&lt;/span>&lt;span class="lnt"> 91
&lt;/span>&lt;span class="lnt"> 92
&lt;/span>&lt;span class="lnt"> 93
&lt;/span>&lt;span class="lnt"> 94
&lt;/span>&lt;span class="lnt"> 95
&lt;/span>&lt;span class="lnt"> 96
&lt;/span>&lt;span class="lnt"> 97
&lt;/span>&lt;span class="lnt"> 98
&lt;/span>&lt;span class="lnt"> 99
&lt;/span>&lt;span class="lnt">100
&lt;/span>&lt;span class="lnt">101
&lt;/span>&lt;span class="lnt">102
&lt;/span>&lt;span class="lnt">103
&lt;/span>&lt;span class="lnt">104
&lt;/span>&lt;span class="lnt">105
&lt;/span>&lt;span class="lnt">106
&lt;/span>&lt;span class="lnt">107
&lt;/span>&lt;span class="lnt">108
&lt;/span>&lt;span class="lnt">109
&lt;/span>&lt;span class="lnt">110
&lt;/span>&lt;span class="lnt">111
&lt;/span>&lt;span class="lnt">112
&lt;/span>&lt;span class="lnt">113
&lt;/span>&lt;span class="lnt">114
&lt;/span>&lt;span class="lnt">115
&lt;/span>&lt;span class="lnt">116
&lt;/span>&lt;span class="lnt">117
&lt;/span>&lt;span class="lnt">118
&lt;/span>&lt;span class="lnt">119
&lt;/span>&lt;span class="lnt">120
&lt;/span>&lt;span class="lnt">121
&lt;/span>&lt;span class="lnt">122
&lt;/span>&lt;span class="lnt">123
&lt;/span>&lt;span class="lnt">124
&lt;/span>&lt;span class="lnt">125
&lt;/span>&lt;span class="lnt">126
&lt;/span>&lt;span class="lnt">127
&lt;/span>&lt;span class="lnt">128
&lt;/span>&lt;span class="lnt">129
&lt;/span>&lt;span class="lnt">130
&lt;/span>&lt;span class="lnt">131
&lt;/span>&lt;span class="lnt">132
&lt;/span>&lt;span class="lnt">133
&lt;/span>&lt;span class="lnt">134
&lt;/span>&lt;span class="lnt">135
&lt;/span>&lt;span class="lnt">136
&lt;/span>&lt;span class="lnt">137
&lt;/span>&lt;span class="lnt">138
&lt;/span>&lt;span class="lnt">139
&lt;/span>&lt;span class="lnt">140
&lt;/span>&lt;span class="lnt">141
&lt;/span>&lt;span class="lnt">142
&lt;/span>&lt;span class="lnt">143
&lt;/span>&lt;span class="lnt">144
&lt;/span>&lt;span class="lnt">145
&lt;/span>&lt;span class="lnt">146
&lt;/span>&lt;span class="lnt">147
&lt;/span>&lt;span class="lnt">148
&lt;/span>&lt;span class="lnt">149
&lt;/span>&lt;span class="lnt">150
&lt;/span>&lt;span class="lnt">151
&lt;/span>&lt;span class="lnt">152
&lt;/span>&lt;span class="lnt">153
&lt;/span>&lt;span class="lnt">154
&lt;/span>&lt;span class="lnt">155
&lt;/span>&lt;span class="lnt">156
&lt;/span>&lt;span class="lnt">157
&lt;/span>&lt;span class="lnt">158
&lt;/span>&lt;span class="lnt">159
&lt;/span>&lt;span class="lnt">160
&lt;/span>&lt;span class="lnt">161
&lt;/span>&lt;span class="lnt">162
&lt;/span>&lt;span class="lnt">163
&lt;/span>&lt;span class="lnt">164
&lt;/span>&lt;span class="lnt">165
&lt;/span>&lt;span class="lnt">166
&lt;/span>&lt;span class="lnt">167
&lt;/span>&lt;span class="lnt">168
&lt;/span>&lt;span class="lnt">169
&lt;/span>&lt;span class="lnt">170
&lt;/span>&lt;span class="lnt">171
&lt;/span>&lt;span class="lnt">172
&lt;/span>&lt;span class="lnt">173
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">esphome&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">emporia-vue2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">external_components&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">source&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github://emporia-vue-local/esphome@dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">components&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">emporia_vue ]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">esp32&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">board&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">esp32dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">framework&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">esp-idf&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">recommended&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Enable Home Assistant API&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">mqtt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">broker&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mqtt.example.domain&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">discovery_unique_id_generator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mac&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">discovery_object_id_generator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">none&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">logger&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">logs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sensor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">INFO&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">ota&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>!&lt;span class="l">secret ota&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">num_attempts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">wifi&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ssid&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>!&lt;span class="l">secret wifi_ssid&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>!&lt;span class="l">secret wifi_password&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">i2c&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sda&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">21&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">scl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">22&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">scan&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">frequency&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">200kHz &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># recommended range is 50-200kHz&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">i2c_a&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">time&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sntp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my_time&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timezone&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">America/Los_Angeles&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">debug&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">update_interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">120s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">text_sensor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">debug&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">reset_reason&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Reset Reason&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># these are called references in YAML. They allow you to reuse&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># this configuration in each sensor, while only defining it once&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">.defaultfilters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="cp">&amp;amp;moving_avg&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># we capture a new sample every 0.24 seconds, so the time can&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># be calculated from the number of samples as n * 0.24.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sliding_window_moving_average&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># we average over the past 2.88 seconds&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window_size&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">12&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># we push a new value every 1.44 seconds&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">send_every&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">6&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="cp">&amp;amp;invert&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># invert and filter out any values below 0.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">lambda&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;return max(-x, 0.0f);&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="cp">&amp;amp;pos&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># filter out any values below 0.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">lambda&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;return max(x, 0.0f);&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="cp">&amp;amp;abs&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># take the absolute value of the value&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">lambda&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;return abs(x);&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Reduce noise in the power class&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="cp">&amp;amp;power_max&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">or&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">delta&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">throttle&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">60s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="cp">&amp;amp;power_min&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">throttle&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">3s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="cp">&amp;amp;throttle_energy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">or&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">delta&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">throttle&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">60s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">sensor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">emporia_vue&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">i2c_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">i2c_a&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">phases&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">phase_a &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Verify that this specific phase/leg is connected to correct input wire color on device listed below&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">BLACK &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Vue device wire color&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">calibration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.022&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 0.022 is used as the default as starting point but may need adjusted to ensure accuracy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># To calculate new calibration value use the formula * / &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">voltage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Phase A Voltage&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="cp">*moving_avg,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">phase_b &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Verify that this specific phase/leg is connected to correct input wire color on device listed below&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RED &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Vue device wire color&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">calibration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.022&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 0.022 is used as the default as starting point but may need adjusted to ensure accuracy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># To calculate new calibration value use the formula * / &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">voltage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Phase B Voltage&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="cp">*moving_avg,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ct_clamps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">phase_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">phase_a&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Verify the CT going to this device input also matches the phase/leg&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Phase A Power&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">phase_a_power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="cp">*moving_avg,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">phase_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">phase_b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;B&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Verify the CT going to this device input also matches the phase/leg&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Phase B Power&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">phase_b_power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="cp">*moving_avg,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Pay close attention to set the phase_id for each breaker by matching it to the phase/leg it connects to in the panel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Some circuits are commented out because they don&amp;#39;t have a CT connected yet&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">phase_id: phase_a, input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="nt">, power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Heat Pump Power #1&amp;#34;&lt;/span>&lt;span class="nt">, internal: true, id: cir1, filters: [ *pos, multiply&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">phase_id: phase_a, input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;2&amp;#34;&lt;/span>&lt;span class="nt">, power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Oven Power #2&amp;#34;&lt;/span>&lt;span class="nt">, id: cir2, internal: true, filters: [ *pos, multiply&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">phase_id: phase_a, input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;5&amp;#34;&lt;/span>&lt;span class="nt">, power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Dryer Power #5&amp;#34;&lt;/span>&lt;span class="nt">, internal: true, id: cir5, filters: [ *pos, multiply&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">phase_id: phase_a, input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;6&amp;#34;&lt;/span>&lt;span class="nt">, power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Dishwasher / Disposal Power #6&amp;#34;&lt;/span>&lt;span class="nt">, internal: true, id: cir6, filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">phase_id: phase_b, input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;8&amp;#34;&lt;/span>&lt;span class="nt">, power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Kitchen Power #8&amp;#34;&lt;/span>&lt;span class="nt">, id: cir8, internal: true, filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">phase_id: phase_a, input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;9&amp;#34;&lt;/span>&lt;span class="nt">, power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Washer Power #9&amp;#34;&lt;/span>&lt;span class="nt">, id: cir9, internal: true, filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">phase_id: phase_a, input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;10&amp;#34;&lt;/span>&lt;span class="nt">, power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Kitchen Power #10&amp;#34;&lt;/span>&lt;span class="nt">, id: cir10, internal: true, filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># - { phase_id: phase_b, input: &amp;#34;11&amp;#34;, power: { name: &amp;#34;Bathroom Power #11&amp;#34;, id: cir11, filters: [ *moving_avg, *pos ] } }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># - { phase_id: phase_b, input: &amp;#34;12&amp;#34;, power: { name: &amp;#34;Stove / Hood Fan Power #12&amp;#34;, id: cir12, filters: [ *moving_avg, *pos ] } }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># - { phase_id: phase_a, input: &amp;#34;13&amp;#34;, power: { name: &amp;#34;Microwave Power #13&amp;#34;, id: cir13, filters: [ *moving_avg, *pos ] } }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">phase_id: phase_a, input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;14&amp;#34;&lt;/span>&lt;span class="nt">, power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Bedroom Power #14&amp;#34;&lt;/span>&lt;span class="nt">, id: cir14, internal: true, filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">phase_id: phase_b, input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;15&amp;#34;&lt;/span>&lt;span class="nt">, power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;General Power #15&amp;#34;&lt;/span>&lt;span class="nt">, internal: true, id: cir15, filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">phase_id: phase_b, input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;16&amp;#34;&lt;/span>&lt;span class="nt">, power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;General Power #16&amp;#34;&lt;/span>&lt;span class="nt">, internal: true, id: cir16, filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">platform: copy, id: cir1_b, source_id: cir1, filters: [ *power_min, *power_max ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Heat Pump Power #1&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">platform: copy, id: cir2_b, source_id: cir2, filters: [ *power_min, *power_max ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Ovean Power #2&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">platform: copy, id: cir5_b, source_id: cir5, filters: [ *power_min, *power_max ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Dryer Power #5&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">platform: copy, id: cir6_b, source_id: cir6, filters: [ *power_min, *power_max ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Dishwasher / Disposal Power #6&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">platform: copy, id: cir8_b, source_id: cir8, filters: [ *power_min, *power_max ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Kitchen Power #8&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">platform: copy, id: cir9_b, source_id: cir9, filters: [ *power_min, *power_max ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Washer Power #9&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">platform: copy, id: cir10_b, source_id: cir10, filters: [ *power_min, *power_max ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Kitchen Power #10&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">platform: copy, id: cir14_b, source_id: cir14, filters: [ *power_min, *power_max ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Bedroom Power #14&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">platform: copy, id: cir15_b, source_id: cir15, filters: [ *power_min, *power_max ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;General Power #15&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">platform: copy, id: cir16_b, source_id: cir16, filters: [ *power_min, *power_max ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;General Power #16&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">template&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Total Power&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">lambda&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">return id(phase_a_power).state + id(phase_b_power).state;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">update_interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">total_power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">measurement&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;W&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Total Daily Energy&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">power_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">total_power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">total_daily_energy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">accuracy_decimals&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restore&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Wh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">total_increasing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">energy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*throttle_energy&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">power_id: cir1, platform: total_daily_energy, accuracy_decimals: 0, restore: false, unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Wh&amp;#34;&lt;/span>&lt;span class="nt">, state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;total_increasing&amp;#34;&lt;/span>&lt;span class="nt">, device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;energy&amp;#34;&lt;/span>&lt;span class="nt">, filters: [ *throttle_energy ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Heat Pump Energy&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">power_id: cir2, platform: total_daily_energy, accuracy_decimals: 0, restore: false, unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Wh&amp;#34;&lt;/span>&lt;span class="nt">, state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;total_increasing&amp;#34;&lt;/span>&lt;span class="nt">, device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;energy&amp;#34;&lt;/span>&lt;span class="nt">, filters: [ *throttle_energy ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Oven Energy&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">power_id: cir5, platform: total_daily_energy, accuracy_decimals: 0, restore: false, unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Wh&amp;#34;&lt;/span>&lt;span class="nt">, state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;total_increasing&amp;#34;&lt;/span>&lt;span class="nt">, device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;energy&amp;#34;&lt;/span>&lt;span class="nt">, filters: [ *throttle_energy ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Dryer Energy&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">power_id: cir6, platform: total_daily_energy, accuracy_decimals: 0, restore: false, unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Wh&amp;#34;&lt;/span>&lt;span class="nt">, state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;total_increasing&amp;#34;&lt;/span>&lt;span class="nt">, device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;energy&amp;#34;&lt;/span>&lt;span class="nt">, filters: [ *throttle_energy ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Dishwasher / Disposal Energy&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">power_id: cir8, platform: total_daily_energy, accuracy_decimals: 0, restore: false, unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Wh&amp;#34;&lt;/span>&lt;span class="nt">, state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;total_increasing&amp;#34;&lt;/span>&lt;span class="nt">, device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;energy&amp;#34;&lt;/span>&lt;span class="nt">, filters: [ *throttle_energy ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Kitchen #8 Energy&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">power_id: cir9, platform: total_daily_energy, accuracy_decimals: 0, restore: false, unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Wh&amp;#34;&lt;/span>&lt;span class="nt">, state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;total_increasing&amp;#34;&lt;/span>&lt;span class="nt">, device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;energy&amp;#34;&lt;/span>&lt;span class="nt">, filters: [ *throttle_energy ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Washer Energy&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">power_id: cir10, platform: total_daily_energy, accuracy_decimals: 0, restore: false, unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Wh&amp;#34;&lt;/span>&lt;span class="nt">, state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;total_increasing&amp;#34;&lt;/span>&lt;span class="nt">, device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;energy&amp;#34;&lt;/span>&lt;span class="nt">, filters: [ *throttle_energy ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Kitchen #10 Energy&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">power_id: cir14, platform: total_daily_energy, accuracy_decimals: 0, restore: false, unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Wh&amp;#34;&lt;/span>&lt;span class="nt">, state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;total_increasing&amp;#34;&lt;/span>&lt;span class="nt">, device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;energy&amp;#34;&lt;/span>&lt;span class="nt">, filters: [ *throttle_energy ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Bedroom Energy&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">power_id: cir15, platform: total_daily_energy, accuracy_decimals: 0, restore: false, unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Wh&amp;#34;&lt;/span>&lt;span class="nt">, state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;total_increasing&amp;#34;&lt;/span>&lt;span class="nt">, device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;energy&amp;#34;&lt;/span>&lt;span class="nt">, filters: [ *throttle_energy ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;General #15 Energy&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">power_id: cir16, platform: total_daily_energy, accuracy_decimals: 0, restore: false, unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Wh&amp;#34;&lt;/span>&lt;span class="nt">, state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;total_increasing&amp;#34;&lt;/span>&lt;span class="nt">, device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;energy&amp;#34;&lt;/span>&lt;span class="nt">, filters: [ *throttle_energy ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;General #16 Energy&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Save the above configuration, then hit Install &amp;gt; Manual Install &amp;gt; Modern Format. Let it compile, then download. Then using the&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">python3&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">m&lt;/span> &lt;span class="n">esptool&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">chip&lt;/span> &lt;span class="n">esp32&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">p&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">cu&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">usbserial&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">130&lt;/span> &lt;span class="n">write_flash&lt;/span> &lt;span class="mh">0x0&lt;/span> &lt;span class="o">~/&lt;/span>&lt;span class="n">Downloads&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">emporia&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">vue2&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">factory&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">bin&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">esptool&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span> &lt;span class="n">v4&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mi">4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Serial&lt;/span> &lt;span class="n">port&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">cu&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">usbserial&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">130&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Connecting&lt;/span>&lt;span class="o">....&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Chip&lt;/span> &lt;span class="n">is&lt;/span> &lt;span class="n">ESP32&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">D0WD&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">revision&lt;/span> &lt;span class="n">v1&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Features&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">WiFi&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">BT&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Dual&lt;/span> &lt;span class="n">Core&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">240&lt;/span>&lt;span class="n">MHz&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">VRef&lt;/span> &lt;span class="n">calibration&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">efuse&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Coding&lt;/span> &lt;span class="n">Scheme&lt;/span> &lt;span class="n">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Crystal&lt;/span> &lt;span class="n">is&lt;/span> &lt;span class="mi">40&lt;/span>&lt;span class="n">MHz&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">MAC&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">a8&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">48&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">fa&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">97&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">90&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="n">c&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Uploading&lt;/span> &lt;span class="n">stub&lt;/span>&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Running&lt;/span> &lt;span class="n">stub&lt;/span>&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Stub&lt;/span> &lt;span class="n">running&lt;/span>&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Configuring&lt;/span> &lt;span class="n">flash&lt;/span> &lt;span class="n">size&lt;/span>&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Flash&lt;/span> &lt;span class="n">will&lt;/span> &lt;span class="n">be&lt;/span> &lt;span class="n">erased&lt;/span> &lt;span class="n">from&lt;/span> &lt;span class="mh">0x00000000&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="mh">0x000dbfff&lt;/span>&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Compressed&lt;/span> &lt;span class="mi">897488&lt;/span> &lt;span class="n">bytes&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="mf">564586.&lt;/span>&lt;span class="o">..&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Wrote&lt;/span> &lt;span class="mi">897488&lt;/span> &lt;span class="n">bytes&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mi">564586&lt;/span> &lt;span class="n">compressed&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">at&lt;/span> &lt;span class="mh">0x00000000&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="mf">54.7&lt;/span> &lt;span class="n">seconds&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">effective&lt;/span> &lt;span class="mf">131.3&lt;/span> &lt;span class="n">kbit&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Hash&lt;/span> &lt;span class="n">of&lt;/span> &lt;span class="n">data&lt;/span> &lt;span class="n">verified&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Leaving&lt;/span>&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Hard&lt;/span> &lt;span class="n">resetting&lt;/span> &lt;span class="n">via&lt;/span> &lt;span class="n">RTS&lt;/span> &lt;span class="n">pin&lt;/span>&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>References&lt;/p>
&lt;p>&lt;a class="link" href="https://flaviutamas.com/2021/reversing-emporia-vue-2" target="_blank" rel="noopener"
>https://flaviutamas.com/2021/reversing-emporia-vue-2&lt;/a>&lt;/p>
&lt;h2 id="installing-cts">Installing CTs&lt;/h2>
&lt;p>Installing the CTs is the hardest and most dangerous part of this. If you&amp;rsquo;re not familiar with the risks with working inside a breaker box and the fact that the mains generally can&amp;rsquo;t be turned off, you should consult a qualified electrician.&lt;/p>
&lt;p>The official hardware guide can be found &lt;a class="link" href="https://www.emporiaenergy.com/installation-guides" target="_blank" rel="noopener"
>here&lt;/a>.&lt;/p>
&lt;p>When installing the CTs, make sure that you match the phases with the colors and the holes in the top of the Vue2. Otherwise you won&amp;rsquo;t get the correct data. Note the bolded items. My BLACK wire went into the A hole and RED went into the B hole on the top of the Vue2.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">phase_a &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">**BLACK**&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Vue device wire color&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">phase_b &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Verify that this specific phase/leg is connected to correct input wire color on device listed below&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">**RED**&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Vue device wire color&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ct_clamps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">phase_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">phase_a&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;**A**&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Verify the CT going to this device input also matches the phase/leg&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Phase A Power&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">phase_a_power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="cp">*moving_avg,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">phase_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">phase_b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;**B**&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Verify the CT going to this device input also matches the phase/leg&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Phase B Power&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">phase_b_power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="cp">*moving_avg,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Pay close attention to set the phase_id for each breaker by matching it to the phase/leg it connects to in the panel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">phase_id: **phase_a**, input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="nt">, power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Heat Pump Power #1&amp;#34;&lt;/span>&lt;span class="nt">, id: cir1, filters: [ *moving_avg, *pos, multiply&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;a class="link" href="images/EnergySensingVue2-BreakerStage2.jpg" >&lt;img src="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/EnergySensingVue2-BreakerStage2-714x1024.jpg"
width="714"
height="1024"
srcset="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/EnergySensingVue2-BreakerStage2-714x1024_hu_fd841c0e00c30c7d.jpg 480w, https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/EnergySensingVue2-BreakerStage2-714x1024_hu_f5b96830cec78f57.jpg 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="69"
data-flex-basis="167px"
>&lt;/a>&lt;/p>
&lt;p>An in progress pic showing it installed with just the CTs on the main loads. Note that the third phase is unused and is joined to the neutral and not left floating.&lt;/p>
&lt;p>When installing the CTs on the individual circuits in North American houses, you may encounter circuits that have two phases (240v circuits). Two CTs are needed if it&amp;rsquo;s unbalanced, but one CT is sufficient if it&amp;rsquo;s a &amp;ldquo;balanced load.&amp;rdquo; I found the following quote to help me when installing:&lt;/p>
&lt;blockquote>
&lt;p>If a double breaker circuit is “balanced”, power is evenly drawn through both poles. In this instance, an energy monitoring app can typically take the reading from one CT and multiply it by 2 to get the correct power reading.&lt;/p>
&lt;p>However, if a circuit is “unbalanced”, two CTs should be used. Pumps, electric resistance heat, and HVAC units are typically balanced. Subpanels, dryers, electric ovens/ranges, and hot tubs are not balanced. Typically, if a piece of equipment is doing more than one thing, it is an unbalanced load. A dryer needs to rotate the drum and dry the clothes. Also, if a circuit has a neutral wire, this most likely means that the load is unbalanced and requires two CTs.&lt;/p>
&lt;p>&lt;a class="link" href="https://www.powerwisesystems.com/blog/measure-electricity-use-current-transformers/" target="_blank" rel="noopener"
>https://www.powerwisesystems.com/blog/measure-electricity-use-current-transformers/&lt;/a>&lt;/p>&lt;/blockquote>
&lt;p>I only found one unbalanced load, my clothes dryer. I installed a CT on both legs, then updated the ESPHome template to aggregate both legs, then send it to MQTT. The following snippet shows how to implement an unbalanced load sensor:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ct_clamps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">phase_id: phase_a, input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;5&amp;#34;&lt;/span>&lt;span class="nt">, power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Dryer Power #5&amp;#34;&lt;/span>&lt;span class="nt">, internal: true, id: cir5, filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">phase_id: phase_b, input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;7&amp;#34;&lt;/span>&lt;span class="nt">, power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Dryer Power #7&amp;#34;&lt;/span>&lt;span class="nt">, internal: true, id: cir7, filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">...]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">template&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Dryer Power&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">lambda&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">return id(cir5).state + id(cir7).state;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">update_interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dryer_power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">measurement&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;W&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*power_min,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*power_max&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">power_id: dryer_power, platform: total_daily_energy, accuracy_decimals: 0, restore: false, unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Wh&amp;#34;&lt;/span>&lt;span class="nt">, state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;total_increasing&amp;#34;&lt;/span>&lt;span class="nt">, device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;energy&amp;#34;&lt;/span>&lt;span class="nt">, filters: [ *throttle_energy ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Dryer Energy&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/20230118_205033-1275x2048.jpg"
width="1275"
height="2048"
srcset="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/20230118_205033-1275x2048_hu_b7bf6260185b9783.jpg 480w, https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/20230118_205033-1275x2048_hu_59662e95af1fedb8.jpg 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="62"
data-flex-basis="149px"
>&lt;/p>
&lt;p>Next phase after installing almost all of the CTs. Not all circuits will get CTs.&lt;/p>
&lt;p>After everything&amp;rsquo;s safely installed, turn the breaker on and verify that you&amp;rsquo;re receiving traffic in MQTT and in Home Assistant.&lt;/p>
&lt;h2 id="ha-recorder-management">HA Recorder Management&lt;/h2>
&lt;p>When I first installed the Vue2 and pointed it to Home Assistant, it didn&amp;rsquo;t take long for the Postgres instance behind my HA Recorder to run out of disk space.&lt;/p>
&lt;p>&lt;a class="link" href="images/image.png" >&lt;img src="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image.png"
width="1360"
height="496"
srcset="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image_hu_38900e0ba651482e.png 480w, https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image_hu_329019336707b868.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="274"
data-flex-basis="658px"
>&lt;/a>&lt;/p>
&lt;p>I checked the highest metrics and as I suspected, the integral was using the most space. It was being emitted every second as a watt-hour with 2 decimal points, but everywhere else I was using kilowatt-hours with 2 decimal points.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cnt&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">states&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cnt_pct&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">entity_id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">states&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GROUP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">entity_id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cnt&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DESC&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th># of states&lt;/th>
&lt;th>% total&lt;/th>
&lt;th>entity_id&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>643,609&lt;/td>
&lt;td>22%&lt;/td>
&lt;td>&lt;strong>sensor.total_energy_3&lt;/strong> (The integral generated by Vue2)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>251,745&lt;/td>
&lt;td>8%&lt;/td>
&lt;td>sensor.total_power&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>251,281&lt;/td>
&lt;td>8%&lt;/td>
&lt;td>sensor.total_energy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>251,129&lt;/td>
&lt;td>8%&lt;/td>
&lt;td>sensor.daily_electricity_usage&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>239,813&lt;/td>
&lt;td>8%&lt;/td>
&lt;td>sensor.total_energy_cost&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>210,866&lt;/td>
&lt;td>7%&lt;/td>
&lt;td>sensor.phase_b_power&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>192,576&lt;/td>
&lt;td>6%&lt;/td>
&lt;td>sensor.phase_a_power&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>92,212&lt;/td>
&lt;td>3%&lt;/td>
&lt;td>sensor.media_cabinet_total_power&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Not good. This is likely due to the fact that the integration sensor in ESPHome emits a new value every 2.88s just like the Watt sensor. This metric is designed to be long-term and Home Assistant&amp;rsquo;s energy tab aggregates it up per hour, so there&amp;rsquo;s no need for that level of precision. Instead, I&amp;rsquo;m going to have it emit it less frequently, but first let&amp;rsquo;s see what else is wrong.&lt;/p>
&lt;h2 id="accuracy-analysis-and-comparison">Accuracy Analysis and Comparison&lt;/h2>
&lt;p>I then tried to compare the accuracy of the Vue2 versus other devices I have been using prior to this. The following shows a comparison with the &lt;a class="link" href="https://www.getzooz.com/zooz-zen15-power-switch/" target="_blank" rel="noopener"
>Zooz ZEN15&lt;/a> Z-Wave Outlet installed on my washing machine.&lt;/p>
&lt;h3 id="baseline">Baseline&lt;/h3>
&lt;p>Here we see there is a difference of about ~1W, but this was attributed to the power draw of the outlet itself combined with another low power device on the same circuit not measured. After removing those devices, the measured value converges on about 750mW from both devices. This is good and indicates both devices are at least agreeing with each other.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-1.png" >&lt;img src="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-1.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-1_hu_db56bd75f68c073a.png 480w, https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-1_hu_cd36d16ba59257b8.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p>
&lt;h3 id="under-load">Under Load&lt;/h3>
&lt;p>However, under load from a single wash load we see quite a different perspective:&lt;/p>
&lt;p>&lt;a class="link" href="images/image.png" >&lt;img src="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-1024x585.png"
width="1024"
height="585"
srcset="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-1024x585_hu_f4547c78094190df.png 480w, https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-1024x585_hu_85440a85f1637aa2.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="175"
data-flex-basis="420px"
>&lt;/a>&lt;/p>
&lt;p>This graph shows a big difference in the wave-forms. The Z-Wave outlet reported &lt;strong>126Wh&lt;/strong> for the entire run vs the Emporia Vue2 reporting &lt;strong>204Wh&lt;/strong> for a difference of &lt;strong>80Wh&lt;/strong>. Zooming in on this wave form in two places:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-2.png" >&lt;img src="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-2.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-2_hu_df5ea5be9da95dbd.png 480w, https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-2_hu_7c3a3576da85ad96.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p>
&lt;p>&lt;a class="link" href="images/image-3.png" >&lt;img src="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-3.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-3_hu_d4e05898c476eb65.png 480w, https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-3_hu_f83bcd96c73ab5c.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p>
&lt;p>I suspect this is caused by the power metric being averaged over ~3seconds and all the momentary drops and increases were getting averaged out.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="cp">&amp;amp;moving_avg&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># we capture a new sample every 0.24 seconds, so the time can&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># be calculated from the number of samples as n * 0.24.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sliding_window_moving_average&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># we average over the past 2.88 seconds&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window_size&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">12&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># we push a new value every 1.44 seconds&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">send_every&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">6&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># [...]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">phase_id: phase_a, input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;9&amp;#34;&lt;/span>&lt;span class="nt">, power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Washer Power #9&amp;#34;&lt;/span>&lt;span class="nt">, id: cir9, filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*moving_avg,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I confirmed this by removing the moving_avg filter and configured my Z-Wave outlet to send updates every 1W change and did another load of laundry.&lt;/p>
&lt;p>To verify, I ran a query to total the watt-hours over the entire run-time:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">from(bucket: &amp;#34;homeassistant&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_measurement&amp;#34;] == &amp;#34;W&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;entity_id&amp;#34;] == &amp;#34;washer_outlet_power&amp;#34; or r[&amp;#34;entity_id&amp;#34;] == &amp;#34;washer_power&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_field&amp;#34;] == &amp;#34;value&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; integral(unit: 1h)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Gave me 75Wh from the Vue2 vs 77Wh from the Z-Wave, a difference of &amp;lt;3% and the wave forms look like this:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-4.png" >&lt;img src="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-4.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-4_hu_c2f5c4ddab0b2a1c.png 480w, https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-4_hu_fcb416aeb644349c.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p>
&lt;p>These wave forms look a lot more similar than before.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-5.png" >&lt;img src="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-5.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-5_hu_20822b3717a77d0e.png 480w, https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-5_hu_ba62386fa42f4c5f.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p>
&lt;p>Wave Form with large difference. Possibly caused by sampling or due to apparent/real power&lt;/p>
&lt;p>To fix this, I changed how all sensors worked. Previously, esphome would perform a 2.88s moving average, then integrate that value for energy consumption. However, in spiky situations, that could become quite inaccurate. Trying to use HA&amp;rsquo;s helpers to integrate this would require the esp to send a huge power updates very frequently and increase Recorder usage.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/sensor-map.svg"
loading="lazy"
>&lt;/p>
&lt;p>Diagram showing the ESPHome component data flow before and after.&lt;/p>
&lt;p>Instead, I could tell ESPHome to integrate the raw value without any averaging and throttle both the power and energy sensor updates to something reasonable. I marked the raw sensors as internal: true, then used the &lt;a class="link" href="https://esphome.io/components/copy.html" target="_blank" rel="noopener"
>copy component&lt;/a> to send the power updates every 3s - 60s. Three seconds ensures we don&amp;rsquo;t send updates too quickly, and between 3s and 60s if the power fluctuates by &amp;gt;10W, then it&amp;rsquo;ll send an update, and if not it sends an updates max every 60s. This configuration provides a nice trade-off so constant, lower power devices don&amp;rsquo;t send a lot of updates.&lt;/p>
&lt;p>The &lt;a class="link" href="https://esphome.io/components/sensor/total_daily_energy.html" target="_blank" rel="noopener"
>energy component&lt;/a> takes the raw value and performs the integration, then sends it every 60s since there&amp;rsquo;s very little value in sending more frequently.&lt;/p>
&lt;p>The following snippet shows how a single circuit is represented in the template:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">.defaultfilters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Reduce noise in the power class&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="cp">&amp;amp;power_max&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">or&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">delta&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">throttle&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">60s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="cp">&amp;amp;power_min&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">throttle&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">3s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="cp">&amp;amp;throttle_energy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">throttle&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">60s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># [...]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">sensor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">emporia_vue&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">...]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ct_clamps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">...]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">phase_id: phase_b, input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;16&amp;#34;&lt;/span>&lt;span class="nt">, power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;General Power #16&amp;#34;&lt;/span>&lt;span class="nt">, internal: true, id: cir16, filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">platform: copy, id: cir16_b, source_id: cir16, filters: [ *power_min, *power_max ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;General Power #16&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">power_id: cir16, platform: total_daily_energy, accuracy_decimals: 0, restore: false, unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Wh&amp;#34;&lt;/span>&lt;span class="nt">, state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;total_increasing&amp;#34;&lt;/span>&lt;span class="nt">, device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;energy&amp;#34;&lt;/span>&lt;span class="nt">, filters: [ *throttle_energy ], name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;General #16 Energy&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="voltage-calibration">Voltage Calibration&lt;/h2>
&lt;p>Each phase has a voltage calibration constant value that influences how the Vue2 measures the voltage. I don&amp;rsquo;t know how this influences the current or power sensors, but let&amp;rsquo;s get it calibrated just to be sure.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-8.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-8_hu_32d26708777e851a.png 480w, https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image-8_hu_946ede211b2272f4.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/p>
&lt;p>Using a volt meter, I initially tried to measure the voltage of each phase inside the breaker box connecting the negative to the neutral bar and positive to each phase main line, however the measured voltage fluctuated depending on whether the panel was open or closed. Opened when I measured it, the volt meter agreed with what was in HA, but then I closed the panel and it&amp;rsquo;d deviate. Notice the first two blue lines in the figure above. I don&amp;rsquo;t know what could cause this.&lt;/p>
&lt;p>After that, I instead measured the voltage from two outlets on each phase without the panel being open. I used the formula as mentioned in the template for each phase:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 0.022 is used as the default as starting point but may need adjusted to ensure accuracy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># To calculate new calibration value use the formula &amp;lt;in-use calibration value&amp;gt; * &amp;lt;accurate voltage&amp;gt; / &amp;lt;reporting voltage&amp;gt;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
&lt;semantics>
&lt;mrow>
&lt;mi>{Initial Calibration Value}&lt;/mi>
&lt;mo>&amp;times;&lt;/mo>
&lt;mfrac>
&lt;mi>{Measured Volts}&lt;/mi>
&lt;mi>{Reported Volts}&lt;/mi>
&lt;/mfrac>
&lt;mo>=&lt;/mo>
&lt;mi>{New Calibration Value}&lt;/mi>
&lt;/mrow>
&lt;/semantics>
&lt;/math>
&lt;p>I plugged in my values and get this:&lt;/p>
&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
&lt;semantics>
&lt;mrow>
&lt;mn>0.022&lt;/mn>
&lt;mo>&amp;times;&lt;/mo>
&lt;mfrac>
&lt;mrow>
&lt;mn>123.0&lt;/mn>
&lt;mi>v&lt;/mi>
&lt;/mrow>
&lt;mrow>
&lt;mn>126.5&lt;/mn>
&lt;mi>v&lt;/mi>
&lt;/mrow>
&lt;/mfrac>
&lt;mo>=&lt;/mo>
&lt;mn>0.022737&lt;/mn>
&lt;/mrow>
&lt;/semantics>
&lt;/math>
&lt;p>Then plugged both values into the template and redeployed:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">sensor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">emporia_vue&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">i2c_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">i2c_a&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">phases&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">phase_a&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">BLACK&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">calibration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.022737&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">voltage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Phase A Voltage&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="cp">*moving_avg,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">phase_b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RED&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">calibration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.021295&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">voltage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Phase B Voltage&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="cp">*moving_avg,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">*pos]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After that the measured voltages seemed to align with my volt meter and converged together (not that both phases need to match.)&lt;/p>
&lt;h2 id="comparison-with-electric-provider">Comparison with Electric Provider&lt;/h2>
&lt;p>Next up, it&amp;rsquo;s time to compare it with my utility provider. This should match as close as possible so ensure that the numbers I show in Home Assistant are actually reflective of reality. My provider shows a daily energy breakdown in my account page (though they round the numbers and I had to use the browser dev tools to see the raw numbers.)&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image.png"
width="1360"
height="496"
srcset="https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image_hu_38900e0ba651482e.png 480w, https://www.technowizardry.net/2023/02/local-energy-monitoring-using-the-emporia-vue-2/images/image_hu_329019336707b868.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="274"
data-flex-basis="658px"
>&lt;/p>
&lt;p>Overall pretty close with a difference of -8.26kWh over the last 30 days and ~2.8%. Not perfect, but I&amp;rsquo;ll continue to monitor and look for opportunities to improve accuracy.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>In this post, I walked through how to use the Emporia Vue2 to monitor my whole home&amp;rsquo;s energy usage, some different strategies to improve accuracy and reduce MQTT traffic.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2023%2F02%2Flocal-energy-monitoring-using-the-emporia-vue-2%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Local+Energy+Monitoring+using+the+Emporia+Vue+2" style="border:0" alt="" /></description></item><item><title>Zeppelin v0.10 not showing matplotlib graphs</title><link>https://www.technowizardry.net/2023/02/zeppelin-v0-10-not-showing-matplotlib/</link><pubDate>Sat, 18 Feb 2023 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2023/02/zeppelin-v0-10-not-showing-matplotlib/</guid><summary>&lt;p>I upgraded to Apache Zeppelin v0.10.x from v0.9.x and randomly my Python Matplotlib scripts stopped rendering images. Anything that called the plot method would just return the string response of the function. Like below:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">%python
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">import matplotlib.pyplot as plt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">plt.plot([1, 2, 3])
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[&amp;lt;matplotlib.lines.Line2D at 0x7ff547624210&amp;gt;]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://www.technowizardry.net/2023/02/zeppelin-v0-10-not-showing-matplotlib/images/Screenshot-2023-02-17-at-10.28.57-PM.png"
width="852"
height="302"
srcset="https://www.technowizardry.net/2023/02/zeppelin-v0-10-not-showing-matplotlib/images/Screenshot-2023-02-17-at-10.28.57-PM_hu_8c69c7bf426a93fa.png 480w, https://www.technowizardry.net/2023/02/zeppelin-v0-10-not-showing-matplotlib/images/Screenshot-2023-02-17-at-10.28.57-PM_hu_bff6a46a3d9c2aa6.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="282"
data-flex-basis="677px"
>&lt;/p>
&lt;p>If this happens to you, just add the following directive after %python:&lt;/p></summary><description>&lt;p>I upgraded to Apache Zeppelin v0.10.x from v0.9.x and randomly my Python Matplotlib scripts stopped rendering images. Anything that called the plot method would just return the string response of the function. Like below:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">%python
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">import matplotlib.pyplot as plt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">plt.plot([1, 2, 3])
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[&amp;lt;matplotlib.lines.Line2D at 0x7ff547624210&amp;gt;]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://www.technowizardry.net/2023/02/zeppelin-v0-10-not-showing-matplotlib/images/Screenshot-2023-02-17-at-10.28.57-PM.png"
width="852"
height="302"
srcset="https://www.technowizardry.net/2023/02/zeppelin-v0-10-not-showing-matplotlib/images/Screenshot-2023-02-17-at-10.28.57-PM_hu_8c69c7bf426a93fa.png 480w, https://www.technowizardry.net/2023/02/zeppelin-v0-10-not-showing-matplotlib/images/Screenshot-2023-02-17-at-10.28.57-PM_hu_bff6a46a3d9c2aa6.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="282"
data-flex-basis="677px"
>&lt;/p>
&lt;p>If this happens to you, just add the following directive after %python:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">%python
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%matplotlib inline
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">import matplotlib.pyplot as plt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">plt.plot([1, 2, 3])
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After that, it should work again:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2023/02/zeppelin-v0-10-not-showing-matplotlib/images/Screenshot-2023-02-17-at-10.33.00-PM.png"
width="976"
height="876"
srcset="https://www.technowizardry.net/2023/02/zeppelin-v0-10-not-showing-matplotlib/images/Screenshot-2023-02-17-at-10.33.00-PM_hu_ed07e551fe412c72.png 480w, https://www.technowizardry.net/2023/02/zeppelin-v0-10-not-showing-matplotlib/images/Screenshot-2023-02-17-at-10.33.00-PM_hu_e0b289076874ec32.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="111"
data-flex-basis="267px"
>&lt;/p>
&lt;p>I&amp;rsquo;m not sure why this seems to happen. After it&amp;rsquo;s applied it seems to apply to all future blocks too.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2023%2F02%2Fzeppelin-v0-10-not-showing-matplotlib%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Zeppelin+v0.10+not+showing+matplotlib+graphs" style="border:0" alt="" /></description></item><item><title>Rewriting Home Assistant Long-term statistics from InfluxDB</title><link>https://www.technowizardry.net/2022/12/rewriting-home-assistant-long-term-statistics-from-influxdb/</link><pubDate>Thu, 01 Dec 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/12/rewriting-home-assistant-long-term-statistics-from-influxdb/</guid><summary>&lt;p>In an &lt;a class="link" href="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/" >earlier post&lt;/a>, I made an error that incorrectly aggregated the energy data which resulted in hugely inflated aggregated energy usage. All the un-aggregated data was accurate, but the sums were wrong. Luckily I had all the raw data stored in InfluxDB and could rebuild it.&lt;/p>
&lt;p>In this post, I walk through how to re-write the Home Assistant Long-term statistics database to fix this mistake.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-8.png" >&lt;img src="https://www.technowizardry.net/2022/12/rewriting-home-assistant-long-term-statistics-from-influxdb/images/image-8.png"
width="333"
height="416"
srcset="https://www.technowizardry.net/2022/12/rewriting-home-assistant-long-term-statistics-from-influxdb/images/image-8_hu_264bd89b71ea90b3.png 480w, https://www.technowizardry.net/2022/12/rewriting-home-assistant-long-term-statistics-from-influxdb/images/image-8_hu_d5b10f20d7ffb420.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="80"
data-flex-basis="192px"
>&lt;/a>&lt;/p></summary><description>&lt;p>In an &lt;a class="link" href="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/" >earlier post&lt;/a>, I made an error that incorrectly aggregated the energy data which resulted in hugely inflated aggregated energy usage. All the un-aggregated data was accurate, but the sums were wrong. Luckily I had all the raw data stored in InfluxDB and could rebuild it.&lt;/p>
&lt;p>In this post, I walk through how to re-write the Home Assistant Long-term statistics database to fix this mistake.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-8.png" >&lt;img src="https://www.technowizardry.net/2022/12/rewriting-home-assistant-long-term-statistics-from-influxdb/images/image-8.png"
width="333"
height="416"
srcset="https://www.technowizardry.net/2022/12/rewriting-home-assistant-long-term-statistics-from-influxdb/images/image-8_hu_264bd89b71ea90b3.png 480w, https://www.technowizardry.net/2022/12/rewriting-home-assistant-long-term-statistics-from-influxdb/images/image-8_hu_d5b10f20d7ffb420.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="80"
data-flex-basis="192px"
>&lt;/a>&lt;/p>
&lt;p>A grossly high electric bill&lt;/p>
&lt;p>First, I opened up the Influx UI and constructed a query to generate the corrected metrics. The HA database requires two fields: &lt;a class="link" href="https://data.home-assistant.io/docs/statistics/" target="_blank" rel="noopener"
>sum and state&lt;/a>. One describes the cumulative running sum, and the other is the sum at the end of the hour. I didn&amp;rsquo;t fully understand how sum differed from state, so I just assumed they were the same.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">from(bucket: &amp;#34;energy-1h&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: -365d, stop: now())
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_measurement&amp;#34;] == &amp;#34;kWh&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_field&amp;#34;] == &amp;#34;value&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;domain&amp;#34;] == &amp;#34;sensor&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;entity_id&amp;#34;] != &amp;#34;main_panel_total_energy&amp;#34; and r[&amp;#34;entity_id&amp;#34;] != &amp;#34;sub_panel_32_total_power&amp;#34; and r[&amp;#34;entity_id&amp;#34;] != &amp;#34;daily_electricity_usage&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; group(columns: [&amp;#34;_measurement&amp;#34;, &amp;#34;_field&amp;#34;])
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; aggregateWindow(every: 1h, fn: sum, createEmpty: false)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; cumulativeSum()
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; yield(name: &amp;#34;mean&amp;#34;)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I ran this query, then clicked download to CSV.&lt;/p>
&lt;p>That gave me a file that looked like the following:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">#group,false,false,true,true,false,false,true,true,true,true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">#default,mean,,,,,,,,,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">,result,table,_start,_stop,_time,_value,_field,_measurement,domain,entity_id
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">,,0,2022-08-01T06:08:15Z,2022-12-01T07:08:15.162Z,2022-08-21T09:00:00Z,2.569999999999993,value,kWh,sensor,main_panel_total_energy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">,,0,2022-08-01T06:08:15Z,2022-12-01T07:08:15.162Z,2022-08-21T10:00:00Z,1.7900000000000063,value,kWh,sensor,main_panel_total_energy
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Delete first 3 lines and one of the two trailing lines.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ tail -n+4 &amp;#39;2022-11-30_23 38_influxdb_data.csv&amp;#39; | head -n-1 &amp;gt; influxdata.csv
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Make sure the file is nearby on the host running HomeAssistant, then stop HA and open up the file:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># sqlite3 home-assistant_v2.db
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">SQLite version 3.31.1 2020-01-27 19:55:54
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Enter &amp;#34;.help&amp;#34; for usage hints.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I looked at my target table schema:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">sqlite&amp;gt; .schema statistics
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">CREATE TABLE statistics (
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> id INTEGER NOT NULL,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> created DATETIME,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> start DATETIME,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> mean FLOAT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> min FLOAT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> max FLOAT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> last_reset DATETIME,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> state FLOAT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> sum FLOAT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> metadata_id INTEGER,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> PRIMARY KEY (id),
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> FOREIGN KEY(metadata_id) REFERENCES statistics_meta (id) ON DELETE CASCADE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">);
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">CREATE UNIQUE INDEX ix_statistics_statistic_id_start ON statistics (metadata_id, start);
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">CREATE INDEX ix_statistics_start ON statistics (start);
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">CREATE INDEX ix_statistics_metadata_id ON statistics (metadata_id);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then import the CSV file into the database&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">sqlite&amp;gt; .mode csv
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sqlite&amp;gt; .import &amp;#34;../influxdata.csv&amp;#34; influx
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sqlite&amp;gt; .schema statistics
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sqlite&amp;gt; .schema influx
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">CREATE TABLE influx(
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;&amp;#34; TEXT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;result&amp;#34; TEXT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;table&amp;#34; TEXT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;_field&amp;#34; TEXT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;_measurement&amp;#34; TEXT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;_start&amp;#34; TEXT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;_stop&amp;#34; TEXT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;_time&amp;#34; TEXT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;_value&amp;#34; TEXT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;domain&amp;#34; TEXT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;entity_id&amp;#34; TEXT,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;state&amp;#34; TEXT
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now we&amp;rsquo;re reading to replace the data. First, find the id of the entity to replace. The first column will be the id&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">sqlite&amp;gt; select * from statistics_meta where statistic_id = &amp;#39;sensor.main_panel_total_energy&amp;#39;;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1,sensor.main_panel_total_energy,recorder,kWh,0,1,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sqlite&amp;gt; delete from statistics where metadata_id = 1;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then insert the data and drop the temp table&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">sqlite&amp;gt; insert into statistics select null AS id, datetime(_time) as created, datetime(_time, &amp;#39;-1 hour&amp;#39;) as start, null as mean, null as min, null as max, null as last_reset, cast(_value as float) as state, cast(_value as float) as sum, 1 as metadata_id from influx;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sqlite&amp;gt; drop table influx;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then delete the history for the energy_cost because at the end of the next hour, HA will distort the metrics again:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">sqlite&amp;gt; delete from states where entity_id = &amp;#39;sensor.main_panel_total_power&amp;#39; or entity_id = &amp;#39;sensor.main_panel_total_energy&amp;#39; or entity_id = &amp;#39;sensor.main_panel_total_energy_cost&amp;#39;;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Home Assistant stores some intermediary data in the statistics_short_term table. This may cause problems and I&amp;rsquo;ve had to delete those records sometimes&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">sqlite&amp;gt; delete from statistics_short_term where metadata_id = 1;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then open home-assistant/.storage/core.restore_state and change the entry for main_panel_total_energy to match the latest value from InfluxDB.&lt;/p>
&lt;p>Restart Home Assistant.&lt;/p>
&lt;p>Voilà. The data is corrected:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-9.png" >&lt;img src="https://www.technowizardry.net/2022/12/rewriting-home-assistant-long-term-statistics-from-influxdb/images/image-9.png"
width="421"
height="468"
srcset="https://www.technowizardry.net/2022/12/rewriting-home-assistant-long-term-statistics-from-influxdb/images/image-9_hu_9afb063c2f6e5841.png 480w, https://www.technowizardry.net/2022/12/rewriting-home-assistant-long-term-statistics-from-influxdb/images/image-9_hu_f7fa42805fc376f4.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="89"
data-flex-basis="215px"
>&lt;/a>&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F12%2Frewriting-home-assistant-long-term-statistics-from-influxdb%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Rewriting+Home+Assistant+Long-term+statistics+from+InfluxDB" style="border:0" alt="" /></description></item><item><title>From a random Kubernetes control plane crash to a new RAID array</title><link>https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/</link><pubDate>Mon, 14 Nov 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/</guid><summary>&lt;p>My external cluster runs on 3 different dedicated servers (most from &lt;a class="link" href="https://soyoustart.com/" target="_blank" rel="noopener"
>SoYouStart.com&lt;/a>.) I have 3 machines since the Kubernetes control plane needs 3 or more to be able to have a quorum and be able to handle any one machine going down. If one machine goes down, then the other two maintain a majority and can agree on the state of the cluster.&lt;/p>
&lt;p>I randomly encountered issues where the Kubernetes control plane of Rancher UI would crash and restart. While this cluster didn&amp;rsquo;t really matter, it still annoyed me and wanted to figure it out.&lt;/p></summary><description>&lt;p>My external cluster runs on 3 different dedicated servers (most from &lt;a class="link" href="https://soyoustart.com/" target="_blank" rel="noopener"
>SoYouStart.com&lt;/a>.) I have 3 machines since the Kubernetes control plane needs 3 or more to be able to have a quorum and be able to handle any one machine going down. If one machine goes down, then the other two maintain a majority and can agree on the state of the cluster.&lt;/p>
&lt;p>I randomly encountered issues where the Kubernetes control plane of Rancher UI would crash and restart. While this cluster didn&amp;rsquo;t really matter, it still annoyed me and wanted to figure it out.&lt;/p>
&lt;p>I narrowed it down to one single host and documented the steps I took to resolve this issue which seems to be have been caused by one machine using HDDs and all other hosts using SSDs.&lt;/p>
&lt;h2 id="the-symptoms">The Symptoms&lt;/h2>
&lt;p>The first thing that I did was looked at all the hosts and noticed that one host (srv6) showed that a few Kubernetes control plane containers were restarting.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">ccb77f64413b&lt;/span> &lt;span class="n">rancher&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">hyperkube&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">v1&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mf">24.4&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">rancher1&lt;/span> &lt;span class="s2">&amp;#34;/opt/rke-tools/entr…&amp;#34;&lt;/span> &lt;span class="mi">5&lt;/span> &lt;span class="n">weeks&lt;/span> &lt;span class="n">ago&lt;/span> &lt;span class="n">Up&lt;/span> &lt;span class="mi">24&lt;/span> &lt;span class="n">hours&lt;/span> &lt;span class="n">kube&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">proxy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">08&lt;/span>&lt;span class="n">ec5c6161eb&lt;/span> &lt;span class="n">rancher&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">hyperkube&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">v1&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mf">24.4&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">rancher1&lt;/span> &lt;span class="s2">&amp;#34;/opt/rke-tools/entr…&amp;#34;&lt;/span> &lt;span class="mi">5&lt;/span> &lt;span class="n">weeks&lt;/span> &lt;span class="n">ago&lt;/span> &lt;span class="n">Up&lt;/span> &lt;span class="mi">24&lt;/span> &lt;span class="n">hours&lt;/span> &lt;span class="n">kubelet&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">88&lt;/span>&lt;span class="n">eaa48e458f&lt;/span> &lt;span class="n">rancher&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">hyperkube&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">v1&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mf">24.4&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">rancher1&lt;/span> &lt;span class="s2">&amp;#34;/opt/rke-tools/entr…&amp;#34;&lt;/span> &lt;span class="mi">5&lt;/span> &lt;span class="n">weeks&lt;/span> &lt;span class="n">ago&lt;/span> &lt;span class="n">Up&lt;/span> &lt;span class="mi">15&lt;/span> &lt;span class="n">hours&lt;/span> &lt;span class="n">kube&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">scheduler&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">4&lt;/span>&lt;span class="n">d603be7b4ef&lt;/span> &lt;span class="n">rancher&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">hyperkube&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">v1&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mf">24.4&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">rancher1&lt;/span> &lt;span class="s2">&amp;#34;/opt/rke-tools/entr…&amp;#34;&lt;/span> &lt;span class="mi">5&lt;/span> &lt;span class="n">weeks&lt;/span> &lt;span class="n">ago&lt;/span> &lt;span class="n">Up&lt;/span> &lt;span class="mi">4&lt;/span> &lt;span class="n">hours&lt;/span> &lt;span class="n">kube&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">controller&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">manager&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">dc86384410c2&lt;/span> &lt;span class="n">rancher&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">hyperkube&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">v1&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mf">24.4&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">rancher1&lt;/span> &lt;span class="s2">&amp;#34;/opt/rke-tools/entr…&amp;#34;&lt;/span> &lt;span class="mi">5&lt;/span> &lt;span class="n">weeks&lt;/span> &lt;span class="n">ago&lt;/span> &lt;span class="n">Up&lt;/span> &lt;span class="mi">24&lt;/span> &lt;span class="n">hours&lt;/span> &lt;span class="n">kube&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">apiserver&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">05&lt;/span>&lt;span class="n">df16372b51&lt;/span> &lt;span class="n">rancher&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">mirrored&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">coreos&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">etcd&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">v3&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mf">5.4&lt;/span> &lt;span class="s2">&amp;#34;/usr/local/bin/etcd…&amp;#34;&lt;/span> &lt;span class="mi">5&lt;/span> &lt;span class="n">weeks&lt;/span> &lt;span class="n">ago&lt;/span> &lt;span class="n">Up&lt;/span> &lt;span class="mi">24&lt;/span> &lt;span class="n">hours&lt;/span> &lt;span class="n">etcd&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">$&lt;/span> &lt;span class="n">docker&lt;/span> &lt;span class="n">inspect&lt;/span> &lt;span class="n">kube&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">controller&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">manager&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;State&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;StartedAt&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2022-11-12T02:30:35.889344997Z&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;FinishedAt&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2022-11-12T02:30:31.970955836Z&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I was already running &lt;a class="link" href="https://grafana.com/oss/loki/" target="_blank" rel="noopener"
>Grafana Loki&lt;/a>, which aggregated all of my Docker container logs from all machines and zoomed into the time when the container last restarted:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-1.png" >&lt;img src="https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/images/image-1.png"
width="841"
height="172"
srcset="https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/images/image-1_hu_f550bdc90cb69740.png 480w, https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/images/image-1_hu_f2a42fb99bec9aa2.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="488"
data-flex-basis="1173px"
>&lt;/a>&lt;/p>
&lt;p>{docker_container=~&amp;quot;/kube-apiserver|/kube-controller-manager&amp;quot;} |= ``&lt;/p>
&lt;p>I scrolled through the logs and started seeing some smoke. A request to the kube-apiserver was timing out. kube-controller-manager calls kube-apiserver, which then calls etcd.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">2022-11-11T18:30:25-08:00 E1112 02:30:25.799867 1 leaderelection.go:330] error retrieving resource lock kube-system/kube-controller-manager: Get &amp;#34;https://127.0.0.1:6443/apis/coordination.k8s.io/v1/namespaces/kube-system/leases/kube-controller-manager?timeout=5s&amp;#34;: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2022-11-11T18:30:26-08:00 Trace[1355871407]: [5.002442529s] [5.002442529s] END
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2022-11-11T18:30:26-08:00 E1112 02:30:26.701570 1 timeout.go:141] post-timeout activity - time-elapsed: 6.504883ms, GET &amp;#34;/apis/coordination.k8s.io/v1/namespaces/kube-system/leases/kube-controller-manager&amp;#34; result: &amp;lt;nil&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Looking in the etcd logs, I found some log statements saying &amp;ldquo;apply request took too long&amp;rdquo;. According to the &lt;a class="link" href="https://etcd.io/docs/v3.1/faq/" target="_blank" rel="noopener"
>etcd docs&lt;/a>, this error statement is printed when the average time to write a change to disk exceeds 100ms. Since at least 2 hosts need to acknowledge a write for quorum, this caused these writes to slow down.&lt;/p>
&lt;p>I searched across the hosts for this log statement and found that one particular host (srv6) gave this error far more than any other host.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-4.png" >&lt;img src="https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/images/image-4-1024x345.png"
width="1024"
height="345"
srcset="https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/images/image-4-1024x345_hu_6bcdf4656fe274a5.png 480w, https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/images/image-4-1024x345_hu_b0b56370e4d24984.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="296"
data-flex-basis="712px"
>&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">sum by(host) (count_over_time({docker_container=&amp;#34;/etcd&amp;#34;} |= `apply request took too long` [1h]))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The biggest difference between these hosts is that everything else has SSDs, but this one has spinning rust HDDs (4x 2TB HDDs in RAID1.) Is the hard drive dying?&lt;/p>
&lt;h2 id="hard-drive-problems">Hard Drive Problems?&lt;/h2>
&lt;p>Next, I investigated further to see if there&amp;rsquo;s something wrong with the drives. The following Prometheus query gets the average write latency of I/O to the disk.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">sum by(instance) (increase(node_disk_write_time_seconds_total[1m])) / sum by(instance) (increase(node_disk_writes_completed_total[1m]))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;a class="link" href="images/image-2.png" >&lt;img src="https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/images/image-2.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/images/image-2_hu_4f33d2c1eb2c8866.png 480w, https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/images/image-2_hu_aabf872e17eb3f06.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p>
&lt;p>Here we see this host showing spikes in write latency every 30 minutes to the &lt;code>md2&lt;/code> disk, but my &lt;code>sd[a-d]&lt;/code> drives show reasonable latency: &amp;lt;50ms. The &lt;code>md2&lt;/code> is a RAID1 array that mirrors to sda, sdb, sdc, and sdd.&lt;/p>
&lt;p>A couple ideas come to mind:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>There&amp;rsquo;s a spike every 30 minutes with smaller spikes every 5 minutes. Possibly caused by scheduled jobs&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Only &lt;code>md2&lt;/code> is spiking, but &lt;code>sd[a-d]&lt;/code> are not spiking as much. Not sure why the RAID virtual array spikes if the underlying drives are not seeing a similar spike.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="okay-by-whats-md2">Okay by what&amp;rsquo;s md2?&lt;/h2>
&lt;p>&lt;code>md2&lt;/code> is a Linux software RAID setup. It&amp;rsquo;s configured at RAID 1 across all 4 drives. When I first setup this machine, I didn&amp;rsquo;t put much thought into what kind of RAID setup I wanted, I just picked some default values in the server UI and this is what I ended up with.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">mdadm --detail /dev/md2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/dev/md2:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Version : 1.2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Creation Time : Tue Dec 7 07:24:59 2021
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Raid Level : raid1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Array Size : 1952332800 (1861.89 GiB 1999.19 GB)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Used Dev Size : 1952332800 (1861.89 GiB 1999.19 GB)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Raid Devices : 4
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Total Devices : 4
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Persistence : Superblock is persistent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Intent Bitmap : Internal
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Update Time : Sun Nov 13 02:48:53 2022
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> State : active
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Active Devices : 4
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Working Devices : 4
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Failed Devices : 0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Spare Devices : 0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Consistency Policy : bitmap
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Name : md2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> UUID : d49251e3:7091f035:ef641ea4:bff15c7f
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Events : 78807
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Number Major Minor RaidDevice State
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 0 8 34 0 active sync /dev/sdc2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 1 8 50 1 active sync /dev/sdd2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 2 8 2 2 active sync /dev/sda2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 3 8 18 3 active sync /dev/sdb2
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="checking-smart">Checking SMART&lt;/h2>
&lt;p>Alright so nothing stands out in the RAID array yet. We can check the health of the drives using &lt;a class="link" href="https://en.wikipedia.org/wiki/Self-Monitoring,_Analysis,_and_Reporting_Technology" target="_blank" rel="noopener"
>SMART&lt;/a> using &lt;a class="link" href="https://www.smartmontools.org/" target="_blank" rel="noopener"
>smartmontools&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;span class="lnt">68
&lt;/span>&lt;span class="lnt">69
&lt;/span>&lt;span class="lnt">70
&lt;/span>&lt;span class="lnt">71
&lt;/span>&lt;span class="lnt">72
&lt;/span>&lt;span class="lnt">73
&lt;/span>&lt;span class="lnt">74
&lt;/span>&lt;span class="lnt">75
&lt;/span>&lt;span class="lnt">76
&lt;/span>&lt;span class="lnt">77
&lt;/span>&lt;span class="lnt">78
&lt;/span>&lt;span class="lnt">79
&lt;/span>&lt;span class="lnt">80
&lt;/span>&lt;span class="lnt">81
&lt;/span>&lt;span class="lnt">82
&lt;/span>&lt;span class="lnt">83
&lt;/span>&lt;span class="lnt">84
&lt;/span>&lt;span class="lnt">85
&lt;/span>&lt;span class="lnt">86
&lt;/span>&lt;span class="lnt">87
&lt;/span>&lt;span class="lnt">88
&lt;/span>&lt;span class="lnt">89
&lt;/span>&lt;span class="lnt">90
&lt;/span>&lt;span class="lnt">91
&lt;/span>&lt;span class="lnt">92
&lt;/span>&lt;span class="lnt">93
&lt;/span>&lt;span class="lnt">94
&lt;/span>&lt;span class="lnt">95
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">sudo&lt;/span> &lt;span class="n">apt&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">get&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">smartmontools&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sudo&lt;/span> &lt;span class="n">smartctl&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">a&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sda&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">smartctl&lt;/span> &lt;span class="mf">7.2&lt;/span> &lt;span class="mi">2020&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">12&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">30&lt;/span> &lt;span class="n">r5155&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">x86_64&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">linux&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mf">5.15&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">52&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">generic&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">local&lt;/span> &lt;span class="n">build&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Copyright&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mi">2002&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">20&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Bruce&lt;/span> &lt;span class="n">Allen&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Christian&lt;/span> &lt;span class="n">Franke&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">www&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">smartmontools&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">org&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">===&lt;/span> &lt;span class="n">START&lt;/span> &lt;span class="n">OF&lt;/span> &lt;span class="n">INFORMATION&lt;/span> &lt;span class="n">SECTION&lt;/span> &lt;span class="o">===&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">===&lt;/span> &lt;span class="n">START&lt;/span> &lt;span class="n">OF&lt;/span> &lt;span class="n">READ&lt;/span> &lt;span class="n">SMART&lt;/span> &lt;span class="n">DATA&lt;/span> &lt;span class="n">SECTION&lt;/span> &lt;span class="o">===&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">SMART&lt;/span> &lt;span class="n">overall&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">health&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">assessment&lt;/span> &lt;span class="n">test&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">PASSED&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">General&lt;/span> &lt;span class="n">SMART&lt;/span> &lt;span class="n">Values&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Offline&lt;/span> &lt;span class="n">data&lt;/span> &lt;span class="n">collection&lt;/span> &lt;span class="n">status&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mh">0x84&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">Offline&lt;/span> &lt;span class="n">data&lt;/span> &lt;span class="n">collection&lt;/span> &lt;span class="n">activity&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">was&lt;/span> &lt;span class="n">suspended&lt;/span> &lt;span class="n">by&lt;/span> &lt;span class="n">an&lt;/span> &lt;span class="n">interrupting&lt;/span> &lt;span class="n">command&lt;/span> &lt;span class="n">from&lt;/span> &lt;span class="n">host&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Auto&lt;/span> &lt;span class="n">Offline&lt;/span> &lt;span class="n">Data&lt;/span> &lt;span class="n">Collection&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Enabled&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Self&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span> &lt;span class="n">execution&lt;/span> &lt;span class="n">status&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">(&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">The&lt;/span> &lt;span class="n">previous&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span> &lt;span class="n">routine&lt;/span> &lt;span class="n">completed&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">without&lt;/span> &lt;span class="n">error&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">no&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span> &lt;span class="n">has&lt;/span> &lt;span class="n">ever&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">been&lt;/span> &lt;span class="n">run&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Total&lt;/span> &lt;span class="n">time&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">complete&lt;/span> &lt;span class="n">Offline&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">data&lt;/span> &lt;span class="n">collection&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">(&lt;/span> &lt;span class="mi">24&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">seconds&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Offline&lt;/span> &lt;span class="n">data&lt;/span> &lt;span class="n">collection&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">capabilities&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mh">0x5b&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">SMART&lt;/span> &lt;span class="n">execute&lt;/span> &lt;span class="n">Offline&lt;/span> &lt;span class="n">immediate&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Auto&lt;/span> &lt;span class="n">Offline&lt;/span> &lt;span class="n">data&lt;/span> &lt;span class="n">collection&lt;/span> &lt;span class="n">on&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">off&lt;/span> &lt;span class="n">support&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Suspend&lt;/span> &lt;span class="n">Offline&lt;/span> &lt;span class="n">collection&lt;/span> &lt;span class="n">upon&lt;/span> &lt;span class="n">new&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">command&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Offline&lt;/span> &lt;span class="n">surface&lt;/span> &lt;span class="n">scan&lt;/span> &lt;span class="n">supported&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Self&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span> &lt;span class="n">supported&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">No&lt;/span> &lt;span class="n">Conveyance&lt;/span> &lt;span class="n">Self&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span> &lt;span class="n">supported&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Selective&lt;/span> &lt;span class="n">Self&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span> &lt;span class="n">supported&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">SMART&lt;/span> &lt;span class="n">capabilities&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mh">0x0003&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">Saves&lt;/span> &lt;span class="n">SMART&lt;/span> &lt;span class="n">data&lt;/span> &lt;span class="n">before&lt;/span> &lt;span class="n">entering&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">power&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">saving&lt;/span> &lt;span class="n">mode&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Supports&lt;/span> &lt;span class="n">SMART&lt;/span> &lt;span class="n">auto&lt;/span> &lt;span class="n">save&lt;/span> &lt;span class="n">timer&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Error&lt;/span> &lt;span class="n">logging&lt;/span> &lt;span class="n">capability&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mh">0x01&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">Error&lt;/span> &lt;span class="n">logging&lt;/span> &lt;span class="n">supported&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">General&lt;/span> &lt;span class="n">Purpose&lt;/span> &lt;span class="n">Logging&lt;/span> &lt;span class="n">supported&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Short&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span> &lt;span class="n">routine&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">recommended&lt;/span> &lt;span class="n">polling&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">(&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">minutes&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Extended&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span> &lt;span class="n">routine&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">recommended&lt;/span> &lt;span class="n">polling&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">(&lt;/span> &lt;span class="mi">309&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">minutes&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">SCT&lt;/span> &lt;span class="n">capabilities&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mh">0x003d&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">SCT&lt;/span> &lt;span class="n">Status&lt;/span> &lt;span class="n">supported&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">SCT&lt;/span> &lt;span class="n">Error&lt;/span> &lt;span class="n">Recovery&lt;/span> &lt;span class="ne">Control&lt;/span> &lt;span class="n">supported&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">SCT&lt;/span> &lt;span class="n">Feature&lt;/span> &lt;span class="ne">Control&lt;/span> &lt;span class="n">supported&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">SCT&lt;/span> &lt;span class="n">Data&lt;/span> &lt;span class="n">Table&lt;/span> &lt;span class="n">supported&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">SMART&lt;/span> &lt;span class="n">Attributes&lt;/span> &lt;span class="n">Data&lt;/span> &lt;span class="n">Structure&lt;/span> &lt;span class="n">revision&lt;/span> &lt;span class="n">number&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">16&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Vendor&lt;/span> &lt;span class="n">Specific&lt;/span> &lt;span class="n">SMART&lt;/span> &lt;span class="n">Attributes&lt;/span> &lt;span class="n">with&lt;/span> &lt;span class="n">Thresholds&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ID&lt;/span>&lt;span class="c1"># ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW_VALUE&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">1&lt;/span> &lt;span class="n">Raw_Read_Error_Rate&lt;/span> &lt;span class="mh">0x000b&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">016&lt;/span> &lt;span class="n">Pre&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">fail&lt;/span> &lt;span class="n">Always&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">2&lt;/span> &lt;span class="n">Throughput_Performance&lt;/span> &lt;span class="mh">0x0005&lt;/span> &lt;span class="mi">137&lt;/span> &lt;span class="mi">137&lt;/span> &lt;span class="mi">054&lt;/span> &lt;span class="n">Pre&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">fail&lt;/span> &lt;span class="n">Offline&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">79&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">3&lt;/span> &lt;span class="n">Spin_Up_Time&lt;/span> &lt;span class="mh">0x0007&lt;/span> &lt;span class="mi">216&lt;/span> &lt;span class="mi">216&lt;/span> &lt;span class="mi">024&lt;/span> &lt;span class="n">Pre&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">fail&lt;/span> &lt;span class="n">Always&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">314&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">Average&lt;/span> &lt;span class="mi">264&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">4&lt;/span> &lt;span class="n">Start_Stop_Count&lt;/span> &lt;span class="mh">0x0012&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">000&lt;/span> &lt;span class="n">Old_age&lt;/span> &lt;span class="n">Always&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">105&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">5&lt;/span> &lt;span class="n">Reallocated_Sector_Ct&lt;/span> &lt;span class="mh">0x0033&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">005&lt;/span> &lt;span class="n">Pre&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">fail&lt;/span> &lt;span class="n">Always&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">7&lt;/span> &lt;span class="n">Seek_Error_Rate&lt;/span> &lt;span class="mh">0x000b&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">067&lt;/span> &lt;span class="n">Pre&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">fail&lt;/span> &lt;span class="n">Always&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">8&lt;/span> &lt;span class="n">Seek_Time_Performance&lt;/span> &lt;span class="mh">0x0005&lt;/span> &lt;span class="mi">142&lt;/span> &lt;span class="mi">142&lt;/span> &lt;span class="mi">020&lt;/span> &lt;span class="n">Pre&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">fail&lt;/span> &lt;span class="n">Offline&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">25&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">9&lt;/span> &lt;span class="n">Power_On_Hours&lt;/span> &lt;span class="mh">0x0012&lt;/span> &lt;span class="mi">092&lt;/span> &lt;span class="mi">092&lt;/span> &lt;span class="mi">000&lt;/span> &lt;span class="n">Old_age&lt;/span> &lt;span class="n">Always&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">59245&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">10&lt;/span> &lt;span class="n">Spin_Retry_Count&lt;/span> &lt;span class="mh">0x0013&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">060&lt;/span> &lt;span class="n">Pre&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">fail&lt;/span> &lt;span class="n">Always&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">12&lt;/span> &lt;span class="n">Power_Cycle_Count&lt;/span> &lt;span class="mh">0x0032&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">000&lt;/span> &lt;span class="n">Old_age&lt;/span> &lt;span class="n">Always&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">104&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">192&lt;/span> &lt;span class="n">Power&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">Off_Retract_Count&lt;/span> &lt;span class="mh">0x0032&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">000&lt;/span> &lt;span class="n">Old_age&lt;/span> &lt;span class="n">Always&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">225&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">193&lt;/span> &lt;span class="n">Load_Cycle_Count&lt;/span> &lt;span class="mh">0x0012&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">000&lt;/span> &lt;span class="n">Old_age&lt;/span> &lt;span class="n">Always&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">225&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">194&lt;/span> &lt;span class="n">Temperature_Celsius&lt;/span> &lt;span class="mh">0x0002&lt;/span> &lt;span class="mi">176&lt;/span> &lt;span class="mi">176&lt;/span> &lt;span class="mi">000&lt;/span> &lt;span class="n">Old_age&lt;/span> &lt;span class="n">Always&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">34&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">Min&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">Max&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="mi">52&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">196&lt;/span> &lt;span class="n">Reallocated_Event_Count&lt;/span> &lt;span class="mh">0x0032&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">000&lt;/span> &lt;span class="n">Old_age&lt;/span> &lt;span class="n">Always&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">197&lt;/span> &lt;span class="n">Current_Pending_Sector&lt;/span> &lt;span class="mh">0x0022&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">000&lt;/span> &lt;span class="n">Old_age&lt;/span> &lt;span class="n">Always&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">198&lt;/span> &lt;span class="n">Offline_Uncorrectable&lt;/span> &lt;span class="mh">0x0008&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="mi">000&lt;/span> &lt;span class="n">Old_age&lt;/span> &lt;span class="n">Offline&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">199&lt;/span> &lt;span class="n">UDMA_CRC_Error_Count&lt;/span> &lt;span class="mh">0x000a&lt;/span> &lt;span class="mi">200&lt;/span> &lt;span class="mi">200&lt;/span> &lt;span class="mi">000&lt;/span> &lt;span class="n">Old_age&lt;/span> &lt;span class="n">Always&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">SMART&lt;/span> &lt;span class="n">Error&lt;/span> &lt;span class="n">Log&lt;/span> &lt;span class="n">Version&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">No&lt;/span> &lt;span class="n">Errors&lt;/span> &lt;span class="n">Logged&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">SMART&lt;/span> &lt;span class="n">Self&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span> &lt;span class="nb">log&lt;/span> &lt;span class="n">structure&lt;/span> &lt;span class="n">revision&lt;/span> &lt;span class="n">number&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Num&lt;/span> &lt;span class="n">Test_Description&lt;/span> &lt;span class="n">Status&lt;/span> &lt;span class="n">Remaining&lt;/span> &lt;span class="n">LifeTime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hours&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">LBA_of_first_error&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1 Short offline Completed without error 00% 51074 -&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2 Short offline Completed without error 00% 51066 -&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3 Short offline Completed without error 00% 51065 -&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 4 Short offline Completed without error 00% 51065 -&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 5 Short offline Completed without error 00% 51052 -&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 6 Short offline Completed without error 00% 51044 -&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 7 Short offline Completed without error 00% 51044 -&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 8 Short offline Completed without error 00% 51036 -&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 9 Short offline Completed without error 00% 51035 -&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#10 Short offline Completed without error 00% 50512 -&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#11 Short offline Completed without error 00% 50495 -&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#12 Short offline Completed without error 00% 50487 -&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">SMART&lt;/span> &lt;span class="n">Selective&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span> &lt;span class="nb">log&lt;/span> &lt;span class="n">data&lt;/span> &lt;span class="n">structure&lt;/span> &lt;span class="n">revision&lt;/span> &lt;span class="n">number&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">SPAN&lt;/span> &lt;span class="n">MIN_LBA&lt;/span> &lt;span class="n">MAX_LBA&lt;/span> &lt;span class="n">CURRENT_TEST_STATUS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">1&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="n">Not_testing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">2&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="n">Not_testing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">3&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="n">Not_testing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">4&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="n">Not_testing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">5&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="n">Not_testing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Selective&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span> &lt;span class="n">flags&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mh">0x0&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">After&lt;/span> &lt;span class="n">scanning&lt;/span> &lt;span class="n">selected&lt;/span> &lt;span class="n">spans&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">do&lt;/span> &lt;span class="n">NOT&lt;/span> &lt;span class="n">read&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">scan&lt;/span> &lt;span class="n">remainder&lt;/span> &lt;span class="n">of&lt;/span> &lt;span class="n">disk&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">If&lt;/span> &lt;span class="n">Selective&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span> &lt;span class="n">is&lt;/span> &lt;span class="n">pending&lt;/span> &lt;span class="n">on&lt;/span> &lt;span class="n">power&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">up&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">resume&lt;/span> &lt;span class="n">after&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="n">minute&lt;/span> &lt;span class="n">delay&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Checking&lt;/span> &lt;span class="n">all&lt;/span> &lt;span class="n">four&lt;/span> &lt;span class="n">hosts&lt;/span> &lt;span class="n">didn&lt;/span>&lt;span class="s1">&amp;#39;t show any SMART concerns and said, while they&amp;#39;&lt;/span>&lt;span class="n">re&lt;/span> &lt;span class="n">older&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">they&lt;/span>&lt;span class="s1">&amp;#39;re still healthy.&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="scheduled-jobs">Scheduled Jobs?&lt;/h2>
&lt;p>What about the regular spikes on the clocks of every 30 minutes and smaller ones every 5 minutes. My host will be running scheduled jobs in Kubernetes and possibly in crontab. Let&amp;rsquo;s take a look at Kubernetes first:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">kubectl --context=local get cronjobs --all-namespaces
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAMESPACE NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">longhorn-system daily-backup 43 10 * * * False 0 16h 70d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">longhorn-system weekly-backup 0 2 * * SUN False 0 53m 70d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">test testjob */30 * * * * False 0 23m 237d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">modtalk wordpress-cron */5 * * * * False 0 171m 119d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">openzipkin zipkin-dependencies 25 5/6 * * * False 0 3h28m 82d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">testjob regular-job 0 3/12 * * * False 0 11h 237d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sentry cleanup */5 * * * * False 0 168m 237d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sentry snuba-cleanup */5 * * * * False 0 8h 237d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sentry snuba-transactions-cleanup */5 * * * * False 0 8h 237d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">technowizardry wordpress-cron */5 * * * * False 0 51m 237d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>There were a number of jobs running every 5 minutes. I changed them to run less frequently and saw a reduction in write latency during the 5 minute spikes, but still saw spikes every time a CronJob ran.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-5.png" >&lt;img src="https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/images/image-5.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/images/image-5_hu_36929289c4c2c669.png 480w, https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/images/image-5_hu_93aa63be565f05d2.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p>
&lt;p>Great so, the problem is because a CronJob starting is causing a large amount of disk I/O and slowing down writes for etcd.&lt;/p>
&lt;h2 id="proposal">Proposal&lt;/h2>
&lt;p>My hypothesis based on the above information is that the hard drives are getting overloaded with the amount of write or read write work meaning that higher priority I/O from Kubernetes&amp;rsquo; etcd system is taking too long and is causing problems.&lt;/p>
&lt;p>RAID1 will mirror the hard drive contents across all four hard drives. This is good for read performance since it can balance reads across all 4 drives, but writes slow down to the slowest drive. In a 4 drive system, this can be problematic.&lt;/p>
&lt;p>Instead, I&amp;rsquo;m going to break the 4 drive mirror array into two, two-drive RAID1 arrays and move certain performance critical data to the second array and leave the first one for&lt;/p>
&lt;h2 id="splitting-raid">Splitting RAID&lt;/h2>
&lt;p>Following &lt;a class="link" href="https://www.thegeekdiary.com/replacing-a-failed-mirror-disk-in-a-software-raid-array-mdadm/" target="_blank" rel="noopener"
>a guide&lt;/a>, I took out two drives from the existing array.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;span class="lnt">68
&lt;/span>&lt;span class="lnt">69
&lt;/span>&lt;span class="lnt">70
&lt;/span>&lt;span class="lnt">71
&lt;/span>&lt;span class="lnt">72
&lt;/span>&lt;span class="lnt">73
&lt;/span>&lt;span class="lnt">74
&lt;/span>&lt;span class="lnt">75
&lt;/span>&lt;span class="lnt">76
&lt;/span>&lt;span class="lnt">77
&lt;/span>&lt;span class="lnt">78
&lt;/span>&lt;span class="lnt">79
&lt;/span>&lt;span class="lnt">80
&lt;/span>&lt;span class="lnt">81
&lt;/span>&lt;span class="lnt">82
&lt;/span>&lt;span class="lnt">83
&lt;/span>&lt;span class="lnt">84
&lt;/span>&lt;span class="lnt">85
&lt;/span>&lt;span class="lnt">86
&lt;/span>&lt;span class="lnt">87
&lt;/span>&lt;span class="lnt">88
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">manage&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md2&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">fail&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sdc2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">set&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sdc2&lt;/span> &lt;span class="n">faulty&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">manage&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md2&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">fail&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sdd2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">set&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sdd2&lt;/span> &lt;span class="n">faulty&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">manage&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md2&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">remove&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sdc2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">hot&lt;/span> &lt;span class="n">removed&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sdc2&lt;/span> &lt;span class="n">from&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">manage&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md2&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">remove&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sdd2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">hot&lt;/span> &lt;span class="n">removed&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sdd2&lt;/span> &lt;span class="n">from&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">grow&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">n&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">raid_disks&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md2&lt;/span> &lt;span class="n">set&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="mi">3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">detail&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md2&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Version&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mf">1.2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Creation&lt;/span> &lt;span class="n">Time&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">Tue&lt;/span> &lt;span class="n">Dec&lt;/span> &lt;span class="mi">7&lt;/span> &lt;span class="mi">07&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">24&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">59&lt;/span> &lt;span class="mi">2021&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Raid&lt;/span> &lt;span class="n">Level&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">raid1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="ne">Array&lt;/span> &lt;span class="n">Size&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mi">3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Used&lt;/span> &lt;span class="n">Dev&lt;/span> &lt;span class="n">Size&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mi">1952332800&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mf">1861.89&lt;/span> &lt;span class="n">GiB&lt;/span> &lt;span class="mf">1999.19&lt;/span> &lt;span class="n">GB&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Raid&lt;/span> &lt;span class="n">Devices&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Total&lt;/span> &lt;span class="n">Devices&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Persistence&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">Superblock&lt;/span> &lt;span class="n">is&lt;/span> &lt;span class="n">persistent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Intent&lt;/span> &lt;span class="n">Bitmap&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">Internal&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Update&lt;/span> &lt;span class="n">Time&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">Sun&lt;/span> &lt;span class="n">Nov&lt;/span> &lt;span class="mi">13&lt;/span> &lt;span class="mi">05&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">07&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">35&lt;/span> &lt;span class="mi">2022&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">State&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">clean&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Active&lt;/span> &lt;span class="n">Devices&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Working&lt;/span> &lt;span class="n">Devices&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Failed&lt;/span> &lt;span class="n">Devices&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Spare&lt;/span> &lt;span class="n">Devices&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Consistency&lt;/span> &lt;span class="n">Policy&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">bitmap&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Name&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">md2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">UUID&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">d49251e3&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">7091&lt;/span>&lt;span class="n">f035&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">ef641ea4&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">bff15c7f&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Events&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mi">79300&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Number&lt;/span> &lt;span class="n">Major&lt;/span> &lt;span class="n">Minor&lt;/span> &lt;span class="n">RaidDevice&lt;/span> &lt;span class="n">State&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">2&lt;/span> &lt;span class="mi">8&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="n">active&lt;/span> &lt;span class="n">sync&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sda2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">3&lt;/span> &lt;span class="mi">8&lt;/span> &lt;span class="mi">18&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="n">active&lt;/span> &lt;span class="n">sync&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sdb2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">## Creating the new array&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Next&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">I&lt;/span> &lt;span class="n">zeroed&lt;/span> &lt;span class="n">out&lt;/span> &lt;span class="n">the&lt;/span> &lt;span class="n">drives&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">created&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="n">new&lt;/span> &lt;span class="n">array&lt;/span> &lt;span class="n">called&lt;/span> &lt;span class="n">md3&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">zero&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">superblock&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sdc2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">zero&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">superblock&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sdd2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">create&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">verbose&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md3&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">level&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">raid&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">devices&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sdc2&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sdd2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Note&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">this&lt;/span> &lt;span class="n">array&lt;/span> &lt;span class="n">has&lt;/span> &lt;span class="n">metadata&lt;/span> &lt;span class="n">at&lt;/span> &lt;span class="n">the&lt;/span> &lt;span class="n">start&lt;/span> &lt;span class="ow">and&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">may&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">be&lt;/span> &lt;span class="n">suitable&lt;/span> &lt;span class="n">as&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="n">boot&lt;/span> &lt;span class="n">device&lt;/span>&lt;span class="o">.&lt;/span> &lt;span class="n">If&lt;/span> &lt;span class="n">you&lt;/span> &lt;span class="n">plan&lt;/span> &lt;span class="n">to&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">store&lt;/span> &lt;span class="s1">&amp;#39;/boot&amp;#39;&lt;/span> &lt;span class="n">on&lt;/span> &lt;span class="n">this&lt;/span> &lt;span class="n">device&lt;/span> &lt;span class="n">please&lt;/span> &lt;span class="n">ensure&lt;/span> &lt;span class="n">that&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">your&lt;/span> &lt;span class="n">boot&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">loader&lt;/span> &lt;span class="n">understands&lt;/span> &lt;span class="n">md&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">v1&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="n">metadata&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">use&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">--&lt;/span>&lt;span class="n">metadata&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">0.90&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">size&lt;/span> &lt;span class="n">set&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="mi">1952331776&lt;/span>&lt;span class="n">K&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">automatically&lt;/span> &lt;span class="n">enabling&lt;/span> &lt;span class="n">write&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">intent&lt;/span> &lt;span class="n">bitmap&lt;/span> &lt;span class="n">on&lt;/span> &lt;span class="n">large&lt;/span> &lt;span class="n">array&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Continue&lt;/span> &lt;span class="n">creating&lt;/span> &lt;span class="n">array&lt;/span>&lt;span class="err">?&lt;/span> &lt;span class="n">y&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Defaulting&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">version&lt;/span> &lt;span class="mf">1.2&lt;/span> &lt;span class="n">metadata&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">array&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md3&lt;/span> &lt;span class="n">started&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">$&lt;/span> &lt;span class="n">vi&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">mdadm&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">conf&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ARRAY&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md2&lt;/span> &lt;span class="n">metadata&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">1.2&lt;/span> &lt;span class="n">UUID&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">d49251e3&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">7091&lt;/span>&lt;span class="n">f035&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">ef641ea4&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">bff15c7f&lt;/span> &lt;span class="n">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">md2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ARRAY&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md3&lt;/span> &lt;span class="n">metadata&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">1.2&lt;/span> &lt;span class="n">UUID&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="n">f87b98&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">e31749b4&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">ee14c8f0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="n">fae7870&lt;/span> &lt;span class="n">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">md3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">$&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">initramfs&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">u&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">update&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">initramfs&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Generating&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">boot&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">initrd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">img&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mf">5.15&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">52&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">generic&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">I&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">The&lt;/span> &lt;span class="n">initramfs&lt;/span> &lt;span class="n">will&lt;/span> &lt;span class="n">attempt&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">resume&lt;/span> &lt;span class="n">from&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sdd3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">I&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">UUID&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">e7159a7b&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">f2b7&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">4253&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bfcd&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">80&lt;/span>&lt;span class="n">d78bb7df9c&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">I&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Set&lt;/span> &lt;span class="n">the&lt;/span> &lt;span class="n">RESUME&lt;/span> &lt;span class="n">variable&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">override&lt;/span> &lt;span class="n">this&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Then&lt;/span> &lt;span class="n">waited&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">it&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">rebuild&lt;/span> &lt;span class="n">the&lt;/span> &lt;span class="n">RAID&lt;/span> &lt;span class="n">array&lt;/span> &lt;span class="n">which&lt;/span> &lt;span class="n">took&lt;/span> &lt;span class="n">all&lt;/span> &lt;span class="n">night&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">complete&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mdadm&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">detail&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">md3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Consistency&lt;/span> &lt;span class="n">Policy&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">bitmap&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Resync&lt;/span> &lt;span class="n">Status&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="o">%&lt;/span> &lt;span class="n">complete&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Name&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">md3&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">local&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">host&lt;/span> &lt;span class="n">srv6&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">technowizardry&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">net&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">UUID&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="n">f87b98&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">e31749b4&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">ee14c8f0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="n">fae7870&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Events&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mi">11&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Sometime&lt;/span> &lt;span class="n">through&lt;/span> &lt;span class="n">this&lt;/span> &lt;span class="n">rebuild&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">the&lt;/span> &lt;span class="n">primary&lt;/span> &lt;span class="n">disk&lt;/span> &lt;span class="n">got&lt;/span> &lt;span class="n">mounted&lt;/span> &lt;span class="n">as&lt;/span> &lt;span class="n">read&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">only&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">couldn&lt;/span>&lt;span class="s1">&amp;#39;t be remounted as read-write which caused problems like, but a reboot fixed this&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">which&lt;/span> &lt;span class="n">service&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">bash&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">usr&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">bin&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">which&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="ne">Input&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">output&lt;/span> &lt;span class="n">error&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I let it sync overnight and finally it was fully completed. Next we need to create an ext4&lt;/p>
&lt;p>mkfs.ext4 /dev/md3&lt;/p>
&lt;h2 id="folder-planning">Folder Planning&lt;/h2>
&lt;p>I then identified which folders I wanted to move over to the new array. I started with just the bare minimum to see what effect it would have on performance:&lt;/p>
&lt;ul>
&lt;li>/etc/kubernetes&lt;/li>
&lt;li>/var/lib/etcd&lt;/li>
&lt;/ul>
&lt;h2 id="creating-the-new-mount">Creating the new mount&lt;/h2>
&lt;p>Next, I identified the UUID of the new RAID array so I can enable auto mounting.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">blkid
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/dev/md3: UUID=&amp;#34;af2d8991-0995-4aef-b5c8-78ca736eb664&amp;#34; BLOCK_SIZE=&amp;#34;4096&amp;#34; TYPE=&amp;#34;ext4&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Next, stop the running containers, move the folders over&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">cd&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">mnt&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">second&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mkdir&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">p&lt;/span> &lt;span class="o">./&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kubernetes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mkdir&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">p&lt;/span> &lt;span class="o">./&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">etcd&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mv&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kubernetes&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">mnt&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">second&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mv&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">etcd&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">mnt&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">second&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">etcd&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mount&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">o&lt;/span> &lt;span class="n">bind&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">mnt&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">second&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kubernetes&lt;/span>&lt;span class="o">/&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kubernetes&lt;/span>&lt;span class="o">/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mount&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">o&lt;/span> &lt;span class="n">bind&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">mnt&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">second&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">etcd&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">etcd&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I changed the /etc/fstab file to ensure that Linux redirects the folders to the new array correctly:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="o">$&lt;/span> &lt;span class="n">vi&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">fstab&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">UUID&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">af2d8991&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">0995&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="n">aef&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">b5c8&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">78&lt;/span>&lt;span class="n">ca736eb664&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">mnt&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">second&lt;/span> &lt;span class="n">ext4&lt;/span> &lt;span class="n">defaults&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">/&lt;/span>&lt;span class="n">mnt&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">second&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">etcd&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">etcd&lt;/span> &lt;span class="n">none&lt;/span> &lt;span class="n">bind&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">/&lt;/span>&lt;span class="n">mnt&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">second&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kubernetes&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kubernetes&lt;/span> &lt;span class="n">none&lt;/span> &lt;span class="n">bind&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Finally, restart all containers and watch as the Kubernetes node comes back online. After some time, Kubernetes stabilized and I saw a large reduction in the number of slow writes that etcd complained about. Unfortunately it didn&amp;rsquo;t hit zero, but sadly HDDs are slow compared to SSDs.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-6.png" >&lt;img src="https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/images/image-6-1024x349.png"
width="1024"
height="349"
srcset="https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/images/image-6-1024x349_hu_3d9fc9b4423f6049.png 480w, https://www.technowizardry.net/2022/11/from-a-random-kubernetes-control-plane-crash-to-a-new-raid-array/images/image-6-1024x349_hu_5f8bf54f75ef9ae2.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="293"
data-flex-basis="704px"
>&lt;/a>&lt;/p>
&lt;p>Loki search results for &amp;ldquo;apply request took too long&amp;rdquo;&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>In this post, I walked through identifying a Kubernetes control plane issue to figuring out it&amp;rsquo;s likely caused by disk write latency on one of the machines. This lead me to redesign the RAID array configuration. It improved things, but not as much as I hoped.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F11%2Ffrom-a-random-kubernetes-control-plane-crash-to-a-new-raid-array%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=From+a+random+Kubernetes+control+plane+crash+to+a+new+RAID+array" style="border:0" alt="" /></description></item><item><title>Visualizing Home Energy Usage in InfluxDB and Home Assistant</title><link>https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/</link><pubDate>Sat, 15 Oct 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/</guid><summary>&lt;p>In previous posts in this series, I walked through how to get data flowing into Home Assistant.&lt;/p>
&lt;p>In this post, we&amp;rsquo;ll get it flowing into InfluxDB for long-term retention.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-13.png" >&lt;img src="https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-13.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-13_hu_921569119dba3a8f.png 480w, https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-13_hu_2cb1ebe4d34cde92.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p></summary><description>&lt;img src="https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image.png" alt="Featured image of post Visualizing Home Energy Usage in InfluxDB and Home Assistant" />&lt;p>In previous posts in this series, I walked through how to get data flowing into Home Assistant.&lt;/p>
&lt;p>In this post, we&amp;rsquo;ll get it flowing into InfluxDB for long-term retention.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-13.png" >&lt;img src="https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-13.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-13_hu_921569119dba3a8f.png 480w, https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-13_hu_2cb1ebe4d34cde92.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p>
&lt;h2 id="influxdb">InfluxDB&lt;/h2>
&lt;p>If you haven&amp;rsquo;t already installed InfluxDB, follow the &lt;a class="link" href="https://docs.influxdata.com/influxdb/v2.3/install/" target="_blank" rel="noopener"
>official guide here&lt;/a>. Then install the &lt;a class="link" href="https://www.home-assistant.io/integrations/influxdb/" target="_blank" rel="noopener"
>HA InfluxDB integration&lt;/a>. The default InfluxDB integration sends a large amount of information that&amp;rsquo;s not useful. Here&amp;rsquo;s an example configuration that reduces it:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">influxdb&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">influxdb-influxdb2.datastore.svc.cluster.local.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8086&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ssl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api_version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">token&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="l">token}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bucket&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">homeassistant&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">organization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">influxdata&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">component_config_domain&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sensor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ignore_attributes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">attribution&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">device_class&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">state_class&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">last_reset&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">integration&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">description&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">unit_of_measurement&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">friendly_name&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">type&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">include&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">domains&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">sensor&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="queries">Queries&lt;/h3>
&lt;p>I&amp;rsquo;ve put together a few example queries that I use in my Grafana dashboards here.&lt;/p>
&lt;h4 id="energy-usage-using-kwh">Energy Usage using KwH&lt;/h4>
&lt;p>The following query returns energy usage over time. This query is more useful than simply averaging Watts over a time since it accounts for spikes and drops smaller than the window period.&lt;/p>
&lt;p>Note: The InfluxDB seems to use the UTC time zone, whereas Home Assistant will use your local time zone on the energy dashboard. This can result in the numbers not matching, but using &lt;strong>timeShift&lt;/strong> will fix them. Update &lt;strong>timeZoneOffset&lt;/strong> to match your time zone.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">import &amp;#34;timezone&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">option location = timezone.location(name: &amp;#34;America/Los_Angeles&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">from(bucket: &amp;#34;homeassistant&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_measurement&amp;#34;] == &amp;#34;kWh&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_field&amp;#34;] == &amp;#34;value&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;domain&amp;#34;] == &amp;#34;sensor&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; difference(nonNegative: true)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; aggregateWindow(every: v.windowPeriod, fn: sum)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; yield(name: &amp;#34;energy&amp;#34;)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="energy-usage-using-watts">Energy Usage using watts&lt;/h4>
&lt;p>The prior query is more accurate if your sensors themselves report energy usage because the devices do continual aggregation, but the GreenEye Monitor does not itself collect kWh so we have to do the integral ourselves.&lt;/p>
&lt;p>The following query returns energy usage over time. This query is more useful than simply averaging Watts over a time since it accounts for spikes and drops smaller than the window period.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">from(bucket: &amp;#34;homeassistant&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_measurement&amp;#34;] == &amp;#34;W&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_field&amp;#34;] == &amp;#34;value&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;domain&amp;#34;] == &amp;#34;sensor&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; aggregateWindow(
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> every: v.windowPeriod,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> fn: (tables=&amp;lt;-, column) =&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> tables
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; integral(unit: v.windowPeriod)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; map(fn: (r) =&amp;gt; ({ r with _value: r._value / 1000.0}))
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; set(key: &amp;#34;_measurement&amp;#34;, value: &amp;#34;kWh&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> )
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; yield(name: &amp;#34;mean&amp;#34;)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="energy-costs">Energy Costs&lt;/h4>
&lt;p>This query calculates the cost of a given energy consumer over a time period and can be aggregated to any time period. If you zoom in far, you&amp;rsquo;ll find that things are measured in fractions of pennies.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">costPerkWh = 0.1056
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">from(bucket: &amp;#34;homeassistant&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;domain&amp;#34;] == &amp;#34;sensor&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_measurement&amp;#34;] == &amp;#34;W&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_field&amp;#34;] == &amp;#34;value&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: true)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; fill(usePrevious: true)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; map(fn: (r) =&amp;gt; ({r with
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> _value: r._value * costPerkWh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }))
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; yield(name: &amp;#34;mean&amp;#34;)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="data-retention">Data Retention&lt;/h2>
&lt;p>The kWh metric is important to know how much energy you&amp;rsquo;re actively using and being billed for, however Home Assistant tracks it as an ever increasing total number from 0 to infinity. This is problematic because you can&amp;rsquo;t aggregate a counter over different time periods to see how much you&amp;rsquo;re using month over month or week over week.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-7.png" >&lt;img src="https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-7.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-7_hu_627b908b56ee1608.png 480w, https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-7_hu_8c209d4356055388.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p>
&lt;p>Instead, we need to convert this to a gauge-style metric where it contains the number of watt-hours used in a particular time period, not ever increasing.&lt;/p>
&lt;p>I initially tried to aggregate to different levels including daily, however I encountered time zone issues (&lt;a class="link" href="https://community.influxdata.com/t/daily-task-in-different-timezone/24085/11" target="_blank" rel="noopener"
>reference&lt;/a>). InfluxDB runs all jobs in UTC, but I want to work in the local zone because that&amp;rsquo;s how my bill is calculated. Instead storing at hourly already provides a massive reduction in data sizes ~10k-15k data points per entity per day to only 24 per entity.&lt;/p>
&lt;p>Data retention is controlled at the bucket level, so create a new bucket with a longer retention policy and name it something like &lt;strong>energy-1hr&lt;/strong> (since it&amp;rsquo;ll store hourly aggregations.)&lt;/p>
&lt;h2 id="seeing-double">Seeing Double&lt;/h2>
&lt;p>I created a downsample job based on the kWh query described ahead, but after running the job for a few days, I tried comparing it to the raw data stored in my original bucket and found that everything seemed to be shifted over by 1 hour.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-1.png" >&lt;img src="https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-1.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-1_hu_2d9decb1a9ef27b5.png 480w, https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-1_hu_df0d8c6d5f91d182.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p>
&lt;p>But after shifting the query over, I still saw differences. I wrote a query to compare the results:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">import &amp;#34;timezone&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">import &amp;#34;date&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">import &amp;#34;join&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">import &amp;#34;math&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">option location = timezone.location(name: &amp;#34;America/Los_Angeles&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">DOWNSAMPLED = from(bucket: &amp;#34;energy-1h&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_measurement&amp;#34;] == &amp;#34;kWh&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_field&amp;#34;] == &amp;#34;value&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;domain&amp;#34;] == &amp;#34;sensor&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;entity_id&amp;#34;] == &amp;#34;total_energy&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; aggregateWindow(every: 1h, fn: sum, createEmpty: false)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; drop(columns: [&amp;#34;_start&amp;#34;, &amp;#34;_stop&amp;#34;])
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; group(columns: [&amp;#34;entity_id&amp;#34;])
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">RAW = from(bucket: &amp;#34;homeassistant&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_measurement&amp;#34;] == &amp;#34;kWh&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_field&amp;#34;] == &amp;#34;value&amp;#34; and r[&amp;#34;entity_id&amp;#34;] == &amp;#34;total_energy&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; difference(nonNegative: true)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; aggregateWindow(every: 1h, fn: sum, createEmpty: false)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; drop(columns: [&amp;#34;_start&amp;#34;, &amp;#34;_stop&amp;#34;])
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; group(columns: [&amp;#34;entity_id&amp;#34;])
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">COMPUTED = RAW
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; timeShift(duration: -1s)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; aggregateWindow(every: 1h, fn: sum, createEmpty: false)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; drop(columns: [&amp;#34;_start&amp;#34;, &amp;#34;_stop&amp;#34;])
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; group(columns: [&amp;#34;entity_id&amp;#34;])
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">join.left(left: RAW, right: DOWNSAMPLED, on: (l, r) =&amp;gt; l._time == r._time, as: (l, r) =&amp;gt; ({ l with _value: r._value - l._value, _field: &amp;#34;error&amp;#34; }))
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; set(key: &amp;#34;_field&amp;#34;, value: &amp;#34;error&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; map(fn: (r) =&amp;gt; ({r with _value: math.abs(x: r._value)}))
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; sum(column: &amp;#34;_value&amp;#34;)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Which gave me a total error of 4.93 kWh / 7 days. This is weird. I started researching bugs on the internet and came across these:&lt;/p>
&lt;p>&lt;a class="link" href="https://github.com/influxdata/influxdb/issues/17100" target="_blank" rel="noopener"
>https://github.com/influxdata/influxdb/issues/17100&lt;/a> - Task alignment issue&lt;/p>
&lt;p>&lt;a class="link" href="https://github.com/influxdata/influxdb/issues/17323" target="_blank" rel="noopener"
>https://github.com/influxdata/influxdb/issues/17323&lt;/a> - Task Alignment issue&lt;/p>
&lt;p>&lt;a class="link" href="https://github.com/influxdata/flux/issues/1730" target="_blank" rel="noopener"
>https://github.com/influxdata/flux/issues/1730&lt;/a> - aggregateWindow _start/_stop issue&lt;/p>
&lt;p>After some investigation, I found out that aggregateWindow() rounds windows to the end of the window. This meant that data from 10am-11am was placed into a record with _time:11am, then the second aggregateWindow when I pulled it out of the downsampled bucket, windowed it to be 12pm. I couldn&amp;rsquo;t just drop the second aggregateWindow function because I wanted to be able to aggregate up into larger buckets if needed.&lt;/p>
&lt;p>My solution for this is to timeShift the data by -1s:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">BY_KWH =
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> from(bucket: &amp;#34;homeassistant&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: -task.every)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;domain&amp;#34;] == &amp;#34;sensor&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_measurement&amp;#34;] == &amp;#34;kWh&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_field&amp;#34;] == &amp;#34;value&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; difference(nonNegative: true)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BY_KWH
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; aggregateWindow(every: 1h, fn: sum, createEmpty: false)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; timeShift(duration: -1s)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; to(bucket: &amp;#34;energy-1h&amp;#34;)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This brought the error down to 0.25 kWh across the last 7 days.&lt;/p>
&lt;h2 id="unexpected-differences">Unexpected differences&lt;/h2>
&lt;p>Even after temporarily adjusting the data and comparing, I still noticed errors. . My thought was that possible the first item was not being calculated correctly because the &lt;a class="link" href="https://docs.influxdata.com/flux/v0.x/stdlib/universe/difference/" target="_blank" rel="noopener"
>difference()&lt;/a> was ignoring the first item or not correctly missing the first few minutes of energy usage. Switching to show the time in UTC seemed to confirm this issue:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-2.png" >&lt;img src="https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-2.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-2_hu_7ad5766a4e7a3719.png 480w, https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-2_hu_715286ead816bf71.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p>
&lt;p>I fixed this by aggregating the last 2 hours of data up to calculate the difference over the last midnight, then saving only the last 1 hour:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">BY_KWH =
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> from(bucket: &amp;#34;homeassistant&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: -2h)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;domain&amp;#34;] == &amp;#34;sensor&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_measurement&amp;#34;] == &amp;#34;kWh&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_field&amp;#34;] == &amp;#34;value&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; difference(nonNegative: true)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BY_KWH
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; aggregateWindow(every: 1h, fn: sum, createEmpty: false)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: -1h)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; timeShift(duration: -1s)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; to(bucket: &amp;#34;energy-1h&amp;#34;)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>That helped, but looking at the raw kWh graph, there didn&amp;rsquo;t seem to be a lot of precision in the data. To fix this, I increased the precision of the Home Assistant Helper.&lt;/p>
&lt;p>Four decimal precision points ensures that we can measure loads as low as 100mWh. Probably over kill, but I&amp;rsquo;ve got some devices that idle there.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-3.png" >&lt;img src="https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-3.png"
width="709"
height="746"
srcset="https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-3_hu_2c8d9a3a98a5de71.png 480w, https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image-3_hu_1204f253dde9f4bb.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="95"
data-flex-basis="228px"
>&lt;/a>&lt;/p>
&lt;p>This increased the number of events flowing into InfluxDB and the Home Assistant Recorder, but the aggressive downsampling will compensate:&lt;/p>
&lt;p>&lt;a class="link" href="images/image.png" >&lt;img src="https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image_hu_fb450ed0532b882a.png 480w, https://www.technowizardry.net/2022/10/visualizing-home-energy-usage-in-influxdb-and-home-assistant/images/image_hu_a4533a843e439171.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p>
&lt;h2 id="create-a-influxdb-job">Create a InfluxDB Job&lt;/h2>
&lt;p>That leads to the final query that I scheduled in the Influx UI under Tasks &amp;gt; Create Task:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">import &amp;#34;timezone&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">import &amp;#34;strings&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">import &amp;#34;date&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">option location = timezone.location(name: &amp;#34;America/Los_Angeles&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">option task = {name: &amp;#34;Energy Downsample (Hourly)&amp;#34;, every: 1h, offset: 2m}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">start = date.add(d: -3h, to: now())
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">actual = -2h
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BY_KWH =
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> from(bucket: &amp;#34;homeassistant&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: start)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;domain&amp;#34;] == &amp;#34;sensor&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_measurement&amp;#34;] == &amp;#34;kWh&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_field&amp;#34;] == &amp;#34;value&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; difference(nonNegative: true, keepFirst: true)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BY_KWH
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; aggregateWindow(every: 1h, fn: sum, createEmpty: false)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: actual)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; drop(columns: [&amp;#34;_start&amp;#34;, &amp;#34;_stop&amp;#34;])
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; to(bucket: &amp;#34;energy-1h&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">// Roll-up Brultech Energy Data
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BY_WATTSEC =
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> from(bucket: &amp;#34;homeassistant&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: start)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;domain&amp;#34;] == &amp;#34;sensor&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_field&amp;#34;] == &amp;#34;watt_seconds&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; difference(nonNegative: true)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BY_WATTSEC
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; aggregateWindow(every: 1h, fn: sum, createEmpty: false)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: actual)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; map(fn: (r) =&amp;gt; ({r with _value: r._value / 1000.0 / 60.0 / 60.0}))
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; set(key: &amp;#34;_field&amp;#34;, value: &amp;#34;value&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; set(key: &amp;#34;_measurement&amp;#34;, value: &amp;#34;kWh&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; to(bucket: &amp;#34;energy-1h&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">COST =
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> from(bucket: &amp;#34;homeassistant&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: start)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_measurement&amp;#34;] == &amp;#34;USD&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_field&amp;#34;] == &amp;#34;value&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;domain&amp;#34;] == &amp;#34;sensor&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; filter(fn: (r) =&amp;gt; strings.hasSuffix(v: r[&amp;#34;entity_id&amp;#34;], suffix: &amp;#34;_energy_cost&amp;#34;))
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; difference(nonNegative: true)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">COST
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; aggregateWindow(every: 1h, fn: sum, createEmpty: false)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; range(start: actual)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; drop(columns: [&amp;#34;_start&amp;#34;, &amp;#34;_stop&amp;#34;])
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |&amp;gt; to(bucket: &amp;#34;energy-1h&amp;#34;)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F10%2Fvisualizing-home-energy-usage-in-influxdb-and-home-assistant%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Visualizing+Home+Energy+Usage+in+InfluxDB+and+Home+Assistant" style="border:0" alt="" /></description></item><item><title>Over-engineering a home air quality dashboard</title><link>https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/</link><pubDate>Mon, 29 Aug 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/</guid><summary>&lt;p>Air—it&amp;rsquo;s invisible, I can&amp;rsquo;t see it, but I feel effects of it in so many ways, temperature, humidity, gas composition, but I lacked sensors to measure it. In this post, I walk through some different Air Quality sensors that I found and how I wired them up into a dashboard.&lt;/p>
&lt;h2 id="prerequisite-software">Prerequisite Software&lt;/h2>
&lt;p>This project depends on a few software components. This post will assume that you have these set up already.&lt;/p></summary><description>&lt;img src="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/header.svg" alt="Featured image of post Over-engineering a home air quality dashboard" />&lt;p>Air—it&amp;rsquo;s invisible, I can&amp;rsquo;t see it, but I feel effects of it in so many ways, temperature, humidity, gas composition, but I lacked sensors to measure it. In this post, I walk through some different Air Quality sensors that I found and how I wired them up into a dashboard.&lt;/p>
&lt;h2 id="prerequisite-software">Prerequisite Software&lt;/h2>
&lt;p>This project depends on a few software components. This post will assume that you have these set up already.&lt;/p>
&lt;ol>
&lt;li>&lt;a class="link" href="https://www.home-assistant.io/" target="_blank" rel="noopener"
>HomeAssistant&lt;/a> - Home Automation central system manages sensor lifecycle&lt;/li>
&lt;li>&lt;a class="link" href="https://mosquitto.org/" target="_blank" rel="noopener"
>MQTT Broker&lt;/a> - Message broker for ingesting sensor data&lt;/li>
&lt;li>InfluxDB - Time series database for short and medium-term data retention&lt;/li>
&lt;li>Grafana (Optional) - Used for building complex visualization dashboards&lt;/li>
&lt;li>Node Red - Graphical programming environment that I use to process sensor data&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/hassio-addons/addon-node-red" target="_blank" rel="noopener"
>Node Red Home Assistant Addon&lt;/a> - Needs to be installed to create HA entities&lt;/li>
&lt;li>ESPHome - Required for DIY built sensors&lt;/li>
&lt;/ol>
&lt;h2 id="architecture-diagram">Architecture Diagram&lt;/h2>
&lt;p>My system architecture for collecting and storing environmental sensor data looks like this:&lt;/p>
&lt;p>&lt;a class="link" href="images/AQI-Architecture.png" >&lt;img src="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/AQI-Architecture.png"
width="886"
height="367"
srcset="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/AQI-Architecture_hu_849ad2693f7cc639.png 480w, https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/AQI-Architecture_hu_7067ba611fbf9d71.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="241"
data-flex-basis="579px"
>&lt;/a>&lt;/p>
&lt;h2 id="a-quick-intro-to-aqi-metrics">A Quick Intro to AQI Metrics&lt;/h2>
&lt;p>AQI stands for Air Quality Index. It doesn&amp;rsquo;t actually represent a singular consistent metric and different organizations and governments calculate their indexes slightly different. Popular ones include the EPA AQI, AQandU&lt;/p>
&lt;p>More information on the different types of indexes can be &lt;a class="link" href="https://www.ucdavis.edu/climate/what-can-i-do/making-sense-of-air-quality-sensors-an-aqi-explainer" target="_blank" rel="noopener"
>found here&lt;/a>.&lt;/p>
&lt;p>Air quality sensors don&amp;rsquo;t directly calculate the AQI and instead track low level metrics such as PM1.0, PM2.5, PM10, etc. PM2.5 corresponds to the amount of Particulate Matter that is 2.5μm or smaller.&lt;/p>
&lt;p>&lt;a class="link" href="images/image.png" >&lt;img src="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image.png"
width="1480"
height="511"
srcset="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image_hu_350e544f97450ad0.png 480w, https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image_hu_e6f6b7d5040d7df0.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="289"
data-flex-basis="695px"
>&lt;/a>&lt;/p>
&lt;p>Courtesy of the CA Air Resources Board&lt;/p>
&lt;p>PM10 includes dust, pollen, and mold, whereas PM2.5 starts to identify soot and smoke. Note that there are many particles that are smaller that can&amp;rsquo;t be easily measured with most home sensors. Some sensors (like the custom sensor I have) may end up estimating PM1.0 counts from the PM2.5 counts.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-1.png" >&lt;img src="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-1-1024x768.png"
width="1024"
height="768"
srcset="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-1-1024x768_hu_486135cde1c73235.png 480w, https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-1-1024x768_hu_afc88f4c8eb4aa58.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="133"
data-flex-basis="320px"
>&lt;/a>&lt;/p>
&lt;p>Given this, we&amp;rsquo;ll need the formulas that each one uses to be able to calculate the AQI indexes.&lt;/p>
&lt;ul>
&lt;li>EPA AQI - Found on page 9 of &lt;a class="link" href="https://www.airnow.gov/sites/default/files/2020-05/aqi-technical-assistance-document-sept2018.pdf" target="_blank" rel="noopener"
>this pdf&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="custom-esp-based-sensor">Custom ESP Based Sensor&lt;/h2>
&lt;p>&lt;a class="link" href="images/AirQuality-Sensor.jpg" >&lt;img src="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/AirQuality-Sensor-1024x845.jpg"
width="1024"
height="845"
srcset="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/AirQuality-Sensor-1024x845_hu_528b65d62d439d68.jpg 480w, https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/AirQuality-Sensor-1024x845_hu_2db4f54424702537.jpg 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="121"
data-flex-basis="290px"
>&lt;/a>&lt;/p>
&lt;p>Photo of my DIY AQI and CO2 monitoring device&lt;/p>
&lt;p>I also created my own sensor using off the shelf components. You don&amp;rsquo;t have to use the same exact ESP MCU as me. I initially used an ESP8266, but later iterations of my sensors switched to the ESP32 with Stemma connectors. &lt;a class="link" href="https://learn.adafruit.com/introducing-adafruit-stemma-qt" target="_blank" rel="noopener"
>Stemma&lt;/a> is a connector standard that most Adafruit sensors include making it even easier to connect.&lt;/p>
&lt;p>Ingredients:&lt;/p>
&lt;table>&lt;tbody>&lt;tr>&lt;td>Count&lt;/td>&lt;td>Item&lt;/td>&lt;td>Cost&lt;/td>&lt;/tr>&lt;tr>&lt;td>1&lt;/td>&lt;td>&lt;a href="https://www.adafruit.com/product/5187">Sensiron SCD-40 CO2 Sensor&lt;/a> - There are a few different CO2 sensors with varying accuracy on Adafruit. This CO2 sensor uses some photoacoustic magic to measure how CO2 gas particles exist, whereas some other sensors just estimate the composition&lt;/td>&lt;td>US$58.95&lt;/td>&lt;/tr>&lt;tr>&lt;td>1&lt;/td>&lt;td>&lt;a href="https://www.adafruit.com/product/4632">Plantower PM2.5 Sensor&lt;/a>&lt;/td>&lt;td>US$44.95&lt;/td>&lt;/tr>&lt;tr>&lt;td>1&lt;/td>&lt;td>&lt;a href="https://www.adafruit.com/product/5426">ESP32-S3 Qt Py w/ Stemma&lt;/a>&lt;/td>&lt;td>US$9.95&lt;/td>&lt;/tr>&lt;tr>&lt;td>2&lt;/td>&lt;td>&lt;a href="https://www.adafruit.com/product/4210">STEMMA QT / Qwiic JST SH 4-pin Cable&lt;/a>&lt;/td>&lt;td>US$0.95&lt;/td>&lt;/tr>&lt;tr>&lt;td>&lt;/td>&lt;td>Total&lt;/td>&lt;td>US$115.75&lt;/td>&lt;/tr>&lt;/tbody>&lt;/table>
&lt;h3 id="esphome">ESPHome&lt;/h3>
&lt;p>I use ESPHome to build and update the firmware running on the controller. Originally, I wrote the code myself, but ESPHome provides all the glue code needed to pull data from i2c devices and publish it to MQTT.&lt;/p>
&lt;p>secrets.yaml&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Your Wi-Fi SSID and password&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">wifi_ssid&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;SSID&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">wifi_password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;PASSWORD&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">ota&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;RANDOMSTRING&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The following config works with the Adafruit QT Py ESP32-C3 linked earlier. If you have a different board make sure to update the esphome and i2c blocks.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt"> 10
&lt;/span>&lt;span class="lnt"> 11
&lt;/span>&lt;span class="lnt"> 12
&lt;/span>&lt;span class="lnt"> 13
&lt;/span>&lt;span class="lnt"> 14
&lt;/span>&lt;span class="lnt"> 15
&lt;/span>&lt;span class="lnt"> 16
&lt;/span>&lt;span class="lnt"> 17
&lt;/span>&lt;span class="lnt"> 18
&lt;/span>&lt;span class="lnt"> 19
&lt;/span>&lt;span class="lnt"> 20
&lt;/span>&lt;span class="lnt"> 21
&lt;/span>&lt;span class="lnt"> 22
&lt;/span>&lt;span class="lnt"> 23
&lt;/span>&lt;span class="lnt"> 24
&lt;/span>&lt;span class="lnt"> 25
&lt;/span>&lt;span class="lnt"> 26
&lt;/span>&lt;span class="lnt"> 27
&lt;/span>&lt;span class="lnt"> 28
&lt;/span>&lt;span class="lnt"> 29
&lt;/span>&lt;span class="lnt"> 30
&lt;/span>&lt;span class="lnt"> 31
&lt;/span>&lt;span class="lnt"> 32
&lt;/span>&lt;span class="lnt"> 33
&lt;/span>&lt;span class="lnt"> 34
&lt;/span>&lt;span class="lnt"> 35
&lt;/span>&lt;span class="lnt"> 36
&lt;/span>&lt;span class="lnt"> 37
&lt;/span>&lt;span class="lnt"> 38
&lt;/span>&lt;span class="lnt"> 39
&lt;/span>&lt;span class="lnt"> 40
&lt;/span>&lt;span class="lnt"> 41
&lt;/span>&lt;span class="lnt"> 42
&lt;/span>&lt;span class="lnt"> 43
&lt;/span>&lt;span class="lnt"> 44
&lt;/span>&lt;span class="lnt"> 45
&lt;/span>&lt;span class="lnt"> 46
&lt;/span>&lt;span class="lnt"> 47
&lt;/span>&lt;span class="lnt"> 48
&lt;/span>&lt;span class="lnt"> 49
&lt;/span>&lt;span class="lnt"> 50
&lt;/span>&lt;span class="lnt"> 51
&lt;/span>&lt;span class="lnt"> 52
&lt;/span>&lt;span class="lnt"> 53
&lt;/span>&lt;span class="lnt"> 54
&lt;/span>&lt;span class="lnt"> 55
&lt;/span>&lt;span class="lnt"> 56
&lt;/span>&lt;span class="lnt"> 57
&lt;/span>&lt;span class="lnt"> 58
&lt;/span>&lt;span class="lnt"> 59
&lt;/span>&lt;span class="lnt"> 60
&lt;/span>&lt;span class="lnt"> 61
&lt;/span>&lt;span class="lnt"> 62
&lt;/span>&lt;span class="lnt"> 63
&lt;/span>&lt;span class="lnt"> 64
&lt;/span>&lt;span class="lnt"> 65
&lt;/span>&lt;span class="lnt"> 66
&lt;/span>&lt;span class="lnt"> 67
&lt;/span>&lt;span class="lnt"> 68
&lt;/span>&lt;span class="lnt"> 69
&lt;/span>&lt;span class="lnt"> 70
&lt;/span>&lt;span class="lnt"> 71
&lt;/span>&lt;span class="lnt"> 72
&lt;/span>&lt;span class="lnt"> 73
&lt;/span>&lt;span class="lnt"> 74
&lt;/span>&lt;span class="lnt"> 75
&lt;/span>&lt;span class="lnt"> 76
&lt;/span>&lt;span class="lnt"> 77
&lt;/span>&lt;span class="lnt"> 78
&lt;/span>&lt;span class="lnt"> 79
&lt;/span>&lt;span class="lnt"> 80
&lt;/span>&lt;span class="lnt"> 81
&lt;/span>&lt;span class="lnt"> 82
&lt;/span>&lt;span class="lnt"> 83
&lt;/span>&lt;span class="lnt"> 84
&lt;/span>&lt;span class="lnt"> 85
&lt;/span>&lt;span class="lnt"> 86
&lt;/span>&lt;span class="lnt"> 87
&lt;/span>&lt;span class="lnt"> 88
&lt;/span>&lt;span class="lnt"> 89
&lt;/span>&lt;span class="lnt"> 90
&lt;/span>&lt;span class="lnt"> 91
&lt;/span>&lt;span class="lnt"> 92
&lt;/span>&lt;span class="lnt"> 93
&lt;/span>&lt;span class="lnt"> 94
&lt;/span>&lt;span class="lnt"> 95
&lt;/span>&lt;span class="lnt"> 96
&lt;/span>&lt;span class="lnt"> 97
&lt;/span>&lt;span class="lnt"> 98
&lt;/span>&lt;span class="lnt"> 99
&lt;/span>&lt;span class="lnt">100
&lt;/span>&lt;span class="lnt">101
&lt;/span>&lt;span class="lnt">102
&lt;/span>&lt;span class="lnt">103
&lt;/span>&lt;span class="lnt">104
&lt;/span>&lt;span class="lnt">105
&lt;/span>&lt;span class="lnt">106
&lt;/span>&lt;span class="lnt">107
&lt;/span>&lt;span class="lnt">108
&lt;/span>&lt;span class="lnt">109
&lt;/span>&lt;span class="lnt">110
&lt;/span>&lt;span class="lnt">111
&lt;/span>&lt;span class="lnt">112
&lt;/span>&lt;span class="lnt">113
&lt;/span>&lt;span class="lnt">114
&lt;/span>&lt;span class="lnt">115
&lt;/span>&lt;span class="lnt">116
&lt;/span>&lt;span class="lnt">117
&lt;/span>&lt;span class="lnt">118
&lt;/span>&lt;span class="lnt">119
&lt;/span>&lt;span class="lnt">120
&lt;/span>&lt;span class="lnt">121
&lt;/span>&lt;span class="lnt">122
&lt;/span>&lt;span class="lnt">123
&lt;/span>&lt;span class="lnt">124
&lt;/span>&lt;span class="lnt">125
&lt;/span>&lt;span class="lnt">126
&lt;/span>&lt;span class="lnt">127
&lt;/span>&lt;span class="lnt">128
&lt;/span>&lt;span class="lnt">129
&lt;/span>&lt;span class="lnt">130
&lt;/span>&lt;span class="lnt">131
&lt;/span>&lt;span class="lnt">132
&lt;/span>&lt;span class="lnt">133
&lt;/span>&lt;span class="lnt">134
&lt;/span>&lt;span class="lnt">135
&lt;/span>&lt;span class="lnt">136
&lt;/span>&lt;span class="lnt">137
&lt;/span>&lt;span class="lnt">138
&lt;/span>&lt;span class="lnt">139
&lt;/span>&lt;span class="lnt">140
&lt;/span>&lt;span class="lnt">141
&lt;/span>&lt;span class="lnt">142
&lt;/span>&lt;span class="lnt">143
&lt;/span>&lt;span class="lnt">144
&lt;/span>&lt;span class="lnt">145
&lt;/span>&lt;span class="lnt">146
&lt;/span>&lt;span class="lnt">147
&lt;/span>&lt;span class="lnt">148
&lt;/span>&lt;span class="lnt">149
&lt;/span>&lt;span class="lnt">150
&lt;/span>&lt;span class="lnt">151
&lt;/span>&lt;span class="lnt">152
&lt;/span>&lt;span class="lnt">153
&lt;/span>&lt;span class="lnt">154
&lt;/span>&lt;span class="lnt">155
&lt;/span>&lt;span class="lnt">156
&lt;/span>&lt;span class="lnt">157
&lt;/span>&lt;span class="lnt">158
&lt;/span>&lt;span class="lnt">159
&lt;/span>&lt;span class="lnt">160
&lt;/span>&lt;span class="lnt">161
&lt;/span>&lt;span class="lnt">162
&lt;/span>&lt;span class="lnt">163
&lt;/span>&lt;span class="lnt">164
&lt;/span>&lt;span class="lnt">165
&lt;/span>&lt;span class="lnt">166
&lt;/span>&lt;span class="lnt">167
&lt;/span>&lt;span class="lnt">168
&lt;/span>&lt;span class="lnt">169
&lt;/span>&lt;span class="lnt">170
&lt;/span>&lt;span class="lnt">171
&lt;/span>&lt;span class="lnt">172
&lt;/span>&lt;span class="lnt">173
&lt;/span>&lt;span class="lnt">174
&lt;/span>&lt;span class="lnt">175
&lt;/span>&lt;span class="lnt">176
&lt;/span>&lt;span class="lnt">177
&lt;/span>&lt;span class="lnt">178
&lt;/span>&lt;span class="lnt">179
&lt;/span>&lt;span class="lnt">180
&lt;/span>&lt;span class="lnt">181
&lt;/span>&lt;span class="lnt">182
&lt;/span>&lt;span class="lnt">183
&lt;/span>&lt;span class="lnt">184
&lt;/span>&lt;span class="lnt">185
&lt;/span>&lt;span class="lnt">186
&lt;/span>&lt;span class="lnt">187
&lt;/span>&lt;span class="lnt">188
&lt;/span>&lt;span class="lnt">189
&lt;/span>&lt;span class="lnt">190
&lt;/span>&lt;span class="lnt">191
&lt;/span>&lt;span class="lnt">192
&lt;/span>&lt;span class="lnt">193
&lt;/span>&lt;span class="lnt">194
&lt;/span>&lt;span class="lnt">195
&lt;/span>&lt;span class="lnt">196
&lt;/span>&lt;span class="lnt">197
&lt;/span>&lt;span class="lnt">198
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">esphome&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">airquality&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ESP32S3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">board&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">esp32-s3-devkitc-1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Enable logging&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">logger&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">ota&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>!&lt;span class="l">secret ota&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">wifi&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ssid&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>!&lt;span class="l">secret wifi_ssid&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>!&lt;span class="l">secret wifi_password&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">i2c&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sda&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">41&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">scl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">40&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">scan&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">frequency&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">10kHz&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">mqtt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">broker&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mqtt.example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">discovery_unique_id_generator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mac&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">discovery_object_id_generator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">device_name&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">on_connect&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">light.turn_on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">neopixel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">effect&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">connected&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">red&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="l">%&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">green&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="l">%&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">blue&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="l">%&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">brightness&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">75&lt;/span>&lt;span class="l">%&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">delay&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">2s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">light.turn_off&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">neopixel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">on_disconnect&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">light.turn_on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">neopixel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">effect&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">broken&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">restart&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Restart&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">template&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;CO2 Recalibrate&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">entity_category&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">on_press&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">then&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">scd4x.perform_forced_calibration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>!&lt;span class="l">lambda &amp;#39;return id(co2_cal).state;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">number&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">template&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;CO2 calibration value (ppm)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">optimistic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">min_value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">350&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">max_value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4500&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">initial_value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">420&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retain&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">step&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">co2_cal&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">icon&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;mdi:molecule-co2&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">entity_category&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;config&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># This board has a small LED that we can use to signal statuses&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">light&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">neopixelbus&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GRB&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">variant&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">WS2812&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pin&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">num_leds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">neopixel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;neopixel-enable&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">internal&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restore_mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ALWAYS_OFF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">effects&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pulse&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">connected&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">strobe&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">broken&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">colors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">state&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">duration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">500ms&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">brightness&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="l">%&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">red&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="l">%&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">green&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="l">%&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">blue&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="l">%&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">state&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">brightness&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="l">%&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">duration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">500ms&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">red&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="l">%&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">green&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="l">%&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">blue&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="l">%&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">sensor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">pmsa003i&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pm_1_0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;PM1.0&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pm_2_5&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;PM2.5&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pm_10_0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;PM10.0&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pmc_0_3&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;PMC &amp;lt;0.3µm&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pmc_0_5&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;PMC &amp;lt;0.5µm&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pmc_1_0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;PMC &amp;lt;1µm&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pmc_2_5&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;PMC &amp;lt;2.5µm&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pmc_5_0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;PMC &amp;lt;5µm&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pmc_10_0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;PMC &amp;lt;10µm&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">update_interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">60s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">scd4x&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">co2&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;CO2&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retain&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">accuracy_decimals&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">temperature&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Temperature&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">accuracy_decimals&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retain&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">humidity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Humidity&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">accuracy_decimals&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retain&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># The below number is based on the average air pressure for my&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># area in mBar divided by 1000 (1.009 == 1009mBar)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># If you don&amp;#39;t know what this is, then disable this.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ambient_pressure_compensation&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;1.009&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Seems to be broken, see https://github.com/esphome/issues/issues/3063&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">temperature_offset&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">measurement_mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">low_power_periodic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Automatic calibration causes problems when indoors. Instead manually calibrate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">automatic_self_calibration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">update_interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">60s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># This next block of code converts the PM2.5 measurement into an AQI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Based on the US EPA algorithm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">copy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">source_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">pm25&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">pm_2_5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">internal&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">accuracy_decimals&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">sliding_window_moving_average&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window_size&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">send_every&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">on_value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">lambda&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> if (id(pm_2_5).state &amp;lt; 12.0) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> // good
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> id(aqi_text).publish_state(&amp;#34;Good&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> id(pm_2_5_aqi).publish_state((50.0 - 0.0) / (12.0 - 0.0) * (id(pm_2_5).state - 0.0) + 0.0);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> } else if (id(pm_2_5).state &amp;lt; 35.4) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> // moderate
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> id(aqi_text).publish_state(&amp;#34;Moderate&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> id(pm_2_5_aqi).publish_state((100.0 - 51.0) / (35.4 - 12.1) * (id(pm_2_5).state - 12.1) + 51.0);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> } else if (id(pm_2_5).state &amp;lt; 55.4) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> // Unhealthy for Sensitive Groups
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> id(aqi_text).publish_state(&amp;#34;Unhealthy for Sensitive Groups&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> id(pm_2_5_aqi).publish_state((150.0 - 101.0) / (55.4 - 35.5) * (id(pm_2_5).state - 35.5) + 101.0);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> } else if (id(pm_2_5).state &amp;lt; 150.4) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> // unhealthy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> id(aqi_text).publish_state(&amp;#34;Unhealthy&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> id(pm_2_5_aqi).publish_state((200.0 - 151.0) / (150.4 - 55.5) * (id(pm_2_5).state - 55.5) + 151.0);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> } else if (id(pm_2_5).state &amp;lt; 250.4) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> // very unhealthy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> id(aqi_text).publish_state(&amp;#34;Very Unhealthy&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> id(pm_2_5_aqi).publish_state((300.0 - 201.0) / (250.4 - 150.5) * (id(pm_2_5).state - 150.5) + 201.0);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> } else if (id(pm_2_5).state &amp;lt; 350.4) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> // hazardous
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> id(aqi_text).publish_state(&amp;#34;Hazardous&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> id(pm_2_5_aqi).publish_state((400.0 - 301.0) / (350.4 - 250.5) * (id(pm_2_5).state - 250.5) + 301.0);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> } else if (id(pm_2_5).state &amp;lt; 500.4) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> // hazardous 2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> id(aqi_text).publish_state(&amp;#34;Hazardous&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> id(pm_2_5_aqi).publish_state((500.0 - 401.0) / (500.4 - 350.5) * (id(pm_2_5).state - 350.5) + 401.0);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">template&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;PM 2.5 AQI&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">icon&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;mdi:air-filter&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">accuracy_decimals&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">measurement&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">aqi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">pm_2_5_aqi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">text_sensor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">template&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;AQI Description&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">icon&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;mdi:air-filter&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">aqi_text&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="accuracy-and-calibration">Accuracy and Calibration&lt;/h3>
&lt;p>One of the issues I&amp;rsquo;ve noticed with the CO2 sensor is that it seems to show sensor values that are incorrect&amp;ndash;below the average outdoor CO2 ppm. The NDIR sensor I have needs to be recalibrated periodically to know how the spectrometer measurements correlate to a specific PPM. The &lt;a class="link" href="https://sensirion.com/products/catalog/SCD30/" target="_blank" rel="noopener"
>Sensiron SCD-30 data sheet&lt;/a> specifies an accuracy of ±30ppm from 400-10k ppm. The auto calibration mode assumes that the sensor is periodically exposed to outdoor fresh air and assumes that outdoor ppm is 400ppm.&lt;/p>
&lt;blockquote>
&lt;p>ASC assumes that the lowest CO2 concentration the&lt;br>
SCD30 is exposed to corresponds to 400 ppm. The sensor&lt;br>
estimates the most likely reading corresponding to this&lt;br>
background level and identifies this as 400ppm.&lt;/p>
&lt;p>&lt;a class="link" href="https://sensirion.com/media/documents/33C09C07/620638B8/Sensirion_SCD30_Field_Calibration.pdf" target="_blank" rel="noopener"
>SCD32 Field Calibration Guide&lt;/a> (pdf)&lt;/p>&lt;/blockquote>
&lt;p>Due to global CO2 emissions, the average outdoor CO2 concentration is slowly increasing over the years, so this will sensor will slowly diverge:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-10.png" >&lt;img src="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-10-1024x878.png"
width="1024"
height="878"
srcset="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-10-1024x878_hu_b7b8d11dab89c9a.png 480w, https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-10-1024x878_hu_1372f8852ad61057.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="116"
data-flex-basis="279px"
>&lt;/a>&lt;/p>
&lt;p>From &lt;a class="link" href="https://www.climate.gov/news-features/understanding-climate/climate-change-atmospheric-carbon-dioxide" target="_blank" rel="noopener"
>Climate.gov&lt;/a>. Average CO2 concentration as measured from &lt;a class="link" href="https://gml.noaa.gov/obop/mlo/" target="_blank" rel="noopener"
>Mauna Loa Observatory&lt;/a> in Hawaii&lt;/p>
&lt;p>In addition, the ambient air pressure influences sensor readings and this changes throughout the day due to changing weather conditions.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-12.png" >&lt;img src="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-12.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-12_hu_d9738450bd56af3e.png 480w, https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-12_hu_2e48b16bd59acca2.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p>
&lt;p>Thus, we have multiple variables that will all cause drift over time:&lt;/p>
&lt;ul>
&lt;li>Sensor drift due to spectrometer sensor drifts&lt;/li>
&lt;li>Global CO2 concentration increasing causing underestimation of CO2 ppm over years&lt;/li>
&lt;li>Ambient pressure fluctuations due to changing weather&lt;/li>
&lt;li>Not exposing the sensor to fresh air when auto calibration is enabled&lt;/li>
&lt;/ul>
&lt;p>There&amp;rsquo;s only so much I can do to ensure accuracy of this data. I set an average air pressure to compensate and open the window once or twice a week to ensure a reference value.&lt;/p>
&lt;h2 id="purpleair-indoor-sensor">PurpleAir Indoor Sensor&lt;/h2>
&lt;p>Before I decided to purchase an air quality sensor, I looked up some independent reviews of the accuracy of different sensors. The US EPA &lt;a class="link" href="https://www.aqmd.gov/aq-spec/evaluations/summary-pm" target="_blank" rel="noopener"
>released a report&lt;/a> showing that the PurpleAir did reasonably well for the price point, so I bought one to test it out.&lt;/p>
&lt;p>&lt;a class="link" href="https://www2.purpleair.com/products/purpleair-pa-i" target="_blank" rel="noopener"
>Manufacturer Detail Page&lt;/a>&lt;/p>
&lt;p>Measures:&lt;/p>
&lt;ul>
&lt;li>Air Quality Index&lt;/li>
&lt;li>Air Pressure&lt;/li>
&lt;li>Temperature&lt;/li>
&lt;li>Humidity&lt;/li>
&lt;/ul>
&lt;p>PurpleAir sensors can publish data directly to the &lt;a class="link" href="https://map.purpleair.com/" target="_blank" rel="noopener"
>PurpleAir Map&lt;/a>, but I want to export this data to Prometheus or InfluxDB. One way is to use an already built Purple Prometheus Exporter (&lt;a class="link" href="https://github.com/steventblack/purpleprom" target="_blank" rel="noopener"
>purpleprom&lt;/a>) which fetches from Purple&amp;rsquo;s API, calculates several AQI metrics, then exposes them to a Prometheus scraper.&lt;/p>
&lt;p>&lt;a class="link" href="images/AQI-PurpleExporter.png" >&lt;img src="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/AQI-PurpleExporter.png"
width="854"
height="246"
srcset="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/AQI-PurpleExporter_hu_ab5ba2951a934fb0.png 480w, https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/AQI-PurpleExporter_hu_39e2e61b8ce2f34e.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="347"
data-flex-basis="833px"
>&lt;/a>&lt;/p>
&lt;p>This will work, however this depends on their API called over the Internet and only gets the data into Prometheus. My home lab tries to avoid dependencies on Internet services and use local devices where possible.&lt;/p>
&lt;p>Luckily, after connecting to my Wi-Fi network, the devices exposes an HTTP endpoint with all sensor data at the URL: http://{ip}:80/json. The following shows an example response (Interesting values are highlighted):&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">{&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;SensorId&amp;#34;: &lt;/span>&lt;span class="s2">&amp;#34;01:23:45:67:89:ab&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;DateTime&amp;#34;: &lt;/span>&lt;span class="s2">&amp;#34;2022/07/26T05:18:38z&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;Geo&amp;#34;: &lt;/span>&lt;span class="s2">&amp;#34;PurpleAir-1234&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;Mem&amp;#34;: &lt;/span>&lt;span class="m">17552&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;memfrag&amp;#34;: &lt;/span>&lt;span class="m">18&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;memfb&amp;#34;: &lt;/span>&lt;span class="m">14464&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;memcs&amp;#34;: &lt;/span>&lt;span class="m">936&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;Id&amp;#34;: &lt;/span>&lt;span class="m">8436&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;lat&amp;#34;: &lt;/span>&lt;span class="m">47.00000&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;lon&amp;#34;: &lt;/span>-&lt;span class="m">122.00000&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;Adc&amp;#34;: &lt;/span>&lt;span class="m">0.02&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;loggingrate&amp;#34;: &lt;/span>&lt;span class="m">15&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;place&amp;#34;: &lt;/span>&lt;span class="s2">&amp;#34;inside&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;version&amp;#34;: &lt;/span>&lt;span class="s2">&amp;#34;7.00&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;uptime&amp;#34;: &lt;/span>&lt;span class="m">253112&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;rssi&amp;#34;: &lt;/span>-&lt;span class="m">72&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;period&amp;#34;: &lt;/span>&lt;span class="m">119&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;httpsuccess&amp;#34;: &lt;/span>&lt;span class="m">8501&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;httpsends&amp;#34;: &lt;/span>&lt;span class="m">8503&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;hardwareversion&amp;#34;: &lt;/span>&lt;span class="s2">&amp;#34;2.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;hardwarediscovered&amp;#34;: &lt;/span>&lt;span class="s2">&amp;#34;2.0+BME280+PMSX003-A&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;current_temp_f&amp;#34;: &lt;/span>&lt;span class="m">87&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;current_humidity&amp;#34;: &lt;/span>&lt;span class="m">33&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;current_dewpoint_f&amp;#34;: &lt;/span>&lt;span class="m">54&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;pressure&amp;#34;: &lt;/span>&lt;span class="m">1003.65&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;p25aqic&amp;#34;: &lt;/span>&lt;span class="s2">&amp;#34;rgb(174,246,0)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;pm2.5_aqi&amp;#34;: &lt;/span>&lt;span class="m">44&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;pm1_0_cf_1&amp;#34;: &lt;/span>&lt;span class="m">5.4&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;p_0_3_um&amp;#34;: &lt;/span>&lt;span class="m">1215.85&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;pm2_5_cf_1&amp;#34;: &lt;/span>&lt;span class="m">10.65&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;p_0_5_um&amp;#34;: &lt;/span>&lt;span class="m">375.31&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;pm10_0_cf_1&amp;#34;: &lt;/span>&lt;span class="m">10.96&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;p_1_0_um&amp;#34;: &lt;/span>&lt;span class="m">74.28&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;pm1_0_atm&amp;#34;: &lt;/span>&lt;span class="m">5.4&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;p_2_5_um&amp;#34;: &lt;/span>&lt;span class="m">9.75&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;pm2_5_atm&amp;#34;: &lt;/span>&lt;span class="m">10.65&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;p_5_0_um&amp;#34;: &lt;/span>&lt;span class="m">0.58&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;pm10_0_atm&amp;#34;: &lt;/span>&lt;span class="m">10.96&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;p_10_0_um&amp;#34;: &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;pa_latency&amp;#34;: &lt;/span>&lt;span class="m">177&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="l">// ... Some more&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>These fields provide the relevant information to be able to calculate the AQI.&lt;/p>
&lt;h2 id="processing-sensor-data">Processing Sensor Data&lt;/h2>
&lt;p>I now have two different sensors that are ready to provide data. My custom sensor is publishing data through MQTT (and some data is already available in Home Assistant) and the PurpleAir sensor is on the Wi-Fi network and ready. Feel free to skip parts if you have only sensor but not the other.&lt;/p>
&lt;h3 id="purpleair-sensor">PurpleAir Sensor&lt;/h3>
&lt;p>Every minute, my Node Red flow calls the HTTP endpoint on the sensor to grab the latest sensor values, calculates the AQI from the PM2.5 metric, then populates several sensors into Home Assistant.&lt;/p>
&lt;p>&lt;a class="link" href="images/image.png" >&lt;img src="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-1024x354.png"
width="1024"
height="354"
srcset="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-1024x354_hu_82bbf7eca257dba1.png 480w, https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-1024x354_hu_a5245d4b5faa1ce1.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="289"
data-flex-basis="694px"
>&lt;/a>&lt;/p>
&lt;p>Here&amp;rsquo;s the Node Red flow for above. Copy and paste this into the Node Red UI to use it as a template.&lt;/p>
&lt;p>[{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;tab&amp;rdquo;,&amp;ldquo;label&amp;rdquo;:&amp;ldquo;Flow 1&amp;rdquo;,&amp;ldquo;disabled&amp;rdquo;:false,&amp;ldquo;info&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;env&amp;rdquo;:[]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;f12427f26b727dc9&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;junction&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;x&amp;rdquo;:1420,&amp;ldquo;y&amp;rdquo;:800,&amp;ldquo;wires&amp;rdquo;:[[&amp;ldquo;3a856e7ab6d6d4c6&amp;rdquo;,&amp;ldquo;bc26db0709e3d34b&amp;rdquo;]]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;96894d899bdea100&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;change&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;rules&amp;rdquo;:[{&amp;ldquo;t&amp;rdquo;:&amp;ldquo;set&amp;rdquo;,&amp;ldquo;p&amp;rdquo;:&amp;ldquo;pm25&amp;rdquo;,&amp;ldquo;pt&amp;rdquo;:&amp;ldquo;msg&amp;rdquo;,&amp;ldquo;to&amp;rdquo;:&amp;ldquo;payload.pm2_5_cf_1&amp;rdquo;,&amp;ldquo;tot&amp;rdquo;:&amp;ldquo;msg&amp;rdquo;}],&amp;ldquo;action&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;property&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;from&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;to&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;reg&amp;rdquo;:false,&amp;ldquo;x&amp;rdquo;:1140,&amp;ldquo;y&amp;rdquo;:720,&amp;ldquo;wires&amp;rdquo;:[[&amp;ldquo;f024a8c76b1f9ce0&amp;rdquo;,&amp;ldquo;c121d65dd0f7dc45&amp;rdquo;,&amp;ldquo;f12427f26b727dc9&amp;rdquo;,&amp;ldquo;98a883db43c85b0f&amp;rdquo;]]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;9e79ed78dd67fc08&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;switch&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;If error&amp;rdquo;,&amp;ldquo;property&amp;rdquo;:&amp;ldquo;error&amp;rdquo;,&amp;ldquo;propertyType&amp;rdquo;:&amp;ldquo;msg&amp;rdquo;,&amp;ldquo;rules&amp;rdquo;:[{&amp;ldquo;t&amp;rdquo;:&amp;ldquo;null&amp;rdquo;},{&amp;ldquo;t&amp;rdquo;:&amp;ldquo;nnull&amp;rdquo;}],&amp;ldquo;checkall&amp;rdquo;:&amp;ldquo;true&amp;rdquo;,&amp;ldquo;repair&amp;rdquo;:false,&amp;ldquo;outputs&amp;rdquo;:2,&amp;ldquo;x&amp;rdquo;:970,&amp;ldquo;y&amp;rdquo;:720,&amp;ldquo;wires&amp;rdquo;:[[&amp;ldquo;96894d899bdea100&amp;rdquo;],[]]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;f024a8c76b1f9ce0&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;function&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;Normalize Temperature&amp;rdquo;,&amp;ldquo;func&amp;rdquo;:&amp;ldquo;msg.payload = msg.payload.current_temp_f - 8;\n\nreturn msg;&amp;rdquo;,&amp;ldquo;outputs&amp;rdquo;:1,&amp;ldquo;noerr&amp;rdquo;:0,&amp;ldquo;initialize&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;finalize&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;libs&amp;rdquo;:[],&amp;ldquo;x&amp;rdquo;:1530,&amp;ldquo;y&amp;rdquo;:720,&amp;ldquo;wires&amp;rdquo;:[[&amp;ldquo;9a5bbf8cc529fb67&amp;rdquo;]]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;3ec53bbca46d0c79&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;http request&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;method&amp;rdquo;:&amp;ldquo;GET&amp;rdquo;,&amp;ldquo;ret&amp;rdquo;:&amp;ldquo;obj&amp;rdquo;,&amp;ldquo;paytoqs&amp;rdquo;:&amp;ldquo;ignore&amp;rdquo;,&amp;ldquo;url&amp;rdquo;:&amp;ldquo;http://192.168.1.45/json&amp;rdquo;,&amp;ldquo;tls&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;persist&amp;rdquo;:false,&amp;ldquo;proxy&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;authType&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;senderr&amp;rdquo;:false,&amp;ldquo;x&amp;rdquo;:810,&amp;ldquo;y&amp;rdquo;:720,&amp;ldquo;wires&amp;rdquo;:[[&amp;ldquo;9e79ed78dd67fc08&amp;rdquo;]]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;c121d65dd0f7dc45&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;switch&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;quot;&amp;gt; 0&amp;quot;,&amp;ldquo;property&amp;rdquo;:&amp;ldquo;payload.pressure&amp;rdquo;,&amp;ldquo;propertyType&amp;rdquo;:&amp;ldquo;msg&amp;rdquo;,&amp;ldquo;rules&amp;rdquo;:[{&amp;ldquo;t&amp;rdquo;:&amp;ldquo;gt&amp;rdquo;,&amp;ldquo;v&amp;rdquo;:&amp;ldquo;0&amp;rdquo;,&amp;ldquo;vt&amp;rdquo;:&amp;ldquo;num&amp;rdquo;}],&amp;ldquo;checkall&amp;rdquo;:&amp;ldquo;true&amp;rdquo;,&amp;ldquo;repair&amp;rdquo;:false,&amp;ldquo;outputs&amp;rdquo;:1,&amp;ldquo;x&amp;rdquo;:1470,&amp;ldquo;y&amp;rdquo;:760,&amp;ldquo;wires&amp;rdquo;:[[&amp;ldquo;b1364f17e9a4f113&amp;rdquo;]]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;b96470e4363c8622&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;inject&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;every minute&amp;rdquo;,&amp;ldquo;props&amp;rdquo;:[{&amp;ldquo;p&amp;rdquo;:&amp;ldquo;payload&amp;rdquo;},{&amp;ldquo;p&amp;rdquo;:&amp;ldquo;topic&amp;rdquo;,&amp;ldquo;vt&amp;rdquo;:&amp;ldquo;str&amp;rdquo;}],&amp;ldquo;repeat&amp;rdquo;:&amp;ldquo;60&amp;rdquo;,&amp;ldquo;crontab&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;once&amp;rdquo;:false,&amp;ldquo;onceDelay&amp;rdquo;:0.1,&amp;ldquo;topic&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;payload&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;payloadType&amp;rdquo;:&amp;ldquo;date&amp;rdquo;,&amp;ldquo;x&amp;rdquo;:620,&amp;ldquo;y&amp;rdquo;:720,&amp;ldquo;wires&amp;rdquo;:[[&amp;ldquo;3ec53bbca46d0c79&amp;rdquo;]]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;98a883db43c85b0f&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;link call&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;links&amp;rdquo;:[&amp;ldquo;4e1646049cd8c3fa&amp;rdquo;],&amp;ldquo;linkType&amp;rdquo;:&amp;ldquo;static&amp;rdquo;,&amp;ldquo;timeout&amp;rdquo;:&amp;ldquo;30&amp;rdquo;,&amp;ldquo;x&amp;rdquo;:1500,&amp;ldquo;y&amp;rdquo;:680,&amp;ldquo;wires&amp;rdquo;:[[&amp;ldquo;fae3173cf70b26fc&amp;rdquo;]]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;3a856e7ab6d6d4c6&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;ha-sensor&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;PurpleAir Humidity&amp;rdquo;,&amp;ldquo;entityConfig&amp;rdquo;:&amp;ldquo;feea3b65bc43902a&amp;rdquo;,&amp;ldquo;version&amp;rdquo;:0,&amp;ldquo;state&amp;rdquo;:&amp;ldquo;payload.current_humidity&amp;rdquo;,&amp;ldquo;stateType&amp;rdquo;:&amp;ldquo;msg&amp;rdquo;,&amp;ldquo;attributes&amp;rdquo;:[],&amp;ldquo;inputOverride&amp;rdquo;:&amp;ldquo;allow&amp;rdquo;,&amp;ldquo;outputProperties&amp;rdquo;:[],&amp;ldquo;x&amp;rdquo;:1790,&amp;ldquo;y&amp;rdquo;:800,&amp;ldquo;wires&amp;rdquo;:[[]]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;9a5bbf8cc529fb67&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;ha-sensor&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;PurpleAir Temperature&amp;rdquo;,&amp;ldquo;entityConfig&amp;rdquo;:&amp;ldquo;24eaa8400b7264e6&amp;rdquo;,&amp;ldquo;version&amp;rdquo;:0,&amp;ldquo;state&amp;rdquo;:&amp;ldquo;payload&amp;rdquo;,&amp;ldquo;stateType&amp;rdquo;:&amp;ldquo;msg&amp;rdquo;,&amp;ldquo;attributes&amp;rdquo;:[],&amp;ldquo;inputOverride&amp;rdquo;:&amp;ldquo;allow&amp;rdquo;,&amp;ldquo;outputProperties&amp;rdquo;:[],&amp;ldquo;x&amp;rdquo;:1800,&amp;ldquo;y&amp;rdquo;:720,&amp;ldquo;wires&amp;rdquo;:[[]]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;fae3173cf70b26fc&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;ha-sensor&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;PurpleAir 1m iAQI&amp;rdquo;,&amp;ldquo;entityConfig&amp;rdquo;:&amp;ldquo;5bd2e57458ace436&amp;rdquo;,&amp;ldquo;version&amp;rdquo;:0,&amp;ldquo;state&amp;rdquo;:&amp;ldquo;iaqi.epa&amp;rdquo;,&amp;ldquo;stateType&amp;rdquo;:&amp;ldquo;msg&amp;rdquo;,&amp;ldquo;attributes&amp;rdquo;:[{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;type&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;iaqi&amp;rdquo;,&amp;ldquo;valueType&amp;rdquo;:&amp;ldquo;str&amp;rdquo;}],&amp;ldquo;inputOverride&amp;rdquo;:&amp;ldquo;allow&amp;rdquo;,&amp;ldquo;outputProperties&amp;rdquo;:[],&amp;ldquo;x&amp;rdquo;:1790,&amp;ldquo;y&amp;rdquo;:680,&amp;ldquo;wires&amp;rdquo;:[[]]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;b1364f17e9a4f113&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;ha-sensor&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;PurpleAir Air Pressure&amp;rdquo;,&amp;ldquo;entityConfig&amp;rdquo;:&amp;ldquo;3d98f8971f231416&amp;rdquo;,&amp;ldquo;version&amp;rdquo;:0,&amp;ldquo;state&amp;rdquo;:&amp;ldquo;payload.pressure&amp;rdquo;,&amp;ldquo;stateType&amp;rdquo;:&amp;ldquo;msg&amp;rdquo;,&amp;ldquo;attributes&amp;rdquo;:[],&amp;ldquo;inputOverride&amp;rdquo;:&amp;ldquo;allow&amp;rdquo;,&amp;ldquo;outputProperties&amp;rdquo;:[],&amp;ldquo;x&amp;rdquo;:1800,&amp;ldquo;y&amp;rdquo;:760,&amp;ldquo;wires&amp;rdquo;:[[]]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;bc26db0709e3d34b&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;ha-sensor&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;PurpleAir Wi-Fi rx RSSI&amp;rdquo;,&amp;ldquo;entityConfig&amp;rdquo;:&amp;ldquo;d04ff191e844b1a0&amp;rdquo;,&amp;ldquo;version&amp;rdquo;:0,&amp;ldquo;state&amp;rdquo;:&amp;ldquo;payload.rssi&amp;rdquo;,&amp;ldquo;stateType&amp;rdquo;:&amp;ldquo;msg&amp;rdquo;,&amp;ldquo;attributes&amp;rdquo;:[],&amp;ldquo;inputOverride&amp;rdquo;:&amp;ldquo;allow&amp;rdquo;,&amp;ldquo;outputProperties&amp;rdquo;:[],&amp;ldquo;x&amp;rdquo;:1810,&amp;ldquo;y&amp;rdquo;:840,&amp;ldquo;wires&amp;rdquo;:[[]]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;4e1646049cd8c3fa&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;link in&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;Compute AQI&amp;rdquo;,&amp;ldquo;links&amp;rdquo;:[],&amp;ldquo;x&amp;rdquo;:765,&amp;ldquo;y&amp;rdquo;:920,&amp;ldquo;wires&amp;rdquo;:[[&amp;ldquo;c256eaec2e31c57f&amp;rdquo;]]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;bb93a33f9c5e9a7d&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;link out&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;link out 2&amp;rdquo;,&amp;ldquo;mode&amp;rdquo;:&amp;ldquo;return&amp;rdquo;,&amp;ldquo;links&amp;rdquo;:[],&amp;ldquo;x&amp;rdquo;:1075,&amp;ldquo;y&amp;rdquo;:920,&amp;ldquo;wires&amp;rdquo;:[]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;c256eaec2e31c57f&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;function&amp;rdquo;,&amp;ldquo;z&amp;rdquo;:&amp;ldquo;3f4962eb31025362&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;Compute AQI&amp;rdquo;,&amp;ldquo;func&amp;rdquo;:&amp;quot;// &lt;a class="link" href="https://github.com/hrbonz/python-aqi/blob/master/aqi/algos/epa.py" target="_blank" rel="noopener"
>https://github.com/hrbonz/python-aqi/blob/master/aqi/algos/epa.py&lt;/a>\nconst data = {\n &amp;lsquo;aqi&amp;rsquo;: [\n [0, 50],\n [51, 100],\n [101, 150],\n [151, 200],\n [201, 300],\n [301, 400],\n [401, 500]\n ],\n &amp;lsquo;bp&amp;rsquo;: {\n POLLUTANT_PM25: [\n [0.0, 12.0],\n [12.1, 35.4],\n [35.5, 55.4],\n [55.5, 150.4],\n [150.5, 250.4],\n [250.5, 350.4],\n [350.5, 500.4],\n ]\n }\n}\n\nfunction convertToIaqi(value) {\n if (value &amp;lt; 0) {\n throw new Error(`PM2.5 can&amp;rsquo;t be negative: ${value}`);\n }\n const adjValue = Math.floor(value * 10) / 10;\n const breakpoints = data.bp.POLLUTANT_PM25;\n var bplo = 0;\n var bphi = 0;\n var index = 0;\n for (const bp of breakpoints) {\n if (adjValue &amp;gt;= bp[0] &amp;amp;&amp;amp; adjValue &amp;lt;= bp[1]) {\n bplo = bp[0];\n bphi = bp[1];\n break;\n }\n index++;\n }\n \n const aqival = data.aqi[index];\n if (!aqival) {\n throw new Error(`Can&amp;rsquo;t calculate AQI for &amp;lsquo;${value}&amp;rsquo; index out of range: ${index}.`);\n }\n //return index;\n const aqiValue = (aqival[1] - aqival[0]) / (bphi - bplo) * (adjValue - bplo) + aqival[0];\n \n return Math.round(aqiValue);\n}\n\nmsg.iaqi = {\n epa: convertToIaqi(msg.pm25),\n aqandu: convertToIaqi(0.778 * msg.pm25 + 2.65),\n lrapa: convertToIaqi(Math.max(0, 0.5 * msg.pm25 - 0.66))\n};\n\nreturn msg;&amp;quot;,&amp;ldquo;outputs&amp;rdquo;:1,&amp;ldquo;noerr&amp;rdquo;:0,&amp;ldquo;initialize&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;finalize&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;libs&amp;rdquo;:[],&amp;ldquo;x&amp;rdquo;:920,&amp;ldquo;y&amp;rdquo;:920,&amp;ldquo;wires&amp;rdquo;:[[&amp;ldquo;bb93a33f9c5e9a7d&amp;rdquo;]]},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;feea3b65bc43902a&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;ha-entity-config&amp;rdquo;,&amp;ldquo;server&amp;rdquo;:&amp;ldquo;434f6479916be393&amp;rdquo;,&amp;ldquo;deviceConfig&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;sensor config for PurpleAir Humidity&amp;rdquo;,&amp;ldquo;version&amp;rdquo;:&amp;ldquo;6&amp;rdquo;,&amp;ldquo;entityType&amp;rdquo;:&amp;ldquo;sensor&amp;rdquo;,&amp;ldquo;haConfig&amp;rdquo;:[{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;name&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;PurpleAir Humidity&amp;rdquo;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;icon&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;mdi:water-percent&amp;rdquo;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;entity_category&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;quot;&amp;quot;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;device_class&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;humidity&amp;rdquo;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;unit_of_measurement&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;quot;%&amp;quot;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;state_class&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;measurement&amp;rdquo;}],&amp;ldquo;resend&amp;rdquo;:true},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;24eaa8400b7264e6&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;ha-entity-config&amp;rdquo;,&amp;ldquo;server&amp;rdquo;:&amp;ldquo;434f6479916be393&amp;rdquo;,&amp;ldquo;deviceConfig&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;sensor config for PurpleAir Temperature&amp;rdquo;,&amp;ldquo;version&amp;rdquo;:&amp;ldquo;6&amp;rdquo;,&amp;ldquo;entityType&amp;rdquo;:&amp;ldquo;sensor&amp;rdquo;,&amp;ldquo;haConfig&amp;rdquo;:[{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;name&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;PurpleAir Temperature&amp;rdquo;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;icon&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;quot;&amp;quot;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;entity_category&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;quot;&amp;quot;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;device_class&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;temperature&amp;rdquo;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;unit_of_measurement&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;quot;°F&amp;quot;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;state_class&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;measurement&amp;rdquo;}],&amp;ldquo;resend&amp;rdquo;:true},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;5bd2e57458ace436&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;ha-entity-config&amp;rdquo;,&amp;ldquo;server&amp;rdquo;:&amp;ldquo;434f6479916be393&amp;rdquo;,&amp;ldquo;deviceConfig&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;sensor config for PurpleAir 1m EPA&amp;rdquo;,&amp;ldquo;version&amp;rdquo;:&amp;ldquo;6&amp;rdquo;,&amp;ldquo;entityType&amp;rdquo;:&amp;ldquo;sensor&amp;rdquo;,&amp;ldquo;haConfig&amp;rdquo;:[{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;name&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;quot;&amp;quot;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;icon&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;mdi:blur&amp;rdquo;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;entity_category&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;quot;&amp;quot;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;device_class&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;aqi&amp;rdquo;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;unit_of_measurement&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;aqi&amp;rdquo;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;state_class&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;measurement&amp;rdquo;}],&amp;ldquo;resend&amp;rdquo;:true},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;3d98f8971f231416&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;ha-entity-config&amp;rdquo;,&amp;ldquo;server&amp;rdquo;:&amp;ldquo;434f6479916be393&amp;rdquo;,&amp;ldquo;deviceConfig&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;sensor config for PurpleAir Air Pressure&amp;rdquo;,&amp;ldquo;version&amp;rdquo;:&amp;ldquo;6&amp;rdquo;,&amp;ldquo;entityType&amp;rdquo;:&amp;ldquo;sensor&amp;rdquo;,&amp;ldquo;haConfig&amp;rdquo;:[{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;name&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;PurpleAir Temperature&amp;rdquo;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;icon&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;quot;&amp;quot;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;entity_category&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;quot;&amp;quot;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;device_class&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;temperature&amp;rdquo;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;unit_of_measurement&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;quot;°C&amp;quot;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;state_class&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;measurement&amp;rdquo;}],&amp;ldquo;resend&amp;rdquo;:true},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;d04ff191e844b1a0&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;ha-entity-config&amp;rdquo;,&amp;ldquo;server&amp;rdquo;:&amp;ldquo;434f6479916be393&amp;rdquo;,&amp;ldquo;deviceConfig&amp;rdquo;:&amp;quot;&amp;quot;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;PurpleAir Wi-Fi RSSI&amp;rdquo;,&amp;ldquo;version&amp;rdquo;:&amp;ldquo;6&amp;rdquo;,&amp;ldquo;entityType&amp;rdquo;:&amp;ldquo;sensor&amp;rdquo;,&amp;ldquo;haConfig&amp;rdquo;:[{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;name&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;quot;&amp;quot;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;icon&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;quot;&amp;quot;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;entity_category&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;diagnostic&amp;rdquo;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;device_class&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;signal_strength&amp;rdquo;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;unit_of_measurement&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;dBm&amp;rdquo;},{&amp;ldquo;property&amp;rdquo;:&amp;ldquo;state_class&amp;rdquo;,&amp;ldquo;value&amp;rdquo;:&amp;ldquo;measurement&amp;rdquo;}],&amp;ldquo;resend&amp;rdquo;:false},{&amp;ldquo;id&amp;rdquo;:&amp;ldquo;434f6479916be393&amp;rdquo;,&amp;ldquo;type&amp;rdquo;:&amp;ldquo;server&amp;rdquo;,&amp;ldquo;name&amp;rdquo;:&amp;ldquo;Home Assistant&amp;rdquo;,&amp;ldquo;version&amp;rdquo;:5,&amp;ldquo;addon&amp;rdquo;:false,&amp;ldquo;rejectUnauthorizedCerts&amp;rdquo;:true,&amp;ldquo;ha_boolean&amp;rdquo;:&amp;ldquo;y|yes|true|on|home|open&amp;rdquo;,&amp;ldquo;connectionDelay&amp;rdquo;:true,&amp;ldquo;cacheJson&amp;rdquo;:true,&amp;ldquo;heartbeat&amp;rdquo;:false,&amp;ldquo;heartbeatInterval&amp;rdquo;:&amp;ldquo;30&amp;rdquo;,&amp;ldquo;areaSelector&amp;rdquo;:&amp;ldquo;friendlyName&amp;rdquo;,&amp;ldquo;deviceSelector&amp;rdquo;:&amp;ldquo;friendlyName&amp;rdquo;,&amp;ldquo;entitySelector&amp;rdquo;:&amp;ldquo;friendlyName&amp;rdquo;,&amp;ldquo;statusSeparator&amp;rdquo;:&amp;ldquo;at: &amp;ldquo;,&amp;ldquo;statusYear&amp;rdquo;:&amp;ldquo;hidden&amp;rdquo;,&amp;ldquo;statusMonth&amp;rdquo;:&amp;ldquo;short&amp;rdquo;,&amp;ldquo;statusDay&amp;rdquo;:&amp;ldquo;numeric&amp;rdquo;,&amp;ldquo;statusHourCycle&amp;rdquo;:&amp;ldquo;h23&amp;rdquo;,&amp;ldquo;statusTimeFormat&amp;rdquo;:&amp;ldquo;h:m&amp;rdquo;,&amp;ldquo;enableGlobalContextStore&amp;rdquo;:true}]&lt;/p>
&lt;p>Or, using &lt;a class="link" href="https://appdaemon.readthedocs.io/en/latest/" target="_blank" rel="noopener"
>AppDaemon&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">hassapi&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">hass&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">requests&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">aqi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">PurpleAirCollector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hass&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Hass&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">initialize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run_minutely&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">fetch_data&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">start&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">set_iaqi&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">entity_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">value&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">call_service&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;state/set&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">entity_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">entity_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">attributes&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;state_class&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;measurement&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;device_class&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;aqi&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;icon&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s1">&amp;#39;mdi:blur&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;friendly_name&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;PurpleAir EPA AQI&amp;#39;&lt;/span>&lt;span class="p">},&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">namespace&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;default&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">fetch_data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">kwargs&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">requests&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;http://192.168.1.45/json&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">epa&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">aqi&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">to_iaqi&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">aqi&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">POLLUTANT_PM25&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;pm2_5_cf_1&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">algo&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">aqi&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ALGO_EPA&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_iaqi&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">entity_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sensor.living_room_purpleair_1m_iaqi&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">epa&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="custom-sensor">Custom Sensor&lt;/h3>
&lt;p>I configured ESPHome to publish raw sensor data to MQTT and HomeAssistant is configured to also connect to the broker and create entities. When I turned on my sensor, I get the following sensors:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-8.png" >&lt;img src="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-8.png"
width="681"
height="822"
srcset="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-8_hu_fb73a41cf698172.png 480w, https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-8_hu_265b293d198f4eee.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="82"
data-flex-basis="198px"
>&lt;/a>&lt;/p>
&lt;p>The only thing I&amp;rsquo;m missing is the IAQI metrics. Using the same function node from before, this is easy:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-9.png" >&lt;img src="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-9-1024x216.png"
width="1024"
height="216"
srcset="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-9-1024x216_hu_f112c6469642e7a3.png 480w, https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-9-1024x216_hu_21edb955d1560aa1.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="474"
data-flex-basis="1137px"
>&lt;/a>&lt;/p>
&lt;p>And the JSON for Node Red (copy and paste into the UI):&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;tab&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;label&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Air Quality&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;disabled&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;info&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;env&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;f12427f26b727dc9&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;junction&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">780&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">380&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;3a856e7ab6d6d4c6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;bc26db0709e3d34b&amp;#34;&lt;/span>\&lt;span class="p">]&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;96894d899bdea100&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;change&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;rules&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;t&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;set&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;p&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;sensor\_name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;pt&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;msg&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;to&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;living\_room&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;tot&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;str&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;t&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;set&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;p&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;pm25&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;pt&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;msg&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;to&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;payload.pm2\_5\_cf\_1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;tot&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;msg&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;action&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;from&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;to&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;reg&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">640&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">300&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;f024a8c76b1f9ce0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;c121d65dd0f7dc45&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;f12427f26b727dc9&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;98a883db43c85b0f&amp;#34;&lt;/span>\&lt;span class="p">]&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;9e79ed78dd67fc08&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;switch&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;If error&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;error&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;propertyType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;msg&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;rules&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;t&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;null&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;t&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;nnull&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;checkall&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;repair&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;outputs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">470&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">300&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;96894d899bdea100&amp;#34;&lt;/span>\&lt;span class="p">],&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;f024a8c76b1f9ce0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;function&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Normalize Temperature&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;func&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;msg.payload = msg.payload.current\_temp\_f - 8;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">nreturn msg;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;outputs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;noerr&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;initialize&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;finalize&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;libs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">890&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">300&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;9a5bbf8cc529fb67&amp;#34;&lt;/span>\&lt;span class="p">]&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3ec53bbca46d0c79&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;http request&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;GET&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;ret&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;obj&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;paytoqs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;ignore&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;http://192.168.1.160/json&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;tls&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;persist&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;proxy&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;insecureHTTPParser&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;authType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;senderr&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;headers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">310&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">300&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;9e79ed78dd67fc08&amp;#34;&lt;/span>\&lt;span class="p">]&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;c121d65dd0f7dc45&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;switch&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;gt; 0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;payload.pressure&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;propertyType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;msg&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;rules&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;t&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;gt&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;v&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;vt&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;num&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;checkall&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;repair&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;outputs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">830&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">340&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;b1364f17e9a4f113&amp;#34;&lt;/span>\&lt;span class="p">]&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;b96470e4363c8622&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;inject&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;every minute&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;props&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;p&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;payload&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;repeat&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;60&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;crontab&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;once&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;onceDelay&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">0.1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;topic&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;payload&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;payloadType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;date&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">120&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">300&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;3ec53bbca46d0c79&amp;#34;&lt;/span>\&lt;span class="p">]&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;98a883db43c85b0f&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;link call&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;links&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;4e1646049cd8c3fa&amp;#34;&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;linkType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;static&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;timeout&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;30&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">860&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">260&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;fae3173cf70b26fc&amp;#34;&lt;/span>\&lt;span class="p">]&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3a856e7ab6d6d4c6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;ha-sensor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;PurpleAir Humidity&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;entityConfig&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;feea3b65bc43902a&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;state&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;payload.current\_humidity&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;stateType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;msg&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;attributes&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;inputOverride&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;allow&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;outputProperties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1150&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">420&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;9a5bbf8cc529fb67&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;ha-sensor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;PurpleAir Temperature&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;entityConfig&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;24eaa8400b7264e6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;state&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;payload&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;stateType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;msg&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;attributes&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;inputOverride&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;allow&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;outputProperties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1160&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">300&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;fae3173cf70b26fc&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;ha-sensor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;PurpleAir 1m EPA&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;entityConfig&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;5bd2e57458ace436&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;state&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;iaqi.epa&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;stateType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;msg&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;attributes&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;iaqi&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;valueType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;str&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;inputOverride&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;allow&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;outputProperties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1150&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">240&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;b1364f17e9a4f113&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;ha-sensor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;PurpleAir Air Pressure&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;entityConfig&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3d98f8971f231416&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;state&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;payload.pressure&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;stateType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;msg&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;attributes&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;inputOverride&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;allow&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;outputProperties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1160&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">360&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;bc26db0709e3d34b&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;ha-sensor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;PurpleAir Wi-Fi rx RSSI&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;entityConfig&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;d04ff191e844b1a0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;state&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;payload.rssi&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;stateType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;msg&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;attributes&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;inputOverride&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;allow&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;outputProperties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1170&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">480&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;4e1646049cd8c3fa&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;link in&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Compute AQI&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;links&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">265&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">500&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;c256eaec2e31c57f&amp;#34;&lt;/span>\&lt;span class="p">]&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;bb93a33f9c5e9a7d&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;link out&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;link out 2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;return&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;links&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">575&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">500&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;c256eaec2e31c57f&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;function&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;z&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3f4962eb31025362&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Compute AQI&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;func&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;// https://github.com/hrbonz/python-aqi/blob/master/aqi/algos/epa.py&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">nconst data = {&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n &amp;#39;aqi&amp;#39;: \[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \[0, 50\],&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \[51, 100\],&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \[101, 150\],&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \[151, 200\],&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \[201, 300\],&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \[301, 400\],&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \[401, 500\]&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \],&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n &amp;#39;bp&amp;#39;: {&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n POLLUTANT\_PM25: \[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \[0.0, 12.0\],&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \[12.1, 35.4\],&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \[35.5, 55.4\],&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \[55.5, 150.4\],&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \[150.5, 250.4\],&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \[250.5, 350.4\],&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \[350.5, 500.4\],&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n \]&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n }&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n}&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">nfunction convertToIaqi(value) {&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n if (value &amp;lt; 0) {&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n throw new Error(\`PM2.5 can&amp;#39;t be negative: ${value}\`);&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n }&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n const adjValue = Math.floor(value \* 10) / 10;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n const breakpoints = data.bp.POLLUTANT\_PM25;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n var bplo = 0;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n var bphi = 0;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n var index = 0;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n for (const bp of breakpoints) {&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n if (adjValue &amp;gt;= bp\[0\] &amp;amp;&amp;amp; adjValue &amp;lt;= bp\[1\]) {&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n bplo = bp\[0\];&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n bphi = bp\[1\];&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n break;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n }&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n index++;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n }&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n const aqival = data.aqi\[index\];&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n if (!aqival) {&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n throw new Error(\`Can&amp;#39;t calculate AQI for &amp;#39;${value}&amp;#39; index out of range: ${index}.\`);&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n }&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n //return index;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n const aqiValue = (aqival\[1\] - aqival\[0\]) / (bphi - bplo) \* (adjValue - bplo) + aqival\[0\];&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n return Math.round(aqiValue);&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n}&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">nmsg.iaqi = {&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n epa: convertToIaqi(msg.pm25),&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n aqandu: convertToIaqi(0.778 \* msg.pm25 + 2.65),&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n lrapa: convertToIaqi(Math.max(0, 0.5 \* msg.pm25 - 0.66))&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n};&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">nreturn msg;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;outputs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;noerr&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;initialize&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;finalize&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;libs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">420&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">500&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;wires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;bb93a33f9c5e9a7d&amp;#34;&lt;/span>\&lt;span class="p">]&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;feea3b65bc43902a&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;ha-entity-config&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;server&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;434f6479916be393&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;deviceConfig&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;sensor config for PurpleAir Humidity&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;entityType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;sensor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;haConfig&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Humidity&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;icon&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;mdi:water-percent&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;entity\_category&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;device\_class&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;humidity&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;unit\_of\_measurement&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;%&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;state\_class&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;measurement&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;resend&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;24eaa8400b7264e6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;ha-entity-config&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;server&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;434f6479916be393&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;deviceConfig&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;sensor config for PurpleAir Temperature&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;entityType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;sensor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;haConfig&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Temperature&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;icon&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;entity\_category&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;device\_class&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;temperature&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;unit\_of\_measurement&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;°F&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;state\_class&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;measurement&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;resend&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;5bd2e57458ace436&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;ha-entity-config&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;server&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;434f6479916be393&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;deviceConfig&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;sensor config for PurpleAir 1m EPA&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;entityType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;sensor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;haConfig&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;AQI (EPA)&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;icon&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;mdi:blur&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;entity\_category&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;device\_class&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;aqi&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;unit\_of\_measurement&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;aqi&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;state\_class&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;measurement&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;resend&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;3d98f8971f231416&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;ha-entity-config&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;server&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;434f6479916be393&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;deviceConfig&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;sensor config for PurpleAir Air Pressure&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;entityType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;sensor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;haConfig&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Air Pressure&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;icon&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;mdi:gauge&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;entity\_category&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;device\_class&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;pressure&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;unit\_of\_measurement&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;mbar&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;state\_class&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;measurement&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;resend&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;d04ff191e844b1a0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;ha-entity-config&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;server&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;434f6479916be393&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;deviceConfig&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;PurpleAir Wi-Fi RSSI&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;entityType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;sensor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;haConfig&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Wi-Fi RSSI&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;icon&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;entity\_category&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;diagnostic&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;device\_class&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;signal\_strength&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;unit\_of\_measurement&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;dBm&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;property&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;state\_class&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;measurement&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;resend&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;434f6479916be393&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;server&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Home Assistant&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;addon&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;rejectUnauthorizedCerts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;ha\_boolean&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;y|yes|true|on|home|open&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;connectionDelay&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;cacheJson&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;heartbeat&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;heartbeatInterval&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;30&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;areaSelector&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;friendlyName&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;deviceSelector&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;friendlyName&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;entitySelector&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;friendlyName&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;statusSeparator&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;at: &amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;statusYear&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;hidden&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;statusMonth&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;short&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;statusDay&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;numeric&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;statusHourCycle&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;h23&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;statusTimeFormat&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;h:m&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;enableGlobalContextStore&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="publishing-to-influxdb">Publishing to InfluxDB&lt;/h2>
&lt;p>HomeAssistant stores sensor data in the &lt;a class="link" href="https://www.home-assistant.io/integrations/recorder/" target="_blank" rel="noopener"
>Recorder&lt;/a>, which by default uses SQLite, but can be changed to use any SQL database like Postgres or MySQL. These aren&amp;rsquo;t efficient at storing large amounts of time series data, so instead I store time series data long-term in InfluxDB using the &lt;a class="link" href="https://www.home-assistant.io/integrations/influxdb/" target="_blank" rel="noopener"
>HA Integration&lt;/a>.&lt;/p>
&lt;p>HomeAssistant by default publishes all entities and all attributes for each state to InfluxDB. This results in *a lot* of useless data being stored and will quickly consume a large amount of disk space. To prevent this, I will explicitly whitelist just the sensor data and ignore any attributes that aren&amp;rsquo;t useful.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">influxdb&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">influxdb-influxdb2.datastore.svc.cluster.local.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8086&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ssl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api_version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">token&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;token&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bucket&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">homeassistant&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">organization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">influxdata&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">component_config_domain&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sensor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ignore_attributes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">attribution&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">device_class&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">state_class&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">last_reset&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">integration&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">description&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">unit_of_measurement&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">type&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">include&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">domain&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">sensor&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="grafana">Grafana&lt;/h2>
&lt;p>Grafana provides the ability to build richer dashboards compared to HomeAssistant. I created an Air Quality monitoring dashboard that fetches data from InfluxDB:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-11.png" >&lt;img src="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-11-1024x545.png"
width="1024"
height="545"
srcset="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-11-1024x545_hu_b39133e1f1006f0e.png 480w, https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-11-1024x545_hu_e16a5abc5ee9b469.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="187"
data-flex-basis="450px"
>&lt;/a>&lt;/p>
&lt;p>Grafana Dashboard JSON. To import, go to Grafana &amp;gt; Dashboards &amp;gt; Import.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;\_\_inputs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;DS\_INFLUXDB&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;label&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;InfluxDB&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;pluginId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;pluginName&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;InfluxDB&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;\_\_elements&amp;#34;&lt;/span>&lt;span class="p">:{},&lt;/span>&lt;span class="s2">&amp;#34;\_\_requires&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;panel&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;gauge&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Gauge&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;grafana&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;grafana&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Grafana&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;9.0.5&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;InfluxDB&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;1.0.0&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;panel&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;state-timeline&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;State timeline&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;panel&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;timeseries&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Time series&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;annotations&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;list&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;builtIn&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;grafana&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;enable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;hide&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;iconColor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;rgba(0, 211, 255, 1)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Annotations &amp;amp; Alerts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;target&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;limit&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;matchAny&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;tags&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;dashboard&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;dashboard&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;editable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;fiscalYearStartMonth&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;graphTooltip&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">null&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;links&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;liveNow&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;panels&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;collapsed&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;prometheus&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;PBFA97CFB590B2093&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;gridPos&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;h&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;w&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">24&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">18&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;panels&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Current AQI&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;row&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;fieldConfig&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;defaults&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;thresholds&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;mappings&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;max&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">500&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;min&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;thresholds&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;absolute&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;steps&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#009966&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">null&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#ffde33&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">50&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#ff9933&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#cc0033&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">150&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#660099&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">200&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#7e0023&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">300&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">]}},&lt;/span>&lt;span class="s2">&amp;#34;overrides&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;gridPos&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;h&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;w&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">19&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;options&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;orientation&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;auto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;reduceOptions&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;calcs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;lastNotNull&amp;#34;&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;fields&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;values&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;showThresholdLabels&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;showThresholdMarkers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">:{}},&lt;/span>&lt;span class="s2">&amp;#34;pluginVersion&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;9.0.5&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;targets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;import &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">strings&lt;/span>\\&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">nfrom(bucket: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">homeassistant&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_measurement&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">aqi&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_field&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">value&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; strings.hasSuffix(v: r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">entity&lt;/span>\&lt;span class="n">_id&lt;/span>\\&lt;span class="s2">&amp;#34;\], suffix: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_iaqi&lt;/span>\\&lt;span class="s2">&amp;#34;))&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; last()&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; keep(columns: \[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_value&lt;/span>\\&lt;span class="s2">&amp;#34;\])&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; mean()&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;refId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Current PM 2.5 AQI&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;gauge&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;fieldConfig&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;defaults&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mappings&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;max&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">500&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;min&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;thresholds&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;absolute&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;steps&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#009966&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">null&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#ffde33&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">50&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#ff9933&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#cc0033&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">150&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#660099&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">200&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#7e0023&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">300&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">]}},&lt;/span>&lt;span class="s2">&amp;#34;overrides&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;gridPos&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;h&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;w&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">20&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;options&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;orientation&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;auto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;reduceOptions&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;calcs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;mean&amp;#34;&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;fields&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;values&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;showThresholdLabels&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;showThresholdMarkers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">:{}},&lt;/span>&lt;span class="s2">&amp;#34;pluginVersion&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;9.0.5&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;targets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;import &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">strings&lt;/span>\\&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">nfrom(bucket: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">homeassistant&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_measurement&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">aqi&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_field&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">value&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; strings.hasSuffix(v: r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">entity&lt;/span>\&lt;span class="n">_id&lt;/span>\\&lt;span class="s2">&amp;#34;\], suffix: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_aqandu&lt;/span>\\&lt;span class="s2">&amp;#34;))&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; last()&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; keep(columns: \[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_value&lt;/span>\\&lt;span class="s2">&amp;#34;\])&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; mean()&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;refId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Current PM 2.5 AQI (AQandU)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;gauge&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;fieldConfig&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;defaults&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mappings&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;max&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">500&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;min&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;thresholds&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;absolute&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;steps&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#009966&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">null&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#ffde33&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">50&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#ff9933&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#cc0033&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">150&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#660099&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">200&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#7e0023&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">300&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">]}},&lt;/span>&lt;span class="s2">&amp;#34;overrides&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;gridPos&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;h&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;w&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">21&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;options&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;orientation&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;auto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;reduceOptions&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;calcs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;mean&amp;#34;&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;fields&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;values&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;showThresholdLabels&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;showThresholdMarkers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">:{}},&lt;/span>&lt;span class="s2">&amp;#34;pluginVersion&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;9.0.5&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;targets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;import &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">strings&lt;/span>\\&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">nfrom(bucket: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">homeassistant&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_measurement&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">aqi&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_field&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">value&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; strings.hasSuffix(v: r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">entity&lt;/span>\&lt;span class="n">_id&lt;/span>\\&lt;span class="s2">&amp;#34;\], suffix: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_lrapa&lt;/span>\\&lt;span class="s2">&amp;#34;))&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; last()&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; keep(columns: \[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_value&lt;/span>\\&lt;span class="s2">&amp;#34;\])&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; mean()&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;refId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Current PM 2.5 AQI (LRAPA)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;gauge&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;collapsed&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;prometheus&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;PBFA97CFB590B2093&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;gridPos&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;h&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;w&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">24&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;panels&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;AQI History&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;row&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;fieldConfig&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;defaults&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;palette-classic&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;custom&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;axisLabel&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;axisPlacement&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;auto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;barAlignment&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;drawStyle&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;line&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;fillOpacity&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;gradientMode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;hideFrom&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;legend&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;tooltip&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;viz&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;lineInterpolation&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;linear&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;lineWidth&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;pointSize&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;scaleDistribution&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;linear&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;showPoints&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;auto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;spanNulls&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;stacking&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;group&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;thresholdsStyle&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;area&amp;#34;&lt;/span>&lt;span class="p">}},&lt;/span>&lt;span class="s2">&amp;#34;mappings&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;thresholds&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;absolute&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;steps&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#009966&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">null&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#ffde33&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">50&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#ff9933&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#cc0033&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">150&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#660099&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">200&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#7e0023&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">300&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">]}},&lt;/span>&lt;span class="s2">&amp;#34;overrides&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;matcher&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;byFrameRefID&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;options&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;G&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;fixedColor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;blue&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;fixed&amp;#34;&lt;/span>&lt;span class="p">}}&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;matcher&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;byFrameRefID&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;options&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;F&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;fixedColor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;green&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;fixed&amp;#34;&lt;/span>&lt;span class="p">}}&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;matcher&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;byFrameRefID&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;options&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;E&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;fixedColor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;red&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;fixed&amp;#34;&lt;/span>&lt;span class="p">}}&lt;/span>\&lt;span class="p">]}&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;gridPos&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;h&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">11&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;w&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;options&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;legend&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;calcs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;displayMode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;list&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;placement&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;bottom&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;tooltip&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;single&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;sort&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">}},&lt;/span>&lt;span class="s2">&amp;#34;pluginVersion&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;8.3.3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;targets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;from(bucket: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">homeassistant&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_measurement&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">aqi&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_field&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">value&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; yield(name: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">mean&lt;/span>\\&lt;span class="s2">&amp;#34;)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;refId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;PM 2.5 AQI&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;timeseries&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;fieldConfig&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;defaults&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;palette-classic&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;custom&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;axisLabel&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;axisPlacement&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;auto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;barAlignment&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;drawStyle&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;line&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;fillOpacity&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;gradientMode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;hideFrom&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;legend&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;tooltip&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;viz&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;lineInterpolation&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;linear&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;lineWidth&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;pointSize&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;scaleDistribution&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;linear&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;showPoints&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;auto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;spanNulls&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;stacking&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;group&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;thresholdsStyle&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;off&amp;#34;&lt;/span>&lt;span class="p">}},&lt;/span>&lt;span class="s2">&amp;#34;mappings&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;max&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;min&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;thresholds&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;absolute&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;steps&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;green&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">null&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;unit&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;percent&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;overrides&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;gridPos&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;h&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">11&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;w&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">23&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;options&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;legend&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;calcs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;displayMode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;list&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;placement&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;bottom&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;tooltip&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;single&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;sort&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">}},&lt;/span>&lt;span class="s2">&amp;#34;targets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;from(bucket: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">homeassistant&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_measurement&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">%&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_field&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">value&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; yield(name: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">mean&lt;/span>\\&lt;span class="s2">&amp;#34;)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;refId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Humidity&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;timeseries&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;fieldConfig&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;defaults&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;palette-classic&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;custom&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;axisLabel&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;°F&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;axisPlacement&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;auto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;barAlignment&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;drawStyle&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;line&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;fillOpacity&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;gradientMode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;hideFrom&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;legend&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;tooltip&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;viz&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;lineInterpolation&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;linear&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;lineWidth&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;pointSize&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;scaleDistribution&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;linear&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;showPoints&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;auto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;spanNulls&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;stacking&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;group&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;thresholdsStyle&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;line&amp;#34;&lt;/span>&lt;span class="p">}},&lt;/span>&lt;span class="s2">&amp;#34;mappings&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;thresholds&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;absolute&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;steps&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;yellow&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">null&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;green&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">60&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;red&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">80&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">]}},&lt;/span>&lt;span class="s2">&amp;#34;overrides&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;matcher&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;byFrameRefID&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;options&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;displayName&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${\_\_field.labels.entity\_id}&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">]},{&lt;/span>&lt;span class="s2">&amp;#34;matcher&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;byFrameRefID&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;options&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;B&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;fixed&amp;#34;&lt;/span>&lt;span class="p">}}&lt;/span>\&lt;span class="p">]}&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;gridPos&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;h&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">11&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;w&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">9&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">15&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">39&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;interval&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;5m&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;options&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;legend&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;calcs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;displayMode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;list&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;placement&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;bottom&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;tooltip&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;single&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;sort&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">}},&lt;/span>&lt;span class="s2">&amp;#34;targets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;import &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">strings&lt;/span>\\&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">nfrom(bucket: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">homeassistant&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">entity&lt;/span>\&lt;span class="n">_id&lt;/span>\\&lt;span class="s2">&amp;#34;\] != &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">fridge&lt;/span>\&lt;span class="n">_door&lt;/span>\&lt;span class="n">_temperature&lt;/span>\&lt;span class="n">_measurement&lt;/span>\\&lt;span class="s2">&amp;#34; and not strings.hasSuffix(v: r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">entity&lt;/span>\&lt;span class="n">_id&lt;/span>\\&lt;span class="s2">&amp;#34;\], suffix: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">index&lt;/span>\\&lt;span class="s2">&amp;#34;) and not strings.hasSuffix(v: r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">entity&lt;/span>\&lt;span class="n">_id&lt;/span>\\&lt;span class="s2">&amp;#34;\], suffix: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">point&lt;/span>\\&lt;span class="s2">&amp;#34;))&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_measurement&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="err">°&lt;/span>&lt;span class="n">F&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_field&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">value&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">domain&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">sensor&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: true)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; fill(usePrevious: true)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; map(fn: (r) =&amp;gt; ({ r with name: r.entity\_id }))&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; yield(name: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">mean&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n// |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">entity&lt;/span>\&lt;span class="n">_id&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">living&lt;/span>\&lt;span class="n">_room&lt;/span>\&lt;span class="n">_temperature&lt;/span>\\&lt;span class="s2">&amp;#34; or r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">entity&lt;/span>\&lt;span class="n">_id&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">home&lt;/span>\&lt;span class="n">_temperature&lt;/span>\\&lt;span class="s2">&amp;#34; or r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">entity&lt;/span>\&lt;span class="n">_id&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">button&lt;/span>\&lt;span class="n">_temperature&lt;/span>\&lt;span class="n">_measurement&lt;/span>\\&lt;span class="s2">&amp;#34; or r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">entity&lt;/span>\&lt;span class="n">_id&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">bedroom&lt;/span>\&lt;span class="n">_temperature&lt;/span>\&lt;span class="n">_esp&lt;/span>\\&lt;span class="s2">&amp;#34; or r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">entity&lt;/span>\&lt;span class="n">_id&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">bedroom&lt;/span>\&lt;span class="n">_temperature&lt;/span>\&lt;span class="n">_ecobee&lt;/span>\\&lt;span class="s2">&amp;#34; or r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">entity&lt;/span>\&lt;span class="n">_id&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">hallway&lt;/span>\&lt;span class="n">_button&lt;/span>\&lt;span class="n">_temperature&lt;/span>\&lt;span class="n">_measurement&lt;/span>\\&lt;span class="s2">&amp;#34;)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;refId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Temperature&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;timeseries&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;fieldConfig&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;defaults&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;palette-classic&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;custom&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;axisLabel&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;axisPlacement&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;auto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;barAlignment&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;drawStyle&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;line&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;fillOpacity&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;gradientMode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;hideFrom&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;legend&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;tooltip&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;viz&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;lineInterpolation&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;linear&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;lineWidth&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;pointSize&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;scaleDistribution&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;linear&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;showPoints&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;auto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;spanNulls&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;stacking&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;group&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;thresholdsStyle&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;line&amp;#34;&lt;/span>&lt;span class="p">}},&lt;/span>&lt;span class="s2">&amp;#34;decimals&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;mappings&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;thresholds&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;absolute&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;steps&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;green&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">null&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;unit&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;pressurembar&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;overrides&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;gridPos&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;h&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;w&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">18&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">33&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;options&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;legend&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;calcs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;displayMode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;list&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;placement&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;bottom&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;tooltip&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;single&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;sort&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">}},&lt;/span>&lt;span class="s2">&amp;#34;targets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;from(bucket: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">homeassistant&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_measurement&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">mbar&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_field&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">value&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">domain&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">sensor&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; yield(name: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">mean&lt;/span>\\&lt;span class="s2">&amp;#34;)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;refId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Air Pressure&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;timeseries&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;alert&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;alertRuleTags&amp;#34;&lt;/span>&lt;span class="p">:{},&lt;/span>&lt;span class="s2">&amp;#34;conditions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;evaluator&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="mi">1000&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;gt&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;operator&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;and&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;5m&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;now&amp;#34;&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;reducer&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;avg&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;executionErrorState&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;alerting&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;for&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;5m&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;frequency&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;1m&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;handler&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;CO2 PPM alert&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;noDataState&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;no\_data&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;notifications&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;fieldConfig&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;defaults&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;thresholds&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;custom&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;axisLabel&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;axisPlacement&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;auto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;barAlignment&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;drawStyle&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;line&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;fillOpacity&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;gradientMode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;hideFrom&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;legend&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;tooltip&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;viz&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;lineInterpolation&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;linear&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;lineWidth&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;pointSize&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;scaleDistribution&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;linear&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;showPoints&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;auto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;spanNulls&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;stacking&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;group&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;thresholdsStyle&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;area&amp;#34;&lt;/span>&lt;span class="p">}},&lt;/span>&lt;span class="s2">&amp;#34;links&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;mappings&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;thresholds&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;absolute&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;steps&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;green&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">null&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#EAB839&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">900&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;red&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1000&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;unit&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;ppm&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;overrides&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;gridPos&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;h&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;w&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">18&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">29&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;options&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;legend&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;calcs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;displayMode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;list&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;placement&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;bottom&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;tooltip&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;single&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;sort&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">}},&lt;/span>&lt;span class="s2">&amp;#34;targets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;from(bucket: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">homeassistant&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_measurement&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">ppm&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_field&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">value&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">domain&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">sensor&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">entity&lt;/span>\&lt;span class="n">_id&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">bedroom&lt;/span>\&lt;span class="n">_co2&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; yield(name: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">mean&lt;/span>\\&lt;span class="s2">&amp;#34;)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;refId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;thresholds&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;colorMode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;critical&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;op&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;gt&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1000&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;visible&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;CO2 PPM&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;timeseries&amp;#34;&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;fieldConfig&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;defaults&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;thresholds&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;custom&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;fillOpacity&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">70&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;lineWidth&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;spanNulls&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">false&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;displayName&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${\_\_field.labels.entity\_id}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;mappings&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;thresholds&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;absolute&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;steps&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#009966&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">null&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#ffde33&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">50&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#ff9933&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#cc0033&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">150&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#660099&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">200&lt;/span>&lt;span class="p">},{&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;#7e0023&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">300&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">]}},&lt;/span>&lt;span class="s2">&amp;#34;overrides&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;gridPos&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;h&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;w&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">18&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">31&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;options&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;alignValue&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;left&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;legend&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;displayMode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;list&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;placement&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;bottom&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;mergeValues&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="bp">true&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;rowHeight&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">0.9&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;showValue&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;never&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;tooltip&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;single&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;sort&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">}},&lt;/span>&lt;span class="s2">&amp;#34;targets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;datasource&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;influxdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;${DS\_INFLUXDB}&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;import &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">strings&lt;/span>\\&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">nfrom(bucket: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">homeassistant&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_measurement&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">aqi&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_field&lt;/span>\\&lt;span class="s2">&amp;#34;\] == &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">value&lt;/span>\\&lt;span class="s2">&amp;#34;)&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; filter(fn: (r) =&amp;gt; strings.hasSuffix(v: r\[&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">entity&lt;/span>\&lt;span class="n">_id&lt;/span>\\&lt;span class="s2">&amp;#34;\], suffix: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>\&lt;span class="n">_iaqi&lt;/span>\\&lt;span class="s2">&amp;#34;))&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">r&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n |&amp;gt; yield(name: &lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="n">mean&lt;/span>\\&lt;span class="s2">&amp;#34;)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;refId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Air Quality&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;state-timeline&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;refresh&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;schemaVersion&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">36&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;style&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;dark&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;tags&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">],&lt;/span>&lt;span class="s2">&amp;#34;templating&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;list&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;time&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;from&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;now-3h&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;to&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;now&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="s2">&amp;#34;timepicker&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="s2">&amp;#34;refresh\_intervals&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;10s&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;30s&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;1m&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;5m&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;15m&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;30m&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;1h&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;2h&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;1d&amp;#34;&lt;/span>\&lt;span class="p">]},&lt;/span>&lt;span class="s2">&amp;#34;timezone&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;America/Los\_Angeles&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;Air Quality (Public)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;uid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;ASFASr23rfasdf&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;weekStart&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>In this post, I walked through two different sensors: a DIY sensor and a prebuilt solution. Showed how to connect to Home Assistant, InfluxDB, and create some basic dashboards for visualization. Future projects include improving accuracy or even designing a custom PCB instead of stringing together sensors with wires.&lt;/p>
&lt;h2 id="updates">Updates&lt;/h2>
&lt;h3 id="jan-2023">Jan 2023&lt;/h3>
&lt;p>After running this for a few months, I noticed that my measured CO2 started getting less accurate. It seemed to get in a bad state until I reset the esp8266, then it would work okay.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-6.png" >&lt;img src="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-6.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-6_hu_2ee20f6e52e89548.png 480w, https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-6_hu_be5da93fb3a34bf3.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p>
&lt;p>A detailed chart showing inaccurate readings&lt;/p>
&lt;p>&lt;a class="link" href="images/image-7.png" >&lt;img src="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-7.png"
width="1000"
height="500"
srcset="https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-7_hu_73bc72b5bc464ce9.png 480w, https://www.technowizardry.net/2022/08/over-engineering-a-home-air-quality-dashboard/images/image-7_hu_b577645815f773eb.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="480px"
>&lt;/a>&lt;/p>
&lt;p>A long-term view&lt;/p>
&lt;p>There were several relevant looking GitHub issues all reporting similar behavior:&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="https://github.com/esphome/issues/issues/3063" target="_blank" rel="noopener"
>https://github.com/esphome/issues/issues/3063&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/esphome/issues/issues/3407" target="_blank" rel="noopener"
>https://github.com/esphome/issues/issues/3407&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/esphome/issues/issues/3529" target="_blank" rel="noopener"
>https://github.com/esphome/issues/issues/3529&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/esphome/issues/issues/3063" target="_blank" rel="noopener"
>https://github.com/esphome/issues/issues/3063&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>To fix this issue, I temporarily set temperature_offset to be 0 and redeployed.&lt;/p>
&lt;h3 id="july-2023">July 2023&lt;/h3>
&lt;p>Occasionally after rebooting the esp device, one of the two sensors would just not come online and report data. It was hard to track down, but I believe that setting i2c.scan: false made it more reliable.&lt;/p>
&lt;h3 id="september-2023">September 2023&lt;/h3>
&lt;ul>
&lt;li>Changed to the Adafruit Qt Py ESP32-S3 which includes a stemma connector&lt;/li>
&lt;li>Updated the ESPHome YAML to match the board and pinout&lt;/li>
&lt;li>Added AQI calculation on device so we don&amp;rsquo;t need the Node Red flow for the custom sensor&lt;/li>
&lt;/ul>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F08%2Fover-engineering-a-home-air-quality-dashboard%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Over-engineering+a+home+air+quality+dashboard" style="border:0" alt="" /></description></item><item><title>Accurate, Local Home Energy Monitoring: Part 3 – Software Config</title><link>https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/</link><pubDate>Sun, 14 Aug 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/</guid><summary>&lt;p>In the previous post in this series, I selected an energy monitoring system that is purely local based (no cloud), integrates into the breaker box, and showed how to connect it to the network and configure the size of each circuit. In this post, I&amp;rsquo;ll show how to connect the BrulTech GreenEye Energy Monitor to HomeAssistant and create some useful monitoring dashboards.&lt;/p>
&lt;h2 id="greeneye-monitor-firmware">GreenEye Monitor Firmware&lt;/h2>
&lt;p>While trying to connect my monitor to Home Assistant, I came across a firmware bug in the GreenEye Monitor and found &lt;a class="link" href="https://www.brultech.com/community/viewtopic.php?f=29&amp;amp;t=2215&amp;amp;sid=14516b8f4f6150c3c70d18afbfc5ea56&amp;amp;start=10#p14902" target="_blank" rel="noopener"
>a forum thread&lt;/a> that Brultech had a bug with their packet formats which has been fixed in firmware version 5.39+. To check and find the serial number which will be needed, navigate to &lt;em>http://{monitor ip}:8000/&lt;/em>, then click &amp;ldquo;Enter Setup Mode&amp;rdquo;.&lt;/p></summary><description>&lt;p>In the previous post in this series, I selected an energy monitoring system that is purely local based (no cloud), integrates into the breaker box, and showed how to connect it to the network and configure the size of each circuit. In this post, I&amp;rsquo;ll show how to connect the BrulTech GreenEye Energy Monitor to HomeAssistant and create some useful monitoring dashboards.&lt;/p>
&lt;h2 id="greeneye-monitor-firmware">GreenEye Monitor Firmware&lt;/h2>
&lt;p>While trying to connect my monitor to Home Assistant, I came across a firmware bug in the GreenEye Monitor and found &lt;a class="link" href="https://www.brultech.com/community/viewtopic.php?f=29&amp;amp;t=2215&amp;amp;sid=14516b8f4f6150c3c70d18afbfc5ea56&amp;amp;start=10#p14902" target="_blank" rel="noopener"
>a forum thread&lt;/a> that Brultech had a bug with their packet formats which has been fixed in firmware version 5.39+. To check and find the serial number which will be needed, navigate to &lt;em>http://{monitor ip}:8000/&lt;/em>, then click &amp;ldquo;Enter Setup Mode&amp;rdquo;.&lt;/p>
&lt;p>Make note of the serial number and firmware version in the top right corner.&lt;/p>
&lt;p>&lt;a class="link" href="images/image.png" >&lt;img src="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image.png"
width="760"
height="1129"
srcset="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image_hu_37d7cae3917d84e.png 480w, https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image_hu_e6861e44232a3736.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="67"
data-flex-basis="161px"
>&lt;/a>&lt;/p>
&lt;p>If you&amp;rsquo;re not on 5.39 or above, download the latest version &lt;a class="link" href="https://www.brultech.com/software/files/checksn/3/1" target="_blank" rel="noopener"
>here&lt;/a> and the &lt;a class="link" href="https://www.brultech.com/software/files/getsoft/1/1" target="_blank" rel="noopener"
>network utility here&lt;/a>. Connect to the monitor, then update the firmware.&lt;/p>
&lt;p>On the top, blue menu panel click &amp;ldquo;Packet Send&amp;rdquo;, then change both COM1 and COM2 Packet Formats to Bin32 NET, then click save.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-2.png" >&lt;img src="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-2-1024x708.png"
width="1024"
height="708"
srcset="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-2-1024x708_hu_197050471742a73f.png 480w, https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-2-1024x708_hu_9540e29226aa09c3.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="144"
data-flex-basis="347px"
>&lt;/a>&lt;/p>
&lt;h2 id="home-assistant-setup">Home Assistant Setup&lt;/h2>
&lt;p>The &lt;a class="link" href="https://www.home-assistant.io/integrations/greeneye_monitor/" target="_blank" rel="noopener"
>integration&lt;/a> for the GreenEye Monitor in HA is a bit out of date and can&amp;rsquo;t be easily added via the UI like some other integrations, however &lt;a class="link" href="https://github.com/home-assistant/core/issues/55112" target="_blank" rel="noopener"
>several developers&lt;/a> are working on improving the state and moving to &lt;a class="link" href="https://hacs.xyz/docs/setup/download" target="_blank" rel="noopener"
>HACS&lt;/a>. I&amp;rsquo;ll be following along and contributing now that I&amp;rsquo;ve got a working setup. Until that&amp;rsquo;s available, let&amp;rsquo;s install with the existing integration.&lt;/p>
&lt;p>Open up the Home Assistant configuration.yml file, and add the following. Provide a short and meaningful name or even just use what&amp;rsquo;s labeled on the circuit breaker. I used the common prefix &amp;ldquo;main_panel_&amp;rdquo;, so I can create templates that reference all power sources on this breaker.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">greeneye_monitor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">monitors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">serial_number&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;01234567&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">channels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">number&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">main_panel_kitchen&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">number&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">main_panel_bathroom&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># For each channel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">voltage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">number&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">main_panel_volts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Save the file, then restart Home Assistant for the changes to take affect.&lt;/p>
&lt;h3 id="kubernetes-networking">Kubernetes Networking&lt;/h3>
&lt;p>The GreenEye needs to open a TCP connection to Home Assistant, however this is complicated in my setup because I&amp;rsquo;m using Kubernetes and all traffic to HA goes through my ingress controller, ingress-nginx. If you&amp;rsquo;re not doing this, skip ahead to the &lt;a class="link" href="#gem-start-sending" >next step&lt;/a>.&lt;/p>
&lt;p>The issue is that NGINX is only proxying HTTP/HTTPS traffic.&lt;/p>
&lt;p>&lt;a class="link" href="images/EnergySensing3-GEMCantSendToHa.png" >&lt;img src="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/EnergySensing3-GEMCantSendToHa.png"
width="687"
height="136"
srcset="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/EnergySensing3-GEMCantSendToHa_hu_c9f200d244127475.png 480w, https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/EnergySensing3-GEMCantSendToHa_hu_ecabcd55869b59ee.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="505"
data-flex-basis="1212px"
>&lt;/a>&lt;/p>
&lt;p>While it&amp;rsquo;s possible to use NGINX to proxy TCP ports, I wanted to preserve the source port which can&amp;rsquo;t be done with this TCP protocol. The easiest thing is to create a separate Layer 4 Load Balancer to forward just the port we need:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">homeassistant&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allocateLoadBalancerNodePorts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">externalTrafficPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">greeneye&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workload.user.cattle.io/workloadselector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps.deployment-smarthome-homeassistant&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sessionAffinity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">None&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LoadBalancer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">status&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">loadBalancer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ip&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">192.168.6.3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Once the load balancer is created, make note of the IP address in the status block. This will be needed in the next step.&lt;/p>
&lt;h2 id="start-sending-power-data">Start sending power data&lt;/h2>
&lt;p>Then switch to the Network page. Enter the IP address of the load balancer created above or the IP address for Home Assistant in the box, then click save&lt;/p>
&lt;p>&lt;a class="link" href="images/image-3.png" >&lt;img src="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-3.png"
width="1024"
height="534"
srcset="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-3_hu_18675f72322463fa.png 480w, https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-3_hu_2ca63c687f7bd766.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="191"
data-flex-basis="460px"
>&lt;/a>&lt;/p>
&lt;p>After a few seconds, Home Assistant should start populating the dashboard with live power data.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-1.png" >&lt;img src="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-1.png"
width="542"
height="720"
srcset="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-1_hu_570ce782cf463aed.png 480w, https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-1_hu_550e3073a7fb0bc1.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="75"
data-flex-basis="180px"
>&lt;/a>&lt;/p>
&lt;h2 id="setting-up-the-ha-dashboard">Setting up the HA dashboard&lt;/h2>
&lt;h3 id="required-attributes">Required Attributes&lt;/h3>
&lt;p>This integration does not correctly set the attributes so that Home Assistant treats this data as power and instead treats it as opaque sensor data. The only way to fix this is to create a *new* template sensor with the correct attributes. In my case, I didn&amp;rsquo;t have a CT covering total usage for my main panel, so I summed everything together:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">sensor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Main Panel Total Power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">measurement&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">W&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">state&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{ states.sensor
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> | selectattr(&amp;#39;entity_id&amp;#39;, &amp;#39;match&amp;#39;, &amp;#39;sensor.main_panel_*&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> | selectattr(&amp;#39;attributes.unit_of_measurement&amp;#39;, &amp;#39;equalto&amp;#39;, &amp;#39;W&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> | rejectattr(&amp;#39;state&amp;#39;, &amp;#39;equalto&amp;#39;, &amp;#39;unknown&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> | rejectattr(&amp;#39;state&amp;#39;, &amp;#39;equalto&amp;#39;, &amp;#39;unavailable&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> | rejectattr(&amp;#39;entity_id&amp;#39;, &amp;#39;equalto&amp;#39;, &amp;#39;sensor.main_panel_total_power&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> | map(attribute=&amp;#39;state&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> | map(&amp;#39;float&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> | sum }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>However, if you do have a total CT, then you can just &lt;a class="link" href="https://www.home-assistant.io/docs/configuration/customizing-devices/" target="_blank" rel="noopener"
>override&lt;/a> the attributes:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">homeassistant&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">customize&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sensor.main_panel_total_power&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">device_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">state_class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">measurement&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">W&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="watts-to-kilowatt-hours">Watts to kilowatt-hours&lt;/h3>
&lt;p>The energy monitor emits volts and watts, but this isn&amp;rsquo;t very useful because watts is instantaneous power usage, but electric companies and the HA energy dashboard both need to work with kWh (kilowatt-hours, 1kWh = 1,000 watts of power for 1 hour.) To convert from Watts to kWh, we need to integrate the value over time using a &lt;a class="link" href="https://en.wikipedia.org/wiki/Riemann_sum" target="_blank" rel="noopener"
>Riemann sum&lt;/a>. This isn&amp;rsquo;t perfectly accurate, but since the input data is discretized over 5 second intervals, it&amp;rsquo;s close enough.&lt;/p>
&lt;p>In Home Assistant, go to &lt;em>Settings &amp;gt; Integrations &amp;gt; Helpers&lt;/em>.&lt;/p>
&lt;p>Click Create helper &amp;gt; Integration - Riemann sum integral sensor&lt;/p>
&lt;ul>
&lt;li>Name your sensor something like &amp;ldquo;Main Panel Total Energy&amp;rdquo; (Energy because Energy is total power over time.&lt;/li>
&lt;li>Use the Left Riemann sum method (&lt;a class="link" href="https://community.home-assistant.io/t/riemann-integral-calculates-wrong-values-with-electrical-devices/328174" target="_blank" rel="noopener"
>why?&lt;/a>)&lt;/li>
&lt;li>Pick the kilo metric prefix&lt;/li>
&lt;/ul>
&lt;p>&lt;a class="link" href="images/image-4.png" >&lt;img src="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-4.png"
width="603"
height="1021"
srcset="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-4_hu_c9f74a4c7938ddd1.png 480w, https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-4_hu_781af2fec1c4dcd3.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="59"
data-flex-basis="141px"
>&lt;/a>&lt;/p>
&lt;p>Then click submit. Give it a minute or two to update.&lt;/p>
&lt;h3 id="knowing-the-current-kwh-rate">Knowing the current kWh rate&lt;/h3>
&lt;p>A lot of electricity providers have different rates through the year or even consumption based changes that increase the rate as you use more per day or month. My provider, Seattle City Light, publishes their rates &lt;a class="link" href="https://www.seattle.gov/city-light/residential-services/billing-information/rates" target="_blank" rel="noopener"
>here&lt;/a>. They have summer rates and winter rates.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>USD/kWh&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Base service charge per day (Ignored for calculations)&lt;/td>
&lt;td>$0.1974&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1st block per kWh (Summer is &amp;gt;10kWh and Winter is &amp;gt;16kWh)&lt;/td>
&lt;td>$0.1056&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2nd block per kWh&lt;/td>
&lt;td>$0.1307&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>First, I need to create a helper that tracks my daily usage and resets every night. Create a helper in the Home Assistant UI in Settings &amp;gt; Devices &amp;amp; Services &amp;gt; Helpers.&lt;/p>
&lt;p>Create a Utility Meter helper and enter a name and the input sensor. The name is used to create an entity name so it must match what you specify in the template in the next step.&lt;/p>
&lt;p>&lt;a class="link" href="images/image.png" >&lt;img src="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-689x1024.png"
width="689"
height="1024"
srcset="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-689x1024_hu_a641628e55d45e2c.png 480w, https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-689x1024_hu_4dfec83f8a389bb8.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="67"
data-flex-basis="161px"
>&lt;/a>&lt;/p>
&lt;p>Next, I defined a new template sensor. The following sensor is based on my rates, but you can adjust as you need.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">sensor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Electricity kWh Rate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit_of_measurement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">USD/kWh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">state&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {% set month = now().month %}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {% set isSummer = month &amp;gt;= 4 and month &amp;lt;= 9 %}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {% set buckets = [0.1056, 0.1307] %}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {% set bucketskWh = {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#39;winter&amp;#39;: 16,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#39;summer&amp;#39;: 10
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> %}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {% set seasonalThreshold = bucketskWh[&amp;#39;summer&amp;#39; if isSummer else &amp;#39;winter&amp;#39;] %}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {% if states(&amp;#39;sensor.daily_electricity_usage&amp;#39;) | float &amp;gt; seasonalThreshold %}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{ buckets[1] }}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {% else %}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{ buckets[0] }}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {%- endif %}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After that reload the templates in Developer Tools &amp;gt; YAML &amp;gt; YAML Reload Template Sensors.&lt;/p>
&lt;h3 id="setup-the-energy-dashboard">Setup the Energy Dashboard&lt;/h3>
&lt;p>Now you should be able to go to the Energy Dashboard and select the newly created Energy sensor for Grid consumption and rate sensor.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-1.png" >&lt;img src="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-1.png"
width="542"
height="720"
srcset="https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-1_hu_570ce782cf463aed.png 480w, https://www.technowizardry.net/2022/08/accurate-local-home-energy-monitoring-part-3-software-config/images/image-1_hu_550e3073a7fb0bc1.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="75"
data-flex-basis="180px"
>&lt;/a>&lt;/p>
&lt;p>After a few hours you should start seeing metrics in this dashboard.&lt;/p>
&lt;h2 id="reducing-recorder-usage">Reducing Recorder Usage&lt;/h2>
&lt;p>By default, the GreenEye Monitor sends updated data every 5 seconds. This ensures good accuracy, but produces too much data that isn&amp;rsquo;t useful for long-term statistics.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">recorder&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exclude&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">entities&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">sensor.main_panel_*_power&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>In this post, I walked through how to connect the BrulTech to Home Assistant. In the next post, I&amp;rsquo;ll walk through how to manage data sizes with downsampling and data retention policies.&lt;/p>
&lt;p>The Brultech is definitely not as polished as some of the other options, but it was very powerful and provided rich data without depending on the cloud.&lt;/p>
&lt;h2 id="errata">Errata&lt;/h2>
&lt;ul>
&lt;li>2022-11-29 - The template to aggregate up the total power did not ignore irrelevant sensors, such as those named sensors.main_panel_total_energy, which caused the sum to infinitely increase.&lt;/li>
&lt;/ul>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F08%2Faccurate-local-home-energy-monitoring-part-3-software-config%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Accurate%2C+Local+Home+Energy+Monitoring%3A+Part+3+%E2%80%93+Software+Config" style="border:0" alt="" /></description></item><item><title>Split Horizon DNS with external-dns and cert-manager for Kubernetes</title><link>https://www.technowizardry.net/2022/06/split-horizon-dns-with-external-dns-and-cert-manager-for-kubernetes/</link><pubDate>Tue, 21 Jun 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/06/split-horizon-dns-with-external-dns-and-cert-manager-for-kubernetes/</guid><summary>&lt;p>There were a few services that I ran that I wanted to be able to access from both inside my home network and outside my home network. If I was inside my home network, I wanted to route directly to the service, but if I was outside I needed to be able to route traffic through a proxy that would then route into my home lab. Additionally, I wanted to support SSL on all my services for security using cert-manager&lt;/p></summary><description>&lt;p>There were a few services that I ran that I wanted to be able to access from both inside my home network and outside my home network. If I was inside my home network, I wanted to route directly to the service, but if I was outside I needed to be able to route traffic through a proxy that would then route into my home lab. Additionally, I wanted to support SSL on all my services for security using cert-manager&lt;/p>
&lt;p>Since my IPv4 addresses differ inside my network vs outside, I need to use split-horizon DNS to respond with the correct DNS query. Split-horizon DNS refers to the DNS on one horizon (inside the network) showing different results than outside the network.&lt;/p>
&lt;p>One day, we&amp;rsquo;ll all be on IPv6 and this won&amp;rsquo;t be needed anymore because all services will be using globally unique IPv6 addresses, but a las we&amp;rsquo;re stuck in IPv4 land. With IPv6, instead of having separate views of the IP addresses, all services could just get a single global IPv6 address registered. Then no matter where you are, the same IPv6 address gets routed to the correct Kubernetes service.&lt;/p>
&lt;p>My cluster is using MetalLB as it&amp;rsquo;s L4 load balancer and it or an equivalent is required to be able to forward IP-level packets to the DNS server. NodePorts don&amp;rsquo;t work because they use a random port and other servers expect to be able to use port 53/udp and 53/tcp.&lt;/p>
&lt;p>Prior to this, I purchased a domain name, mydomain.com. I reserved a subdomain, *.home.mydomain.com that all of my home lab software will run on-top of. I usually purchase my domain names from &lt;a class="link" href="https://porkbun.com/" target="_blank" rel="noopener"
>porkbun.com&lt;/a>, but any domain name registrar will work.&lt;/p>
&lt;h2 id="the-problem-with-tls">The Problem with TLS&lt;/h2>
&lt;p>To provision certificates, Let&amp;rsquo;s Encrypt needs to confirm that you own or at least have control over the DNS domain name that you&amp;rsquo;re requesting. Since I&amp;rsquo;m running home lab inside my private network that&amp;rsquo;s not accessible to the internet, it won&amp;rsquo;t be able to verify I own *.home.mydomain.com. To fix this, I need cert-manager to be able to create a DNS record on a publicly resolvable DNS record.&lt;/p>
&lt;p>The diagram below shows the problem with split horizon DNS in this case. cert-manager needs to be able to update the external DNS server and be able to verify the DNS record is updated, but if it hits the internal DNS server then it won&amp;rsquo;t be able to confirm everything is working.&lt;/p>
&lt;p>&lt;a class="link" href="images/dnsproblems.svg" >&lt;img src="https://www.technowizardry.net/2022/06/split-horizon-dns-with-external-dns-and-cert-manager-for-kubernetes/images/dnsproblems.svg"
loading="lazy"
alt="A diagram showing how the split-horizon DNS prevents Let’s Encrypt and cert-manager from verifying ownership because there’s two different DNS servers."
>&lt;/a>&lt;/p>
&lt;p>Architectural diagram showing how DNS queries progress through the network. Clients call into Pi-Hole, which then conditionally forwards the lab zone to the CoreDNS instance, everything else goes to the internet. CoreDNS then queries etcd for the lab zone results.&lt;/p>
&lt;h2 id="solution">Solution&lt;/h2>
&lt;p>Let&amp;rsquo;s break down the different components and how to configure each one.&lt;/p>
&lt;p>&lt;a class="link" href="images/area-focus.svg" >&lt;img src="https://www.technowizardry.net/2022/06/split-horizon-dns-with-external-dns-and-cert-manager-for-kubernetes/images/area-focus.svg"
loading="lazy"
>&lt;/a>&lt;/p>
&lt;p>Component diagram showing the software we&amp;rsquo;re going to deploy&lt;/p>
&lt;h3 id="ingress-nginx">Ingress-NGINX&lt;/h3>
&lt;p>Ingress controllers in Kubernetes automatically update each Ingress resource&amp;rsquo;s status block with an IP address that can be used to access that Ingress. However, there are multiple IP addresses that can be associated with a given ingress. If you&amp;rsquo;re using a NodePort service, then that ingress would be pointing to the Node&amp;rsquo;s IP, but a Layer 3 LB service, would need to use the IP of the ingress-nginx&amp;rsquo;s LB.&lt;/p>
&lt;p>See the below example, this ingress is pointing to the node:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Ingress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sonos-api&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">status&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">loadBalancer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ip&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">192.168.2.196&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># This is the node&amp;#39;s IP, not the service IP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The correct IP as exposed by MetalLB for NGINX is 192.168.6.8. To fix this, we need to tell ingress-nginx to instead use it&amp;rsquo;s service IP. If you&amp;rsquo;ve deployed nginx using Helm, then change the values.yaml&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">controller&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">publishService&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Change from false to true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After deploying, NGINX should modify the Ingress statuses to point to the correct IP:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Ingress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sonos-api&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">status&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">loadBalancer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ip&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">**192.168.6.8**&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="etcd">etcd&lt;/h2>
&lt;p>External-dns will store the DNS records in etcd and CoreDNS will look up query responses for the zone in in etcd.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">StatefulSet&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">etcd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workload.user.cattle.io/workloadselector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps.statefulset-dns-etcd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workload.user.cattle.io/workloadselector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps.statefulset-dns-etcd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/bin/sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;-c&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> exec etcd --name ${HOSTNAME} \\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --listen-peer-urls http://0.0.0.0:2380 \\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --listen-client-urls http://0.0.0.0:2379 \\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --advertise-client-urls http://${HOSTNAME}.etcd:2379 \\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --initial-advertise-peer-urls http://${HOSTNAME}:2380 \\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --initial-cluster-token etcd-cluster-1 \\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --initial-cluster-state new \\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --data-dir /var/run/etcd/default.etcd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">quay.io/coreos/etcd:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">etcd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2379&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">client&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2380&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">peer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/var/run/etcd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">hostPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/home/docker/dns-etcd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="external-dns">External-DNS&lt;/h2>
&lt;p>&lt;a class="link" href="https://github.com/kubernetes-sigs/external-dns" target="_blank" rel="noopener"
>External-DNS&lt;/a> is a Kubernetes tool that will take ingresses or services defined in Kubernetes and automatically create DNS records in some provider for you. For ingresses, it takes the IP address defined in the status of the Ingress by the ingress controller (ingress-nginx.)&lt;/p>
&lt;p>Following the Helm install method, udate the following Helm values. Note that I&amp;rsquo;m using the K8s namespace &amp;lsquo;dns&amp;rsquo;. If you deploy your etcd/external-dns in a different namespace, make sure to update ETCD_URLs below. Also note, I&amp;rsquo;m using the fully qualified names because of an issue I discovered and documented in &lt;a class="link" href="https://www.technowizardry.net/2022/05/domain-names-end-with-a-period-and-why-that-causes-problems/" >this post&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ETCD_URLS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://etcd.dns.svc.cluster.local.:2379&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">coredns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">sources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">ingress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Once deployed, check the logs to verify that the records are being created correctly.&lt;/p>
&lt;h2 id="coredns">CoreDNS&lt;/h2>
&lt;p>CoreDNS will answer the DNS queries by querying etcd for records.&lt;/p>
&lt;p>Kubernetes has &lt;a class="link" href="https://github.com/kubernetes/kubernetes/issues/23880" target="_blank" rel="noopener"
>a limitation&lt;/a> where we can&amp;rsquo;t use TCP and UDP on the same Load Balancer service.&lt;/p>
&lt;p>To fix this, you have two options:&lt;/p>
&lt;ol>
&lt;li>Enable the feature gate &lt;code>MixedProtocolLBService&lt;/code>&lt;/li>
&lt;li>Disable TCP based DNS queries and hope you don&amp;rsquo;t exceed the size of a UDP packet (&lt;a class="link" href="https://github.com/helm/charts/issues/13383" target="_blank" rel="noopener"
>ref&lt;/a>.)&lt;/li>
&lt;/ol>
&lt;p>Many Kubernetes clusters come built in with a CoreDNS instance already to handle pod DNS queries.&lt;/p>
&lt;ol>
&lt;li>Create a separate CoreDNS instance to handle split-horizon zone queries.&lt;/li>
&lt;li>Update the existing kube-dns instance (if it&amp;rsquo;s CoreDNS)&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">isClusterService&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">servers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">plugins&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">errors&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">configBlock&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">lameduck 5s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">health&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ready&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">prometheus&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parameters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.0.0.0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="m">9153&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">loadbalance&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">configBlock&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> stubzones
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> path /skydns
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> endpoint http://etcd.dns.svc.cluster.local.:2379&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">etcd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parameters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">home.mydomain.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">zones&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">zone&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">home.mydomain.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Disable TCP unless MixedProtocolLBService=true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># https://github.com/kubernetes/kubernetes/issues/23880&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">scheme&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dns://&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">use_tcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If the downstream DNS server is located outside the cluster, then enable the LB. If you&amp;rsquo;re running something like Pi-hole in the cluster, then you don&amp;rsquo;t need this and can route directly here&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">externalTrafficPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">serviceType&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LoadBalancer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After that, you should have a Load Balancer created like below. Use this to configure your DNS server to forward queries for your internal domain to the LB IP address.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">split-horizon-dns-coredns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LoadBalancer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">status&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">loadBalancer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ip&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">192.168.6.2&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># &amp;lt;-- Changed&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="cert-manager">cert-manager&lt;/h2>
&lt;p>Cert-manager is responsible for requesting certificates from &lt;a class="link" href="https://letsencrypt.org/" target="_blank" rel="noopener"
>Let&amp;rsquo;s Encrypt&lt;/a> (or any other compliant ACME certificate provider) and storing them in your cluster. To install, following the &lt;a class="link" href="https://cert-manager.io/docs/installation/helm/" target="_blank" rel="noopener"
>standard Helm installation process&lt;/a>, but make sure to update following values.&lt;/p>
&lt;p>The &lt;code>--dns01-recursive-nameservers&lt;/code> flag tells cert-manager not to use the internal DNS server to verify propagation of the DNS01 verifier and instead hit Google&amp;rsquo;s public DNS server. Feel free to use any public resolver.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">extraArgs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">dns01-recursive-nameservers=8.8.8.8:53,8.8.4.4:53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">installCRDs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After that, create a ClusterIssuer or Issuer. In the DNS01 solver, you&amp;rsquo;ll need to configure a DNS solver for your domain name. See &lt;a class="link" href="https://cert-manager.io/docs/configuration/acme/dns01/" target="_blank" rel="noopener"
>the official docs&lt;/a> on how to do that for your case.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterIssuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">home-issuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">acme&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">email&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">user@example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">preferredChain&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">privateKeySecretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager-info&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">server&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://acme-v02.api.letsencrypt.org/directory&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">solvers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">dns01&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Point this to your DNS provider&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dnsNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Replace with your domain zone&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">home.mydomain.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;*.home.mydomain.com&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="router-configuration">Router Configuration&lt;/h2>
&lt;p>After the service is deployed, update the router to forward traffic for your given domain name to the newly created service IP address.&lt;/p>
&lt;p>In the Ubiquiti EdgeRouter config, this is defined as:&lt;/p>
&lt;p>&lt;code>set service dns forwarding options server=/home.mydomain.com/192.168.6.2&lt;/code>&lt;/p>
&lt;p>dnsmasq:&lt;/p>
&lt;p>&lt;code>server=/home.mydomain.com/192.168.6.2&lt;/code>&lt;/p>
&lt;h2 id="future-work">Future Work&lt;/h2>
&lt;p>After this is setup, you should be able to access any ingress you&amp;rsquo;ve got configured under your home DNS zone when you&amp;rsquo;re inside your home network.&lt;/p>
&lt;p>If you want to be able to access your home lab services outside your home network, you can use Wireguard or expose NGINX to the internet and create DNS records in a publicly accessible DNS zone. If you expose any services to the internet, do take care to ensure that you securely restrict access. In a future post, I may document how I do this for my own network.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F06%2Fsplit-horizon-dns-with-external-dns-and-cert-manager-for-kubernetes%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Split+Horizon+DNS+with+external-dns+and+cert-manager+for+Kubernetes" style="border:0" alt="" /></description></item><item><title>Best Practices for Java testing with JUnit</title><link>https://www.technowizardry.net/2022/06/best-practices-for-java-testing-with-junit/</link><pubDate>Tue, 14 Jun 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/06/best-practices-for-java-testing-with-junit/</guid><summary>&lt;p>&lt;a class="link" href="https://junit.org/" target="_blank" rel="noopener"
>JUnit&lt;/a> is a popular testing library for Java applications and I extensively used it when working at Amazon for the numerous Java applications and services there. However, I came across a number of different anti-patterns and areas to improve the quality of the test code. This post introduces many of the different tricks and patterns that I&amp;rsquo;ve learned and shared with my coworkers, and now want to share&lt;/p>
&lt;p>Another library to know and reference is &lt;a class="link" href="https://site.mockito.org/" target="_blank" rel="noopener"
>Mockito&lt;/a>, which I use extensively in JUnit test cases and will reference this too below.&lt;/p></summary><description>&lt;p>&lt;a class="link" href="https://junit.org/" target="_blank" rel="noopener"
>JUnit&lt;/a> is a popular testing library for Java applications and I extensively used it when working at Amazon for the numerous Java applications and services there. However, I came across a number of different anti-patterns and areas to improve the quality of the test code. This post introduces many of the different tricks and patterns that I&amp;rsquo;ve learned and shared with my coworkers, and now want to share&lt;/p>
&lt;p>Another library to know and reference is &lt;a class="link" href="https://site.mockito.org/" target="_blank" rel="noopener"
>Mockito&lt;/a>, which I use extensively in JUnit test cases and will reference this too below.&lt;/p>
&lt;p>These are all real things that I&amp;rsquo;ve seen developers do.&lt;/p>
&lt;h3 id="migrate-from-junit4-to-junit5">Migrate from JUnit4 to JUnit5&lt;/h3>
&lt;p>If you&amp;rsquo;re still using JUnit4, why should migrate? Much of this guide will reference features in JUnit5.&lt;/p>
&lt;ul>
&lt;li>JUnit5 has been out since 2017&lt;/li>
&lt;li>JUnit5 removes several testing paradigms that contributed to broken test cases&lt;/li>
&lt;li>JUnit5 test suites can be be run in the same package with JUnit4 test cases allowing you to slowly migrate (&lt;a class="link" href="https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4" target="_blank" rel="noopener"
>reference&lt;/a>)&lt;/li>
&lt;li>JUnit5&amp;rsquo;s Extension API works much better than the runner and can be used to compose different cross-cutting testing concerns&lt;/li>
&lt;/ul>
&lt;p>&lt;a class="link" href="https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4" target="_blank" rel="noopener"
>https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4&lt;/a>&lt;/p>
&lt;h2 id="good-test-cases-give-meaningful-error-messages">Good test cases give meaningful error messages&lt;/h2>
&lt;p>A failing test case that doesn&amp;rsquo;t tell you why it failed is not very developer friendly.&lt;/p>
&lt;p>Test cases are often times capturing business logic and decisions about how code should behave. For example, they check that action X happens when Y case, but sometimes in the future it&amp;rsquo;s hard to understand why X should happen in that case. When it&amp;rsquo;s not obvious why the test case is asserting a situation, provide a useful Javadoc above the method or above the assertion to clarify to other developers.&lt;/p>
&lt;p>Remember: Just because you know why it exists today, doesn&amp;rsquo;t mean you&amp;rsquo;ll remember why you did something 6 months down the road.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="cm">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * CorporateId is a mandatory field on the Device entity. Without it
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * we&amp;#39;d fail the Frobinator process, thus the DAO must reject it.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> **/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testNoRecordSavedWhenCorporationMissing&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Prefer using assertEquals and friends over assertTrue&lt;/p>
&lt;p>Given the following two test cases that both assert on the size of an array:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testAssertTrue&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Long&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myList&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ArrayList&lt;/span>&lt;span class="o">&amp;lt;&amp;gt;&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertTrue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">myList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">size&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">==&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">1&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testAssertEquals&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Long&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myList&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ArrayList&lt;/span>&lt;span class="o">&amp;lt;&amp;gt;&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertEquals&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">size&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Which error message is easier to read?&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">org.opentest4j.AssertionFailedError:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Expected :true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Actual :false
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at FooTest.testAssertTrue(FooTest.java:7)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Or this one using assertEquals?&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">org.opentest4j.AssertionFailedError:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Expected :1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Actual :0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at FooTest.testAssertEquals(FooTest.java:13)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>When assertTrue fails, it doesn&amp;rsquo;t tell you why it failed. You have to go to the line of code to understand why it failed. Imagine if you had five test cases that all failed and they all said gave no useful message. It would take a long time to fix the problem.&lt;/p>
&lt;p>Instead, take a look at the &lt;a class="link" href="https://junit.org/junit5/docs/current/api/org.junit.jupiter.api/org/junit/jupiter/api/Assertions.html" target="_blank" rel="noopener"
>Assertions&lt;/a> class in JUnit5 and find a relevant method that best matches your assertion. Some examples:&lt;/p>
&lt;ul>
&lt;li>assertArrayEquals&lt;/li>
&lt;li>assertEquals&lt;/li>
&lt;li>assertInstanceOf&lt;/li>
&lt;li>assertNotEquals&lt;/li>
&lt;li>assertNotNull&lt;/li>
&lt;li>assertSame&lt;/li>
&lt;li>assertThrows&lt;/li>
&lt;/ul>
&lt;p>If assertTrue is your only option, provide an assertion message (see next item) to help clarify the problem.&lt;/p>
&lt;h3 id="provide-assertion-messages-when-the-problem-is-non-obvious">Provide assertion messages when the problem is non obvious&lt;/h3>
&lt;p>An assertion failure message becomes non-obvious when the message does not clearly convey what property is being compared.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testAssertEquals&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Long&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myList&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ArrayList&lt;/span>&lt;span class="o">&amp;lt;&amp;gt;&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertEquals&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">size&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>When this fails, it just states the expected value is 1, but actual is 0. It doesn&amp;rsquo;t say why.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testAssertEquals&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Long&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myList&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ArrayList&lt;/span>&lt;span class="o">&amp;lt;&amp;gt;&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">myList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">5L&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertEquals&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">size&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;The array is supposed to contain one item because we added one to it.&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Adding a message at the end can help clarify the problem to the developer. It&amp;rsquo;s not required to add messages to all assertions.&lt;/p>
&lt;h3 id="use-assertall-when-testing-different-properties-on-an-entity">Use assertAll when testing different properties on an entity&lt;/h3>
&lt;p>The assertAll method is a special assertion that enables you to perform multiple asserts and fail if any of them failed. If multiple assertions fail, then it&amp;rsquo;ll print out all failed assertions making it easy to see problems at a glance:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testAssertEquals&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">MyObject&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">object&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">MyObject&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;test&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;foobar&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertNotNull&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">object&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Further asserts depend on the above, so it must be separated out&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertEquals&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;testf&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">first&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;First field&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertEquals&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;test&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">second&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;Second field&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>When this fails, it clearly states all the problems at once so I can tackle them instead of fixing one thing, rerunning the tests, then fixing the next problem, until it finally goes green:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">org.opentest4j.MultipleFailuresError: MyObject check (2 failures)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> org.opentest4j.AssertionFailedError: First field ==&amp;gt; expected: &amp;lt;testf&amp;gt; but was: &amp;lt;test&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> org.opentest4j.AssertionFailedError: Second field ==&amp;gt; expected: &amp;lt;test&amp;gt; but was: &amp;lt;foobar&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Note that you shouldn&amp;rsquo;t put all assertions into a single assertAll method. If any assertions depend on previous results, for example I need to separate out into multiple phases of assertions. Otherwise, the future assertion failures provide more and more meaningless messages.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertNotNull&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">object&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">Foo&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">foo&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getFoo&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertNotNull&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">foo&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertEquals&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;baz&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">foo&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getBaz&lt;/span>&lt;span class="p">()),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertEquals&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;bar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">foo&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getBar&lt;/span>&lt;span class="p">())&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="dont-verify-inside-a-finally-block">Don&amp;rsquo;t verify inside a finally block&lt;/h3>
&lt;p>In Java, finally blocks are executed even if an exception is thrown. If your block of code that you&amp;rsquo;re testing fails an assertion or throws an exception, then running more assertions in the finally block will mask the original exception and instead will show you verification failure exception. This will mask the exception that matters with an exception message that is obviously going to fail because the Code under Test failed.&lt;/p>
&lt;p>Bad:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testSomething&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">    &lt;/span>&lt;span class="n">myObject&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">callSomething&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// &amp;lt;-- What if this throws?&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">finally&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">    &lt;/span>&lt;span class="n">Mockito&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">verify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">someObject&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">didSomething&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Instead, avoid running verifications in a finally block and run them after you run your code. This ensures that when your test case fails, you&amp;rsquo;ll always see the most relevant and useful exception message.&lt;/p>
&lt;p>Better:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testSomething&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="n">myObject&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">callSomething&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="n">Mockito&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">verify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">someObject&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">didSomething&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="test-case-accuracy">Test Case Accuracy&lt;/h2>
&lt;h3 id="a-test-case-must-be-able-to-fail">A test case must be able to fail&lt;/h3>
&lt;p>Some developers will just write a unit test that covers their newly written code to get the code coverage, then think that&amp;rsquo;s a sufficient test. A test case that doesn&amp;rsquo;t fail isn&amp;rsquo;t useful and even worse, if it doesn&amp;rsquo;t properly catch bugs, then it gives a false sense of security that the business logic does work correctly.&lt;/p>
&lt;p>Ensure that your test cases do fail when your code has bugs or problems. Try introducing an issue and seeing if your test cases fail. Another strategy is TDD (Test Driven Development.) In this paradigm, you write test cases first that refer to code that doesn&amp;rsquo;t work and implement assertions, then write the code to make the test cases pass.&lt;/p>
&lt;p>The &lt;a class="link" href="https://pitest.org/" target="_blank" rel="noopener"
>PIT Mutation Testing framework&lt;/a> is another strategy to ensure that your test cases are effectively testing code. When you run a PIT test against your unit tests, it&amp;rsquo;ll modify the production code randomly and verify that a test case fails.&lt;/p>
&lt;p>&lt;a class="link" href="#enablecheck" >IntelliJ Inspection Name&lt;/a>: &lt;strong>Java&lt;/strong> -&amp;gt; &lt;strong>JUnit&lt;/strong> -&amp;gt; &lt;strong>JUnit test method without any assertions&lt;/strong>&lt;/p>
&lt;h3 id="dont-use-testexpected--exceptionclass-junit4">Don&amp;rsquo;t use @Test(expected = *Exception.class) (JUnit4)&lt;/h3>
&lt;p>In JUnit4, it&amp;rsquo;s common to write unit tests that look like this to test that your code throws exceptions in error cases:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">expected&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">NullPointerException&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testArgumentAssert&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// If a NullPointerException is thrown ANYWHERE in this test, it&amp;#39;ll pass.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Initialization works&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">something&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">testFunction&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// What if &amp;#34;something&amp;#34; was null?&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">callSomething&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// If the exception is thrown, this never executes&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Mockito&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">verify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">something&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">importantMethod&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>However, this introduces the risk of false test passes, i.e. the test can pass when it should fail.&lt;/p>
&lt;p>&lt;strong>NullPointerException&lt;/strong> is commonly thrown with parameter validators (e.g. &lt;a class="link" href="https://projectlombok.org/features/NonNull" target="_blank" rel="noopener"
>Lombok&amp;rsquo;s @Nonnull&lt;/a>) if the caller passes in a null for a parameter, but it&amp;rsquo;s also thrown if you call a method on a null method. Devs often times want to validate these parameter validators, but since NPE can mean a variety of things, their test cases end up being low value.&lt;/p>
&lt;p>I also frequently see developers expect a &lt;strong>RuntimeException&lt;/strong>, but this has &lt;a class="link" href="https://docs.oracle.com/javase/8/docs/api/java/lang/RuntimeException.html" target="_blank" rel="noopener"
>many subclasses&lt;/a>. If you expect this type, how do you know it&amp;rsquo;s what you expected?&lt;/p>
&lt;p>Additionally, there&amp;rsquo;s a Mockito verification after the exception is thrown. This line will never execute, so your Mockito verification is entirely worthless.&lt;/p>
&lt;p>&lt;strong>Better:&lt;/strong>&lt;/p>
&lt;p>Instead, upgrade to JUnit 5 and use the new &lt;a class="link" href="https://junit.org/junit5/docs/5.0.1/api/org/junit/jupiter/api/Assertions.html#assertThrows-java.lang.Class-org.junit.jupiter.api.function.Executable-" target="_blank" rel="noopener"
>Assertions.assertThrows&lt;/a> method:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testArgumentAssert&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Initialization works&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">something&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">testFunction&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// We guarantee that only NPEs thrown on this line are considered passes&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">NullPointerException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ex&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertThrows&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">NullPointerException&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">callSomething&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// With the exception, we can verify that it&amp;#39;s the exception that we expected.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertEquals&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Argument foo should not be null&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ex&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getMessage&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Mockito&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">verify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">something&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">importantMethod&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>With a handle to the actual exception, you can know that it was thrown on the line you expected, however some care needs to be made still that you&amp;rsquo;re catching what you expect.&lt;/p>
&lt;h2 id="reducing-boilerplate">Reducing Boilerplate&lt;/h2>
&lt;h3 id="create-helper-test-methods">Create helper test methods&lt;/h3>
&lt;p>Test case readability matters. Sometimes a test suite will contain a lot of test case methods that all create test harnesses, create mocks, walk through test flows, or perform validations and they end up looking the same over and over again.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testSomeCase1&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Mockito&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">doReturn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">xyz&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">when&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fooBar&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">someCall&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">foo&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Mockito&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">doReturn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">abc&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">when&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">xyz&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">someCall&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// more test case initialization&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">AResult&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myResult&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myClass&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">performImportantAction&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fooBar&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">1&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Some test code&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertEquals&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;expected&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myRequest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getField&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// more Assertions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testSomeCase2&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Mockito&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">doReturn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">xyz&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">when&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fooBar&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">someCall&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">foo&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Mockito&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">doReturn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">abc&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">when&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">xyz&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">someCall&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// more test case initialization&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">AResult&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myResult&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myClass&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">performImportantAction&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fooBar&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">2&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Some test code&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertEquals&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;expected&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myRequest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getField&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// more Assertions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testSomeCase3&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Mockito&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">doReturn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">xyz&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">when&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fooBar&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">someCall&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">foo&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Mockito&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">doReturn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">abc&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">when&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">xyz&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">someCall&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// more test case initialization&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">AResult&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myResult&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myClass&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">performImportantAction&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fooBar&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">3&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Some test code&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertEquals&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;expected&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myRequest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getField&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// more Assertions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Sure, we were able to add test coverage that verified the code worked, but it&amp;rsquo;s an unreadable mess. Code reviewers won&amp;rsquo;t be able to read it to ensure it&amp;rsquo;s doing the right thing, other developers won&amp;rsquo;t be able to understand it. Instead create reusable methods.&lt;/p>
&lt;p>In the below example, I moved all common logic out to separate methods. Mocks that are needed for all test cases go into the beforeEach, mocks needed only for some methods go into a private method that is then called depending on the test case, then wrapper method is created to call the target method and perform common assertions.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@BeforeEach&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">beforeEach&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Place common initialize code here.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// JUnit calls it before every test case&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Mockito&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">doReturn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">xyz&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">when&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fooBar&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">someCall&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">foo&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Mockito&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">doReturn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">abc&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">when&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">xyz&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">someCall&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// If some test cases have differing situations&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// Create methods that initial mocked objects&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">mockSpecialCase1&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Mockito&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">doReturn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">123&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">when&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">xyz&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">calculateValue&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ARest&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">performActionAndVerify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">case&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">AResult&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myResult&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myClass&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">performImportantAction&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fooBar&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">case&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Perform any common assertions that always exist for all test cases&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertEquals&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;expected&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myRequest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getField&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myResult&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testSomeCase1&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">mockSpecialCase1&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">performActionAndVerify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">1&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testSomeCase2&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">AResult&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myResult&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">performActionAndVerify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">2&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Some test code&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertEquals&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;something&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myRequest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getAnotherField&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// more Assertions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testSomeCase3&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">AResult&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myResult&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">performActionAndVerify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">3&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertEquals&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">myRequest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getAnotherField&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Each test case becomes easier to read as there&amp;rsquo;s less irrelevant code in each method.&lt;/p>
&lt;h3 id="junit5-extensions">JUnit5 Extensions&lt;/h3>
&lt;p>Do you find yourself writing unit test classes that contain lots of the same initialization or tear down logic? In the previous examples, we discussed options for duplicating within a single class, but sometimes multiple classes all have to do some work that isn&amp;rsquo;t the goal of the test class.&lt;/p>
&lt;p>For example, when unit testing a service that emits metrics or X-Ray traces, you may end up with a bunch of code responsible for initializing, collecting, and verifying that those metrics are emitted from many different classes. Each test class itself shouldn&amp;rsquo;t have to handle this logic and instead should delegate to a common class.&lt;/p>
&lt;p>Instead of multiple classes all looking like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">FooControllerTest&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FooController&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fooController&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FooController&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@BeforeEach&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">beforeEach&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Initialize metrics&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Create mocks&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@AfterEach&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">afterEach&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Tear down metrics&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">BarControllerTest&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">BarController&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">BarController&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">BarController&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@BeforeEach&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">beforeEach&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Initialize metrics&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Create mocks&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@AfterEach&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">afterEach&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Tear down metrics&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>For this, JUnit provides the &lt;a class="link" href="https://junit.org/junit5/docs/current/user-guide/#extensions" target="_blank" rel="noopener"
>extension API&lt;/a> that provides many different places to hook into the test runner. Here&amp;rsquo;s how an example extension can cleanup a class:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">FooControllerTest&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FooController&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fooController&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FooController&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// MetricsExtension is a custom extension that you write&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// that handles mocking &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@RegisterExtension&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">MetricsExtension&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">metricsExtension&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">MetricsExtension&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">testApiMethod&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">fooController&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">callApiMethod&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// --- Assertions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Extension classes can be used to handle assertions too&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">metricsExtension&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertMetricsEmitted&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;DatabaseSuccess&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">1&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="appendix">Appendix&lt;/h2>
&lt;h3 id="enabling-intellij-inspections">Enabling IntelliJ Inspections&lt;/h3>
&lt;p>IntelliJ&amp;rsquo;s inspections provide a number of extra static analysis checks that you can enable to catch bugs. To enable one specified in this blog post, go to File -&amp;gt; Settings -&amp;gt; Editor -&amp;gt; Inspections, then find the mentioned inspection.&lt;/p>
&lt;p>All testing related inspections I have enabled:&lt;/p>
&lt;ul>
&lt;li>Java -&amp;gt; JUnit
&lt;ul>
&lt;li>assertEquals() called on array&lt;/li>
&lt;li>JUnit test method in product source&lt;/li>
&lt;li>JUnit test method without any assertions&lt;/li>
&lt;li>JUnit 5 malformed @Nested class&lt;/li>
&lt;li>JUnit 5 malformed repeated test&lt;/li>
&lt;li>Malformed setUp() or tearDown()&lt;/li>
&lt;li>Malformed @Before or @After method&lt;/li>
&lt;li>Malformed @BeforeClass@BeforeAll&lt;/li>
&lt;li>Malformed test method&lt;/li>
&lt;li>Parameterized test class without data provider method&lt;/li>
&lt;li>Test class with no tests&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F06%2Fbest-practices-for-java-testing-with-junit%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Best+Practices+for+Java+testing+with+JUnit" style="border:0" alt="" /></description></item><item><title>How to build a useful service data change audit log</title><link>https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/</link><pubDate>Wed, 25 May 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/</guid><summary>&lt;p>&lt;a class="link" href="images/AuditLog-Component.png" >&lt;img src="https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/AuditLog-Component.png"
width="749"
height="354"
srcset="https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/AuditLog-Component_hu_60dfa8fc5e376e91.png 480w, https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/AuditLog-Component_hu_841e9baca771efd1.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="211"
data-flex-basis="507px"
>&lt;/a>&lt;/p>
&lt;p>If you&amp;rsquo;ve got a service that provides clients with the ability to make changes to those entities, then you probably want an audit log that tracks who makes what changes.&lt;/p>
&lt;p>I decided to write this post because I frequently saw teams at Amazon not thinking through these considerations. Some of the guidance does focus on AWS IAM, but a lot of it is practical for any type of audit log.&lt;/p></summary><description>&lt;p>&lt;a class="link" href="images/AuditLog-Component.png" >&lt;img src="https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/AuditLog-Component.png"
width="749"
height="354"
srcset="https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/AuditLog-Component_hu_60dfa8fc5e376e91.png 480w, https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/AuditLog-Component_hu_841e9baca771efd1.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="211"
data-flex-basis="507px"
>&lt;/a>&lt;/p>
&lt;p>If you&amp;rsquo;ve got a service that provides clients with the ability to make changes to those entities, then you probably want an audit log that tracks who makes what changes.&lt;/p>
&lt;p>I decided to write this post because I frequently saw teams at Amazon not thinking through these considerations. Some of the guidance does focus on AWS IAM, but a lot of it is practical for any type of audit log.&lt;/p>
&lt;p>Important aspects to an audit log:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Who&lt;/strong> made the change?&lt;/li>
&lt;li>&lt;strong>When&lt;/strong> did they make the change?&lt;/li>
&lt;li>&lt;strong>Where&lt;/strong> did they make the change?&lt;/li>
&lt;li>&lt;strong>What&lt;/strong> did they do?&lt;/li>
&lt;/ul>
&lt;p>Not all audit logs are the same. I predominately worked in services that owned some kind of data, then needed to track who and what changes were made to its data in a SOA (Service Oriented Architecture) environment. An audit log that tracks user sign-ins or other types of operations may not have any changes to an entity, but they still have a &lt;strong>Who&lt;/strong>, &lt;strong>When&lt;/strong>, and &lt;strong>Where&lt;/strong>.&lt;/p>
&lt;h2 id="terminology">Terminology&lt;/h2>
&lt;ul>
&lt;li>Principal - An authenticated entity that can make calls to your service. Can be a person, computer, or a service&lt;/li>
&lt;li>Service Principal - A Principal that represents a service. Since a service may be made up of more than one host/process/AWS Account, this uniquely represents the service&lt;/li>
&lt;li>User Principal - A Principal representing a human.&lt;/li>
&lt;/ul>
&lt;h2 id="characteristics-of-the-log-itself">Characteristics of the Log Itself&lt;/h2>
&lt;p>&lt;strong>Append Only&lt;/strong>&lt;/p>
&lt;p>Audit Logs are built for security purposes to know who made a change. If a malicious actor can rewrite the audit log to hide their tracks. An audit log must be append only. It should not be possible for any entries to be deleted or modified. The service should only have the ability to write new entries.&lt;/p>
&lt;p>&lt;strong>Consider the Domain instead of Storage&lt;/strong>&lt;/p>
&lt;p>I wrote an &lt;a class="link" href="https://www.technowizardry.net/2022/03/stop-using-your-storage-models-everywhere/" >earlier post about the difference&lt;/a> between domain models and storage models that is relevant. The domain model are the core classes that you implement business logic upon, whereas the storage model is what that domain model when forced into the limitations that a given data store requires.&lt;/p>
&lt;p>Your audit log should be composed of changes to the domain, not based on the storage model.&lt;/p>
&lt;p>&lt;strong>Versioned&lt;/strong>&lt;/p>
&lt;p>Take care when designing the schema for your audit log, since it has a longer lifetime than your storage schema. With a database, you can run migrations to upgrade the schema, but for security audit logs may be append only and not allow any writes.&lt;/p>
&lt;h2 id="who-made-the-change">Who made the change?&lt;/h2>
&lt;p>Who called you is really what person or system made the change. This identity should come directly out of your authentication system It may seem easy at first, but make sure you don&amp;rsquo;t make any bad assumptions.&lt;/p>
&lt;h3 id="find-a-meaningful-service-identity">Find a meaningful service identity&lt;/h3>
&lt;p>When you&amp;rsquo;re processing a request, you need to think about your authentication system to figure out what types of identities you have. Does your authentication handler give you a user id or is it generic like an AWS IAM ARN?&lt;/p>
&lt;p>If you use AWS IAM for all calls to your service both human and services with something like Cognito (which I don&amp;rsquo;t recommend using,) then you&amp;rsquo;ll get several different pieces of information such as an ARN (eg. &lt;em>arn:aws:iam::1234567890123:root&lt;/em>) and session tags. Use this information to convert to something meaningful like a user id or map the account id to a known service name.&lt;/p>
&lt;p>&lt;code>arn:aws:iam::1234567890123:assume-role/UserAccessRole_ + STS Session Tags -&amp;gt; userid:2345 (user: foobar123)&lt;/code>&lt;/p>
&lt;h3 id="dont-assume-that-all-callers-are-the-same-type-eg-always-human">Don&amp;rsquo;t assume that all callers are the same type (e.g. always human)&lt;/h3>
&lt;p>I&amp;rsquo;ve frequently seen services just store a username as a string in the event log:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">AuditLogEntry&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">username&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Example: &amp;#39;user@example.com&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>What happens if your service also allows non-humans, such as service callers? If you used to store a username like &amp;ldquo;foobar123&amp;rdquo; in username, now trying to store a service identity like &amp;ldquo;InventoryForecastingService/Prod&amp;rdquo; in the same field is not useful because you don&amp;rsquo;t know if it&amp;rsquo;s a human or service.&lt;/p>
&lt;p>Instead, distinguish between identities from different identity providers by including a type:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Provide a different type for all unique identity providers. &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">enum&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ActorType&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">USER&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">SERVICE&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">ActorIdentity&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">ActorType&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">type&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">identity&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">AuditLogEntry&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">ActorIdentity&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">actor&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="consider-systems-acting-on-behalf-of-another-identity">Consider systems acting on behalf of another identity&lt;/h3>
&lt;p>Sometimes services will need to call another service, but will be acting on behalf of another identity, such as a user. This might happen in a transitive service call, when a user calls a service that then needs to call another service:&lt;/p>
&lt;p>&lt;a class="link" href="images/AuditLog-Transitive-1.png" >&lt;img src="https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/AuditLog-Transitive-1.png"
width="559"
height="195"
srcset="https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/AuditLog-Transitive-1_hu_b7b9a4d38c46ee0e.png 480w, https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/AuditLog-Transitive-1_hu_c106cde540c2b27a.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="286"
data-flex-basis="688px"
>&lt;/a>&lt;/p>
&lt;p>When Service B receives the request, it&amp;rsquo;s important that it logs both identities since Service A calling it is different than the user calling directly.&lt;/p>
&lt;p>There&amp;rsquo;s two different ways of representing this:&lt;/p>
&lt;ol>
&lt;li>The primary caller is ServiceA and an optional OnBehalfOf attribute denotes the transitive user&lt;/li>
&lt;li>The primary caller is UserA and an optional Via attribute denotes the service that made the call&lt;/li>
&lt;/ol>
&lt;p>I&amp;rsquo;ve built systems with both cases, but I prefer the second solution because generally authorization systems primarily evaluate the user permissions and service permissions are coarse grained and the relevant principal is the user.&lt;/p>
&lt;p>Make sure to log both principals to the log:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Provide a different type for all unique identity providers. &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">enum&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ActorType&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">USER&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">SERVICE&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">ActorIdentity&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">ActorType&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">type&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">identity&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">AuditLogEntry&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">ActorIdentity&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">actor&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Optional&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">ActorIdentity&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">via&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="example-from-aws-cloudtrail">Example from AWS CloudTrail&lt;/h3>
&lt;p>For a practical example, we can take a look at how AWS CloudTrail exposes their identities to AWS customers (&lt;a class="link" href="https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-user-identity.html" target="_blank" rel="noopener"
>docs&lt;/a>).&lt;/p>
&lt;p>Note how depending on the type, there are different relevant attributes:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;userIdentity&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Root&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;principalId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;1234567890123&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;arn&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;arn:aws:iam::1234567890123:root&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;accountId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;1234567890123&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;accessKeyId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ASIABCDEFGHIJKLMNOPQ&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>When an internal AWS service calls your service, it doesn&amp;rsquo;t provide any of the internal details and instead exposes a service principal. While internally they&amp;rsquo;re using IAM, this is an example of how the low-level IAM identity may get mapped into a more meaningful identity when stored in a log:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;userIdentity&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;AWSService&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;invokedBy&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;compute-optimizer.amazonaws.com&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="consider-privacy-laws-like-gdpr-and-use-non-changing-identifiers">Consider privacy laws like GDPR and use non-changing identifiers&lt;/h3>
&lt;p>Privacy laws like GDPR will come into play when you&amp;rsquo;re storing a user&amp;rsquo;s actions in a log. Often times usernames are the user&amp;rsquo;s email address which has two problems: it can change and they are considered personal information.&lt;/p>
&lt;p>Instead of storing a username or email address in the log, use a unique, non-changing user id that refers to the actual user in another identity service. This reduces the number of services that store sensitive personal information.&lt;/p>
&lt;h2 id="where-did-they-make-the-change">Where did they make the change?&lt;/h2>
&lt;p>Now we know who supposedly made the change, but what happens if a malicious actor secured valid credentials to a user? The next part is including relevant information to aid any investigation.&lt;/p>
&lt;p>Some information to include:&lt;/p>
&lt;ul>
&lt;li>Timestamp&lt;/li>
&lt;li>IP Address&lt;/li>
&lt;li>User Agent&lt;/li>
&lt;li>Session Id&lt;/li>
&lt;/ul>
&lt;h2 id="what-change-did-they-make">What change did they make?&lt;/h2>
&lt;p>The next problem to solve is to figure out how to represent what mutations they made on the models.&lt;/p>
&lt;p>Audit logs should be thought in terms of the domain, not the storage model. The domain model are the core classes that you implement business logic upon, whereas the storage model is what that domain model when forced into the limitations that a given data store requires. I wrote an &lt;a class="link" href="https://www.technowizardry.net/2022/03/stop-using-your-storage-models-everywhere/" >earlier post about the difference&lt;/a> between domain models and storage models that is relevant.&lt;/p>
&lt;p>&lt;strong>Snapshot&lt;/strong>&lt;/p>
&lt;p>A snapshot based log stores a snapshot of the entity at each version.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">v1:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;GivenName&amp;#34;: &amp;#34;John&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;FamilyName&amp;#34;: &amp;#34;Jones&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Email&amp;#34;: &amp;#34;jpjones2@technowizardry.net&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Entitlements&amp;#34;: [ &amp;#34;CAN_READ&amp;#34;, &amp;#34;CAN_WRITE&amp;#34;, &amp;#34;CAN_DELETE&amp;#34; ]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">v2:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;GivenName&amp;#34;: &amp;#34;John&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;FamilyName&amp;#34;: &amp;#34;Jones&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Email&amp;#34;: &amp;#34;jpjones@technowizardry.net&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Entitlements&amp;#34;: [ &amp;#34;CAN_READ&amp;#34;, &amp;#34;CAN_WRITE&amp;#34;, &amp;#34;CAN_DELETE&amp;#34; ]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">v3:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;GivenName&amp;#34;: &amp;#34;John Paul&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;FamilyName&amp;#34;: &amp;#34;Jones&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Email&amp;#34;: &amp;#34;jpjones@technowizardry.net&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Entitlements&amp;#34;: [ &amp;#34;CAN_READ&amp;#34;, &amp;#34;CAN_WRITE&amp;#34;, &amp;#34;CAN_FOOBAR&amp;#34; ]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Snapshots make it easy to revert and back to prior revisions, but to actually figure out what changes in between v2 and v3, you need to grab both versions then diff each field. This can get quite complex if you have complex objects with nested fields or arrays.&lt;/p>
&lt;p>&lt;strong>Diff&lt;/strong>&lt;/p>
&lt;p>A diff based log stores only the values that changed from the previous version.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">v1 (All fields because it&amp;#39;s the first version)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;GivenName&amp;#34;: &amp;#34;John Paul&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;FamilyName&amp;#34;: &amp;#34;Jones&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Email&amp;#34;: &amp;#34;jpjones2@technowizardry.net&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Entitlements&amp;#34;: [ &amp;#34;CAN_READ&amp;#34;, &amp;#34;CAN_WRITE&amp;#34;, &amp;#34;CAN_DELETE&amp;#34; ]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">v2:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Email&amp;#34;: &amp;#34;jpjones@technowizardry.net&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">v3:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;GivenName&amp;#34;: &amp;#34;John Paul&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> **&amp;#34;Entitlements&amp;#34;: ???**
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Reading through from v1 to v3, we see that the first entry includes all fields because it&amp;rsquo;s newly created. The second entry only contains the email change.&lt;/p>
&lt;p>But how do we represent the third change? Primitive types are easy to represent, but how do you represent an array? If you represent it as &lt;code>[&amp;quot;CAN_READ&amp;quot;, &amp;quot;CAN_WRITE&amp;quot;, &amp;quot;CAN_FOO&amp;quot;]&lt;/code>, the new entire value, then it&amp;rsquo;s effectively a snapshot at the field level and you need to still do a field level diff to identify the change.&lt;/p>
&lt;p>Another way is to break down the changes to arrays into additions and removals:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">v3:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;GivenName&amp;#34;: &amp;#34;John Paul&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Entitlements&amp;#34;: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Added&amp;#34;: [&amp;#34;CAN_FOO&amp;#34;],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Removed&amp;#34;: [&amp;#34;CAN_DELETE&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now the audit log clearly states what *changed* in v3 of the log. Other complex types can be broken down similarly.&lt;/p>
&lt;h2 id="how-does-this-relate-to-event-sourcing-and-cqrs">How does this relate to Event Sourcing and CQRS?&lt;/h2>
&lt;p>In conventional data store patterns, the service will read and write to the same entity in the database with no separation. The database does not inherently maintain a log of changes to a given entity and an audit log must be created independently of the main entities.&lt;/p>
&lt;p>&lt;a class="link" href="images/EventSourcing-Example.png" >&lt;img src="https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/EventSourcing-Example.png"
width="462"
height="417"
srcset="https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/EventSourcing-Example_hu_557cc7947ceb30c6.png 480w, https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/EventSourcing-Example_hu_917dfd68f69d0543.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="110"
data-flex-basis="265px"
>&lt;/a>&lt;/p>
&lt;p>Event sourcing is an architectural pattern in which the data store actually maintains the events and changes to a given entity. The current state of an entity is discovered by replaying all of the events from the start. For performance reasons, event sourcing systems often times create periodic snapshots so the entire store doesn&amp;rsquo;t have to be scanned.&lt;/p>
&lt;p>This has the advantage that the change log inherently represents most of the information in an audit log (the change to the entity) and just needs to be supplemented with identity information to become useable.&lt;/p>
&lt;p>While this inherent audit log behavior is quite valuable, there are a few disadvantages. In a sample architecture built upon AWS DynamoDB:&lt;/p>
&lt;p>&lt;a class="link" href="images/EventSourcing-Architecture.png" >&lt;img src="https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/EventSourcing-Architecture.png"
width="676"
height="203"
srcset="https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/EventSourcing-Architecture_hu_dcc8bf6525f04a6d.png 480w, https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/EventSourcing-Architecture_hu_6fc0eb202f88bf6e.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="333"
data-flex-basis="799px"
>&lt;/a>&lt;/p>
&lt;p>&lt;strong>Worse Consistency&lt;/strong> - While data stores like DynamoDB do have eventual consistency where a write may not be instantly seen by a read afterwards, this can become even more prevalent in event sourcing data stores (especially if you build them on-top of other data stores.)&lt;/p>
&lt;p>Each event must be strictly serializable such that event B comes after event A. In the above sample architecture, this means that when you create a new event you must get the latest snapshot and apply all pending events before you can create a new event.&lt;/p>
&lt;p>&lt;a class="link" href="images/EventSourcing-Problem.png" >&lt;img src="https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/EventSourcing-Problem.png"
width="462"
height="369"
srcset="https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/EventSourcing-Problem_hu_64aff73b945a3504.png 480w, https://www.technowizardry.net/2022/05/how-to-build-a-useful-service-data-change-audit-log/images/EventSourcing-Problem_hu_680ba6c6fce4e551.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="125"
data-flex-basis="300px"
>&lt;/a>&lt;/p>
&lt;p>&lt;strong>Lack of Type Safety through Strong Domain Models&lt;/strong> - As I mentioned in my &lt;a class="link" href="https://www.technowizardry.net/2022/03/stop-using-your-storage-models-everywhere/" >blog post about domain model safety&lt;/a>, my preferred strategy is to develop strictly validated domain model classes that implement business logic. Only when the entity is saved is it converted to a loose storage model and saved to the data store. This becomes very challenging in the above sample architecture. Any validations must be performed when the event is created and before it&amp;rsquo;s saved since any invalid customer input should result in a 400 error to the client.&lt;/p>
&lt;p>However, the service merely creates events that &amp;ldquo;request&amp;rdquo; an entity to be changed. The consumer is the component that actually makes the change and saves it. If the consumer rejects a change then it could block processing of other events and cause a system outage.&lt;/p>
&lt;p>Instead, the event should only be created if it results in a valid entity on the output. Unfortunately this can be challenging to implement in code since validation logic needs to be implemented in each command that creates an event. There&amp;rsquo;s no one single, central place that enforces validations.&lt;/p>
&lt;p>&lt;strong>Architectural Complexity&lt;/strong> - In the sample architecture, there are more moving parts involved. All of this adds more things you have to monitor, more code written, and more things to deploy. Developer time isn&amp;rsquo;t free and systems will break in weird ways at 3am on a weekend.&lt;/p>
&lt;p>&lt;strong>Conclusion&lt;/strong> - Both strategies, event sourcing as the audit log and separate audit logs, have advantages and disadvantages. I&amp;rsquo;ve worked in multiple systems and have had a chance to see both in practice. The goal of this post was to introduce some of the differences and help ask questions so you can build the best solution for your project.&lt;/p>
&lt;h2 id="how-do-you-identify-changes">How do you identify changes?&lt;/h2>
&lt;p>Now that we have some characteristics of a good audit log, how do we actually generate audit events in the source code? This is going to depend heavily on your language and frameworks. In my work, I always used in-house built solutions. Automated solutions didn&amp;rsquo;t really do what I needed. However, some of the below libraries may be valuable:&lt;/p>
&lt;ul>
&lt;li>Java - &lt;a class="link" href="https://javers.org/" target="_blank" rel="noopener"
>Javers&lt;/a>&lt;/li>
&lt;li>Ruby on Rails - &lt;a class="link" href="https://github.com/paper-trail-gem/paper_trail" target="_blank" rel="noopener"
>paper_trail gem&lt;/a> - This one seems to use the snapshot style to store revisions so you have to compute diffs yourself&lt;/li>
&lt;/ul>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>In this post, I walked through various aspects of useful audit log for services that control access to a data set and allow users to make changes. This should provide a framework of questions and concerns for you to build your own audit log.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F05%2Fhow-to-build-a-useful-service-data-change-audit-log%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=How+to+build+a+useful+service+data+change+audit+log" style="border:0" alt="" /></description></item><item><title>Best Practices for working with Google Guice</title><link>https://www.technowizardry.net/2022/05/best-practices-for-working-with-google-guice/</link><pubDate>Wed, 18 May 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/05/best-practices-for-working-with-google-guice/</guid><summary>&lt;p>Google Guice is a dependency injection library for Java and I frequently used it on a number of Java services. Compared to Spring, I liked how simple and narrow focused on just dependency injection it was. However, I often times saw developers using it in incorrect or non-ideal patterns that increased boilerplate or were just wrong.&lt;/p>
&lt;p>These are all recommendations that I&amp;rsquo;ve accumulated over several years at working at Amazon watching engineers and sometimes myself improperly leverage Google Guice.&lt;/p></summary><description>&lt;p>Google Guice is a dependency injection library for Java and I frequently used it on a number of Java services. Compared to Spring, I liked how simple and narrow focused on just dependency injection it was. However, I often times saw developers using it in incorrect or non-ideal patterns that increased boilerplate or were just wrong.&lt;/p>
&lt;p>These are all recommendations that I&amp;rsquo;ve accumulated over several years at working at Amazon watching engineers and sometimes myself improperly leverage Google Guice.&lt;/p>
&lt;h2 id="recommendations">Recommendations&lt;/h2>
&lt;h3 id="when-to-reuse-modules">When to reuse modules&lt;/h3>
&lt;p>If you&amp;rsquo;ve got multiple different components (e.g. a service API, workers, or other processes) then your Guice modules can be reused and shared to reduce boilerplate code.&lt;/p>
&lt;p>Break down your bindings into modules with categories:&lt;/p>
&lt;ul>
&lt;li>Application-specific - These are bindings are for a specific application (e.g. FooServiceModule, FooWorkerModule) and are the top level module that imports everything else&lt;/li>
&lt;li>Environment-specific - Contains any bindings that make I/O calls that are shared by may differ between apps, for example AWS clients, secret providers, etc.&lt;/li>
&lt;li>Feature-specific - Everything else falls in here. Organize them based on related bindings. Like AuthenticationModule, AuditLogModule, FooFeatureModule.&lt;/li>
&lt;/ul>
&lt;h3 id="prefer-interfaces-over-concrete-classes">Prefer Interfaces over concrete classes&lt;/h3>
&lt;p>The purpose of dependency injection is to minimize coupling between different components. If your class binds to concrete classes, then you&amp;rsquo;re preventing others from swapping out the implementations based on the environment.&lt;/p>
&lt;p>&lt;strong>Considerations&lt;/strong>&lt;/p>
&lt;p>If it&amp;rsquo;s impractical to create separate implementations for a given class, such as pure functional class with no I/O or other external dependencies such as a helper class, then it&amp;rsquo;s okay to inject the concrete class.&lt;/p>
&lt;p>If you need to have multiple copies of the same effective class, then use a @Named instead.&lt;/p>
&lt;p>&lt;strong>Example&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">SpecificFooClient&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">implements&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FooClient&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">FooModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="n">SpecificFooClient&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">providesFooClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ClassA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SpecificFooClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">ConsumingClass&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@Inject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">ConsumingClass&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SpecificFooClient&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">client&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>In the above example, the Guice binding returns a concrete class type instead of an interface. This forces consuming classes to refer to the concrete class instead of the generic interface. When writing unit tests, now you&amp;rsquo;re forced to construct the real class instead of swapping out a mocked up implementation.&lt;/p>
&lt;h3 id="avoid-named-to-signal-implementation-when-irrelevant">Avoid @Named to signal implementation when irrelevant&lt;/h3>
&lt;p>@Named bindings can suffer from the same problem as using concrete classes vs interfaces. Don&amp;rsquo;t use @Named as a way to signal what specific implementation a class is, instead only add @Named when you need to have two copies. For example, use it to delineate two different AWSCredentialProviders to two different AWS accounts.&lt;/p>
&lt;p>&lt;strong>Example&lt;/strong>&lt;/p>
&lt;p>I found one service that using @Named to signal the effective type of the class. There was a service client that was exposed by the Guice config that was different depending on the process it was running in. A long-running service got a client that had a time limited cached, whereas a short-term scheduled job process got a client that cached for the entire lifetime of the job.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">FooModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@Named&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;CACHING_FOO_CLIENT&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="n">FooClient&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">providesFooClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ClassA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SpecificFooClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// In another module&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">FooModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@Named&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;NON_CACHED_FOO_CLIENT&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="n">FooClient&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">providesFooClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ClassA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SpecificFooClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>In the above example, there was only ever one type of FooClient that should be available in each process, but depending on the process a different FooClient should be loaded. By including the type in the @Named qualification, it caused the rest of the Guice graph to become more complicated because they all had to be aware of the type instead of being agnostic.&lt;/p>
&lt;p>In the below alternative, the Guice binding becomes generic to only expose an abstract FooClient and it internalizes the logic to decide the caching strategy. The rest of the code becomes simpler.&lt;/p>
&lt;p>Better:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// In another module&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">FooModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="n">FooClient&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">providesFooClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ClassA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">isShortRunningJob&lt;/span>&lt;span class="p">())&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LongCachedFooClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">else&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TimeLimitedCacheFooClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Or create two separate Guice modules for each environment that return the correct type and avoid any runtime configuration:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">ShortRunningJobModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="n">FooClient&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">providesFooClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ClassA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LongCachedFooClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">LongRunningServiceModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="n">FooClient&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">providesFooClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ClassA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TimeLimitedCacheFooClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="avoid-bindfooclasstoinstancenew-foo">Avoid bind(Foo.class).toInstance(new Foo(&amp;hellip;))&lt;/h3>
&lt;p>In the following example, I&amp;rsquo;m binding the class type Foo.class to an instance of the Foo class, however this defeats the purpose of Guice since I&amp;rsquo;m not using it to initialize the class. Any dependency that the MessageProcessor has must be initialized outside of Guice&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">configure&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="n">bind&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">MessageProcessor&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">toInstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">MessageProcessor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">sqsClientBuilder&lt;/span>&lt;span class="p">())&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Instead, add @Inject to the MessageProcessor class and use Guice to fully instantiate the class.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">MessageProcessor&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@Inject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">MessageProcessor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SqsClientBuilder&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">builder&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">MyModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">extends&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AbstractModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">configure&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// This space intentionally left blank because Guice automatically &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// knows how to initialize it when used&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>With the new approach, I can easily add or remove new constructor arguments without having to update several different points in the code.&lt;/p>
&lt;h3 id="use-singletons-carefully">Use @Singletons carefully&lt;/h3>
&lt;p>Imagine if you have a dependency graph that looks like this. We have a root class marked as @Singleton. Only one class was marked as a Singleton, but the Jackson ObjectMapper at the bottom is not marked as Singleton. The Jackson ObjectMapper class is notoriously expensive to construct and has frequently caused massive latency issues in services because they don&amp;rsquo;t cache it.&lt;/p>
&lt;p>&lt;a class="link" href="images/Guice-SingletonGraph-1.png" >&lt;img src="https://www.technowizardry.net/2022/05/best-practices-for-working-with-google-guice/images/Guice-SingletonGraph-1.png"
width="495"
height="175"
srcset="https://www.technowizardry.net/2022/05/best-practices-for-working-with-google-guice/images/Guice-SingletonGraph-1_hu_68b35c7afa151da3.png 480w, https://www.technowizardry.net/2022/05/best-practices-for-working-with-google-guice/images/Guice-SingletonGraph-1_hu_238b4ea7c39bf033.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="282"
data-flex-basis="678px"
>&lt;/a>&lt;/p>
&lt;p>MyRootClass was marked as @Singleton because the developer knew it had classes that were expensive and they didn&amp;rsquo;t want to redundantly create classes.&lt;/p>
&lt;p>&lt;strong>Problem&lt;/strong>: If another class refers to MyOtherClass or reuses the Guice module or that binding for other use cases, the ObjectMapper isn&amp;rsquo;t marked as Singleton. Thus we&amp;rsquo;d accidentally re-instantiate it each time.&lt;/p>
&lt;p>This is an example of a developer poorly communicating their desires through code. If a class needs to be a Singleton, then that binding itself should be marked as&lt;/p>
&lt;p>&lt;strong>Solution&lt;/strong>: Don&amp;rsquo;t depend on your root classes to be marked as @Singleton. Instead use @Singleton on any class that needs to be a Singleton. Don&amp;rsquo;t mark it on every class, just the ones that care. Guice will automatically figure out which classes need to be recreated and which ones will be instantiated. That way you can re-use shared Guice modules across multiple systems.&lt;br>
&lt;strong>Example&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">com.fasterxml.jackson.databind.ObjectMapper&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">javax.inject.Singleton&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">SerializationModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">extends&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AbstractModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">configure&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">bind&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ObjectMapper&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">in&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Singleton&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>See Also: &lt;a class="link" href="#prod-stage" >Use the Prod Stage&lt;/a>&lt;/p>
&lt;h3 id="use-the-prod-stage-when-your-service-is-deployed">Use the Prod Stage when your service is deployed&lt;/h3>
&lt;p>Guice may eager or lazily initialize the Singleton depending on what stage (ie. PRODUCTION or DEVELOPMENT) the Injector was created using. &lt;a class="link" href="https://github.com/google/guice/wiki/Scopes#eager-singletons" target="_blank" rel="noopener"
>See here&lt;/a> for details.&lt;/p>
&lt;p>Lazily initialized Singletons can very easily cause a high latency for the initial few requests to the service until Guice initializes all instances. Instead, you want to eagerly initialize all instances. Since this generally happens before the service added to a load balancer you can take as much time as needed to initialize singletons. This also has the benefit of the JVM preloading most of your code base.&lt;/p>
&lt;p>Example on how to initialize Guice with eagerly initialized singletons:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">com.google.inject.Injector&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">com.google.inject.Stage&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">Injector&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">injector&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Injector&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">createInjector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Stage&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">PRODUCTION&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Module&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="prefer-bind-over-provides-or-providers">Prefer bind() over @Provides or Providers&lt;/h3>
&lt;p>The &lt;em>AbstractModule.bind()&lt;/em> method is only one line of code compared to &lt;em>@Provides&lt;/em> or a &lt;em>Provider&lt;/em>. It&amp;rsquo;s far more concise and doesn&amp;rsquo;t require changes if you add or remove parameters in your constructor.&lt;/p>
&lt;p>Only define a @Provides when you specifically have complex initialization logic that can&amp;rsquo;t be handled by Guice. Creating them increases the amount of boilerplate code that your application includes with no improvement in code readability.&lt;/p>
&lt;p>Bad:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">MyModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">extends&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AbstractModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="nd">@Provides&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="n">FooBar&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">providesFooBar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ClassA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ClassB&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">b&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ClassC&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">c&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ClassD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">d&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SpecificFooBar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">b&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">c&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">d&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">SpecificFooBar&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">SpecificFooBar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ClassA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ClassB&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">b&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ClassC&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">c&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ClassD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">d&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Better:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">MyModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">configure&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">    &lt;/span>&lt;span class="n">bind&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">FooBar&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">to&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SpecificFooBar&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">SpecificFooBar&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@Inject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">SpecificFooBar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ClassA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ClassB&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">b&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ClassC&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">c&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ClassD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">d&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="dont-add-inject-to-a-provides-method">Don&amp;rsquo;t add @Inject to a @Provides method&lt;/h3>
&lt;p>Don&amp;rsquo;t add @Inject to your @Provides methods. This is entirely meaningless. @Inject defines what constructor on a class Guice will use to construct or what fields Guice should inject post instantiating. It does not affect anything when defined on a @Provides or when defined on a class&lt;/p>
&lt;p>Bad:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">MyModuleextends&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AbstractModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@Provides&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@Inject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SomeClass&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">provideSomeClass&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p> Bad:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Bad (This does nothing)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Inject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">SomeClass&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Better:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">SomeClass&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// OK&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@Inject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">someField&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// OK&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@Inject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">SomeClass&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Another&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">classFoo&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">SomeClass&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">someField&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="c1">// OK&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="nd">@Inject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">SomeClass&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Another&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">classFoo&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Even Simpler:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@AllArgsConstructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">onConstructor&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nd">@__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nd">@Inject&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">classSomeClass&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="c1">// OK&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="nd">@Inject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">someField&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="nd">@Inject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">SomeClass&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Another&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">classFoo&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="avoid-inject-on-private-fields">Avoid @Inject on private fields&lt;/h3>
&lt;p>Avoid setting @Inject on fields unless you&amp;rsquo;re defining optional fields with default values (e.g. configuration values.)&lt;/p>
&lt;p>Fields with @Inject on them&amp;hellip;&lt;/p>
&lt;ul>
&lt;li>can&amp;rsquo;t be initialized in unit tests without using Guice to construct them. Developers may forget about this and try to instantiate it with new Example() and will be surprised by a NullPointerException later.&lt;/li>
&lt;li>are initialized after the constructor runs which means you have &amp;ldquo;two initialization phases&amp;rdquo;. This is often unexpected by other developers who expect the constructor to have all the values needed&lt;/li>
&lt;/ul>
&lt;p>Bad:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">Example&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="nd">@Inject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SomeClass&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">someClass&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Better:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">Example&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SomeClass&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">someClass&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@Inject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">Example&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SomeClass&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">someClass&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">someClass&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">someClass&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>See also: &lt;a class="link" href="#lombok-allargsconstructor" >Lombok AllArgsConstructor&lt;/a>&lt;/p>
&lt;h3 id="use-lomboks-allargsconstructor-to-reduce-boilerplate">Use Lombok&amp;rsquo;s AllArgsConstructor to reduce boilerplate&lt;/h3>
&lt;p>Lombok can reduce the amount of boilerplate in your code if you wish to use it. Here&amp;rsquo;s how you can use Lombok to create your constructor.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">javax.inject.Named&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">javax.inject.Inject&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@AllArgsConstructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">onConstructor&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nd">@__&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="nd">@Inject&lt;/span>&lt;span class="p">}))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">Example&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SomeClass&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">someClass&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="c1">// An example with @Named&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="nd">@Named&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;MyTestString&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">aTestString&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And add to your &lt;em>lombok.config&lt;/em>. Without this, the @Named annotation on fields won&amp;rsquo;t be copied to the constructor. This will mean Guice will try to bind a generic value instead of your @Named() value&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">lombok.copyableAnnotations += javax.inject.Named
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="unit-tests">Unit Tests&lt;/h3>
&lt;p>Why would you want to write unit tests for your Guice modules and code?&lt;/p>
&lt;p>What does it mean to unit test Guice modules and bindings? What should you test and not test? Here are some examples of what not to do.&lt;/p>
&lt;p>The following example unit test directly calls the @Provides methods on a Module. Notice how there&amp;rsquo;s 3 different unit tests for a single @Provides methods, but this has several issues:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">expected&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">NullPointerException&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getServiceClient_withNullRegion_throwsException&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">    &lt;/span>&lt;span class="n">serviceClientModule&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">expected&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">NullPointerException&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getServiceClient_withSystemEnvNotSet_throwsException&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">   &lt;/span>&lt;span class="c1">// given&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">   &lt;/span>&lt;span class="n">ENVIRONMENT_VARIABLES&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ServiceClientModule&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">SERVICE_AUTH_ROLE&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">   &lt;/span>&lt;span class="c1">// when&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">   &lt;/span>&lt;span class="n">serviceClientModule&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">REGION&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getServiceClient_withValidRegion_returnsValidClient&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">   &lt;/span>&lt;span class="c1">// given&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">   &lt;/span>&lt;span class="c1">// when&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Interceptor&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">interceptor&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">serviceClientModule&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getAuthInterceptor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">REGION&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">   &lt;/span>&lt;span class="c1">// then&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">   &lt;/span>&lt;span class="n">assertThat&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">interceptor&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">is&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">notNullValue&lt;/span>&lt;span class="p">()));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ol>
&lt;li>It&amp;rsquo;s testing passing nulls into a @Provides method, but &lt;strong>this is useless because Guice won&amp;rsquo;t pass nulls to your @Provides method&lt;/strong> (unless you specifically configure it to do so.) Instead, Guice itself going to throw an exception that says it can&amp;rsquo;t find the binding for your @Provides method. Thus you&amp;rsquo;re testing a path that will never happen.&lt;/li>
&lt;li>Every @Provides method is tested independently &lt;strong>requiring copy and pasted code&lt;/strong> to implement it. There&amp;rsquo;s a lot of work involved just to get code coverage.&lt;/li>
&lt;li>By independently testing methods, you&amp;rsquo;re not really testing to see if the entire dependency graph is valid. For example, what if you had a module like this:&lt;/li>
&lt;li>@Test(expected = NullPointerException.class) is extremely bad because an NPE can be thrown for reasons other than what you expected.&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">BadModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">extends&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AbstractModule&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="c1">// ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="nd">@Provides&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FooBar&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">providesFooBar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">BazBoo&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">baz&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FooBar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">baz&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="nd">@Provides&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Bar&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">providesBar&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">    &lt;/span>&lt;span class="c1">// ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>You could independently test each method, but then it&amp;rsquo;s still fail at runtime because there&amp;rsquo;s no binding for BazBoo, it&amp;rsquo;s actually called Bar. Thus, we&amp;rsquo;ve written a lot of code that adds brittle code, but minimal value add. Guice has much smarter validations than your own tests. Don&amp;rsquo;t repeat them.&lt;/p>
&lt;p>Instead, the following would be better.&lt;/p>
&lt;p>Key Improvements:&lt;/p>
&lt;ul>
&lt;li>We&amp;rsquo;re testing the root-level classes and Guice automatically calls any @Provides, Providers, or @Inject=annotated constructors for us validating correctness&lt;/li>
&lt;li>Using JUnit5&amp;rsquo;s @ParameterizedTest feature reduces the amount of test duplication we have&lt;/li>
&lt;li>We can mock out classes that can&amp;rsquo;t be tested&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Use JUnit5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.junit.jupiter.api.Test&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.junit.jupiter.params.provider.ValueSource&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.junit.jupiter.params.ParameterizedTest&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">ModuleTest&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@ParameterizedTest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@ValueSource&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">classes&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">    &lt;/span>&lt;span class="c1">// Add your root-level classes here. Guice will automatically initialize the entire dependency graph behind the scenes and validate that all dependencies work correctly.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">    &lt;/span>&lt;span class="n">SomeClient&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">    &lt;/span>&lt;span class="n">AnotherClient&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="p">})&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">canInitializeClients&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Class&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">someClass&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">    &lt;/span>&lt;span class="n">Injector&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">injector&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Guice&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">createInjector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">binder&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">     &lt;/span>&lt;span class="n">binder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">install&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ModuleUnderTest&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Install the module(s) you&amp;#39;re testing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">      &lt;/span>&lt;span class="c1">// If you need to mock anything out because it can&amp;#39;t be unit tested, then do this:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">      &lt;/span>&lt;span class="n">binder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">bind&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">AWSCredentialsProvider&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">toInstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Mockito&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">mock&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">AWSCredentialsProvider&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">    &lt;/span>&lt;span class="p">});&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// If your ModuleUnderTest provides objects you need to mock out, then you need to override:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">   &lt;/span>&lt;span class="n">Injector&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">injector&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Guice&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">createInjector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">      &lt;/span>&lt;span class="n">Modules&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">override&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ModuleUnderTest&lt;/span>&lt;span class="p">())&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">      &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">with&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">binder&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">        &lt;/span>&lt;span class="c1">// If you need to mock anything out because it can&amp;#39;t be unit tested, then do this:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">        &lt;/span>&lt;span class="n">binder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">bind&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">AWSCredentialsProvider&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">toInstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Mockito&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">mock&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">AWSCredentialsProvider&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">      &lt;/span>&lt;span class="p">}));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertNotNull&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">injector&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getInstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">someClass&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="binding-only-test-cases">Binding only test cases&lt;/h3>
&lt;p>If the entire purpose of your Guice module is to make a call to an external service and get data for a binding, then mocking it out doesn&amp;rsquo;t do much.&lt;/p>
&lt;p>Instead, you can do a binding only test case that verifies that all binds and providers are available and bound to working methods without actually calling any of the code. While it&amp;rsquo;s not testing code, it&amp;rsquo;s still extremely valuable test case because it can ensure that you&amp;rsquo;re not missing a @Provides or any other type of binding.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.junit.jupiter.api.Test&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.junit.jupiter.params.provider.ValueSource&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.junit.jupiter.params.ParameterizedTest&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">com.google.inject.Stage&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">ModuleTest&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="nd">@ParameterizedTest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="nd">@ValueSource&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">classes&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">    &lt;/span>&lt;span class="c1">// Add your root-level classes here. Guice will automatically initialize the entire dependency graph behind the scenes and validate that all dependencies work correctly.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">    &lt;/span>&lt;span class="n">SomeClient&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">    &lt;/span>&lt;span class="n">AnotherClient&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="p">})&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">canInitializeClients&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Class&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">someClass&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">   &lt;/span>&lt;span class="c1">// Stage.TOOL means that no Providers actually run, but it&amp;#39;s still sufficient to validate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">   &lt;/span>&lt;span class="n">Injector&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">injector&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Guice&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">createInjector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Stage&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">TOOL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ModuleUnderTest&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">    &lt;/span>&lt;span class="n">Assertions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">assertNotNull&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">injector&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getBinding&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">someClass&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">  &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>In this post, I provide several Guice anti-patterns that I&amp;rsquo;ve seen in practice across different teams and alternative approaches to avoid these anti-patterns.&lt;/p>
&lt;p>For more information, see the Guice &lt;a class="link" href="https://github.com/google/guice/wiki" target="_blank" rel="noopener"
>wiki page&lt;/a>.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F05%2Fbest-practices-for-working-with-google-guice%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Best+Practices+for+working+with+Google+Guice" style="border:0" alt="" /></description></item><item><title>Domain names actually end with a period and why that might subtly break your system</title><link>https://www.technowizardry.net/2022/05/domain-names-end-with-a-period-and-why-that-causes-problems/</link><pubDate>Wed, 11 May 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/05/domain-names-end-with-a-period-and-why-that-causes-problems/</guid><summary>&lt;p>DNS is the protocol that converts domain names like &amp;ldquo;technowizardry.net&amp;rdquo; into the IP address of the server that will respond like &amp;ldquo;144.217.181.222&amp;rdquo;. In DNS, domain names actually are supposed to end with a period. For example, the URL of this website is not &amp;ldquo;&lt;a class="link" href="https://www.technowizardry.net" target="_blank" rel="noopener"
>www.technowizardry.net&lt;/a>&amp;rdquo;, but it&amp;rsquo;s actually &amp;ldquo;&lt;a class="link" href="https://www.technowizardry.net" target="_blank" rel="noopener"
>www.technowizardry.net&lt;/a>.&amp;rdquo; Notice the period at the end.&lt;/p>
&lt;p>Where does this come from? If you look at a DNS packet in a packet capture, you&amp;rsquo;ll see that each query looks something like this:&lt;/p></summary><description>&lt;p>DNS is the protocol that converts domain names like &amp;ldquo;technowizardry.net&amp;rdquo; into the IP address of the server that will respond like &amp;ldquo;144.217.181.222&amp;rdquo;. In DNS, domain names actually are supposed to end with a period. For example, the URL of this website is not &amp;ldquo;&lt;a class="link" href="https://www.technowizardry.net" target="_blank" rel="noopener"
>www.technowizardry.net&lt;/a>&amp;rdquo;, but it&amp;rsquo;s actually &amp;ldquo;&lt;a class="link" href="https://www.technowizardry.net" target="_blank" rel="noopener"
>www.technowizardry.net&lt;/a>.&amp;rdquo; Notice the period at the end.&lt;/p>
&lt;p>Where does this come from? If you look at a DNS packet in a packet capture, you&amp;rsquo;ll see that each query looks something like this:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-1.png" >&lt;img src="https://www.technowizardry.net/2022/05/domain-names-end-with-a-period-and-why-that-causes-problems/images/image-1-1024x262.png"
width="1024"
height="262"
srcset="https://www.technowizardry.net/2022/05/domain-names-end-with-a-period-and-why-that-causes-problems/images/image-1-1024x262_hu_11cf56c112648682.png 480w, https://www.technowizardry.net/2022/05/domain-names-end-with-a-period-and-why-that-causes-problems/images/image-1-1024x262_hu_f2173369e1ae3eb3.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="390"
data-flex-basis="938px"
>&lt;/a>&lt;/p>
&lt;p>The queried domain starts right where I&amp;rsquo;ve highlighted in the above picture. Domain names are separated by each period. In this example, I have 3 separate domain parts: [&amp;ldquo;www&amp;rdquo;, &amp;ldquo;technowizardry&amp;rdquo;, &amp;ldquo;net&amp;rdquo;]. The byte sequence looks like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">0000 03 77 77 77 0e 74 65 63 68 6e 6f 77 69 7a 61 72 ·www·tec hnowizar
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">0010 64 72 79 03 6e 65 74 00 dry·net·
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The first byte (0x03) means the first label is 3 bytes. Then 3 bytes come ASCII encoded as &amp;ldquo;www&amp;rdquo;. The next byte (0x0e) comes meaning 14 bytes for &amp;ldquo;technowizardry&amp;rdquo;. Then another 0x03 saying 3 bytes for &amp;ldquo;net&amp;rdquo;. Then finally a 0x00 meaning no more labels in the domain name. The array is null terminated. Once you hit a 0 length label, then you&amp;rsquo;re done. However, this 0x00 also represents a &amp;ldquo;.&amp;rdquo; suffix.&lt;/p>
&lt;h2 id="why-does-this-matter">Why does this matter?&lt;/h2>
&lt;p>You might think, I&amp;rsquo;ve never had to type a dot at the end of a domain name, why should I care? To find out, first thing is to understand how a DNS query gets performed. When you type in a domain name into your browser, it doesn&amp;rsquo;t directly turn what you typed into a DNS query and see what it returns. This process is more complicated with modern browsers that combine the search bar with the address bar and have to figure out if you mean to search or go to a domain name. I&amp;rsquo;ll ignore the search part for now.&lt;/p>
&lt;p>On Linux, the browser or application will call a method, getaddrinfo, which itself is responsible for performing DNS lookup. This method will then consult /etc/resolv.conf to understand how to perform the query. Something like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">nameserver 192.168.2.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">search us-west-2.technowizardry.net technowizardry.net
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">options ndots:1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>In the above file, there&amp;rsquo;s 3 configuration options:&lt;/p>
&lt;ul>
&lt;li>nameserver - Which just states where to forward your queries. This often times come from DHCP&lt;/li>
&lt;li>search - This is the DNS search list. Multiple can be specified. This states that queries for &amp;ldquo;example&amp;rdquo; may be translated into &amp;ldquo;example.us-west-2.technowizardry.net&amp;rdquo; or &amp;ldquo;example.technowizardry.net&amp;rdquo; when creating the query&lt;/li>
&lt;li>options ndots - If the query contains 1 or more dots in the query, then it&amp;rsquo;s considered to be fully qualified. If it contains 0, then it&amp;rsquo;s uses the search list&lt;/li>
&lt;/ul>
&lt;p>Now, maybe you&amp;rsquo;re starting to see the risk. ndots controls whether or not the query is *assumed* to be fully qualified, i.e. complete. Most web users will type in a full domain name and domains will always have at least one dot in them. facebook.com, google.com, etc. All have a dot, so users never notice an issue.&lt;/p>
&lt;p>However, there are a few situations where this will break:&lt;/p>
&lt;h3 id="the-curse-of-email-addresses">The Curse of Email Addresses&lt;/h3>
&lt;p>At my job, a coworker recently had to figure out how to validate an email address to provide early feedback to a user that it may be wrong. That coworker found the Apache Commons &lt;a class="link" href="https://github.com/apache/commons-validator/blob/bef6bf16ab9c2435faaec9c62636929ada6791b5/src/main/java/org/apache/commons/validator/routines/EmailValidator.java#L102" target="_blank" rel="noopener"
>EmailValidator.java class&lt;/a> (note that and the validation looked something like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">boolean&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">isValid&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">email&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">email&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">==&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">email&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">endsWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;.&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now this is interesting. If you give an email that looks like &amp;ldquo;user@foo.&amp;rdquo;, it&amp;rsquo;s considered to be invalid, however this check is wrong.&lt;/p>
&lt;p>It&amp;rsquo;s technically valid to have an MX record (the DNS record type that denotes where to deliver email) on a Top Level Domain (TLD). Several TLDs actually have it (&lt;a class="link" href="https://serverfault.com/questions/154991/why-do-some-tld-have-an-mx-record-on-the-zone-root-e-g-ai" target="_blank" rel="noopener"
>source&lt;/a>):&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">.AI =&amp;gt; mail.offshore.AI.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.AS =&amp;gt; dca.relay.gdns.net.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.BJ =&amp;gt; mail6.domain-mail.com.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.CF =&amp;gt; mail.intnet.CF.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.DJ =&amp;gt; smtp.intnet.DJ.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> =&amp;gt; relais2.intnet.DJ.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.DM =&amp;gt; mail.nic.DM.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.GP =&amp;gt; ns1.nic.GP.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> =&amp;gt; ns34259.ovh.net.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> =&amp;gt; manta.outremer.com.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.HR =&amp;gt; alpha.carnet.HR.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.IO =&amp;gt; mailer2.IO.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.KH =&amp;gt; ns1.dns.net.KH.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.KM =&amp;gt; mail1.comorestelecom.KM.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.MH =&amp;gt; imap.pwke.twtelecom.net.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.MQ =&amp;gt; mx1-mq.mediaserv.net.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.NE =&amp;gt; bow.rain.fr.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> =&amp;gt; bow.intnet.NE.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.PA =&amp;gt; ns.PA.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.TD =&amp;gt; mail.intnet.TD.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.TT =&amp;gt; 66-27-54-142.san.rr.com.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> =&amp;gt; 66-27-54-138.san.rr.com.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.UA =&amp;gt; mr.kolo.net.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.VA =&amp;gt; proxy2.urbe.it.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> =&amp;gt; john.vatican.VA.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> =&amp;gt; paul.vatican.VA.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> =&amp;gt; lists.vatican.VA.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.WS =&amp;gt; mail.worldsite.WS.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.TD =&amp;gt; mail.intnet.TD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.YE =&amp;gt; mail.yemen.net.YE.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>However, if I try to send an email to &lt;em>user@ai&lt;/em>, what does my mail server do? It will call &lt;em>getaddrinfo&lt;/em> to look up the MX record for &amp;ldquo;ai&amp;rdquo;. Consulting the previous resolv.conf, since there&amp;rsquo;s no dots, it&amp;rsquo;ll try to query &amp;ldquo;ai.us-west-2.technowizardry.net&amp;rdquo; or &amp;ldquo;ai.technowizardry.net&amp;rdquo;&amp;ndash; not what I expected.&lt;/p>
&lt;p>This kind of configuration is very common in corporate networks. They&amp;rsquo;ll specify a DNS search path of their corporate domain name so employees can type &amp;ldquo;www&amp;rdquo;. Users end up not being able to email this perfectly valid email account because their mail servers end up being misconfigured.&lt;/p>
&lt;p>The generally accepted practice in this case (mentioned in the &lt;a class="link" href="https://serverfault.com/a/155484" target="_blank" rel="noopener"
>Stack Overflow answer&lt;/a>) is to instead email &amp;ldquo;&lt;em>user@ai.&lt;/em>&amp;rdquo;. Unfortunately this email validator considers this to be invalid.&lt;/p>
&lt;p>Technically it should be legal to send email to both &amp;ldquo;&lt;em>user@example.com&lt;/em>&amp;rdquo; and &amp;ldquo;&lt;em>&lt;a class="link" href="mailto:user@example.com" >user@example.com&lt;/a>.&lt;/em>&amp;rdquo; However due to this subtle behavior we end up with non-compliant software configuration.&lt;/p>
&lt;p>Now, in 99.99999% of cases, this is probably a mistake by the user, but I&amp;rsquo;ve had issues when I was running my Postfix in Kubernetes (see next section) and it caused issues because it tried to resolve internal DNS records until I fixed it.&lt;/p>
&lt;h3 id="the-curse-of-kubernetes">The Curse of Kubernetes&lt;/h3>
&lt;p>In the previous example, we talked about an issue when ndots is set 1, but what if we dial it up to 5? That&amp;rsquo;s exactly the case in Kubernetes.&lt;/p>
&lt;p>By default, pods running in Kubernetes get a custom /etc/resolv.conf that looks like this:&lt;/p>
&lt;p>nameserver 10.43.0.10 # kube-dns instance in the cluster
search mail.svc.cluster.local svc.cluster.local cluster.local
options ndots:5&lt;/p>
&lt;p>This is done because Kubernetes wants you to be able to query for other pods in the same namespace using just the name like &amp;ldquo;&lt;em>dovecot&lt;/em>&amp;rdquo; -&amp;gt; &amp;ldquo;&lt;em>dovecot.mail.svc.cluster.local.&lt;/em>&amp;rdquo; or &amp;ldquo;&lt;em>mysql.datastore&lt;/em>&amp;rdquo; -&amp;gt; &amp;ldquo;&lt;em>mysql.datastore.svc.cluster.local&lt;/em>&amp;rdquo;.&lt;/p>
&lt;p>But what happens if you try to query for &amp;ldquo;&lt;a class="link" href="https://www.google.com" target="_blank" rel="noopener"
>www.google.com&lt;/a>&amp;rdquo;? There&amp;rsquo;s two dots there, you and I know it&amp;rsquo;s fully qualified, but unfortunately &lt;em>getaddrinfo&lt;/em> doesn&amp;rsquo;t. Instead it issues queries for:&lt;/p>
&lt;ol>
&lt;li>&lt;a class="link" href="https://www.google.com.mail.svc.cluster.local" target="_blank" rel="noopener"
>www.google.com.mail.svc.cluster.local&lt;/a>.&lt;/li>
&lt;li>&lt;a class="link" href="https://www.google.com.svc.cluster.local" target="_blank" rel="noopener"
>www.google.com.svc.cluster.local&lt;/a>.&lt;/li>
&lt;li>&lt;a class="link" href="https://www.google.com.cluster.local" target="_blank" rel="noopener"
>www.google.com.cluster.local&lt;/a>.&lt;/li>
&lt;li>&lt;a class="link" href="https://www.google.com" target="_blank" rel="noopener"
>www.google.com&lt;/a>.&lt;/li>
&lt;/ol>
&lt;p>Everything but the last query all result in NXDOMAINs (DNS response code for no record found.) This happens for every query you make in a Kubernetes container, except if you explicitly query for &amp;ldquo;&lt;a class="link" href="https://www.google.com" target="_blank" rel="noopener"
>www.google.com&lt;/a>.&amp;rdquo; with a trailing dot.&lt;/p>
&lt;p>All of this results in a significant amount of DNS traffic flying around my cluster, and we can clearly see the amount of NXDOMAIN responses for cluster.local domains in the following graph. This causes increased latencies for service operations inside the cluster:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-2.png" >&lt;img src="https://www.technowizardry.net/2022/05/domain-names-end-with-a-period-and-why-that-causes-problems/images/image-2-1024x449.png"
width="1024"
height="449"
srcset="https://www.technowizardry.net/2022/05/domain-names-end-with-a-period-and-why-that-causes-problems/images/image-2-1024x449_hu_33daa26a3045d060.png 480w, https://www.technowizardry.net/2022/05/domain-names-end-with-a-period-and-why-that-causes-problems/images/image-2-1024x449_hu_8f8027058bda3e55.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="228"
data-flex-basis="547px"
>&lt;/a>&lt;/p>
&lt;p>sum(increase(coredns_dns_responses_total{rcode=&amp;ldquo;NXDOMAIN&amp;rdquo;}[1h])) by (rcode, zone)&lt;/p>
&lt;p>Unfortunately, the search list is part of Kubernetes&amp;rsquo; service discovery process and can&amp;rsquo;t be changed across the cluster.&lt;/p>
&lt;h4 id="fixing-kubernetes">Fixing Kubernetes&lt;/h4>
&lt;p>The only way to avoid the increased DNS traffic in Kubernetes is to change the ndots configuration for each Pod:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Pod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dns-example&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nginx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dnsConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">options&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ndots&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Once I deployed this across a few key deployments, I saw a dramatic decrease in the number of DNS queries that my cluster sent.&lt;/p>
&lt;p>Before making this change, check your application to see if it is making any non fully qualified DNS queries, since those could break. Setting &lt;code>ndots&lt;/code> to 1 will ensure that same namespace queries like querying A for &lt;code>redis&lt;/code> would find &lt;code>redis.samenamespace.svc.cluster.local&lt;/code>. That is the only sane way of using DNS queries. If you need to do cross namespace queries, just go fully qualified and do &lt;code>redis.othernamespace.svc.cluster.local&lt;/code>.&lt;/p>
&lt;h2 id="the-curse-of-dns-servers">The Curse of DNS Servers&lt;/h2>
&lt;p>Trailing dots are important when creating records in a DNS zone too. In BIND and many DNS configuration UIs, when you create a CNAME, it looks something like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">google.technowizardry.net. 900 IN CNAME google.com.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">^ Domain Name ^TTL ^Type ^ Target
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>A CNAME is a type of DNS alias, when I query google.technowizardry.net, it should tell the client to go lookup google.com:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ dig google.technowizardry.net
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">;; ANSWER SECTION:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">google.technowizardry.net. 0 IN CNAME google.com
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>However, in some servers if I leave off the trailing dot in the CNAME, I actually end up seeing:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ dig google.technowizardry.net
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">;; ANSWER SECTION:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">google.technowizardry.net. 0 IN CNAME google.com.technowizardry.net
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Thus, trailing dot is again critical.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>In this post, I talked about the subtle assumption that everybody makes that domain names don&amp;rsquo;t need a trailing dot. While most users don&amp;rsquo;t have to type trailing dots normally because the DNS query library usually fixes the problem for you, it&amp;rsquo;s technically required and will cause some strange behavior if you&amp;rsquo;re not aware of it.&lt;/p>
&lt;h2 id="references">References&lt;/h2>
&lt;p>&lt;a class="link" href="https://github.com/kubernetes/kubernetes/issues/45976" target="_blank" rel="noopener"
>https://github.com/kubernetes/kubernetes/issues/45976&lt;/a>&lt;/p>
&lt;p>&lt;a class="link" href="https://pracucci.com/kubernetes-dns-resolution-ndots-options-and-why-it-may-affect-application-performances.html" target="_blank" rel="noopener"
>https://pracucci.com/kubernetes-dns-resolution-ndots-options-and-why-it-may-affect-application-performances.html&lt;/a>&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F05%2Fdomain-names-end-with-a-period-and-why-that-causes-problems%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Domain+names+actually+end+with+a+period+and+why+that+might+subtly+break+your+system" style="border:0" alt="" /></description></item><item><title>Accurate, Local Home Energy Monitoring: Part 2 - Network Config</title><link>https://www.technowizardry.net/2022/05/accurate-local-home-energy-monitoring-part-2-network-config/</link><pubDate>Sun, 01 May 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/05/accurate-local-home-energy-monitoring-part-2-network-config/</guid><summary>&lt;p>This post continues from the previous post in the series where I walked through the decision process on what energy monitor system to use and how to install Brultech GEM Monitor. I ended with the hardware physically installed and all Current Transformers (CTs) connected.&lt;/p>
&lt;p>In this post, I continue from that point and walk through the network and software configuration defining each circuit size.&lt;/p>
&lt;h1 id="network-configuration">Network Configuration&lt;/h1>
&lt;p>First, connect the device to the network (I&amp;rsquo;m using Ethernet) and ensure it&amp;rsquo;s turned on. Then discover the IP address of the monitor. I found it by logging in to the router&amp;rsquo;s config web page and checking for the DHCP lease from the device. Brultech has a few different tools that help you discover the device, but they didn&amp;rsquo;t really work for me and looked like they were written a decade ago.&lt;/p></summary><description>&lt;p>This post continues from the previous post in the series where I walked through the decision process on what energy monitor system to use and how to install Brultech GEM Monitor. I ended with the hardware physically installed and all Current Transformers (CTs) connected.&lt;/p>
&lt;p>In this post, I continue from that point and walk through the network and software configuration defining each circuit size.&lt;/p>
&lt;h1 id="network-configuration">Network Configuration&lt;/h1>
&lt;p>First, connect the device to the network (I&amp;rsquo;m using Ethernet) and ensure it&amp;rsquo;s turned on. Then discover the IP address of the monitor. I found it by logging in to the router&amp;rsquo;s config web page and checking for the DHCP lease from the device. Brultech has a few different tools that help you discover the device, but they didn&amp;rsquo;t really work for me and looked like they were written a decade ago.&lt;/p>
&lt;p>To configure how to control the device, I port scanned it and found it listens on several different ports:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">PORT STATE SERVICE VERSION
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">23/tcp open telnet?
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">80/tcp open http
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1025/tcp open NFS-or-IIS?
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">8000/tcp open http-alt GEM ver1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Port 80 and port 8000 are both HTTP services that control different aspects of the monitor.&lt;/p>
&lt;p>The default credentials for port 80 are username:&lt;strong>admin&lt;/strong> and password:&lt;strong>admin&lt;/strong>.&lt;/p>
&lt;h1 id="disabling-wi-fi">Disabling Wi-Fi&lt;/h1>
&lt;p>This GEM monitor includes both Wi-Fi and Ethernet connections. By default, it broadcasts a Wi-Fi network (SSID: GreenEye_XYZ) that you can connect to, however if you&amp;rsquo;re connecting via Ethernet, an extra Wi-Fi network just adds noise to the RF channel. Removing the antenna doesn&amp;rsquo;t fully stop it. Luckily in COM firmware version v4.17+, you can explicitly disable it (&lt;a class="link" href="https://www.brultech.com/community/viewtopic.php?t=1557" target="_blank" rel="noopener"
>reference&lt;/a>.) To disable it, navigate to:&lt;/p>
&lt;p>http://{ipaddress}:8000/&lt;/p>
&lt;p>and click the &amp;ldquo;Adv&amp;rdquo; tab at the top., then scroll down to near the bottom where it has a button for &amp;ldquo;AP off&amp;rdquo;&lt;/p>
&lt;p>&lt;a class="link" href="images/Gem-Disable-WiFi.jpg" >&lt;img src="https://www.technowizardry.net/2022/05/accurate-local-home-energy-monitoring-part-2-network-config/images/Gem-Disable-WiFi.jpg"
width="1024"
height="707"
srcset="https://www.technowizardry.net/2022/05/accurate-local-home-energy-monitoring-part-2-network-config/images/Gem-Disable-WiFi_hu_2f604d9bbdd253ec.jpg 480w, https://www.technowizardry.net/2022/05/accurate-local-home-energy-monitoring-part-2-network-config/images/Gem-Disable-WiFi_hu_5d88d72b2a33150d.jpg 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="144"
data-flex-basis="347px"
>&lt;/a>&lt;/p>
&lt;p>The GEM monitor should reboot and the Wi-Fi network will be gone. The first time I did this, the monitor failed to come back (possibly because I modified Wi-Fi settings on the port 80 before trying to disable it.) If that happens, reset it by holding the network reset button for 5 seconds and going directly to this page and disabling the AP.&lt;/p>
&lt;h1 id="configuring-the-cts">Configuring the CTs&lt;/h1>
&lt;p>Since CTs can come in different sizes (30amps all the way to 200 amps,) the GEM device needs to know exactly what CT is installed on each channel. Otherwise it won&amp;rsquo;t know how to translate the signals it receives into the correct current measurement.&lt;/p>
&lt;p>To assign the channels, download the GEM Network Utility from &lt;a class="link" href="https://www.brultech.com/software/files/getsoft/1/1" target="_blank" rel="noopener"
>BrulTech download page&lt;/a>. Switch to TCP Client mode, set Port: 8000, and enter the IP address of the device, then click Open on the right side.&lt;/p>
&lt;p>&lt;a class="link" href="images/NetworkUtility-Connected.jpg" >&lt;img src="https://www.technowizardry.net/2022/05/accurate-local-home-energy-monitoring-part-2-network-config/images/NetworkUtility-Connected.jpg"
width="1013"
height="800"
srcset="https://www.technowizardry.net/2022/05/accurate-local-home-energy-monitoring-part-2-network-config/images/NetworkUtility-Connected_hu_c722572100b36a8.jpg 480w, https://www.technowizardry.net/2022/05/accurate-local-home-energy-monitoring-part-2-network-config/images/NetworkUtility-Connected_hu_2271ab6af80ff3fb.jpg 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="126"
data-flex-basis="303px"
>&lt;/a>&lt;/p>
&lt;p>If it succeeds, it should show Status: Connected. In the menu bar, click CT and PT Settings. In the window that opens, configure the CT types for each channel then click Save.&lt;/p>
&lt;p>&lt;a class="link" href="images/CTSettingsPage-Web.png" >&lt;img src="https://www.technowizardry.net/2022/05/accurate-local-home-energy-monitoring-part-2-network-config/images/CTSettingsPage-Web-1024x597.png"
width="1024"
height="597"
srcset="https://www.technowizardry.net/2022/05/accurate-local-home-energy-monitoring-part-2-network-config/images/CTSettingsPage-Web-1024x597_hu_f12ab4a325fa805.png 480w, https://www.technowizardry.net/2022/05/accurate-local-home-energy-monitoring-part-2-network-config/images/CTSettingsPage-Web-1024x597_hu_36c51822fb301965.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="171"
data-flex-basis="411px"
>&lt;/a>&lt;/p>
&lt;p>After that, check out the Live Data tab to verify everything looks right. Below, we tested turning on several heaters and verified that the values of each channel sum up approximately to the total in channel 1. Before changing the models in the previous steps, the channels were not adding up. After the change, I saw that the individual values didn&amp;rsquo;t sum up to be exactly equal to the total in channel 1, but it was pretty close and I imagine there&amp;rsquo;s some measurement tolerances because each channel is being measured independently.&lt;/p>
&lt;p>&lt;a class="link" href="images/GEM-AllCircuitsRunning.jpg" >&lt;img src="https://www.technowizardry.net/2022/05/accurate-local-home-energy-monitoring-part-2-network-config/images/GEM-AllCircuitsRunning.jpg"
width="1013"
height="800"
srcset="https://www.technowizardry.net/2022/05/accurate-local-home-energy-monitoring-part-2-network-config/images/GEM-AllCircuitsRunning_hu_5b2c64d71800d6ce.jpg 480w, https://www.technowizardry.net/2022/05/accurate-local-home-energy-monitoring-part-2-network-config/images/GEM-AllCircuitsRunning_hu_f25ab1969bf6abc8.jpg 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="126"
data-flex-basis="303px"
>&lt;/a>&lt;/p>
&lt;p>GEM Utility showing lots of energy usage from several different devices confirming that it works.&lt;/p>
&lt;h1 id="next-steps">Next Steps&lt;/h1>
&lt;p>Now everything should be measured correctly. The next step will be to connect it to HomeAssistant. This will come in the next blog post.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F05%2Faccurate-local-home-energy-monitoring-part-2-network-config%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Accurate%2C+Local+Home+Energy+Monitoring%3A+Part+2+-+Network+Config" style="border:0" alt="" /></description></item><item><title>Kubernetes: A hybrid Calico and Layer 2 Bridge+DHCP network using Multus</title><link>https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/</link><pubDate>Wed, 27 Apr 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/</guid><summary>&lt;p>Previously in my &lt;a class="link" href="https://www.technowizardry.net/series/home-lab/" >Home Lab series&lt;/a>, I described how my home lab Kubernetes clusters runs with a DHCP CNI&amp;ndash;all pods get an IP address on the same layer 2 network as the rest of my home and an IP from DHCP. This enabled me to run certain software that needed this like Home Assistant which wanted to be able to do mDNS and send broadcast packets to discover device.&lt;/p>
&lt;p>However, not all pods actually needed to be on the same layer 2 network and lead to a few situations where I ran out of IP addresses on the DHCP server and couldn&amp;rsquo;t connect any new devices until reservations expired:&lt;/p></summary><description>&lt;p>Previously in my &lt;a class="link" href="https://www.technowizardry.net/series/home-lab/" >Home Lab series&lt;/a>, I described how my home lab Kubernetes clusters runs with a DHCP CNI&amp;ndash;all pods get an IP address on the same layer 2 network as the rest of my home and an IP from DHCP. This enabled me to run certain software that needed this like Home Assistant which wanted to be able to do mDNS and send broadcast packets to discover device.&lt;/p>
&lt;p>However, not all pods actually needed to be on the same layer 2 network and lead to a few situations where I ran out of IP addresses on the DHCP server and couldn&amp;rsquo;t connect any new devices until reservations expired:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-1.png" >&lt;img src="https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/image-1.png"
width="976"
height="401"
srcset="https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/image-1_hu_dec050caec2b1b1.png 480w, https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/image-1_hu_cd32e33ba538d744.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="243"
data-flex-basis="584px"
>&lt;/a>&lt;/p>
&lt;p>My DHCP IP pool completely out of addresses to give to clients&lt;/p>
&lt;p>I also had a circular dependency where the main VLAN told clients to use a DNS server that was running in Kubernetes. If I had to reboot the cluster, my Kubernetes cluster could get stuck starting because it tried to query a DNS server that wasn&amp;rsquo;t started yet (For simplicity, I use DHCP for everything instead of static config).&lt;/p>
&lt;p>In this post, I explain how I built a new home lab cluster with K3s and used Multus to run both Calico and my custom Bridge+DHCP CNI so that only pods that need layer 2 access get access.&lt;/p>
&lt;p>I wanted to move the K8s pods into a separate IP pool and VLAN so I could reduce the blast radius of something going wrong.&lt;/p>
&lt;p>If you&amp;rsquo;re trying to start a K3s cluster (like I am) from scratch, then you may run into issues where the K3s cluster that Rancher provisions won&amp;rsquo;t start without a working CNI. If this happens, check out my other post on how to &lt;a class="link" href="https://www.technowizardry.net/2022/04/how-to-gain-access-to-a-rke2-cluster-when-rancher-cant/" >install the CNI&lt;/a>.&lt;/p>
&lt;h2 id="network-configuration">Network Configuration&lt;/h2>
&lt;p>I created a new VLAN (ID 20) and trunked this VLAN to my router and all switches and configured the router as a DHCP server and enabled it to route traffic between VLANs and the internet.&lt;/p>
&lt;p>&lt;a class="link" href="images/Multus-NetworkConfiguration-1.png" >&lt;img src="https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/Multus-NetworkConfiguration-1.png"
width="597"
height="558"
srcset="https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/Multus-NetworkConfiguration-1_hu_490672df60543951.png 480w, https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/Multus-NetworkConfiguration-1_hu_3c5e1e5dace69f33.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="106"
data-flex-basis="256px"
>&lt;/a>&lt;/p>
&lt;p>I tried trunking the VLAN 20 (and left the default VLAN as untagged) to both computers, however I ran in to an issue where the &lt;a class="link" href="https://www.microsoft.com/en-us/d/surface-dock-2/8qd908364sg2?activetab=pivot:overviewtab" target="_blank" rel="noopener"
>Surface Dock 2 Ethernet adapter&lt;/a> wouldn&amp;rsquo;t work because it couldn&amp;rsquo;t receive ARP packets from certain devices on the network on the VLAN tagged adapter. This didn&amp;rsquo;t make any sense because it was able to get an IP address from DHCP.&lt;/p>
&lt;p>The router wasn&amp;rsquo;t able to send ARP responses/queries to the VM, but other machines on the network were able to. Logically the only difference I saw was the packet lengths, packets were always less than 64 bytes, but Ethernet is supposed to pad all bytes to a minimum of 64 bytes. This didn&amp;rsquo;t make any sense, so instead I bought a USB-Ethernet adapter for this computer and used that for my secondary network.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-5.png" >&lt;img src="https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/image-5-1024x765.png"
width="1024"
height="765"
srcset="https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/image-5-1024x765_hu_bfef339951e18da4.png 480w, https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/image-5-1024x765_hu_f78b89418e8dd95e.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="133"
data-flex-basis="321px"
>&lt;/a>&lt;/p>
&lt;p>A packet capture from a tap on the Ethernet cable showing packets, but the responses never made it to the IP stack in the VM&lt;/p>
&lt;h2 id="vm-network-adapter-configuration">VM Network Adapter Configuration&lt;/h2>
&lt;p>I&amp;rsquo;m running my Kubernetes nodes as a Hyper-V VM on two of my Windows computers.&lt;/p>
&lt;p>I previously created a virtual switch (See &lt;a class="link" href="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/" >part #1&lt;/a> if you&amp;rsquo;re interested in the step by step) for Kubernetes that references my Ethernet adapter:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-4.png" >&lt;img src="https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/image-4.png"
width="901"
height="858"
srcset="https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/image-4_hu_6b4dadc99f4c2a6f.png 480w, https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/image-4_hu_e6a615bbee1234be.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="105"
data-flex-basis="252px"
>&lt;/a>&lt;/p>
&lt;p>Then I created a virtual machine that includes two network adapters, both of them bound to that same switch, but one of them included the VLAN Id 20&lt;/p>
&lt;p>&lt;a class="link" href="images/image-3.png" >&lt;img src="https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/image-3.png"
width="901"
height="858"
srcset="https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/image-3_hu_42d3c12795138c88.png 480w, https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/image-3_hu_ba8cbaa63194436d.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="105"
data-flex-basis="252px"
>&lt;/a>&lt;/p>
&lt;h2 id="cluster-configuration">Cluster Configuration&lt;/h2>
&lt;p>I&amp;rsquo;m using Rancher&amp;rsquo;s UI to provision a k3s cluster. By default K3s uses &lt;a class="link" href="https://github.com/flannel-io/flannel" target="_blank" rel="noopener"
>Flannel&lt;/a> for it&amp;rsquo;s networking CNI, but I want Multus so I can use Calico. While creating the cluster, set the agent Env variable, &lt;code>INSTALL_K3S_EXEC&lt;/code> to be &lt;code>--flannel-backend=none --disable-network-policy&lt;/code> to deploy without the default Flannel cluster, then follow &lt;a class="link" href="https://www.technowizardry.net/2022/04/how-to-gain-access-to-a-rke2-cluster-when-rancher-cant" >this guide&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">provisioning.cattle.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Cluster&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">example&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fleet-default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">agentEnvVars&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">INSTALL_K3S_EXEC&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;--flannel-backend=none --disable-network-policy&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="os-network-configuration">OS Network Configuration&lt;/h2>
&lt;p>Once Linux is booted in the VM, we can setup the network adapters inside the VM. For more details, see my post on &lt;a class="link" href="https://www.technowizardry.net/2021/12/home-lab-using-the-bridge-cni-with-systemd/" >Bridge + Systemd&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ ls /etc/systemd/network
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">10-netplan-eth0.network 10-netplan-eth1.network cni0.netdev cni0.network
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ cat /etc/systemd/network/10-netplan-eth0.network
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Match]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Name=eth0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Network]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Bridge=cni0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ cat /etc/systemd/network/cni0.netdev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[NetDev]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Name=cni0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Kind=bridge
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">MACAddress=00:15:5d:02:cb:09 # Same as eth0 MAC address
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ cat /etc/systemd/network/cni0.network
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Match]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Name=cni0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Network]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">DHCP=yes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IPv6AcceptRA=yes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The eth1 adapter should be bound to the VLAN 20 network adapter&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ cat /etc/systemd/network/10-netplan-eth1.network
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Match]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Name=eth1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Link]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">MTUBytes=9000 # Optional: Configure Jumbo Frames. All devices on this VLAN need to be configured with this. Since this is limited to just my K8s nodes, this is possible
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Network]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">DHCP=yes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># Without this we end up with two different default routes. For security, I want to prefer all traffic to pass over the separate VLAN. This VLAN will bypass certain restrictions that I have configured on my main VLAN like forcing all DNS traffic to go through my pi-hole
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Route]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Destination=0.0.0.0/0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Gateway=192.168.3.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Metric=90
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now I end up with the following route table:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ ip route
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">default via 192.168.3.1 dev eth1 proto static metric 90 onlink
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">default via 192.168.3.1 dev eth1 proto dhcp src 192.168.3.2 metric 1024
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">default via 192.168.2.1 dev cni0 proto dhcp src 192.168.2.151 metric 1024
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.2.0/24 dev cni0 proto kernel scope link src 192.168.2.151
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.2.1 dev cni0 proto dhcp scope link src 192.168.2.151 metric 1024
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.3.0/24 dev eth1 proto kernel scope link src 192.168.3.2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.3.1 dev eth1 proto dhcp scope link src 192.168.3.2 metric 1024
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="intro-to-multus">Intro to Multus&lt;/h2>
&lt;p>&lt;a class="link" href="https://github.com/k8snetworkplumbingwg/multus-cni" target="_blank" rel="noopener"
>Multus&lt;/a> is a special CNI that enables you to configure one or more network interfaces on a pod&amp;rsquo;s network namespace. Each pod always gets the default cluster network/master plugin interface. Then pods can add a special annotation to get more network adapters.&lt;/p>
&lt;p>I&amp;rsquo;m going to use Calico as my cluster network plugin because it supports BGP and is what I was already using based on &lt;a class="link" href="https://www.technowizardry.net/2021/10/home-lab-part-2-networking-setup/" >previous posts&lt;/a> in my series.&lt;/p>
&lt;p>&lt;a class="link" href="images/multus-pod-image.svg" >&lt;img src="https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/multus-pod-image.svg"
loading="lazy"
>&lt;/a>&lt;/p>
&lt;p>&lt;a class="link" href="https://www.technowizardry.net/2021/10/home-lab-dhcp-ipam/" >My DHCP CNI&lt;/a> will be the optional secondary network attachment.&lt;/p>
&lt;h2 id="installing-multus">Installing Multus&lt;/h2>
&lt;p>Unfortunately, Multus doesn&amp;rsquo;t currently provide any Helm templates. Instead they only provide a YAML file that needs to be modified because it can be used.&lt;/p>
&lt;p>First, download the &lt;a class="link" href="https://github.com/k8snetworkplumbingwg/multus-cni/blob/master/deployments/multus-daemonset.yml" target="_blank" rel="noopener"
>multus-daemonset.yml&lt;/a> from their GitHub repository and save it.&lt;/p>
&lt;p>Find the ConfigMap that defines &lt;em>multus-cni-config&lt;/em>. This is what defines the primary network plugin.&lt;/p>
&lt;p>Since I&amp;rsquo;m using Calico, I used the following ConfigMap:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">multus-cni-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tier&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">multus&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cni-conf.json&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;name&amp;#34;: &amp;#34;multus-cni-network&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;type&amp;#34;: &amp;#34;multus&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;cniVersion&amp;#34;: &amp;#34;0.3.1&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;capabilities&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;portMappings&amp;#34;: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;delegates&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;name&amp;#34;: &amp;#34;calico-network&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;cniVersion&amp;#34;: &amp;#34;0.3.1&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;plugins&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;type&amp;#34;: &amp;#34;calico&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;datastore_type&amp;#34;: &amp;#34;kubernetes&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;mtu&amp;#34;: 0,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;nodename_file_optional&amp;#34;: false,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;log_level&amp;#34;: &amp;#34;Info&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;log_file_path&amp;#34;: &amp;#34;/var/log/calico/cni/cni.log&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;ipam&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;type&amp;#34;: &amp;#34;calico-ipam&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;assign_ipv4&amp;#34;: &amp;#34;true&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;assign_ipv6&amp;#34;: &amp;#34;false&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;container_settings&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;allow_ip_forwarding&amp;#34;: false
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;policy&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;type&amp;#34;: &amp;#34;k8s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;kubernetes&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;k8s_api_root&amp;#34;: &amp;#34;https://10.43.0.1:443&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;kubeconfig&amp;#34;: &amp;#34;/etc/cni/net.d/calico-kubeconfig&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;type&amp;#34;: &amp;#34;bandwidth&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;capabilities&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;bandwidth&amp;#34;: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;type&amp;#34;: &amp;#34;portmap&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;snat&amp;#34;: true,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;capabilities&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;portMappings&amp;#34;: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;kubeconfig&amp;#34;: &amp;#34;/etc/cni/net.d/multus.d/multus.kubeconfig&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then find the &lt;em>kube-multus-ds&lt;/em> DaemonSet and change both the args and the volumes section to look like below. This forces Multus to run before Calico&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl"> containers:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - name: kube-multus
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> image: ghcr.io/k8snetworkplumbingwg/multus-cni:v3.9.3
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> command: [&amp;#34;/entrypoint.sh&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> args:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">- - --multus-conf-file=auto&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+ - --multus-conf-file=/tmp/multus-conf/0-multus.conf
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span> - &amp;#34;--cni-version=0.3.1&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">----
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span> volumes:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - configMap:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> defaultMode: 420
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> items:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - key: cni-conf.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">- path: 70-multus.conf
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+ path: 0-multus.conf
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span> name: multus-cni-config
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="install-calico">Install Calico&lt;/h2>
&lt;p>Now you can install Calico as you normally would. I&amp;rsquo;ve already got a guide on how I configure Calico in my network &lt;a class="link" href="https://www.technowizardry.net/2021/10/home-lab-part-2-networking-setup" >here&lt;/a>.&lt;/p>
&lt;p>When installing using the Tigera Operator, make sure to configure the nodeAddressAutodetectionV4/V6 settings to use the VLAN 20 (in my case eth1.)&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">operator.tigera.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Installation&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">calicoNetwork&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bgp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Enabled&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ipPools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">blockSize&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">26&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cidr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">192.168.7.0&lt;/span>&lt;span class="l">/24&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">encapsulation&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">None&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">natOutgoing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Disabled&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">linuxDataplane&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Iptables&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nodeAddressAutodetectionV4&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">canReach&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">eth1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cni&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ipam&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Calico&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Calico&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">controlPlaneNodeSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">node-role.kubernetes.io/master&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nodeUpdateStrategy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rollingUpdate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxUnavailable&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RollingUpdate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nonPrivileged&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Disabled&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">variant&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Calico&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After installing Calico, the cluster should start up correctly and you should be able to launch pods with at least internet connectivity. Next, we need to configure the layer 2 network CNI.&lt;/p>
&lt;h2 id="install-the-layer-2-bridge-cni">Install the Layer 2 Bridge CNI&lt;/h2>
&lt;p>Now install the DHCP CNI and DaemonSet that I&amp;rsquo;ve been working on in previous posts (&lt;a class="link" href="https://www.technowizardry.net/2021/10/home-lab-dhcp-ipam/" >see here&lt;/a>):&lt;/p>
&lt;p>&lt;a class="link" href="https://github.com/ajacques/cni-plugins/blob/bridge/plugins/ipam/dhcp/k8s.yaml" target="_blank" rel="noopener"
>github.com/ajacques/cni-plugins/&amp;hellip;/dhcp/k8s.yaml&lt;/a>:&lt;/p>
&lt;p>&lt;code>kubectl apply -f https://raw.githubusercontent.com/ajacques/cni-plugins/bridge/plugins/ipam/dhcp/k8s.yaml&lt;/code>&lt;/p>
&lt;p>Then create a Multus NetworkAttachment:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">k8s.cni.cncf.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NetworkAttachmentDefinition&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">layer2-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;cniVersion&amp;#34;: &amp;#34;0.4.0&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;name&amp;#34;: &amp;#34;dhcp-cni-network&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;plugins&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;type&amp;#34;: &amp;#34;bridge&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;name&amp;#34;: &amp;#34;mybridge&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;bridge&amp;#34;: &amp;#34;cni0&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;isDefaultGateway&amp;#34;: false,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;uplinkInterface&amp;#34;: &amp;#34;eth0&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;enableIPv6&amp;#34;: true,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;ipam&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;type&amp;#34;: &amp;#34;dhcp&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;provide&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> { &amp;#34;option&amp;#34;: &amp;#34;12&amp;#34;, &amp;#34;fromArg&amp;#34;: &amp;#34;K8S_POD_NAME&amp;#34; }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="updating-the-deployment">Updating the Deployment&lt;/h2>
&lt;p>Configuring the deployment to use dual network adapters is easy, add the annotation to the pod annotations not the deployment annotations:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">homeassistant&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">smarthome&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">k8s.v1.cni.cncf.io/networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default/layer2-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="solving-network-routing-problems">Solving Network Routing Problems&lt;/h2>
&lt;p>As I encountered previously in the series (in &lt;a class="link" href="https://www.technowizardry.net/2021/11/home-lab-part-6-problems-with-asymmetrical-routing/" >Part 5&lt;/a>) the containers have an entirely separate route table&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">sudo ip netns exec {containerns} ip route
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">default via 169.254.1.1 dev eth0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">169.254.1.1 dev eth0 scope link
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.2.151 dev net1 scope link
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>When HomeAssistant tries to send a Wake On Lan packet to turn on my TV at IP address 192.168.2.xy it needs to send a packet to 192.168.2.255. But now our traffic isn&amp;rsquo;t making it directly out onto the layer 2 network. It matches the default route and goes through the host which is prevents broadcast packets from being broadcast since it&amp;rsquo;s considered an layer 3 hop and multicast.&lt;/p>
&lt;p>We need to tell the container that it can send traffic destined for the main LAN towards to cni0/eth0/VLAN 1 network adapter.&lt;/p>
&lt;p>I tried creating a custom route by using the &lt;a class="link" href="https://github.com/redhat-nfvpe/cni-route-override" target="_blank" rel="noopener"
>redhat-nfvpe/cni-route-override plugin&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">k8s.cni.cncf.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NetworkAttachmentDefinition&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">layer2-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;cniVersion&amp;#34;: &amp;#34;0.4.0&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;name&amp;#34;: &amp;#34;dhcp-cni-network&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;plugins&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;type&amp;#34;: &amp;#34;bridge&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;name&amp;#34;: &amp;#34;mybridge&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;bridge&amp;#34;: &amp;#34;cni0&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;isDefaultGateway&amp;#34;: false,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;uplinkInterface&amp;#34;: &amp;#34;eth0&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;enableIPv6&amp;#34;: true,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;ipam&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;type&amp;#34;: &amp;#34;dhcp&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;provide&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> { &amp;#34;option&amp;#34;: &amp;#34;12&amp;#34;, &amp;#34;fromArg&amp;#34;: &amp;#34;K8S_POD_NAME&amp;#34; }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;type&amp;#34;: &amp;#34;route-override&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;addroutes&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> { &amp;#34;dst&amp;#34;: &amp;#34;192.168.2.0/24&amp;#34; }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This allows the container to send traffic through cni0 onto the correct VLAN, but with the wrong source IP and it sends it as 192.168.7.xy (The Calico K8s Pod subnet.) The container route table looks like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ sudo ip netns exec {containerns} ip route
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">default via 169.254.1.1 dev eth0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">169.254.1.1 dev eth0 scope link
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.2.158 dev net1 scope link
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.2.0/24 dev net1 scope link
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The route is missing a &lt;em>src 192.168.2.xyz&lt;/em> to tell the Linux IP stack to use the right source IP address.&lt;/p>
&lt;p>I see the same problem with mDNS traffic that HomeAssistant uses to discover devices on the local network. The following are DEBUG logs showing it&amp;rsquo;s creating a socket to 239.255.255.250, the multicast IP address for mDNS.&lt;/p>
&lt;p>&lt;code>2022-04-17 00:19:51 DEBUG (MainThread) [async_upnp_client.ssdp] Creating socket, source: (&amp;lt;AddressFamily.AF_INET: 2&amp;gt;, &amp;lt;SocketKind.SOCK_DGRAM: 2&amp;gt;, 17, '192.168.7.253', ('192.168.7.253', 0)), target: (&amp;lt;AddressFamily.AF_INET: 2&amp;gt;, &amp;lt;SocketKind.SOCK_DGRAM: 2&amp;gt;, 17, '239.255.255.250', ('239.255.255.250', 1900))&lt;/code>&lt;/p>
&lt;p>Unfortunately, the route-override CNI plugin doesn&amp;rsquo;t allow us to define the source field on a defined route, so we have to define our own CNI plugin.&lt;/p>
&lt;p>To figure out how to create the right rule, we need the subnet of the loocal network and the network adapter inside the container. Kubernetes stores all of the outputs from the CNI plugins in /var/lib/cni. If we inspect the output file, we can that this information will get passed into a custom CNI:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// cat /var/lib/cni/multus/results/dhcp-cni-network-018684df7309507adb027bbd2b1cec1c06be49d9f1b1ab51601fdb798ce85b7f-net1 | jq
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nt">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cniVersion&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.4.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;dns&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;interfaces&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mac&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;00:15:5d:02:cb:09&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;cni0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mac&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;3a:34:17:72:61:28&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;veth4fd7d3d5&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mac&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;56:c6:ce:2b:6e:f7&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;net1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;sandbox&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/var/run/netns/cni-090e5f88-9a95-cb98-6cd0-1b8b74ebe32f&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ips&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;address&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;192.168.2.158/24&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;gateway&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;192.168.2.1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;interface&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;4&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;routes&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;dst&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.0.0.0/0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;gw&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;192.168.2.1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;dst&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;192.168.2.0/24&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The full code for the CNI is &lt;a class="link" href="https://github.com/ajacques/cni-plugins/blob/bridge/plugins/meta/route-fix/main.go" target="_blank" rel="noopener"
>here&lt;/a>. A break down:&lt;/p>
&lt;p>First, we grab a reference to the network namespace for the container (CNI passes this in directly.) linkName will be the name of the container once we&amp;rsquo;re inside the container and containerNet is 192.168.2.158/24.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Load Configuration (See GitHub for code)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">netns&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">ns&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">GetNS&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">args&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Netns&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">defer&lt;/span> &lt;span class="nx">netns&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">linkName&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">prevResult&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Interfaces&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">Name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">containerNet&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">prevResult&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">IPs&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">Address&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then swap inside the container network namespace and get a reference to adapter:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netns&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Do&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">_&lt;/span> &lt;span class="nx">ns&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">NetNS&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">containerLink&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">LinkByName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">linkName&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Next, we need to convert the IP address 192.168.2.158/24 to 192.168.2.0/24 since Linux prohibits the former to be used as part of a route and then add it as a route.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 192.168.2.0/24 dev net1 scope link src 192.168.2.158&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">route&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Route&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">LinkIndex&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">containerLink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Attrs&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">Index&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Scope&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SCOPE_LINK&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Src&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">containerNet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">IP&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Dst&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">net&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">IPNet&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">IP&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">containerNet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">IP&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Mask&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">containerNet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Mask&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Mask&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">containerNet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Mask&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RouteAdd&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">route&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then create a similar route for multicast traffic:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">net&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ParseCIDR&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;224.0.0.0/4&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">mcastroute&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Route&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">LinkIndex&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">containerLink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Attrs&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">Index&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Scope&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SCOPE_LINK&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Src&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">containerNet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">IP&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Dst&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RouteAdd&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">mcastroute&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This is all taken care of if you use the Docker Image I wrote and update the network attachment:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">k8s.cni.cncf.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NetworkAttachmentDefinition&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">layer2-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;cniVersion&amp;#34;: &amp;#34;0.4.0&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;name&amp;#34;: &amp;#34;dhcp-cni-network&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;plugins&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;type&amp;#34;: &amp;#34;bridge&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;name&amp;#34;: &amp;#34;mybridge&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;bridge&amp;#34;: &amp;#34;cni0&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;isDefaultGateway&amp;#34;: false,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;uplinkInterface&amp;#34;: &amp;#34;eth0&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;enableIPv6&amp;#34;: true,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;ipam&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;type&amp;#34;: &amp;#34;dhcp&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;provide&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> { &amp;#34;option&amp;#34;: &amp;#34;12&amp;#34;, &amp;#34;fromArg&amp;#34;: &amp;#34;K8S_POD_NAME&amp;#34; }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;type&amp;#34;: &amp;#34;route-fix&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now, if we redeploy HomeAssistant it successfully discovers devices on my LAN!&lt;/p>
&lt;p>&lt;a class="link" href="images/image.png" >&lt;img src="https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/image.png"
width="354"
height="229"
srcset="https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/image_hu_844cc09d140b6bf8.png 480w, https://www.technowizardry.net/2022/04/multus-with-calico-and-layer2-bridge/images/image_hu_25936b8b57ed8ee8.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="154"
data-flex-basis="371px"
>&lt;/a>&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>In this post, I pulled together several different techniques I applied in previous posts in this series showing how to use Multus to run both Calico and Bridge+DHCP CNIs at the same time. Calico enables us to isolate traffic to a separate VLAN and avoid consuming all the IP addresses in the LAN and Multus with the bridge CNI ensures that software like HomeAssistant, my Sonos control software, and software that uses mDNS can continue to discover devices like they should.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F04%2Fmultus-with-calico-and-layer2-bridge%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Kubernetes%3A+A+hybrid+Calico+and+Layer+2+Bridge%2BDHCP+network+using+Multus" style="border:0" alt="" /></description></item><item><title>How to gain access to a RKE2 cluster without Rancher when the CNI doesn't work</title><link>https://www.technowizardry.net/2022/04/how-to-gain-access-to-a-rke2-cluster-when-rancher-cant/</link><pubDate>Mon, 11 Apr 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/04/how-to-gain-access-to-a-rke2-cluster-when-rancher-cant/</guid><summary>&lt;p>In &lt;a class="link" href="https://www.technowizardry.net/2021/12/rancher-is-painful-to-use/" >my previous post&lt;/a> where I outlined challenges that I&amp;rsquo;ve encountered with Rancher. As part of the feedback to that I ended up having to rebuild one of my clusters. I took that time to try out RKE2 and K3s for my home lab. In this home lab, I use a custom CNI based on the official Bridge and DHCP IPAM CNIs (&lt;a class="link" href="https://www.technowizardry.net/2021/10/home-lab-dhcp-ipam/" >Read more&lt;/a>) to enable my smart home software (HomeAssistant) to communicate with other devices on the same Layer 2 domain.&lt;/p></summary><description>&lt;p>In &lt;a class="link" href="https://www.technowizardry.net/2021/12/rancher-is-painful-to-use/" >my previous post&lt;/a> where I outlined challenges that I&amp;rsquo;ve encountered with Rancher. As part of the feedback to that I ended up having to rebuild one of my clusters. I took that time to try out RKE2 and K3s for my home lab. In this home lab, I use a custom CNI based on the official Bridge and DHCP IPAM CNIs (&lt;a class="link" href="https://www.technowizardry.net/2021/10/home-lab-dhcp-ipam/" >Read more&lt;/a>) to enable my smart home software (HomeAssistant) to communicate with other devices on the same Layer 2 domain.&lt;/p>
&lt;p>However, it seems that if you try to spin up a RKE2 cluster on a host with a Bridge interface setup (&lt;a class="link" href="https://www.technowizardry.net/2021/11/home-lab-replacing-macvlan-with-a-bridge/" >See here&lt;/a>) then it&amp;rsquo;ll get stuck during provisioning and you won&amp;rsquo;t be able to download a Kube Config from Rancher Server because Rancher thinks it&amp;rsquo;s offline. I reported &lt;a class="link" href="https://github.com/rancher/rancher/issues/35678" target="_blank" rel="noopener"
>this issue initially here&lt;/a>.&lt;/p>
&lt;p>In this blog post, I explain more about the problem and how to directly connect to the cluster to install a working CNI such that Rancher will correctly start.&lt;/p>
&lt;h2 id="problem-continued">Problem Continued&lt;/h2>
&lt;p>In this cluster, I setup a single Ubuntu Server node that has a bridge interface configured exactly as I&amp;rsquo;ve done before (&lt;a class="link" href="https://www.technowizardry.net/2021/11/home-lab-replacing-macvlan-with-a-bridge/" >See here&lt;/a>). I&amp;rsquo;ve configured with &lt;em>cni:multus,calico&lt;/em>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ ip addr
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1: lo: &amp;lt;LOOPBACK,UP,LOWER_UP&amp;gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> inet 127.0.0.1/8 scope host lo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> valid_lft forever preferred_lft forever
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> inet6 ::1/128 scope host
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> valid_lft forever preferred_lft forever
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2: eth0: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc mq master cni0 state UP group default qlen 1000
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> link/ether 00:15:5d:02:cb:08 brd ff:ff:ff:ff:ff:ff
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">3: docker0: &amp;lt;NO-CARRIER,BROADCAST,MULTICAST,UP&amp;gt; mtu 1500 qdisc noqueue state DOWN group default
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> link/ether 02:42:06:25:9b:31 brd ff:ff:ff:ff:ff:ff
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> valid_lft forever preferred_lft forever
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">4: cni0: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue state UP group default qlen 1000
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> link/ether 00:15:5d:02:cb:08 brd ff:ff:ff:ff:ff:ff
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> inet 192.168.2.241/24 brd 192.168.2.255 scope global dynamic cni0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> inet6 fe80::215:5dff:fe02:cb08/64 scope link
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> valid_lft forever preferred_lft forever
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ ip route
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">default via 192.168.2.1 dev cni0 proto dhcp src 192.168.2.241 metric 1024
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.2.0/24 dev cni0 proto kernel scope link src 192.168.2.241
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.2.1 dev cni0 proto dhcp scope link src 192.168.2.241 metric 1024
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>There&amp;rsquo;s a valid route outwards however Calico can&amp;rsquo;t start because it reports:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ sudo crictl --runtime-endpoint=unix:///run/k3s/containerd/containerd.sock ps -a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ac2bab78f970e c59896fc7ca44 3 minutes ago Exited calico-node 8 c9c4aa34f68a9
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ sudo crictl --runtime-endpoint=unix:///run/k3s/containerd/containerd.sock logs ac2bab78f970e
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2022-04-10 18:12:09.146 [WARNING][10] startup/startup.go 710: Unable to auto-detect an IPv4 address: no valid IPv4 addresses found on the host interfaces
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2022-04-10 18:12:09.146 [WARNING][10] startup/startup.go 477: Couldn&amp;#39;t autodetect an IPv4 address. If auto-detecting, choose a different autodetection method. Otherwise provide an explicit address.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2022-04-10 18:12:09.146 [INFO][10] startup/startup.go 361: Clearing out-of-date IPv4 address from this node IP=&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2022-04-10 18:12:09.150 [WARNING][10] startup/utils.go 48: Terminating
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Calico node failed to start
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Rancher Server shows the following logs.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[INFO ] waiting for at least one bootstrap node
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[INFO ] provisioning bootstrap node(s) custom-3bfcd9ce3995: waiting for agent to check in and apply initial plan
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[INFO ] provisioning bootstrap node(s) custom-3bfcd9ce3995: waiting on probes: etcd, kube-apiserver, kube-controller-manager, kube-scheduler, kubelet
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[INFO ] provisioning bootstrap node(s) custom-3bfcd9ce3995: waiting on probes: etcd, kube-apiserver, kube-controller-manager, kube-scheduler
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[INFO ] provisioning bootstrap node(s) custom-3bfcd9ce3995: waiting on probes: kube-apiserver, kube-controller-manager, kube-scheduler
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[INFO ] non-ready bootstrap machine(s) custom-3bfcd9ce3995: waiting for cluster agent to be available and join url to be available on bootstrap node
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The cluster will never progress because Rancher needs to launch the cattle-cluster-agent, but this needs a working CNI to launch correctly. However, we can&amp;rsquo;t fix the CNI because Rancher won&amp;rsquo;t give a Kube Config that allows us to connect to the cluster and deploy the working CNI.&lt;/p>
&lt;h2 id="rke2---get-a-valid-credential">RKE2 - Get a valid credential&lt;/h2>
&lt;p>Since I have full access to the host running the RKE2 cluster, I should be able to gain access to it somehow. Each Kubernetes pod deployed to a host gets a special volume mounted inside the container that it can use to communicate to the Kubernetes apiserver. By default, these pods don&amp;rsquo;t generally have any privileges, but if we can find one that has enough privileges to create the resources we need, we can get the cluster working.&lt;/p>
&lt;p>In this cluster, I enabled the kubernetes API endpoint in Rancher. This deployed a container called kube-api-auth. Luckily this container grants all the privileges we need.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ sudo ctr --address /run/k3s/containerd/containerd.sock --namespace k8s.io c ls | grep kube-api-auth
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">57d148cadbdceb998ab7be8e38f72dec1fa0fe8c6f313dcab19e09ba9245eb1f docker.io/rancher/kube-api-auth:v0.1.6 io.containerd.runc.v2
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>There may be two containers displayed. One of them is the pause container which serves as a special init process. If you want to know why here&amp;rsquo;s a &lt;a class="link" href="https://www.ianlewis.org/en/almighty-pause-container" target="_blank" rel="noopener"
>good blog post&lt;/a>.&lt;/p>
&lt;p>Inspect the container and look for the volume mount kube-api-access:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="o">$&lt;/span> &lt;span class="n">sudo&lt;/span> &lt;span class="n">ctr&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">address&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">k3s&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">containerd&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">containerd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sock&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">namespace&lt;/span> &lt;span class="n">k8s&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">io&lt;/span> &lt;span class="n">c&lt;/span> &lt;span class="n">info&lt;/span> &lt;span class="mi">57&lt;/span>&lt;span class="n">d148cadbdceb998ab7be8e38f72dec1fa0fe8c6f313dcab19e09ba9245eb1f&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="n">grep&lt;/span> &lt;span class="n">kube&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">api&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">access&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;source&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/var/lib/kubelet/pods/972647f6-e514-40c6-a0a0-6891898a2dec/volumes/kubernetes.io~projected/kube-api-access-vj98z&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="o">$&lt;/span> &lt;span class="n">sudo&lt;/span> &lt;span class="n">ls&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kubelet&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">pods&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="mi">972647&lt;/span>&lt;span class="n">f6&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">e514&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">40&lt;/span>&lt;span class="n">c6&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">a0a0&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">6891898&lt;/span>&lt;span class="n">a2dec&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">volumes&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kubernetes&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">io&lt;/span>&lt;span class="o">~&lt;/span>&lt;span class="n">projected&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kube&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">api&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">access&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">vj98z&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ca&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">crt&lt;/span> &lt;span class="n">namespace&lt;/span> &lt;span class="n">token&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The JWT token can be extracted from the &amp;rsquo;token&amp;rsquo; file. With this we&amp;rsquo;re going to effectively impersonate this container and the use the privileges that it has:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="o">$&lt;/span> &lt;span class="n">sudo&lt;/span> &lt;span class="n">cat&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kubelet&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">pods&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="mi">972647&lt;/span>&lt;span class="n">f6&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">e514&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">40&lt;/span>&lt;span class="n">c6&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">a0a0&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">6891898&lt;/span>&lt;span class="n">a2dec&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">volumes&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kubernetes&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">io&lt;/span>&lt;span class="o">~&lt;/span>&lt;span class="n">projected&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kube&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">api&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">access&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">vj98z&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">token&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">eyJhbGciOi&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="n">My&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Kubectl needs the CA certificate to validate the SSL certificate:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="o">$&lt;/span> &lt;span class="n">sudo&lt;/span> &lt;span class="n">cat&lt;/span> &lt;span class="s2">&amp;#34;/var/lib/kubelet/pods/22d4bf53-2f87-4d58-9272-9c4d0bad47f2/volumes/kubernetes.io~projected/kube-api-access-tl4qz/ca.crt&amp;#34;&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="n">base64&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">w&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="p">;&lt;/span> &lt;span class="n">echo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJlVENDQVIrZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREF&lt;/span>\&lt;span class="p">[&lt;/span>&lt;span class="o">...&lt;/span>\&lt;span class="p">]&lt;/span>&lt;span class="n">RU5EIENFUlRJRklDQVRFLS0tLS0K&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Edit ~/.kube/config and insert the content:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">clusters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;my-new-cluster&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cluster&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">server&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;https://**{myip}**:6443&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">certificate-authority-data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;**{base64d ca.crt}**&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">users&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;my-new-user&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">user&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">token&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;**{contents of /token}**&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">contexts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;new-cluster&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">user&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;my-new-user&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cluster&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;my-new-cluster&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">current-context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;new-cluster&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After that you should be able to use kubectl to launch whatever resources you need. Remember to change get a new kubectl from Rancher afterwards so you&amp;rsquo;re not using system level credentials.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F04%2Fhow-to-gain-access-to-a-rke2-cluster-when-rancher-cant%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=How+to+gain+access+to+a+RKE2+cluster+without+Rancher+when+the+CNI+doesn%27t+work" style="border:0" alt="" /></description></item><item><title>Defensive Coding: Stop using your storage models everywhere</title><link>https://www.technowizardry.net/2022/03/stop-using-your-storage-models-everywhere/</link><pubDate>Fri, 18 Mar 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/03/stop-using-your-storage-models-everywhere/</guid><summary>&lt;p>&lt;em>How to make your system robust against your worst nightmare&amp;ndash;your future self&lt;/em>&lt;/p>
&lt;p>In this post, I talk about some strategies that I&amp;rsquo;ve learned to simplify class structures in Java services that load and persist data into data stores like DynamoDB or RDS at the same time making the codebase safer.&lt;/p>
&lt;p>As always, my opinions are my own.&lt;/p>
&lt;p>At Amazon, I ended up joining two teams that were suffering under the technical debt. Each time, I was asked to spend some time understanding why the products were unstable and users were encountering frequent bugs. In one system, responsible for managing critical metadata about products in the catalog, was experiencing problems where users were reporting that they&amp;rsquo;d randomly lose data.&lt;/p></summary><description>&lt;p>&lt;em>How to make your system robust against your worst nightmare&amp;ndash;your future self&lt;/em>&lt;/p>
&lt;p>In this post, I talk about some strategies that I&amp;rsquo;ve learned to simplify class structures in Java services that load and persist data into data stores like DynamoDB or RDS at the same time making the codebase safer.&lt;/p>
&lt;p>As always, my opinions are my own.&lt;/p>
&lt;p>At Amazon, I ended up joining two teams that were suffering under the technical debt. Each time, I was asked to spend some time understanding why the products were unstable and users were encountering frequent bugs. In one system, responsible for managing critical metadata about products in the catalog, was experiencing problems where users were reporting that they&amp;rsquo;d randomly lose data.&lt;/p>
&lt;p>A service that was losing client data is a terrible service and caused users to lose trust in this system. Note that some details of this story have been modified for confidentiality reasons. Let&amp;rsquo;s dive in.&lt;/p>
&lt;blockquote>
&lt;p>Cognitive complexity in software engineering is the effort that one has to spend to understand and modify a code base or system. The abstractions we use and APIs we expose are a big part in increasing or decreasing this mental burden.&lt;/p>&lt;/blockquote>
&lt;h2 id="background">Background&lt;/h2>
&lt;p>In this system inspired by real life, there was a web UI that called into a REST service which then persisted data into a graph database, AWS Neptune.&lt;/p>
&lt;p>&lt;a class="link" href="images/ArchitectureInitial.png" >&lt;img src="https://www.technowizardry.net/2022/03/stop-using-your-storage-models-everywhere/images/ArchitectureInitial.png"
width="562"
height="243"
srcset="https://www.technowizardry.net/2022/03/stop-using-your-storage-models-everywhere/images/ArchitectureInitial_hu_b326498388def7d1.png 480w, https://www.technowizardry.net/2022/03/stop-using-your-storage-models-everywhere/images/ArchitectureInitial_hu_470938b42e484dde.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="231"
data-flex-basis="555px"
>&lt;/a>&lt;/p>
&lt;p>The primary data in this system was a &lt;strong>Project&lt;/strong> which included a number of &lt;strong>Values&lt;/strong> both containing a variety of different fields. The data was stored in a graph database in AWS Neptune and the service used &lt;a class="link" href="https://rdf4j.org/" target="_blank" rel="noopener"
>Eclipse rdf4j&lt;/a> as a high-level ORM that mapped Java classes into the SPARQL queries (SPARQL is the language used to query and mutate Neptune databases.)&lt;/p>
&lt;h3 id="problems-in-every-interface-layer">Problems in every interface layer&lt;/h3>
&lt;p>There were many problems with this system. First, the service used rdf4j to save the entire Project entity to the database. The query language, SPARQL, only gave guarantees about transactions at a per triple level (effectively a entity column or field level), but for updates spanning multiple columns/fields, any concurrency would just cause broken consistency. Neptune was the wrong choice for this problem.&lt;/p>
&lt;p>Second, the REST service didn&amp;rsquo;t expose fine-grained edit APIs to apply business logic or mutate specific fields on the objects. Instead it exposed a single giant UpdateProject API in which the client-side JS code would make changes to the record locally, then persist it to the service. Combined with the above, the likelihood of corruption was high.&lt;/p>
&lt;p>Third, (the point of this post) the &lt;strong>Project&lt;/strong> entity class was written exactly like rdf4j needed to persist.&lt;/p>
&lt;p>The class looked something like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Lombok that auto creates get* and set* methods for all fields&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Getter&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Setter&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// Special sauce for rdf4j so it knows how to save to the graph DB&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@RDFBean&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">IRIs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">PROJECT&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">ProjectRdf&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@RDF&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">IRIs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">PROJECTID&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Nested objects&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@RDF&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">IRIs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">VALUES&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Set&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">values&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Effectively a map of [X -&amp;gt; State], but modeled as a set of tuples&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@RDF&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">IRIs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">STATES&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Set&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Literal&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">states&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@RDF&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">IRIs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">LAST_MODIFIED&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">lastModified&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Many more fields&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">validate&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// A big method that attempted to validate every different problem&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>There were a few key problems:&lt;/p>
&lt;p>The object tried to be immutable, but the object itself didn&amp;rsquo;t handle cloning. Instead the calling code had to construct a new instance copying fields and changing as it went. This was inconsistently applied and every place that modified the object ended up being lots of boilerplate to clone and mutate.&lt;/p>
&lt;p>This object is the class structure that rdf4j needs to be able to load the objects. It exposed getters and setters for every single field directly as rdf4j used Java reflection to create an instance, then called set*() to populate the object as it loaded it into memory. Other code in the same system had full access to modify every single field in this instance and even get this object into invalid states.&lt;/p>
&lt;h3 id="storage-layer-interface">Storage Layer Interface&lt;/h3>
&lt;p>The storage layer (sometimes called a DAO-Data Access Object) is the component that is supposed to abstract out interactions with the data store and provider APIs to load and save. The interface looked a little like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">interface&lt;/span> &lt;span class="nc">ProjectStore&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">ProjectRdf&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">load&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">projectId&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">save&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ProjectRdf&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">project&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">delete&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ProjectRdf&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">project&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This interface also contributed to the problems. There&amp;rsquo;s only a single save method that saves the entire record to the data store and it used some kind of optimistic locking (using a version counter to identify concurrent writes) to prevent two users from modifying the same record.&lt;/p>
&lt;p>However, if your object has a lot of fields and many people concurrently modify it, the risk of a version conflict exception grows higher even if those independent operations could be applied atomically. Not every model easily lends to lots of independent concurrent updates to the same record. In this post, I&amp;rsquo;ll give two examples. This example could be broken down into separate entities, but breaking down operations past that was challenging because we would have needed version tracking per field.&lt;/p>
&lt;h2 id="influence-on-the-rest-of-the-system">Influence on the rest of the system&lt;/h2>
&lt;p>&lt;a class="link" href="images/Catalog-ClassStructure.png" >&lt;img src="https://www.technowizardry.net/2022/03/stop-using-your-storage-models-everywhere/images/Catalog-ClassStructure.png"
width="656"
height="549"
srcset="https://www.technowizardry.net/2022/03/stop-using-your-storage-models-everywhere/images/Catalog-ClassStructure_hu_1e75c3ddb8bb9f83.png 480w, https://www.technowizardry.net/2022/03/stop-using-your-storage-models-everywhere/images/Catalog-ClassStructure_hu_48e14f487c88da55.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="119"
data-flex-basis="286px"
>&lt;/a>&lt;/p>
&lt;p>Let&amp;rsquo;s look at how the system evolved in a system shaped like this.&lt;br>
&lt;strong>Decentralized business logic&lt;/strong>&lt;/p>
&lt;p>&lt;strong>First&lt;/strong>, because the storage models (in green) encapsulate no logic, the business logic gets pushed outside into the callers. Business logic might include making some change to one or more properties on these entities and ensuring they continue to maintain correctness.&lt;/p>
&lt;p>The logic was decentralized and could be bypassed by interacting with the classes directly. The team created utility classes to help perform changes to the entities, but this led to decentralized logic You have to know where to look to find them and how to use them. If you&amp;rsquo;re trying to interact with a domain model, do you look on that model, or do you look around in other Java packages to find a relevant util or helper class? Different developers did different things because nothing forced them to be consistent.&lt;/p>
&lt;p>&lt;strong>Proliferation of storage limitations&lt;/strong>&lt;/p>
&lt;p>Data stores bring with them many limitations on what you can store in them. For example, Neptune required you to store values as a literal (a string, numeric or boolean) leading the system to use strings to represent values with no validations. A developer could have easily added whatever they wanted by accident and that may or may not have been caught by the validate method (assuming it was called or a validation was added.)&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="n">myProject&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getStates&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Literal&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;TotallyGarbageData&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>DynamoDB has its &lt;a class="link" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBMapper.DataTypes.html" target="_blank" rel="noopener"
>own limitations&lt;/a>, too and the common DynamoDBMapper tries to map types, but something as simple as a Set&lt;Enum> can&amp;rsquo;t be automatically mapped.&lt;/p>
&lt;p>Developers couldn&amp;rsquo;t create custom classes or use the best data type for the situation.&lt;/p>
&lt;p>&lt;strong>Abstractions in Layered Systems&lt;/strong>&lt;/p>
&lt;p>As you went higher in the stack, abstractions should become more abstract and less aware of the details, but in this case the models were not able to hide the complexity and everything had to be aware of how the database stored objects. Each layer added only minimum abstraction value and mostly punted to problem upwards.&lt;/p>
&lt;h3 id="what-is-the-domain">What is the domain?&lt;/h3>
&lt;p>Every system has a domain. The domain, in this case, represents the business problem that you&amp;rsquo;re trying to solve without considering constraints like database structures. The domain models should be considered the best way to reason about the business concepts working in memory.&lt;/p>
&lt;p>The above class structure doesn&amp;rsquo;t model the domain, it represents the limitations of the databases and those limitations imposed on the entire rest of the system.&lt;/p>
&lt;h2 id="separating-the-storage-concern-from-the-domain">Separating the storage concern from the domain&lt;/h2>
&lt;p>The first thing we need to do as build a good domain model class that represents exactly how we want to think about the business logic.&lt;/p>
&lt;p>Let&amp;rsquo;s create the class.&lt;/p>
&lt;p>First, there&amp;rsquo;s no default Setter methods. We only expose read-only access, but explicitly control which attributes have setters.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">Project&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@NonNull&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@Getter&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">UUID&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Version is only here to carry back into the storage layer &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// which increments it&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@Getter&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">version&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">Project&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">UUID&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">...&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">Project&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">UUID&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">version&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">...&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Instead of before where we had a no args constructor that allowed the ORM to create empty objects to later populate, developers MUST provide the minimum set of values required. After the id or versions are set, you can&amp;rsquo;t modify them.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@Getter&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Instant&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">createdBy&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@Getter&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Instant&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">lastModified&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Let&amp;rsquo;s use appropriate data types for timestamps instead of longs so they&amp;rsquo;re validated automatically by Java.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Map&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Language&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">State&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">states&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Map&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Language&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">State&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getLanguageStates&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Collections&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">unmodifiableMap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">states&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">State&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getStateForLanguage&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Language&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">...&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Set&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">KnownValue&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">knownValues&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now we&amp;rsquo;re getting to the important part of the domain model. Don&amp;rsquo;t worry about names or anything, but note how instead of storing the states as a &lt;em>Set&lt;Literal>&lt;/em> (i.e. a string set) we&amp;rsquo;re realizing a structure that this is a state for each language.&lt;/p>
&lt;p>We also don&amp;rsquo;t expose the raw state map instead only exposing read-only access. All writes have to go through methods that ensure it&amp;rsquo;s a safe transition:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">changeState&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Language&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">State&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Is this state transition permitted?&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Apply state change&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now instead of higher layered methods having to worry about how to apply a change, they only call the method and the model figures out how. This simplifies those higher-layers making them easier to read and understand because they&amp;rsquo;re phrased in terms of the domain.&lt;/p>
&lt;h3 id="the-storage-layer">The Storage Layer&lt;/h3>
&lt;p>We locked down access to the domain model, but the storage layer needs to be restricted. It can&amp;rsquo;t return or take in a storage model otherwise developers could just save whatever they want.&lt;/p>
&lt;p>Instead, the class should only take in the domain model, then translate it from the domain model into a storage model, then submit the API request.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">interface&lt;/span> &lt;span class="nc">ProjectStore&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Project&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">load&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">projectId&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">save&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Project&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">project&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">delete&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Project&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">project&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now, it&amp;rsquo;s not possible to bypass any validations on the model and in theory all objects should be perfectly valid in the data store.&lt;/p>
&lt;h2 id="another-example---devices">Another Example - Devices&lt;/h2>
&lt;p>This system responsible for managing the status of tens of thousands of devices, was having problems where devices would get into unexpected states and break requiring manual intervention by a user or by the service team developers.&lt;/p>
&lt;p>The operational burden was becoming unmanageable and the dev team was getting burned out.&lt;/p>
&lt;h3 id="background-1">Background&lt;/h3>
&lt;p>In this example system, the primary resource can be called a &lt;strong>Device&lt;/strong> and each device had numerous status fields tracking the desired state, actual state, and other various health statuses.&lt;/p>
&lt;p>The architecture involved a lot of concurrency on each device record in the database as at any given point multiple processes could be requesting updates to a single device or processing the updates getting pushed to the devices. Due to the low latency requirements, we couldn&amp;rsquo;t just serialize all updates to each update device record.&lt;/p>
&lt;p>&lt;a class="link" href="images/ArchitectureInitial.png" >&lt;img src="https://www.technowizardry.net/2022/03/stop-using-your-storage-models-everywhere/images/ArchitectureInitial.png"
width="562"
height="243"
srcset="https://www.technowizardry.net/2022/03/stop-using-your-storage-models-everywhere/images/ArchitectureInitial_hu_b326498388def7d1.png 480w, https://www.technowizardry.net/2022/03/stop-using-your-storage-models-everywhere/images/ArchitectureInitial_hu_470938b42e484dde.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="231"
data-flex-basis="555px"
>&lt;/a>&lt;/p>
&lt;p>&lt;strong>Class Structure&lt;/strong>&lt;/p>
&lt;p>Since this was a set of Java applications that were interacting with DynamoDB, we were using &lt;a class="link" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBMapper.html" target="_blank" rel="noopener"
>DynamoDBMapper&lt;/a> as a higher-level interface for reading and writing records to the database. In this world, you annotate classes with annotations that tell DynamoDB which attributes to save to which columns and it constructs the API calls for you.&lt;/p>
&lt;p>In our example, the device class looked like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Getter&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Setter&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@DynamoDBTable&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tableName&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;Devices&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">Device&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">deviceId&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">productName&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">desiredContent&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">requestedAt&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">actualContent&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">updatedAt&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// ... Lots more fields&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">version&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">lastUpdatedAt&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@DynamoDBHashKey&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">attributeName&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;DeviceId&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getId&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">deviceId&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">setId&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">deviceId&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@DynamoDBAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">attributeName&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;ProductName&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getProductName&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">...&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">setProductName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">productName&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">...&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@DynamoDBAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">attributeName&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;DesiredContent&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getDesiredContent&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">...&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">setSpecialPayloadInfo&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">...&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@DynamoDBVersionAttribute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getVersion&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">...&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">setVersion&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">version&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">...&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nd">@DynamoDBAutoGeneratedTimestamp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">strategy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">DynamoDBAutoGenerateStrategy&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">ALWAYS&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getLastUpdatedDate&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">...&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">setLastUpdatedDate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">lastUpdatedDate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">...&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Similar to the previous example, everything is fully open. One key difference with this example was that there were multiple writers that were all operating on different parts of the record to maintain that low-latency requirement.&lt;/p>
&lt;p>Events were coming from the devices about the state of the world that needed to be persisted to the store. Because it&amp;rsquo;s the state of the world, it should be considered to be absolute truth. Thus we wanted to always update this field without considering version conflicts.&lt;/p>
&lt;p>This was done by using DynamoDBMapper&amp;rsquo;s &lt;a class="link" href="https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/dynamodbv2/datamodeling/DynamoDBMapperConfig.SaveBehavior.html#UPDATE_SKIP_NULL_ATTRIBUTES" target="_blank" rel="noopener"
>SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES&lt;/a> which stated it would only save fields that are non-null. The system only populated the id and field to change and left everything else as null.&lt;/p>
&lt;p>Effectively the code looked like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">changeActualContent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">deviceId&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">actualContent&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Device&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">device&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Device&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">device&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setId&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">deviceId&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">device&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setActualContent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">actualContent&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">deviceStore&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">save&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">device&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SaveBehavior&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">UPDATE_SKIP_NULL_ATTRIBUTES&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The goal was that only the changed fields would get saved and nothing else would get saved, however this skips null attribute values. What happens if you want to change desiredContent to be null? It can&amp;rsquo;t be done. This actually actually made it to production and caused a bug.&lt;/p>
&lt;h3 id="solution">Solution&lt;/h3>
&lt;p>We applied similar refactoring techniques here: created a domain-focused model and a refined storage layer that only accepted domain constructs to load/save.&lt;/p>
&lt;p>However, what was different is that this service had low-latency requirements and couldn&amp;rsquo;t use record-level versioning with retries for changes that were completely orthogonal. Instead it needed to be able to apply changes to the records atomically with no record-level versioning using field-level concurrency using DynamoDB&amp;rsquo;s &lt;a class="link" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html" target="_blank" rel="noopener"
>Conditional Expressions&lt;/a>.&lt;/p>
&lt;p>We also banned the usage of UPDATE_SKIP_NULL_ATTRIBUTES due to the ease in causing a bug.&lt;/p>
&lt;p>This required more focus on the storage layer API to expose just the right level of abstractions.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">interface&lt;/span> &lt;span class="nc">DeviceStore&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Device&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">load&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">deviceId&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Used when you need to save the entire object&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">save&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Device&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">device&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">updateActualContent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">deviceId&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">actualContent&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now for these patch operations, we exposed a updatedActualContent API on the storage layer. This contrasts with the strategy in the first example which was only a single save API. However, this is a safe change because the device was reporting it&amp;rsquo;s state. There&amp;rsquo;s no world in which the actual state coming from the device should be rejected due to a conflicting write.&lt;/p>
&lt;p>Since we couldn&amp;rsquo;t use the SKIP_NULL flag, we instead created specific patch objects:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// This ensures that the patch goes over the same table&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@DynamoDBTable&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tableName&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;Devices&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@AllArgsConstructor&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">PatchDeviceActualContent&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">deviceId&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">actualContent&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And the storage model used that instead:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">updateActualContent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">deviceId&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">actualContent&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">var&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">patch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PatchDeviceActualContent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">deviceId&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">actualContent&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">dynamoMapper&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">save&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">patch&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Thus allowing us to change fields to be null and apply these changed atomically and concurrently in DynamoDB.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>After applying this technique across these two teams, we were able to eliminate data corruption issues caused by bugs in improperly applied business logic. This dramatically reduced the cognitive complexity of the system and helped developers move faster because they knew they could rely on the safety of the model.&lt;/p>
&lt;p>Don&amp;rsquo;t follow best practices blindly. When I asked engineers why they made some choices (like the immutable objects) and they said that they thought it was a best practice and were trying it out. Nobody questioned whether it made sense in the case. If somebody claims some thing is a best practice you need to understand why, does it make sense, does it even apply in your situation? Not every best practice is really even that good.&lt;/p>
&lt;p>A lot of developers have the mentality that because the code is internal, developers won&amp;rsquo;t make mistakes and accidentally forget a validation or apply an improper change to the object. However, one thing I&amp;rsquo;ve realized is that without being malicious developers frequently make mistakes because modern code bases become gigantic and it&amp;rsquo;s hard to keep track of every expectation.&lt;/p>
&lt;p>When designing your classes, you should instead think about how to model your business logic intelligently and safely, then separately how to save within the limitations of your data store. This will reduce the cognitive overhead.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F03%2Fstop-using-your-storage-models-everywhere%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Defensive+Coding%3A+Stop+using+your+storage+models+everywhere" style="border:0" alt="" /></description></item><item><title>Plot your health with Samsung Health and Pandas</title><link>https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/</link><pubDate>Wed, 23 Feb 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/</guid><summary>&lt;p>Artwork by &lt;a class="link" href="https://samilee.me" target="_blank" rel="noopener"
>Sami Lee&lt;/a>.&lt;/p>
&lt;p>For the last 5+ years, I&amp;rsquo;ve been tracking my various aspects of my personal health using Samsung Health. It helps track weight, calories, heart rate, stress, and exercise and stores all of it in the app.&lt;/p>
&lt;p>However, the app only gives some basic high level charts and insights. Luckily, it enables you to export your personal data into CSV files that you can then import into your tool of choice and perform any kind of analytics. In this post, I&amp;rsquo;m going to show how to export it all, then load it into Zeppelin and some sample Pandas queries that&amp;rsquo;ll enable you to start building more complex queries yourself.&lt;/p></summary><description>&lt;img src="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/header.svg" alt="Featured image of post Plot your health with Samsung Health and Pandas" />&lt;p>Artwork by &lt;a class="link" href="https://samilee.me" target="_blank" rel="noopener"
>Sami Lee&lt;/a>.&lt;/p>
&lt;p>For the last 5+ years, I&amp;rsquo;ve been tracking my various aspects of my personal health using Samsung Health. It helps track weight, calories, heart rate, stress, and exercise and stores all of it in the app.&lt;/p>
&lt;p>However, the app only gives some basic high level charts and insights. Luckily, it enables you to export your personal data into CSV files that you can then import into your tool of choice and perform any kind of analytics. In this post, I&amp;rsquo;m going to show how to export it all, then load it into Zeppelin and some sample Pandas queries that&amp;rsquo;ll enable you to start building more complex queries yourself.&lt;/p>
&lt;h2 id="download-your-data">Download your Data&lt;/h2>
&lt;p>First, open Samsung Health on your mobile phone&lt;/p>
&lt;p>&lt;a class="link" href="images/SHealth-Export-Step1.png" >&lt;img src="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step1-507x1024.png"
width="507"
height="1024"
srcset="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step1-507x1024_hu_6ce8bbe0c9202a5c.png 480w, https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step1-507x1024_hu_b23ed7eb315c7c79.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="49"
data-flex-basis="118px"
>&lt;/a>&lt;/p>
&lt;p>Scroll down and tap &amp;ldquo;Download personal data&amp;rdquo;&lt;/p>
&lt;p>&lt;a class="link" href="images/SHealth-Export-Step2.png" >&lt;img src="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step2-507x1024.png"
width="507"
height="1024"
srcset="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step2-507x1024_hu_4b21af4f943e4653.png 480w, https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step2-507x1024_hu_ff63ea8167c5e68a.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="49"
data-flex-basis="118px"
>&lt;/a>&lt;/p>
&lt;p>Tap the Download button&lt;/p>
&lt;p>&lt;a class="link" href="images/SHealth-Export-Step3.png" >&lt;img src="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step3-508x1024.png"
width="508"
height="1024"
srcset="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step3-508x1024_hu_5479571151a80f96.png 480w, https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step3-508x1024_hu_4e78dad4a8d755b9.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="49"
data-flex-basis="119px"
>&lt;/a>&lt;/p>
&lt;p>Login to your account when it prompts you&lt;/p>
&lt;p>&lt;a class="link" href="images/SHealth-Export-Step4.png" >&lt;img src="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step4-507x1024.png"
width="507"
height="1024"
srcset="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step4-507x1024_hu_4163bfcdf027344c.png 480w, https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step4-507x1024_hu_75ea37fa30e16f4b.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="49"
data-flex-basis="118px"
>&lt;/a>&lt;/p>
&lt;p>Wait for it to download&lt;/p>
&lt;p>&lt;a class="link" href="images/SHealth-Export-Step5.png" >&lt;img src="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step5-507x1024.png"
width="507"
height="1024"
srcset="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step5-507x1024_hu_c165e00e7ab3d656.png 480w, https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step5-507x1024_hu_9ded6e4bf29818b8.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="49"
data-flex-basis="118px"
>&lt;/a>&lt;/p>
&lt;p>After it completes downloading, tap View files.&lt;/p>
&lt;p>&lt;a class="link" href="images/SHealth-Export-Step6.png" >&lt;img src="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step6-511x1024.png"
width="511"
height="1024"
srcset="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step6-511x1024_hu_eb8f84456c0e43a8.png 480w, https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step6-511x1024_hu_42125af81c372908.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="49"
data-flex-basis="119px"
>&lt;/a>&lt;/p>
&lt;p>Select the file, tap compress, save it as a zip file.&lt;/p>
&lt;p>&lt;a class="link" href="images/SHealth-Export-Step7.png" >&lt;img src="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step7-511x1024.png"
width="511"
height="1024"
srcset="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step7-511x1024_hu_a9507d180505f671.png 480w, https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/SHealth-Export-Step7-511x1024_hu_8d32ea035512a4a9.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="49"
data-flex-basis="119px"
>&lt;/a>&lt;/p>
&lt;p>Then upload the file to the computer running Zeppelin. I upload it to OneDrive, then SFTP it to the server, then extract it into a folder&lt;/p>
&lt;h2 id="working-with-the-data">Working with the Data&lt;/h2>
&lt;p>The export should contain a number of interesting files in the main directory along with the json and files directories. The main files are CSV formatted.&lt;/p>
&lt;p>&lt;a class="link" href="https://developer.samsung.com/health/android/data/guide/health-data-type.html" target="_blank" rel="noopener"
>Data Type Listing&lt;/a>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>File Name&lt;/th>
&lt;th>Doc Link&lt;/th>
&lt;th>Notes&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>com.samsung.health.ecg.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.health.floors_climbed.{ts}.csv&lt;/td>
&lt;td>&lt;a class="link" href="https://developer.samsung.com/health/android/data/api-reference/com/samsung/android/sdk/healthdata/HealthConstants.FloorsClimbed.html" target="_blank" rel="noopener"
>Link&lt;/a>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.health.food_info.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.health.food_intake.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.health.height.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.health.nutrition.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.health.sleep_stage.{ts}.csv&lt;/td>
&lt;td>&lt;a class="link" href="https://img-developer.samsung.com/onlinedocs/health/android/data/com/samsung/android/sdk/healthdata/HealthConstants.SleepStage.html" target="_blank" rel="noopener"
>Link&lt;/a>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.health.user_profile.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.health.water_intake.{ts}.csv&lt;/td>
&lt;td>&lt;a class="link" href="https://img-developer.samsung.com/onlinedocs/health/android/data/com/samsung/android/sdk/healthdata/HealthConstants.WaterIntake.html" target="_blank" rel="noopener"
>Link&lt;/a>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.health.weight.{ts}.csv&lt;/td>
&lt;td>&lt;a class="link" href="https://img-developer.samsung.com/onlinedocs/health/android/data/com/samsung/android/sdk/healthdata/HealthConstants.Weight.html" target="_blank" rel="noopener"
>Link&lt;/a>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.activity.day_summary.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.activity.goal.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.activity_level.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.best_records.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.blood_pressure.{ts}.csv&lt;/td>
&lt;td>&lt;a class="link" href="https://img-developer.samsung.com/onlinedocs/health/android/data/com/samsung/android/sdk/healthdata/HealthConstants.BloodPressure.html" target="_blank" rel="noopener"
>Link&lt;/a>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.breathing.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.caloric_balance_goal.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.calories_burned.details.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.exercise.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.exercise.weather.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.floor_goal.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.food_favorite.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.food_frequent.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.food_goal.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.goal.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.goal_history.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.insight.milestones.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.library_subscription.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.permission.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.preferences.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.report.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.rewards.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.sleep.{ts}.csv&lt;/td>
&lt;td>&lt;a class="link" href="https://img-developer.samsung.com/onlinedocs/health/android/data/com/samsung/android/sdk/healthdata/HealthConstants.Sleep.html" target="_blank" rel="noopener"
>Link&lt;/a>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.sleep_combined.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.sleep_data.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.sleep_goal.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.social.friends.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.social.leaderboard.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.social.public_challenge.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.social.public_challenge.detail.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.social.public_challenge.extra.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.social.public_challenge.history.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.social.service_status.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.stand_day_summary.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.step_daily_trend.{ts}.csv&lt;/td>
&lt;td>&lt;a class="link" href="https://img-developer.samsung.com/onlinedocs/health/android/data/com/samsung/android/sdk/healthdata/HealthConstants.StepDailyTrend.html" target="_blank" rel="noopener"
>Link&lt;/a>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.stress.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.stress.base_histogram.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.stress.histogram.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.tip.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.tracker.heart_rate.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.tracker.oxygen_saturation.{ts}.csv&lt;/td>
&lt;td>&lt;a class="link" href="https://img-developer.samsung.com/onlinedocs/health/android/data/com/samsung/android/sdk/healthdata/HealthConstants.OxygenSaturation.html" target="_blank" rel="noopener"
>Link&lt;/a>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.tracker.pedometer_day_summary.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.tracker.pedometer_recommendation.{ts}.csv&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>com.samsung.shealth.tracker.pedometer_step_count.{ts}.csv&lt;/td>
&lt;td>&lt;a class="link" href="https://img-developer.samsung.com/onlinedocs/health/android/data/com/samsung/android/sdk/healthdata/HealthConstants.StepCount.html" target="_blank" rel="noopener"
>Link&lt;/a>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>files/&lt;/td>
&lt;td>&lt;/td>
&lt;td>Contains random files like your profile picture&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>json/&lt;/td>
&lt;td>&lt;/td>
&lt;td>Contains the &lt;em>binned data&lt;/em> which is higher resolution (e.g. minute level) data from workouts, sleep, etc.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Notes:&lt;/p>
&lt;p>Once the data is loaded and accessible to Zeppelin, create a new notebook. This first block of code provides some basic package imports for Pandas and charting.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">numpy&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">np&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">pandas&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">pd&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">matplotlib.pyplot&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">plt&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">matplotlib.ticker&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">mtick&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">datetime&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">date&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">datetime&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">style&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">use&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;ggplot&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I define a few utility methods that help me load data from the different CSV files.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">ts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{timestamp}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">file_name&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s2">&amp;#34;/shealth/samsunghealth_example_user_&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">ts&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34;/&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">name&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34;.&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">ts&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34;.csv&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">bin_file&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">type&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">uuid&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s2">&amp;#34;/shealth/samsunghealth_example_user_&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">ts&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34;/jsons/&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nb">type&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34;/&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">uuid&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34;/&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">uuid&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34;.binning_data.json&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The timestamps stored in time based fields are UTC time, but the time offset is in a separate column. This makes it hard to do certain kinds of investigations because you can&amp;rsquo;t see things like what time you go to bed on average if you travel around.&lt;/p>
&lt;p>The following code provides a load method: load_file(), that can be used to correctly parse the dates.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">date_parser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">date&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tz&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">to_datetime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">date&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34; &amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">tz&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">utc&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">format&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;%Y-%m-&lt;/span>&lt;span class="si">%d&lt;/span>&lt;span class="s1"> %H:%M:%S.fff %Z%z&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">load_file&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">date_cols&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tz_col&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">**&lt;/span>&lt;span class="n">kwargs&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">parse_dates_param&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">([(&lt;/span>&lt;span class="s1">&amp;#39;foo&amp;#39;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tz_col&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">date_cols&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">df&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read_csv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">file_name&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">header&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">index_col&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">keep_date_col&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">parse_dates&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">parse_dates_param&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">date_parser&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">date_parser&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">**&lt;/span>&lt;span class="n">kwargs&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">val&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">parse_dates_param&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">items&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">val&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">df&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">df&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">drop&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">columns&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">df&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="analytics">Analytics&lt;/h3>
&lt;p>Below I show a few different examples of how to load and visualize interesting data sets.&lt;/p>
&lt;h4 id="sleep-data">Sleep Data&lt;/h4>
&lt;p>Sleep data can be loaded with the following code. This produces two data frames: sleep_data and sleep_data_by_day. If take naps during the day/night, then you&amp;rsquo;ll get one record per nap + one record per sleep event. If you wake up at night and move around enough, then Samsung Health may create a separate sleep record for when you go back to sleep.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">sleep_data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">load_file&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;com.samsung.shealth.sleep&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">date_cols&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;com.samsung.health.sleep.start_time&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;com.samsung.health.sleep.end_time&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">tz_col&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;com.samsung.health.sleep.time_offset&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sleep_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;start_time&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sleep_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;com.samsung.health.sleep.start_time&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sleep_data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sleep_data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_index&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;com.samsung.health.sleep.start_time&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sort_index&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Drop invalid data&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sleep_data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sleep_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">sleep_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;efficiency&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sleep_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;day&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sleep_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;start_time&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">floor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;d&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sleep_data_by_day&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sleep_data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">groupby&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;day&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">agg&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="s1">&amp;#39;sleep_duration&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;sum&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;movement_awakening&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;sum&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;start_time&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;min&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;physical_recovery&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;mean&amp;#39;&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sleep_data_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;first_sleep&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sleep_data_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;start_time&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">apply&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">lambda&lt;/span> &lt;span class="n">dt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">dt&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">dt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hour&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">minute&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">second&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">microsecond&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">total_seconds&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">60&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">60&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sleep_data_by_day&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sleep_data_by_day&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sort_index&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>With this, you can graph how many hours of sleep you get per night:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">sleep_data_by_day&lt;/span>&lt;span class="p">[[&lt;/span>&lt;span class="s1">&amp;#39;sleep_duration&amp;#39;&lt;/span>&lt;span class="p">]]&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">60&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_ylabel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;hours&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;a class="link" href="images/image-4.png" >&lt;img src="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/image-4.png"
width="668"
height="365"
srcset="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/image-4_hu_73dd00e5f99cb685.png 480w, https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/image-4_hu_e614268ba02f94f9.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="183"
data-flex-basis="439px"
>&lt;/a>&lt;/p>
&lt;p>Hours of sleep per day. The dips near zero are likely data quality issues.&lt;/p>
&lt;p>Sleep data is additionally broken out into a separate file for &amp;ldquo;sleep stages&amp;rdquo;. Sleep stages breaks down each night sleep into light, deep, REM, and awake stages. These stages help explain how good of a night&amp;rsquo;s sleep you&amp;rsquo;re getting and identify if you&amp;rsquo;re tossing and turning too much.&lt;/p>
&lt;p>The following code will load sleep data into a data frame:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">sleep_stages&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read_csv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">file_name&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;com.samsung.health.sleep_stage&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">skiprows&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">index_col&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sleep_stage_name_map&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="mi">40001&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;awake&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">40002&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;light&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">40003&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;deep&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">40004&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;rem&amp;#39;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sleep_stages&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sleep_stages&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">sleep_stages&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;start_time&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="s1">&amp;#39;2021-01-01&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sleep_stages&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sleep_stages&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sort_values&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">by&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;start_time&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Data Massaging&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Convert Samsung ids into friendly names e.g. 40001 -&amp;gt; &amp;#39;awake&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sleep_stages&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;stage_name&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sleep_stages&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;stage&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sleep_stage_name_map&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sleep_stages&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;duration&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sleep_stages&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;end_time&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">sleep_stages&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;start_time&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sleep_stages&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;day&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sleep_stages&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;start_time&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">floor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;d&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">stage_by_day&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">stages&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">groupby&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;day&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;stage&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span> \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">.&lt;/span>&lt;span class="n">agg&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="s1">&amp;#39;duration&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;sum&amp;#39;&lt;/span>&lt;span class="p">})&lt;/span> \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">.&lt;/span>&lt;span class="n">unstack&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">stage_by_day&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">columns&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">stage_by_day&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">columns&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get_level_values&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">stage_by_day&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">stage_by_day&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">fillna&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;total&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;awake&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;deep&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;light&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;rem&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>There&amp;rsquo;s a couple interesting views of this data that I use. One is to project out the % of time in each stage over a time period to see if my sleep is changing.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;awake%&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;awake&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;total&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;light%&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;light&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;total&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;rem%&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;rem&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;total&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;deep%&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;deep&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;total&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">stage_by_day&lt;/span>&lt;span class="p">[[&lt;/span>&lt;span class="s1">&amp;#39;awake%&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;light%&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;rem%&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;deep%&amp;#39;&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_title&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">% s&lt;/span>&lt;span class="s2">leep time spent per stage&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">yaxis&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_major_formatter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mtick&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">PercentFormatter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">xmax&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;a class="link" href="images/image-5.png" >&lt;img src="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/image-5.png"
width="733"
height="484"
srcset="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/image-5_hu_86145b1f5463807.png 480w, https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/image-5_hu_ac359e2ce9d1eb71.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="151"
data-flex-basis="363px"
>&lt;/a>&lt;/p>
&lt;h4 id="heart-rate">Heart Rate&lt;/h4>
&lt;p>Heart Rate data can be found in two locations: the main CSV file and in the binned JSON files. The JSON files contain higher resolution per minute. The main CSV file will contain either hourly summaries in the case of binned data or individual data points. My watch can be configured to collect heart rate data either continuously or intermittently. If it&amp;rsquo;s continuous, then you&amp;rsquo;ll see the binned data.&lt;/p>
&lt;p>The following block will load the summary data and additionally load any binned data that is associated with an hour block into a data frame.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Load Heart Rate Data (non-binned)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">heart_rate&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read_csv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">file_name&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;com.samsung.shealth.tracker.heart_rate&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">skiprows&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">index_col&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">dtype&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;com.samsung.health.heart_rate.custom&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="sa">u&lt;/span>&lt;span class="s1">&amp;#39;com.samsung.health.heart_rate.binning_data&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">})&lt;/span>&lt;span class="c1">#, parse_dates=[&amp;#39;com.samsung.health.heart_rate.start_time&amp;#39;])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">heart_rate&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;start_time_tz&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">to_datetime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">heart_rate&lt;/span>&lt;span class="p">[[&lt;/span>&lt;span class="s1">&amp;#39;com.samsung.health.heart_rate.start_time&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;com.samsung.health.heart_rate.time_offset&amp;#39;&lt;/span>&lt;span class="p">]]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">agg&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39; &amp;#39;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">axis&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">heart_rate&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_index&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;start_time_tz&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Load the binned data&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">glob&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">hr_bins&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">hr_simplified&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">heart_rate&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">heart_rate&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;com.samsung.health.heart_rate.binning_data&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isna&lt;/span>&lt;span class="p">()]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">columns&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;com.samsung.health.heart_rate.end_time&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;end_time&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;start_time_tz&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;start_time&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;com.samsung.health.heart_rate.heart_rate&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;heart_rate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;com.samsung.health.heart_rate.min&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;heart_rate_min&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;com.samsung.health.heart_rate.max&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;heart_rate_max&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">})[[&lt;/span>&lt;span class="s1">&amp;#39;start_time&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;end_time&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;heart_rate_min&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;heart_rate_max&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;heart_rate&amp;#39;&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">hr_bins&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hr_simplified&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">file&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">glob&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">glob&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">bin_file&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;com.samsung.shealth.tracker.heart_rate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">)):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">hr_bins&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read_json&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">file&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">heart_rate_full&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">concat&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hr_bins&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ul>
&lt;li>heart_rate contains just the summaries meaning either hourly buckets if you enable continuous heart rate monitoring or single samples if don&amp;rsquo;t&lt;/li>
&lt;li>heart_rate_full contains all available heart rate data at the lowest resolution available&lt;/li>
&lt;/ul>
&lt;p>Plot min, max, and average daily heart rate per day:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">heart_rate&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;day&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">heart_rate&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;start_time_tz&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">floor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;d&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">heart_rate&lt;/span> \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">.&lt;/span>&lt;span class="n">groupby&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;day&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">.&lt;/span>&lt;span class="n">agg&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="s1">&amp;#39;com.samsung.health.heart_rate.heart_rate&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;mean&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;min&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;max&amp;#39;&lt;/span>&lt;span class="p">]})&lt;/span> \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;a class="link" href="images/image-2.png" >&lt;img src="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/image-2.png"
width="713"
height="521"
srcset="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/image-2_hu_e4a5ea6014ab3c68.png 480w, https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/image-2_hu_86d77589e2ef6a08.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="136"
data-flex-basis="328px"
>&lt;/a>&lt;/p>
&lt;h4 id="weight">Weight&lt;/h4>
&lt;p>Tracking your weight is easy too. The weight is stored as kilograms. The code below concerts to pounds, but if you want to use kilograms remove the conversion line.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">weight&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">load_file&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;com.samsung.health.weight&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">date_cols&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;start_time&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">tz_col&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;time_offset&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">weight&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">weight&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_index&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;start_time&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sort_index&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Convert to pounds&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">weight&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;weight&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">weight&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;weight&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mf">2.20462&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">fig&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ax&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">subplots&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">figsize&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">12&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">sharex&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;all&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">weight&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">index&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">weight&lt;/span>&lt;span class="p">[[&lt;/span>&lt;span class="s1">&amp;#39;weight&amp;#39;&lt;/span>&lt;span class="p">]])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">weight&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">index&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">weight&lt;/span>&lt;span class="p">[[&lt;/span>&lt;span class="s1">&amp;#39;body_fat&amp;#39;&lt;/span>&lt;span class="p">]])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_xlabel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Time&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_ylabel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Weight&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_ylabel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Body Fat %&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">show&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>That gives us a plot similar to the below:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-1.png" >&lt;img src="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/image-1.png"
width="732"
height="469"
srcset="https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/image-1_hu_d1d50db2dd9cfd6f.png 480w, https://www.technowizardry.net/2022/02/plot-your-health-with-samsung-health-and-pandas/images/image-1_hu_4f2b2df62a2c3cf6.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="156"
data-flex-basis="374px"
>&lt;/a>&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>Samsung Health gives a great export of data that you can analyze. I&amp;rsquo;ve given examples of how to leverage a few of the files. In the future, I plan to share some other analytics that I&amp;rsquo;ve done as I&amp;rsquo;ve worked to bring data analytics to my personal health.&lt;/p>
&lt;p>If you&amp;rsquo;ve noticed any issues or have any other insights you&amp;rsquo;ve found, leave a comment below.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F02%2Fplot-your-health-with-samsung-health-and-pandas%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Plot+your+health+with+Samsung+Health+and+Pandas" style="border:0" alt="" /></description></item><item><title>Accurate, Local Home Energy Monitoring: Part 1 - Hardware</title><link>https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/</link><pubDate>Wed, 19 Jan 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/</guid><summary>&lt;p>Ever wondered where the energy is going in your house and know exactly when and which circuit is consuming the most electricity? How much is your air conditioning unit costing you each month in kWh?&lt;/p>
&lt;p>Home energy monitors are devices that you can use to monitor how much energy you&amp;rsquo;re using at any given point in time. You can use them to figure out how much each device or circuit you&amp;rsquo;re using overnight vs the day. If you have differing energy costs at the day vs night, you can use them to ensure devices run at lower cost time of day, you can use it to as part of a smart home automation to automatically notify you when your washing machine is done, or even identify when you need to upgrade a circuit because your server room is pulling too much.&lt;/p></summary><description>&lt;p>Ever wondered where the energy is going in your house and know exactly when and which circuit is consuming the most electricity? How much is your air conditioning unit costing you each month in kWh?&lt;/p>
&lt;p>Home energy monitors are devices that you can use to monitor how much energy you&amp;rsquo;re using at any given point in time. You can use them to figure out how much each device or circuit you&amp;rsquo;re using overnight vs the day. If you have differing energy costs at the day vs night, you can use them to ensure devices run at lower cost time of day, you can use it to as part of a smart home automation to automatically notify you when your washing machine is done, or even identify when you need to upgrade a circuit because your server room is pulling too much.&lt;/p>
&lt;p>In this post, I&amp;rsquo;m going to walk through the different products I considered for a project at a friend&amp;rsquo;s house, pros and cons, and how to order the appropriate equipment.&lt;/p>
&lt;h2 id="hardware-options">Hardware Options&lt;/h2>
&lt;p>Several different companies make solutions that monitor your energy usage. All of the ones I considered make use of CTs (current transformers) that clamp around the wire to sense the current flowing across that wire.&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="https://sense.com/" target="_blank" rel="noopener"
>Sense&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Sense provides two CTs that you attach to the two main lines into your circuit box. It does not monitor each circuit independently, but instead uses machine learning to try to identify what devices are running based on their signature.&lt;/p>
&lt;p>While installation is simpler with only two CTs, it&amp;rsquo;s not able to measure load of each circuit and say that you&amp;rsquo;re about to exceed a circuit limit. Additionally, using ML to identify appliances is not guaranteed to actually work. It may not work and some reviews have attested that it can&amp;rsquo;t detect all appliances. Ultimately, using ML to detect device types adds risk to this project that it wouldn&amp;rsquo;t provide useful data compared to circuit level monitoring.&lt;/p>
&lt;p>&lt;a class="link" href="images/sense.svg" >&lt;img src="https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/sense.svg"
loading="lazy"
>&lt;/a>&lt;/p>
&lt;p>Sense is cloud-based where all energy usage is sent to the cloud. You can view this on the web or use an API to pull data back into whatever software you want to use.&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="https://energycurb.com/" target="_blank" rel="noopener"
>Curb&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Instead of using ML, Curb allows you to install CTs on individual circuits which allows you to break down the energy usage accurately between each circuit and show the total across all circuits. Compared to Sense, this can have the advantage of not trying to use ML to identify the type of device through signature and instead directly measure usage of that circuit. Although, it&amp;rsquo;s possible that if two or more devices are on the same circuit, ML-based sensing could independently detect both devices.&lt;/p>
&lt;p>&lt;a class="link" href="images/curb-diagram.svg" >&lt;img src="https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/curb-diagram.svg"
loading="lazy"
>&lt;/a>&lt;/p>
&lt;p>Curb is also cloud-based like Sense and requires an API to consume energy usage data into your own system.&lt;/p>
&lt;p>Curb communicates with your network over power lines. It communicates with the HomePlug receiver over a single circuit, which then uses Ethernet to connect to the internet. This avoids the Wi-Fi requirement, but power line communications can be noisy when certain devices are on the same one.&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="https://www.brultech.com/greeneye/" target="_blank" rel="noopener"
>Brultech Systems - GreenEye Monitor&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;a class="link" href="images/gem.svg" >&lt;img src="https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/gem.svg"
loading="lazy"
>&lt;/a>&lt;/p>
&lt;p>The GEM is similar to the Curb since it allows per-circuit sensing, but the primary advantage is that it provides a purely local data collection and does not send data to the cloud. It can directly connected to HomeAssistant using the &lt;a class="link" href="https://www.home-assistant.io/integrations/greeneye_monitor/" target="_blank" rel="noopener"
>GEM integration&lt;/a>.&lt;/p>
&lt;h3 id="summary">Summary&lt;/h3>
&lt;table>
&lt;tbody>
&lt;tr>
&lt;td>&lt;/td>
&lt;td>Sense&lt;/td>
&lt;td>Curb&lt;/td>
&lt;td>GreenEye Monitor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Data access&lt;/td>
&lt;td>Cloud&lt;/td>
&lt;td>Cloud&lt;/td>
&lt;td>Local&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Per-circuit monitoring&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Per-device monitoring*&lt;br>(dependent on ability to detect)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>No&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Network Connectivity&lt;/td>
&lt;td>Wi-Fi&lt;/td>
&lt;td>Power line + Ethernet&lt;/td>
&lt;td>Ethernet or Wi-Fi&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Subjective user-friendliness&lt;/td>
&lt;td>Easiest&lt;/td>
&lt;td>Easier&lt;/td>
&lt;td>Harder&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HomeAssistant Integration&lt;/td>
&lt;td>&lt;a href="https://www.home-assistant.io/integrations/sense/">Cloud Pull&lt;/a>&lt;/td>
&lt;td>No native support&lt;/td>
&lt;td>&lt;a href="https://www.home-assistant.io/integrations/greeneye_monitor/">Local Push&lt;/a>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Each option had their own advantages and disadvantages, but I landed on the GreenEye Monitor for its local-only data access, support for individual circuit monitoring, and Ethernet. Avoiding cloud meant that the manufacturer could never stop offering, modify, or even charge for the API; this isn&amp;rsquo;t new in the world of smart homes when Wink started charging a monthly fee to use their hub.&lt;/p>
&lt;h3 id="purchasing-the-hardware">&lt;strong>Purchasing the Hardware&lt;/strong>&lt;/h3>
&lt;p>&lt;a class="link" href="images/breaker-box.jpg" >&lt;img src="https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/breaker-box-768x1024.jpg"
width="768"
height="1024"
srcset="https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/breaker-box-768x1024_hu_89d5eab93e938872.jpg 480w, https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/breaker-box-768x1024_hu_5d4e3d6c823a8edf.jpg 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="75"
data-flex-basis="180px"
>&lt;/a>&lt;/p>
&lt;p>This is the circuit breaker box that will be monitored. This work is being done as part of an electrical rewire, so at the same time the box will be upgraded and replaced, but we&amp;rsquo;ll use this as the baseline to help order the correct equipment.&lt;/p>
&lt;p>Ordering the CTs and correct packages from Brultech was challenging since they have multiple packages and different style of CTs.&lt;/p>
&lt;p>Count the number of circuits that exist right now:&lt;/p>
&lt;ul>
&lt;li>2 200amps - The breaker is labeled 200amp, thus each phase is 200amp&lt;/li>
&lt;li>4 40amps&lt;/li>
&lt;li>4 30amps&lt;/li>
&lt;li>16 20amps&lt;/li>
&lt;li>6 15amps&lt;/li>
&lt;li>Total: 32 circuits&lt;/li>
&lt;/ul>
&lt;p>CTs can be either donut style or split style. A split style CT can be clipped onto a cable without the cable being removed from the breaker, but a donut style requires you to unplug the cable from the breaker, run it through the circle, then plug it back in. The donuts are smaller than the splits, so to help reduce mess in the breaker box, I wanted to order as many donut CTs as possible.&lt;/p>
&lt;p>Since we&amp;rsquo;re upgrading the panel at the same time and didn&amp;rsquo;t have clear guidance from the electrician, we ended up ordering some extra CTs and arrived at the following set of CTs to accommodate expansion:&lt;/p>
&lt;ul>
&lt;li>2 200amps&lt;/li>
&lt;li>4 100amps&lt;/li>
&lt;li>6 40 amps&lt;/li>
&lt;li>5 30 amps&lt;/li>
&lt;li>25 20amps&lt;/li>
&lt;li>6 15 amps&lt;/li>
&lt;li>Total: 48 CTs&lt;/li>
&lt;/ul>
&lt;p>We didn&amp;rsquo;t order any DashBoxes because I was going to integrate the monitors directly into HomeAssistant and didn&amp;rsquo;t need any UIs on-top of the raw TCP stream. Thus I started with the &lt;a class="link" href="https://www.brultech.com/store/index.php?id_product=108&amp;amp;controller=product" target="_blank" rel="noopener"
>GreenEye Monitor Package&lt;/a> which gave two CT packages along with the box. Each package provides a different set of CTs, but the following were the ones I was interested in:&lt;/p>
&lt;ul>
&lt;li>Package B: 2 100amps, 6 donut 40s&lt;/li>
&lt;li>Package E: 5 split 60amps&lt;/li>
&lt;li>Package F: 7 split 30amps&lt;/li>
&lt;/ul>
&lt;p>We ordered two monitor packages with three package Fs, one package E, and one Package B along with some individual CTs and got the following CTs:&lt;/p>
&lt;p>Note that we separately ordered the 200amp CTs.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-1.png" >&lt;img src="https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/image-1.png"
width="626"
height="462"
srcset="https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/image-1_hu_d5b9a0b04454455a.png 480w, https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/image-1_hu_b4dbb511aa65dbc.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="135"
data-flex-basis="325px"
>&lt;/a>&lt;/p>
&lt;h2 id="installation">Installation&lt;/h2>
&lt;p>The installation guide for the GEM system monitor can be found on the Brultech website &lt;a class="link" href="https://www.brultech.com/software/files/getsoft/1/1" target="_blank" rel="noopener"
>here&lt;/a> (GEM Hardware Installation Manual GEM-MAN). Follow the directions to install the CTs correctly.&lt;/p>
&lt;p>&lt;a class="link" href="images/panel-monitoring-proposal.svg" >&lt;img src="https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/panel-monitoring-proposal.svg"
loading="lazy"
>&lt;/a>&lt;/p>
&lt;p>This is a high-level diagram showing how the different panels are electrically connected to each other.&lt;/p>
&lt;p>As part of the parallel electric rewire project, we ended up with three total electric panels in the house: a main panel, a sub panel for overflow located in the server room, and a sub panel located in the garage. For the first project phase, we only got two GEM monitors instead of three. One GEM monitor was for the main panel and one for the house sub panel.&lt;/p>
&lt;p>Even though we didn&amp;rsquo;t have a monitor specifically in the garage sub panel, we&amp;rsquo;d still be able to monitor total garage utilization from the main panel since a CT was attached to the breaker wire heading to the garage. Since we have a monitor for the house sub panel, we did not install CTs in the main panel for this circuit since it would be covered using CTs installed in the sub panel.&lt;/p>
&lt;p>&lt;strong>Important Notes for Installation&lt;/strong>&lt;/p>
&lt;p>&lt;strong>Safety Warning&lt;/strong>: Always turn off the circuit before installing a CT and make sure it&amp;rsquo;s attached to the GEM monitor or the wire leads are twisted together before turning on the circuit. CTs can produce energy flow in the wires and damage the CT. Some CTs from Brultech do include a &amp;ldquo;burden resistor&amp;rdquo; (e.g. all splits) where this isn&amp;rsquo;t a concern, but not all the mini CTs have it. We encountered an issue where one CT started buzzing because the leads were not twisted together.&lt;/p>
&lt;p>If possible, have an electric outlet installed near the GEM that connects to the same panel. This will make it easier to install because there are two separate wall adapters, one for power and one to measure the voltage.&lt;/p>
&lt;p>When installing each CT, make sure to label each wire with a number and note down: breaker number, breaker label, and type of CT (e.g. split-30, donut-40, etc.). The correct CT type needs to be programmed into the GEM monitor software. Without it, you&amp;rsquo;ll get incorrect current measurements.&lt;/p>
&lt;p>Split phase (e.g. your mains or any circuits with two breakers like for clothes dryers) require you to install the CTs into the same channel on the GEM, but depending on whether you have a type-A or type-B you need to wire them differently.&lt;/p>
&lt;blockquote>
&lt;p>Two types of current transformers are compatible with the GEM, Types A and B.&lt;/p>
&lt;p>TYPE A: SPLIT-60, SPLIT-100, SPLIT-200, SPLIT-400&lt;br>
TYPE B: Micro-40, Micro-80, Micro-100, SPLIT-170&lt;/p>
&lt;p>From the GEM installation manual&lt;/p>&lt;/blockquote>
&lt;p>Some install pictures&lt;/p>
&lt;p>&lt;a class="link" href="images/Breaker-Box-with-CTs.jpg" >&lt;img src="https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/Breaker-Box-with-CTs-468x1024.jpg"
width="468"
height="1024"
srcset="https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/Breaker-Box-with-CTs-468x1024_hu_5f4814a7994f7f8.jpg 480w, https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/Breaker-Box-with-CTs-468x1024_hu_83e984b94f8dd272.jpg 1024w"
loading="lazy"
alt="A photograph of the inside of the breaker box with CTs attached to each circuit wire."
class="gallery-image"
data-flex-grow="45"
data-flex-basis="109px"
>&lt;/a>&lt;/p>
&lt;p>Here&amp;rsquo;s a picture of the circuit breaker box with the CTs installed. Note on the left, we&amp;rsquo;re using donuts, but on the right, it&amp;rsquo;s the split CTs. The donut CTs are noticeably more compact. If you have the opportunity to use them, they&amp;rsquo;ll keep your box a lot cleaner.&lt;/p>
&lt;p>&lt;a class="link" href="images/gem-with-wires-installed.jpg" >&lt;img src="https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/gem-with-wires-installed-768x1024.jpg"
width="768"
height="1024"
srcset="https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/gem-with-wires-installed-768x1024_hu_63de0e4ebb728978.jpg 480w, https://www.technowizardry.net/2022/01/accurate-local-home-energy-monitoring-part-1/images/gem-with-wires-installed-768x1024_hu_19d3369b05f2876f.jpg 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="75"
data-flex-basis="180px"
>&lt;/a>&lt;/p>
&lt;p>Here&amp;rsquo;s a picture of one of the GEMs with the CTs connected. I&amp;rsquo;ve temporarily run an Ethernet cable directly to the port, but eventually I&amp;rsquo;ll install it more cleanly.&lt;/p>
&lt;p>Now everything should be functional. In the next post of this series, I&amp;rsquo;ll walk through the software configuration.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F01%2Faccurate-local-home-energy-monitoring-part-1%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Accurate%2C+Local+Home+Energy+Monitoring%3A+Part+1+-+Hardware" style="border:0" alt="" /></description></item><item><title>A Wireguard VPN from a home lab to Kubernetes cluster</title><link>https://www.technowizardry.net/2022/01/wireguard-vpn-to-dedicated-servers/</link><pubDate>Wed, 05 Jan 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/01/wireguard-vpn-to-dedicated-servers/</guid><summary>&lt;p>In addition to my home lab K8s cluster, I have two dedicated servers that I run in the cloud running a separate Kubernetes cluster. This cluster runs my production servers, like this blog, Postfix, DNS, etc. I wanted to add a VPN between my home network and my prod k8s network for two reasons:&lt;/p>
&lt;ol>
&lt;li>All data should be encrypted between these networks. While I use HTTPS when possible, some traffic like DNS isn&amp;rsquo;t encrypted&lt;/li>
&lt;li>My servers outside the NAT should be able to access servers running behind my NAT. I run a Prometheus instance at home that I want my primary Prometheus instance to be able to scrape. Using a VPN can help bypass the NAT and firewall on my router so it can scrape. Additionally, I wanted to be able to access pods directly from my home as needed.&lt;/li>
&lt;/ol>
&lt;p>I came across a number of guides for basic Wireguard VPN tunnel configurations which were fine, but they didn&amp;rsquo;t describe how to solve some of the more advanced issues like BGP routing for MetalLB or how to encrypt traffic to the host itself.&lt;/p></summary><description>&lt;p>In addition to my home lab K8s cluster, I have two dedicated servers that I run in the cloud running a separate Kubernetes cluster. This cluster runs my production servers, like this blog, Postfix, DNS, etc. I wanted to add a VPN between my home network and my prod k8s network for two reasons:&lt;/p>
&lt;ol>
&lt;li>All data should be encrypted between these networks. While I use HTTPS when possible, some traffic like DNS isn&amp;rsquo;t encrypted&lt;/li>
&lt;li>My servers outside the NAT should be able to access servers running behind my NAT. I run a Prometheus instance at home that I want my primary Prometheus instance to be able to scrape. Using a VPN can help bypass the NAT and firewall on my router so it can scrape. Additionally, I wanted to be able to access pods directly from my home as needed.&lt;/li>
&lt;/ol>
&lt;p>I came across a number of guides for basic Wireguard VPN tunnel configurations which were fine, but they didn&amp;rsquo;t describe how to solve some of the more advanced issues like BGP routing for MetalLB or how to encrypt traffic to the host itself.&lt;/p>
&lt;p>For example, since I have more than one host in my cluster, if I use MetalLB to announce an IP, the Wireguard instance on my router won&amp;rsquo;t know which host to forward traffic to because it uses the destination IP to pick the encryption key. This results in Wireguard sending traffic possibly to the wrong host.&lt;/p>
&lt;p>This blog post will explain everything you need to know to configure a Wireguard VPN that doesn&amp;rsquo;t suffer from these limitations.&lt;/p>
&lt;p>My current setup involves:&lt;/p>
&lt;ul>
&lt;li>2 dedicated servers running Kubernetes
&lt;ul>
&lt;li>Each server needs an extra IP address that&amp;rsquo;s not encrypted. IPv6 makes this super easy to assign a secondary IP address that is excluded.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>1 Ubiquiti EdgeRouter (Alternatives are acceptable provided they support Wireguard VPN and BGP for MetalLB)&lt;/li>
&lt;/ul>
&lt;p>&lt;a class="link" href="images/WireguardVPN-NoVPN.png" >&lt;img src="https://www.technowizardry.net/2022/01/wireguard-vpn-to-dedicated-servers/images/WireguardVPN-NoVPN.png"
width="496"
height="311"
srcset="https://www.technowizardry.net/2022/01/wireguard-vpn-to-dedicated-servers/images/WireguardVPN-NoVPN_hu_ff5e4ff753e2135d.png 480w, https://www.technowizardry.net/2022/01/wireguard-vpn-to-dedicated-servers/images/WireguardVPN-NoVPN_hu_bad1fa76bdbbfe1b.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="159"
data-flex-basis="382px"
>&lt;/a>&lt;/p>
&lt;p>I had two networks: my home network and the prod K8s network. Each K8s node was on a separate dedicated server and had different public IPs, but were part of a Kubernetes overlay network. All devices at IPv6 addresses too.&lt;/p>
&lt;p>Wireguard is pretty simple to setup. Each side gets a private key and a public key, you exchange the public keys between nodes, then state what IP addresses should be forward to that endpoint (AllowedIPs).&lt;/p>
&lt;p>Where do other guides fall short?&lt;/p>
&lt;p>&lt;strong>Circular Loops in Routing&lt;/strong>&lt;/p>
&lt;p>I wanted to protect traffic destined to the public IP address of any node (e.g. 192.99.38.172) and traffic to the K8s CIDR (10.43.0.0/16), but I couldn&amp;rsquo;t set the Wireguard endpoint to also be in the AllowedIPs because the router would try to route the encrypted Wireguard packets back to the Wireguard interface and fail.&lt;/p>
&lt;p>It may be possible to fix this issue using router VRFs (Virtual Routing and forwarding) which define separate routing tables for different interfaces. Unfortunately, my current router does not support this feature. If I upgrade, I may revisit this design.&lt;/p>
&lt;p>&lt;strong>Packet Routing&lt;/strong>&lt;/p>
&lt;p>Wireguard uses the destination IP of every packet to figure out which public key/endpoint it should be forward to. But say you&amp;rsquo;re using MetalLB in BGP mode to automatically provision Kubernetes Services in the subnet 192.168.10.0/24. I tried setting AllowedIPs=192.168.10.0/24 on both SRV4 and SRV5 and used MetalLB BGP to announce an IP address from the correct node to the router, but with only one Wireguard interface (wg0) on the router side, this didn&amp;rsquo;t work.&lt;/p>
&lt;p>&lt;a class="link" href="images/WireguardVPN-Before-1.png" >&lt;img src="https://www.technowizardry.net/2022/01/wireguard-vpn-to-dedicated-servers/images/WireguardVPN-Before-1.png"
width="610"
height="410"
srcset="https://www.technowizardry.net/2022/01/wireguard-vpn-to-dedicated-servers/images/WireguardVPN-Before-1_hu_8617aebbfb7397c4.png 480w, https://www.technowizardry.net/2022/01/wireguard-vpn-to-dedicated-servers/images/WireguardVPN-Before-1_hu_b466d04879e4ea62.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="148"
data-flex-basis="357px"
>&lt;/a>&lt;/p>
&lt;p>When I attempted to connect to a service IP, the router attempted to send the packet to the correct node following it&amp;rsquo;s own routing table (see the picture,) but Wireguard ignores that next hop IP address and instead consults the AllowedIPs configuration to figure out where to send it. In this case, both nodes listed this IP address, so Wireguard would send the traffic to the node that appears last in the configuration. If I was using &lt;em>externalTrafficPolicy: Local&lt;/em> on the service, then it could hit a node that doesn&amp;rsquo;t even know how to forward traffic and fail.&lt;/p>
&lt;h2 id="high-level-solution">High-level Solution&lt;/h2>
&lt;p>Lets look at my proposed high-level architecture. In this design, the router will get separate Wireguard interfaces for each node pair. Adding more WG interfaces allows us to have overlapping AllowedIPs on each node because the router will run multiple separate instances each with their own config. This enables the router to make its own decisions on which node to forward to before handing it off to Wireguard.&lt;/p>
&lt;p>The diagram below shows how this is setup. Each server has one WG interface and the router has multiple WG interfaces. Each interface needs to have a unique /32 IP address in a range. These IP addresses should be private and not overlap with any other IP addresses. We will add more addresses later.&lt;/p>
&lt;p>&lt;a class="link" href="images/Wireguard-ProposedDesign.png" >&lt;img src="https://www.technowizardry.net/2022/01/wireguard-vpn-to-dedicated-servers/images/Wireguard-ProposedDesign.png"
width="571"
height="415"
srcset="https://www.technowizardry.net/2022/01/wireguard-vpn-to-dedicated-servers/images/Wireguard-ProposedDesign_hu_3324d61625790d4b.png 480w, https://www.technowizardry.net/2022/01/wireguard-vpn-to-dedicated-servers/images/Wireguard-ProposedDesign_hu_2cb0ddd706c30936.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="137"
data-flex-basis="330px"
>&lt;/a>&lt;/p>
&lt;p>This strategy isn&amp;rsquo;t without its downsides. One challenge I faced with my specific router software is that there is an option to automatically add the AllowedIPs to the route table (&lt;em>route-allowed-ips&lt;/em>). This will conflict with IP addresses that are announced through the tunnel via BGP and we only want certain IPs to be added to the tunnel, specifically the tunnel private IPs (192.168.5.x) and the public IPs (e.g. 192.99.38.172.) A solution will be explained below.&lt;/p>
&lt;p>Additionally, each interface requires a separate UDP port to listen on.&lt;/p>
&lt;p>Note that if you also have Wireguard connections to other networks or devices, such as a phone, you can reuse one of the existing Wireguard VPN configurations since they won&amp;rsquo;t have any overlapping IP space. My mobile phone reuses wg0 instead of creating another interface.&lt;/p>
&lt;h2 id="configuration">Configuration&lt;/h2>
&lt;p>First step is to prepare the cluster-side software with Wireguard.&lt;/p>
&lt;h3 id="assign-a-secondary-ip-address-to-the-host">&lt;strong>Assign a secondary IP address to the host&lt;/strong>&lt;/h3>
&lt;p>Since I want to encrypt traffic destined to the public IP address of the nodes, I need to assign a secondary IP address to the interface that isn&amp;rsquo;t encrypted that the router can forward to.&lt;/p>
&lt;p>SRV4 already had two IP addresses: &lt;code>192.99.38.172&lt;/code> and `2607:5300:60:5fac::/64&lt;/p>
&lt;p>To temporarily configure a secondary IP address, I added another IPv6 address to the interface. This will get reset after a restart, but it&amp;rsquo;s useful for testing.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">sudo ip addr add 2607:5300:60:5fac::abcd/64 dev eno1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>In Systemd, I ensure the IP address is added upon reboot by editing the network file for this interface.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># networkctl status eno1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">● 2: eno1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Link File: n/a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Network File: /etc/systemd/network/50-default.network
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># vi /etc/systemd/network/50-default.network
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Match]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">MACAddress=00:ab:cd:ef:fe:dc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># Some parts elided
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Address]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Address=2607:5300:0060:5fac::/64
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Address=2607:5300:60:5fac::abcd/64 # Second IP address
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After this, you should have two IP addresses that can be used to access the server. Only the first IP address will be encrypted, so ensure that you use that when creating DNS records.&lt;/p>
&lt;h3 id="preventing-ip-fragmentation">Preventing IP fragmentation&lt;/h3>
&lt;p>Every network interface on all devices have a MTU (Maximum Transmission unit) which defines the maximum size of the IP packet that can be sent across that network. Most of the time when you&amp;rsquo;re traversing the internet, it&amp;rsquo;s 1500 bytes.&lt;/p>
&lt;p>However, Wireguard works by encapsulating IP packets within aUDP packet that is sent across the internet. A full 1500 byte packet will not fit within a UDP packet. Devices on the networks don&amp;rsquo;t know what the MTU is (unless you&amp;rsquo;re using IPv6 which has path MTU detection built-in&amp;ndash;another reason to upgrade to IPv6) and when they try to send a full sized packet, the router that&amp;rsquo;s running the Wireguard VPN will respond with an ICMP Needs Fragmentation packet to let the sender know a new MTU to use.&lt;/p>
&lt;p>Unfortunately in my monitoring, the computers didn&amp;rsquo;t remember this so they would continually try to send too large packets across Wireguard. This reduces performance since it has to keep trying.&lt;/p>
&lt;p>Here&amp;rsquo;s a packet capture showing a multiple computers hit this issue:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">IP 192.168.2.1 &amp;gt; 192.168.6.5: ICMP 192.168.5.4 unreachable - need to frag (mtu 1420), length 556
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IP 192.168.2.1 &amp;gt; 192.168.2.242: ICMP 192.168.5.4 unreachable - need to frag (mtu 1420), length 556
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IP 192.168.2.1 &amp;gt; 192.168.2.242: ICMP 192.168.5.4 unreachable - need to frag (mtu 1420), length 556
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IP 192.168.2.1 &amp;gt; 192.168.6.5: ICMP 192.168.5.4 unreachable - need to frag (mtu 1420), length 556
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IP 192.168.2.1 &amp;gt; 192.168.2.242: ICMP 192.168.5.4 unreachable - need to frag (mtu 1420), length 556
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IP 192.168.2.1 &amp;gt; 192.168.2.242: ICMP 192.168.5.4 unreachable - need to frag (mtu 1420), length 556
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>There&amp;rsquo;s a few ways to fix this problem. One way would be to modify the route table on every computer on the network so they don&amp;rsquo;t ever send too large packet. This solution ensures that all packets are correctly sized (including non TCP protocols like UDP), but that&amp;rsquo;s too much work.&lt;/p>
&lt;p>A common practice is to use TCP MSS (maximum segment size) clamping. Every time a TCP connection is setup, the SYN and SYN-ACK packets include the MSS size. This size dictates how large the TCP payload can be. The MSS is similar to the MTU, except just for the TCP payload.&lt;/p>
&lt;p>To understand, we need to look at what a packet looks like. The upstream internet connection starts at an MTU of 1500 Bytes&lt;/p>
&lt;ul>
&lt;li>20 bytes for an IPv4 header or 40 bytes for an IPv6 header&lt;/li>
&lt;li>40 bytes for &lt;a class="link" href="https://www.wireguard.com/protocol/" target="_blank" rel="noopener"
>Wireguard overhead&lt;/a>
&lt;ul>
&lt;li>8 bytes for UDP header&lt;/li>
&lt;li>28 bytes for Wireguard crypto&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>-&amp;ndash; Encrypted Payload &amp;mdash;&lt;/li>
&lt;li>20 bytes for IPv4 header or 40 bytes for an IPv6 header&lt;/li>
&lt;li>20 bytes for a TCP header (if you&amp;rsquo;re using TCP inside the connection)&lt;/li>
&lt;/ul>
&lt;p>Wireguard has a default MTU of 1420 which requires an MSS of IPv4:1380 and IPv6:1360. If you want to use the default MTU, feel free to skip directly to the &lt;a class="link" href="#MSSClampingConfig" >MSS clamping config step&lt;/a> and use these MSS clamping values. This value avoids mistakes from people incorrectly calculating their MTUs and encountering strange problems&lt;/p>
&lt;p>First we need to calculate the Wireguard MTU. Take the MTU of your uplink interface. Raw Ethernet is generally 1500 bytes, whereas PPPoE might be 1492 bytes, subtract the IP header (20 bytes if you&amp;rsquo;re using IPv4 as the peer address or 40 bytes if you&amp;rsquo;re using IPv6 as the peer as is the case in this example), then subtract 8 bytes for UDP.&lt;/p>
&lt;p>In my case the MTU is &lt;em>1500 bytes - 40 bytes - 40 bytes = 1420 bytes&lt;/em>.&lt;/p>
&lt;p>Now that we have the MTU, the MSS is MTU - 20 bytes for IPv4 - 20 bytes for the inner TCP header. &lt;em>1452 - 20 (IPv4) - 20 (TCP) = 1380 bytes&lt;/em> and &lt;em>1360 bytes&lt;/em> for IPv6 traffic.&lt;/p>
&lt;p>&lt;a class="link" href="images/WireguardVPN-MSSandMTU-1.png" >&lt;img src="https://www.technowizardry.net/2022/01/wireguard-vpn-to-dedicated-servers/images/WireguardVPN-MSSandMTU-1.png"
width="464"
height="481"
srcset="https://www.technowizardry.net/2022/01/wireguard-vpn-to-dedicated-servers/images/WireguardVPN-MSSandMTU-1_hu_31735bd5ac563ae.png 480w, https://www.technowizardry.net/2022/01/wireguard-vpn-to-dedicated-servers/images/WireguardVPN-MSSandMTU-1_hu_c38cc991cab2451d.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="96"
data-flex-basis="231px"
>&lt;/a>&lt;/p>
&lt;p>The router modifies the TCP SYN and SYN-ACK packet headers to clamp the MSS to be a max of 1432 bytes.&lt;/p>
&lt;p>Take note of all the values that you calculated above. You should have an MTU for the VPN, an MSS value for IPv4, and an MSS value for IPv6.&lt;/p>
&lt;h3 id="deploy-wireguard-config-files">Deploy Wireguard config files&lt;/h3>
&lt;p>I&amp;rsquo;m running multiple worker nodes in my Kubernetes cluster and want each node to run Wireguard, so I created a DaemonSet to deploy the Wireguard software. Each pod will start up using the host network so it can modify the host&amp;rsquo;s route table.&lt;/p>
&lt;p>In this configuration, I define separate files for each server. The script defined in startup.sh loads the config for the correct server and launches the server. There are some security issues with this approach since each server has private keys for all other services that I plan to fix in a future iteration.&lt;/p>
&lt;p>Each node gets its own private key since the remote node uses the public key to uniquely identify each server.&lt;/p>
&lt;p>Take note in the following example of the AllowedIPs.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">srv4.conf&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> [Interface]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Address = 192.168.5.4
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ListenPort = 51820
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> PrivateKey = [...]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> [Peer]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # peer1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> PublicKey = [...]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> AllowedIPs = 192.168.5.2/32, 192.168.2.0/24, 192.168.6.0/24, [ipv6]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">srv5.conf&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> [Interface]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Address = 192.168.5.1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ListenPort = 51820
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> PrivateKey = [...]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> [Peer]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> PublicKey = [...]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> AllowedIPs = 192.168.5.2/32, 192.168.2.0/24, 192.168.6.0/24&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">startup.sh&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> #!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> set -e
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> set -x
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> set -u
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> set -o pipefail
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> HOSTNAME=$(hostname --short)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cp /wg-config/$HOSTNAME.conf /etc/wireguard/wg0.conf
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> exec /bin/sh /etc/services.d/wireguard/run&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">wireguard&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vpn&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This DaemonSet is relatively simple, but the real magic is in the ConfigMap.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">DaemonSet&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">wireguard&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vpn&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workload.user.cattle.io/workloadselector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps.daemonset-vpn-wireguard2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workload.user.cattle.io/workloadselector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps.daemonset-vpn-wireguard2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">affinity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/wg-config/startup.sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/bin/bash&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TZ&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">America/New_York&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">linuxserver/wireguard:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">imagePullPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">IfNotPresent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">wireguard&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">100m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">128Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">100m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">128Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">securityContext&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">capabilities&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">add&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">NET_ADMIN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">SYS_MODULE&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">privileged&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/wg-config/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readOnly&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostNetwork&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">configMap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">defaultMode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">256&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">wireguard&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">updateStrategy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rollingUpdate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxSurge&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxUnavailable&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RollingUpdate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="router-side-configuration">Router-side Configuration&lt;/h2>
&lt;p>Router config may differ depending on what router model you have. I have the Ubiquiti EdgeRouter 12. Wireguard support can be used using the third-party package, &lt;a class="link" href="https://github.com/WireGuard/wireguard-vyatta-ubnt" target="_blank" rel="noopener"
>wireguard-vyatta-ubnt&lt;/a>. This package also supports UnifiOS product lines, however I have not tested this.&lt;/p>
&lt;p>Install the software package by following the &lt;a class="link" href="https://github.com/WireGuard/wireguard-vyatta-ubnt/wiki/EdgeOS-and-Unifi-Gateway#installation" target="_blank" rel="noopener"
>installation guide&lt;/a>.&lt;/p>
&lt;p>Then for each server node peer you have configure a Wireguard connection. Replace # with a number for each peer&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># Repeat for each pair
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">wg genkey | tee /config/auth/wg#.key | wg pubkey
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">configure
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set interfaces wireguard wg# address 192.168.5.#/32
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set interfaces wireguard wg# listen-port 5182#
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set interfaces wireguard wg# route-allowed-ips false
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set interfaces wireguard wg# peer GIPWDet2eswjz1JphYFb51sh6I+CwvzOoVyD7z7kZVc= endpoint example1.org:31820
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set interfaces wireguard wg# peer GIPWDet2eswjz1JphYFb51sh6I+CwvzOoVyD7z7kZVc= allowed-ips 192.168.5.1#/32
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set interfaces wireguard wg# private-key SOaiixdfppbXQK194IzG1IE2+M9MiyduY8tLCxG0kGY=
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set firewall name WAN\_LOCAL rule 2# action accept
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set firewall name WAN\_LOCAL rule 2# protocol udp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set firewall name WAN\_LOCAL rule 2# description &amp;#39;Wireguard #&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set firewall name WAN\_LOCAL rule 2# destination port 5182#
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set service ubnt-discover interface wg# disable
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h1 id="after-all-peers-are-configured">After all peers are configured&lt;/h1>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">commit
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">save
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">exit
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Don&amp;rsquo;t forget to create any firewall rules or chains that you need to protect traffic from the Kubernetes cluster into your home network. I limit inbound traffic to a few IP ranges.&lt;/p>
&lt;h3 id="routing-allowed-ips">Routing Allowed IPs&lt;/h3>
&lt;p>As mentioned earlier, one of the problems with this package is that it creates routes for CIDRs that are also handled by BGP causing conflicts. My strategy is not enable route-allowed-ips and instead configure routes explicitly. This ensures I can easily disable routes if my Wireguard VPN breaks and can exclude the MetalLB range.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">set protocols static interface-route **192.168.5.#**/32 next-hop-interface **wg#** description Wireguard-ServerA-Internal
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set protocols static interface-route **51.81.64.31**/32 next-hop-interface **wg#** description Wireguard-ServerA-Public
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="optional---forward-the-k8s-pod-ip-range">Optional - Forward the K8s Pod IP range&lt;/h3>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">set protocols static interface-route **10.42.#.0/24** next-hop-interface **wg#** description Wireguard-ServerA-Calico-Pod-Range
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="mss-clamping">MSS Clamping&lt;/h3>
&lt;p>MSS clamping is configured using firewall rules to modify every packet with the SYN flag set. In the EdgeRouter, the following config will enable MSS clamping.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">configure
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set firewall options mss-clamp interface-type wg
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set firewall options mss-clamp mss **1380**
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set firewall options mss-clamp6 interface-type wg
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set firewall options mss-clamp6 mss **1360**
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">commit
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">save
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">exit
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>At this point, your Wireguard VPN should be working. Leave a comment below if this worked or didn&amp;rsquo;t work for you or if you got it working on any other router types.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F01%2Fwireguard-vpn-to-dedicated-servers%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=A+Wireguard+VPN+from+a+home+lab+to+Kubernetes+cluster" style="border:0" alt="" /></description></item><item><title>CenturyLink Gigabit service on Mikrotik RouterOS with PPPoE and IPv6</title><link>https://www.technowizardry.net/2022/01/centurylink-pppoe-gigabit-service-on-mikrotik-routeros/</link><pubDate>Wed, 05 Jan 2022 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2022/01/centurylink-pppoe-gigabit-service-on-mikrotik-routeros/</guid><summary>&lt;p>I recently helped my friends configure their CenturyLink Gigabit fiber service so they can use their own hardware instead of the provided hardware. This gave them a lot of flexibility in how the network is configured, however CenturyLink requires you to enable PPPoE and use 6RD to use IPv6 instead of natively supporting IP packets, you have to jump through hoops. I&amp;rsquo;m sure there&amp;rsquo;s some reason why their network works like that, but I figured I&amp;rsquo;d document what needs to be done and explain how it works.&lt;/p></summary><description>&lt;p>I recently helped my friends configure their CenturyLink Gigabit fiber service so they can use their own hardware instead of the provided hardware. This gave them a lot of flexibility in how the network is configured, however CenturyLink requires you to enable PPPoE and use 6RD to use IPv6 instead of natively supporting IP packets, you have to jump through hoops. I&amp;rsquo;m sure there&amp;rsquo;s some reason why their network works like that, but I figured I&amp;rsquo;d document what needs to be done and explain how it works.&lt;/p>
&lt;h2 id="basic-internet-access-pppoe">Basic Internet Access (PPPoE)&lt;/h2>
&lt;p>First thing is to get basic IP access working. This will require PPPoE and VLAN configuration. In this example, the cable from the CPE (Customer Premise Equipment, may be called the ONT or network termination unit) will be plugged into the Mikrotik router using the SFP+ port. Don&amp;rsquo;t connect the internet Ethernet cable yet until you have NAT and DHCP configured on the other ports.&lt;/p>
&lt;p>The diagram below explains the three separate layers of interfaces involved. The lowest level is the SFP+ port which is the physical connection to the CPE. The next level interface tags all packets with the VLAN id 201. CenturyLink uses VLAN 201 to differentiate between internet traffic and any other service like television. The inner most layer is the PPPoE interface which provides authentication and authorization of your service. All Internet bound traffic gets forwarded to the pppoe-out1 interface which then causes it to be wrapped with a VLAN tag and forwarded out on the SFP port.&lt;/p>
&lt;p>&lt;a class="link" href="images/CenturyLink-PPPoE.png" >&lt;img src="https://www.technowizardry.net/2022/01/centurylink-pppoe-gigabit-service-on-mikrotik-routeros/images/CenturyLink-PPPoE.png"
width="725"
height="248"
srcset="https://www.technowizardry.net/2022/01/centurylink-pppoe-gigabit-service-on-mikrotik-routeros/images/CenturyLink-PPPoE_hu_b91257567b52def5.png 480w, https://www.technowizardry.net/2022/01/centurylink-pppoe-gigabit-service-on-mikrotik-routeros/images/CenturyLink-PPPoE_hu_67aa5495d624443b.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="292"
data-flex-basis="701px"
>&lt;/a>&lt;/p>
&lt;h3 id="getting-your-pppoe-credentials">Getting your PPPoE credentials&lt;/h3>
&lt;p>The first step is to retrieve the PPPoE credentials from the ISP provided router. This differs depending on what model you have, but a good strategy is to navigate to the WAN configuration page of that router and checking the source code or using the browser dev tools to see the password.&lt;/p>
&lt;h2 id="configuration">Configuration&lt;/h2>
&lt;p>Configuration is easy once you have the credentials&lt;/p>
&lt;p>First, enable strict IP reverse path filtering (&lt;a class="link" href="https://datatracker.ietf.org/doc/html/rfc3704" target="_blank" rel="noopener"
>RFC3704&lt;/a>). This isn&amp;rsquo;t mandatory, but it&amp;rsquo;s a good security practice to ensure that your network isn&amp;rsquo;t used in certain types of Distributed Denial of Service attacks (DDOS). All it does it check to make sure that packets are not being spoofed.&lt;/p>
&lt;p>/ip settings
set rp-filter=strict&lt;/p>
&lt;p>Next, create the VLAN&lt;/p>
&lt;p>/interface vlan
add interface=sfp-sfpplus1 loop-protect=off name=&amp;ldquo;clink-vlan&amp;rdquo; vlan-id=201&lt;/p>
&lt;p>Then configure PPPoE&lt;/p>
&lt;p>/interface pppoe-client
add add-default-route=yes disabled=no interface=&amp;ldquo;clink-vlan&amp;rdquo; name=clink-pppoe password={password} use-peer-dns=yes user={username}&lt;/p>
&lt;p>/interface list member
add interface=pppoe-out1 list=WAN&lt;/p>
&lt;p>Plug in the Ethernet cable to the correct port and check to see if your internet works.&lt;/p>
&lt;h2 id="ipv6-using-6rd">IPv6 using 6rd&lt;/h2>
&lt;p>Unfortunately CenturyLink doesn&amp;rsquo;t support native dual-stack IPv6 traffic and instead requires you to use IPv6 6rd. This isn&amp;rsquo;t as clean, but supporting IPv6 is important to enable the rest of the internet to transition to use IPv6 so I recommend enabling it. Otherwise we get into a chicken and egg problem.&lt;/p>
&lt;p>6rd works by assigning an IPv6 prefix for the IPv4 address that the ISP receives, then translates into an IPv4 packet and sends to your router. To calculate your 6rd prefix, use &lt;a class="link" href="https://dl.nspeed.app/6rdcalc.html" target="_blank" rel="noopener"
>this web tool&lt;/a>. CenturyLink uses the prefix: 2602::/24 (&lt;a class="link" href="https://www.centurylink.com/home/help/internet/modems-and-routers/advanced-setup/enable-ipv6.html" target="_blank" rel="noopener"
>doc link&lt;/a>) then enter your public IP address.&lt;/p>
&lt;p>For an example IP address:&lt;/p>
&lt;p>&lt;a class="link" href="images/image.png" >&lt;img src="https://www.technowizardry.net/2022/01/centurylink-pppoe-gigabit-service-on-mikrotik-routeros/images/image.png"
width="678"
height="290"
srcset="https://www.technowizardry.net/2022/01/centurylink-pppoe-gigabit-service-on-mikrotik-routeros/images/image_hu_cd80bf339fd06b5c.png 480w, https://www.technowizardry.net/2022/01/centurylink-pppoe-gigabit-service-on-mikrotik-routeros/images/image_hu_2a8ee08061908bc8.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="233"
data-flex-basis="561px"
>&lt;/a>&lt;/p>
&lt;p>CenturyLink exposes an IP address 205.171.2.64 as their 6rd server. This is the same regardless of your geographical location.&lt;/p>
&lt;p>This will create a 6to4 interface pointing to CenturyLink&amp;rsquo;s 6rd server&lt;/p>
&lt;p>/interface 6to4 add !keepalive mtu=1480 name=&amp;ldquo;clink-6rd&amp;rdquo; remote-address=205.171.2.64&lt;/p>
&lt;p>Then create default route pointing through this interface. For some reason the documentation uses 2000::/3 instead ::/0. I don&amp;rsquo;t happen to know why. If you know, leave a comment.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">/ipv6 route
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">add distance=1 dst-address=2000::/3 gateway=cl-6rd
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then give the route an IPv6 address in this prefix. Note how I added ::1 to the end. This is just a convention to separate between the prefix abcd::/64 and a host abcd::1/64 in that prefix. Additionally, advertise=yes is set on the bridge interface (the LAN side interface) to redistribute this prefix to all hosts, but not on the 6rd interface.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">/ipv6 address add interface=bridge advertise=yes address=**2602:7B:2D43:5900::1/64**
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/ipv6 address add interface=cl-6rd advertise=no address=**2602:7B:2D43:5900::1/64**
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="automating-it-wip">Automating it (WIP)&lt;/h2>
&lt;p>I originally set this up manually, but then I found out that CenturyLink will change your IPv4 address and it&amp;rsquo;ll break everything, so instead we need to automate this. I found a script from &lt;a class="link" href="https://forum.mikrotik.com/viewtopic.php?t=99030" target="_blank" rel="noopener"
>this forum post&lt;/a>, &lt;a class="link" href="https://forum.mikrotik.com/viewtopic.php?t=134621" target="_blank" rel="noopener"
>this one&lt;/a>, and &lt;a class="link" href="https://github.com/FwMotion/6rd-on-routeros" target="_blank" rel="noopener"
>this one&lt;/a>. However, I had several issues with them. The first one of them didn&amp;rsquo;t calculate the IP address correct when the IPv4 had a 0 octet (ex. 123.45.0.5) which happened. The second one didn&amp;rsquo;t calculate the 6RD address correctly either. The last one finally did work (after some simple fixes), but it still remains to be seen if it will in all situations.&lt;/p>
&lt;p>In the script below, I only had to modify the parameters to use the correct WAN and LAN interfaces. If you have multiple VLANs, make sure to add them to the &lt;code>ipv6interfaceLanArray&lt;/code> variable.&lt;/p>
&lt;p>Create a script with the following code, name it update-6rd-centurylink, and give it read and write permissions.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt"> 10
&lt;/span>&lt;span class="lnt"> 11
&lt;/span>&lt;span class="lnt"> 12
&lt;/span>&lt;span class="lnt"> 13
&lt;/span>&lt;span class="lnt"> 14
&lt;/span>&lt;span class="lnt"> 15
&lt;/span>&lt;span class="lnt"> 16
&lt;/span>&lt;span class="lnt"> 17
&lt;/span>&lt;span class="lnt"> 18
&lt;/span>&lt;span class="lnt"> 19
&lt;/span>&lt;span class="lnt"> 20
&lt;/span>&lt;span class="lnt"> 21
&lt;/span>&lt;span class="lnt"> 22
&lt;/span>&lt;span class="lnt"> 23
&lt;/span>&lt;span class="lnt"> 24
&lt;/span>&lt;span class="lnt"> 25
&lt;/span>&lt;span class="lnt"> 26
&lt;/span>&lt;span class="lnt"> 27
&lt;/span>&lt;span class="lnt"> 28
&lt;/span>&lt;span class="lnt"> 29
&lt;/span>&lt;span class="lnt"> 30
&lt;/span>&lt;span class="lnt"> 31
&lt;/span>&lt;span class="lnt"> 32
&lt;/span>&lt;span class="lnt"> 33
&lt;/span>&lt;span class="lnt"> 34
&lt;/span>&lt;span class="lnt"> 35
&lt;/span>&lt;span class="lnt"> 36
&lt;/span>&lt;span class="lnt"> 37
&lt;/span>&lt;span class="lnt"> 38
&lt;/span>&lt;span class="lnt"> 39
&lt;/span>&lt;span class="lnt"> 40
&lt;/span>&lt;span class="lnt"> 41
&lt;/span>&lt;span class="lnt"> 42
&lt;/span>&lt;span class="lnt"> 43
&lt;/span>&lt;span class="lnt"> 44
&lt;/span>&lt;span class="lnt"> 45
&lt;/span>&lt;span class="lnt"> 46
&lt;/span>&lt;span class="lnt"> 47
&lt;/span>&lt;span class="lnt"> 48
&lt;/span>&lt;span class="lnt"> 49
&lt;/span>&lt;span class="lnt"> 50
&lt;/span>&lt;span class="lnt"> 51
&lt;/span>&lt;span class="lnt"> 52
&lt;/span>&lt;span class="lnt"> 53
&lt;/span>&lt;span class="lnt"> 54
&lt;/span>&lt;span class="lnt"> 55
&lt;/span>&lt;span class="lnt"> 56
&lt;/span>&lt;span class="lnt"> 57
&lt;/span>&lt;span class="lnt"> 58
&lt;/span>&lt;span class="lnt"> 59
&lt;/span>&lt;span class="lnt"> 60
&lt;/span>&lt;span class="lnt"> 61
&lt;/span>&lt;span class="lnt"> 62
&lt;/span>&lt;span class="lnt"> 63
&lt;/span>&lt;span class="lnt"> 64
&lt;/span>&lt;span class="lnt"> 65
&lt;/span>&lt;span class="lnt"> 66
&lt;/span>&lt;span class="lnt"> 67
&lt;/span>&lt;span class="lnt"> 68
&lt;/span>&lt;span class="lnt"> 69
&lt;/span>&lt;span class="lnt"> 70
&lt;/span>&lt;span class="lnt"> 71
&lt;/span>&lt;span class="lnt"> 72
&lt;/span>&lt;span class="lnt"> 73
&lt;/span>&lt;span class="lnt"> 74
&lt;/span>&lt;span class="lnt"> 75
&lt;/span>&lt;span class="lnt"> 76
&lt;/span>&lt;span class="lnt"> 77
&lt;/span>&lt;span class="lnt"> 78
&lt;/span>&lt;span class="lnt"> 79
&lt;/span>&lt;span class="lnt"> 80
&lt;/span>&lt;span class="lnt"> 81
&lt;/span>&lt;span class="lnt"> 82
&lt;/span>&lt;span class="lnt"> 83
&lt;/span>&lt;span class="lnt"> 84
&lt;/span>&lt;span class="lnt"> 85
&lt;/span>&lt;span class="lnt"> 86
&lt;/span>&lt;span class="lnt"> 87
&lt;/span>&lt;span class="lnt"> 88
&lt;/span>&lt;span class="lnt"> 89
&lt;/span>&lt;span class="lnt"> 90
&lt;/span>&lt;span class="lnt"> 91
&lt;/span>&lt;span class="lnt"> 92
&lt;/span>&lt;span class="lnt"> 93
&lt;/span>&lt;span class="lnt"> 94
&lt;/span>&lt;span class="lnt"> 95
&lt;/span>&lt;span class="lnt"> 96
&lt;/span>&lt;span class="lnt"> 97
&lt;/span>&lt;span class="lnt"> 98
&lt;/span>&lt;span class="lnt"> 99
&lt;/span>&lt;span class="lnt">100
&lt;/span>&lt;span class="lnt">101
&lt;/span>&lt;span class="lnt">102
&lt;/span>&lt;span class="lnt">103
&lt;/span>&lt;span class="lnt">104
&lt;/span>&lt;span class="lnt">105
&lt;/span>&lt;span class="lnt">106
&lt;/span>&lt;span class="lnt">107
&lt;/span>&lt;span class="lnt">108
&lt;/span>&lt;span class="lnt">109
&lt;/span>&lt;span class="lnt">110
&lt;/span>&lt;span class="lnt">111
&lt;/span>&lt;span class="lnt">112
&lt;/span>&lt;span class="lnt">113
&lt;/span>&lt;span class="lnt">114
&lt;/span>&lt;span class="lnt">115
&lt;/span>&lt;span class="lnt">116
&lt;/span>&lt;span class="lnt">117
&lt;/span>&lt;span class="lnt">118
&lt;/span>&lt;span class="lnt">119
&lt;/span>&lt;span class="lnt">120
&lt;/span>&lt;span class="lnt">121
&lt;/span>&lt;span class="lnt">122
&lt;/span>&lt;span class="lnt">123
&lt;/span>&lt;span class="lnt">124
&lt;/span>&lt;span class="lnt">125
&lt;/span>&lt;span class="lnt">126
&lt;/span>&lt;span class="lnt">127
&lt;/span>&lt;span class="lnt">128
&lt;/span>&lt;span class="lnt">129
&lt;/span>&lt;span class="lnt">130
&lt;/span>&lt;span class="lnt">131
&lt;/span>&lt;span class="lnt">132
&lt;/span>&lt;span class="lnt">133
&lt;/span>&lt;span class="lnt">134
&lt;/span>&lt;span class="lnt">135
&lt;/span>&lt;span class="lnt">136
&lt;/span>&lt;span class="lnt">137
&lt;/span>&lt;span class="lnt">138
&lt;/span>&lt;span class="lnt">139
&lt;/span>&lt;span class="lnt">140
&lt;/span>&lt;span class="lnt">141
&lt;/span>&lt;span class="lnt">142
&lt;/span>&lt;span class="lnt">143
&lt;/span>&lt;span class="lnt">144
&lt;/span>&lt;span class="lnt">145
&lt;/span>&lt;span class="lnt">146
&lt;/span>&lt;span class="lnt">147
&lt;/span>&lt;span class="lnt">148
&lt;/span>&lt;span class="lnt">149
&lt;/span>&lt;span class="lnt">150
&lt;/span>&lt;span class="lnt">151
&lt;/span>&lt;span class="lnt">152
&lt;/span>&lt;span class="lnt">153
&lt;/span>&lt;span class="lnt">154
&lt;/span>&lt;span class="lnt">155
&lt;/span>&lt;span class="lnt">156
&lt;/span>&lt;span class="lnt">157
&lt;/span>&lt;span class="lnt">158
&lt;/span>&lt;span class="lnt">159
&lt;/span>&lt;span class="lnt">160
&lt;/span>&lt;span class="lnt">161
&lt;/span>&lt;span class="lnt">162
&lt;/span>&lt;span class="lnt">163
&lt;/span>&lt;span class="lnt">164
&lt;/span>&lt;span class="lnt">165
&lt;/span>&lt;span class="lnt">166
&lt;/span>&lt;span class="lnt">167
&lt;/span>&lt;span class="lnt">168
&lt;/span>&lt;span class="lnt">169
&lt;/span>&lt;span class="lnt">170
&lt;/span>&lt;span class="lnt">171
&lt;/span>&lt;span class="lnt">172
&lt;/span>&lt;span class="lnt">173
&lt;/span>&lt;span class="lnt">174
&lt;/span>&lt;span class="lnt">175
&lt;/span>&lt;span class="lnt">176
&lt;/span>&lt;span class="lnt">177
&lt;/span>&lt;span class="lnt">178
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># Configuration
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv4interface &amp;#34;pppoe-out1&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6interfaceWan &amp;#34;6rd&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6interfaceLanArray {&amp;#34;bridge&amp;#34;}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6addrcomment &amp;#34;6rd&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6gatewayDestination &amp;#34;2000::/3&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6prefix &amp;#34;2602:&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6prefixLen 24
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6pool &amp;#34;pool-6rd-centurylink&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6suffixLanPool &amp;#34;00::&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6suffixLanPoolDelta 8
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6suffixWan &amp;#34;00::1/64&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6addressLan &amp;#34;::1/64&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv4border &amp;#34;205.171.2.64&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6mtu 1472
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># Set up
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv4address [/ip address get [/ip address find interface=$ipv4interface] address]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:set ipv4address [:pick $ipv4address 0 [:find $ipv4address &amp;#34;/&amp;#34;]]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">if ($ipv4address=&amp;#34;&amp;#34;) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :error &amp;#34;Error getting IPv4 address&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># IPv6 6to4 Tunnel
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6tunnel [/interface 6to4 find where name=$ipv6interfaceWan]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:if ($ipv6tunnel=&amp;#34;&amp;#34;) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :log info &amp;#34;[6rd] Creating tunnel name=$ipv6interfaceWan&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :put &amp;#34;[6rd] Creating tunnel name=$ipv6interfaceWan&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> /interface 6to4 add name=$ipv6interfaceWan local-address=$ipv4address remote-address=$ipv4border mtu=$ipv6mtu !keepalive
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">} else={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local oldipv4address [/interface 6to4 get $ipv6tunnel local-address]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :if ($oldipv4address!=$ipv4address) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :log info &amp;#34;[6rd] Changing tunnel name=$ipv6interfaceWan from local-address=$oldipv4address to local-address=$ipv4address&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :put &amp;#34;[6rd] Changing tunnel name=$ipv6interfaceWan from local-address=$oldipv4address to local-address=$ipv4address&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> /interface 6to4 set $ipv6tunnel local-address=$ipv4address
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># IPv4 -&amp;gt; IPv6-style octet function
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local buildIPv4Octets do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local ipv4addr [:toip6 (&amp;#34;1::&amp;#34; . $ipv4address)]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :if ($ipv4addr=&amp;#34;&amp;#34;) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :error &amp;#34;Error converting IPv4 to IPv6 address&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local emptyOctet [pick &amp;#34;&amp;#34; 1]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local ipv4addrOctetsSetOne &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local ipv4index 3
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local ipv4Octet &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :for ipv4octetCountOne from=1 to=4 step=1 do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv4Octet [:pick $ipv4addr $ipv4index]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :if (($ipv4Octet=$emptyOctet) or ($ipv4Octet=&amp;#34;:&amp;#34;)) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv4addrOctetsSetOne (&amp;#34;0&amp;#34; . $ipv4addrOctetsSetOne)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> } else={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv4addrOctetsSetOne ($ipv4addrOctetsSetOne . $ipv4Octet)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv4index ($ipv4index + 1)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local ipv4addrOctetsSetTwo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv4index ($ipv4index + 1)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :for ipv4octetCountTwo from=1 to=4 step=1 do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv4Octet [:pick $ipv4addr $ipv4index]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :if (($ipv4Octet=$emptyOctet) or ($ipv4Octet=&amp;#34;:&amp;#34;)) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv4addrOctetsSetTwo (&amp;#34;0&amp;#34; . $ipv4addrOctetsSetTwo)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> } else={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv4addrOctetsSetTwo ($ipv4addrOctetsSetTwo . $ipv4Octet)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv4index ($ipv4index + 1)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :return ($ipv4addrOctetsSetOne . $ipv4addrOctetsSetTwo)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv4addressOctets [$buildIPv4Octets ipv4address=$ipv4address]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># IPv4 -&amp;gt; IPv6 prefix function
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local buildIPv6PrefixFromIPv4 do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local ipv6pre $ipv6prefix
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local ipv4index 0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local ipv6preHadNonZero true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local ipv4OctetToCopy &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local ipv4ShouldDoCopy false
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :for ipv6len from=$ipv6prefixLen to=($ipv6prefixLen + 28) step=4 do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :if (($ipv6len % 16)=0) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv6pre ($ipv6pre . &amp;#34;:&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv6preHadNonZero false
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv4OctetToCopy [:pick $ipv4addressOctets $ipv4index]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :if ($ipv4OctetToCopy=&amp;#34;0&amp;#34;) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :if ($ipv6preHadNonZero) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv4ShouldDoCopy true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> } else={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv4ShouldDoCopy false
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> } else={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv4ShouldDoCopy true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv6preHadNonZero true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :if ($ipv4ShouldDoCopy) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv6pre ($ipv6pre . $ipv4OctetToCopy)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv4index ($ipv4index + 1)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :return $ipv6pre
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6addressPrefix [$buildIPv6PrefixFromIPv4 ipv6prefix=$ipv6prefix ipv6prefixLen=$ipv6prefixLen ipv4addressOctets=$ipv4addressOctets]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># IPv6 address pool
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6poolPrefix ($ipv6addressPrefix . $ipv6suffixLanPool . &amp;#34;/&amp;#34; . ($ipv6prefixLen + 32))
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6poolPrefixLength ($ipv6prefixLen + 32 + $ipv6suffixLanPoolDelta)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6poolNumber [/ipv6 pool find where name=$ipv6pool]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6poolChanged false
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:if ($ipv6poolNumber=&amp;#34;&amp;#34;) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :log info &amp;#34;[6rd] Adding IPv6 pool name=$ipv6pool with prefix=$ipv6poolPrefix prefix-length=$ipv6poolPrefixLength&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :put &amp;#34;[6rd] Adding IPv6 pool name=$ipv6pool with prefix=$ipv6poolPrefix prefix-length=$ipv6poolPrefixLength&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> /ipv6 pool add name=$ipv6pool prefix=$ipv6poolPrefix prefix-length=$ipv6poolPrefixLength
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">} else={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local oldipv6poolPrefix [/ipv6 pool get $ipv6poolNumber prefix]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :if ($oldipv6poolPrefix!=$ipv6poolPrefix) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :set ipv6poolChanged true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :log info &amp;#34;[6rd] Removing IPv6 addresses prior to pool change; pool name=$ipv6pool&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :put &amp;#34;[6rd] Removing IPv6 addresses prior to pool change; pool name=$ipv6pool&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> /ipv6 address remove [/ipv6 address find where from-pool=$ipv6pool]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :log info &amp;#34;[6rd] Changing IPv6 pool name=$ipv6pool from prefix=$oldipv6poolPrefix to prefix=$ipv6poolPrefix prefix-length=$ipv6poolPrefixLength&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :put &amp;#34;[6rd] Changing IPv6 pool name=$ipv6pool from prefix=$oldipv6poolPrefix to prefix=$ipv6poolPrefix prefix-length=$ipv6poolPrefixLength&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> /ipv6 pool set $ipv6poolNumber prefix=$ipv6poolPrefix prefix-length=$ipv6poolPrefixLength
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># IPv6 address update function
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local changeIPv6Address do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local ipv6addr [/ipv6 address find where interface=$ipv6interface and comment=$ipv6comment]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :if ($ipv6addr=&amp;#34;&amp;#34;) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> /ipv6 address add interface=$ipv6interface comment=$ipv6comment address=$ipv6address advertise=$ipv6advertise from-pool=$ipv6pool
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local newipv6address [/ipv6 address get [/ipv6 address find where interface=$ipv6interface and comment=$ipv6comment] address]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :log info &amp;#34;[6rd] Created IPv6 address for interface=$ipv6interface with address=$newipv6address&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :put &amp;#34;[6rd] Created IPv6 address for interface=$ipv6interface with address=$newipv6address&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> } else={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :if ($ipv6poolChanged) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local oldipv6address [/ipv6 address get $ipv6addr address]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> /ipv6 address set $ipv6addr address=$ipv6address from-pool=$ipv6pool
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :local newipv6address [/ipv6 address get $ipv6addr address]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :if ($oldipv6address!=$newipv6address) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :log info &amp;#34;[6rd] Changed IPv6 address for interface=$ipv6interface from address=$oldipv6address to address=$newipv6address&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :put &amp;#34;[6rd] Changed IPv6 address for interface=$ipv6interface from address=$oldipv6address to address=$newipv6address&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># IPv6 addresses
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$changeIPv6Address ipv6interface=$ipv6interfaceWan ipv6comment=$ipv6addrcomment ipv6address=($ipv6addressPrefix . $ipv6suffixWan) \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ipv6advertise=no ipv6pool=$ipv6pool ipv6poolChanged=$ipv6poolChanged
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:foreach ipv6interfaceLan in=$ipv6interfaceLanArray do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> $changeIPv6Address ipv6interface=$ipv6interfaceLan ipv6comment=$ipv6addrcomment ipv6address=$ipv6addressLan \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ipv6advertise=yes ipv6pool=$ipv6pool ipv6poolChanged=$ipv6poolChanged
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># IPv6 gateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:local ipv6route [/ipv6 route find where dst-address=$ipv6gatewayDestination and gateway=$ipv6interfaceWan]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:if ($ipv6route=&amp;#34;&amp;#34;) do={
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :log info &amp;#34;[6rd] Adding route through 6rd gateway to dst-address=$ipv6gatewayDestination&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> :put &amp;#34;[6rd] Adding route through 6rd gateway to dst-address=$ipv6gatewayDestination&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> /ipv6 route add dst-address=$ipv6gatewayDestination gateway=$ipv6interfaceWan distance=1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Next, this script needs to be scheduled to run after the PPPoE IP address renews. Unfortunately, Mikrotik does not currently support running a script after the PPPoE connection updates (&lt;a class="link" href="https://forum.mikrotik.com/viewtopic.php?t=105660" target="_blank" rel="noopener"
>1&lt;/a>). Instead, I schedule this script to be run every 5 minutes to make sure the IPv6 connection stays working.&lt;/p>
&lt;p>Create it using the UI:&lt;/p>
&lt;ul>
&lt;li>System &amp;gt; Scheduler&lt;/li>
&lt;li>Click New&lt;/li>
&lt;li>Interval every 5 minutes&lt;/li>
&lt;li>Policy: read and write&lt;/li>
&lt;li>On Event: /system/script run script-6rd-centurylink&lt;/li>
&lt;/ul>
&lt;p>or the CLI:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">/system/scheduler add interval=5m name=6rd-update on-event=\
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;/system/script run script-6rd-centurylink&amp;#34; policy=read,write
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After that devices on your network should receive an IPv6 address. Test your internet connectivity at &lt;a class="link" href="https://ipv6-test.com/" target="_blank" rel="noopener"
>ipv6-test.com&lt;/a>.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2022%2F01%2Fcenturylink-pppoe-gigabit-service-on-mikrotik-routeros%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=CenturyLink+Gigabit+service+on+Mikrotik+RouterOS+with+PPPoE+and+IPv6" style="border:0" alt="" /></description></item><item><title>The one where Rancher ruined my birthday</title><link>https://www.technowizardry.net/2021/12/rancher-is-painful-to-use/</link><pubDate>Wed, 22 Dec 2021 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2021/12/rancher-is-painful-to-use/</guid><summary>&lt;p>Feb 2026 Update:&lt;/p>
&lt;p>This post is from 2021. Since then, I&amp;rsquo;ve made many changes to my cluster and Rancher has also made many changes to their product. Some better, some still challenging. I haven&amp;rsquo;t had to rebuild my cluster from scratch since then, which is a positive improvement. Now my issues are mostly due to self-hosted Kubernetes cluster + Longhorn PVC issues instead of Rancher. They deprecated RKE1 without an in-place migration plan, but I figured out how to &lt;a class="link" href="https://www.technowizardry.net/series/replatform-rke1-to-nix" >migrate to NixOS&lt;/a>. Every upgrade to Rancher fixes one issue, then add another. I stopped using Rancher Fleet because it was buggy and started using &lt;a class="link" href="https://www.technowizardry.net/2025/04/abandon-the-helm-leveraging-cdk-for-kubernetes/" >cdk8s+Helm&lt;/a>. While it had it&amp;rsquo;s own issues, I was able to more easily navigate them. Rancher v2.11 broke copy from the view YAML screen. v2.13 got rid of the combined Workloads screen which pulled in deployments, jobs, etc. because of supposed performance issues. I used that feature way too much. I&amp;rsquo;ve remained on v2.11.&lt;/p></summary><description>&lt;img src="https://www.technowizardry.net/2021/12/rancher-is-painful-to-use/images/header.svg" alt="Featured image of post The one where Rancher ruined my birthday" />&lt;p>Feb 2026 Update:&lt;/p>
&lt;p>This post is from 2021. Since then, I&amp;rsquo;ve made many changes to my cluster and Rancher has also made many changes to their product. Some better, some still challenging. I haven&amp;rsquo;t had to rebuild my cluster from scratch since then, which is a positive improvement. Now my issues are mostly due to self-hosted Kubernetes cluster + Longhorn PVC issues instead of Rancher. They deprecated RKE1 without an in-place migration plan, but I figured out how to &lt;a class="link" href="https://www.technowizardry.net/series/replatform-rke1-to-nix" >migrate to NixOS&lt;/a>. Every upgrade to Rancher fixes one issue, then add another. I stopped using Rancher Fleet because it was buggy and started using &lt;a class="link" href="https://www.technowizardry.net/2025/04/abandon-the-helm-leveraging-cdk-for-kubernetes/" >cdk8s+Helm&lt;/a>. While it had it&amp;rsquo;s own issues, I was able to more easily navigate them. Rancher v2.11 broke copy from the view YAML screen. v2.13 got rid of the combined Workloads screen which pulled in deployments, jobs, etc. because of supposed performance issues. I used that feature way too much. I&amp;rsquo;ve remained on v2.11.&lt;/p>
&lt;p>Ultimately, I&amp;rsquo;ve still used Rancher because I&amp;rsquo;m used to it, and overall it does the job I need to do better than anything else I&amp;rsquo;ve tried. The ability to view multiple clusters all in one UI is great.&lt;/p>
&lt;p>Other titles:&lt;/p>
&lt;ul>
&lt;li>You were supposed to bring balance to Kubernetes, Rancher, not destroy it&lt;/li>
&lt;li>et tu? Rancher?&lt;/li>
&lt;/ul>
&lt;p>I&amp;rsquo;ve been maintaining my own dedicated servers for around 7 years now as a way to learn and improve skills and have a place to run my different web sites, mail servers, even this blog. Over the years the hardware has changed and I&amp;rsquo;ve moved from hosting Rails applications directly on the OS to Docker and finally Kubernetes. I&amp;rsquo;ve learned a lot of skills that eventually helped me in my professional career at my job that it&amp;rsquo;s definitely been worth it, but maintaining this server has had its massive pain points where I&amp;rsquo;ve just had to walk away and leave stuff broken for days until I finally fix the issues.&lt;/p>
&lt;p>I selected &lt;a class="link" href="https://rancher.com/" target="_blank" rel="noopener"
>Rancher&lt;/a> several years ago (at least 3 or 4 years ago I&amp;rsquo;d estimate) when I finally moved to Kubernetes. I liked how it automatically provisioned my clusters, managed networking, and provided a nice UI. It was also reasonably recommended by the internet. Things worked reasonably well, but after adopting Rancher and Kubernetes, every 6-12 months I&amp;rsquo;d end up having something massively break and I&amp;rsquo;d have to rebuild the entire Kubernetes cluster painstakingly and many times I&amp;rsquo;d tell myself if it broke, then I&amp;rsquo;d just swear off Rancher entirely, but it never happened because I eventually got everything working.&lt;/p>
&lt;p>After upgrading to Rancher v2.6.3 that just launched yesterday and finding that all my clusters were removed from Rancher, I hit my breaking point.&lt;/p>
&lt;p>Maybe this was just because Rancher wasn&amp;rsquo;t designed for single node clusters, but from my experience working at my employer where we had much larger clusters, problems just become harder when you&amp;rsquo;re working with distributed systems.&lt;/p>
&lt;p>Maybe it was because I didn&amp;rsquo;t know what I was doing, but I consider myself to be somewhat knowledgeable and skilled in different aspects of software engineering. I&amp;rsquo;ve designed and developed software in big data processing pipelines, services, web applications, etc. I&amp;rsquo;ve worked in different paradigms. Does this give me sufficient credentials? Not really, but I feel confident saying this is not 100% operator error. If there&amp;rsquo;s any software that needs to focus on reliability and correctness, it&amp;rsquo;s orchestration software.&lt;/p>
&lt;p>Why do I claim Rancher is painful and unreliable?&lt;/p>
&lt;p>&lt;strong>Rancher v2.6.3 upgrade&lt;/strong>&lt;/p>
&lt;p>I upgraded from Rancher v2.6.2 to v2.6.3 that just launched. As soon as it came up I found that all my clusters were just missing. The clusters were still running, but Rancher had lost communication with them so I could no-longer manage them. Luckily I took an etcd backup right before, but apparently this was insufficient since Rancher made several other changes to /var/lib/rancher meaning that restoring this snapshot prevented the Rancher server container from coming up.&lt;/p>
&lt;p>If I recall it was an TLS certificate issue with etcd, but I only restored the snapshot, why did this break? I ended up having to go even farther back for a backup I had made of /var/lib/rancher from a previous time Rancher crashed, using that, then restoring the etcd snapshot which worked. Rancher does put out a &lt;a class="link" href="https://rancher.com/docs/rancher/v2.6/en/backups/docker-installs/docker-backups/" target="_blank" rel="noopener"
>guide&lt;/a> on backing up Rancher which I am planning on testing out for the next time.&lt;/p>
&lt;p>However the main problem was that Rancher completely lost all my clusters when upgrading a single 0.0.1 version bump. Maybe I was just unlucky and hit a rare edge case, but then again based on my experience, it doesn&amp;rsquo;t seem like it, and looking through the GitHub issues, I am not alone: &lt;a class="link" href="https://github.com/rancher/rancher/issues/35955" target="_blank" rel="noopener"
>1&lt;/a>, &lt;a class="link" href="https://github.com/rancher/rancher/issues/35956" target="_blank" rel="noopener"
>2&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Multiple Rancher cluster losses&lt;/strong>&lt;/p>
&lt;p>Over the years, I&amp;rsquo;ve had to rebuild the cluster from scratch maybe 3-4 times. Something will break (I unfortunately don&amp;rsquo;t recall the details,) and I&amp;rsquo;ll have to provision a brand new cluster and create all my applications again. And it&amp;rsquo;s always painful. Sometimes I&amp;rsquo;ll have the previous etcd snapshot folder that I can copy the Kubernetes YAML files out and directly reapply this, but this is extremely onerous.&lt;/p>
&lt;p>In fact, this happened right before my birthday one yeah and I had to rebuild everything from scratch.&lt;/p>
&lt;p>I enabled backups on my clusters too, but they don&amp;rsquo;t always help. My install is configured where Rancher has a local cluster and my main cluster. This is the standard single-node install. Unfortunately this means that if that management state gets lost and the cluster is broken, then you can&amp;rsquo;t restore a cluster from a backup, because the Rancher management install won&amp;rsquo;t know about this downstream cluster and you&amp;rsquo;re stuck.&lt;/p>
&lt;p>However, I haven&amp;rsquo;t experienced a full cluster outage in about 2 years looking at the age of this cluster. Maybe I got better at preventing this, but if it does happen again I&amp;rsquo;m not sure if the built in backup feature is going to be sufficient to recover from all failures if the cluster doesn&amp;rsquo;t boot.&lt;/p>
&lt;p>&lt;strong>Rancher + Helm = Sadness&lt;/strong> 😢&lt;/p>
&lt;p>I eventually upgraded to Rancher v2.6.0 from v2.5.x and I found several issues with Helm.&lt;/p>
&lt;p>Helm 2 support was completely removed. I&amp;rsquo;m not frustrated that it was removed, I know Helm 2 is old, but the helm 2to3 CLI didn&amp;rsquo;t work for me in Rancher. It never found the Tiller so I couldn&amp;rsquo;t upgrade anything. I ended up manually deleting and recreating a few Helm applications entirely which was intrusive, but I didn&amp;rsquo;t mind.&lt;/p>
&lt;p>But I was also running Longhorn. Longhorn is Rancher&amp;rsquo;s block storage CSI solution, which is useful when it works. Unfortunately, I found no Longhorn guidance on how to handle this, with even one Longhorn engineer saying &amp;ldquo;&lt;a class="link" href="https://github.com/longhorn/longhorn/issues/3424#issuecomment-998519218" target="_blank" rel="noopener"
>to use the old UI&lt;/a>&amp;rdquo;. This isn&amp;rsquo;t a good solution, is everybody supposed to continue switching back and forth forever? Is there a migration plan coming? Any guidance whatsoever?&lt;/p>
&lt;p>Ultimately, I looked around more and found I could modify the resources to let Helm 3 run overtop of the existing install and I got it working, but the lack of any solution for people that have been running Longhorn for a while (even keeping up to date with everything else) was frustrating.&lt;/p>
&lt;p>Separate from that, the entire Rancher Helm UI is frustrating and broken. In fact I have two separate Helm bugs: &lt;a class="link" href="https://github.com/rancher/rancher/issues/35236" target="_blank" rel="noopener"
>1&lt;/a> and &lt;a class="link" href="https://github.com/rancher/rancher/issues/35717" target="_blank" rel="noopener"
>2&lt;/a>.&lt;/p>
&lt;p>In Rancher v2.6.x, if you modify an array Helm value, Rancher will merge any upstream changes and completely corrupt the values. I reported &lt;a class="link" href="https://github.com/rancher/rancher/issues/35717" target="_blank" rel="noopener"
>this issue&lt;/a> in GitHub and separately in a Reddit thread where a Rancher employee responded, but after multiple back and forth responses, the Rancher employee didn&amp;rsquo;t seem to see that Rancher was responsible for this bug, not me. I explicitly did not want Rancher to merge arrays, but this was the UI&amp;rsquo;s fault. There&amp;rsquo;s no workaround in the UI if you hit this. I&amp;rsquo;ve hit this with external-dns, CoreDNS, Prometheus. The only solution is to either use the Helm CLI or remember to fix up the values every time through the UI.&lt;/p>
&lt;p>The Rancher Helm UI does not make it easy to know what values are changing or what values I&amp;rsquo;ve overridden, but in that communication with a Rancher employee they were confident just showing a toggle showing my values was sufficient. Users performing upgrades need a lot more context and I wanted to see:&lt;/p>
&lt;ol>
&lt;li>A diff between the upstream values from vPrev to vNext and present this in a UI for the user to review and decide&lt;/li>
&lt;li>Visually separate the overridden values from the upstream values so I can understand what I&amp;rsquo;ve actually changed&lt;/li>
&lt;li>Highlight conflicts between my overridden values and changes in the upstream values&lt;/li>
&lt;/ol>
&lt;p>Rancher doesn&amp;rsquo;t even show you the comments from the app&amp;rsquo;s values.yaml because they made the choice to store the values as JSON.&lt;/p>
&lt;p>There&amp;rsquo;s more issues too. If you try to upgrade a Helm application while it&amp;rsquo;s currently in an upgrade, what version of values does it use? Upgrades can get stuck with a failed Helm operation and Rancher will just reject the upgrade and you&amp;rsquo;ll lose all your value changes. If there&amp;rsquo;s a Rancher questions.yaml in the Chart (I love these because it makes it easy for myself to set values) and you have custom values, Rancher will just silently delete values and you&amp;rsquo;ll end up with a broken install.&lt;/p>
&lt;p>Right now Rancher Helm UI is basically just an upgrade and pray and hope everything works. They need to cohesively rethink the experience to provide me, the user, with the tools and the confidence that it&amp;rsquo;s going to work. I even gave some suggestions in &lt;a class="link" href="https://github.com/rancher/rancher/issues/35717#issuecomment-997344945" target="_blank" rel="noopener"
>my GitHub issue&lt;/a> and would love to give further feedback if Rancher wants it.&lt;/p>
&lt;p>&lt;strong>Rancher Pipeline Deprecation&lt;/strong>&lt;/p>
&lt;p>The Rancher 2.6.0 release deprecated Rancher Pipelines and instead implemented Rancher Fleets with Continuous Delivery feature. &lt;a class="link" href="https://rancher.com/docs/rancher/v2.5/en/pipelines/" target="_blank" rel="noopener"
>Their documentation states&lt;/a>: &amp;ldquo;&lt;em>Fleet does not replace Rancher pipelines; the distinction is that Rancher pipelines are now powered by Fleet&lt;/em>&amp;rdquo;, but &lt;a class="link" href="https://rancher.com/docs/rancher/v2.6/en/pipelines/" target="_blank" rel="noopener"
>it also says&lt;/a>: &amp;ldquo;&lt;em>Pipelines in Kubernetes 1.21+ are no longer supported&lt;/em>.&amp;rdquo; That seems contradictory.&lt;/p>
&lt;p>Am I allowed to use my pipelines that I created before or do I need to go somewhere else? I tried adding my repository to the Rancher Continuous Delivery UI and it did&amp;hellip;nothing. No feedback for several minutes until: &amp;ldquo;Request entity too large: limit is 3145728&amp;rdquo; What&amp;rsquo;s wrong? I don&amp;rsquo;t know. Is the pipelines YAML still supported or not?&lt;/p>
&lt;p>Rancher Pipelines was simple. My use case was just kick off a Docker build for a GitHub webhook, push to the Rancher registry, then deploy the latest version. But Rancher Fleet seems far more complicated.&lt;/p>
&lt;p>So instead, I went to GitHub actions which I didn&amp;rsquo;t like because now every simple repository needs to have GitHub secrets to push to my Docker Hub and a Kube Config to my cluster.&lt;/p>
&lt;p>&lt;strong>Update Jan-24-22:&lt;/strong> I have started experimenting with Rancher Fleets to see if it can win my heart again after extensively reading through the docs and fleet-examples repository. I managed to get it working successfully. Turns out I needed a separate pipeline just to contain the YAML files. However, it still doesn&amp;rsquo;t solve the same itch that Rancher Pipeline solved. It doesn&amp;rsquo;t seem to handle the Docker build aspect so now I had to start using GitHub actions which requires me to add K8s secrets to every repository. Less fun.&lt;/p>
&lt;p>Fleets give indecipherable error messages. For example, I didn&amp;rsquo;t add a dependency from one fleet.yaml to another. It failed, then I tried to add a dependsOn, but force updating the repo didn&amp;rsquo;t work. I had to delete it, then recreate the entire GitRepo. As soon as it&amp;rsquo;s in an error state, it doesn&amp;rsquo;t want to recover.&lt;/p>
&lt;p>&lt;strong>Longhorn Wonkiness&lt;/strong>&lt;/p>
&lt;p>In theory, Longhorn is great. It was simple to deploy and start using. I didn&amp;rsquo;t have to deploy anything like Gluster or custom NFS servers or anything. I loved it so I started using it, but then I&amp;rsquo;d encounter random issues.&lt;/p>
&lt;p>For example, I had a PVC deployed where I used subPaths to mount folders in a copy different places. This worked fine, until randomly after an upgrade of *something* all the subPaths were just empty. I&amp;rsquo;d mount the PVC in maintenance mode onto a host and take a look and I could directly see the different folders exactly as I expected with all the files, but then when Kubernetes mounts it into the pod, each folder was empty. I was specifically doing this so a temp folder in the pod didn&amp;rsquo;t get stored permanently. Instead, I had to mount the entire folder with no subPaths and it worked (less ideally.) Does this always happen? No. But I never found the root cause so I was forced to go with this fix.&lt;/p>
&lt;p>Backups and recurring jobs were changed in the latest version. Now they seem to be separate concepts. You can delete a recurring job, but that doesn&amp;rsquo;t warn you that backups are linked to that job and the backup doesn&amp;rsquo;t seem to run.&lt;/p>
&lt;p>I have a backup scheduled for every day, but looking at the history I see only backups from 4 days and 16 hours ago. Where are the backups in between that? I even have retain set to 6, so the last 6 backups should be available.&lt;/p>
&lt;p>Don&amp;rsquo;t even get me started when I tried to install Longhorn on my home lab K8s cluster and apparently it failed to install giving the ever helpful &amp;ldquo;failed to wait for roles to be populated&amp;rdquo; error message, then being in a bad state where I have to explicitly delete the longhorn-system namespace. Apparently my cluster was still provisioning even though I had been running this cluster for months and only restarted the master an hour earlier. Then when attempting to install the Helm template, I&amp;rsquo;d get prompted with an empty values tab and it&amp;rsquo;d break.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-2.png" >&lt;img src="https://www.technowizardry.net/2021/12/rancher-is-painful-to-use/images/image-2.png"
width="634"
height="336"
srcset="https://www.technowizardry.net/2021/12/rancher-is-painful-to-use/images/image-2_hu_6dc7ec9cb57ec33a.png 480w, https://www.technowizardry.net/2021/12/rancher-is-painful-to-use/images/image-2_hu_e1a1704dee79d7ec.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="188"
data-flex-basis="452px"
>&lt;/a>&lt;/p>
&lt;p>I had to dig around in the Kubernetes resources to manually cleanup all resources and somehow magically I got it working. The frustrating that there was minimal feedback to know why it failed.&lt;/p>
&lt;p>&lt;strong>How do services work?&lt;/strong>&lt;/p>
&lt;p>I do understand how to create services directly using YAML in Kubernetes and link Ingresses to them, but Rancher has a UI component that enables you to create ports for a deployment. This seems to create services sometimes? Sometimes I end up with two services that have the exact same name. This UI is confusing. Sometimes I delete a service directly, but then it just gets recreated with no feedback whatsoever.&lt;/p>
&lt;p>Selecting &amp;ldquo;Do not create a service&amp;rdquo; seems like it wouldn&amp;rsquo;t create a service, but if there&amp;rsquo;s already a service that exists (like an LB), then it changes to match that service. I get validation errors if I try to create two services with the same port, but then somehow the services end up getting created anyway. I should be able to create Headless services from here too. If there&amp;rsquo;s multiple services (both an LB and a ClusterIP/Headless service) then editing the LB service seems to edit both services, even though the Deployment page only lists the LB.&lt;/p>
&lt;p>Unfortunately, this UI component does not actually mean anything. Rancher seems to ignore it if services already exist, but a UI should not be surprising. It should tell me exactly what&amp;rsquo;s going to happen.&lt;/p>
&lt;p>Then there&amp;rsquo;s this annotation:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">field.cattle.io/publicEndpoints&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> \[{&amp;#34;addresses&amp;#34;:\[&amp;#34;192.168.6.0&amp;#34;\],&amp;#34;port&amp;#34;:80,&amp;#34;protocol&amp;#34;:&amp;#34;TCP&amp;#34;,&amp;#34;serviceName&amp;#34;:&amp;#34;smarthome:homeassistant-loadbalancer&amp;#34;,&amp;#34;allNodes&amp;#34;:false},{&amp;#34;addresses&amp;#34;:\[&amp;#34;192.168.6.8&amp;#34;\],&amp;#34;port&amp;#34;:443,&amp;#34;protocol&amp;#34;:&amp;#34;HTTPS&amp;#34;,&amp;#34;serviceName&amp;#34;:&amp;#34;smarthome:homeassistant&amp;#34;,&amp;#34;ingressName&amp;#34;:&amp;#34;smarthome:homeassistant&amp;#34;,&amp;#34;hostname&amp;#34;:&amp;#34;ha.home.example.com&amp;#34;,&amp;#34;path&amp;#34;:&amp;#34;/&amp;#34;,&amp;#34;allNodes&amp;#34;:true}\]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This annotation seems to control the ports that appear in the UI here:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-1.png" >&lt;img src="https://www.technowizardry.net/2021/12/rancher-is-painful-to-use/images/image-1.png"
width="136"
height="129"
srcset="https://www.technowizardry.net/2021/12/rancher-is-painful-to-use/images/image-1_hu_2d1333d1f5fcb5c3.png 480w, https://www.technowizardry.net/2021/12/rancher-is-painful-to-use/images/image-1_hu_85b4397f2e93cb79.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="105"
data-flex-basis="253px"
>&lt;/a>&lt;/p>
&lt;p>But this annotation also ended up preventing me from deleting an LB service until I modified this annotation. Additionally, these links frequently show the wrong URL. Even though there&amp;rsquo;s an ingress configured and that ingress appears in that annotation, it still only shows 192.168.6.0 and https://192.168.6.8.&lt;/p>
&lt;p>Deleting the ports block in the deployment and the annotation results in duplicate services:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-3.png" >&lt;img src="https://www.technowizardry.net/2021/12/rancher-is-painful-to-use/images/image-3-1024x194.png"
width="1024"
height="194"
srcset="https://www.technowizardry.net/2021/12/rancher-is-painful-to-use/images/image-3-1024x194_hu_628e34c8e5e3514.png 480w, https://www.technowizardry.net/2021/12/rancher-is-painful-to-use/images/image-3-1024x194_hu_1acff11c03c9c20f.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="527"
data-flex-basis="1266px"
>&lt;/a>&lt;/p>
&lt;p>There&amp;rsquo;s probably some simple bugs behind here, but they end up causing the user experience to be incredibly confusing. I don&amp;rsquo;t always have issues with&lt;/p>
&lt;p>&lt;strong>Rancher Role Change&lt;/strong>&lt;/p>
&lt;p>Due to some issues I encountered whenever I&amp;rsquo;d reboot my primary server and the cluster not coming back up without some manual intervention, I eventually decided to get a 3rd dedicated server and run etcd/control plane across all 3 nodes so the Kubernetes control plane could continue running while one machine was rebooted. I was previously running a node as just a worker since I know that etcd doesn&amp;rsquo;t work well with only two nodes (due to the lack of majority.)&lt;/p>
&lt;p>In another test cluster, I had a two etcd cluster (just for testing) and I stopped one of the VMs to test removal of that node, but RKE1 simply failed because the etcd on the remaining node would continually restart trying to connect. I had to take an etcd snapshot and restore it manually to recover it.&lt;/p>
&lt;p>Back to the main problem, since there&amp;rsquo;s &lt;a class="link" href="https://github.com/rancher/rancher/issues/22319" target="_blank" rel="noopener"
>no change node role option&lt;/a>, I removed the node from Rancher and went to re-provision it with all roles. It eventually came up, then a few minutes later I&amp;rsquo;d come back and find all Kubernetes containers were just deleted (etcd, kube-apiserver, kube-controller-manager.) I retried several times by deleting the node, then reprovisioned, each time it would work for a few minutes, then get deleted. Finally I figured out that there was a Job scheduled on that node to uninstall everything, but because the node was already gone, the job got ran the next time the node came up. But why is the job being scheduled if the node doesn&amp;rsquo;t exist in Kubernetes? If the node gets reprovisioned, should Rancher cancel a previous cleanup job?&lt;/p>
&lt;p>This was one is definitely an edge case, if it was just these kinds of issues, I would be more likely to ignore them, but considering the breadth of other issues I have with Rancher, it&amp;rsquo;s hard to overlook it.&lt;/p>
&lt;p>&lt;strong>Other Random Issues&lt;/strong>&lt;/p>
&lt;p>Sure, Rancher v2.6.x is brand new and I&amp;rsquo;m sure there&amp;rsquo;s going to be bugs, but some of them are easy to hit. Examples:&lt;/p>
&lt;ul>
&lt;li>If you&amp;rsquo;re filtering for a namespace, clicking Workloads will open the Apps view&lt;/li>
&lt;li>Clicking workloads sometimes doesn&amp;rsquo;t go to all workloads and instead shows the view you&amp;rsquo;re currently seeing (e.g. Jobs, Deployments, Stateful Sets)
&lt;ul>
&lt;li>I frequently experience missing left-nav links where I can&amp;rsquo;t click to view some resource without reloading the page&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Updating resources sometimes will give me an error (&lt;em>Cannot read properties of undefined (reading &amp;lsquo;management.cattle.io/ui-managed&amp;rsquo;&lt;/em>), but then it&amp;rsquo;ll successfully save&lt;/li>
&lt;li>I often hit conflicts when saving changes to workloads because it changed versions, but my changes don&amp;rsquo;t conflict and sometimes they&amp;rsquo;re trying to fix a boot loop issue, so instead I have to really quickly set replicas to 0, then make my change and scale up. It should be possible to make patch changes in the UI&lt;/li>
&lt;li>The client-side UI randomly caches data and I have to refresh it periodically to clear out state.&lt;/li>
&lt;li>Creating a Helm application used to let you auto create a namespace and default the release name to the app name, it doesn&amp;rsquo;t any more. Now I have to manually go create a namespace first.&lt;/li>
&lt;li>I can&amp;rsquo;t delete a K3s cluster that is turned off because there seems to be a finalizer on the resource that blocks until it can communicate with the cluster so I have to manually modify the resources to delete it.&lt;/li>
&lt;li>The Edit YAML view sometimes provides the schema for fields you haven&amp;rsquo;t provided. This can be useful, but there are weird bugs if you try to uncomment a value. Sometimes you can&amp;rsquo;t recomment it out, sometimes it leads to incorrect YAML schema.&lt;/li>
&lt;li>I just tried to delete a path from an Ingress, but upon saving, nothing happened (no error feedback). I had to manually edit the YAML to delete the path&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Summary&lt;/strong>&lt;/p>
&lt;p>Rancher is, in theory, pretty cool which is why I&amp;rsquo;ve continued to use it in face of so many issues. I understand that orchestration systems are very challenging to implement correctly and I want to recognize the hard work of those who worked on it, but ultimately I&amp;rsquo;m hitting a point where I can&amp;rsquo;t trust Rancher to safely manage my own cluster. And if I can&amp;rsquo;t trust it on a simple cluster, am I going to recommend that an employer uses this product?&lt;/p>
&lt;p>All through these events, I&amp;rsquo;ve had the opportunity to learn in depth how Kubernetes and Rancher both work, but is that what I wanted to spend my free-time on? Ehh not exactly. One cluster rebuild is enough.&lt;/p>
&lt;p>Filing GitHub issues felt fruitless since the only issues I filed were ignored or in the case of my reported Helm issue was able to get somebody to look at it, they misunderstood it and assumed I was wrong. Or the issues are hard to reproduce or hard to get a relevant log statement to demonstrate. I get that they have 2k current open issues.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2021%2F12%2Francher-is-painful-to-use%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=The+one+where+Rancher+ruined+my+birthday" style="border:0" alt="" /></description></item><item><title>Home Lab - Using the bridge CNI with Systemd</title><link>https://www.technowizardry.net/2021/12/home-lab-using-the-bridge-cni-with-systemd/</link><pubDate>Mon, 20 Dec 2021 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2021/12/home-lab-using-the-bridge-cni-with-systemd/</guid><summary>&lt;p>After I&amp;rsquo;ve had time to run my home lab for a while, I&amp;rsquo;ve started switching to a more up to date Linux distribution (instead of RancherOS.) I&amp;rsquo;m currently testing Ubuntu Server which leverages Systemd. Systemd-networkd is responsible for managing the network interface configuration and it differs in behavior compared to NetworkManager enough that we need to update the Home Lab Bridge CNI to handle it.&lt;/p>
&lt;p>Previously the CNI was creating a bridge network adapter when the first container started up, but this causes problems with systemd because resolved (the DNS resolver component) was eventually failing to make DNS queries and networkd was duplicating IP addresses on both eth0 (the actual uplink adapter) and on cni0 because we were copying it over.&lt;/p></summary><description>&lt;p>After I&amp;rsquo;ve had time to run my home lab for a while, I&amp;rsquo;ve started switching to a more up to date Linux distribution (instead of RancherOS.) I&amp;rsquo;m currently testing Ubuntu Server which leverages Systemd. Systemd-networkd is responsible for managing the network interface configuration and it differs in behavior compared to NetworkManager enough that we need to update the Home Lab Bridge CNI to handle it.&lt;/p>
&lt;p>Previously the CNI was creating a bridge network adapter when the first container started up, but this causes problems with systemd because resolved (the DNS resolver component) was eventually failing to make DNS queries and networkd was duplicating IP addresses on both eth0 (the actual uplink adapter) and on cni0 because we were copying it over.&lt;/p>
&lt;p>Prior to identifying this fix, I was experiencing issues where containers would work for a brief time, but then something would go wrong on the node host and it stopped being able to pull images and network traffic wasn&amp;rsquo;t routing between containers correctly.&lt;/p>
&lt;p>After some digging, it looked like systemd was trying to use eth0 even though it had become enslaved to the cni0 bridge and the cni0 bridge interface effectively replaced it. The resolvectl command showed that cni0 had no configuration and only eth0 was configured.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># resolvectl
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Global
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">resolv.conf mode: stub
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Link 2 (eth0)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Current Scopes: DNS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Protocols: +DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Current DNS Server: 2604:dead:beef:cafe::1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> DNS Servers: 192.168.2.1 2604:dead:beef:cafe::1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Link 3 (cni0)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Current Scopes: none
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Protocols: -DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">The networkctl command showed that eth0 and cni0 were both operational.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># networkctl
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IDX LINK TYPE OPERATIONAL SETUP
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 1 lo loopback carrier unmanaged
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 2 eth0 ether routable configured
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 3 cni0 bridge routable unmanaged
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 4 docker0 bridge no-carrier unmanaged
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># networkctl status eth0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">● 2: eth0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Link File: /usr/lib/systemd/network/99-default.link
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Network File: /run/systemd/network/10-netplan-eth0.network
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Type: ether
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> State: routable (configured)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> HW Address: 00:15:5d:02:26:00 (Microsoft Corporation)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>The Fix&lt;/strong>&lt;/p>
&lt;p>Instead of trying to create the bridge using my CNI, I instead changed cni0 to be created and managed by systemd. I created the following files.&lt;/p>
&lt;p>This file changes eth0 to be bridged to cni0 and disables DHCP on this interface.&lt;/p>
&lt;p>Note: This file name needs to be the same name as returned in networkctl status eth0 above, but is located in a different folder. Systemd uses this to know that the new file overrides the previous file.&lt;/p>
&lt;p>/run/systemd/network/&lt;strong>10-netplan-eth0.network&lt;/strong> -&amp;gt; /etc/systemd/network/&lt;strong>10-netplan-eth0.network&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># cat /etc/systemd/network/10-netplan-eth0.network
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Match]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Name=eth0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Network]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Bridge=cni0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This next file tells networkd that it needs to create the cni0 bridge. The MACAddress is the same MAC address as eth0 and I copied it from the networkctl status eth0 output. Interestingly, bridge interfaces usually take the MAC address of the first interface in the bridge (in this case, it should be eth0). Systemd should be doing the same thing as per &lt;a class="link" href="https://github.com/systemd/systemd/issues/12558" target="_blank" rel="noopener"
>this GitHub issue&lt;/a>, however this was not working for me, so I explicitly added this option to ensure it does.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># cat /etc/systemd/network/cni0.netdev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[NetDev]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Name=cni0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Kind=bridge
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">MACAddress=**00:15:5d:02:26:00**
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This final file tells networkd that it should enable DHCP and IPv6 autoconfig. If you need to disable IPv6, remove the IPv6AcceptRA line.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># cat /etc/systemd/network/cni0.network
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Match]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Name=cni0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Network]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">DHCP=yes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IPv6AcceptRA=yes # Enable IPv6 autoconfig
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then after rebooting, the host should come back up and should now work correctly. Networkctl should now show eth0 is enslaved to cni0.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ networkctl
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IDX LINK TYPE OPERATIONAL SETUP
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 1 lo loopback carrier unmanaged
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 2 eth0 ether enslaved configured
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 3 cni0 bridge routable configured
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 4 docker0 bridge no-carrier unmanaged
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>resolvectl should now show that cni0 is being used for DNS queries instead of eth0.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># resolvectl
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Global
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">resolv.conf mode: stub
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Link 2 (eth0)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Current Scopes: none
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Protocols: -DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Link 3 (cni0)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Current Scopes: DNS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Protocols: +DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Current DNS Server: 2604:dead:beef:cafe::1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> DNS Servers: 192.168.2.1 2604:dead:beef:cafe::1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2021%2F12%2Fhome-lab-using-the-bridge-cni-with-systemd%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Home+Lab+-+Using+the+bridge+CNI+with+Systemd" style="border:0" alt="" /></description></item><item><title>Upgrading Longhorn from Helm 2 in Rancher 2.6 the hard-way</title><link>https://www.technowizardry.net/2021/12/upgrading-longhorn-from-helm-2-in-rancher-2-6-the-hard-way/</link><pubDate>Wed, 15 Dec 2021 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2021/12/upgrading-longhorn-from-helm-2-in-rancher-2-6-the-hard-way/</guid><summary>&lt;p>Long ago, I installed Longhorn onto my Kubernetes cluster using Helm 2. Then eventually Helm 3 was released and helm 2to3 was made available. However, I was not able to use helm 2to3 for whatever reason because Rancher didn&amp;rsquo;t deploy Tiller in the way that this CLI expected. Additionally, Rancher did not provide an upgrade mechanism to handle this. Eventually Rancher 2.6 was released which entirely dropped Helm 2 support and I was stuck with a cluster where Longhorn was deployed, but not managed by a working Helm installation.&lt;/p></summary><description>&lt;p>Long ago, I installed Longhorn onto my Kubernetes cluster using Helm 2. Then eventually Helm 3 was released and helm 2to3 was made available. However, I was not able to use helm 2to3 for whatever reason because Rancher didn&amp;rsquo;t deploy Tiller in the way that this CLI expected. Additionally, Rancher did not provide an upgrade mechanism to handle this. Eventually Rancher 2.6 was released which entirely dropped Helm 2 support and I was stuck with a cluster where Longhorn was deployed, but not managed by a working Helm installation.&lt;/p>
&lt;p>This blog post outlines how you can recover Longhorn and upgrade it to Helm 3 without deleting all your volumes. This guide isn&amp;rsquo;t specific to Rancher 2.6.&lt;/p>
&lt;p>Any time I tried to install Longhorn using Helm, I was getting this error in Rancher. This is telling me that Kubernetes resources already exist, but are supposedly being managed by a different Helm install.&lt;/p>
&lt;p>Waiting for Kubernetes API to be available
helm upgrade &amp;ndash;install=true &amp;ndash;namespace=longhorn-system &amp;ndash;timeout=10m0s &amp;ndash;values=/home/shell/helm/values-longhorn-crd-100.1.0-up1.2.2.yaml &amp;ndash;version=100.1.0+up1.2.2 &amp;ndash;wait=true longhorn-crd /home/shell/helm/longhorn-crd-100.1.0-up1.2.2.tgz
Release &amp;ldquo;longhorn-crd&amp;rdquo; does not exist. Installing it now.
Error: rendered manifests contain a resource that already exists. Unable to continue with install: CustomResourceDefinition &amp;ldquo;engines.longhorn.io&amp;rdquo; in namespace &amp;quot;&amp;quot; exists and cannot be imported into the current release: invalid ownership metadata; annotation validation error: missing key &amp;ldquo;meta.helm.sh/release-name&amp;rdquo;: must be set to &amp;ldquo;longhorn-crd&amp;rdquo;; annotation validation error: missing key &amp;ldquo;meta.helm.sh/release-namespace&amp;rdquo;: must be set to &amp;ldquo;longhorn-system&amp;rdquo;&lt;/p>
&lt;p>To fix this issue, we must update every single resource to include the following annotations and labels:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="l">meta.helm.sh/release-name=longhorn-crd or longhorn&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="l">meta.helm.sh/release-namespace=longhorn-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="l">app.kubernetes.io/managed-by=Helm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>First, configure kubectl to be able to connect back to your cluster, then run the following commands to update all resources:&lt;/p>
&lt;p>Note &lt;code>KUBE_CONTEXT=prod&lt;/code>. Make sure you change this to match the name of the Kubernetes cluster. In Rancher, by default it&amp;rsquo;s the name of the cluster.&lt;/p>
&lt;p>The first step is to fix the CRDs (make sure to update your KUBE_CONTEXT to point to the correct cluster.)&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">KUBE_CONTEXT=default
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl --context=$KUBE_CONTEXT get crds -o name | grep longhorn | xargs -I % kubectl --context=$KUBE_CONTEXT label --overwrite -n longhorn-system % app.kubernetes.io/managed-by=Helm
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl --context=$KUBE_CONTEXT get crds -o name | grep longhorn | xargs -I % kubectl --context=$KUBE_CONTEXT annotate -n longhorn-system % meta.helm.sh/release-name=longhorn-crd
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl --context=$KUBE_CONTEXT get crds -o name | grep longhorn | xargs -I % kubectl --context=$KUBE_CONTEXT annotate -n longhorn-system % meta.helm.sh/release-namespace=longhorn-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After that you can fix the Longhorn application itself. If you&amp;rsquo;re upgrading from Longhorn 1.2.0 or earlier, you may already have the Longhorn Helm application deployed. If you do, then you should be able to skip the next step. If not because you&amp;rsquo;re coming from Helm v2 (like I was) then run the following:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">KUBE_CONTEXT=default
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl --context=$KUBE_CONTEXT get psp longhorn-psp -o name | xargs -I % kubectl --context=$KUBE_CONTEXT annotate % meta.helm.sh/release-name=longhorn
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl --context=$KUBE_CONTEXT get psp longhorn-psp -o name | xargs -I % kubectl --context=$KUBE_CONTEXT annotate % meta.helm.sh/release-namespace=longhorn-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl --context=$KUBE_CONTEXT get psp longhorn-psp -o name | xargs -I % kubectl --context=$KUBE_CONTEXT label --overwrite -n longhorn-system % app.kubernetes.io/managed-by=Helm
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl --context=$KUBE_CONTEXT -n longhorn-system get configMap,service,ds,deploy,serviceaccount,role,rolebinding -o name | xargs -I % kubectl --context=$KUBE_CONTEXT label --overwrite -n longhorn-system % app.kubernetes.io/managed-by=Helm
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl --context=$KUBE_CONTEXT -n longhorn-system get configMap,service,ds,deploy,serviceaccount,role,rolebinding -o name | xargs -I % kubectl --context=$KUBE_CONTEXT -n longhorn-system annotate % meta.helm.sh/release-name=longhorn
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl --context=$KUBE_CONTEXT -n longhorn-system get configMap,service,ds,deploy,serviceaccount,role,rolebinding -o name | xargs -I % kubectl --context=$KUBE_CONTEXT -n longhorn-system annotate % meta.helm.sh/release-namespace=longhorn-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl --context=$KUBE_CONTEXT get clusterrole,clusterrolebinding -o name | grep longhorn | xargs -I % kubectl --context=$KUBE_CONTEXT label --overwrite -n longhorn-system % app.kubernetes.io/managed-by=Helm
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl --context=$KUBE_CONTEXT get clusterrole,clusterrolebinding -o name | grep longhorn | xargs -I % kubectl --context=$KUBE_CONTEXT annotate % meta.helm.sh/release-namespace=longhorn-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl --context=$KUBE_CONTEXT get clusterrole,clusterrolebinding -o name | grep longhorn | xargs -I % kubectl --context=$KUBE_CONTEXT annotate % meta.helm.sh/release-name=longhorn
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After fixing all of these issues, use the Rancher UI to deploy the Longhorn application. Search for Longhorn in the Rancher repository.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-6.png" >&lt;img src="https://www.technowizardry.net/2021/12/upgrading-longhorn-from-helm-2-in-rancher-2-6-the-hard-way/images/image-6-1024x410.png"
width="1024"
height="410"
srcset="https://www.technowizardry.net/2021/12/upgrading-longhorn-from-helm-2-in-rancher-2-6-the-hard-way/images/image-6-1024x410_hu_e261ada7e624238c.png 480w, https://www.technowizardry.net/2021/12/upgrading-longhorn-from-helm-2-in-rancher-2-6-the-hard-way/images/image-6-1024x410_hu_d4b53dc5d83549c5.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="249"
data-flex-basis="599px"
>&lt;/a>&lt;/p>
&lt;p>Then click Install&lt;/p>
&lt;p>&lt;a class="link" href="images/image-7.png" >&lt;img src="https://www.technowizardry.net/2021/12/upgrading-longhorn-from-helm-2-in-rancher-2-6-the-hard-way/images/image-7.png"
width="967"
height="325"
srcset="https://www.technowizardry.net/2021/12/upgrading-longhorn-from-helm-2-in-rancher-2-6-the-hard-way/images/image-7_hu_69370941da058bb6.png 480w, https://www.technowizardry.net/2021/12/upgrading-longhorn-from-helm-2-in-rancher-2-6-the-hard-way/images/image-7_hu_3ad1c5b382cb6ea9.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="297"
data-flex-basis="714px"
>&lt;/a>&lt;/p>
&lt;p>Make any changes to the install project or settings, then click Install.&lt;/p>
&lt;p>If you&amp;rsquo;re lucky, everything should successfully deploy and you should now have Longhorn managed by Helm again.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">---------------------------------------------------------------------
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">SUCCESS: helm upgrade --history-max=5 --install=true --namespace=longhorn-system --timeout=10m0s --values=/home/shell/helm/values-longhorn-100.1.0-up1.2.2.yaml --version=100.1.0+up1.2.2 --wait=true longhorn /home/shell/helm/longhorn-100.1.0-up1.2.2.tgz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">---------------------------------------------------------------------
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2021%2F12%2Fupgrading-longhorn-from-helm-2-in-rancher-2-6-the-hard-way%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Upgrading+Longhorn+from+Helm+2+in+Rancher+2.6+the+hard-way" style="border:0" alt="" /></description></item><item><title>Why is Kubernetes opening random ports?</title><link>https://www.technowizardry.net/2021/12/why-is-kubernetes-opening-random-ports/</link><pubDate>Mon, 13 Dec 2021 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2021/12/why-is-kubernetes-opening-random-ports/</guid><summary>&lt;p>I recently responded to the Log4j vulnerability. If you&amp;rsquo;re not aware, Log4j is a very popular Java logging library used in many Java applications. There was a vulnerability where malicious actors could remotely take control of your computer by submitting a specially crafted request parameter that gets directly logged to log4j.&lt;/p>
&lt;p>This situation was not ideal since I was running several Java applications on my servers, thus I decided to use Nmap to port scan my dedicated server to see what ports were open. I ended up finding a number of ports I didn&amp;rsquo;t expect because several of Kubernetes Service instances were being mapped as node ports.&lt;/p></summary><description>&lt;p>I recently responded to the Log4j vulnerability. If you&amp;rsquo;re not aware, Log4j is a very popular Java logging library used in many Java applications. There was a vulnerability where malicious actors could remotely take control of your computer by submitting a specially crafted request parameter that gets directly logged to log4j.&lt;/p>
&lt;p>This situation was not ideal since I was running several Java applications on my servers, thus I decided to use Nmap to port scan my dedicated server to see what ports were open. I ended up finding a number of ports I didn&amp;rsquo;t expect because several of Kubernetes Service instances were being mapped as node ports.&lt;/p>
&lt;p>In this post, I outline the problem with Kubernete&amp;rsquo;s default strategy for services and how to avoid exposing ports that you don&amp;rsquo;t need.&lt;/p>
&lt;p>Using &lt;a class="link" href="https://nmap.org/" target="_blank" rel="noopener"
>Nmap&lt;/a>, I scanned all TCP ports on my own server using the following command. This command scanned TCP ports from 1-65535. Note: always have permission before scanning a target.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">nmap -p 1-65535 -T4 -A -v 192.168.5.1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This is also a great place to leverage &lt;a class="link" href="https://github.com/natlas/natlas" target="_blank" rel="noopener"
>natlas/natlas&lt;/a>, a project that a colleague (0xdade) and I have been working on. It provides an automated agent with a dashboard website to view port scan results.&lt;/p>
&lt;p>After a few minutes I got a list of open ports:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">[...]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 80/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 22/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 53/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 110/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 587/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 995/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 993/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 25/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 443/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 143/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 6443/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 31171/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 9120/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 32006/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 30941/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 10250/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 9100/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 30516/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 8081/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 10254/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 8181/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Discovered open port 30921/tcp on 192.168.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[...]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Many of these ports I expected, but some of the ports in the 30k-65k range, showed that they were exposing some internal applications. For example, a PowerDNS status page:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="mi">31171&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">tcp&lt;/span> &lt;span class="n">open&lt;/span> &lt;span class="n">unknown&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">fingerprint&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">strings&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">GenericLines&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">HTTP&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="mf">1.1&lt;/span> &lt;span class="mi">404&lt;/span> &lt;span class="n">Not&lt;/span> &lt;span class="n">Found&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">Connection&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">close&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">Content&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">Length&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">9&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">Content&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">Type&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">text&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">plain&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">charset&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">utf&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">8&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">Server&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">PowerDNS&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="mf">4.3&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">Found&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">GetRequest&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">HTTPOptions&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">HTTP&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="mf">1.1&lt;/span> &lt;span class="mi">200&lt;/span> &lt;span class="n">OK&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">Connection&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">close&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">Content&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">Length&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">21271&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">Content&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">Type&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">text&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">html&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">charset&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">utf&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">8&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">Server&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">PowerDNS&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="mf">4.3&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="o">&amp;lt;!&lt;/span>&lt;span class="n">DOCTYPE&lt;/span> &lt;span class="n">html&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">html&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;&lt;/span>&lt;span class="n">head&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="n">PowerDNS&lt;/span> &lt;span class="n">Authoritative&lt;/span> &lt;span class="n">Server&lt;/span> &lt;span class="n">Monitor&lt;/span>&lt;span class="o">&amp;lt;/&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">link&lt;/span> &lt;span class="n">rel&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;stylesheet&amp;#34;&lt;/span> &lt;span class="n">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;style.css&amp;#34;&lt;/span>&lt;span class="o">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="o">&amp;lt;/&lt;/span>&lt;span class="n">head&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;&lt;/span>&lt;span class="n">body&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">div&lt;/span> &lt;span class="k">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;row&amp;#34;&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">div&lt;/span> &lt;span class="k">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;headl columns&amp;#34;&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;&lt;/span>&lt;span class="n">a&lt;/span> &lt;span class="n">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/&amp;#34;&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;appname&amp;#34;&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="n">PowerDNS&lt;/span> &lt;span class="mf">4.3&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="o">&amp;lt;/&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="n">div&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">div&lt;/span> &lt;span class="k">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;headr columns&amp;#34;&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="n">div&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="n">div&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;&lt;/span>&lt;span class="n">div&lt;/span> &lt;span class="k">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;row&amp;#34;&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;&lt;/span>&lt;span class="n">div&lt;/span> &lt;span class="k">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;all columns&amp;#34;&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;&lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="n">Uptime&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">3.83&lt;/span> &lt;span class="n">days&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">br&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">Queries&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">second&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span> &lt;span class="n">minute&lt;/span> &lt;span class="n">averages&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">0.&lt;/span> &lt;span class="n">Max&lt;/span> &lt;span class="n">queries&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">second&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">br&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">Cache&lt;/span> &lt;span class="n">hitrate&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span> &lt;span class="n">minute&lt;/span> &lt;span class="n">averages&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.0&lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">0.0&lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">0.0&lt;/span>&lt;span class="o">%&amp;lt;&lt;/span>&lt;span class="n">br&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">Backend&lt;/span> &lt;span class="n">query&lt;/span> &lt;span class="n">cache&lt;/span> &lt;span class="n">hitrate&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span> &lt;span class="n">minute&lt;/span> &lt;span class="n">averages&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.0&lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">0.0&lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">1.3&lt;/span>&lt;span class="o">%&amp;lt;&lt;/span>&lt;span class="n">br&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">Backend&lt;/span> &lt;span class="n">query&lt;/span> &lt;span class="nb">load&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span> &lt;span class="n">minute&lt;/span> &lt;span class="n">averages&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">0.&lt;/span> &lt;span class="n">Max&lt;/span> &lt;span class="n">queries&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">second&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">br&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span> &lt;span class="n">Total&lt;/span> &lt;span class="n">queries&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">900.&lt;/span> &lt;span class="n">Question&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">answer&lt;/span> &lt;span class="n">latency&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">51.8&lt;/span>&lt;span class="n">ms&lt;/span>&lt;span class="o">&amp;lt;/&lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;&lt;/span>&lt;span class="n">br&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">|&lt;/span>&lt;span class="n">_&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">div&lt;/span> &lt;span class="k">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;panel&amp;#34;&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;&lt;/span>&lt;span class="n">span&lt;/span> &lt;span class="k">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">resetring&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;&lt;/span>&lt;span class="n">a&lt;/span> &lt;span class="n">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;?resetring=logmessages&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This matched a Kubernetes Service that looked like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">manager&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">controller&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operation&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Update&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">pdns-tcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">technowizardry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">clusterIP&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10.43.125.234&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">clusterIPs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="m">10.43.125.234&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">externalTrafficPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="cp">**healthCheckNodePort:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30516&lt;/span>&lt;span class="cp">**&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="cp">**nodePort:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30921&lt;/span>&lt;span class="cp">**&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="cp">**nodePort:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">31171&lt;/span>&lt;span class="cp">**&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8081&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8081&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workload.user.cattle.io/workloadselector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">daemonSet-technowizardry-powerdns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sessionAffinity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">None&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LoadBalancer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">status&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">loadBalancer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ip&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">192.168.10.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This service was created to privately expose a service over a Wireguard VPN, I didn&amp;rsquo;t want it to be exposed publicly. While write access was still protected by an API key, for security, I didn&amp;rsquo;t want to expose this.&lt;/p>
&lt;p>Unfortunately, while Kubernetes works on-premise, it was designed with the mind that you&amp;rsquo;d be running in the cloud and load balancers (like AWS ELB) need a TCP port on each host to forward traffic to, thus it was allocating node ports for everything by default. I was using MetalLB which allocates a new layer 3 IP address and didn&amp;rsquo;t need this.&lt;/p>
&lt;p>I used kubectl to then find all node ports exposed:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">kubectl get svc --all-namespaces -o go-template=&amp;#39;{{range $item :=.items}}{{range $item.spec.ports}}{{if .nodePort}}{{.nodePort}}/{{.protocol}} {{ $item.metadata.name }} {{ .name }}{{&amp;#34;n&amp;#34;}}{{en
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">d}}{{end}}{{end}}&amp;#39;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Digging around, I found the GitHub issue &lt;a class="link" href="https://github.com/kubernetes/kubernetes/issues/69845" target="_blank" rel="noopener"
>kubernetes/kubernetes#69845&lt;/a> which requested an option to disable allocating the node ports: &lt;code>spec.allocateLoadBalancerNodePorts=false&lt;/code>&lt;/p>
&lt;p>As of Kubernetes 1.20, this is currently in Alpha and can be enabled with a Kubernetes feature-gate. Alpha features can change before they become stable, thus be careful before setting this on a cluster that actually matters. If you&amp;rsquo;re using Rancher RKE1 to deploy your cluster, this is as easy as modifying your cluster:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">rancher_kubernetes_engine_config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kube-api&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extra_args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">feature-gates&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;ServiceLBNodePortControl=true&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kube-controller&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extra_args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">feature-gates&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;ServiceLBNodePortControl=true&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kubelet&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extra_args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">feature-gates&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;ServiceLBNodePortControl=true&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kubeproxy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extra_args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">feature-gates&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;ServiceLBNodePortControl=true&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">scheduler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extra_args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">feature-gates&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;ServiceLBNodePortControl=true&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After the cluster gets updated, you can now take advantage of this new setting. For each service, add the allocateLoadBalancerNodePorts: false and delete the nodePort values. This will cause Kubernetes to redeploy the service and remove the exposed ports.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">manager&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">controller&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operation&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Update&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">pdns-tcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">technowizardry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="cp">**allocateLoadBalancerNodePorts:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="cp">**&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">clusterIP&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10.43.125.234&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">clusterIPs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="m">10.43.125.234&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">externalTrafficPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">healthCheckNodePort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30516&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="cp">**nodePort:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30921&lt;/span>&lt;span class="cp">**&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="cp">**nodePort:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">31171&lt;/span>&lt;span class="cp">**&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8081&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8081&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workload.user.cattle.io/workloadselector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">daemonSet-technowizardry-powerdns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sessionAffinity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">None&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LoadBalancer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Unfortunately the healthCheckNodePort can&amp;rsquo;t be hidden in the same manner. The only way that I found to hide this is to set the global property &lt;code>--nodeport-addresses=127.0.0.1&lt;/code> on the kube-proxy, but I wouldn&amp;rsquo;t recommend that because it applies to all&lt;/p>
&lt;h1 id="updates">Updates&lt;/h1>
&lt;p>Oct 2023: I discovered how to use &lt;a class="link" href="https://kyverno.io/" target="_blank" rel="noopener"
>Kyverno&lt;/a> to auto fix this issue in my newer post &lt;a class="link" href="https://www.technowizardry.net/2023/10/auto-disable-kubernetes-service-lb-nodeports/" >here&lt;/a>.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2021%2F12%2Fwhy-is-kubernetes-opening-random-ports%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Why+is+Kubernetes+opening+random+ports%3F" style="border:0" alt="" /></description></item><item><title>Picking a mortgage for data engineers using Python</title><link>https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/</link><pubDate>Wed, 01 Dec 2021 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/</guid><summary>&lt;p>Over the past year I helped a few people pick mortgages while buying their homes by helping them visualize different mortgage options from different companies. In a seller&amp;rsquo;s market, like where I live, you only get a few days to pick from a number of different mortgages that all offer different fees, points, and interest rates that all influence the monthly rate that you pay.&lt;/p>
&lt;p>Given all this data, how do you compare the difference options and decide which one to go with? The lowest monthly rate isn&amp;rsquo;t always the best option.&lt;/p></summary><description>&lt;p>Over the past year I helped a few people pick mortgages while buying their homes by helping them visualize different mortgage options from different companies. In a seller&amp;rsquo;s market, like where I live, you only get a few days to pick from a number of different mortgages that all offer different fees, points, and interest rates that all influence the monthly rate that you pay.&lt;/p>
&lt;p>Given all this data, how do you compare the difference options and decide which one to go with? The lowest monthly rate isn&amp;rsquo;t always the best option.&lt;/p>
&lt;h2 id="a-brief-summary-of-mortgages">A brief summary of mortgages&lt;/h2>
&lt;p>Note this article will be focused on US mortgages in 2020. Other countries may have difference regulations, but hopefully the math should be reusable. Additionally, I am not a/your tax accountant, nor a lawyer, so this should be purely informational that you can use to reference.&lt;/p>
&lt;p>When you view a mortgage offering, you&amp;rsquo;ll generally get a table of different offerings that look something like:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-8.png" >&lt;img src="https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/image-8-1024x369.png"
width="1024"
height="369"
srcset="https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/image-8-1024x369_hu_d4bb527545aac27b.png 480w, https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/image-8-1024x369_hu_a72833ac8b7db40c.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="277"
data-flex-basis="666px"
>&lt;/a>&lt;/p>
&lt;p>An example mortgage rate table&lt;/p>
&lt;p>The estimated rate tells you the interest rate you&amp;rsquo;ll pay. The APR is the interest rate combined with the fees, but since fees are treated different than interest for tax purposes, we&amp;rsquo;ll separate them out. Points are money that you pay upfront to buy a lower interest rate. This point will visualize the differences later.&lt;/p>
&lt;p>The above table hides some of the data points we care about, but in the US all mortgage providers are supposed to provide you with a loan estimate document similar to below document. The CFPB/Consumer Financial Protection Bureau has &lt;a class="link" href="https://www.consumerfinance.gov/owning-a-home/loan-estimate/" target="_blank" rel="noopener"
>an explainer&lt;/a> on this document.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-7.png" >&lt;img src="https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/image-7.png"
width="625"
height="847"
srcset="https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/image-7_hu_41a11fc77d68cfa3.png 480w, https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/image-7_hu_c738e042357e197f.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="73"
data-flex-basis="177px"
>&lt;/a>&lt;/p>
&lt;p>When looking at this document, we care about the origination charges. I won&amp;rsquo;t include any escrow because I always exclude escrow from mortgages.&lt;/p>
&lt;h2 id="coding">Coding&lt;/h2>
&lt;p>For simplicity, I started by searching for financial related web pages explaining how to model finances using Python, Pandas, and matplotlib. I choose these tools because they were quite popular with data analytics and number crunching problems and I knew there would be libraries and examples available.&lt;/p>
&lt;p>I found &lt;a class="link" href="https://pbpython.com/amortization-model-revised.html" target="_blank" rel="noopener"
>this blog post&lt;/a> that I used as a baseline to start coding and created a Zeppelin notebook in my own Zeppelin instance.&lt;/p>
&lt;p>First, some basic Python imports&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">pandas&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">pd&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">datetime&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">date&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">numpy&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">np&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">collections&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">OrderedDict&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">dateutil.relativedelta&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="o">*&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">matplotlib.pyplot&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">plt&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">IPython.core.pylabtools&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">figsize&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">style&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">use&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;ggplot&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">matplotlib.ticker&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">ticker&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">money_formatter&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ticker&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">FormatStrFormatter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;$&lt;/span>&lt;span class="si">%1.0f&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Next, we need to define an amortization table function. Given the inputs, these functions will return a Pandas DataFrame that contains a monthly breakdown of the different payments and costs that we&amp;rsquo;ll build upon.&lt;/p>
&lt;p>Inputs:&lt;/p>
&lt;p>amortization_table&lt;/p>
&lt;table>&lt;tbody>&lt;tr>&lt;td>Parameter Name&lt;/td>&lt;td>Example&lt;/td>&lt;td>Description&lt;/td>&lt;/tr>&lt;tr>&lt;td>principal&lt;/td>&lt;td>500000&lt;/td>&lt;td>Amount of money for the loan (total house - down payment)&lt;/td>&lt;/tr>&lt;tr>&lt;td>interest_rate&lt;/td>&lt;td>0.03&lt;/td>&lt;td>0-1 representing the interest rate APY&lt;/td>&lt;/tr>&lt;tr>&lt;td>years&lt;/td>&lt;td>30&lt;/td>&lt;td>Number of years (e.g. 15 or 30)&lt;/td>&lt;/tr>&lt;tr>&lt;td>addl_principal&lt;/td>&lt;td>&lt;/td>&lt;td>&lt;/td>&lt;/tr>&lt;tr>&lt;td>annual_payments&lt;/td>&lt;td>12&lt;/td>&lt;td>Number of payments per year (usually 12)&lt;/td>&lt;/tr>&lt;tr>&lt;td>start_date&lt;/td>&lt;td>date(2022, 1, 1)&lt;/td>&lt;td>Represents the date when the loan will start. e.g (date.today())&lt;/td>&lt;/tr>&lt;tr>&lt;td>fees&lt;/td>&lt;td>999&lt;/td>&lt;td>Origination fees&lt;/td>&lt;/tr>&lt;tr>&lt;td>points&lt;/td>&lt;td>1000&lt;/td>&lt;td>&lt;/td>&lt;/tr>&lt;/tbody>&lt;/table>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;span class="lnt">68
&lt;/span>&lt;span class="lnt">69
&lt;/span>&lt;span class="lnt">70
&lt;/span>&lt;span class="lnt">71
&lt;/span>&lt;span class="lnt">72
&lt;/span>&lt;span class="lnt">73
&lt;/span>&lt;span class="lnt">74
&lt;/span>&lt;span class="lnt">75
&lt;/span>&lt;span class="lnt">76
&lt;/span>&lt;span class="lnt">77
&lt;/span>&lt;span class="lnt">78
&lt;/span>&lt;span class="lnt">79
&lt;/span>&lt;span class="lnt">80
&lt;/span>&lt;span class="lnt">81
&lt;/span>&lt;span class="lnt">82
&lt;/span>&lt;span class="lnt">83
&lt;/span>&lt;span class="lnt">84
&lt;/span>&lt;span class="lnt">85
&lt;/span>&lt;span class="lnt">86
&lt;/span>&lt;span class="lnt">87
&lt;/span>&lt;span class="lnt">88
&lt;/span>&lt;span class="lnt">89
&lt;/span>&lt;span class="lnt">90
&lt;/span>&lt;span class="lnt">91
&lt;/span>&lt;span class="lnt">92
&lt;/span>&lt;span class="lnt">93
&lt;/span>&lt;span class="lnt">94
&lt;/span>&lt;span class="lnt">95
&lt;/span>&lt;span class="lnt">96
&lt;/span>&lt;span class="lnt">97
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Original credit: https://pbpython.com/amortization-model-revised.html&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">amortize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">principal&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">interest_rate&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">years&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">pmt&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">addl_principal&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">start_date&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">date&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">today&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">annual_payments&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">12&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> Calculate the amortization schedule given the loan details.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :param principal: Amount borrowed
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :param interest_rate: The annual interest rate for this loan
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :param years: Number of years for the loan
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :param pmt: Payment amount per period
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :param addl_principal: Additional payments to be made each period.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :param start_date: Start date for the loan.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :param annual_payments: Number of payments in a year.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :return:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> schedule: Amortization schedule as an Ordered Dictionary
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># initialize the variables to keep track of the periods and running balances&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">p&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">beg_balance&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">principal&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">end_balance&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">principal&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">while&lt;/span> &lt;span class="n">end_balance&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Recalculate the interest based on the current balance&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">interest&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">round&lt;/span>&lt;span class="p">(((&lt;/span>&lt;span class="n">interest_rate&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">annual_payments&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">beg_balance&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Determine payment based on whether or not this period will pay off the loan&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pmt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">min&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pmt&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">beg_balance&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">interest&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">principal&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pmt&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">interest&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Ensure additional payment gets adjusted if the loan is being paid off&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">addl_principal&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">min&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">addl_principal&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">beg_balance&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">principal&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">end_balance&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">beg_balance&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">principal&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">addl_principal&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">yield&lt;/span> &lt;span class="n">OrderedDict&lt;/span>&lt;span class="p">([(&lt;/span>&lt;span class="s1">&amp;#39;Month&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="n">start_date&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Period&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Begin Balance&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">beg_balance&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Payment&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">pmt&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Principal&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">principal&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Interest&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">interest&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Additional_Payment&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">addl_principal&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;End Balance&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">end_balance&lt;/span>&lt;span class="p">)])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Increment the counter, balance and date&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">p&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">start_date&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">relativedelta&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">months&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">beg_balance&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">end_balance&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">monthly_payment&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">principal&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">years&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">interest_rate&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">annual_payments&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">12&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="nb">round&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pmt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">interest_rate&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">annual_payments&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">years&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">annual_payments&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">principal&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">amortization_table&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">principal&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">interest_rate&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">years&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">addl_principal&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">annual_payments&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">12&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">start_date&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">date&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">today&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">fees&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">points&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> Calculate the amortization schedule given the loan details as well as summary stats for the loan
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :param principal: Amount borrowed
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :param interest_rate: The annual interest rate for this loan
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :param years: Number of years for the loan
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :param annual_payments (optional): Number of payments in a year. Default 12.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :param addl_principal (optional): Additional payments to be made each period. Default 0.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :param start_date (optional): Start date. Default first of next month if none provided
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> :return:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> schedule: Amortization schedule as a pandas dataframe
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> summary: Pandas dataframe that summarizes the payoff information
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Payment stays constant based on the original terms of the loan&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">payment&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">monthly_payment&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">principal&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">years&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">interest_rate&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">annual_payments&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Generate the schedule and order the resulting columns for convenience&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">schedule&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">DataFrame&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">amortize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">principal&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">interest_rate&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">years&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">payment&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">addl_principal&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">start_date&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">annual_payments&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">schedule&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">schedule&lt;/span>&lt;span class="p">[[&lt;/span>&lt;span class="s2">&amp;#34;Period&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Month&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Begin Balance&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Payment&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Interest&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Principal&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Additional_Payment&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;End Balance&amp;#34;&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Convert to a datetime object to make subsequent calcs easier&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">schedule&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Month&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">to_datetime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">schedule&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Month&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">schedule&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Payment&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">iloc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">points&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">fees&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">schedule&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Interest&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">iloc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">points&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">schedule&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Total Payment&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">schedule&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Payment&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">schedule&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Additional_Payment&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">#schedule[&amp;#34;Total Payment&amp;#34;].iloc[0] += fees&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">#Create a summary statistics table&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">payoff_date&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">schedule&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Month&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">iloc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">stats&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Series&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="n">payoff_date&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">schedule&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Period&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">count&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">interest_rate&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">years&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">principal&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">payment&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">addl_principal&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">schedule&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Interest&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sum&lt;/span>&lt;span class="p">()],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Payoff Date&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Num Payments&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Interest Rate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Years&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Principal&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Payment&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Additional Payment&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Total Interest&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">schedule&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_index&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Month&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">inplace&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">drop&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">schedule&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">stats&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>With these functions, I can now create different visualizations.&lt;/p>
&lt;p>Let&amp;rsquo;s plot a per month how much I&amp;rsquo;m paying for interest vs principal.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">home_value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">500000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">down_payment&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">0.2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">interest&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">0.03&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">fig&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ax&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">subplots&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">yaxis&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_major_formatter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">money_formatter&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mortgage&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">amortization_table&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">home_value&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">down_payment&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">interest&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mortgage&lt;/span>&lt;span class="p">[[&lt;/span>&lt;span class="s1">&amp;#39;Principal&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Interest&amp;#39;&lt;/span>&lt;span class="p">]])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">yaxis&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_major_formatter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">money_formatter&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">legend&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;Principal&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Interest&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Amortization Table&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Running that gives a graph like below. In the beginning, I&amp;rsquo;m paying mostly towards interest vs principal. Classic banks.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-11.png" >&lt;img src="https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/image-11.png"
width="391"
height="252"
srcset="https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/image-11_hu_9c6678d50cd0cd94.png 480w, https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/image-11_hu_cadd3fcfea147cda.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="155"
data-flex-basis="372px"
>&lt;/a>&lt;/p>
&lt;p>An amortization table showing monthly payments towards interest or principal.&lt;/p>
&lt;h2 id="extra-payments">Extra Payments&lt;/h2>
&lt;p>Let&amp;rsquo;s try playing around with different payment scenarios. How does making extra payments affect the pay-off?&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">payment_table&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">home_value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">500000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">down_payment&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">0.20&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">interest&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">0.03&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">extra_pay&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1000&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">100&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mortgage&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">amortization_table&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">home_value&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">down_payment&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">interest&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">addl_principal&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">extra_pay&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">start_date&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">date&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2020&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mortgage&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;Extra Payment&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">extra_pay&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">payment_table&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mortgage&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">amounts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">fig&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ax&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">subplots&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">yaxis&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_major_formatter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">money_formatter&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="nb">type&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">payment_table&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">amounts&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;+$&lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">/mo&amp;#34;&lt;/span> &lt;span class="o">%&lt;/span> &lt;span class="nb">type&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Extra Payment&amp;#39;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">type&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Month&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nb">type&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;End Balance&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Extra payments pay balance faster&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">legend&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">amounts&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;a class="link" href="images/image-10.png" >&lt;img src="https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/image-10.png"
width="403"
height="265"
srcset="https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/image-10_hu_e8074f2290288262.png 480w, https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/image-10_hu_5ae7d0eebc81af36.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="152"
data-flex-basis="364px"
>&lt;/a>&lt;/p>
&lt;p>Clearly paying more money per month causes you to pay a mortgage faster, but let&amp;rsquo;s expand this to introduce the concept of opportunity cost.&lt;/p>
&lt;h2 id="opportunity-cost">Opportunity Cost&lt;/h2>
&lt;p>Opportunity cost is an economic concept that considers not just the cost of a good or service, but what is the cost that you lose out on by deciding not to buy an alternative good or service. For example, instead of spending $100 today on a good, you could invest it something that yields 5% real (inflation adjusted) and have $105 in one year. Your opportunity cost of that good is $105. The same concept can be applied to mortgages.&lt;/p>
&lt;p>A mortgage is usually a fixed rate over large number of years. At the time of this writing, they were about 3% for 30 years. If you put an additional $100 into the mortgage as an extra principal payment, yes that reduces your principal and interest paid by some small amount, but you earn a guaranteed 3%, no more, no less than that for that $100. Compared to checking and saving accounts at a paltry 0.1% - 0.5% interest rate, this is clearly better. But compared to the stock market which has returned an average of 10% per year, this is lower.&lt;/p>
&lt;p>However, it&amp;rsquo;s important to note that &lt;strong>you must compare the return rate combined with the risk of that investment&lt;/strong>. A stock can return -10% or +30% and still average to be +10% whereas a mortgage will always return the interest rate.&lt;/p>
&lt;p>To calculate the opportunity cost of an additional payment, I&amp;rsquo;m going to assume that an individual will either pay into the mortgage or put the money into a stock market yield 8% (I&amp;rsquo;ll chart different returns later.) I&amp;rsquo;m also going to adjust for the tax deduction of the interest payments.&lt;/p>
&lt;p>First, some simple utility code. Define the standard deduction which is $12,500 for 2020 and a function to normalize everything to 30 years (in case of comparing a 15 year to a 30 year mortgage).&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">standard_deduction&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">12500&lt;/span> &lt;span class="c1"># US Single Deduction 2020&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">extend_to_max&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">start_date&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">table&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">thirty_years&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">date_range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">start&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">start_date&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">periods&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">30&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">freq&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;MS&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">closed&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;left&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">table&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">table&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">reindex&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">thirty_years&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">table&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Month&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">thirty_years&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">table&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">fillna&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Next, another utility function that takes a Pandas Series stating how much money to put into the market per month, and returns a Series stating the value of the stock at the end of each month. This is useful to calculate the growth over time and allows me to define separate investment amounts depending on the month; for example, the first month has origination fees that don&amp;rsquo;t appear in later months.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">calculate_stock_value&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">investment&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">returns&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">output&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">prev&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">tick&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">investment&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">tick&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">prev&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">returns&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="p">)))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">output&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">prev&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">value&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">previous_gains&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Series&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">output&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">investment&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">index&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">investment&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">previous_gains&lt;/span> &lt;span class="c1"># The value of the money combined with the additional money that we added this month&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Next, the meat of this equation. First calculate the $ amount of deductions (property taxes + interest payments) adjusted by the tax bracket. Since these are deductible, we assume that we&amp;rsquo;ll get this extra month per month to invest.&lt;/p>
&lt;p>Then we can calculate how much each month we have to invest. total_bucket defines a set amount of money that could go either to stocks or to the mortgage. Out of the total bucket, we must pull out the monthly payment, then we adjust for the tax deduction, which gives some money back. That gives an equation like $3k - Principal - Interest + TaxDeduction(interest) * TaxBracket) = Remaining.&lt;/p>
&lt;p>The Remaining (in the investment variable) is compounded monthly based on the expected returns. The equity in the house (i.e. how much of the house you own) is the cumulative sum of the principal payments. That gives us a per-month net value calculation stating how much these two assets are theoretically worth.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">tax_bracket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">0.32&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">adjusted_networth&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">total_bucket&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">table&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">max_payment&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">property_taxes&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">returns&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">initial_invest&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">table&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Tax Deduction&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">table&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Interest&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">clip&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">lower&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">property_taxes&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">standard_deduction&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">tax_bracket&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">table&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AfterTaxes&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">table&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Total Payment&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">table&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Tax Deduction&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">table&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Investment&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">investment&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">total_bucket&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">table&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;AfterTaxes&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">clip&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">lower&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># Extra money available for mortgage or investment&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">table&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Investment&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">iloc&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">initial_invest&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">table&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;StockValue&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">calculate_stock_value&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">investment&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">returns&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># The value of the money combined with the additional money that we added this month&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">table&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;StockValue&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">table&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Equity&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># Equity in house combined with value of investment&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Let&amp;rsquo;s plot it out:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">loan_value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">500000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">monthly_total_bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">4000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">yr_property_taxes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">5000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">expected_mkt_returns&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">0.08&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">amounts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">fig&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ax&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">subplots&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">extra_pay&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1500&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mortgage&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">stats&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">amortization_table&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">loan_value&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">0.03&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">addl_principal&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">extra_pay&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">start_date&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">date&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">today&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">amounts&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;+$&lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">/mo&amp;#34;&lt;/span> &lt;span class="o">%&lt;/span> &lt;span class="n">extra_pay&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mortgage&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">extend_to_max&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">date&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">today&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">mortgage&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mortgage&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;NetWorth&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">adjusted_networth&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">monthly_total_bucket&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">mortgage&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">yr_property_taxes&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">expected_mkt_returns&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mortgage&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Month&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">mortgage&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;NetWorth&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">yaxis&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_major_formatter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">money_formatter&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Net worth&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">legend&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">amounts&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Running that gives us the following plot. So assuming, the market returns 8% on average, doing just the minimum payments and investing it elsewhere will theoretically give the best returns at the current mortgage interest rates.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-12.png" >&lt;img src="https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/image-12.png"
width="410"
height="265"
srcset="https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/image-12_hu_caff160c30530f2b.png 480w, https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/image-12_hu_7d4ae8d78e867a3c.png 1024w"
loading="lazy"
alt="Chart claiming that the opportunity cost of investing in a 3% mortgage vs an average 8% in the markets shows that extra payments lower the adjusted net worth."
class="gallery-image"
data-flex-grow="154"
data-flex-basis="371px"
>&lt;/a>&lt;/p>
&lt;p>Of course, past performance of the stock market does not mean that it will continue to yield that and personal risk tolerances and goals may lead you to make different choices.&lt;/p>
&lt;h2 id="comparing-mortgage-options">Comparing Mortgage Options&lt;/h2>
&lt;p>Now with some simple basics down, let&amp;rsquo;s compare a number of different loan options. First, let&amp;rsquo;s get all input parameters inputted. The different mortgages come from the different offerings that they all provide. Below is some sample interest rates with different years&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Multiple loan products&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">property_taxes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">5000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">start_date&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">date&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2021&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">11&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">home_value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">500000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">total_bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">4000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mortgages&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;down_pct&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.25&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;company&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;rate&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.0225&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;fees&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">614&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;years&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;points&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">11081&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;down_pct&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.25&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;company&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;rate&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.0275&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;fees&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">614&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;years&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;points&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mi">3609&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;down_pct&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;company&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;rate&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.02&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;fees&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">614&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;years&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">15&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;points&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then with that data, iterate over all combinations of mortgages along with different stock market values&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">output&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#fig, ax = plt.subplots()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">opportunity_return_options&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">arange&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">0.00&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">.15&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">0.05&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># Range of returns the stock market *could* give&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">fig&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">subplots&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">subplots&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">opportunity_return_options&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sharex&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sharey&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">figsize&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">legends&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">max_rate&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">max&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mortgages&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">lambda&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;points&amp;#34;&lt;/span>&lt;span class="p">])[&lt;/span>&lt;span class="s2">&amp;#34;rate&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">total_bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">monthly_payment&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">home_value&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mf">0.25&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">max_rate&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">stonk_market_returns&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">subplot&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">zip&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">opportunity_return_options&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">subplots&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mortgage_labels&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">subplot&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;If the stock market returns &lt;/span>&lt;span class="si">%s%%&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">%&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">stonk_market_returns&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">100&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">subplot&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">yaxis&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_major_formatter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">money_formatter&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">extra_payment&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">product&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">mortgages&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">loan_principal&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">home_value&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">product&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;down_pct&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">payment&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">monthly_payment&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">loan_principal&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">product&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;years&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">product&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;rate&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">table&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">amortization_table&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">loan_principal&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">product&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;rate&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">product&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;years&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">addl_principal&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">extra_payment&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">start_date&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">start_date&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">fees&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">product&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;fees&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">points&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">product&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;points&amp;#34;&lt;/span>&lt;span class="p">])[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">table&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">extend_to_max&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">start_date&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">table&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">table&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Equity&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">table&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Principal&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cumsum&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">home_value&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">product&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;down_pct&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">table&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;TotalWorth&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">adjusted_networth&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">total_bucket&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">table&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">property_taxes&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">stonk_market_returns&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># Equity in house combined with value of investment&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">time&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">table&lt;/span> &lt;span class="c1">#table[table[&amp;#39;Month&amp;#39;] &amp;lt; &amp;#39;2026-02-01&amp;#39;]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">avg_invest&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Investment&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mean&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">rate&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">product&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;rate&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">100&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mortgage_labels&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="si">%s%%&lt;/span>&lt;span class="s2"> for &lt;/span>&lt;span class="si">%d&lt;/span>&lt;span class="s2"> yrs final=$&lt;/span>&lt;span class="si">%d&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">%&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">product&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;company&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">rate&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">product&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;years&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;TotalWorth&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">max&lt;/span>&lt;span class="p">()))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">subplot&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">time&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Month&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;TotalWorth&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">subplot&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">legend&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mortgage_labels&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>That gives us a chart like below showing the relationship between the stock market returns, along with my net worth (purely example values.)&lt;/p>
&lt;p>&lt;a class="link" href="images/MortgageRatesOpportunityCost.png" >&lt;img src="https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/MortgageRatesOpportunityCost.png"
width="633"
height="592"
srcset="https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/MortgageRatesOpportunityCost_hu_833ce3afdc7da981.png 480w, https://www.technowizardry.net/2021/12/picking-a-mortgage-for-data-engineers-using-python/images/MortgageRatesOpportunityCost_hu_9c11412cd58a9384.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="106"
data-flex-basis="256px"
>&lt;/a>&lt;/p>
&lt;p>This is just the start what you can do when you model your finances in Pandas. Since it&amp;rsquo;s all in code, I can make it more advanced than simple web calculators that aren&amp;rsquo;t able to account for tax differences or opportunity costs. I even extended this to visualize a refinancing a mortgage.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2021%2F12%2Fpicking-a-mortgage-for-data-engineers-using-python%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Picking+a+mortgage+for+data+engineers+using+Python" style="border:0" alt="" /></description></item><item><title>Home Lab: Part 6 - Replacing MACvlan with a Bridge</title><link>https://www.technowizardry.net/2021/11/home-lab-replacing-macvlan-with-a-bridge/</link><pubDate>Wed, 24 Nov 2021 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2021/11/home-lab-replacing-macvlan-with-a-bridge/</guid><summary>&lt;p>In previous posts, I leveraged the MACvlan CNI to provide the networking to forward packets between containers and the rest of my network, however I ran into several issues rooted from the fact that MACvlan traffic bypasses several parts of the host&amp;rsquo;s IP stack including conntrack and IPTables. This conflicted with how Kubernetes expects to handle routing and meant we had to bypass and modify IPTables chains to get it to work.&lt;/p></summary><description>&lt;p>In previous posts, I leveraged the MACvlan CNI to provide the networking to forward packets between containers and the rest of my network, however I ran into several issues rooted from the fact that MACvlan traffic bypasses several parts of the host&amp;rsquo;s IP stack including conntrack and IPTables. This conflicted with how Kubernetes expects to handle routing and meant we had to bypass and modify IPTables chains to get it to work.&lt;/p>
&lt;p>While I got it to work, there was simply too much wire bending involved and I wanted to investigate alternatives to see if anything was able to fit my requirements better. Let&amp;rsquo;s consider the &lt;a class="link" href="https://www.cni.dev/plugins/current/main/bridge/" target="_blank" rel="noopener"
>bridge CNI&lt;/a>.&lt;/p>
&lt;p>To recap what we&amp;rsquo;re looking for in this CNI: we want to be able to run pods on the same subnet as my home LAN, this ultimately requires some kind of L2 layer bridge combined with a DHCP IPAM. Nothing pre-existing fully supports this situation. Thus I ended up modifying and extending existing CNIs.&lt;/p>
&lt;h2 id="bridge-cni">Bridge CNI&lt;/h2>
&lt;p>&lt;a class="link" href="images/HomeLab-BridgeCNI-First-1.png" >&lt;img src="https://www.technowizardry.net/2021/11/home-lab-replacing-macvlan-with-a-bridge/images/HomeLab-BridgeCNI-First-1.png"
width="304"
height="482"
srcset="https://www.technowizardry.net/2021/11/home-lab-replacing-macvlan-with-a-bridge/images/HomeLab-BridgeCNI-First-1_hu_626f3784fa8fb09d.png 480w, https://www.technowizardry.net/2021/11/home-lab-replacing-macvlan-with-a-bridge/images/HomeLab-BridgeCNI-First-1_hu_4eaaf18e51f53923.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="63"
data-flex-basis="151px"
>&lt;/a>&lt;/p>
&lt;p>The bridge CNI&amp;rsquo;s IP stack&lt;/p>
&lt;p>The Bridge stack is slightly different that the MACvlan stack. On the host side, we now have a point-to-point adapter (prefixed with veth*). These are added to the bridge and traffic from them can be routed between the adapters associated with the bridge.&lt;/p>
&lt;p>Starting with the reference bridge CNI along with the following configuration:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cniVersion&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.3.1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;dhcp-cni-network&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;plugins&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;bridge&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;mybridge&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ipam&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;dhcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Unfortunately, the reference bridge CNI gives us the following errors in the Kubelet log:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;#34;Error adding pod to network&amp;#34; err=&amp;#34;error calling DHCP.Allocate: no more tries&amp;#34; pod=&amp;#34;metallb/metallb-controller-7cb7dd579d-8zlgr&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The DHCP daemon isn&amp;rsquo;t receiving any responses from the DHCP server. While the daemon is configured to use the host network, it &lt;a class="link" href="https://github.com/ajacques/cni-plugins/blob/ce1dbf7f1302213d1efcfb27c987cc6f90c0a673/plugins/ipam/dhcp/lease.go#L87-L107" target="_blank" rel="noopener"
>assumes&lt;/a> the Pod&amp;rsquo;s network namespace while sending the DHCP request packets. Taking a look at the pod&amp;rsquo;s network namespace, I see that the requests are being sent, but no responses are received:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">[rancher@rancher ~]$ sudo docker run -ti --rm --net=container:e6d4baa7820f crccheck/tcpdump -i any
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IP 0.0.0.0.68 &amp;gt; 255.255.255.255.67: BOOTP/DHCP, Request from aa:9c:3e:ae:d1:68 (oui Unknown), length 336
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IP 0.0.0.0.68 &amp;gt; 255.255.255.255.67: BOOTP/DHCP, equest from c2:be:1c:d3:d4:ba (oui Unknown), length 336
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Looking at the host&amp;rsquo;s network adapter, we can see that they&amp;rsquo;re making it to the bridge, but not being sent outwards on eth0, thus the rest of the network won&amp;rsquo;t hear anything.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">[rancher@rancher ~]$ sudo docker run -ti --rm --net=host --cap-add NET_ADMIN crccheck/tcpdump -i any -f &amp;#39;udp port 67 or udp port 68&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IP 0.0.0.0.68 &amp;gt; 255.255.255.255.67: BOOTP/DHCP, Request from 82:a0:15:d9:51:02 (oui Unknown), length 336
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IP 0.0.0.0.68 &amp;gt; 255.255.255.255.67: BOOTP/DHCP, Request from 82:a0:15:d9:51:02 (oui Unknown), length 336
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Additionally, there are absolutely no routes in the Pod&amp;rsquo;s netns, thus nothing will ever work because it has no idea where to send packets. Luckily broadcast packets don&amp;rsquo;t need to be routed, so they somehow manage to get to host&amp;rsquo;s bridge adapter.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">[rancher@rancher ~]$ sudo docker run -ti --rm --net=container:e6d4baa7820f igneoussystems/iproute2 ip route
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[rancher@rancher ~]$
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This should be an easy fix since the bridge plugin contains two different configuration options: isGateway and isDefaultGateway. We should be able to set one of these to true and it should work. Unfortunately, it decides to use the gateway as returned by the IPAM plugin (&lt;a class="link" href="https://github.com/containernetworking/plugins/blob/f1f128e3c9220634d3f26b96950271f87c34e424/plugins/main/bridge/bridge.go#L481-L508" target="_blank" rel="noopener"
>see here&lt;/a>). In the DHCP IPAM case, this is the IP of the network&amp;rsquo;s router (192.168.2.1) not the local host (192.168.2.125) which we want everything to forward through to get IPTables.&lt;/p>
&lt;p>The fix for this is the same as in the MACvlan CNI. As part of the CNI, I need to define routes that forward all traffic to the host&amp;rsquo;s IP. I modified the bridge CNI code &lt;a class="link" href="https://github.com/ajacques/cni-plugins/blob/bridge/plugins/main/bridge/bridge.go#L676-L710" target="_blank" rel="noopener"
>here&lt;/a>. Ultimately, it gets the host&amp;rsquo;s primary IPv4 address (IPv6 to come later) and creates a default route to send all traffic to the host. Note that we again&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">gwIp&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">uplinkAddrs&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">IP&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netns&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Do&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">_&lt;/span> &lt;span class="nx">ns&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">NetNS&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">containerLink&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">LinkByName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">args&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">IfName&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">routes&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RouteList&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">containerLink&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">FAMILY_ALL&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">route&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">routes&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RouteDel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">route&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// This route tells the OS that 192.168.2.125/32 can be found on eth0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Before we can set a default route, Linux needs to know where to find the gateway&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RouteAdd&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Route&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">LinkIndex&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">containerLink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Attrs&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">Index&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Scope&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SCOPE_LINK&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Dst&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewIPNet&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">gwIp&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// This route tells the OS to forward 0.0.0.0/0 (all traffic, even on the local LAN)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">/&lt;/span> &lt;span class="c1">// to 192.168.2.125. It knows that 192.168.2.125 is on the eth0 interface&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RouteAdd&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Route&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">LinkIndex&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">containerLink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Attrs&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">Index&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Gw&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">gwIp&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Src&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">ipamResult&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">IPs&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">Address&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">IP&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Great, now the Pod has the correct routes:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">[rancher@rancher ~]$ sudo docker run -ti --rm --net=container:e6d4baa7820f igneoussystems/iproute2 ip route
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">default via 192.168.2.25 dev eth0 src 192.168.2.167
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.2.125 dev eth0 proto kernel scope link
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>DHCP still doesn&amp;rsquo;t work though. Looking back at the IP stack diagram, there&amp;rsquo;s a missing link from the bridge to eth0:&lt;/p>
&lt;p>&lt;a class="link" href="images/HomeLab-BridgeCNI-MissingEth0-1.png" >&lt;img src="https://www.technowizardry.net/2021/11/home-lab-replacing-macvlan-with-a-bridge/images/HomeLab-BridgeCNI-MissingEth0-1.png"
width="302"
height="482"
srcset="https://www.technowizardry.net/2021/11/home-lab-replacing-macvlan-with-a-bridge/images/HomeLab-BridgeCNI-MissingEth0-1_hu_cfebd3d56b89bebb.png 480w, https://www.technowizardry.net/2021/11/home-lab-replacing-macvlan-with-a-bridge/images/HomeLab-BridgeCNI-MissingEth0-1_hu_37ef6cd171923947.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="62"
data-flex-basis="150px"
>&lt;/a>&lt;/p>
&lt;p>This is confirmed using the brctl command:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">[rancher@rancher ~]$ sudo docker run -ti --rm --net=host igneoussystems/iproute2 brctl show
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">bridge name bridge id STP enabled interfaces
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cni0 8000.00155d02cb02 no veth0c90ef73
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>That means we need to add the eth0 interface to the bridge. This is done &lt;a class="link" href="https://github.com/ajacques/cni-plugins/blob/41e1fdcecb8bdeaa27a3cfe5a0e1983a34977a6f/plugins/main/bridge/bridge.go#L313-L355" target="_blank" rel="noopener"
>here&lt;/a>. The code (error handling removed) below shows how it works. First, we need to copy the IP address from eth0 to the bridge. This is because the bridge interface will effectively replace eth0 as the primary interface handling all traffic for even this host. Then we call LinkSetMaster to add eth0 into the bridge.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Copy the IPv4 address from eth0 to the bridge&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">addrs&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">AddrList&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">br&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">FAMILY_V4&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">gwIp&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">uplinkAddrs&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">IP&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">foundAddr&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="kc">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">addr&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">addrs&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">addr&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">IP&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Equal&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">gwIp&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">foundAddr&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">break&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">failed&lt;/span> &lt;span class="kt">bool&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">!&lt;/span>&lt;span class="nx">foundAddr&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">addr&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Addr&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">IPNet&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewIPNet&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">gwIp&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">AddrAdd&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">br&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">addr&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Add the uplink interface to the bridge if it isn&amp;#39;t already there&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// If MasterIndex == 0, then the interface isn&amp;#39;t part of a bridge&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// If MasterIndex != BridgeIndex, then the interface is part of a different bridge&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nx">uplinkLink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Attrs&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">MasterIndex&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="nx">br&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Attrs&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">Index&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">uplinkLink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Attrs&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">MasterIndex&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Fail&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">LinkSetMaster&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">uplinkLink&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">br&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Unfortunately this caused my SSH connection to disconnect after a minute and still prevent traffic. To fix this, we need to move the routes to the bridge interface because it needs to effectively replace eth0 as the primary interface.&lt;/p>
&lt;p>In the code below, we get the routes defined on eth0 so we can add them to the bridge. This failed at first with Linux giving a syscall error.&lt;/p>
&lt;p>This was tricky to figure out, but in Linux you can&amp;rsquo;t define a route that points to a destination, that Linux doesn&amp;rsquo;t also know how to find. For example, defining the route &lt;em>default via 192.168.2.125/32&lt;/em> means that you also need to define where to find 192.168.2.125/32. In most networks, you get a free route defined, &lt;em>192.168.2.0/24 dev eth0&lt;/em>, that tells Linux to find that IP on the eth0 interface, but in our case we&amp;rsquo;re explicitly defining all routes so that doesn&amp;rsquo;t work. We need to define &lt;em>192.168.2.125/32 dev eth0&lt;/em>, then define &lt;em>default via 192.168.2.125&lt;/em>.&lt;/p>
&lt;p>As a simple trick, I sort the routes based on their mask length so more specific routes appear first in the slice.&lt;/p>
&lt;p>After sorting it, I remove it from eth0 and add it to the bridge.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">routes&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RouteList&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">uplinkLink&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">FAMILY_V4&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">routes&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Sort routes so that most specific routes appear first. This is to avoid an issue where we can&amp;#39;t create a&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// default route until the subnet route is available&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">sort&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Slice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">routes&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">j&lt;/span> &lt;span class="kt">int&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">bool&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">l&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">routes&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">Dst&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Mask&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Size&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">routes&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">j&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">Dst&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">routes&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">j&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">Dst&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Mask&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">routes&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">j&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">Dst&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Mask&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Size&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">l&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="nx">r&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">route&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">routes&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RouteDel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">route&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">route&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">LinkIndex&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">br&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Index&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RouteAdd&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">route&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now I have a route table that looks like this and my pods are able to work on RancherOS:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">rancher@rancher$ ip route
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[...]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">default via 192.168.2.1 dev cni0 src 192.168.2.125 metric 203
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.2.0/24 dev cni0 proto kernel scope link src 192.168.2.125 metric 203
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.2.92 dev veth3ec79a35 scope link
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[...]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Of course, what would this blog series be without a new problem to solve. When I tried running this on an Ubuntu machine, I encountered more issues with networking as DHCP requests were not making it out to the network.&lt;/p>
&lt;p>As it turns out, there&amp;rsquo;s a difference in the default IPTables rule set between Ubuntu Server and RancherOS.&lt;/p>
&lt;p>In RancherOS, the FORWARD chain has a default value of ACCEPT:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">[rancher@rancher ~]$ sudo iptables -L -v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[...]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Chain FORWARD (policy ACCEPT 15883 packets, 2853K bytes)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[...]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Whereas, Ubuntu Server has a default value of DROP:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">user@ubuntu:~$ sudo iptables -L -v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[...]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Chain FORWARD (policy DROP 2735 packets, 495K bytes)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[...]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This means we&amp;rsquo;re going to have manage IPTables rules that permit each pod to communicate with the network. Stay tuned for the next post where we extend the CNI to include IPTables rules.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2021%2F11%2Fhome-lab-replacing-macvlan-with-a-bridge%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Home+Lab%3A+Part+6+-+Replacing+MACvlan+with+a+Bridge" style="border:0" alt="" /></description></item><item><title>Home Lab: Part 5 - Problems with asymmetrical routing</title><link>https://www.technowizardry.net/2021/11/home-lab-part-6-problems-with-asymmetrical-routing/</link><pubDate>Sat, 06 Nov 2021 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2021/11/home-lab-part-6-problems-with-asymmetrical-routing/</guid><summary>&lt;p>In the previous post (DHCP IPAM), we successfully got our containers running with macvlan + DHCP. I additionally installed MetalLB and everything seemingly worked, however when I tried to retroactively add this to my existing Kubernetes home lab cluster already running Calico, I was not able to access the Metallb service. All connections were timing out.&lt;/p>
&lt;p>A quick Wireshark packet capture of the situation exposed this problem:&lt;/p>
&lt;p>&lt;a class="link" href="images/image.png" >&lt;img src="https://www.technowizardry.net/2021/11/home-lab-part-6-problems-with-asymmetrical-routing/images/image.png"
width="992"
height="200"
srcset="https://www.technowizardry.net/2021/11/home-lab-part-6-problems-with-asymmetrical-routing/images/image_hu_a54422b02b7fab6e.png 480w, https://www.technowizardry.net/2021/11/home-lab-part-6-problems-with-asymmetrical-routing/images/image_hu_3cc89b22c30c6e11.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="496"
data-flex-basis="1190px"
>&lt;/a>&lt;/p></summary><description>&lt;p>In the previous post (DHCP IPAM), we successfully got our containers running with macvlan + DHCP. I additionally installed MetalLB and everything seemingly worked, however when I tried to retroactively add this to my existing Kubernetes home lab cluster already running Calico, I was not able to access the Metallb service. All connections were timing out.&lt;/p>
&lt;p>A quick Wireshark packet capture of the situation exposed this problem:&lt;/p>
&lt;p>&lt;a class="link" href="images/image.png" >&lt;img src="https://www.technowizardry.net/2021/11/home-lab-part-6-problems-with-asymmetrical-routing/images/image.png"
width="992"
height="200"
srcset="https://www.technowizardry.net/2021/11/home-lab-part-6-problems-with-asymmetrical-routing/images/image_hu_a54422b02b7fab6e.png 480w, https://www.technowizardry.net/2021/11/home-lab-part-6-problems-with-asymmetrical-routing/images/image_hu_3cc89b22c30c6e11.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="496"
data-flex-basis="1190px"
>&lt;/a>&lt;/p>
&lt;p>The SYN packet from my computer made it to the container (LB IP 1921.168.6.2), but the responding SYN/ACK packet that came back had a source address of 192.168.2.76 (the pod&amp;rsquo;s network interface.) This wouldn&amp;rsquo;t work because my computer ignored it because it didn&amp;rsquo;t belong to an active flow.&lt;/p>
&lt;p>On the far side, Metallb is responsible for destination NATing (DNAT) by rewriting the destination IP from 192.168.6.2 to the pod&amp;rsquo;s IP 192.168.2.76, then the response packets are supposed to be source NATed (SNAT) so that the client computer only sees the LB IP address.&lt;/p>
&lt;p>Checking out iptables -t nat -L -v, we see the difference in two different chains. One has KUBE-MARK-MASQ, the other doesn&amp;rsquo;t.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">Chain&lt;/span> &lt;span class="n">KUBE&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">FW&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="n">IWBTEYTRLLAGWWJ&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="n">references&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pkts&lt;/span> &lt;span class="n">bytes&lt;/span> &lt;span class="n">target&lt;/span> &lt;span class="n">prot&lt;/span> &lt;span class="n">opt&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">out&lt;/span> &lt;span class="n">source&lt;/span> &lt;span class="n">destination&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">0&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="n">KUBE&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">MARK&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">MASQ&lt;/span> &lt;span class="n">all&lt;/span> &lt;span class="o">--&lt;/span> &lt;span class="n">any&lt;/span> &lt;span class="n">any&lt;/span> &lt;span class="n">anywhere&lt;/span> &lt;span class="n">anywhere&lt;/span> &lt;span class="o">/*&lt;/span> &lt;span class="n">prometheus&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">prometheus&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pushgateway&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">http&lt;/span> &lt;span class="n">loadbalancer&lt;/span> &lt;span class="ne">IP&lt;/span> &lt;span class="o">*/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">0&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="n">KUBE&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">SVC&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="n">IWBTEYTRLLAGWWJ&lt;/span> &lt;span class="n">all&lt;/span> &lt;span class="o">--&lt;/span> &lt;span class="n">any&lt;/span> &lt;span class="n">any&lt;/span> &lt;span class="n">anywhere&lt;/span> &lt;span class="n">anywhere&lt;/span> &lt;span class="o">/*&lt;/span> &lt;span class="n">prometheus&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">prometheus&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pushgateway&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">http&lt;/span> &lt;span class="n">loadbalancer&lt;/span> &lt;span class="ne">IP&lt;/span> &lt;span class="o">*/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">0&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="n">KUBE&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">MARK&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">DROP&lt;/span> &lt;span class="n">all&lt;/span> &lt;span class="o">--&lt;/span> &lt;span class="n">any&lt;/span> &lt;span class="n">any&lt;/span> &lt;span class="n">anywhere&lt;/span> &lt;span class="n">anywhere&lt;/span> &lt;span class="o">/*&lt;/span> &lt;span class="n">prometheus&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">prometheus&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pushgateway&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">http&lt;/span> &lt;span class="n">loadbalancer&lt;/span> &lt;span class="ne">IP&lt;/span> &lt;span class="o">*/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Chain&lt;/span> &lt;span class="n">KUBE&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">FW&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">BI5FYS4DEZGC5QLO&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="n">references&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pkts&lt;/span> &lt;span class="n">bytes&lt;/span> &lt;span class="n">target&lt;/span> &lt;span class="n">prot&lt;/span> &lt;span class="n">opt&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">out&lt;/span> &lt;span class="n">source&lt;/span> &lt;span class="n">destination&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">17&lt;/span> &lt;span class="mi">884&lt;/span> &lt;span class="n">KUBE&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">XLB&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">BI5FYS4DEZGC5QLO&lt;/span> &lt;span class="n">all&lt;/span> &lt;span class="o">--&lt;/span> &lt;span class="n">any&lt;/span> &lt;span class="n">any&lt;/span> &lt;span class="n">anywhere&lt;/span> &lt;span class="n">anywhere&lt;/span> &lt;span class="o">/*&lt;/span> &lt;span class="n">smarthome&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">pihole&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">tcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">http&lt;/span> &lt;span class="n">loadbalancer&lt;/span> &lt;span class="ne">IP&lt;/span> &lt;span class="o">*/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">0&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="n">KUBE&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">MARK&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">DROP&lt;/span> &lt;span class="n">all&lt;/span> &lt;span class="o">--&lt;/span> &lt;span class="n">any&lt;/span> &lt;span class="n">any&lt;/span> &lt;span class="n">anywhere&lt;/span> &lt;span class="n">anywhere&lt;/span> &lt;span class="o">/*&lt;/span> &lt;span class="n">smarthome&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">pihole&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">tcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">http&lt;/span> &lt;span class="n">loadbalancer&lt;/span> &lt;span class="ne">IP&lt;/span> &lt;span class="o">*/&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>At first, I thought Calico was doing something different between these two services, because when changed back to Calico, it worked correctly. But this was only partially correct. There were two main differences.&lt;/p>
&lt;p>The first difference was that the service that worked had &lt;strong>externalTrafficPolicy: Cluster&lt;/strong>, but the service that wasn&amp;rsquo;t working had &lt;strong>externalTrafficPolicy: Local&lt;/strong>. In Cluster mode, the source IP address is rewritten to use the host&amp;rsquo;s IP address, whereas Local only DNATs the packet. (&lt;a class="link" href="https://serenafeng.github.io/2020/03/26/kube-proxy-in-iptables-mode/" target="_blank" rel="noopener"
>Here is useful blog post&lt;/a>)&lt;/p>
&lt;blockquote>
&lt;p>Usually the link doing NAT will remember how it mangled a packet, and when a reply packet passes through the other way, it will do the reverse mangling on that reply packet, so everything works.&lt;/p>
&lt;p>&lt;a class="link" href="https://www.netfilter.org/documentation/HOWTO/NAT-HOWTO.txt" target="_blank" rel="noopener"
>IPTables Docs - NAT-HOWTO&lt;/a>&lt;/p>&lt;/blockquote>
&lt;p>IPTables automatically handles the packets flowing in the reverse direction, but are our packets being processed by the host&amp;rsquo;s IPTables?&lt;/p>
&lt;p>My new containers were using the following routing table:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">root@macvlan-test-6fcfb775c9-zmbpc:/# ip route
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">default via 192.168.2.1 dev eth0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">10.43.0.0/16 via 192.168.2.225 dev eth0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.2.0/24 dev eth0 proto kernel scope link src 192.168.2.187
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>In this route table, all packets (except for K8s cluster local IPs) would get forwarded to the destination switch port and bypass the host&amp;rsquo;s iptables rule set. In the previous post, this already caused problems with the K8s service routing.&lt;/p>
&lt;p>The packets were following a path like this:&lt;/p>
&lt;p>&lt;a class="link" href="images/image-2.png" >&lt;img src="https://www.technowizardry.net/2021/11/home-lab-part-6-problems-with-asymmetrical-routing/images/image-2.png"
width="400"
height="470"
srcset="https://www.technowizardry.net/2021/11/home-lab-part-6-problems-with-asymmetrical-routing/images/image-2_hu_bb4e5ebebc6ee291.png 480w, https://www.technowizardry.net/2021/11/home-lab-part-6-problems-with-asymmetrical-routing/images/image-2_hu_4a6ad612d36ce09f.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="85"
data-flex-basis="204px"
>&lt;/a>&lt;/p>
&lt;p>However, Calico used a different routing table where all traffic was routed through 169.254.1.1. Calico&amp;rsquo;s FAQ mentions this &lt;a class="link" href="https://docs.projectcalico.org/reference/faq#why-does-my-container-have-a-route-to-16925411" target="_blank" rel="noopener"
>here&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">root@pihole-6f776b89bc-9lbw6:/# ip route
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">default via 169.254.1.1 dev eth0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">169.254.1.1 dev eth0 scope link
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The packets were following a path where all packets were being forwarded through the host&amp;rsquo;s iptables rule set.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-3.png" >&lt;img src="https://www.technowizardry.net/2021/11/home-lab-part-6-problems-with-asymmetrical-routing/images/image-3-1024x342.png"
width="1024"
height="342"
srcset="https://www.technowizardry.net/2021/11/home-lab-part-6-problems-with-asymmetrical-routing/images/image-3-1024x342_hu_5c7ff404babac12d.png 480w, https://www.technowizardry.net/2021/11/home-lab-part-6-problems-with-asymmetrical-routing/images/image-3-1024x342_hu_86573b3153773048.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="299"
data-flex-basis="718px"
>&lt;/a>&lt;/p>
&lt;p>Thus, we need to change the route tables so that everything flows through the host.&lt;/p>
&lt;p>This seems like it would be easy to do, but the following config:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cniVersion&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.3.1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;dhcp-cni-network&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;plugins&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;macvlan&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;macvlan&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;master&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;eth0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ipam&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;dhcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;route-override&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;flushroutes&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;addroutes&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;dst&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.0.0.0/0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;gw&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;192.168.2.125&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Results in the following route table:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">root@macvlan-test-6fcfb775c9-zmbpc:/# ip route
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">default via 192.168.2.225 dev eth0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.2.0/24 dev eth0 proto kernel scope link src 192.168.2.187
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The second route allows traffic destined to the local subnet to continue bypassing the host&amp;rsquo;s IP stack. If you&amp;rsquo;re not on the local subnet, then it does work correctly.&lt;/p>
&lt;p>I was not able to get the routing tables corrected using the route-override CNI plugin. Setting flushroutes: true wouldn&amp;rsquo;t delete this route because of &lt;a class="link" href="https://github.com/redhat-nfvpe/cni-route-override/blob/6263d6876c6aa52d6b660e4b54da6b0b58b04022/cmd/route-override/route-override.go#L133" target="_blank" rel="noopener"
>this check&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">route&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">routes&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">route&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Scope&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SCOPE_LINK&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">^^^^^&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">route&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Dst&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If you know how this mystery route is created, let me know in the comments.&lt;/p>
&lt;p>Instead, I ended up forking the CNI references plugins and writing a custom plugin that setup my routes explicitly. Resulting in the following route table:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">default via 169.254.1.1 dev eth0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">169.254.1.1 dev eth0 scope link
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Partial success.&lt;/p>
&lt;h2 id="surprise-issues-with-macvlan">Surprise Issues with MACvlan&lt;/h2>
&lt;p>Now, I&amp;rsquo;m able to load it using the LB, but I can&amp;rsquo;t ping the pod IP, but the pod can ping outwards. After much investigation, I tracked this down to an IPTables rule that was dropping INVALID connections.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Chain KUBE-FORWARD (1 references)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> pkts bytes target prot opt in out source destination
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 984 102K DROP all -- any any anywhere anywhere ctstate INVALID
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Why are these packets considered INVALID by conntrack when outbound pings from the pod itself are able to succeed? My guess is that inbound packets are again bypassing the host IP stack going directly to the pod network stack as macvlan is supposed to do.&lt;/p>
&lt;p>&lt;strong>TBD: Figure out how to fix this&lt;/strong>. My temporary solution is to override the DROP command and force IPTables to pass any traffic coming from the containers.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">sudo iptables -I FORWARD 1 -i mac0 -j ACCEPT
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>These problems are suggesting that maybe MACvlan is the wrong technology to use here. I previously ruled out using bridge because it was lower performance and required the kernel to &amp;rsquo;learn&amp;rsquo; STP and MACs, of which we shouldn&amp;rsquo;t need.&lt;/p>
&lt;h2 id="writing-a-custom-cni-plugin">Writing a custom CNI Plugin&lt;/h2>
&lt;p>Let&amp;rsquo;s review how I wrote a new CNI plugin:&lt;/p>
&lt;p>The CNI plugins repository contains a &lt;a class="link" href="https://github.com/containernetworking/plugins/blob/f1f128e3c9220634d3f26b96950271f87c34e424/plugins/sample/main.go" target="_blank" rel="noopener"
>sample CNI&lt;/a> that we can use. For brevity, I&amp;rsquo;m going to exclude error handling&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">link&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">LinkByName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;eth0&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">addrs&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">AddrList&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">link&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">FAMILY_V4&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">mainIP&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">addrs&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The default route needs to point towards the host&amp;rsquo;s IP address, so we need to grab that. In the future, this should be configurable.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netns&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Do&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">_&lt;/span> &lt;span class="nx">ns&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">NetNS&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">containerIFName&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">netif&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">result&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Interfaces&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">netif&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Sandbox&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s">&amp;#34;&amp;#34;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">containerIFName&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netif&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">link&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">LinkByName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">netif&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">routes&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RouteList&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">link&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">FAMILY_ALL&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">route&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">routes&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RouteDel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">route&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">dev&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">LinkByName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">containerIFName&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now, we switch to the context of the container&amp;rsquo;s network namespace and iterate over the interfaces and purge all routes. Nothing survives.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">route&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Route&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">LinkIndex&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">dev&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Attrs&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">Index&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Scope&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SCOPE_LINK&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Dst&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewIPNet&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">mainIP&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">IP&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RouteAdd&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">route&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Next, we tell Linux which interface the gateway IP lives.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RouteAdd&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">netlink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Route&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">LinkIndex&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">dev&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Attrs&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">Index&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Gw&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">mainIP&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">IP&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Src&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">prevResult&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">IPs&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">Address&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">IP&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Finally, we add the default route telling Linux to forward everything to the host&amp;rsquo;s IP stack.&lt;/p>
&lt;p>Then our CNI config (/etc/cni/net.d/0-bridge.conflist) looks like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cniVersion&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.3.1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;dhcp-cni-network&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;plugins&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;macvlan&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;macvlan&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;master&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;eth0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ipam&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;dhcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;route-fix&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;master&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;eth0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>To simplify everything, I&amp;rsquo;ve also changed the mac0 interface IP address to explicitly 169.254.1.1 to follow in Calico&amp;rsquo;s model. This is all handled by the custom CNI I wrote in the section below.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">rancher&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">network&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">post_cmds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/var/lib/macvlan-init.sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">write_files&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">container&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">network&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">content&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|+&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> #!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> set -ex
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> echo &amp;#39;macvlan is up. Configuring&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> MASTER_IFACE=&amp;#34;eth0&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ip link add mac0 link eth0 type macvlan mode bridge &amp;amp;&amp;amp; ip link set mac0 up || true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ip addr add 169.254.1.1 dev mac0 || true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> )
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # the last line of the file needs to be a blank line or a comment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">owner&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">root:root&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/var/lib/macvlan-init.sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">permissions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0755&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If you&amp;rsquo;re not using RancherOS, the important part is:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">MASTER_IFACE=&amp;#34;eth0&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ip link add mac0 link eth0 type macvlan mode bridge &amp;amp;&amp;amp; ip link set mac0 up || true
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="custom-cni">Custom CNI&lt;/h2>
&lt;p>This is deployable with the following K8s YAML:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;span class="lnt">68
&lt;/span>&lt;span class="lnt">69
&lt;/span>&lt;span class="lnt">70
&lt;/span>&lt;span class="lnt">71
&lt;/span>&lt;span class="lnt">72
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">DaemonSet&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dhcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-dhcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tier&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-dhcp-daemon&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-dhcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dhcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-dhcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tier&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PRIORITY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ghcr.io/ajacques/k8s-dhcp-cni-helper:dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">imagePullPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Always&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">lifecycle&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">preStop&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/bin/sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- -&lt;span class="l">c&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">rm /host/cni_net/$PRIORITY-bridge.conflist&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-dhcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">100m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">50Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">10m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">50Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">securityContext&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">privileged&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/run&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">run&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/host/cni_bin/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cnibin&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/host/cni_net&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cni&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostNetwork&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostPID&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tolerations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CriticalAddonsOnly&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Exists&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">effect&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NoSchedule&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Exists&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">effect&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NoExecute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Exists&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">hostPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/run&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">run&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">hostPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/etc/cni/net.d&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cni&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">hostPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/opt/cni/bin&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cnibin&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Stay tuned for more work on the cluster.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2021%2F11%2Fhome-lab-part-6-problems-with-asymmetrical-routing%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Home+Lab%3A+Part+5+-+Problems+with+asymmetrical+routing" style="border:0" alt="" /></description></item><item><title>Home Lab: Part 4 - A DHCP IPAM</title><link>https://www.technowizardry.net/2021/10/home-lab-dhcp-ipam/</link><pubDate>Tue, 26 Oct 2021 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2021/10/home-lab-dhcp-ipam/</guid><summary>&lt;p>In the previous post, we end up abusing subnets and routing to get Calico to exist on the correct subnet, but what if we could get rid of Calico&amp;rsquo;s duplicate IPAM system and just depend on our existing DHCP server to handle reservations? In this post, we&amp;rsquo;re going to prototype a cluster that uses DHCP + layer 2 Linux bridging to avoid the complications outlined in Part 3.&lt;/p>
&lt;p>The official &lt;a class="link" href="https://www.cni.dev/plugins/current/" target="_blank" rel="noopener"
>CNI documentation&lt;/a> describes two plugins that could be relevant.&lt;/p></summary><description>&lt;p>In the previous post, we end up abusing subnets and routing to get Calico to exist on the correct subnet, but what if we could get rid of Calico&amp;rsquo;s duplicate IPAM system and just depend on our existing DHCP server to handle reservations? In this post, we&amp;rsquo;re going to prototype a cluster that uses DHCP + layer 2 Linux bridging to avoid the complications outlined in Part 3.&lt;/p>
&lt;p>The official &lt;a class="link" href="https://www.cni.dev/plugins/current/" target="_blank" rel="noopener"
>CNI documentation&lt;/a> describes two plugins that could be relevant.&lt;/p>
&lt;blockquote>
&lt;p>With dhcp plugin the containers can get an IP allocated by a DHCP server already running on your network.&lt;/p>
&lt;p>&lt;a class="link" href="https://www.cni.dev/plugins/current/ipam/dhcp/" target="_blank" rel="noopener"
>https://www.cni.dev/plugins/current/ipam/dhcp/&lt;/a>&lt;/p>&lt;/blockquote>
&lt;p>This avoids overlapping IPAM problems with the previous solution and means that the DHCP server already running on my network would be responsible for handing out IP addresses directly to the containers.&lt;/p>
&lt;p>That handles IP address assignment, now we need to be able to switch packets to the correct container interface. The documentation references both macvlan and ipvlan as possible switching options. &lt;a class="link" href="https://hicu.be/macvlan-vs-ipvlan" target="_blank" rel="noopener"
>Comparing&lt;/a> the different options, ipvlan will expose only a single MAC address whereas macvlan will assign separate MAC addresses per container and expose them to the rest of the network. Ipvlan is generally recommended only when you need a single MAC address, like when you&amp;rsquo;re binding to a Wi-Fi adapter which only permits one MAC address per station.&lt;/p>
&lt;p>I created a new cluster in Rancher with a new VM following my previous blog posts, however in Rancher 2.6.1+ it seems that I am unable to access the cluster if there&amp;rsquo;s no CNI plugin installed on the cluster, so I instead use kubectl to connect to the cluster. This is possibly a regression from 2.6.0 and I need to get around to reporting it.&lt;/p>
&lt;p>I didn&amp;rsquo;t find a k8s installer that would deploy and configure the macvlan + DHCP CNI correctly, so we&amp;rsquo;re going to need to do this manually. In a future blog post, I will package this up into a polished file that can be deployed. First, download the latest release of the CNI plugins from their &lt;a class="link" href="https://github.com/containernetworking/plugins/releases" target="_blank" rel="noopener"
>GitHub releases&lt;/a> page. Extract it to the host&amp;rsquo;s &lt;strong>/opt/cni/bin&lt;/strong> folder, so you have /opt/cni/bin/dhcp.&lt;/p>
&lt;p>Then create /etc/cni/net.d/15-bridge.conflist and reboot.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cniVersion&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.3.1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;default-cni-network&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;plugins&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;macvlan&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;macvlan&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;master&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;eth0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ipam&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;dhcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After the host came up, the DHCP requests were not making it out to the network, but they were visible on the VM&amp;rsquo;s network interface:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">[rancher@rancher ~]$ sudo docker run --net=host --rm crccheck/tcpdump -i any -f &amp;#39;udp port 67 or udp port 68&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">06:09:33.665302 IP 0.0.0.0.68 &amp;gt; 255.255.255.255.67: BOOTP/DHCP, Request from 42:84:46:b7:d5:e5 (oui Unknown), length 272
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">06:09:33.737211 IP 0.0.0.0.68 &amp;gt; 255.255.255.255.67: BOOTP/DHCP, Request from f2:53:1c:66:f2:51 (oui Unknown), length 272
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">06:09:33.993417 IP 0.0.0.0.68 &amp;gt; 255.255.255.255.67: BOOTP/DHCP, Request from 56:df:48:0a:4d:92 (oui Unknown), length 272
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">06:09:43.426731 IP 0.0.0.0.68 &amp;gt; 255.255.255.255.67: BOOTP/DHCP, Request from fe:fa:30:18:23:6f (oui Unknown), length 272
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">### On the Router:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ubnt:~$ sudo tcpdump -i eth0 -f &amp;#39;udp port 67 or udp port 68&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">^C
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">0 packets captured
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Some digging revealing that this is because I&amp;rsquo;m using macvlan which enables each container to use it&amp;rsquo;s own MAC address. Hyper-V was configured to block this for security. To fix this, check the &amp;ldquo;Enable MAC address spoofing&amp;rdquo; option in VM Settings &amp;gt; Network Adapter &amp;gt; Advanced Features. My understanding is that ipvlan may not require this option since it rewrites to use the VM&amp;rsquo;s MAC address.&lt;/p>
&lt;p>&lt;a class="link" href="images/image-11.png" >&lt;img src="https://www.technowizardry.net/2021/10/home-lab-dhcp-ipam/images/image-11.png"
width="857"
height="482"
srcset="https://www.technowizardry.net/2021/10/home-lab-dhcp-ipam/images/image-11_hu_cab33b665fe20871.png 480w, https://www.technowizardry.net/2021/10/home-lab-dhcp-ipam/images/image-11_hu_5a1f75f147d4df20.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="177"
data-flex-basis="426px"
>&lt;/a>&lt;/p>
&lt;p>Enabling MAC address spoofing in Hyper-V enables us to use macvlan, but could reduce security.&lt;/p>
&lt;p>After that, I restarted the DHCP container and poof we had reservations:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">12d74a2fe[...]/default-cni-network: lease acquired, expiration is 2021-10-23 06:13:25.746364924 +0000 UTC
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">432fd1694[...]/default-cni-network: lease acquired, expiration is 2021-10-23 06:13:25.827760638 +0000 UTC
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Containers were coming up with the right IP addresses, I was able to ping the containers from other computers, but I was not able to ping the containers from the host VM. This was odd. If anything I would have expected the reverse of this. Apparently this is expected behavior from a macvlan:&lt;/p>
&lt;blockquote>
&lt;p>Irrespective of the mode used for the macvlan, &lt;em>there&amp;rsquo;s no connectivity from whatever uses the macvlan (eg a container) to the lower device&lt;/em>. This is by design, and is due to the the way macvlan interfaces &amp;ldquo;hook into&amp;rdquo; their physical interface.&lt;/p>
&lt;p>&lt;a class="link" href="https://backreference.org/2014/03/20/some-notes-on-macvlanmacvtap/" target="_blank" rel="noopener"
>https://backreference.org/2014/03/20/some-notes-on-macvlanmacvtap/&lt;/a>&lt;/p>&lt;/blockquote>
&lt;p>This was also preventing kubelet from initializing the cluster:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">I1022 19:07:40.843340 1274 prober.go:116] &amp;#34;Probe failed&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">probeType=&amp;#34;Readiness&amp;#34; pod=&amp;#34;kube-system/coredns-685d6d555d-pss58&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">podUID=cecc8eb0-56f2-4fa3-aad2-518bcd5aec55 containerName=&amp;#34;coredns&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">probeResult=failure output=&amp;#34;Get \&amp;#34;http://192.168.2.125:8181/ready\&amp;#34;:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">context deadline exceeded (Client.Timeout exceeded while awaiting headers)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This is because macvlan by default does not route traffic from the container to the host. To fix this, we need to add the host interface into the bridge so containers can send traffic to it.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">sudo ip link add mac0 link eth0 type macvlan mode bridge
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo ip addr add 192.168.2.125/24 dev mac0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo ip link set mac0 up
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The next problem I encountered was particularly insidious partly due to the fact that I was already running a K8s cluster in a separate VM.&lt;/p>
&lt;p>In Kubernetes networking is complicated. The CNI is responsible for creating the network interface that each container uses, however Kubernetes also has something called kube-proxy which is responsible for exposing certain services, such as kube-dns and the kubernetes main HTTPS endpoint. Each container automatically gets a K8s service token and several environmental variables pointing it to the correct IP address:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">rancher&lt;/span>&lt;span class="err">@&lt;/span>&lt;span class="n">rancher&lt;/span>&lt;span class="o">$&lt;/span> &lt;span class="n">docker&lt;/span> &lt;span class="n">inspect&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="n">anyk8scontainer&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;Mounts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;bind&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Source&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/opt/rke/var/lib/kubelet/pods/f5f70c5b-7526-4873-aa21-57dedf551e3d/volumes/kubernetes.io~projected/kube-api-access-p8mqs&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Destination&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/var/run/secrets/kubernetes.io/serviceaccount&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;Config&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Env&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;KUBERNETES_SERVICE_HOST=10.43.0.1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Note how it provides the 10.43.0.1 address for Kubernetes! This IP address doesn&amp;rsquo;t match anything that we&amp;rsquo;ve previous configured in any of the CNI configuration. Kube-proxy uses iptables to fake these IP addresses:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">rancher@rancher$ sudo iptables-save
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-A KUBE-SEP-ECW7X2JHZ5GHPAME -p tcp -m comment --comment &amp;#34;default/kubernetes:https&amp;#34; -m tcp -j DNAT --to-destination 192.168.2.125:6443
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-A KUBE-SERVICES -d 10.43.0.1/32 -p tcp -m comment --comment &amp;#34;default/kubernetes:https cluster IP&amp;#34; -m tcp --dport 443 -j KUBE-SVC-NPX46M4PTMTKRN6Y
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-A KUBE-SVC-NPX46M4PTMTKRN6Y -m comment --comment &amp;#34;default/kubernetes:https&amp;#34; -j KUBE-SEP-ECW7X2JHZ5GHPAME
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>However, macvlan is special because packets from containers don&amp;rsquo;t get processed by the host&amp;rsquo;s iptables rules. Thus, this iptables magic doesn&amp;rsquo;t work and the packet gets forwarded out to the physical network. In my case, I was already running a separate k8s cluster and my router was forwarding it to the old API gateway which lead&lt;/p>
&lt;p>To fix this, I use the &lt;a class="link" href="https://github.com/redhat-nfvpe/cni-route-override" target="_blank" rel="noopener"
>route-override CNI&lt;/a> plugin to add a route for 10.43.0.0/16 to send it to the host&amp;rsquo;s IP chain where the iptables rules will apply. I downloaded this CNI plugin and extracted it to &lt;strong>/opt/cni/bin/route-override&lt;/strong>. We add the following plugin to the CNI configuration in &lt;strong>/etc/cni/net.d/10-bridge.conflist&lt;/strong> and reboot:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cniVersion&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.3.1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;default-cni-network&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;plugins&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;macvlan&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;macvlan&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;master&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;eth0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ipam&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;dhcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;route-override&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;addroutes&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;dst&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;10.43.0.0/16&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;gw&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;192.168.2.125&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Both of these IP addresses are hard coded and are dependent on the cluster configuration and the host IP, so when we expand to multiple hosts we&amp;rsquo;ll need to genericize this.&lt;/p>
&lt;p>After this, all of my pods successfully came up with IP and all my pods were able to communicate successfully. However, ~12 hours later the routes on the mac0 interface get removed and all networking stops working.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">[...]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">I1027 06:50:01.302034 containerName=&amp;#34;coredns&amp;#34; probeResult=failure output=&amp;#34;Get \&amp;#34;http://192.168.2.93:8181/ready\&amp;#34;: dial tcp 192.168.2.93:8181: i/o timeout (Client.Timeout exceeded while awaiting headers)&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">I1027 06:50:01.682678 containerName=&amp;#34;coredns&amp;#34; probeResult=failure output=&amp;#34;Get \&amp;#34;http://192.168.2.93:8080/health\&amp;#34;: dial tcp 192.168.2.93:8080: connect: no route to host&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This seems to coincide when the host&amp;rsquo;s DHCP client renews the IP address for eth0.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">rancher&lt;/span>&lt;span class="err">@&lt;/span>&lt;span class="n">rancher&lt;/span>&lt;span class="o">$&lt;/span> &lt;span class="n">sudo&lt;/span> &lt;span class="n">system&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">docker&lt;/span> &lt;span class="n">logs&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">t&lt;/span> &lt;span class="n">network&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">021&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">26&lt;/span>&lt;span class="n">T06&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">49&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">40.689173492&lt;/span>&lt;span class="n">Z&lt;/span> &lt;span class="n">Failed&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">connect&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">non&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">global&lt;/span> &lt;span class="n">ctrl_ifname&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">eth0&lt;/span> &lt;span class="n">error&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">No&lt;/span> &lt;span class="n">such&lt;/span> &lt;span class="n">file&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">directory&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">2021&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">26&lt;/span>&lt;span class="n">T06&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">49&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">40.689195892&lt;/span>&lt;span class="n">Z&lt;/span> &lt;span class="n">Failed&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">connect&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">non&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">global&lt;/span> &lt;span class="n">ctrl_ifname&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">mac0&lt;/span> &lt;span class="n">error&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">No&lt;/span> &lt;span class="n">such&lt;/span> &lt;span class="n">file&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">directory&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">2021&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">26&lt;/span>&lt;span class="n">T06&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">49&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">40.790714733&lt;/span>&lt;span class="n">Z&lt;/span> &lt;span class="n">sending&lt;/span> &lt;span class="k">signal&lt;/span> &lt;span class="n">TERM&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">pid&lt;/span> &lt;span class="mi">584&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">2021&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">26&lt;/span>&lt;span class="n">T06&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">49&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">40.790741234&lt;/span>&lt;span class="n">Z&lt;/span> &lt;span class="n">waiting&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">pid&lt;/span> &lt;span class="mi">584&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">exit&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">2021&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">26&lt;/span>&lt;span class="n">T06&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">49&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">48.760578200&lt;/span>&lt;span class="n">Z&lt;/span> &lt;span class="n">netconf&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Apply&lt;/span> &lt;span class="n">Network&lt;/span> &lt;span class="n">Config&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">2021&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">26&lt;/span>&lt;span class="n">T06&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">49&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">48.770687300&lt;/span>&lt;span class="n">Z&lt;/span> &lt;span class="n">netconf&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Running&lt;/span> &lt;span class="n">DHCP&lt;/span> &lt;span class="n">on&lt;/span> &lt;span class="n">eth0&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">dhcpcd&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">MA4&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">e&lt;/span> &lt;span class="n">force_hostname&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="bp">true&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">timeout&lt;/span> &lt;span class="mi">10&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">w&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">debug&lt;/span> &lt;span class="n">eth0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">2021&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">26&lt;/span>&lt;span class="n">T06&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">49&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">49.988228300&lt;/span>&lt;span class="n">Z&lt;/span> &lt;span class="n">netconf&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Checking&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">see&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">DNS&lt;/span> &lt;span class="n">was&lt;/span> &lt;span class="n">set&lt;/span> &lt;span class="n">by&lt;/span> &lt;span class="n">DHCP&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">2021&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">26&lt;/span>&lt;span class="n">T06&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">49&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">49.988241300&lt;/span>&lt;span class="n">Z&lt;/span> &lt;span class="n">netconf&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">dns&lt;/span> &lt;span class="n">testing&lt;/span> &lt;span class="n">eth0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">2021&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">26&lt;/span>&lt;span class="n">T06&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">49&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">50.021991000&lt;/span>&lt;span class="n">Z&lt;/span> &lt;span class="n">netconf&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">dns&lt;/span> &lt;span class="n">was&lt;/span> &lt;span class="n">dhcp&lt;/span> &lt;span class="n">set&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">eth0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">2021&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">26&lt;/span>&lt;span class="n">T06&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">49&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">50.022006300&lt;/span>&lt;span class="n">Z&lt;/span> &lt;span class="n">netconf&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">DNS&lt;/span> &lt;span class="n">set&lt;/span> &lt;span class="n">by&lt;/span> &lt;span class="n">DHCP&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">2021&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">26&lt;/span>&lt;span class="n">T06&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">49&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">50.022008500&lt;/span>&lt;span class="n">Z&lt;/span> &lt;span class="n">netconf&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Apply&lt;/span> &lt;span class="n">Network&lt;/span> &lt;span class="n">Config&lt;/span> &lt;span class="n">SyncHostname&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">2021&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">26&lt;/span>&lt;span class="n">T06&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">49&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">50.022010300&lt;/span>&lt;span class="n">Z&lt;/span> &lt;span class="n">netconf&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Restart&lt;/span> &lt;span class="n">syslog&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Apparently the DHCP system is clearing out the mac0 interface configuration. To fix this, we can run the following command:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">sudo&lt;/span> &lt;span class="n">ros&lt;/span> &lt;span class="n">config&lt;/span> &lt;span class="n">merge&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">write_files&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">-&lt;/span> &lt;span class="n">container&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">network&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">path&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">macvlan&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">init&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sh&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">permissions&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0755&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">owner&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">root&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">root&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">content&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="o">|&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">#!/bin/bash&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">set&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">ex&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">echo&lt;/span> &lt;span class="s1">&amp;#39;macvlan is up. Configuring&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">MASTER_IFACE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;eth0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">LOCAL_HOST_CIDR&lt;/span>&lt;span class="o">=$&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ip&lt;/span> &lt;span class="n">addr&lt;/span> &lt;span class="n">show&lt;/span> &lt;span class="n">dev&lt;/span> &lt;span class="n">eth0&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="n">grep&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">E&lt;/span> &lt;span class="s1">&amp;#39;^\s*inet&amp;#39;&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="n">grep&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">m1&lt;/span> &lt;span class="n">global&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="n">awk&lt;/span> &lt;span class="s1">&amp;#39;{ print $2 }&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ip&lt;/span> &lt;span class="n">link&lt;/span> &lt;span class="n">add&lt;/span> &lt;span class="n">mac0&lt;/span> &lt;span class="n">link&lt;/span> &lt;span class="n">eth0&lt;/span> &lt;span class="n">type&lt;/span> &lt;span class="n">macvlan&lt;/span> &lt;span class="n">mode&lt;/span> &lt;span class="n">bridge&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">ip&lt;/span> &lt;span class="n">addr&lt;/span> &lt;span class="n">add&lt;/span> &lt;span class="o">$&lt;/span>&lt;span class="n">LOCAL_HOST_CIDR&lt;/span> &lt;span class="n">dev&lt;/span> &lt;span class="n">mac0&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">ip&lt;/span> &lt;span class="n">link&lt;/span> &lt;span class="n">set&lt;/span> &lt;span class="n">mac0&lt;/span> &lt;span class="n">up&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="bp">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># the last line of the file needs to be a blank line or a comment&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">rancher&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">network&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">post_cmds&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">-&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">macvlan&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">init&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sh&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="n">Ctrl&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Unfortunately I don&amp;rsquo;t know of a good way to do this from Kubernetes or if this is necessary from non-RancherOS based host VMs. Leave a comment below if you have a better suggestion.&lt;/p>
&lt;p>In the next post, I&amp;rsquo;ve revisited this and found out that MACvlan causes some problems with K8s service routing, so read through that.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2021%2F10%2Fhome-lab-dhcp-ipam%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Home+Lab%3A+Part+4+-+A+DHCP+IPAM" style="border:0" alt="" /></description></item><item><title>Home Lab: Part 3 - Networking Revisited</title><link>https://www.technowizardry.net/2021/10/home-lab-part-2-revisited-problems-with-networking/</link><pubDate>Thu, 21 Oct 2021 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2021/10/home-lab-part-2-revisited-problems-with-networking/</guid><summary>&lt;p>&lt;strong>The Problem&lt;/strong>&lt;/p>
&lt;p>In my previous post series, I described how I installed my Kubernetes Home Lab using Calico and MetalLB. This worked great up until I started installing smart home software that expected to be able to do local network discovery. For example, Home Assistant and my Sonos control software both attempted to do subnet local discovery using mDNS or broadcast packets. This did not work because the pods were running on a 192.168.4.0/24 subnet, but all of my physical devices were on 192.168.2.0/24.&lt;/p></summary><description>&lt;p>&lt;strong>The Problem&lt;/strong>&lt;/p>
&lt;p>In my previous post series, I described how I installed my Kubernetes Home Lab using Calico and MetalLB. This worked great up until I started installing smart home software that expected to be able to do local network discovery. For example, Home Assistant and my Sonos control software both attempted to do subnet local discovery using mDNS or broadcast packets. This did not work because the pods were running on a 192.168.4.0/24 subnet, but all of my physical devices were on 192.168.2.0/24.&lt;/p>
&lt;p>This prevented Home Assistant from discovering any devices and had to be fixed.&lt;/p>
&lt;p>&lt;a class="link" href="images/HomeLab-Calico.png" >&lt;img src="https://www.technowizardry.net/2021/10/home-lab-part-2-revisited-problems-with-networking/images/HomeLab-Calico.png"
width="301"
height="482"
srcset="https://www.technowizardry.net/2021/10/home-lab-part-2-revisited-problems-with-networking/images/HomeLab-Calico_hu_ffe4c3da4c7d9794.png 480w, https://www.technowizardry.net/2021/10/home-lab-part-2-revisited-problems-with-networking/images/HomeLab-Calico_hu_1b0b91a39487344d.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="62"
data-flex-basis="149px"
>&lt;/a>&lt;/p>
&lt;p>Calico isolates each pod into it&amp;rsquo;s own broadcast domain. Notice how the brd address is the same as the adapter IP address.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">root@ubuntu-6bcd7c9fdb-kntg7:/# ip addr
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">3: eth0@if41: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue state UP group default
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> link/ether 82:74:d8:c2:20:df brd ff:ff:ff:ff:ff:ff
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> inet 192.168.4.199/32 brd 192.168.4.199 scope global eth0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> valid_lft forever preferred_lft foreverinet 192.168.4.199/32 brd 192.168.4.199 scope global eth0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>When Home Assistant tries to scan for devices using the broadcast address, it will stay inside the Pod. Queries to 255.255.255.255 will hit the host stack, but will not be rebroadcast onto the LAN subnet. Multicast traffic, used by mDNS, also is not supported, but &lt;a class="link" href="https://docs.projectcalico.org/reference/faq#can-calico-do-ip-multicast" target="_blank" rel="noopener"
>according to the FAQ&lt;/a> it may be possible to support with a Multicast software router.&lt;/p>
&lt;h2 id="options">Options&lt;/h2>
&lt;p>&lt;strong>Host Network&lt;/strong>&lt;/p>
&lt;p>One option would be to run pods with hostNetwork: true so that every pod will runs on the end up with a 192.168.2.225 (in my case) address. This enabled Home Assistant to be able to discover devices on my LAN, but it had a number of disadvantages such as not being able to do rolling upgrades and software that tried to use the same ports would conflict with each other.&lt;/p>
&lt;p>&lt;strong>IPv6&lt;/strong>&lt;/p>
&lt;p>But what about IPv6? Great question. Unfortunately, I&amp;rsquo;ve found most K8s software to be lacking in IPv6 support. It&amp;rsquo;s coming soon and when it does, some of our problems will be solved, but not all of them.&lt;/p>
&lt;p>&lt;strong>Reuse the same subnet&lt;/strong>&lt;/p>
&lt;p>The current IP network plan looks like this:&lt;/p>
&lt;ul>
&lt;li>192.168.2.0/24 - Home network subnet&lt;/li>
&lt;li>192.168.2.225/32 - The RancherOS VM IP&lt;/li>
&lt;li>192.168.4.0/24 - Kubernetes pod subnet&lt;/li>
&lt;li>192.168.6.0/24 - MetalLB subnet&lt;/li>
&lt;/ul>
&lt;p>Instead of using 192.168.4.0/24, could we change it so that the Kubernetes pod is also 192.168.2.0/24?&lt;/p>
&lt;p>Pretty much any CNI (like Calico) will manage it&amp;rsquo;s own IP reservations for pods since it assumes it has full control over the IP range. If we tried to change the Calico IP Block to be 192.168.2.0/24, it wouldn&amp;rsquo;t work. Thus we have several requirements:&lt;/p>
&lt;ol>
&lt;li>The DHCP server (an EdgeRouter) should not hand out IP address reservations that conflict with a K8s Pod IP addresses&lt;/li>
&lt;li>The K8s CNI plugin must be configured with the same subnet mask as the LAN. It can&amp;rsquo;t be configured as (e.x. 192.168.2.192/26)&lt;/li>
&lt;li>The K8s CNI plugin should not use IP addresses that are used by hardware devices&lt;/li>
&lt;li>The K8s CNI plugin needs at least one /26 block per node&lt;/li>
&lt;li>The K8s node must properly respond to ARP requests for all pod IP addresses.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Requirements #1 - #4 are related&lt;/strong>. They just require us to split the subnet up into parts such that both services don&amp;rsquo;t conflict.&lt;/p>
&lt;p>My router provides the ability to define the start and end IP address in the DHCP block:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">service {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> shared-network-name LAN2 {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> authoritative enable
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> subnet 192.168.2.0/24 {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> default-router 192.168.2.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> dns-server 192.168.2.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> lease 86400
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> start 192.168.2.38 {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> stop 192.168.2.243
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>However, I&amp;rsquo;m currently using IP addresses over the entire IP range which means there isn&amp;rsquo;t a clean place to carve out a /26 block without re-addressing multiple devices on my network. An alternative to this (assuming we can solve all conflict issues) would be to change the IP Addr Plan to be:&lt;/p>
&lt;ul>
&lt;li>192.168.2.0**/23** - New super subnet (.2.0 - 3.255)
&lt;ul>
&lt;li>192.168.2.0-192.168.2.255 - Home devices
&lt;ul>
&lt;li>192.168.2.225/32 - The RancherOS VM IP&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>192.168.3.0-192.168.3.254 - Kubernetes pod range&lt;/li>
&lt;li>192.168.3.255 - Broadcast address&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>192.168.6.0/24 - MetalLB subnet&lt;/li>
&lt;/ul>
&lt;p>With this change, we expand the size of the existing subnet to include 192.168.3.x. This avoids us having to readdress any existing physical devices, but it does mean that the pods need to move. This is a lot easier because nothing hard-codes those addresses in my network.&lt;/p>
&lt;p>Assuming we can carve out one or more /26&amp;rsquo;s in our block, we still need to get Calico not use those IP addresses because we have to configure Calico to use the same subnet mask as the LAN or else K8s pods will use the wrong broadcast address.&lt;/p>
&lt;p>I looked around the Calico documentation if it&amp;rsquo;s possible to exclude certain IP addresses from their IP block assignment logic and found &lt;a class="link" href="https://github.com/projectcalico/calicoctl/issues/797" target="_blank" rel="noopener"
>one GitHub issue&lt;/a> that talked about this. The maintainers suggest that there&amp;rsquo;s a &lt;em>calicoctl ipam reserve&lt;/em> command, but nothing seemed to exist in the codebase or documentation. However, a &lt;a class="link" href="https://github.com/projectcalico/calicoctl/commit/340bc708ad3949566752147dd9546ad39da91418#diff-0ca9354a920030f50f5ebee06efb134f9e64baadf9803662e3c6b4b2c882a4f6" target="_blank" rel="noopener"
>recent commit&lt;/a> (at the time of this post&amp;rsquo;s writing) suggests IP reservation support is being added in v3.22 and this &lt;a class="link" href="https://docs.projectcalico.org/master/reference/resources/ipreservation" target="_blank" rel="noopener"
>doc&lt;/a> in nightly supports that . This may be an option.&lt;/p>
&lt;p>Interestingly, if we do move all DHCP addresses to &amp;lt;.192 and allow one /26 on the top end of the block at 192.168.2.192/26, we&amp;rsquo;d get the range 192.168.2.192 - 192.168.2.255 with the broadcast IP address also matching the broadcast for 192.168.2.0/24. I&amp;rsquo;m not sure if this would actually happen to work. However, it only allows a single worker node and still doesn&amp;rsquo;t solve our next requirement.&lt;/p>
&lt;p>&lt;strong>Requirement #5 is the tricky one.&lt;/strong>&lt;/p>
&lt;p>First, a quick review how Calico currently works.&lt;/p>
&lt;p>ARP (Address Resolution Protocol) is the mechanism that switches use to translate IPv4 addresses into the correct MAC address that the switch should forward the packet to. If it hasn&amp;rsquo;t learned what switch port a given IP address is, it uses ARP to figure out what switch port packets should be destined to.&lt;/p>
&lt;p>In Calico BGP mode, we don&amp;rsquo;t use ARP because Calico would directly announce a set of pod IP addresses to the router (See below)&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ show ip bgp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BGP table version is 47, local router ID is 192.168.2.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Status codes: s suppressed, d damped, h history, * valid, &amp;gt; best, i - internal, l - labeled
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> S Stale
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Origin codes: i - IGP, e - EGP, ? - incomplete
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Network Next Hop Metric LocPrf Weight Path
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">*&amp;gt;i 192.168.4.192/26 192.168.2.225 0 100 0 i
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Total number of prefixes 1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>All pods that are running on the 192.168.2.225 VM are going to be in the range 192.168.4.192/26.&lt;/p>
&lt;p>We can also see that the switch doesn&amp;rsquo;t know the MAC address of any K8s pods, however it does know where to find the RancherOS VM running the pods:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ show arp | grep 192.168.4
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ show arp | grep 192.168.2.225
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Address HWtype HWaddress Flags Mask Iface
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.2.225 ether 00:15:5d:02:cb:00 C switch0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>As soon as K8s assigns a pod to this node, Calico picks an unused IP address in this range. Calico stores IP addresses for assignment to pods in a K8s resource like below:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">crd.projectcalico.org/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">IPAMBlock&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">192-168-4-192-26&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">affinity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">host:rancher&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allocations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="m">4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="m">6&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">...]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">attributes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">handle_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">k8s-pod-network.aaaca7882a69e27f1fcd4e4b00d388a6c5e966a99145f1d672c93519b84a650a&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secondary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">node&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rancher&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pod&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">coredns-55b58f978-w5cb8&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timestamp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="ld">2021-10-20 18:37:24.673734846 +0000&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">UTC&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">...]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cidr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">192.168.4.192&lt;/span>&lt;span class="l">/26&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">deleted&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">strictAffinity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unallocated&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="m">32&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="m">31&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">...]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Calico uses Layer 3 routing instead of Layer 2 routing because it&amp;rsquo;s more scalable than layer 2 routing. If I were to have hundreds of servers and thousands of K8s pods, layer 2 routing with switches trying to ARP request for every single pod would cause a significant amount of Ethernet protocol overhead.&lt;/p>
&lt;p>This is specifically called out in the Calico documentation &lt;a class="link" href="https://docs.projectcalico.org/reference/architecture/design/l2-interconnect-fabric" target="_blank" rel="noopener"
>here&lt;/a>.&lt;/p>
&lt;blockquote>
&lt;p>[&amp;hellip;] &lt;strong>In a Calico network, the Ethernet interconnect fabric only sees the routers/compute servers, not the end point.&lt;/strong> In a standard cloud model, where there is tens of VMs per server (or hundreds of containers), this reduces the number of nodes that the Ethernet sees (and has to learn) by one to two orders of magnitude. [&amp;hellip;]&lt;/p>
&lt;p>&lt;em>&lt;a class="link" href="https://docs.projectcalico.org/reference/architecture/design/l2-interconnect-fabric" target="_blank" rel="noopener"
>Calico documentation&lt;/a>&lt;/em>&lt;/p>&lt;/blockquote>
&lt;p>However, I don&amp;rsquo;t have hundreds of servers, I just have one server in my home lab and this is a trade-off that enables more seamless K8s routing configuration in a small network. I&amp;rsquo;m expecting my switch to be able to handle the traffic. If not, we&amp;rsquo;ll revisit this.&lt;/p>
&lt;p>That being said, Calico won&amp;rsquo;t respond to ARP requests to the individual pod addresses. Thus, Calico will need to announce the node /26 to BGP even though it&amp;rsquo;ll overlap with a directly connected switch route. An example route table below:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ show ip route
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Codes: K - kernel, C - connected, S - static, R - RIP, B - BGP
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> O - OSPF, IA - OSPF inter area
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> E1 - OSPF external type 1, E2 - OSPF external type 2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;gt; - selected route, * - FIB route, p - stale info
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IP Route Table for VRF &amp;#34;default&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">S *&amp;gt; 0.0.0.0/0 [210/0] via w.x.y.z, eth9
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">C *&amp;gt; 127.0.0.0/8 is directly connected, lo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> vvvvvvvvvvvvvv
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">C *&amp;gt; 192.168.2.0/23 is directly connected, switch0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">B *&amp;gt; 192.168.3.0/26 [200/0] via 192.168.2.225, switch0, 11:41:13
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ^^^^^^^^^^^^^^
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This &lt;em>is almost&lt;/em> fine since routers will pick the most specific route to forward the packets to, however the Layer 2 switches will have no idea what to do with packets and they&amp;rsquo;ll desperately try to send ARP requests if another computer on the same network (not the router) tries to communicate with this pod.&lt;/p>
&lt;p>What about Proxy ARP?&lt;/p>
&lt;p>&lt;a class="link" href="https://en.wikipedia.org/wiki/Proxy_ARP" target="_blank" rel="noopener"
>Proxy ARP&lt;/a> is a mechanism in which one computer responds to ARP requests for an IP address for other machines and responds with its own MAC address. It&amp;rsquo;s almost like MAC address rewriting. Proxy ARP requires the host to have static routes for all the containers, which Calico takes care of for us:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">rancher@rancher$ ip route
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">default via 192.168.2.1 dev eth0 src 192.168.2.225 metric 203
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[...]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.2.0/24 dev eth0 proto kernel scope link src 192.168.2.225 metric 203
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.4.192 dev califcdcb1fb802 scope link
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">blackhole 192.168.4.192/26 proto bird
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.4.193 dev caliaf783f96326 scope link
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.4.195 dev cali0d276a32961 scope link
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">192.168.4.196 dev cali8f741724e18 scope link
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[...]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This may be an option, however many people complain about Proxy ARP breaking behavior unexpectedly, so we need to be careful. We only want Proxy ARP on the inbound side towards the node, but not to Proxy ARP from containers towards the network. Additionally, we&amp;rsquo;re going to be going outside the norm for Calico&lt;/p>
&lt;p>&lt;strong>Back to the proposal&lt;/strong>&lt;/p>
&lt;p>In conclusion, we find that:&lt;/p>
&lt;ol>
&lt;li>Calico does not yet support reserving IP addresses but this is expected in v3.22. &lt;strong>This is a hard requirement&lt;/strong>&lt;/li>
&lt;li>We either have to expand our subnet to avoid conflicts between DHCP reservations and Calico or move a lot of my existing home network devices around&lt;/li>
&lt;li>Our only solution for ARP responses seems to be to enable Proxy ARP on the node&lt;/li>
&lt;/ol>
&lt;p>This option sounds feasible, but has a number of caveats. Let&amp;rsquo;s review other options.&lt;/p>
&lt;p>To be continued in a future post&amp;hellip;&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2021%2F10%2Fhome-lab-part-2-revisited-problems-with-networking%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Home+Lab%3A+Part+3+-+Networking+Revisited" style="border:0" alt="" /></description></item><item><title>Home Lab: Part 2 - Networking Setup</title><link>https://www.technowizardry.net/2021/10/home-lab-part-2-networking-setup/</link><pubDate>Fri, 08 Oct 2021 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2021/10/home-lab-part-2-networking-setup/</guid><summary>&lt;p>Next up in the series, we&amp;rsquo;re going to manually configure all of the network settings to get our flat network home lab. Our flat network should not use any packet encapsulation with all pods and services fully routable to and from the existing network.&lt;/p>
&lt;p>Detailed in the previous post, I want a so-called flat network because packet encapsulation tunnels IP packets inside of other IP packets and creates a separate IP network that runs on-top of my existing network.) I wanted all nodes, pods, and services to be fully routable on my home network. Additionally, I had several &lt;a class="link" href="https://www.sonos.com/" target="_blank" rel="noopener"
>Sonos speakers&lt;/a> and other smart-home devices that I wanted to be control from my k8s cluster which required pods that ran on the same subnet as my other software.&lt;/p></summary><description>&lt;p>Next up in the series, we&amp;rsquo;re going to manually configure all of the network settings to get our flat network home lab. Our flat network should not use any packet encapsulation with all pods and services fully routable to and from the existing network.&lt;/p>
&lt;p>Detailed in the previous post, I want a so-called flat network because packet encapsulation tunnels IP packets inside of other IP packets and creates a separate IP network that runs on-top of my existing network.) I wanted all nodes, pods, and services to be fully routable on my home network. Additionally, I had several &lt;a class="link" href="https://www.sonos.com/" target="_blank" rel="noopener"
>Sonos speakers&lt;/a> and other smart-home devices that I wanted to be control from my k8s cluster which required pods that ran on the same subnet as my other software.&lt;/p>
&lt;h2 id="install-cni-plugin">Install CNI Plugin&lt;/h2>
&lt;p>The CNI (Container Network Interface) plugin is responsible configuring the network adapter that each Kubernetes pod has. Since each pod usually gets a separate network namespace isolated from the host&amp;rsquo;s main network adapter, without it, no pod could make any network calls. For more information, check out &lt;a class="link" href="https://www.cni.dev/" target="_blank" rel="noopener"
>cni.dev&lt;/a> or the &lt;a class="link" href="https://kubernetes.io/docs/concepts/cluster-administration/networking/" target="_blank" rel="noopener"
>K8s documentation&lt;/a>.&lt;/p>
&lt;p>&lt;strong>IP Network Plan&lt;/strong>&lt;/p>
&lt;p>I already have an existing home network IP space, so instead of changing everything, I&amp;rsquo;m going to define a network plan that fits around that. Readers can use their own network plan, however this blog series will reference these ranges. The only requirement is that the different subnets don&amp;rsquo;t overlap with each other.&lt;/p>
&lt;ul>
&lt;li>192.168.2.1/32 - Main router&lt;/li>
&lt;li>192.168.2.0/24 - Home network subnet&lt;/li>
&lt;li>192.168.2.225/32 - The Host VM IP&lt;/li>
&lt;li>192.168.4.0/24 - Kubernetes pod subnet&lt;/li>
&lt;li>192.168.6.0/24 - MetalLB subnet&lt;/li>
&lt;/ul>
&lt;p>We&amp;rsquo;re going to use Calico as our CNI because it supports a flat network. Our goal is to use this networking option outlined &lt;a class="link" href="https://docs.projectcalico.org/networking/determine-best-networking" target="_blank" rel="noopener"
>here&lt;/a>:&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2021/10/home-lab-part-2-networking-setup/images/image-10.png"
width="598"
height="214"
srcset="https://www.technowizardry.net/2021/10/home-lab-part-2-networking-setup/images/image-10_hu_792536131d5c557b.png 480w, https://www.technowizardry.net/2021/10/home-lab-part-2-networking-setup/images/image-10_hu_69e6fedb21dfaed5.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="279"
data-flex-basis="670px"
>&lt;/p>
&lt;p>This will display any overlay network (no packet encapsulation) and use BGP enable all networks and pods to communicate. BGP (Border Gateway Protocol) is a very popular routing protocol used by ISPs to tell other ISPs what IP addresses are available on their network. It&amp;rsquo;s also being popularized inside large data centers as a mechanism to route packets to the correct rack of servers.&lt;/p>
&lt;p>To start setting up Calico, follow along with the &lt;a class="link" href="https://docs.projectcalico.org/getting-started/kubernetes/quickstart#install-calico" target="_blank" rel="noopener"
>Quick Start Guide&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">kubectl create -f https://docs.projectcalico.org/manifests/tigera-operator.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Once the Tigera operator is installed and running on your cluster, configure Calico by creating a custom resource:&lt;/p>
&lt;p>This snippet will install all Calico software and agents onto the cluster.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">operator.tigera.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Installation&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">controlPlaneNodeSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">node-role.kubernetes.io/master&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">calicoNetwork&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bgp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Enabled&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostPorts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Enabled&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ipPools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">blockSize&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">26&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cidr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">192.168.7.0&lt;/span>&lt;span class="l">/24&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">encapsulation&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">None&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">natOutgoing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Disabled&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nodeSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">all()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">typhaDeployment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nodeSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kubernetes.io/os&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">linux&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">node-role.kubernetes.io/master&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">operator.tigera.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">APIServer &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This will configure Calico to run in a BGP mode without any type of encapsulation.&lt;/p>
&lt;p>Note: The nodeSelector feature is important. Without it, the cluster may fail to start up if the Calico control software is stuck being scheduled on a down node.&lt;/p>
&lt;p>Now it&amp;rsquo;s time to configure BGP. We&amp;rsquo;ll need to configure both the Router and K8s cluster.&lt;/p>
&lt;p>&lt;strong>Router&lt;/strong>&lt;/p>
&lt;p>Your router may be different, but my EdgeRouter had native support for BGP. The following block configures the router to accept and make connections to the Calico node running on my VM with the correct AS number. The AS number (remote-as 64512 and bgp 64512) uniquely identifies each BGP &amp;ldquo;network&amp;rdquo; and defines a rudimentary (and bad) security control. For the purposes of this series, we&amp;rsquo;ll use the same number.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">protocols {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> # 64512 is the AS number for both the router and Calico
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> # This runs the peering as an iBGP (Internal) network
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> bgp 64512 {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> neighbor 192.168.2.225 {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> remote-as 64512
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> parameters {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> router-id 192.168.2.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>Kubernetes&lt;/strong>&lt;/p>
&lt;p>Create a BGP Peer relationship with the router.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">crd.projectcalico.org/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">BGPPeer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-global-peer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">peerIP&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">192.168.2.1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># The IP address of the LAN router&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">asNumber&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">64512&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># AS number of the router&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This should cause Calico to connect to your router. You can verify this by SSHing to the router and checking peering stats.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ show ip bgp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BGP table version is 47, local router ID is 192.168.2.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Status codes: s suppressed, d damped, h history, * valid, &amp;gt; best, i - internal, l - labeled
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> S Stale
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Origin codes: i - IGP, e - EGP, ? - incomplete
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Network Next Hop Metric LocPrf Weight Path
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">*&amp;gt;i 192.168.4.192/26 192.168.2.225 0 100 0 i
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Total number of prefixes 1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Here we can see that Calico set the node to use IPs within the range 192.168.4.192/26. All pods running on this node should fall within this CIDR.&lt;/p>
&lt;p>Now you should be able to launch pods that IP addresses that you can directly connect to.&lt;/p>
&lt;h2 id="install-metallb">Install MetalLB&lt;/h2>
&lt;p>&lt;a class="link" href="https://metallb.universe.tf/installation/" target="_blank" rel="noopener"
>Reference&lt;/a>&lt;/p>
&lt;p>&lt;a class="link" href="https://metallb.universe.tf/installation/" target="_blank" rel="noopener"
>Add a helm catalog for MetalLB&lt;/a>&lt;/p>
&lt;p>When installing metallb, use the following values.&lt;/p>
&lt;p>Note: MetalLB also uses BGP to announce routes, however this won&amp;rsquo;t work with Calico also announcing routes because BGP only permits one connection at a time. This problem is documented extensively &lt;a class="link" href="https://metallb.universe.tf/configuration/calico/" target="_blank" rel="noopener"
>here&lt;/a>. However, thanks to this &lt;a class="link" href="https://github.com/projectcalico/confd/pull/385" target="_blank" rel="noopener"
>Pull Request&lt;/a>, we can disable the MetalLB speaker so that Calico announces routes for each Kubernetes LoadBalancer instance to the router for us.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">configInline&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">address-pools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">addresses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Define a separate IP pool that LBs will be allocated from&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Must not overlap with any other pool&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="m">192.168.6.0&lt;/span>&lt;span class="l">/24&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bgp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">speaker&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>At this point, MetalLB will be installed. Now you can create an L4 LoadBalancer in Kubernetes and it should be announced over BGP:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ show ip bgp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BGP table version is 47, local router ID is 192.168.2.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Status codes: s suppressed, d damped, h history, * valid, &amp;gt; best, i - internal, l - labeled
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> S Stale
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Origin codes: i - IGP, e - EGP, ? - incomplete
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Network Next Hop Metric LocPrf Weight Path
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">*&amp;gt;i 192.168.4.192/26 192.168.2.225 0 100 0 i
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">*&amp;gt;i 192.168.6.0 192.168.2.225 0 100 0 i
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Total number of prefixes 2
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Here we can see that the load balancer at IP 192.168.6.0/32 is now being announced and I should be able to open that up in my browser and access it.&lt;/p>
&lt;p>After this, you should be able to access a LoadBalancer type service running in your Kubernetes cluster from any machine on your LAN. However, Pods are not yet running on the same subnet as my LAN. Thus the smart-home software will not work without running it on the hostNetwork. In future posts, I will expose alternative networking solutions to fix this.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2021%2F10%2Fhome-lab-part-2-networking-setup%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Home+Lab%3A+Part+2+-+Networking+Setup" style="border:0" alt="" /></description></item><item><title>Home Lab: Part 1 - Cluster Setup</title><link>https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/</link><pubDate>Thu, 07 Oct 2021 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/</guid><summary>&lt;p>I recently setup a Kubernetes cluster home lab and wanted to do it the hard-way and share it with you. I setup a home lab so I could run my smart home software and learn more about different Kubernetes networking technologies.&lt;/p>
&lt;p>This blog post is broken up into several sections. Feel free to skip directly to the section that applies to you.&lt;/p>
&lt;p>When I started I had a few things already:&lt;/p></summary><description>&lt;p>I recently setup a Kubernetes cluster home lab and wanted to do it the hard-way and share it with you. I setup a home lab so I could run my smart home software and learn more about different Kubernetes networking technologies.&lt;/p>
&lt;p>This blog post is broken up into several sections. Feel free to skip directly to the section that applies to you.&lt;/p>
&lt;p>When I started I had a few things already:&lt;/p>
&lt;ul>
&lt;li>I was already using &lt;a class="link" href="https://rancher.com/" target="_blank" rel="noopener"
>Rancher&lt;/a> as a UI to manage my Kubernetes clusters on my dedicated servers&lt;/li>
&lt;li>A Windows computer that can run K8s&lt;/li>
&lt;li>A &lt;a class="link" href="https://www.ui.com/edgemax/edgerouter-12/" target="_blank" rel="noopener"
>Ubiquiti EdgeRouter 12&lt;/a> acting as my home network&amp;rsquo;s router&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Requirements&lt;/strong>&lt;/p>
&lt;p>I wanted a fully flat network, that means no packet encapsulation. Packet encapsulation tunnels IP packets inside of other IP packets and creates a separate IP network that runs on-top of my existing network.) I wanted all nodes, pods, and services to be fully routable on my home network. Additionally, I had several &lt;a class="link" href="https://www.sonos.com/" target="_blank" rel="noopener"
>Sonos speakers&lt;/a> and other smart-home devices that I wanted to be control from my k8s cluster which required pods that ran on the same IP network.&lt;/p>
&lt;p>&lt;strong>Alternatives&lt;/strong>&lt;/p>
&lt;p>Docker Desktop and WSL2 are both great for development Docker projects where you use the Docker CLI, but when you try to run Kubernetes you&amp;rsquo;ll quickly run into networking issues. WSL2 and Docker Desktop can&amp;rsquo;t expose services to the rest of your network very easily because they use NAT&amp;rsquo;d network adapters. (GitHub &lt;a class="link" href="https://github.com/microsoft/WSL/issues/4150" target="_blank" rel="noopener"
>microsoft/WSL#4150&lt;/a>) This means you can&amp;rsquo;t expose nodes or pods as devices on the network, they will always be NAT&amp;rsquo;d to the host&amp;rsquo;s IP address. This failed my requirement.&lt;/p>
&lt;h2 id="cluster-setup">Cluster Setup&lt;/h2>
&lt;p>What operating system should we use to run the nodes? There&amp;rsquo;s a few different options, like Flatcar Linux, K3OS, etc.&lt;/p>
&lt;p>&lt;strong>NOTE:&lt;/strong> At the time of writing this blog post, I went with RancherOS just because I was vaguely familiar with it, since then I&amp;rsquo;ve learned that RancherOS is no-longer maintained and new replacements are coming. In a future post, I&amp;rsquo;ll replace this one with a new OS recommendation.&lt;/p>
&lt;h3 id="hyper-v-vm-setup">Hyper-V VM Setup&lt;/h3>
&lt;p>I first learned about this strategy from &lt;a class="link" href="https://medium.com/@benjaminabt/install-rancheros-in-hyper-v-564d22c51f8a" target="_blank" rel="noopener"
>this blog post&lt;/a>. While most of the steps are the same, I&amp;rsquo;m going to include a few changes that I did.&lt;/p>
&lt;p>We&amp;rsquo;re going to be using Hyper-V since it comes with Windows 10 and I&amp;rsquo;m already using it for Docker Desktop/WSL2. We&amp;rsquo;re also going to use RancherOS as our base image. RancherOS is a lightweight Linux base OS that comes prepared with Docker.&lt;/p>
&lt;ul>
&lt;li>Download the latest version of RancherOS from &lt;a class="link" href="https://github.com/rancher/os/releases" target="_blank" rel="noopener"
>here&lt;/a> called &lt;em>rancheros-hyperv.iso&lt;/em>&lt;/li>
&lt;li>Open the Hyper-V Manager&lt;/li>
&lt;li>Open the Virtual Switch Manager&lt;/li>
&lt;li>Create a new external virtual switch
&lt;ul>
&lt;li>Associate it with your network adapter. Here I&amp;rsquo;m using my Ethernet adapter&lt;/li>
&lt;li>Make sure to check &lt;em>Allow management operating system to share this network adapter&lt;/em> so that you can continue using the internet on this machine.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-4.png"
width="901"
height="858"
srcset="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-4_hu_bf0a35afb040bd9c.png 480w, https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-4_hu_2cfe7c43dc78f2ed.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="105"
data-flex-basis="252px"
>&lt;/p>
&lt;ul>
&lt;li>Create a new virtual machine.&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image.png"
width="880"
height="666"
srcset="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image_hu_afb8b3337ea33fb2.png 480w, https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image_hu_265fa3057978178.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="132"
data-flex-basis="317px"
>&lt;/p>
&lt;ul>
&lt;li>Use Generation 1 - RancherOS didn&amp;rsquo;t support Gen2&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image.png"
width="880"
height="666"
srcset="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image_hu_afb8b3337ea33fb2.png 480w, https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image_hu_265fa3057978178.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="132"
data-flex-basis="317px"
>&lt;/p>
&lt;ul>
&lt;li>Give enough memory to run all of your software. I&amp;rsquo;d recommend at least 2-3GB&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-2.png"
width="880"
height="666"
srcset="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-2_hu_e461e5d04042e772.png 480w, https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-2_hu_a5b6103de3452faa.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="132"
data-flex-basis="317px"
>&lt;/p>
&lt;ul>
&lt;li>Use the switch you created earlier&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-5.png"
width="880"
height="666"
srcset="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-5_hu_c572b2445ce1eb80.png 480w, https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-5_hu_3482926bba1d3aa9.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="132"
data-flex-basis="317px"
>&lt;/p>
&lt;ul>
&lt;li>Create a virtual hard disk with enough space&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-6.png"
width="880"
height="666"
srcset="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-6_hu_c5fc47afe8a8b94e.png 480w, https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-6_hu_f9ac890385953575.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="132"
data-flex-basis="317px"
>&lt;/p>
&lt;ul>
&lt;li>Point the CD &amp;ldquo;drive&amp;rdquo; to the ISO you downloaded earlier&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-7.png"
width="880"
height="666"
srcset="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-7_hu_885837e7853b9a13.png 480w, https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-7_hu_d93955ed442e705c.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="132"
data-flex-basis="317px"
>&lt;/p>
&lt;h3 id="install-the-os">Install the OS&lt;/h3>
&lt;p>Start the virtual machine.&lt;/p>
&lt;p>It&amp;rsquo;s a little hard to work with Rancher inside the Hyper-V console, so let&amp;rsquo;s create a password so we can login using SSH.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">[rancher@rancher ~]$ sudo passwd rancher
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Changing password for rancher
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">New password: {enter something here}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Retry password: {enter it again}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Use PuTTY to login.&lt;/p>
&lt;p>&lt;img src="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-8.png"
width="603"
height="558"
srcset="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-8_hu_f819f85923d39385.png 480w, https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-8_hu_aadc9b1d6a96413f.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="108"
data-flex-basis="259px"
>&lt;/p>
&lt;p>Create a new config file so we can install RancherOS to the hard drive:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">vi config.yml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Insert the following content and then save it. This will enable you to SSH into the host without using passwords.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">ssh_authorized_keys:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- ssh-rsa {Insert your SSH public key here}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now install to the disk. This will copy the files from the virtual ISO onto the hard drive so you can keep persistent data:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">sudo ros config validate -i ./config.yml &amp;amp;&amp;amp; sudo ros install -c ./config.yml -d /dev/sda
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Eject the install disk by clicking Media (in the menu bar), DVD Drive, Eject rancheros-hyperv.iso&lt;/p>
&lt;p>Reboot the VM&lt;/p>
&lt;h3 id="setup-the-kubernetes-cluster">Setup the Kubernetes cluster&lt;/h3>
&lt;p>Now it&amp;rsquo;s time to install K8s.&lt;/p>
&lt;ul>
&lt;li>Open up your Rancher UI&lt;/li>
&lt;li>Create a new cluster. It should be a custom cluster using existing nodes&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-9.png"
width="932"
height="292"
srcset="https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-9_hu_63948f8bfc9d2967.png 480w, https://www.technowizardry.net/2021/10/run-bgp-and-kubernetes-at-home/images/image-9_hu_f591ab7484ffb4.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="319"
data-flex-basis="766px"
>&lt;/p>
&lt;ul>
&lt;li>Give the cluster a name, then under Cluster Options click &lt;strong>Edit as YAML&lt;/strong>&lt;/li>
&lt;li>Make sure to update the following parameters&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># We&amp;#39;re going to use MetalLB to handle HTTP routing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">none&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">network&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># We&amp;#39;re going to manually install Calico&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">plugin&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">none&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ul>
&lt;li>Make sure to check etcd, Control Plane, and Worker.&lt;/li>
&lt;li>Rancher will give you a docker run command. Run this in your RancherOS install and let it initialize the cluster.&lt;/li>
&lt;li>The cluster will mostly initialize, but several pods will get stuck pending because there&amp;rsquo;s no CNI plugin installed. This is okay. Once Rancher gives you the option to run commands, move forward&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Note&lt;/strong>: Several weeks into running this I rebooted the VM and found out that only certain directories on the RancherOS machine are preserved across reboots. The following directories are preserved:&lt;/p>
&lt;ul>
&lt;li>/home&lt;/li>
&lt;li>/opt&lt;/li>
&lt;li>/var/lib/kubelet&lt;/li>
&lt;/ul>
&lt;p>If you use bind mounts, I&amp;rsquo;d recommend using something like /home/docker/{container}&lt;/p>
&lt;p>At this point, I now have a single Linux node that has a basic Kubernetes cluster configured (but not yet working) installed on a Hyper-V VM. Next I&amp;rsquo;ll configure the network using the CNI.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2021%2F10%2Frun-bgp-and-kubernetes-at-home%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Home+Lab%3A+Part+1+-+Cluster+Setup" style="border:0" alt="" /></description></item><item><title>A precompiled almost-HAML engine in C#</title><link>https://www.technowizardry.net/2020/04/a-precompiled-almost-haml-engine-in-c/</link><pubDate>Sat, 11 Apr 2020 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2020/04/a-precompiled-almost-haml-engine-in-c/</guid><summary>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>This project is still a work in progress, so this article serves as an introduction to the problem space and walks through how the code works.&lt;/p>
&lt;p>In the past when I wrote different web applications, I used Ruby on Rails combined with the &lt;a class="link" href="http://haml.info/" target="_blank" rel="noopener"
>HAML template language&lt;/a>. HAML is my favorite way to write HTML because it is an abstract representation of an HTML DOM combined with a hint of Python syntax.&lt;/p></summary><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>This project is still a work in progress, so this article serves as an introduction to the problem space and walks through how the code works.&lt;/p>
&lt;p>In the past when I wrote different web applications, I used Ruby on Rails combined with the &lt;a class="link" href="http://haml.info/" target="_blank" rel="noopener"
>HAML template language&lt;/a>. HAML is my favorite way to write HTML because it is an abstract representation of an HTML DOM combined with a hint of Python syntax.&lt;/p>
&lt;p>Being an abstract representation means that it doesn&amp;rsquo;t have to directly correspond to what the resulting HTML looks like. This decoupling enables a HAML render engine to reorganize the code to cleaner and simpler.&lt;/p>
&lt;p>Take a look at the following example:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">%html{ lang: &amp;#39;en&amp;#39; }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %head
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %title Hello world!
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %body
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %a{ href: &amp;#39;https://technowizardry.net&amp;#39; }= my_link
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %div{ b: &amp;#39;abc&amp;#39;, a: &amp;#39;xyz&amp;#39; } test
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>In other template engines like Ruby&amp;rsquo;s ERB or C#&amp;rsquo;s Razor, the white space is preserved and whatever indention you add, is included in the output HTML.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">html&lt;/span> &lt;span class="na">lang&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;en&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Hello world!&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">a&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;https://technowizardry.net&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Test&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">a&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">b&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;abc&amp;#34;&lt;/span> &lt;span class="na">a&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;xyz&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>test&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">html&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Indention can be handy when developing, but why waste the space when running production? One could just delete all the spacing in the source code and check this in, but now your code is harder to read. Can we have the best of both worlds?&lt;/p>
&lt;h2 id="the-current-state-of-the-world">The current state of the world&lt;/h2>
&lt;p>I&amp;rsquo;ve started experimenting with the new &lt;a class="link" href="https://github.com/dotnet/coreclr" target="_blank" rel="noopener"
>.net Core framework&lt;/a> a lot because I like the framework and C# as a language. Unfortunately, HAML isn&amp;rsquo;t directly supported and instead the default render engine in ASP.NET MVC is just an low level HTML renderer which has the same problems as we highlighted above.&lt;/p>
&lt;p>Instead I wanted to try to see if I can build my own solution and want to see how far we can push it with performance optimizations. Can we precompile the template into partial HTML streams? Can we optimize the HTML to be more friendly to Gzip? For example, if you have &lt;code>&amp;lt;a class=&amp;quot;foo bar&amp;quot; /&amp;gt;&lt;/code> and &lt;code>&amp;lt;a class=&amp;quot;bar foo&amp;quot; /&amp;gt;&lt;/code> Both of these elements are semantically equivalent and the classes can be ordered consistently so that Gzip can be efficiently compress them.&lt;/p>
&lt;p>Fair warning, this will be prototype code and not ready for production quite yet.&lt;/p>
&lt;h2 id="adding-c-to-the-mix">Adding C# to the mix&lt;/h2>
&lt;p>I found a previous attempt at this called &lt;a class="link" href="https://github.com/NHaml/NHaml" target="_blank" rel="noopener"
>NHaml&lt;/a>. There was quite a bit of work done on it, but it did not support .NET Core and seemed coupled to ASP.NET. I ended up borrowing the parsing logic (with modifications) and writing my own rendering engine.&lt;/p>
&lt;p>But first, let&amp;rsquo;s see some results:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">!!!
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%html{ lang: &amp;#39;en&amp;#39; }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %head
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %title Hello world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %meta{ charset: &amp;#39;utf-8&amp;#39; }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %meta{ content: &amp;#39;width=device-width, initial-scale=1.0, maximum-scale=1.0&amp;#39;, name: &amp;#39;viewport&amp;#39; }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %body
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> .page-wrap{ class: DateTime.Now.ToString(&amp;#34;yyyy&amp;#34;), d: &amp;#39;bar&amp;#39;, a: &amp;#39;foo&amp;#39; }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> = DateTime.Now.ToString(&amp;#34;yyyy-mm-dd&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %h1= new Random().Next().ToString()
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %p= model.ToString()
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> .content-pane.container
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - if (true)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - if (1 &amp;gt; 0)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %div really true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %div Is True
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - else
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %div wat
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - if (false)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> %div Is False
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> .modal-backdrop.in
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Gets compiled into the following, then the cached class is called for following executions.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="k">using&lt;/span> &lt;span class="nn">System&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">using&lt;/span> &lt;span class="nn">System.IO&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">internal&lt;/span> &lt;span class="kd">sealed&lt;/span> &lt;span class="k">class&lt;/span> &lt;span class="err">\&lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_haml&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_UserCode&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_CompilationTarget&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">private&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="n">model&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">public&lt;/span> &lt;span class="err">\&lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_haml&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_UserCode&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_CompilationTarget&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">string&lt;/span> &lt;span class="err">\&lt;/span>&lt;span class="n">_modelType&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">model&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="err">\&lt;/span>&lt;span class="n">_modelType&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">public&lt;/span> &lt;span class="k">void&lt;/span> &lt;span class="n">render&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">TextWriter&lt;/span> &lt;span class="n">textWriter&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">textWriter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;&amp;lt;!DOCTYPE html&amp;gt;&amp;lt;html lang=\\&amp;#34;&lt;/span>&lt;span class="n">en&lt;/span>&lt;span class="err">\\&lt;/span>&lt;span class="s">&amp;#34;&amp;gt;&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;Hello world&amp;lt;/title&amp;gt;&amp;lt;meta charset=\\&amp;#34;&lt;/span>&lt;span class="n">utf&lt;/span>&lt;span class="p">-&lt;/span>&lt;span class="m">8&lt;/span>&lt;span class="err">\\&lt;/span>&lt;span class="s">&amp;#34;/&amp;gt;&amp;lt;meta content=\\&amp;#34;&lt;/span>&lt;span class="n">width&lt;/span>&lt;span class="p">=&lt;/span>&lt;span class="n">device&lt;/span>&lt;span class="p">-&lt;/span>&lt;span class="n">width&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">initial&lt;/span>&lt;span class="p">-&lt;/span>&lt;span class="n">scale&lt;/span>&lt;span class="p">=&lt;/span>&lt;span class="m">1.0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">maximum&lt;/span>&lt;span class="p">-&lt;/span>&lt;span class="n">scale&lt;/span>&lt;span class="p">=&lt;/span>&lt;span class="m">1.0&lt;/span>&lt;span class="err">\\&lt;/span>&lt;span class="s">&amp;#34; name=\\&amp;#34;&lt;/span>&lt;span class="n">viewport&lt;/span>&lt;span class="err">\\&lt;/span>&lt;span class="s">&amp;#34;/&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;div a=\\&amp;#34;&lt;/span>&lt;span class="n">foo&lt;/span>&lt;span class="err">\\&lt;/span>&lt;span class="s">&amp;#34; class=\\&amp;#34;&lt;/span>&lt;span class="n">page&lt;/span>&lt;span class="p">-&lt;/span>&lt;span class="n">wrap&lt;/span> &lt;span class="s">&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&lt;/span> &lt;span class="n">textWriter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">DateTime&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">get&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Now&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">ToString&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;yyyy&amp;#34;&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">textWriter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34; d=\\&amp;#34;&lt;/span>&lt;span class="n">bar&lt;/span>&lt;span class="err">\\&lt;/span>&lt;span class="s">&amp;#34; \\&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="s">&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&lt;/span> &lt;span class="n">textWriter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">DateTime&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">get&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Now&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">ToString&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;yyyy-mm-dd&amp;#34;&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">textWriter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;&amp;lt;h1&amp;gt;&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">textWriter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="n">Random&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">Next&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">ToString&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">textWriter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">textWriter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">model&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ToString&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">textWriter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;&amp;lt;/p&amp;gt;&amp;lt;div class=\\&amp;#34;&lt;/span>&lt;span class="n">container&lt;/span> &lt;span class="n">content&lt;/span>&lt;span class="p">-&lt;/span>&lt;span class="n">pane&lt;/span>&lt;span class="err">\\&lt;/span>&lt;span class="s">&amp;#34;/&amp;gt;&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">textWriter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;&amp;lt;div&amp;gt;really true&amp;lt;/div&amp;gt;&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">textWriter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;&amp;lt;div&amp;gt;Is True&amp;lt;/div&amp;gt;&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">textWriter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;&amp;lt;/div&amp;gt;&amp;lt;div class=\\&amp;#34;&lt;/span>&lt;span class="k">in&lt;/span> &lt;span class="n">modal&lt;/span>&lt;span class="p">-&lt;/span>&lt;span class="n">backdrop&lt;/span>&lt;span class="err">\\&lt;/span>&lt;span class="s">&amp;#34;/&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Note how the runs of HTML that never changes is transformed into static strings and all elements are normalized consistently.&lt;/p>
&lt;h2 id="a-walk-through-the-code">A walk through the code&lt;/h2>
&lt;p>The &lt;a class="link" href="https://github.com/ajacques/NHaml/blob/master/src/Haml.ASPNet.Core/HamlView.cs" target="_blank" rel="noopener"
>HamlView&lt;/a> is the effective entry point. It checks to see if it has a cached copy of the template in memory, if not then it requests a compilation.&lt;/p>
&lt;p>[ To Be continued]&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2020%2F04%2Fa-precompiled-almost-haml-engine-in-c%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=A+precompiled+almost-HAML+engine+in+C%23" style="border:0" alt="" /></description></item><item><title>Best Practices for Elasticsearch mappings</title><link>https://www.technowizardry.net/2019/11/best-practices-for-elasticsearch-mappings/</link><pubDate>Sun, 24 Nov 2019 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2019/11/best-practices-for-elasticsearch-mappings/</guid><summary>&lt;p>At first, Elasticsearch may appear to be schemaless since you can add new fields any time you want, but every field in a document must match the mapping.&lt;/p>
&lt;h2 id="dynamic-templates-reduce-boilerplate">Dynamic Templates reduce boilerplate&lt;/h2>
&lt;p>How many times have you opened up a mapping file to something like this where the same type definition is repeated over and over again?&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;foo&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;keyword&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;foo&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;keyword&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;foo&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;keyword&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;baz&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;keyword&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;other&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>It&amp;rsquo;s super easy to refactor this into an alternative where by default all string values are mapped as keyword, except for the specific field listed as &amp;ldquo;text&amp;rdquo;.&lt;/p></summary><description>&lt;p>At first, Elasticsearch may appear to be schemaless since you can add new fields any time you want, but every field in a document must match the mapping.&lt;/p>
&lt;h2 id="dynamic-templates-reduce-boilerplate">Dynamic Templates reduce boilerplate&lt;/h2>
&lt;p>How many times have you opened up a mapping file to something like this where the same type definition is repeated over and over again?&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;foo&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;keyword&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;foo&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;keyword&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;foo&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;keyword&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;baz&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;keyword&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;other&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>It&amp;rsquo;s super easy to refactor this into an alternative where by default all string values are mapped as keyword, except for the specific field listed as &amp;ldquo;text&amp;rdquo;.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;dynamic_templates&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;example_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;match_mapping_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;string&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mapping&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;keyword&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;other&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="disable-type-detection">Disable type detection&lt;/h2>
&lt;p>For new fields, Elasticsearch can automatically identify what type to use, but it can be wrong or do unexpected things. For example, I&amp;rsquo;ve seen Elasticsearch accidentally identify a decimal value as a long because the first value to go into the index did not have any decimal points. Then all other documents failed to be indexed because they did not match. This is especially important if you have fields that have a wide range of values (for example, user controlled) because you can&amp;rsquo;t predict if the first value is going to look like a number or a date, when it should always be considered to be a string.&lt;/p>
&lt;p>Reference: &lt;a class="link" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-field-mapping.html" target="_blank" rel="noopener"
>https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-field-mapping.html&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mappings&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;date_detection&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;numeric_detection&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2019%2F11%2Fbest-practices-for-elasticsearch-mappings%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Best+Practices+for+Elasticsearch+mappings" style="border:0" alt="" /></description></item><item><title>Query-level metrics for PostgreSQL/MySQL in Kubernetes with Packetbeat</title><link>https://www.technowizardry.net/2019/01/query-level-metrics-for-postgresql-mysql-in-kubernetes-with-packetbeat/</link><pubDate>Thu, 17 Jan 2019 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2019/01/query-level-metrics-for-postgresql-mysql-in-kubernetes-with-packetbeat/</guid><summary>&lt;p>MySQL and PostgreSQL can be a bit of a black box when running if you don&amp;rsquo;t take the time to configure metrics. How do you identify which queries are slow and need to be optimized? MySQL has the slow log, but that requires a time threshold to log queries that run for longer than &amp;gt;N seconds. What if you want to identify the most common queries even if they are fast?&lt;/p></summary><description>&lt;p>MySQL and PostgreSQL can be a bit of a black box when running if you don&amp;rsquo;t take the time to configure metrics. How do you identify which queries are slow and need to be optimized? MySQL has the slow log, but that requires a time threshold to log queries that run for longer than &amp;gt;N seconds. What if you want to identify the most common queries even if they are fast?&lt;/p>
&lt;p>Elastic.co has a neat product called &lt;a class="link" href="https://www.elastic.co/products/beats" target="_blank" rel="noopener"
>beats&lt;/a> that run small processes that collect information and send them to Elasticsearch and can view them in Kibana. As of now, they have a few different agents:&lt;/p>
&lt;ul>
&lt;li>Filebeat - Reads log files and sends to Elasticsearch. Very similar to Logstash&lt;/li>
&lt;li>Metricbeat - CPU, memory, application usage, etc.&lt;/li>
&lt;li>Packetbeat - Captures network packets and extracts statistics. This is one I&amp;rsquo;m going to use&lt;/li>
&lt;li>Winlogbeat - Windows events&lt;/li>
&lt;li>Auditbeat - Linux audit log&lt;/li>
&lt;/ul>
&lt;p>Packetbeat attaches to a network interface, captures all the packets, and for certain supported protocols it can analyze the packets to extract application-level insights. For MySQL and PostgreSQL, it will include:&lt;/p>
&lt;ul>
&lt;li>The full query (SELECT * FROM foo WHERE baz &amp;hellip;)&lt;/li>
&lt;li>Query run time&lt;/li>
&lt;li>Client IP address&lt;/li>
&lt;li># of rows&lt;/li>
&lt;li>Did the query succeed or fail?&lt;/li>
&lt;li>And a few other attributes&lt;/li>
&lt;/ul>
&lt;p>With this information in Elasticsearch, you can build dashboards and perform analytics.&lt;/p>
&lt;h3 id="getting-it-working">Getting it working&lt;/h3>
&lt;p>If you&amp;rsquo;re already running your database server in Kubernetes, then it&amp;rsquo;s pretty easy to add Packetbeat as a sidecar pod. A sidecar pod is effectively a second Docker container that runs with the same network interface as the primary container. We can use this to our advantage.&lt;/p>
&lt;h3 id="mysqlpostgres">MySQL/Postgres&lt;/h3>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">percona:5.7.14&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">imagePullPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Always&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mysql&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">...]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker.elastic.co/beats/packetbeat:6.5.4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">imagePullPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Always&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">packetbeat&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">securityContext&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowPrivilegeEscalation&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">capabilities&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">add&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">NET_ADMIN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">privileged&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">procMount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readOnlyRootFilesystem&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runAsNonRoot&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/usr/share/packetbeat/packetbeat.yml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">pbconfig&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readOnly&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">subPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">packetbeat.yml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/usr/share/packetbeat/data/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">state&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">configMap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">defaultMode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">292&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mysqlpacketbeat&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">optional&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">beatconfig&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="configmap">ConfigMap&lt;/h2>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">packetbeat.yml&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> packetbeat.interfaces.device: eth0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> packetbeat.protocols:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - type: mysql
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ports: [3306]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> output.elasticsearch:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> hosts:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - http://elasticsearch.elasticsearch.svc.cluster.local:9200
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> index: packetbeat-%{+yyyy.MM}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> setup.kibana:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> host: &amp;#34;kibana.elasticsearch.svc.cluster.local:5601&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> setup.template:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> name: packetbeat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> pattern: packetbeat-*&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mysqlpacketbeat&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2019%2F01%2Fquery-level-metrics-for-postgresql-mysql-in-kubernetes-with-packetbeat%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Query-level+metrics+for+PostgreSQL%2FMySQL+in+Kubernetes+with+Packetbeat" style="border:0" alt="" /></description></item><item><title>Migrate Sprockets to Webpacker with React-rails</title><link>https://www.technowizardry.net/2018/08/migrate-sprockets-to-webpacker-with-react-rails/</link><pubDate>Sat, 25 Aug 2018 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2018/08/migrate-sprockets-to-webpacker-with-react-rails/</guid><summary>&lt;p>Ruby on Rails recently launched support for compiling static assets, such as JavaScript, using &lt;a class="link" href="https://webpack.js.org/" target="_blank" rel="noopener"
>Webpack&lt;/a>. Among other things, Webpack is more powerful at JS compilation when compared to the previous Rails default of &lt;a class="link" href="https://github.com/rails/sprockets" target="_blank" rel="noopener"
>Sprockets&lt;/a>. Integration with Rails is provided by the &lt;a class="link" href="https://github.com/rails/webpacker" target="_blank" rel="noopener"
>Webpacker gem&lt;/a>. Several features that I was interested in leveraging were tree shaking and support for the NPM package repository. With Sprockets, common JS libraries such as ReactJS had to be imported using Gems such as &lt;a class="link" href="https://rubygems.org/gems/react-rails" target="_blank" rel="noopener"
>react-rails&lt;/a> or &lt;a class="link" href="https://rubygems.org/gems/classnames-rails" target="_blank" rel="noopener"
>classnames-rails&lt;/a>. This added friction to adding new dependencies and upgrading to new versions of dependencies.&lt;/p></summary><description>&lt;p>Ruby on Rails recently launched support for compiling static assets, such as JavaScript, using &lt;a class="link" href="https://webpack.js.org/" target="_blank" rel="noopener"
>Webpack&lt;/a>. Among other things, Webpack is more powerful at JS compilation when compared to the previous Rails default of &lt;a class="link" href="https://github.com/rails/sprockets" target="_blank" rel="noopener"
>Sprockets&lt;/a>. Integration with Rails is provided by the &lt;a class="link" href="https://github.com/rails/webpacker" target="_blank" rel="noopener"
>Webpacker gem&lt;/a>. Several features that I was interested in leveraging were tree shaking and support for the NPM package repository. With Sprockets, common JS libraries such as ReactJS had to be imported using Gems such as &lt;a class="link" href="https://rubygems.org/gems/react-rails" target="_blank" rel="noopener"
>react-rails&lt;/a> or &lt;a class="link" href="https://rubygems.org/gems/classnames-rails" target="_blank" rel="noopener"
>classnames-rails&lt;/a>. This added friction to adding new dependencies and upgrading to new versions of dependencies.&lt;/p>
&lt;p>A couple of my projects used react-rails to render React components on the server-side using the legacy Sprockets system. This worked well, but I wanted to migrate to Webpacker to easily upgrade to the newest versions of React and React Bootstrap (previously I imported this using the &lt;a class="link" href="https://github.com/mariopeixoto/react-bootstrap-rails" target="_blank" rel="noopener"
>reactbootstrap-rails&lt;/a>, but this stopped being maintained with the launch of Webpacker.) However, migrating React components to support Webpack required changes to every single file adding ES6-style imports, file moves/renames, and scoping changes. This would have been too large to do all at once. What if there was a way to slowly migrate the JS code from Sprockets to Webpack, making components in either side available to the other side?&lt;/p>
&lt;p>Here outlines a guide on how to migrate an existing Sprockets-based Rails application to start using Webpacker. After this, we&amp;rsquo;ll have 2 different&lt;/p>
&lt;h2 id="enable-webpacker-and-add-react">Enable Webpacker and add React&lt;/h2>
&lt;p>Note: The Webpacker gem includes more &lt;a class="link" href="https://github.com/rails/webpacker#installation" target="_blank" rel="noopener"
>detailed instructions&lt;/a>.&lt;/p>
&lt;p>Add Webpacker gem to Gemfile:&lt;br>
&lt;code>gem 'webpacker'&lt;/code>&lt;br>
This must be loaded in development, production, and during asset compilation.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">rails&lt;/span> &lt;span class="n">webpacker&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">install&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">rails&lt;/span> &lt;span class="n">webpacker&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">install&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">react&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">rails&lt;/span> &lt;span class="n">generate&lt;/span> &lt;span class="n">react&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">install&lt;/span>&lt;span class="err">```&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">## Load React through Webpack&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Now&lt;/span> &lt;span class="n">that&lt;/span> &lt;span class="n">React&lt;/span> &lt;span class="n">has&lt;/span> &lt;span class="n">been&lt;/span> &lt;span class="n">installed&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">is&lt;/span> &lt;span class="n">available&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">the&lt;/span> &lt;span class="n">node&lt;/span>\&lt;span class="n">_modules&lt;/span> &lt;span class="n">folder&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">we&lt;/span> &lt;span class="n">need&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="nb">load&lt;/span> &lt;span class="n">it&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">the&lt;/span> &lt;span class="n">Webpack&lt;/span> &lt;span class="n">JS&lt;/span> &lt;span class="n">context&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">make&lt;/span> &lt;span class="n">it&lt;/span> &lt;span class="n">available&lt;/span> &lt;span class="n">on&lt;/span> &lt;span class="n">the&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">side&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">Sprockets&lt;/span>&lt;span class="o">.&lt;/span> &lt;span class="n">We&lt;/span> &lt;span class="n">can&lt;/span> &lt;span class="n">attach&lt;/span> &lt;span class="n">it&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">window&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">React&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">etc&lt;/span>&lt;span class="o">.&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">share&lt;/span> &lt;span class="n">it&lt;/span> &lt;span class="n">with&lt;/span> &lt;span class="n">other&lt;/span> &lt;span class="n">components&lt;/span>&lt;span class="o">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">javascript&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">packs&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">application&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">js&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">```&lt;/span>&lt;span class="n">javascript&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">import&lt;/span> &lt;span class="n">React&lt;/span> &lt;span class="n">from&lt;/span> &lt;span class="s1">&amp;#39;react&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">import&lt;/span> &lt;span class="n">ReactDOM&lt;/span> &lt;span class="n">from&lt;/span> &lt;span class="s1">&amp;#39;react-dom&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">import&lt;/span> &lt;span class="n">PropTypes&lt;/span> &lt;span class="n">from&lt;/span> &lt;span class="s1">&amp;#39;prop-types&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">global&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">React&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">React&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">global&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ReactDOM&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ReactDOM&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">global&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">PropTypes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">PropTypes&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Don&amp;rsquo;t forget to remove any references to React in your Sprockets JS manifest in app/assets/javascript/application.js.&lt;/p>
&lt;p>Now you can migrate scripts from the app/assets/javascript/* to app/javascripts/*.&lt;/p>
&lt;h2 id="expose-webpack-to-asset-pipeline">Expose Webpack to Asset Pipeline&lt;/h2>
&lt;p>Once you start moving code to the Webpack location, you&amp;rsquo;ll probably see that code in Asset Pipeline that references a component in Webpack won&amp;rsquo;t work. This is because Webpack doesn&amp;rsquo;t export components to the global scope (window.) There&amp;rsquo;s an easy to get this to work, you can add the following to app/javascript/packs/application.js&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// For all components
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">componentRequireContext&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;..&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">component&lt;/span> &lt;span class="k">of&lt;/span> &lt;span class="nx">componentRequireContext&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">keys&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">f&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">endsWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.js&amp;#39;&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">window&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">f&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">substring&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">componentRequireContext&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">f&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="k">default&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This will take all of the JavaScript files that are defined in Webpack and export them to the window. The name of the JavaScript file will be used as the component name, so make sure the name matches the default export in that file. Also, this will prevent Webpack from being able to do any tree shaking, so make sure to remove this once all code is migrated or tweak the path in require.context(&lt;strong>&amp;quot;..&amp;quot;&lt;/strong>, true) to specify a folder that you want to export.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2018%2F08%2Fmigrate-sprockets-to-webpacker-with-react-rails%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Migrate+Sprockets+to+Webpacker+with+React-rails" style="border:0" alt="" /></description></item><item><title>You don't have enough static analysis</title><link>https://www.technowizardry.net/2017/06/you-dont-have-enough-static-analysis/</link><pubDate>Tue, 06 Jun 2017 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2017/06/you-dont-have-enough-static-analysis/</guid><summary>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>Pretty much every programming language out there has tools that statically analyze your source code and detect different problems. These problems can range from simple things like ensuring that you have consistent casing for variable names in Java to ruthlessly enforcing method limits in Ruby. If you&amp;rsquo;ve ever used one of these tools, they may seem overbearing and not worth the hassle, but they will soon prove their value once your application becomes larger, has multiple developers, or is business critical and can&amp;rsquo;t afford outages caused by trivial mistakes. Static analysis tools are a super-low cost solution for improving the quality of a code-base.&lt;/p></summary><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>Pretty much every programming language out there has tools that statically analyze your source code and detect different problems. These problems can range from simple things like ensuring that you have consistent casing for variable names in Java to ruthlessly enforcing method limits in Ruby. If you&amp;rsquo;ve ever used one of these tools, they may seem overbearing and not worth the hassle, but they will soon prove their value once your application becomes larger, has multiple developers, or is business critical and can&amp;rsquo;t afford outages caused by trivial mistakes. Static analysis tools are a super-low cost solution for improving the quality of a code-base.&lt;/p>
&lt;h2 id="the-testing-pyramid">The Testing Pyramid&lt;/h2>
&lt;p>&lt;a class="link" href="images/testing_triangle.svg" >&lt;img src="https://www.technowizardry.net/2017/06/you-dont-have-enough-static-analysis/images/testing_triangle-1.png"
width="256"
height="256"
srcset="https://www.technowizardry.net/2017/06/you-dont-have-enough-static-analysis/images/testing_triangle-1_hu_5b664a2ff9bdd169.png 480w, https://www.technowizardry.net/2017/06/you-dont-have-enough-static-analysis/images/testing_triangle-1_hu_c852594f5e6e2b71.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="100"
data-flex-basis="240px"
>&lt;/a>&lt;/p>
&lt;p>A pyramid with lowest cost/complexity on the bottom and high complexity on the top. From bottom to top: static analysis, unit tests, integration tests, and UI tests.&lt;/p>
&lt;p>How does static analysis fit into the testing pyramid? As you go up the pyramid, the complexity of your tests rise and along with that the cost of implementing them goes up. At the top, you have UI functional tests that execute an entire user story, such as loading a page, logging in etc. These will uncover complex regressions caused by the integration of different components. Since they cover so much code, they are often expensive to implement in developer time and can be brittle, but they&amp;rsquo;re important to uncover certain user issues. At the bottom, you have unit tests that focus on testing a single method or other unit of work. Since they cover only a small piece of code, they can be very fast to run and are less prone accidental breakage. They&amp;rsquo;re usually much cheaper to implement than some of the higher order test cases.&lt;/p>
&lt;p>Static analysis is even cheaper to implement than unit tests, since you can opt-in an entire code base all at once and if you&amp;rsquo;re using an advanced enough IDE, you see the results in real-time. This makes them fit in well at the bottom of the pyramid, below even unit tests.&lt;/p>
&lt;h2 id="moving-failures-earlier">Moving failures earlier&lt;/h2>
&lt;p>Static analysis enables you to move failures that used to occur at run-time or at code review time, all the way to the build-time or even coding time. This reduces the feedback loop time for developers to realize issues&lt;/p>
&lt;h2 id="types-of-checks">Types of Checks&lt;/h2>
&lt;p>Static analysis comes in all shapes and sizes. Depending on what programming language you choose, you might be able to have more detailed type checking (with statically typed languages, like Java.) Below are some examples of different checks that you could enable and enforce:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Style guides&lt;/strong> - If you&amp;rsquo;re a developer at an enterprise, you might have a defined style guide that ensures that developers always use tabs/spaces, place braces in the right location, and consistent naming. These can be strictly enforced at build time to ensure that all code is consistent.&lt;/li>
&lt;li>&lt;strong>Code complexity&lt;/strong> - Methods and files that become too long or have too much complex conditional logic will quickly become difficult to understand and manage. Method length and conditional logic can be monitored and minimized using analysis tools with &lt;a class="link" href="https://en.wikipedia.org/wiki/ABC_score" target="_blank" rel="noopener"
>ABC scoring&lt;/a> or &lt;a class="link" href="https://en.wikipedia.org/wiki/Cyclomatic_complexity" target="_blank" rel="noopener"
>cyclomatic complexity&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Unused/redundant logic&lt;/strong> - Code that&amp;rsquo;s no-longer used, redundant, or is unreachable will make code more confusing and hard to read. Removing this code (which can always be recovered with source control) helps keeps your code cleaner.&lt;/li>
&lt;li>&lt;strong>Code coupling&lt;/strong> - Coupling refers to how much one bit of code depends on another bit of code. Code that&amp;rsquo;s highly coupled can be difficult to refactor or test. Many tools can detect when one class is overly dependent on another and can alert on it.&lt;/li>
&lt;li>&lt;strong>Undefined behavior&lt;/strong> - Some languages have undefined behavior, meaning that the language spec does not define exactly how to compile a block of code. Depending on undefined behavior can be unsafe as different compilers could treat it differently, making your code non-portable.&lt;/li>
&lt;li>&lt;strong>Performance&lt;/strong> - It&amp;rsquo;s quite easy to write code that&amp;rsquo;s non-performant in a critical path. Some of these issues, like inefficient string concatenation in Java, can be easily detected and alerted. This wouldn&amp;rsquo;t be considered a case of premature optimization since generally the fix is trivial and can be done right as the developer is coding.&lt;/li>
&lt;li>&lt;strong>Security&lt;/strong> - Good security practices come at every point of the development process with security driven development. Many security exploits, like buffer overflows, etc., can be detected by a developer using the wrong string method in C++ and can be alerted at development time as opposed to security review time.&lt;/li>
&lt;li>&lt;strong>Other semantic issues&lt;/strong> - Often times code will be syntactically correct and is legal code, but can be confusing to developers.&lt;/li>
&lt;/ul>
&lt;h2 id="case-studies">Case Studies&lt;/h2>
&lt;p>Here&amp;rsquo;s a few examples of interesting issues that could have been discovered with static analysis.&lt;/p>
&lt;h3 id="bad-string-concatenation-causes-poor-performance">Bad string concatenation causes poor performance&lt;/h3>
&lt;p>This is an example of an issue I experienced at my day job. In high-throughput data processing pipelines, the critical path is executed millions of times. This makes it important to watch for mistakes. Take a look at the below block of code and see if you see the mistake:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.slf4j.Logger&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="p">...&lt;/span>&lt;span class="o">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">performLogic&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SomeDomainModel&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">input&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">LOGGER&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">debug&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Performing logic for &amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">type&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>In this case, the application executed the string concatenation regardless of whether or not logging was enabled. This can subtly create a performance bottleneck, especially if the toString is expensive. In many cases, debug logging is disabled in production, causing this string concatenation to be executed and the result be ignored. There&amp;rsquo;s a couple ways that I&amp;rsquo;ve discovered that can detect this particular issue: IntelliJ has an inspection that can be enabled (Internationalization issues) that flags all string concatenation, but it does result in false positives as sometimes string concatenation is valuable. Another tool that directly targets this issue is &lt;a class="link" href="https://github.com/KengoTODA/findbugs-slf4j" target="_blank" rel="noopener"
>findbugs-slf4j&lt;/a>, which will auto flag non static log messages like this one, enabling the logging framework to automatically perform string joining if required.&lt;/p>
&lt;h3 id="cve-2014-1266---apple-ssl-goto-fail">CVE-2014-1266 - Apple SSL Goto Fail&lt;/h3>
&lt;p>This security vulnerability in 2014 caused certain Apple clients to improperly validate TLS certificates. It was caused by a trivial coding mistake and one that could have been caught by many different linting tools.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">((&lt;/span>&lt;span class="n">err&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">SSLHashSHA1&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">hashCtx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">serverRandom&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">goto&lt;/span> &lt;span class="n">fail&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">((&lt;/span>&lt;span class="n">err&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">SSLHashSHA1&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">hashCtx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">signedParams&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">goto&lt;/span> &lt;span class="n">fail&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">goto&lt;/span> &lt;span class="n">fail&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">The&lt;/span> &lt;span class="n">second&lt;/span> &lt;span class="n">bug&lt;/span> &lt;span class="err">\*/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">((&lt;/span>&lt;span class="n">err&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">SSLHashSHA1&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">final&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">hashCtx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">hashOut&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">goto&lt;/span> &lt;span class="n">fail&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Note the extra goto that was indented and was easy to miss. This bug could have been caught with the Clang/GCC compiler flag -Wunreachable-code and GCC includes the -Wmisleading-indentation which also would have flagged this issue.&lt;/p>
&lt;h2 id="example-tools">Example Tools&lt;/h2>
&lt;h2 id="java">Java&lt;/h2>
&lt;p>As Java is a type-safe language, analysis tools can be a lot stricter about enforcing standards around types (more-so than the compiler already does.) There&amp;rsquo;s a few tools that I like to use for all of my applications:&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="https://www.jetbrains.com/idea/" target="_blank" rel="noopener"
>IntelliJ&lt;/a> - My IDE of choice. It comes with a huge library of analyzers, called inspections, that can detect a variety of issues. They highlight directly in the code and some of them can be automatically fixed. I&amp;rsquo;ve enabled a custom set of inspections that are more strict than the default.&lt;/li>
&lt;li>&lt;a class="link" href="http://checkstyle.sourceforge.net/" target="_blank" rel="noopener"
>Checkstyle&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="http://findbugs.sourceforge.net/" target="_blank" rel="noopener"
>FindBugs&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Other tools include:&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="http://fbinfer.com/" target="_blank" rel="noopener"
>FB Infer&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="rubyrails">Ruby/Rails&lt;/h3>
&lt;p>For Ruby developers, a popular static analysis tool is &lt;a class="link" href="https://github.com/bbatsov/rubocop" target="_blank" rel="noopener"
>Rubocop&lt;/a>. If you thought you wrote clean, idiomatic Ruby code take a look at the &lt;a class="link" href="https://github.com/bbatsov/ruby-style-guide" target="_blank" rel="noopener"
>Ruby style guide&lt;/a> that it enforces and see how many rules you may violate.&lt;/p>
&lt;p>Useful tools:&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="https://github.com/bbatsov/rubocop" target="_blank" rel="noopener"
>Rubocop&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/troessner/reek" target="_blank" rel="noopener"
>Reek&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/seattlerb/flog" target="_blank" rel="noopener"
>Flog&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/seattlerb/flay" target="_blank" rel="noopener"
>Flay&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/brigade/haml-lint" target="_blank" rel="noopener"
>haml-lint&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="javascript">JavaScript&lt;/h3>
&lt;p>JS has a huge number of options for static analysis. One of the best ones that I&amp;rsquo;ve found is &lt;a class="link" href="http://eslint.org/" target="_blank" rel="noopener"
>Eslint&lt;/a>. It comes with a number of different linters, but most are disabled by default and you have to explicitly enable them as you want. There&amp;rsquo;s a few pre-defined rule sets available by companies like &lt;a class="link" href="https://github.com/google/eslint-config-google" target="_blank" rel="noopener"
>Google&lt;/a> or &lt;a class="link" href="https://github.com/airbnb/javascript" target="_blank" rel="noopener"
>Airbnb&lt;/a>. Eslint also supports plugins including ones that lint for &lt;a class="link" href="https://github.com/yannickcr/eslint-plugin-react" target="_blank" rel="noopener"
>React&lt;/a>.&lt;/p>
&lt;h3 id="cloudformation">CloudFormation&lt;/h3>
&lt;p>Related Article: &lt;a class="link" href="https://www.technowizardry.net/2017/06/structured-and-auditable-changes-to-infrastructure/" >Structured and auditable changes to infrastructure&lt;/a>&lt;/p>
&lt;p>Another advantage of modelling your infrastructure as code is that it can also be analyzed to detect possible bugs or other problems. As CloudFormation is written in JSON or YAML, you can benefit from first ensuring that the code is syntactically correct, then use a tool such as &lt;a class="link" href="https://github.com/stelligent/cfn_nag" target="_blank" rel="noopener"
>cf_nag&lt;/a> to detect issues like overly permissive S3 bucket permissions or missing security groups.&lt;/p>
&lt;h2 id="legacy-codebases">Legacy codebases&lt;/h2>
&lt;p>For applications that don&amp;rsquo;t already have any analysis running at build-time, it can be difficult to just flip the switch and start enforcing coding standards. Often times the entire application would fail and you would have to spend days fixing everything, adding change risk. My approach has been to enable it at build-time, but then mark most checks as warnings so that developers see issues, and can slowly work to fix the code as they refactor existing code or add new features. Developers working on the code-base can be incentivized to fix a small amount at a time or enforce a single rule at a time, then fix those issues. As long as you continuously work to fix existing issues and increase enforcement, then this approach will slowly increase your code-base quality until you&amp;rsquo;re eventually enforcing all rules.&lt;/p>
&lt;h2 id="summary">Summary&lt;/h2>
&lt;p>The above is just a small sample of the different issues that static analysis can identify and improve and only a small sample of different tools that perform static analysis. It should function as a jumping off point so that you can start thinking about where static analysis makes sense and how you should start using more. Anytime you discover and fix a bug, you should think whether it should be covered with a unit test, integration test, or another static analysis rule.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2017%2F06%2Fyou-dont-have-enough-static-analysis%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=You+don%27t+have+enough+static+analysis" style="border:0" alt="" /></description></item><item><title>Structured and auditable changes to infrastructure</title><link>https://www.technowizardry.net/2017/06/structured-and-auditable-changes-to-infrastructure/</link><pubDate>Fri, 02 Jun 2017 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2017/06/structured-and-auditable-changes-to-infrastructure/</guid><summary>&lt;p>Note: I&amp;rsquo;m going to use AWS services as most of my examples for this post, but that&amp;rsquo;s just because I&amp;rsquo;m most familiar with them, the patterns found below are not limited to just AWS and can be applied to any cloud provider or self-hosted where similar patterns exist.&lt;/p>
&lt;h1 id="introduction">Introduction&lt;/h1>
&lt;p>Every service has some amount of supporting infrastructure required to support it. This includes any virtual servers (EC2 or other), storage (ex. S3, DynamoDB), load balancing, etc. basically any resources that your service uses that is not your direct business logic could be considered infrastructure. If you use continuous integration and change control on your business logic, then why would you not apply the same rules to your infrastructure?&lt;/p></summary><description>&lt;p>Note: I&amp;rsquo;m going to use AWS services as most of my examples for this post, but that&amp;rsquo;s just because I&amp;rsquo;m most familiar with them, the patterns found below are not limited to just AWS and can be applied to any cloud provider or self-hosted where similar patterns exist.&lt;/p>
&lt;h1 id="introduction">Introduction&lt;/h1>
&lt;p>Every service has some amount of supporting infrastructure required to support it. This includes any virtual servers (EC2 or other), storage (ex. S3, DynamoDB), load balancing, etc. basically any resources that your service uses that is not your direct business logic could be considered infrastructure. If you use continuous integration and change control on your business logic, then why would you not apply the same rules to your infrastructure?&lt;/p>
&lt;p>Allowing and requiring developers to make changes using the UI introduces risk that one might make a mistake and bring down your production service. Continuing from my last post about &lt;a class="link" href="https://www.technowizardry.net/2017/04/dynamic-aws-resource-discovery-for-one-click-region-spin-ups/" >infrastructure names&lt;/a>, you could also make a mistake in any of regional clones.&lt;/p>
&lt;h1 id="cloudformation">CloudFormation&lt;/h1>
&lt;p>CloudFormation is service offered for all AWS accounts that enables you to represent any resource as a simple YAML or JSON file. Instead of making changes by clicking through the UI, you configure AWS resources in a YAML or JSON file and submit it to the CloudFormation console.&lt;/p>
&lt;p>An example template might look like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">Resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">WebsiteFileS3Bucket&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AWS::S3::Bucket&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Properties&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">AccessControl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Private&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>You then check this into your source control and apply it on the UI. This creates a new S3 bucket with an auto generated name. Now, you might want to enable static website hosting so you update the template with this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">WebsiteFileS3Bucket&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AWS::S3::Bucket&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Properties&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">AccessControl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Private&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">WebsiteConfiguration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">IndexDocument&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">index.html&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This can be code-reviewed along with any other changes any other developers can discover any mistakes. Infrastructure as code reduces a set of clicks on the console to an easier to read configuration.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl"> WebsiteFileS3Bucket:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Type: AWS::S3::Bucket
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Properties:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> AccessControl: Private
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ WebsiteConfiguration:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ IndexDocument: index.html
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Any issues detected will cause CloudFormation to automatically roll-back to the previously known good template version.&lt;/p>
&lt;p>In summary, there&amp;rsquo;s a number of key benefits of describing your infrastructure as code:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Code reviewed&lt;/strong> - Spot and prevent mistakes from causing outages&lt;/li>
&lt;li>&lt;strong>Audited&lt;/strong> - Know exactly which engineer made a change to an AWS resource&lt;/li>
&lt;li>&lt;strong>Durable&lt;/strong> - Some mistakes are detected and rolled-back automatically by CloudFormation&lt;/li>
&lt;li>&lt;strong>Revision controlled&lt;/strong> - Can follow the history of your infrastructure and git blame as needed&lt;/li>
&lt;li>&lt;strong>Concise&lt;/strong> - Infrastructure as code is simpler to read and understand than a series of UI interactions or shell commands&lt;/li>
&lt;/ul>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2017%2F06%2Fstructured-and-auditable-changes-to-infrastructure%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Structured+and+auditable+changes+to+infrastructure" style="border:0" alt="" /></description></item><item><title>Vending Software Good Practices - Docker Security</title><link>https://www.technowizardry.net/2017/04/vending-software-good-practices-docker-security/</link><pubDate>Wed, 26 Apr 2017 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2017/04/vending-software-good-practices-docker-security/</guid><summary>&lt;p>Docker containers are the latest craze taking the world by storm. They enable software vendors to have more control over how their software is executed reducing the amount of work that software hosters need to be responsible for. By shifting the burden of figuring out environment requirements on to the software vendor, certain critical decisions that help improve security can be made once and only once and distributed to end-users. This reduces the cost barrier of having more stable/secure software as users no-longer have to think about intricacies of security and management, which we can see that users rarely take the time to invest in.&lt;/p></summary><description>&lt;p>Docker containers are the latest craze taking the world by storm. They enable software vendors to have more control over how their software is executed reducing the amount of work that software hosters need to be responsible for. By shifting the burden of figuring out environment requirements on to the software vendor, certain critical decisions that help improve security can be made once and only once and distributed to end-users. This reduces the cost barrier of having more stable/secure software as users no-longer have to think about intricacies of security and management, which we can see that users rarely take the time to invest in.&lt;/p>
&lt;p>Docker containers have a number of different security mechanisms. I won&amp;rsquo;t go into details on that, if you&amp;rsquo;re interested in learning more, make sure to read the &lt;a class="link" href="https://docs.docker.com/engine/security/security/" target="_blank" rel="noopener"
>Docker security documentation page&lt;/a>.&lt;/p>
&lt;h2 id="capabilities">Capabilities&lt;/h2>
&lt;p>In Linux kernels, each process has a set of capability flags that the kernel checks when the process makes certain privileged syscalls. Processes running as root automatically get certain capabilities assigned to it.&lt;/p>
&lt;p>Some example capabilities:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>CAP_NET_BIND_SERVICE&lt;/strong> - Enables processes to bind to ports &amp;lt; 1024. By default, non-root processes can&amp;rsquo;t find to these reserved ports. Dropping this capability prevents even root processes from binding to these ports&lt;/li>
&lt;li>Even more on the &lt;a class="link" href="http://man7.org/linux/man-pages/man7/capabilities.7.html" target="_blank" rel="noopener"
>man page&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>According to the principal of least privilege, running with fewer capabilities will reduce the attack surface of a given piece of software.&lt;/p>
&lt;h3 id="docker-composeyml">Docker compose.yml&lt;/h3>
&lt;p>Docker compose files are a popular way to vendor an entire service stack to users. With it you can describe one or more Docker containers in a YAML-based format. More information is available in the &lt;a class="link" href="https://docs.docker.com/compose/overview/" target="_blank" rel="noopener"
>official docs&lt;/a>. A little used feature enables you to specify which capabilities your service requires.&lt;/p>
&lt;p>For example, this is the configuration that I use for running NGINX on my server:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">nginx&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nginx:1.9.10&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cap_drop&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">ALL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cap_add&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">CHOWN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">DAC_OVERRIDE&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">NET_BIND_SERVICE&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">SETGID&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">SETUID&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>In this example, I enable a whitelist for capabilities instead of using the default list that Docker provides and enable only the minimal capabilities that are required. This list enables NGINX to modify file permissions (for access logs,) bind to port 80 and 443, and change the process user account. The default whitelist is available in the Docker &lt;a class="link" href="https://github.com/moby/moby/blob/master/oci/defaults_linux.go#L62-L77" target="_blank" rel="noopener"
>source code here&lt;/a>. Based on this, we&amp;rsquo;re reducing the attack surface that a malicious actor can leverage.&lt;/p>
&lt;p>Docker compose is fully self-contained and doesn&amp;rsquo;t require the user to make any changes to their environment to start using. Docker compose and capabilities are a low-cost way to start reducing the attack surface of an application. Every service owner should attempt to run their application with &amp;ndash;cap-drop ALL, then selectively enable capabilities until their application works, then vend that list as a best practice.&lt;/p>
&lt;h3 id="apparmorsecurity-profiles">AppArmor/Security Profiles&lt;/h3>
&lt;p>Capabilities are a cheap way to begin to improve security, but they can only restrict a limited subset of kernel sys calls, making fine grained security control impossible. This is where mandatory access control and AppArmor strives. For distributions that support it (such as Ubuntu,) AppArmor is an opt-in security model that enables you to whitelist and/or blacklist specific sys calls, along with the parameters of those sys calls. For example, you could configure a Docker container application to only be able to open TCP connections to specific IP ranges and ports. Docker supports the ability to run containers with specific AppArmor profiles. While this requires more work on the user&amp;rsquo;s side to use, security conscious service vendors could vend an AppArmor profile along with their service that users could install. I plan to go into more detail on this in the future.&lt;/p>
&lt;h3 id="conclusion">Conclusion&lt;/h3>
&lt;p>Anybody who builds a Docker container should leverage the security model that Docker provides by running with least privileges and capabilities, then include that configuration in vendor configuration, like Docker compose files. By doing this, your end users all will be able to take advantage of slightly reduced attack surface area, with only minimal effort on your side. Capabilities are in no way fool-proof, and one should never believe that they will significantly reduce the attack surface, but it&amp;rsquo;s better than nothing.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2017%2F04%2Fvending-software-good-practices-docker-security%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Vending+Software+Good+Practices+-+Docker+Security" style="border:0" alt="" /></description></item><item><title>Dynamic AWS resource discovery for one-click region spin-ups</title><link>https://www.technowizardry.net/2017/04/dynamic-aws-resource-discovery-for-one-click-region-spin-ups/</link><pubDate>Tue, 04 Apr 2017 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2017/04/dynamic-aws-resource-discovery-for-one-click-region-spin-ups/</guid><summary>&lt;p>&lt;strong>Disclaimer&lt;/strong>: At the time of this article&amp;rsquo;s writing, I work at Amazon, but not in AWS. This article is based on my own research and ideas and is not the official position of Amazon. This article is not intended as marketing material for AWS, only as some architectural patterns for you to use if you do leverage AWS.&lt;/p>
&lt;p>AWS provides a number of different resources that you can use to build services using, including S3 buckets, SQS queues, etc. When you create a new instance of that resource, you must pick a name that usually must be unique in a given namespace. Depending on your naming scheme, you may also have to start embedding resource names in code or configuration files. This makes spinning up new regions difficult as now you have to update configuration with names for every stage/region that you might use. This may not seem like that big of a deal, but consider that you may have tens of different SQS queues, S3 buckets, etc. for each region/stage. This can begin to combinatorically explode as you now have &lt;code># regions * # stages * # resources&lt;/code> of different configuration definitions. This results in a lot of boilerplate.&lt;/p></summary><description>&lt;p>&lt;strong>Disclaimer&lt;/strong>: At the time of this article&amp;rsquo;s writing, I work at Amazon, but not in AWS. This article is based on my own research and ideas and is not the official position of Amazon. This article is not intended as marketing material for AWS, only as some architectural patterns for you to use if you do leverage AWS.&lt;/p>
&lt;p>AWS provides a number of different resources that you can use to build services using, including S3 buckets, SQS queues, etc. When you create a new instance of that resource, you must pick a name that usually must be unique in a given namespace. Depending on your naming scheme, you may also have to start embedding resource names in code or configuration files. This makes spinning up new regions difficult as now you have to update configuration with names for every stage/region that you might use. This may not seem like that big of a deal, but consider that you may have tens of different SQS queues, S3 buckets, etc. for each region/stage. This can begin to combinatorically explode as you now have &lt;code># regions * # stages * # resources&lt;/code> of different configuration definitions. This results in a lot of boilerplate.&lt;/p>
&lt;p>But what if there was a better way?&lt;/p>
&lt;p>Here I propose a slightly different method of naming and interacting with resources such that you don&amp;rsquo;t need to manually pick names for S3 buckets and hope nobody else is using them.&lt;/p>
&lt;p>For example, an S3 bucket name must be globally unique across all other AWS customers. This is because each S3 bucket exists in DNS as &lt;code>https://{bucket_name}.s3.amazonaws.com&lt;/code>.&lt;/p>
&lt;p>Say I have a service &amp;lsquo;Foo Service&amp;rsquo; and I currently operate in one region and am not planning on expanding to other regions. I might create the S3 bucket &lt;code>foo-service&lt;/code>, then I add some config to use my bucket:&lt;/p>
&lt;p>s3BucketName=foo-service&lt;/p>
&lt;p>This works fine until I need to scale up to another region and create a new bucket &lt;code>foo-service-eu-west-1&lt;/code>. Now my config will continue to grow for every region:&lt;/p>
&lt;p>s3BucketName.us-east-1=foo-service
s3BucketName.eu-west-1=foo-service-eu-west-1&lt;/p>
&lt;p>While it does not look like much, it still multiplies as you have more resources and regions and it increases the risks of a mistake during a critical time of spinning up a new region. What other options are there?&lt;/p>
&lt;p>Say I know I&amp;rsquo;ll need an S3 bucket in all regions that I operate: us-east-1, us-west-2, and eu-west-1. I might configure my service to use the bucket named &lt;code>foo-service-{region}&lt;/code> (automatically replacing {region} with whatever region the service is running in.) I then create three buckets: &lt;code>foo-service-us-east-1&lt;/code>, &lt;code>foo-service-us-west-2&lt;/code>, &lt;code>foo-service-eu-west-1&lt;/code>. This works great until somebody else discovers the pattern and creates a bucket with the name &lt;code>foo-service-ap-northeast-1&lt;/code>. Now I&amp;rsquo;m stuck and have to rework the bucket logic to support other naming patterns to get around this.&lt;/p>
&lt;p>Another risk with using the patterned approach is that I have hard-coded the fact that the only parameter in the resource name is {region}. I may want to add a testing environment and add a stage parameter such that I have &lt;code>foo-service-{region}-{stage}&lt;/code>. This is difficult once you&amp;rsquo;re in production and have critical data stored in buckets that you can&amp;rsquo;t remove. Another strategy for ensuring high-availability is to shard across many different AWS accounts and stacks to reduce the blast radius of something going wrong. If you don&amp;rsquo;t predict this requirement during design, this might come back and become more difficult to fix later.&lt;/p>
&lt;p>Instead I should configure my service to connect to the AWS account and automatically detect the correct bucket. If I were using CloudFormation to configure my resources (which is a separate article on why it&amp;rsquo;s powerful,) then I could just delegate naming of the bucket to CF and it&amp;rsquo;ll automatically generate a unique bucket name for me.&lt;/p>
&lt;p>&lt;a class="link" href="http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html#cfn-s3-bucket-name" target="_blank" rel="noopener"
>CloudFormation API Documentation&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">WidgetBucket&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AWS::S3::Bucket&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Properties&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Tags&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">Name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bucket-purpose&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">widget-storage&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This CloudFormation snippet will automatically generate a new randomly generate bucket with a name like &lt;code>fooservice-widgetbucket-abc123&lt;/code> and a tag &lt;code>foo-service:bucket-purpose=widget-storage&lt;/code>. During application start-up, I know I need the &amp;lsquo;widget-storage&amp;rsquo; bucket so I just need to find the bucket.&lt;/p>
&lt;p>Sample code:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">findBucketName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">AmazonS3&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s3Client&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">bucketUseTag&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">buckets&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s3Client&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">listBuckets&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">for&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Bucket&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">bucket&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">buckets&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">BucketTaggingConfiguration&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">tags&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s3Client&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getBucketTaggingConfiguration&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">bucket&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getName&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tags&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">==&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">continue&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">tags&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getTagSet&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">getTag&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;bucket-purpose&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">bucketUseTag&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">equals&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">bucket&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getName&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And now you can spin up as many regions, stages, and AWS accounts you want using CloudFormation and your service can automatically discover the correct bucket assuming you have a provisioned AWS access key. This works even better when you use IAM instance roles to auto distribute credentials to hosts.&lt;/p>
&lt;h3 id="what-about-sqs">What about SQS?&lt;/h3>
&lt;p>The same problem can also impact other AWS resource types, such as SQS queues. While an SQS queue name doesn&amp;rsquo;t have to be globally unique, it still can result in boilerplate configuration if if you have to hard-code every permutation of a given queue name in your config. An SQS queue name only needs to be unique inside of a given AWS Account + Region, since its fully-qualified URL is &lt;code>https://{region}.sqs.amazonaws.com/{account-id}/{queue-name}&lt;/code>.&lt;/p>
&lt;p>One solution for SQS queues to use static queue names across all regions and stages (i.e. not &lt;code>WorkQueue-us-west-2&lt;/code> or &lt;code>WorkQueue-Beta&lt;/code>,) instead just &lt;code>WorkQueue&lt;/code>. Then on application start-up use the &lt;a class="link" href="http://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_GetQueueUrl.html" target="_blank" rel="noopener"
>GetQueueUrl&lt;/a> API call to fetch the URL and use that.&lt;/p>
&lt;p>While the SQS queue example might seem obvious, but I&amp;rsquo;ve seen a number of examples of services using the configuration approach when they don&amp;rsquo;t need to.&lt;/p>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2017%2F04%2Fdynamic-aws-resource-discovery-for-one-click-region-spin-ups%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Dynamic+AWS+resource+discovery+for+one-click+region+spin-ups" style="border:0" alt="" /></description></item><item><title>Fast development environments</title><link>https://www.technowizardry.net/2015/03/fast-development-environments/</link><pubDate>Tue, 31 Mar 2015 00:00:00 +0000</pubDate><guid>https://www.technowizardry.net/2015/03/fast-development-environments/</guid><summary>&lt;p>Update from 2023: I have long since stopped using this mechanism. However I leave it here in case you find it useful.&lt;/p>
&lt;p>Setting up new hosts entries for every different web site that you develop is hard. This workflow allows you to completely automate it. First thing you&amp;rsquo;ll want to do is setup a wildcard DNS record that points to your host. This allows you to dynamically setup new development websites without having create new DNS records for each one of them. I created a fake internal-only TLD on my local network&amp;rsquo;s DNS server that automatically returns the IP address of my development VM for any query to &lt;code>*.devvm&lt;/code>. If you don&amp;rsquo;t have access to that, you could re-use an actual domain and automatically forward something like &lt;code>*.dev.technowizardry.net&lt;/code> to the VM. For example, I have the ASUS RT-AC68U router for my personal network. So I SSH&amp;rsquo;d to the router, typed vi /etc/dnsmasq.conf, then appended:&lt;/p></summary><description>&lt;p>Update from 2023: I have long since stopped using this mechanism. However I leave it here in case you find it useful.&lt;/p>
&lt;p>Setting up new hosts entries for every different web site that you develop is hard. This workflow allows you to completely automate it. First thing you&amp;rsquo;ll want to do is setup a wildcard DNS record that points to your host. This allows you to dynamically setup new development websites without having create new DNS records for each one of them. I created a fake internal-only TLD on my local network&amp;rsquo;s DNS server that automatically returns the IP address of my development VM for any query to &lt;code>*.devvm&lt;/code>. If you don&amp;rsquo;t have access to that, you could re-use an actual domain and automatically forward something like &lt;code>*.dev.technowizardry.net&lt;/code> to the VM. For example, I have the ASUS RT-AC68U router for my personal network. So I SSH&amp;rsquo;d to the router, typed vi /etc/dnsmasq.conf, then appended:&lt;/p>
&lt;p>&lt;code>address=/.devvm/192.168.1.155&lt;/code>&lt;/p>
&lt;p>then ran:&lt;/p>
&lt;p>&lt;code>killall dnsmasq &amp;amp;&amp;amp; dnsmasq --log-async&lt;/code>&lt;/p>
&lt;p>Then you should be able to resolve *.devvm on any host on the local network. Next you&amp;rsquo;re going to want to setup NGINX on your dev machine. Just install the latest version using:&lt;/p>
&lt;p>&lt;code>sudo apt-get install nginx docker&lt;/code>&lt;/p>
&lt;p>NGINX will bind to port 80 and automatically forward HTTP requests to the correct web app. Then to automatically configure the site bindings in NGINX, I use a tool called &lt;a class="link" href="https://github.com/jwilder/docker-gen" target="_blank" rel="noopener"
>docker-gen&lt;/a> to automatically listen to Docker events and reload NGINX when I start and stop Docker containers. Then To run Docker-gen I do:&lt;/p>
&lt;p>&lt;code>./docker-gen -only-exposed -watch -notify &amp;quot;/usr/sbin/service nginx reload&amp;quot; nginx.tmpl /etc/nginx/sites-enabled/docker_autoconf&lt;/code>&lt;/p>
&lt;p>The nginx.tmpl file can be found below. Any Docker container that has the VIRTUAL_HOST environmental variable will get registered in NGINX as a web site. docker-compose.yml:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">web&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">build&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/bin/sh -c &amp;#39;rm -f tmp/pids/server.pid &amp;amp;&amp;amp; cat &amp;amp;&amp;amp; rails s -b 0.0.0.0&amp;#39;&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">.:/rails-app&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expose&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="m">3000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">RAILS_ENV&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">development&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">VIRTUAL_HOST&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">certmgr.devvm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>nginx.tmpl:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">{{ range $host, $containers := groupBy $ &amp;#34;Env.VIRTUAL\_HOST&amp;#34; }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">upstream {{ $host }} {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{ range $index, $value := $containers }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{ with $address := index $value.Addresses 0 }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> server {{ $address.IP }}:{{ $address.Port }};
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{ end }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{ end }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">server {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> gzip\_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> server\_name {{ $host }};
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> location / {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> try\_files $uri @ruby;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> location @ruby {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> proxy\_pass http://{{ $host }};
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> include /etc/nginx/proxy\_params;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{ end }}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>
&lt;img referrerpolicy="no-referrer-when-downgrade" src="https://metrics.technowizardry.net/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fwww.technowizardry.net%2F2015%2F03%2Ffast-development-environments%2F%3Fmtm_campaign%3Drss&amp;apiv=1&amp;action_name=Fast+development+environments" style="border:0" alt="" /></description></item></channel></rss>