<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://maxromanovsky.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://maxromanovsky.com/" rel="alternate" type="text/html" /><updated>2025-12-26T12:34:15+00:00</updated><id>https://maxromanovsky.com/feed.xml</id><title type="html">Maksym Romanowski</title><subtitle>My homepage. Maksym Romanowski aka maxromanovsky. Occasional blogging, basic info. MLOps, DevOps, Solutions Architect, nerd, traveler, binge-watcher, newbie gamer.
</subtitle><author><name>Maksym Romanowski</name></author><entry><title type="html">Running TP-Link Tapo cameras without access to internet</title><link href="https://maxromanovsky.com/blog/local-mode/2025-12-26-tplink-tapo-camera-local-only.html" rel="alternate" type="text/html" title="Running TP-Link Tapo cameras without access to internet" /><published>2025-12-26T00:00:00+00:00</published><updated>2025-12-26T00:00:00+00:00</updated><id>https://maxromanovsky.com/blog/local-mode/tplink-tapo-camera-local-only</id><content type="html" xml:base="https://maxromanovsky.com/blog/local-mode/2025-12-26-tplink-tapo-camera-local-only.html"><![CDATA[<h2 id="tldr">TL;DR</h2>
<p>This snippet shows how to run TP-Link Tapo cameras fully locally, without access to internet.</p>

<!--more-->

<h2 id="environment">Environment</h2>
<ul>
  <li>TP-Link Tapo C110 hw 2.0 fw 1.4.7</li>
  <li>TP-Link Tapo C225 hw 2.0 fw 1.1.1</li>
  <li>NTP server running locally that doesn’t use <code class="language-plaintext highlighter-rouge">time.windows.com</code> as a source of time</li>
</ul>

<h2 id="rationale">Rationale</h2>
<p>By default, TP-Link Tapo cameras are connected to cloud for multiple purposes:</p>
<ul>
  <li>share video streams</li>
  <li>perform firmware updates</li>
  <li>sync time</li>
</ul>

<p>Unfortunately, these cameras ignore NTP servers specified via DHCP config,
and seem to rely on a list of hardcoded NTP servers.
One of them is <code class="language-plaintext highlighter-rouge">time.windows.com</code>. You can find others in your firewall log once internet connectivity is blocked.</p>

<h2 id="configuration">Configuration</h2>
<ul>
  <li>Setup cameras via Tapo app with internet connected
    <ul>
      <li>Update firmware if necessary</li>
      <li>Create RTSP account (otherwise what’s the point of running it fully locally?)</li>
      <li>Use DHCP or specify local DNS</li>
    </ul>
  </li>
  <li>Block internet connection from cameras via network firewall (most likely on router)</li>
  <li>Create DNS <code class="language-plaintext highlighter-rouge">A</code> record (on DNS used by Tapo) pointing <code class="language-plaintext highlighter-rouge">time.windows.com</code> to local NTP server</li>
</ul>]]></content><author><name>Maksym Romanowski</name></author><category term="local-mode" /><category term="tp-link-tapo" /><category term="camera" /><category term="tl;dr" /><summary type="html"><![CDATA[TL;DR This snippet shows how to run TP-Link Tapo cameras fully locally, without access to internet.]]></summary></entry><entry><title type="html">Adding Let’s Encrypt TLS certificate to UniFi</title><link href="https://maxromanovsky.com/blog/unifi/2025-12-26-unifi-letsencrypt.html" rel="alternate" type="text/html" title="Adding Let’s Encrypt TLS certificate to UniFi" /><published>2025-12-26T00:00:00+00:00</published><updated>2025-12-26T00:00:00+00:00</updated><id>https://maxromanovsky.com/blog/unifi/unifi-letsencrypt</id><content type="html" xml:base="https://maxromanovsky.com/blog/unifi/2025-12-26-unifi-letsencrypt.html"><![CDATA[<h2 id="tldr">TL;DR</h2>
<p>This snippet installs Let’s Encrypt TLS certificate, ensures it is renewed, and makes sure that UniFi is resolved to LAN IP when accessed via FQDN in TLS certificate.</p>

<!--more-->

<h2 id="environment">Environment</h2>
<ul>
  <li>UniFi OS 4.4.6</li>
  <li>UniFi Network 10.0.162</li>
  <li><a href="https://github.com/kchristensen/udm-le/commit/316a8509063ce5161436c49a844da18de2681d27">kchristensen/udm-le commit <code class="language-plaintext highlighter-rouge">316a8509063ce5161436c49a844da18de2681d27</code></a></li>
</ul>

<h2 id="configuration">Configuration</h2>
<ul>
  <li>Follow installation instructions in <a href="kchristensen/udm-le/commit/316a8509063ce5161436c49a844da18de2681d27">udm-le Readme</a></li>
  <li>Add <code class="language-plaintext highlighter-rouge">A</code> DNS record pointing to UniFi device LAN IP in Policy Engine</li>
</ul>]]></content><author><name>Maksym Romanowski</name></author><category term="unifi" /><category term="unifi" /><category term="letsencrypt" /><category term="tl;dr" /><summary type="html"><![CDATA[TL;DR This snippet installs Let’s Encrypt TLS certificate, ensures it is renewed, and makes sure that UniFi is resolved to LAN IP when accessed via FQDN in TLS certificate.]]></summary></entry><entry><title type="html">My approach to Kubernetes installation &amp;amp; management on bare metal</title><link href="https://maxromanovsky.com/blog/kubernetes/2020-05-01-kubernetes-management.html" rel="alternate" type="text/html" title="My approach to Kubernetes installation &amp;amp; management on bare metal" /><published>2020-05-01T00:00:00+00:00</published><updated>2020-05-01T00:00:00+00:00</updated><id>https://maxromanovsky.com/blog/kubernetes/kubernetes-management</id><content type="html" xml:base="https://maxromanovsky.com/blog/kubernetes/2020-05-01-kubernetes-management.html"><![CDATA[<h2 id="installation">Installation</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">x86_64</code>:
    <ul>
      <li>CoreOS (now <a href="https://www.flatcar-linux.org/">Flatcar Container Linux</a>) <a href="/blog/kubernetes/2019-07-06-coreos-k8s-home-baremetal-01.html">as a Linux distro</a></li>
      <li><a href="https://kubespray.io/">Kubespray</a> as a <a href="/blog/kubernetes/2019-07-29-coreos-k8s-home-baremetal-02.html">Kubernetes installer</a></li>
      <li><a href="https://metallb.universe.tf/">Metallb</a> and <a href="https://kubernetes.github.io/ingress-nginx/">NGINX Ingress Controller</a> for <a href="/blog/kubernetes/2019-12-26-coreos-k8s-home-baremetal-03.html">incoming traffic</a></li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">arm</code> (Raspberry PI 3B)
    <ul>
      <li><a href="https://github.com/hypriot/image-builder-rpi/releases">HypriotOS</a> as a lightweight container-oriented Debian-based Linux</li>
      <li><a href="https://k3s.io/">k3s</a> as a lightweight Kubernetes distribution with <code class="language-plaintext highlighter-rouge">sqlite</code> instead of <code class="language-plaintext highlighter-rouge">etcd</code></li>
      <li><a href="https://metallb.universe.tf/">Metallb</a> and <a href="https://containo.us/traefik/">Traefik v1</a> for incoming traffic</li>
    </ul>
  </li>
</ul>

<h2 id="configuration">Configuration</h2>

<ul>
  <li><a href="https://www.pulumi.com/">Pulumi</a> for <a href="/blog/kubernetes/2020-04-21-pulumi.html">everything except Helm charts</a></li>
  <li><a href="https://github.com/Praqma/helmsman">Helmsman</a> for <a href="/blog/kubernetes/2020-04-24-helmsman.html">Helm charts</a></li>
  <li><a href="https://github.com/sbstp/kubie">kubie</a> for using multiple Kubernetes contexts simultaneously in different terminals</li>
</ul>]]></content><author><name>Maksym Romanowski</name></author><category term="kubernetes" /><category term="kubernetes" /><category term="flatcar" /><category term="coreos" /><category term="hypriotos" /><category term="kubespray" /><category term="k3s" /><category term="pulumi" /><category term="helmsman" /><category term="helm" /><summary type="html"><![CDATA[Installation x86_64: CoreOS (now Flatcar Container Linux) as a Linux distro Kubespray as a Kubernetes installer Metallb and NGINX Ingress Controller for incoming traffic arm (Raspberry PI 3B) HypriotOS as a lightweight container-oriented Debian-based Linux k3s as a lightweight Kubernetes distribution with sqlite instead of etcd Metallb and Traefik v1 for incoming traffic Configuration Pulumi for everything except Helm charts Helmsman for Helm charts kubie for using multiple Kubernetes contexts simultaneously in different terminals]]></summary></entry><entry><title type="html">Automating Helm applications installation and upgrade with Helmsman</title><link href="https://maxromanovsky.com/blog/kubernetes/2020-04-24-helmsman.html" rel="alternate" type="text/html" title="Automating Helm applications installation and upgrade with Helmsman" /><published>2020-04-24T00:00:00+00:00</published><updated>2020-04-24T00:00:00+00:00</updated><id>https://maxromanovsky.com/blog/kubernetes/helmsman</id><content type="html" xml:base="https://maxromanovsky.com/blog/kubernetes/2020-04-24-helmsman.html"><![CDATA[<p>As I’ve mentioned in my post about <a href="/blog/kubernetes/2020-04-21-pulumi.html">Pulumi</a>, I don’t like <code class="language-plaintext highlighter-rouge">helm template</code> approach.
In my opinion, it’s better to stick with the tool rather that mimic it’s behaviour. In case of helm “sticking with the tool”
also means out of the box support for the standard <code class="language-plaintext highlighter-rouge">helm</code> tool, including plugins.</p>

<p>My tool of choice is <a href="https://github.com/Praqma/helmsman">Helmsman</a></p>

<!--more-->

<p>I appreciate the fact that building abstraction on top of the tool such as helm is not ideal. That’s why there are no
major players in this field.</p>

<p>And there are even less of these which were not abandoned by their authors, and support changes introduced in helm 3.</p>

<h2 id="alternatives">Alternatives</h2>

<ul>
  <li><a href="http://pulumi.com/">Pulumi</a> - didn’t like it for use of <code class="language-plaintext highlighter-rouge">helm template</code>.</li>
  <li><a href="https://www.replicated.com/ship/">Ship</a> - concentrated on a single application lifecycle.</li>
  <li><a href="https://github.com/roboll/helmfile">helmfile</a> - quite popular, but I didn’t like the fact that the configuration file
  (<code class="language-plaintext highlighter-rouge">helmfile.yaml</code>) was treated as a Go template file.</li>
  <li><a href="https://github.com/Eneco/landscaper">Landscaper</a> - deprecated in favour of <code class="language-plaintext highlighter-rouge">helmfile</code> just couple days ago.</li>
  <li><a href="https://github.com/FairwindsOps/reckoner">Reckoner</a> - Python-based, quite slow in my experience,
  very <a href="https://github.com/FairwindsOps/reckoner/tree/master/docs">brief</a> documentation, lack of secrets or environment
  variables management.</li>
</ul>

<h2 id="installation-on-macos">Installation (on macOS)</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>helm helmsman sops
helm plugin <span class="nb">install </span>https://github.com/databus23/helm-diff <span class="nt">--version</span> master
helm plugin <span class="nb">install </span>https://github.com/fabmation-gmbh/helm-whatup
helm plugin <span class="nb">install </span>https://github.com/futuresimple/helm-secrets
</code></pre></div></div>

<h2 id="usage">Usage</h2>

<ul>
  <li>Create <a href="https://github.com/Praqma/helmsman/blob/master/docs/desired_state_specification.md">Desired State Specification</a>
  file (<code class="language-plaintext highlighter-rouge">helmsman.yaml</code> in this example)</li>
  <li><code class="language-plaintext highlighter-rouge">helmsman --apply -p 3 -f helmsman.yaml</code>, where <code class="language-plaintext highlighter-rouge">-p</code> is parallelism level. I set it to <code class="language-plaintext highlighter-rouge">1</code> on my Raspberry PI k3s cluster,
  otherwise it can’t cope with the load; my intel-based cluster works better, and <code class="language-plaintext highlighter-rouge">-p 3</code> speeds things up.</li>
  <li>…</li>
  <li>Profit!</li>
</ul>

<h2 id="secrets-management">Secrets Management</h2>
<p>Use <a href="https://github.com/zendesk/helm-secrets">helm-secrets</a> and <a href="https://github.com/mozilla/sops">sops</a> (installed in
my example) for secret management.</p>

<p>With GPG key <a href="https://github.com/mozilla/sops#test-with-the-dev-pgp-key">it’s as easy as</a>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">SOPS_PGP_FP</span><span class="o">=</span><span class="s2">"&lt;KEY_ID&gt;"</span>
sops /path/to/new/or/existing/secrets-file.yaml
</code></pre></div></div>

<ul>
  <li>Each value is encoded separately, which makes it perfect for GitOps and change management.</li>
  <li>Cloud KMS services are supported.</li>
</ul>

<h2 id="extracting-common-variables-into-dotenv">Extracting common variables into dotenv</h2>

<p><code class="language-plaintext highlighter-rouge">.env</code> files are supported, and they are ideal for reusable values like URLs, hostnames, IP addresses, versions.</p>

<p><code class="language-plaintext highlighter-rouge">ohmyzsh</code> has even a <a href="https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/dotenv">dotenv plugin</a> to load them up
automatically into your shell.</p>

<p>Simple <code class="language-plaintext highlighter-rouge">.env</code> looks like:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">IP_ETCD</span><span class="o">=</span>192.168.0.100
<span class="c"># Some comment</span>
<span class="nv">VERSION_KIBANA</span><span class="o">=</span>7.6.2
</code></pre></div></div>

<h2 id="desired-state-definition-dsd-tips">Desired State Definition (DSD) tips</h2>

<p>Both environment variables can secrets can then be referenced in the
<a href="https://github.com/Praqma/helmsman/blob/master/docs/desired_state_specification.md">Desired State Specification</a> file.</p>

<p>You can (and should) also extract value files for individual charts into separate files.</p>

<p>I use the following folder structure:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.
├── charts
│   ├── kibana
│   └── prometheus-operator
├── helmsman.yaml
├── secrets
│   ├── kibana.yaml
│   └── prometheus-operator.yaml
└── values
    ├── kibana-default.yaml
    ├── kibana.yaml
    ├── prometheus-operator-default.yaml
    └── prometheus-operator.yaml
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">charts/</code> hold unpacked chart archives (<code class="language-plaintext highlighter-rouge">.gitignore</code>‘d) - I use it for updating to latest versions, more on that later.</li>
  <li><code class="language-plaintext highlighter-rouge">secrets/</code> hold files with sensitive information, encrypted by <code class="language-plaintext highlighter-rouge">sops</code></li>
  <li><code class="language-plaintext highlighter-rouge">values/</code> hold value files used to configure charts, as well as default value files extracted from <code class="language-plaintext highlighter-rouge">charts/</code>. Default
  value files are stored in git, that’s how I find out what changed between currently installed and latest chart release.
  Then I upgrade values used by installed apps accordingly.</li>
</ul>

<p>Helmsman DSD file looks like this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">settings</span><span class="pi">:</span>
  <span class="c1"># I use that to control chart removal order - use it rarely :)</span>
  <span class="na">reverseDelete</span><span class="pi">:</span> <span class="no">true</span>

<span class="c1"># Namespaces observed by helmsman. It will remove any charts from these namespaces that are not managed by helmsman.</span>
<span class="na">namespaces</span><span class="pi">:</span>
  <span class="na">kibana</span><span class="pi">:</span> <span class="pi">{}</span>
  <span class="na">prometheus-operator</span><span class="pi">:</span> <span class="pi">{}</span>

<span class="c1"># All your helm repos go here</span>
<span class="na">helmRepos</span><span class="pi">:</span>
  <span class="na">stable</span><span class="pi">:</span> <span class="s1">'</span><span class="s">https://kubernetes-charts.storage.googleapis.com'</span>

<span class="na">apps</span><span class="pi">:</span>
  <span class="c1"># That's pretty self-descriptive IMHO :)</span>
  <span class="na">kibana</span><span class="pi">:</span>
    <span class="na">namespace</span><span class="pi">:</span> <span class="s">kibana</span>
    <span class="na">chart</span><span class="pi">:</span> <span class="s">stable/kibana</span>
    <span class="na">version</span><span class="pi">:</span> <span class="s">3.2.6</span> <span class="c1"># Chart version</span>
    <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span> <span class="c1"># Set to false, and release will be removed</span>
    <span class="na">secretsFile</span><span class="pi">:</span> <span class="s">secrets/kibana.yaml</span> <span class="c1"># Path to yaml file with secrets managed by sops. It will be decrypted, and later used as a regular values file (helm -f)</span>
    <span class="na">valuesFile</span><span class="pi">:</span> <span class="s">values/kibana.yaml</span> <span class="c1"># Yaml file with unencrypted configuration values  (helm -f)</span>
    <span class="na">set</span><span class="pi">:</span>
      <span class="na">image.tag</span><span class="pi">:</span> <span class="s1">'</span><span class="s">${VERSION_KIBANA}'</span> <span class="c1"># Here you can use variables defined in .env (helm --set key=value)</span>
      <span class="s1">'</span><span class="s">ingress.hosts[0]'</span><span class="err">:</span> <span class="s1">'</span><span class="s">${DNS_NAME_KIBANA}'</span> <span class="c1"># And even address array indices</span>

  <span class="na">prometheus-operator</span><span class="pi">:</span>
    <span class="na">namespace</span><span class="pi">:</span> <span class="s">prometheus-operator</span>
    <span class="na">chart</span><span class="pi">:</span> <span class="s">stable/prometheus-operator</span>
    <span class="na">version</span><span class="pi">:</span> <span class="s">8.13.0</span>
    <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">valuesFiles</span><span class="pi">:</span> <span class="c1"># You can use multiple values files if necessary</span>
      <span class="pi">-</span> <span class="s">values/prometheus-operator.yaml</span>
      <span class="pi">-</span> <span class="s">values/prometheus-operator-rules.yaml</span>
    <span class="na">secretsFile</span><span class="pi">:</span> <span class="s">secrets/prometheus-operator.yaml</span>
    <span class="na">priority</span><span class="pi">:</span> <span class="s">-50</span> <span class="c1"># And also set priority. The lower, the sooner it will be executed when installing. And the later during deletion (due to reverseDelete)</span>
    <span class="na">wait</span><span class="pi">:</span> <span class="no">true</span> <span class="c1"># You can also wait...</span>
    <span class="na">timeout</span><span class="pi">:</span> <span class="m">600</span> <span class="c1">#... for a specific timeout until chart is fully deployed</span>
    <span class="na">noHooks</span><span class="pi">:</span> <span class="no">true</span> <span class="c1"># You can disable Helm hooks</span>
    <span class="na">helmFlags</span><span class="pi">:</span> <span class="pi">[</span><span class="s1">'</span><span class="s">--skip-crds'</span><span class="pi">]</span> <span class="c1"># Or pass additional flags to helm command line, if necessary</span>
</code></pre></div></div>

<h2 id="upgrading-charts">Upgrading charts</h2>

<p>I use <a href="https://github.com/fabmation-gmbh/helm-whatup">helm-whatup</a> plugin to find updates for the charts:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm whatup <span class="nt">--all-namespaces</span> <span class="nt">-a</span>
</code></pre></div></div>

<p>And I wrote a script that supplements the helmsman upgrade process:</p>
<ul>
  <li>Run <code class="language-plaintext highlighter-rouge">helm whatup</code></li>
  <li>For each outdated chart run <code class="language-plaintext highlighter-rouge">helm fetch &lt;chart_name&gt; --version=&lt;chart_new_version&gt; --untar --destination charts/</code></li>
  <li>Then I copy <code class="language-plaintext highlighter-rouge">charts/&lt;chart_name&gt;/values.yaml</code> to <code class="language-plaintext highlighter-rouge">values/&lt;chart_name&gt;-default.yaml</code></li>
</ul>

<p>I am too ashamed of the script to share it though :smile:.</p>

<p>Once updated charts are fetched, I update chart versions in <code class="language-plaintext highlighter-rouge">helmsman.yaml</code>, make necessary updates to my overridden values,
and run <code class="language-plaintext highlighter-rouge">helmsman apply...</code> again.</p>]]></content><author><name>Maksym Romanowski</name></author><category term="kubernetes" /><category term="kubernetes" /><category term="helmsman" /><category term="helm" /><category term="sops" /><category term="helm-secrets" /><category term="helm-whatup" /><summary type="html"><![CDATA[As I’ve mentioned in my post about Pulumi, I don’t like helm template approach. In my opinion, it’s better to stick with the tool rather that mimic it’s behaviour. In case of helm “sticking with the tool” also means out of the box support for the standard helm tool, including plugins. My tool of choice is Helmsman]]></summary></entry><entry><title type="html">Kubernetes state management with Pulumi and Python</title><link href="https://maxromanovsky.com/blog/kubernetes/2020-04-21-pulumi.html" rel="alternate" type="text/html" title="Kubernetes state management with Pulumi and Python" /><published>2020-04-21T00:00:00+00:00</published><updated>2020-04-21T00:00:00+00:00</updated><id>https://maxromanovsky.com/blog/kubernetes/pulumi</id><content type="html" xml:base="https://maxromanovsky.com/blog/kubernetes/2020-04-21-pulumi.html"><![CDATA[<p>I like Kubernetes way of declarative workload configuration, but handling cluster state using dozens or hundreds of YAML
files is impractical.</p>

<p>Of course, one can just combine them all into a single uber-YAML :smile:. But the harsh reality is, despite the fact
that Kubernetes by design can and will apply this configuration asynchronously, and eventually cluster state will
achieve the desired state, this “eventually” might be equal to infinity.</p>

<p>There are certain cases when order matters, for instance when new CRD definitions are added, and then new objects with
that <code class="language-plaintext highlighter-rouge">kind</code> are declared.</p>

<p>Another aspect is complexity, which can be encapsulated by tools such as Helm. While Helm is a good solution for the
problem of installing third-party apps, it’s not necessary a right choice for your own services, or for lightweight
overall cluster configuration.</p>

<p>And one more thing. I enjoy the kubernetes architecture, even (and especially!) the fact that numerous abstractions are
needed to “canonically” expose a single container to the rest of the world. But it doesn’t mean that I enjoy to break a
DRY principle, and copy-paste-modify same YAMLs over and over.</p>

<p>So… Pulumi to the rescue!</p>

<!--more-->

<p>Disclaimer: I like declarative configuration. I really do. There’s no need to think what can go wrong, or what needs to
be done to the infrastructure to change it from the current to the desired state. You just declare the target state,
and the system decides on it’s own the effective <code class="language-plaintext highlighter-rouge">diff</code> between the two. It’s not the case with Ansible, for instance.
You have to explicitly describe all the changes. But the Ansible is so simple, so good and so predictive, that I keep
using it for simple infrastructure configuration.</p>

<p>Bearing that in mind, I was looking for a tool that could be used to automate cluster configuration after provisioning:</p>
<ul>
  <li>Apply numerous YAMLs or pseudo-YAMLs (good old kubernetes configs), and ideally refactor them by extracting common parts.</li>
  <li>Apply Helm charts.</li>
  <li>Extract configuration parameters (DNS names, IP addresses, secrets) and store them separately.</li>
</ul>

<h2 id="alternatives">Alternatives</h2>

<p>I’ve looked at different tools, and here’s what I disliked about them:</p>
<ul>
  <li>Plain YAMLs. These are just unmanageable. And not DRY enough for me (Plus all the issues mentioned above).
  Even with <a href="https://github.com/kubernetes-sigs/kustomize">kustomize</a>. Even with <a href="https://googlecontainertools.github.io/kpt/">kpt</a>.</li>
  <li><a href="https://docs.ansible.com/ansible/latest/modules/k8s_module.html">Ansible</a>.
  Uh… Thanks, but no, thanks. Not declarative enough for me :)</li>
  <li><a href="https://www.terraform.io/docs/providers/kubernetes/index.html">Terraform</a>. Now, here’s the thing about Terraform.
  I tried to like it, but I simply don’t. At this point in my life I don’t understand why yet-another-DSL should be
  used in a situation, where general-purpose programming language can be used. Especially taking into consideration,
  that some of these languages already have features that enable programmers to build DSL on top of them (think of
  Groovy or Kotlin). Real languages, with loops and conditions. With debuggers, code styles, test frameworks and all
  other features that mature development technologies provide. HCL is what I don’t like in Terraform. Sorry, it’s
  just my opinion. And yes, I super-enjoyed when Hashicorp folks made breaking changes to the syntax between <code class="language-plaintext highlighter-rouge">0.11</code>
  and <code class="language-plaintext highlighter-rouge">0.12</code> :disappointed:. It’s possible to migrate Kubernetes descriptors from YAML to HCL. I don’t think
  it’ll make them more DRY though. Probably more vendor-locked.</li>
  <li><a href="https://www.pulumi.com/docs/intro/cloud-providers/kubernetes/">Pulumi</a>. I’ve heard about it on an episode of
  <a href="https://kubernetespodcast.com/episode/076-pulumi/">Kubernetes Podcast</a>, and was happy to find same-minded
  developers :smiley:. Pulumi supports Python, Javascript, Typescript, and as of version 2 (released just days ago)
  also Go and .NET. Pulumi supports numerous cloud providers, and even have a Terraform bridge. In regards to Kubernetes,
  Pulumi claims to support declarative configuration via these programming languages, but also existing YAML descriptors
  and Helm charts.</li>
</ul>

<h2 id="pulumi-101">Pulumi 101</h2>

<p>Detailed explanation of <a href="https://www.pulumi.com/docs/intro/concepts/how-pulumi-works/">how Pulumi works</a> can be found on
their website, but in a nutshell it’s simple:</p>
<ol>
  <li>Run a program written in general-purpose language using their SDK that will declare a desired state.</li>
  <li>Compare it to the actual state stored by Pulumi (that state might actually be different from the actual state of the
cluster, in which case you might need to <code class="language-plaintext highlighter-rouge">pulumi refresh</code> it).</li>
  <li>Perform necessary operations (using K8s API Server) to ensure that actual state matches the desired state.</li>
  <li>Store the new actual state.</li>
</ol>

<p>Unfortunately, I am not coding day-to-day anymore, so I don’t have hands-on experience with either of languages supported
by Pulumi, but Python and Go are on my todo list. When I’ve started looking at Pulumi, Go support was not in GA, hence
choice was obvious: Python.</p>

<p>I was able to cover majority of my use cases at the end (all except Helm), and now I have 95% confidence (and already verified
it) that I can configure a new cluster after spinning it up with a simple plan:</p>
<ul>
  <li>Run Pulumi (configuration: skip CRDs)</li>
  <li>Deploy Helm charts (containing CRD definitions) with <a href="/blog/kubernetes/2020-04-24-helmsman.html">Helmsman</a></li>
  <li>Run Pulumi (configuration: enable CRDs)</li>
  <li>Do one small <code class="language-plaintext highlighter-rouge">kubectl create</code> / <code class="language-plaintext highlighter-rouge">kubectl replace</code> - more on that below</li>
</ul>

<h2 id="pros--cons">Pros &amp; cons</h2>

<p>Pulumi has the following pros to me (in regards to K8s handling):</p>
<ul>
  <li><strong>Real programming language support</strong>. Possibility to extract common parts to functions, and then reuse them.</li>
  <li><strong>Open source. Free. Not vendor-locked to their backend</strong>. It is possible to use their SaaS backend, but it’s also possible
  to store the state locally or on self-managed backends (AWS S3, GCP GCS, Azure Blob). Same applies to secrets
  management: Pulumi SaaS, <a href="https://www.pulumi.com/blog/managing-secrets-with-pulumi/#configuring-your-secrets-provider">encrypted locally</a>
  with <code class="language-plaintext highlighter-rouge">AES-256-GCM</code>, using cloud KMS (AWS, GCP, Azure) or Hashicorp Vault.</li>
  <li><strong>YAML generation</strong> instead of applying changes to server. That’s a continuation of their awesomeness in fight with
  vendor lock-in! Not happy with Pulumi? Generate YAMLs and move on. Kudos to Pulumi team for the wise decision :bow:!</li>
  <li><strong>YAML provisioning support</strong> (and even customization via <a href="https://www.pulumi.com/docs/guides/adopting/from_kubernetes/#configuration-transformations">transformations</a>) -
  super-useful to provision existing external apps that don’t have Helm charts, or where Helm charts add unnecessary
  complexity (Fluent Bit, for instance).</li>
  <li><strong>Automatic SDK generation</strong> from k8s OpenAPI. Very wise decision, allowing Pulumi to quickly become up-to-date with
  latest changes in spec.</li>
  <li>They provide a <strong>toolkit for <a href="https://www.pulumi.com/docs/guides/testing/">automated infrastructure testing</a></strong>, but I haven’t
  tried it.</li>
  <li><strong>Detailed documentation</strong>. Lots of examples for all supported languages, as well as API docs. Amazing.</li>
</ul>

<p>And in my opinion these are the drawbacks (again, for K8s only):</p>
<ul>
  <li><strong>Auto-naming approach</strong>. That’s a design decision. I don’t like it, IMHO it’s a leaky abstraction (Resources from
  existing YAMLs or Helm charts are not auto-named), but it’s an easy fix: explicitly provide a name in <code class="language-plaintext highlighter-rouge">metadata.name</code>.
  And take responsiblity for potential clashes. But you should be doing that already :smiley:</li>
  <li>
    <p><strong>Helm support via local template rendering</strong>. Helm v2 was a painful experience (tiller and friends), but v3 made it a lot
  easier. And while kubernetes itself is moving <a href="https://kubernetes.io/blog/2020/04/01/kubernetes-1.18-feature-server-side-apply-beta-2/">apply to the server</a>,
  helm moved it to the client. Wise decision for Helm IMHO, and now they can leverage native server-side k8s apply.
  In my opinion, Pulumi should’ve delegated underlying resource management to helm, but they made a decision to manage
  granular resources within Pulumi. In fact, Helm v3 support is identical to v2 (at least in Python), implemented
  <a href="https://github.com/pulumi/pulumi-kubernetes/blob/master/sdk/python/pulumi_kubernetes/helm/v3/__init__.py">with a single line</a>:</p>

    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kn">from</span> <span class="nn">..v2.helm</span> <span class="kn">import</span> <span class="o">*</span>  <span class="c1"># v3 support is identical to v2
</span></code></pre></div>    </div>

    <p>I don’t like <a href="https://github.com/pulumi/pulumi-kubernetes/issues/1051">the decision that Pulumi made</a>, it’s not suitable to me:</p>
    <ul>
      <li>There are problems with charts rendered via <code class="language-plaintext highlighter-rouge">helm template</code>, for instance lifecycle hooks are not supported
  (<a href="https://github.com/pulumi/pulumi-kubernetes/issues/555#issuecomment-516202234">#555</a>,
  <a href="https://github.com/pulumi/pulumi-kubernetes/pull/666">#666</a>), CRDs are not installed
  (<a href="https://github.com/pulumi/pulumi-kubernetes/issues/1023">#1023</a>) etc.</li>
      <li>It’s impossible to leverage the existing helm tooling for the installed releases, as helm won’t see them. What’s
  crucial to me is finding out updates to the installed charts, as <code class="language-plaintext highlighter-rouge">helm whatup</code> won’t work.</li>
    </ul>
  </li>
  <li><strong>Lack of first-class Namespace support</strong>. That one really makes me sad, but it’s also an easy fix: specify namespace
  name explicitly in <code class="language-plaintext highlighter-rouge">metadata.namespace</code>. However, in my opinion that should be handled in a different way by Pulumi,
  providing better abstraction, as that’s a crucial object within Kubernetes.</li>
  <li><strong>No type hinting for Kubernetes objects in Python</strong>. It looks like they’ve <a href="https://www.pulumi.com/blog/deploy-kubernetes-and-apps-with-go/">implemented it for Go</a>,
  but in Python it’s all <code class="language-plaintext highlighter-rouge">list</code>s and <code class="language-plaintext highlighter-rouge">dict</code>s in <code class="language-plaintext highlighter-rouge">metadata</code>, <code class="language-plaintext highlighter-rouge">spec</code> and everywhere else. Pulumi converts canonical
  Python <code class="language-plaintext highlighter-rouge">snake_case</code> to Kubernetes <code class="language-plaintext highlighter-rouge">camelCase</code> and vice versa, but that’s about it. Other than that - you’re on your
  own. And as far as I can tell, Pulumi does not (or at least did not, as of v1) perform validation against an OpenAPI spec.
  I remember seeing errors while objects with misspelled properties were applied.</li>
</ul>

<h2 id="my-pulumi-infrastructure-as-code">My Pulumi Infrastructure as Code</h2>

<ul>
  <li>Separate stacks for k8s clusters</li>
  <li>Python</li>
  <li>GCP GCS for state storage (recently replaced with local due to <a href="https://github.com/pulumi/pulumi/issues/4258">#4258</a>)</li>
  <li>Locally encrypted secrets with passphrase provided via <code class="language-plaintext highlighter-rouge">PULUMI_CONFIG_PASSPHRASE</code></li>
  <li>Helm managed outside of Pulumi. Flag in Pulumi Config that enables provisioning of CRDs that depends on definitions from Helm charts.</li>
  <li>YAML files for third-party services (Fluent Bit, K8s dashboard) stored as resources and applied via
  <code class="language-plaintext highlighter-rouge">pulumi_kubernetes.ConfigFile</code> with <code class="language-plaintext highlighter-rouge">transformations</code></li>
  <li>Grafana dashboards <code class="language-plaintext highlighter-rouge">ConfigMap</code>s generated as YAML files by a separate provider and applied manually
  (workaround for <a href="https://github.com/pulumi/pulumi-kubernetes/issues/1048">#1048</a>)</li>
</ul>

<h2 id="issues">Issues</h2>

<p>Pulumi is a relatively new tool, and probably not as popular as Terraform. It has certain issues, but none are critical for me.
Trying to be a good citizen, I’ve at least documented them on Github.</p>
<ol>
  <li><a href="https://github.com/pulumi/pulumi-kubernetes/issues/1051">Lack of native Helm 3 support</a>. As I’ve mentioned before,
 that’s a design decision. As a result I am just using another tool for helm charts management.</li>
  <li>
    <p><a href="https://github.com/pulumi/pulumi-kubernetes/issues/1048">Resources with long manifests cannot be created due to too long last-applied-configuration annotation</a>.
 That’s not an issue of Pulumi per se, rather the one of <a href="https://github.com/kubernetes/kubectl/issues/712"><code class="language-plaintext highlighter-rouge">kubectl apply</code></a>.
 However, due to that issue I am currently unable to provision <code class="language-plaintext highlighter-rouge">ConfigMap</code>s with Grafana Dashboards.</p>

    <p>Workaround (also described in the <a href="https://github.com/pulumi/pulumi-kubernetes/issues/1048">Github issue</a>):</p>
    <ul>
      <li>separate provider generating YAML</li>
      <li>Python script removing <code class="language-plaintext highlighter-rouge">kubectl.kubernetes.io/last-applied-configuration</code> annotation</li>
      <li><code class="language-plaintext highlighter-rouge">kubectl create</code> or <code class="language-plaintext highlighter-rouge">kubectl replace</code></li>
    </ul>
  </li>
  <li><a href="https://github.com/pulumi/pulumi-kubernetes/issues/1040">“Released” PersistentVolumes with Reclaim Policy “Retain” are not recreated</a>.
 They have to be manually removed and re-created by pulumi (after <code class="language-plaintext highlighter-rouge">pulumi refresh</code>)</li>
  <li><a href="https://github.com/pulumi/pulumi-kubernetes/issues/1049">Resource getter function does not work for CustomResourceDefinition</a>.
 Again, workaround in the issue.</li>
  <li><a href="https://github.com/pulumi/pulumi-kubernetes/issues/1050">Allow conditional resource creation depending on existence of external resources</a>.
 Not Kubernetes-specific, hence marked as a duplicate to <a href="https://github.com/pulumi/pulumi/issues/3364">#3364</a></li>
</ol>

<h3 id="gcp-gcs-state-storage-issue">GCP GCS state storage issue</h3>

<p>One more issue I’ve suffered from recently - rate limit errors for GCS bucket
(<a href="https://github.com/pulumi/pulumi/issues/4258">#4258</a>).</p>

<p>Symptoms in logs:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubernetes:core:Secret (kubernetes-dashboard/kubernetes-dashboard-csrf):
error: pre-step event returned an error: failed to save snapshot: An IO error occurred during the current operation: blob (key ".pulumi/stacks/dev.json") (code=Unknown): googleapi: Error 429: The rate of change requests to the object /path/to/code/.pulumi/stacks/dev.json exceeds the rate limit. Please reduce the rate of create, update, and delete requests., rateLimitExceeded

kubernetes:core:Endpoints (XXX):
error: pre-step event returned an error: failed to save snapshot: An IO error occurred during the current operation: blob (key ".pulumi/stacks/dev.json") (code=Unknown): googleapi: Error 429: The rate of change requests to the object /path/to/code/.pulumi/stacks/dev.json exceeds the rate limit. Please reduce the rate of create, update, and delete requests., rateLimitExceeded

pulumi:pulumi:Stack (YYY-dev):
error: update failed
</code></pre></div></div>

<p>Current workaround: migrated to local storage.</p>

<h2 id="conclusion">Conclusion</h2>

<p>All in all, I am extremely happy with Pulumi. I use it for all provisioning except Helm charts.</p>

<p>And I know for sure, if for some reason I’d want to move off from it, it would be as simple as configuring K8s provider
to generate YAML descriptors. And that’s one of the best design decisions for such tool.</p>]]></content><author><name>Maksym Romanowski</name></author><category term="kubernetes" /><category term="kubernetes" /><category term="pulumi" /><category term="python" /><category term="helm" /><summary type="html"><![CDATA[I like Kubernetes way of declarative workload configuration, but handling cluster state using dozens or hundreds of YAML files is impractical. Of course, one can just combine them all into a single uber-YAML :smile:. But the harsh reality is, despite the fact that Kubernetes by design can and will apply this configuration asynchronously, and eventually cluster state will achieve the desired state, this “eventually” might be equal to infinity. There are certain cases when order matters, for instance when new CRD definitions are added, and then new objects with that kind are declared. Another aspect is complexity, which can be encapsulated by tools such as Helm. While Helm is a good solution for the problem of installing third-party apps, it’s not necessary a right choice for your own services, or for lightweight overall cluster configuration. And one more thing. I enjoy the kubernetes architecture, even (and especially!) the fact that numerous abstractions are needed to “canonically” expose a single container to the rest of the world. But it doesn’t mean that I enjoy to break a DRY principle, and copy-paste-modify same YAMLs over and over. So… Pulumi to the rescue!]]></summary></entry><entry><title type="html">Home pet cluster. Kubernetes on CoreOS. Part 3: Ingress</title><link href="https://maxromanovsky.com/blog/kubernetes/2019-12-26-coreos-k8s-home-baremetal-03.html" rel="alternate" type="text/html" title="Home pet cluster. Kubernetes on CoreOS. Part 3: Ingress" /><published>2019-12-26T00:00:00+00:00</published><updated>2019-12-26T00:00:00+00:00</updated><id>https://maxromanovsky.com/blog/kubernetes/coreos-k8s-home-baremetal-03</id><content type="html" xml:base="https://maxromanovsky.com/blog/kubernetes/2019-12-26-coreos-k8s-home-baremetal-03.html"><![CDATA[<p>My Kubernetes is up and running, and I’ve decided to expose certain services to the Internet, while keeping other services inside the home network.</p>

<!--more-->

<h2 id="10000ft-overview">10000ft Overview</h2>

<p>From the high level perspective it looks like this:</p>
<ul>
  <li>External DNS configuration to point to the ISP-owned IP of my router (either using static IP or via DynDNS).</li>
  <li>Router configuration, one of the following:
    <ul>
      <li>OpenWRT router has HAProxy 2, which is configured to route clients supporting SNI either to K8s or to other targets (this is my case).</li>
      <li>Simple port forwarding to route TCP traffic on port <code class="language-plaintext highlighter-rouge">443</code> to proper destination.</li>
    </ul>
  </li>
  <li>K8s <code class="language-plaintext highlighter-rouge">LoadBalancer</code> (MetalLB) that would propagate custom IP address to the router. This allows scheduling LB on different nodes and failover.</li>
  <li>External and Internal Ingress Controller (Nginx) that would take care of k8s <code class="language-plaintext highlighter-rouge">Ingress</code> objects and create <code class="language-plaintext highlighter-rouge">LoadBalancer</code> services via MetalLB.</li>
  <li>OAuth2 Proxy configured in Nginx Ingress as external auth (for externally exposed services)</li>
  <li>K8s CoreDNS pointing to router DNS instead of Google DNS to allow inter-service communication via LAN (useful for Nginx &lt;-&gt; OAuth2 Proxy, for example)</li>
  <li>Cert Manager requesting SSL certificates that are used by Ingress Controller</li>
</ul>

<p>User request (assuming HTTPS traffic to K8s service using client supporting SNI) originating from the Internet is routed via:</p>
<ul>
  <li>External DNS pointing to the router IP</li>
  <li>HAProxy 2</li>
  <li>LAN IP address associated with external MetalLB <code class="language-plaintext highlighter-rouge">LoadBalancer</code> associated with MAC address of one of the nodes network card</li>
  <li><code class="language-plaintext highlighter-rouge">kube-proxy</code> spreading traffic between one of <code class="language-plaintext highlighter-rouge">metallb-speaker</code>s</li>
  <li>Nginx with proper SSL certificates performs SSL termination</li>
  <li>Nginx performs authentication via OAuth2 Proxy (K8s CoreDNS uses OpenWRT DNS to lookup the IP address, and then request is forwarded (again) via MetalLB and nginx)</li>
  <li>Service and Pod networking do their magic</li>
  <li>HTTP traffic finally reaches the pod</li>
</ul>

<p><img src="/assets/images/k8s-coreos-home-baremetal/k8s-ingress.png" alt="10000ft Overview" /></p>

<h1 id="openwrt">OpenWRT</h1>

<h2 id="haproxy">HAProxy</h2>
<p>OpenWRT router is configured with <a href="http://www.haproxy.org/">HAProxy 2</a> listening on port <code class="language-plaintext highlighter-rouge">443</code> with SNI configuration allowing to proxy certain domain names to Kubernetes, while other to another target.</p>

<p>If Kubernetes (on a single IP) is the only destination, simple port forwarding can be used instead.</p>

<p>Here’s snippet of HAProxy config, including web UI and prometheus metrics:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>global
    daemon

defaults
    timeout client 30s
    timeout server 30s
    timeout connect 5s

frontend ft_ssl
  bind :443
  mode tcp
  tcp-request inspect-delay 5s
  tcp-request content accept if { req_ssl_hello_type 1 }

  acl k8s_app_1 req_ssl_sni -i app1.k8s.example.com
  acl k8s_app_2 req_ssl_sni -i app2.k8s.example.com

  use_backend bk_ssl_k8s_x64 if k8s_app_1
  use_backend bk_ssl_k8s_x64 if k8s_app_2

  default_backend bk_ssl_non_k8s

  # K8s x64
backend bk_ssl_k8s_x64
  mode tcp
  balance roundrobin
  stick-table type binary len 32 size 30k expire 30m
  acl clienthello req_ssl_hello_type 1
  acl serverhello rep_ssl_hello_type 2
  tcp-request inspect-delay 5s
  tcp-request content accept if clienthello
  tcp-response content accept if serverhello
  stick on payload_lv(43,1) if clienthello
  stick store-response payload_lv(43,1) if serverhello
  option ssl-hello-chk
  server k8s-x64-node1 192.168.0.3:443 check
  #server k8s-x64-node2 192.168.0.4:443 check

backend bk_ssl_non_k8s
  mode tcp
  balance roundrobin
  stick-table type binary len 32 size 30k expire 30m
  acl clienthello req_ssl_hello_type 1
  acl serverhello rep_ssl_hello_type 2
  tcp-request inspect-delay 5s
  tcp-request content accept if clienthello
  tcp-response content accept if serverhello
  stick on payload_lv(43,1) if clienthello
  stick store-response payload_lv(43,1) if serverhello
  option ssl-hello-chk
  server non_k8s 192.168.0.2:443

listen stats
    bind *:1936
    http-request use-service prometheus-exporter if { path /metrics }
    mode http
    stats enable
    stats uri /
    stats refresh 10s
</code></pre></div></div>

<h2 id="internal-dns">Internal DNS</h2>

<p>Router’s DNS is used to avoid routing traffic with both source and destination within the LAN.
It maps hostnames to the IP addresses of Kubernetes ingress services (<code class="language-plaintext highlighter-rouge">type: LoadBalancer</code>).</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uci add dhcp domain
uci <span class="nb">set </span>dhcp.@domain[-1].name<span class="o">=</span><span class="s1">'app1.k8s.example.com'</span>
uci <span class="nb">set </span>dhcp.@domain[-1].ip<span class="o">=</span><span class="s1">'192.168.0.3'</span>

uci add dhcp domain
uci <span class="nb">set </span>dhcp.@domain[-1].name<span class="o">=</span><span class="s1">'app2.k8s.example.com'</span>
uci <span class="nb">set </span>dhcp.@domain[-1].ip<span class="o">=</span><span class="s1">'192.168.0.3'</span>

uci commit
luci-reload
service dnsmasq restart
</code></pre></div></div>

<h2 id="kubernetes">Kubernetes</h2>

<p>I was not a huge fan of Helm 2, especially due to the Tiller component. But also due to many charts using outdated <code class="language-plaintext highlighter-rouge">apiVersion</code> in their templates, but now as K8s API became much more mature, I am enjoying Helm 3 instead. That’s why most of the services in this post are deployed via Helm.</p>

<h3 id="kubespray-config">Kubespray config</h3>

<p>Several Kubespray configuration options have to be adjusted to make this setup working.</p>

<p>One is related to configure Kubernetes DNS to use OpenWRT DNS as an upstream instead of Google DNS.</p>

<p>Another one is about enabling scheduling on a master node of my two-node cluster.</p>

<p>Also, I used this as a chance to convert inventory to YAML, which allows me to configure node labels :)</p>

<p>Additionally, I’ve configured several components to expose their metrics in Prometheus format.</p>

<p>Inventory:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">all</span><span class="pi">:</span>
  <span class="na">hosts</span><span class="pi">:</span>
    <span class="na">nuc</span><span class="pi">:</span>
      <span class="na">ansible_host</span><span class="pi">:</span> <span class="s">192.168.0.10</span>
      <span class="na">ansible_user</span><span class="pi">:</span> <span class="s">core</span>
      <span class="na">etcd_member_name</span><span class="pi">:</span> <span class="s">nuc5ppyh</span>
      <span class="na">node_labels</span><span class="pi">:</span> <span class="pi">{</span><span class="s1">'</span><span class="s">disktype'</span><span class="pi">:</span> <span class="s1">'</span><span class="s">hdd'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">cputype'</span><span class="pi">:</span> <span class="s1">'</span><span class="s">pentium'</span><span class="pi">}</span>

    <span class="na">udoo</span><span class="pi">:</span>
      <span class="na">ansible_host</span><span class="pi">:</span> <span class="s">192.168.0.11</span>
      <span class="na">ansible_user</span><span class="pi">:</span> <span class="s">core</span>
      <span class="na">node_labels</span><span class="pi">:</span> <span class="pi">{</span><span class="s1">'</span><span class="s">disktype'</span><span class="pi">:</span> <span class="s1">'</span><span class="s">ssd'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">cputype'</span><span class="pi">:</span> <span class="s1">'</span><span class="s">celeron'</span><span class="pi">}</span>

  <span class="na">children</span><span class="pi">:</span>
    <span class="na">etcd</span><span class="pi">:</span> <span class="pi">{</span><span class="nv">hosts</span><span class="pi">:</span> <span class="pi">{</span><span class="nv">nuc</span><span class="pi">:</span> <span class="pi">{}}}</span>
    <span class="na">k8s-cluster</span><span class="pi">:</span>
      <span class="na">children</span><span class="pi">:</span>
        <span class="na">kube-master</span><span class="pi">:</span> <span class="pi">{</span><span class="nv">hosts</span><span class="pi">:</span> <span class="pi">{</span><span class="nv">nuc</span><span class="pi">:</span> <span class="pi">{}}}</span>
        <span class="c1"># Master node must be included into kube-node to avoid NoSchedule taint</span>
        <span class="na">kube-node</span><span class="pi">:</span> <span class="pi">{</span><span class="nv">hosts</span><span class="pi">:</span> <span class="pi">{</span><span class="nv">nuc</span><span class="pi">:</span> <span class="pi">{},</span> <span class="nv">udoo</span><span class="pi">:</span> <span class="pi">{}}}</span>
</code></pre></div></div>

<p>Configuration:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">## Upstream dns servers</span>
<span class="na">upstream_dns_servers</span><span class="pi">:</span>
 <span class="pi">-</span> <span class="s">192.168.0.1</span>
<span class="c1"># - 8.8.8.8</span>
<span class="c1"># - 8.8.4.4</span>

<span class="c1">## Prometheus metrics</span>
<span class="c1"># The IP address and port for the metrics server to serve on</span>
<span class="c1"># (set to 0.0.0.0 for all IPv4 interfaces and `::` for all IPv6 interfaces)</span>
<span class="na">kube_proxy_metrics_bind_address</span><span class="pi">:</span> <span class="s">0.0.0.0:10249</span>

<span class="na">calico_felix_prometheusmetricsenabled</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">calico_felix_prometheusmetricsport</span><span class="pi">:</span> <span class="m">9091</span>
<span class="na">calico_felix_prometheusgometricsenabled</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">calico_felix_prometheusprocessmetricsenabled</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>Here’s quick way to verify that changes were correctly applied:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Checking labels</span>
kubectl get node <span class="nt">--show-labels</span>

<span class="c"># Checking that NoSchedule was removed from master</span>
kubectl get nodes <span class="nt">-o</span> json | jq <span class="s1">'.items[].spec.taints'</span>

<span class="c"># Checking that K8s DNS uses local DNS as an upstream</span>
kubectl <span class="nt">-nkube-system</span> get cm coredns <span class="nt">-oyaml</span> | <span class="nb">grep </span>upstream
kubectl <span class="nt">-nkube-system</span> get cm nodelocaldns <span class="nt">-oyaml</span> | <span class="nb">grep </span>forward

<span class="c"># Checking that Prometheus metrics are accessible</span>
kubectl <span class="nt">-n</span> kube-system get cm kube-proxy <span class="nt">-oyaml</span> | <span class="nb">grep </span>metricsBindAddress
curl 192.168.0.10:9091/metrics
curl 192.168.0.11:9091/metrics
</code></pre></div></div>

<h3 id="metallb">MetalLB</h3>

<p><a href="https://metallb.universe.tf/">MetalLB</a> is an awesome little load balancer implementation for both enthusiasts (like myself) and serious production deployments running on bare metal.</p>

<p>I neither have fancy hardware to support BGP, nor actually need it.</p>

<p><a href="https://metallb.universe.tf/concepts/layer2/">Layer 2</a> (ARP) mode and proxying all the traffic using single node (with an automatic failover, thanks to ARP!) works for me!</p>

<p>Without further ado, here’s my Helm 3 configuration for MetalLB. It matches IP addresses that I use for OpenWRT internal DNS, as well as for HAProxy configuration. That’s the key :smile:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">configInline</span><span class="pi">:</span>
  <span class="na">address-pools</span><span class="pi">:</span>
  <span class="c1"># Used in HAProxy and internal DNS, for services accessible from Internet</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">external</span>
    <span class="na">protocol</span><span class="pi">:</span> <span class="s">layer2</span>
    <span class="na">addresses</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">192.168.0.3/32</span>

  <span class="c1"># Used only in internal DNS, for services accessible from LAN only</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">internal</span>
    <span class="na">protocol</span><span class="pi">:</span> <span class="s">layer2</span>
    <span class="na">addresses</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">192.168.0.5/32</span>

<span class="c1"># Of course, Prometheus!</span>
<span class="na">prometheus</span><span class="pi">:</span>
  <span class="na">serviceMonitor</span><span class="pi">:</span>
    <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">prometheusRule</span><span class="pi">:</span>
    <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<h3 id="nginx-ingress-controller">Nginx Ingress Controller</h3>

<p>Nginx Ingress Controller have to be configured to create proper <code class="language-plaintext highlighter-rouge">LoadBalancer</code> service, and <code class="language-plaintext highlighter-rouge">Ingress</code> objects must map to proper ingress controller and include Authn configuration.</p>

<p>Here’s Helm configuration for external Nginx Ingress (keep an eye on <a href="https://metallb.universe.tf/usage/#requesting-specific-ips">MetalLB annotation</a>!):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">controller</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">controller-external</span>
  <span class="na">electionID</span><span class="pi">:</span> <span class="s">ingress-controller-external-leader</span>
  <span class="na">ingressClass</span><span class="pi">:</span> <span class="s">nginx-external</span>
  <span class="na">kind</span><span class="pi">:</span> <span class="s">DaemonSet</span>
  <span class="na">service</span><span class="pi">:</span>
    <span class="na">omitClusterIP</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">annotations</span><span class="pi">:</span>
      <span class="na">metallb.universe.tf/address-pool</span><span class="pi">:</span> <span class="s">external</span>
  <span class="na">metrics</span><span class="pi">:</span>
    <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">service</span><span class="pi">:</span>
      <span class="na">omitClusterIP</span><span class="pi">:</span> <span class="no">true</span>

<span class="na">defaultBackend</span><span class="pi">:</span>
  <span class="na">service</span><span class="pi">:</span>
    <span class="na">omitClusterIP</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>Here’s sample <code class="language-plaintext highlighter-rouge">Ingress</code> object leveraging Nginx Ingress <a href="https://kubernetes.github.io/ingress-nginx/examples/auth/oauth-external-auth/">external OAuth support</a> and SSL certificates managed via Cert Manager:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.k8s.io/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Ingress</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">app1</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">app1</span>
  <span class="na">annotations</span><span class="pi">:</span>
    <span class="na">kubernetes.io/ingress.class</span><span class="pi">:</span> <span class="s2">"</span><span class="s">nginx-external"</span>
    <span class="na">nginx.ingress.kubernetes.io/auth-url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://$host/oauth2/auth"</span>
    <span class="na">nginx.ingress.kubernetes.io/auth-signin</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://$host/oauth2/start?rd=$escaped_request_uri"</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">tls</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">hosts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">app1.k8s.example.com</span>
      <span class="na">secretName</span><span class="pi">:</span> <span class="s">tls-app1</span>
  <span class="na">rules</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">host</span><span class="pi">:</span> <span class="s">app1.k8s.example.com</span>
      <span class="na">http</span><span class="pi">:</span>
        <span class="na">paths</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">backend</span><span class="pi">:</span>
              <span class="na">serviceName</span><span class="pi">:</span> <span class="s">app1</span>
              <span class="na">servicePort</span><span class="pi">:</span> <span class="m">80</span>
            <span class="na">path</span><span class="pi">:</span> <span class="s">/</span>

</code></pre></div></div>

<h3 id="oauth2-proxy">OAuth2 Proxy</h3>

<p><a href="https://pusher.github.io/oauth2_proxy/">OAuth2 Proxy</a> is another wonderful service transparently enabling OAuth2 for applications that don’t support it natively. It works great with Nginx Ingress too.</p>

<p>Helm configuration:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">config</span><span class="pi">:</span>
  <span class="na">clientID</span><span class="pi">:</span> <span class="s2">"</span><span class="s">app1-cid"</span>
  <span class="na">clientSecret</span><span class="pi">:</span> <span class="s2">"</span><span class="s">app1-csec"</span>
  <span class="c1"># openssl rand -base64 32 | head -c 32</span>
  <span class="na">cookieSecret</span><span class="pi">:</span> <span class="s2">"</span><span class="s">cookieMonsterAteTheSecret"</span>
  <span class="na">configFile</span><span class="pi">:</span> <span class="pi">|-</span>
    <span class="s">provider = "foooo"</span>
    <span class="s">upstream = "file:///dev/null"</span>
    <span class="s">footer = "-"</span>

<span class="na">ingress</span><span class="pi">:</span>
  <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">path</span><span class="pi">:</span> <span class="s">/oauth2</span>
  <span class="na">hosts</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">app1.k8s.example.com</span>
  <span class="na">annotations</span><span class="pi">:</span>
    <span class="na">kubernetes.io/ingress.class</span><span class="pi">:</span> <span class="s2">"</span><span class="s">nginx-external"</span>
  <span class="na">tls</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">secretName</span><span class="pi">:</span> <span class="s">tls-app1</span>
    <span class="na">hosts</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">app1.k8s.example.com</span>

</code></pre></div></div>

<h1 id="conclusion">Conclusion</h1>

<p>That’s about it, folks. Now I can easily and (more or less) securely access my k8s services from both LAN and Internet using the same host names, proper SSL certificates and OAuth 2 support.</p>]]></content><author><name>Maksym Romanowski</name></author><category term="kubernetes" /><category term="coreos" /><category term="kubernetes" /><category term="kubespray" /><category term="nuc" /><category term="udoo" /><category term="metallb" /><category term="nginx" /><category term="cert-manager" /><category term="openwrt" /><category term="haproxy" /><category term="oauth2-proxy" /><category term="helm" /><summary type="html"><![CDATA[My Kubernetes is up and running, and I’ve decided to expose certain services to the Internet, while keeping other services inside the home network.]]></summary></entry><entry><title type="html">Running ElasticSearch on 32-bit Linux machine</title><link href="https://maxromanovsky.com/blog/elasticsearch/2019-12-26-elasticsearch-32bit.html" rel="alternate" type="text/html" title="Running ElasticSearch on 32-bit Linux machine" /><published>2019-12-26T00:00:00+00:00</published><updated>2019-12-26T00:00:00+00:00</updated><id>https://maxromanovsky.com/blog/elasticsearch/elasticsearch-32bit</id><content type="html" xml:base="https://maxromanovsky.com/blog/elasticsearch/2019-12-26-elasticsearch-32bit.html"><![CDATA[<p>I have an old piece of hardware with <a href="https://ark.intel.com/content/www/us/en/ark/products/59683/intel-atom-processor-d2700-1m-cache-2-13-ghz.html">Atom D2700</a> CPU, which according to ARK is capable of running x64 OS. Vendor, however, never released a BIOS with x64 support, and I was unable to find it on an Internet.</p>

<p>Aside this sad fact, that small PC have decent specs, including 4 gigs of RAM, which makes it a good candidate for a single-node ElasticSearch cluster. I have to collect logs from my Kubernetes cluster somewhere, right?</p>

<p>Unfortunately, Elastic <a href="https://www.elastic.co/support/matrix">dropped an official <code class="language-plaintext highlighter-rouge">i586</code> support</a> long time ago, which totally makes sense from commercial perspective.</p>

<p>Well, thanks to Debian and Java we still can run ElasticSearch on top of 32-bit Linux!</p>

<p><strong>This tutorial is written for ElasticSearch OSS v7.5.1, and might not work for newer versions, as source code <a href="https://github.com/elastic/elasticsearch/commits/master/server/src/main/java/org/elasticsearch/bootstrap/SystemCallFilter.java">might change</a>!</strong></p>

<!--more-->

<p>I’ve chosen official <code class="language-plaintext highlighter-rouge">deb</code> OSS distribution w/o JVM over <code class="language-plaintext highlighter-rouge">tar.gz</code> and commercial because:</p>
<ul>
  <li>Deb has convenient integration with systemd and follows Debian conventions for files and directories.</li>
  <li>I don’t plan on using any additional components from the commercial distribution, and don’t want to unintentionally break any license for modifications required to run ES on 32-bit OS.</li>
  <li>Bundled JVM is 64-bit, thus it’s of no use to me.</li>
</ul>

<p>First things first, we need to prepare our OS:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># I know, it's bad practice</span>
<span class="nb">sudo</span> <span class="nt">-i</span>
<span class="nb">export </span><span class="nv">ES_VERSION</span><span class="o">=</span>7.5.1

apt <span class="nb">install</span> <span class="nt">-y</span> openjdk-11-jdk seccomp
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-oss-<span class="k">${</span><span class="nv">ES_VERSION</span><span class="k">}</span><span class="nt">-no-jdk-amd64</span>.deb
<span class="nb">export </span><span class="nv">JAVA_HOME</span><span class="o">=</span>/usr/lib/jvm/java-11-openjdk-i386
dpkg <span class="nt">--force-all</span> <span class="nt">-i</span> elasticsearch-oss-<span class="k">${</span><span class="nv">ES_VERSION</span><span class="k">}</span><span class="nt">-no-jdk-amd64</span>.deb
</code></pre></div></div>

<p>After this installation you’ll get the similar message once you try using <code class="language-plaintext highlighter-rouge">apt</code> again:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[...]
You might want to run 'apt --fix-broken install' to correct these.
The following packages have unmet dependencies:
elasticsearch : Depends: libc6 but it is not installable
[...]
</code></pre></div></div>

<p>In order to fix it you <a href="https://unix.stackexchange.com/questions/404444/how-to-make-apt-ignore-unfulfilled-dependencies-of-installed-package">need to modify</a> <code class="language-plaintext highlighter-rouge">/var/lib/dpkg/status</code>: search for <code class="language-plaintext highlighter-rouge">elasticsearch</code> and remove <code class="language-plaintext highlighter-rouge">depends: libc6</code>.</p>

<p>Now the next (and most important) trick: enable 32-bit support in ElasticSearch itself. Even though it’s written in Java (which is cross-platform), it uses certain native bindings.</p>

<p>Minor change should be applied to a single class, <a href="https://github.com/elastic/elasticsearch/commits/v7.5.1/server/src/main/java/org/elasticsearch/bootstrap/SystemCallFilter.java"><code class="language-plaintext highlighter-rouge">SystemCallFilter</code></a>:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">diff --git a/server/src/main/java/org/elasticsearch/bootstrap/SystemCallFilter.java b/server/src/main/java/org/elasticsearch/bootstrap/SystemCallFilter.java
index 59f8bd5daf7..6a6b360726e 100644
</span><span class="gd">--- a/server/src/main/java/org/elasticsearch/bootstrap/SystemCallFilter.java
</span><span class="gi">+++ b/server/src/main/java/org/elasticsearch/bootstrap/SystemCallFilter.java
</span><span class="p">@@ -243,6 +243,7 @@</span> final class SystemCallFilter {
         Map&lt;String,Arch&gt; m = new HashMap&lt;&gt;();
         m.put("amd64", new Arch(0xC000003E, 0x3FFFFFFF, 57, 58, 59, 322, 317));
         m.put("aarch64",  new Arch(0xC00000B7, 0xFFFFFFFF, 1079, 1071, 221, 281, 277));
<span class="gi">+        m.put("i386",  new Arch(0x40000003, 0xFFFFFFFF, 2, 190, 11, 358, 354));
</span>         ARCHITECTURES = Collections.unmodifiableMap(m);
     }
</code></pre></div></div>

<p>I am doing it on my macOS, which compiles Java much faster. Save <code class="language-plaintext highlighter-rouge">systemcallfilter-i386.patch</code> and navigate shell to the same directory (replace <code class="language-plaintext highlighter-rouge">&lt;elasticsearch-host&gt;</code> with your ES hostname):</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">ES_VERSION</span><span class="o">=</span>7.5.1

<span class="c"># Prepare build environment</span>
curl <span class="nt">-s</span> <span class="s2">"https://get.sdkman.io"</span> | bash
sdk list java
sdk <span class="nb">install </span>java 12.0.2-open
sdk use java 12.0.2-open

<span class="c"># Clone the repo, apply the patch</span>
git clone https://github.com/elastic/elasticsearch.git
<span class="nb">cd </span>elasticsearch
git checkout v<span class="k">${</span><span class="nv">ES_VERSION</span><span class="k">}</span>
git apply ../systemcallfilter-i386.patch

<span class="c"># Download the whole internet to compile a single class</span>
./gradlew clean compileJava

<span class="c"># Copy compiled class and replace it in the existing JAR</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> ../org/elasticsearch/bootstrap
<span class="nb">cp </span>server/build/classes/java/main/org/elasticsearch/bootstrap/SystemCallFilter.class ../org/elasticsearch/bootstrap
<span class="nb">cd</span> ..
scp &lt;elasticsearch-host&gt;:/usr/share/elasticsearch/lib/elasticsearch-<span class="k">${</span><span class="nv">ES_VERSION</span><span class="k">}</span>.jar <span class="nb">.</span>
<span class="nb">cp </span>elasticsearch-<span class="k">${</span><span class="nv">ES_VERSION</span><span class="k">}</span>.jar elasticsearch-<span class="k">${</span><span class="nv">ES_VERSION</span><span class="k">}</span>.jar.bak
<span class="c"># https://stackoverflow.com/questions/1667153/updating-class-file-in-jar</span>
jar <span class="nt">-uf</span> elasticsearch-<span class="k">${</span><span class="nv">ES_VERSION</span><span class="k">}</span>.jar org/elasticsearch/bootstrap/SystemCallFilter.class

<span class="c"># Copy modified JAR back to the i586 host</span>
scp elasticsearch-<span class="k">${</span><span class="nv">ES_VERSION</span><span class="k">}</span>.jar &lt;elasticsearch-host&gt;:/tmp
</code></pre></div></div>

<p>Now we can continue (in the same shell with sudo and <code class="language-plaintext highlighter-rouge">ES_VERSION</code>):</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Replacing the ElasticSearch jar with the patched one</span>
<span class="nb">mv</span> /tmp/elasticsearch-<span class="k">${</span><span class="nv">ES_VERSION</span><span class="k">}</span>.jar /usr/share/elasticsearch/lib/

<span class="c"># Replacing stripped JNA bindings (w/o i586 support) with the full, original version</span>
wget <span class="nt">-O</span> /usr/share/elasticsearch/lib/jna-4.5.1.jar https://github.com/java-native-access/jna/raw/4.5.1/dist/jna.jar

<span class="c"># Using system JVM</span>
<span class="nb">echo</span> <span class="s2">"JAVA_HOME=/usr/lib/jvm/java-11-openjdk-i386"</span> <span class="o">&gt;&gt;</span> /etc/default/elasticsearch

<span class="c"># Configuring ElasticSearch single node to act as production-ready cluster</span>
<span class="nb">echo</span> <span class="s2">"network.host: 0.0.0.0"</span> <span class="o">&gt;&gt;</span> /etc/elasticsearch/elasticsearch.yml
<span class="nb">echo</span> <span class="s2">"discovery.type: single-node"</span> <span class="o">&gt;&gt;</span> /etc/elasticsearch/elasticsearch.yml
<span class="nb">echo</span> <span class="s2">"-Des.enforce.bootstrap.checks=true"</span> <span class="o">&gt;&gt;</span> /etc/elasticsearch/jvm.options

<span class="c"># Using half of memory for JVM heap, per official recommendation. Another part would be used for file system cache</span>
<span class="nb">sed</span> <span class="nt">-Ei</span> <span class="s1">'s/^-Xms.*/-Xms2g/g'</span> /etc/elasticsearch/jvm.options
<span class="nb">sed</span> <span class="nt">-Ei</span> <span class="s1">'s/^-Xmx.*/-Xmx2g/g'</span> /etc/elasticsearch/jvm.options

systemctl <span class="nb">enable </span>elasticsearch.service
systemctl start elasticsearch.service

<span class="nb">cat</span> /var/log/elasticsearch/elasticsearch.log

<span class="c"># Checking file descriptors: https://www.elastic.co/guide/en/elasticsearch/reference/current/file-descriptors.html</span>
curl localhost:9200/_nodes/stats/process?filter_path<span class="o">=</span><span class="k">**</span>.max_file_descriptors

<span class="c"># And finally:</span>
curl localhost:9200
</code></pre></div></div>

<p>Bingo! It’s up and running in no time.</p>

<p>Just remember, in order to upgrade to the new ES version you have to:</p>
<ol>
  <li>Download fresh <code class="language-plaintext highlighter-rouge">deb</code> file &amp; install it.</li>
  <li>Modify <code class="language-plaintext highlighter-rouge">/var/lib/dpkg/status</code> to get rid of unmet apt dependency</li>
  <li>Check if <a href="https://github.com/elastic/elasticsearch/commits/v7.5.1/server/src/main/java/org/elasticsearch/bootstrap/SystemCallFilter.java"><code class="language-plaintext highlighter-rouge">SystemCallFilter</code></a> in your particular release (<code class="language-plaintext highlighter-rouge">v7.5.1</code> in this case) has changed. <a href="https://github.com/elastic/elasticsearch/commits/master/server/src/main/java/org/elasticsearch/bootstrap/SystemCallFilter.java"><code class="language-plaintext highlighter-rouge">master</code> branch is already different</a>, so it’s just a matter of time.</li>
  <li>Re-compile <code class="language-plaintext highlighter-rouge">SystemCallFilter.java</code> if necessary (after applying patch)</li>
  <li>Re-package <code class="language-plaintext highlighter-rouge">elasticsearch-*.jar</code> with the patched <code class="language-plaintext highlighter-rouge">SystemCallFilter.class</code></li>
  <li>Check if <code class="language-plaintext highlighter-rouge">/usr/share/elasticsearch/lib/jna-*.jar</code> has changed (maybe version was bumped), and replace it with the proper full version</li>
</ol>]]></content><author><name>Maksym Romanowski</name></author><category term="elasticsearch" /><category term="elasticsearch" /><category term="ELK" /><category term="32bit" /><category term="i586" /><summary type="html"><![CDATA[I have an old piece of hardware with Atom D2700 CPU, which according to ARK is capable of running x64 OS. Vendor, however, never released a BIOS with x64 support, and I was unable to find it on an Internet. Aside this sad fact, that small PC have decent specs, including 4 gigs of RAM, which makes it a good candidate for a single-node ElasticSearch cluster. I have to collect logs from my Kubernetes cluster somewhere, right? Unfortunately, Elastic dropped an official i586 support long time ago, which totally makes sense from commercial perspective. Well, thanks to Debian and Java we still can run ElasticSearch on top of 32-bit Linux! This tutorial is written for ElasticSearch OSS v7.5.1, and might not work for newer versions, as source code might change!]]></summary></entry><entry><title type="html">Home pet cluster. Kubernetes on CoreOS. Part 2: Spraying some kubes with Kubespray</title><link href="https://maxromanovsky.com/blog/kubernetes/2019-07-29-coreos-k8s-home-baremetal-02.html" rel="alternate" type="text/html" title="Home pet cluster. Kubernetes on CoreOS. Part 2: Spraying some kubes with Kubespray" /><published>2019-07-29T00:00:00+00:00</published><updated>2019-07-29T00:00:00+00:00</updated><id>https://maxromanovsky.com/blog/kubernetes/coreos-k8s-home-baremetal-02</id><content type="html" xml:base="https://maxromanovsky.com/blog/kubernetes/2019-07-29-coreos-k8s-home-baremetal-02.html"><![CDATA[<p>At this point I have two Linux machines running CoreOS Container Linux.</p>

<p>Now it’s time to finally install Kubernetes on them!</p>

<!--more-->

<p><a href="https://github.com/kubernetes-sigs/kubespray/">Kubespray</a> is an official tool for deploying a Production-Ready Kubernetes cluster on wide variety of infrastructure, including bare metal. It is based on Ansible, and uses kubeadm under the hood (if I’m not mistaken).</p>

<p>I’ve used <a href="https://github.com/kubernetes-sigs/kubespray/releases/tag/v2.10.4">v2.10.4</a>, which was latest as of time I’ve written this post. Personally, I configured it as a submodule to my git repo with custom configs, but it could be just downloaded from a tag, and used independently of git.</p>

<p><a href="https://kubespray.io/#/?id=usage">Quickstart</a> usage commands are available on it’s website, and I won’t repeat them. However, I’d like to emphasize that you should use an Ansible from kubespray’s <code class="language-plaintext highlighter-rouge">requirements.txt</code> instead of latest &amp; greatest from your package manager. It turns out, that Ansible sometimes breaks backward compatibility or introduces bugs, which break kubespray. It happend to me twice (in 2018 and in 2019), and both times Ansible pinned to Kubespray worked like a charm.</p>

<p><a href="https://kubespray.io/#/docs/coreos">CoreOS</a> is supported, and main <code class="language-plaintext highlighter-rouge">cluster.yml</code> playbook even auto-detects CoreOS and sets proper defaults, however other playbooks (such as <code class="language-plaintext highlighter-rouge">reset.yml</code>) don’t support them. Following changes worked for me, when I’ve configured, and then tore down the cluster multiple times.</p>

<h2 id="configuration">Configuration</h2>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">diff --git a/k8s/inventory-x64/group_vars/all/all.yml b/k8s/inventory-x64/group_vars/all/all.yml
index 4b45b66..d16c4c2 100644
</span><span class="gd">--- a/k8s/inventory-x64/group_vars/all/all.yml
</span><span class="gi">+++ b/k8s/inventory-x64/group_vars/all/all.yml
</span><span class="p">@@ -3,7 +3,9 @@</span>
 etcd_data_dir: /var/lib/etcd

 ## Directory where the binaries will be installed
<span class="gd">-bin_dir: /usr/local/bin
</span><span class="gi">+bin_dir: /opt/bin
+
+ansible_python_interpreter: /opt/bin/python
</span>
 ## The access_ip variable is used to define how other nodes should access
 ## the node.  This is used in flannel to allow other flannel nodes to see
<span class="p">@@ -39,9 +41,9 @@</span> loadbalancer_apiserver_healthcheck_port: 8081
 # kubelet_load_modules: false

 ## Upstream dns servers
<span class="gd">-# upstream_dns_servers:
-#   - 8.8.8.8
-#   - 8.8.4.4
</span><span class="gi">+upstream_dns_servers:
+ - 8.8.8.8
+ - 8.8.4.4
</span>
 ## There are some changes specific to the cloud providers
 ## for instance we need to encapsulate packets with some network plugins
<span class="gh">diff --git a/k8s/inventory-x64/group_vars/k8s-cluster/k8s-cluster.yml b/k8s/inventory-x64/group_vars/k8s-cluster/k8s-cluster.yml
index b2bfdf0..dcd9fcc 100644
</span><span class="gd">--- a/k8s/inventory-x64/group_vars/k8s-cluster/k8s-cluster.yml
</span><span class="gi">+++ b/k8s/inventory-x64/group_vars/k8s-cluster/k8s-cluster.yml
</span><span class="p">@@ -124,7 +124,7 @@</span> kube_encrypt_secret_data: false

 # DNS configuration.
 # Kubernetes cluster name, also will be used as DNS domain
<span class="gd">-cluster_name: cluster.local
</span><span class="gi">+cluster_name: your_cluster_name.local
</span> # Subdomains of DNS domain to be resolved via /etc/resolv.conf for hostnet pods
 ndots: 2
 # Can be coredns, coredns_dual, manual or none
<span class="p">@@ -136,7 +136,7 @@</span> enable_nodelocaldns: true
 nodelocaldns_ip: 169.254.25.10

 # Can be docker_dns, host_resolvconf or none
<span class="gd">-resolvconf_mode: docker_dns
</span><span class="gi">+resolvconf_mode: host_resolvconf
</span> # Deploy netchecker app to verify DNS resolve as an HTTP service
 deploy_netchecker: false
 # Ip address of the kubernetes skydns service
<span class="p">@@ -175,9 +175,9 @@</span> dynamic_kubelet_configuration_dir: "{{ kubelet_config_dir | default(default_kube
 podsecuritypolicy_enabled: false

 # Make a copy of kubeconfig on the host that runs Ansible in {{ inventory_dir }}/artifacts
<span class="gd">-# kubeconfig_localhost: false
</span><span class="gi">+kubeconfig_localhost: true
</span> # Download kubectl onto the host that runs Ansible in {{ bin_dir }}
 # kubectl_localhost: false
</code></pre></div></div>

<p>Couple notes about those changes:</p>

<ul>
  <li>You need to specify <code class="language-plaintext highlighter-rouge">upstream_dns_servers</code>, otherwise DNS resolution won’t work, and Kubespray will even fail to download containers to bootstrap the cluster. The issue <a href="https://github.com/kubernetes-sigs/kubespray/issues/2831">#2831</a> was reported in kubespray about that (but closed unresolved due to inactivity).</li>
  <li>I prefer changing <code class="language-plaintext highlighter-rouge">cluster_name</code> to something meaningful. Pick your own name for a pet cluster :pig:</li>
</ul>

<p>My inventory file is pretty straightforward. My cluster doesn’t have HA for K8s master and etcd, as it has just 2 nodes.</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">nuc</span> <span class="py">ansible_host</span><span class="p">=</span><span class="s">192.168.0.10 ansible_user=core etcd_member_name=nuc5ppyh</span>
<span class="err">udoo</span> <span class="py">ansible_host</span><span class="p">=</span><span class="s">192.168.0.11 ansible_user=core</span>

<span class="nn">[kube-master]</span>
<span class="err">nuc</span>

<span class="nn">[etcd]</span>
<span class="err">nuc</span>

<span class="nn">[kube-node]</span>
<span class="err">udoo</span>

<span class="nn">[k8s-cluster:children]</span>
<span class="err">kube-master</span>
<span class="err">kube-node</span>

</code></pre></div></div>

<h2 id="cluster-provisioning">Cluster provisioning</h2>

<p>Provisioning requires just a single command, but prepare for a long process :clock1:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible-playbook <span class="nt">-i</span> inventory-x64/inventory.ini kubespray-x64/cluster.yml <span class="nt">-b</span> <span class="nt">-v</span> <span class="o">&gt;</span> install.log
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-b</code> stands for <code class="language-plaintext highlighter-rouge">become</code>, which means <code class="language-plaintext highlighter-rouge">sudo</code> to CoreOS nodes in our case, while <code class="language-plaintext highlighter-rouge">-v</code> produces a verbose output to the <code class="language-plaintext highlighter-rouge">install.log</code> file. If cluster provisioning fails, look at the last lines of this log file.</p>

<p>Once provisioning is completed, copy <code class="language-plaintext highlighter-rouge">artifacts/admin.conf</code> to <code class="language-plaintext highlighter-rouge">~/.kube/config</code> file and install <code class="language-plaintext highlighter-rouge">kubectl</code> on your machine.</p>

<p>Now (hopefully) you have a running cluster, and you can verify it by invoking some kubectl commands:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl version
Client Version: version.Info<span class="o">{</span>Major:<span class="s2">"1"</span>, Minor:<span class="s2">"15"</span>, GitVersion:<span class="s2">"v1.15.1"</span>, GitCommit:<span class="s2">"4485c6f18cee9a5d3c3b4e523bd27972b1b53892"</span>, GitTreeState:<span class="s2">"clean"</span>, BuildDate:<span class="s2">"2019-07-18T14:25:20Z"</span>, GoVersion:<span class="s2">"go1.12.7"</span>, Compiler:<span class="s2">"gc"</span>, Platform:<span class="s2">"darwin/amd64"</span><span class="o">}</span>
Server Version: version.Info<span class="o">{</span>Major:<span class="s2">"1"</span>, Minor:<span class="s2">"14"</span>, GitVersion:<span class="s2">"v1.14.3"</span>, GitCommit:<span class="s2">"5e53fd6bc17c0dec8434817e69b04a25d8ae0ff0"</span>, GitTreeState:<span class="s2">"clean"</span>, BuildDate:<span class="s2">"2019-06-06T01:36:19Z"</span>, GoVersion:<span class="s2">"go1.12.5"</span>, Compiler:<span class="s2">"gc"</span>, Platform:<span class="s2">"linux/amd64"</span><span class="o">}</span>

<span class="nv">$ </span>kubectl cluster-info
Kubernetes master is running at https://192.168.0.10:6443
coredns is running at https://192.168.0.10:6443/api/v1/namespaces/kube-system/services/coredns:dns/proxy
kubernetes-dashboard is running at https://192.168.0.10:6443/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy

To further debug and diagnose cluster problems, use <span class="s1">'kubectl cluster-info dump'</span><span class="nb">.</span>

<span class="nv">$ </span>kubectl get nodes
NAME   STATUS   ROLES    AGE   VERSION
nuc    Ready    master   28d   v1.14.3
udoo   Ready    &lt;none&gt;   28d   v1.14.3

<span class="nv">$ </span>kubectl get all <span class="nt">--all-namespaces</span>
NAMESPACE     NAME                                           READY   STATUS    RESTARTS   AGE
kube-system   pod/calico-kube-controllers-79c7fd7f68-cshvb   1/1     Running   3          28d
kube-system   pod/calico-node-bzc7t                          1/1     Running   3          28d
kube-system   pod/calico-node-sr6x8                          1/1     Running   3          28d
kube-system   pod/coredns-56bc6b976d-2zslx                   1/1     Running   3          28d
kube-system   pod/coredns-56bc6b976d-p9jql                   1/1     Running   3          28d
kube-system   pod/dns-autoscaler-5fc5fdbf6-f4zq7             1/1     Running   3          28d
kube-system   pod/kube-apiserver-nuc                         1/1     Running   4          28d
kube-system   pod/kube-controller-manager-nuc                1/1     Running   4          28d
kube-system   pod/kube-proxy-6xnwn                           1/1     Running   3          28d
kube-system   pod/kube-proxy-pqfcw                           1/1     Running   3          28d
kube-system   pod/kube-scheduler-nuc                         1/1     Running   4          28d
kube-system   pod/kubernetes-dashboard-6c7466966c-lmh72      1/1     Running   4          28d
kube-system   pod/nginx-proxy-udoo                           1/1     Running   3          28d
kube-system   pod/nodelocaldns-7pfm2                         1/1     Running   3          28d
kube-system   pod/nodelocaldns-k8kll                         1/1     Running   3          28d


NAMESPACE     NAME                           TYPE        CLUSTER-IP     EXTERNAL-IP   PORT<span class="o">(</span>S<span class="o">)</span>                  AGE
default       service/kubernetes             ClusterIP   10.233.0.1     &lt;none&gt;        443/TCP                  28d
kube-system   service/coredns                ClusterIP   10.233.0.3     &lt;none&gt;        53/UDP,53/TCP,9153/TCP   28d
kube-system   service/kubernetes-dashboard   ClusterIP   10.233.5.252   &lt;none&gt;        443/TCP                  28d

NAMESPACE     NAME                          DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR                 AGE
kube-system   daemonset.apps/calico-node    2         2         2       2            2           &lt;none&gt;                        28d
kube-system   daemonset.apps/kube-proxy     2         2         2       2            2           beta.kubernetes.io/os<span class="o">=</span>linux   28d
kube-system   daemonset.apps/nodelocaldns   2         2         2       2            2           &lt;none&gt;                        28d

NAMESPACE     NAME                                      READY   UP-TO-DATE   AVAILABLE   AGE
kube-system   deployment.apps/calico-kube-controllers   1/1     1            1           28d
kube-system   deployment.apps/coredns                   2/2     2            2           28d
kube-system   deployment.apps/dns-autoscaler            1/1     1            1           28d
kube-system   deployment.apps/kubernetes-dashboard      1/1     1            1           28d

NAMESPACE     NAME                                                 DESIRED   CURRENT   READY   AGE
kube-system   replicaset.apps/calico-kube-controllers-79c7fd7f68   1         1         1       28d
kube-system   replicaset.apps/coredns-56bc6b976d                   2         2         2       28d
kube-system   replicaset.apps/dns-autoscaler-5fc5fdbf6             1         1         1       28d
kube-system   replicaset.apps/kubernetes-dashboard-6c7466966c      1         1         1       28d
</code></pre></div></div>

<p>Long story short, it’s Kubernetes v1.14.3 with the following components:</p>
<ul>
  <li>Calico for pod networking</li>
  <li>CoreDNS as Kubernetes DNS server</li>
  <li>Nginx as proxy to API Server for worker nodes (makes it available on <code class="language-plaintext highlighter-rouge">localhost:6443</code>), and is called a <a href="https://github.com/kubernetes-sigs/kubespray/blob/master/docs/ha-mode.md#kube-apiserver">localhost loadbalancing</a> in Kubespray docs.</li>
  <li>Kubernetes web-based dashboard</li>
</ul>

<p>One important thing we’re missing is an Ingress for an external acces to cluster, but we’ll configure it next time.</p>]]></content><author><name>Maksym Romanowski</name></author><category term="kubernetes" /><category term="coreos" /><category term="kubernetes" /><category term="kubespray" /><category term="nuc" /><category term="udoo" /><summary type="html"><![CDATA[At this point I have two Linux machines running CoreOS Container Linux. Now it’s time to finally install Kubernetes on them!]]></summary></entry><entry><title type="html">Home pet cluster. Kubernetes on CoreOS. Part 1: don’t call us cattle!</title><link href="https://maxromanovsky.com/blog/kubernetes/2019-07-06-coreos-k8s-home-baremetal-01.html" rel="alternate" type="text/html" title="Home pet cluster. Kubernetes on CoreOS. Part 1: don’t call us cattle!" /><published>2019-07-06T00:00:00+00:00</published><updated>2019-07-06T00:00:00+00:00</updated><id>https://maxromanovsky.com/blog/kubernetes/coreos-k8s-home-baremetal-01</id><content type="html" xml:base="https://maxromanovsky.com/blog/kubernetes/2019-07-06-coreos-k8s-home-baremetal-01.html"><![CDATA[<p>I always wanted to run a small Kubernetes cluster at home.</p>

<p>Why not in cloud? Kubernetes in cloud is still expensive if it’s just for fun. And I have a couple mini computers at home, as well as desire to look how Kubernetes works on the network layer.</p>

<p>I was never satisfied with “fat” Linux distributions for running containers, and didn’t want to configure OS auto upgrades. Trying out CoreOS Container Linux sounded like a natural fit for my little pets.</p>

<p><strong>Upd 2020-04-23</strong>: I have migrated to <a href="https://www.flatcar-linux.org/">Flatcar Container Linux</a>, which is more or less a drop-in replacement for CoreOS Container Linux</p>

<!--more-->

<p>Anyway, I’ve decided to start with this:</p>
<ul>
  <li>Hardware
    <ul>
      <li>Intel <a href="https://www.intel.com/content/www/us/en/products/boards-kits/nuc/kits/nuc5ppyh.html">NUC5PPYH</a></li>
      <li>UDOO <a href="https://www.udoo.org/udoo-x86/">x86</a> (I have the first version)</li>
    </ul>
  </li>
  <li>Software
    <ul>
      <li>CoreOS <a href="https://coreos.com/os/docs/latest/">Container Linux</a></li>
      <li><a href="https://kubespray.io/">Kubespray</a> v2.10.4</li>
    </ul>
  </li>
</ul>

<h1 id="hardware">Hardware</h1>

<p>I have two x64 mini computers, which are good candidates for Kubernetes nodes. They are not equal, but powerful enough. And they are unique to me, not only because they’re so different :cat:</p>

<p>That’s why I still treat them as pets, not cattle.</p>

<p><img src="/assets/images/k8s-coreos-home-baremetal/pet-cattle.jpg" alt="Pets vs cattle" /></p>

<p>Image from <a href="https://devops.stackexchange.com/questions/653/what-is-the-definition-of-cattle-not-pets">StackExchange</a></p>

<p>One of them is called Nuc, and it’s breed is Intel <a href="https://www.intel.com/content/www/us/en/products/boards-kits/nuc/kits/nuc5ppyh.html">NUC5PPYH</a>.</p>

<p><img src="/assets/images/k8s-coreos-home-baremetal/nuc5ppyh.jpg" alt="NUC5PPYH" /></p>

<p>Image from <a href="https://www.amazon.com/Intel-Nuc5ppyh-Components-Silver-BOXNUC5PPYH/dp/B00XPVQHDU">Amazon</a></p>

<p>According to <a href="https://ark.intel.com/content/www/us/en/ark/products/87740/intel-nuc-kit-nuc5ppyh.html">spec</a>, it has 4-core Pentium <a href="https://ark.intel.com/content/www/us/en/ark/products/87261/intel-pentium-processor-n3700-2m-cache-up-to-2-40-ghz.html">N3700</a> and supports up to 8Gb RAM. Mine has 8Gb RAM and 1Tb HDD.</p>

<p>Another one responds to Udoo, and it’s breed is Udoo x86 (first version from <a href="https://www.kickstarter.com/projects/udoo/udoo-x86-the-most-powerful-maker-board-ever">Kickstarter</a>).</p>

<p><img src="/assets/images/k8s-coreos-home-baremetal/udoo.png" alt="Udoo x86" /></p>

<p>Image from <a href="https://www.kickstarter.com/projects/udoo/udoo-x86-the-most-powerful-maker-board-ever">Kickstarter</a></p>

<p>My edition is called UDOO X86 Advanced, and it has 4-core Celeron <a href="https://ark.intel.com/content/www/us/en/ark/products/91831/intel-celeron-processor-n3160-2m-cache-up-to-2-24-ghz.html">N3160</a>, as well as 4Gb of RAM, but rather small 8Gb eMMC.</p>

<p>Both processors are <a href="https://ark.intel.com/content/www/us/en/ark/compare.html?productIds=91831,87261">quite similar</a> in their performance, but Nuc has twice the memory and larger (but slower) HDD.</p>

<h1 id="coreos-installation">CoreOS installation</h1>

<p>Both pets have Ethernet port, and I prefer stable wired connection with DHCP address reservation configured to their MAC addresses. But here’s the thing, it’s either Ethernet or monitor in my apartment :smile:. I had to choose either to plug the physical screen or the network. This somewhat drove the approach I’ve used for OS installation.</p>

<p>I’ve used two USB drives:</p>
<ul>
  <li>One with the <a href="https://coreos.com/os/docs/latest/booting-with-iso.html">latest stable CoreOS ISO</a> flashed by <a href="https://www.balena.io/etcher/">Balena Etcher</a>. This ISO would be used to boot the CoreOS and run <code class="language-plaintext highlighter-rouge">coreos-install</code>.</li>
  <li>Another with couple of goodies:
    <ul>
      <li><a href="https://stable.release.core-os.net/amd64-usr/current/coreos_production_image.bin.bz2">Latest stable</a> <code class="language-plaintext highlighter-rouge">coreos_production_image.bin.bz2</code> (not to be confused with the ISO). That would be actually an image that is installed on the machine.</li>
      <li><code class="language-plaintext highlighter-rouge">ignition.json</code>, a CoreOS config file consumable by <code class="language-plaintext highlighter-rouge">coreos-install</code>.</li>
    </ul>
  </li>
</ul>

<p>Here’s the process I’ve followed to install the OS:</p>
<ul>
  <li>Generate a hash from the password:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">mkpasswd --method=SHA-512 --rounds=4096</code> should be used, as shown in <a href="https://coreos.com/os/docs/latest/clc-examples.html#generating-a-password-hash">an example</a></li>
      <li>I’m using the password for authentication as an alternative to SSH pubkey to log into the machine interactively, while it’s still connected to the screen, not network.</li>
    </ul>
  </li>
  <li>Write <code class="language-plaintext highlighter-rouge">ignition.yaml</code>, a human-readable version of CoreOS config file that contains:
    <ul>
      <li>Password hash</li>
      <li>SSH pubkeys</li>
      <li>CoreOS autoupdate strategy</li>
    </ul>
  </li>
  <li>I store my <code class="language-plaintext highlighter-rouge">ignition.yaml</code> in the repo (without password hash and pubkey) as a reference, but CoreOS installer uses another config format, which is a less-readable <code class="language-plaintext highlighter-rouge">json</code>. Latest <a href="https://github.com/coreos/container-linux-config-transpiler/releases">Config Transpiler</a> should be downloaded to create the JSON config: <code class="language-plaintext highlighter-rouge">ct &lt; ignition.yaml &gt; ignition.json</code>. There’s even a <a href="https://coreos.com/validate/">validation service</a> to check that config is well-formed.</li>
  <li>Resulting config is written to a USB drive (different from the one where bootable ISO is flashed), alongside with the <code class="language-plaintext highlighter-rouge">bin.bz2</code> image.</li>
  <li>After computer is booted from the CoreOS ISO run the installation command: <code class="language-plaintext highlighter-rouge">coreos-install -d /dev/target-disk -i /path/to/ignition.json -f /path/to/image.bin.bz2</code>, where:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">target-disk</code> is the disk where CoreOS should be installed (disk, not a partition). CoreOS will erase the entire disk and create a <a href="https://coreos.com/os/docs/latest/sdk-disk-partitions.html">new partition table</a>.</li>
      <li><code class="language-plaintext highlighter-rouge">/path/to</code> is path to the second USB drive (previously needs to be <code class="language-plaintext highlighter-rouge">mount</code>‘ed), containing both the image &amp; ignition config.</li>
    </ul>
  </li>
</ul>

<p>Here’s my <code class="language-plaintext highlighter-rouge">ignition.yaml</code> config:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Replace &lt;PWD_HASH&gt; with the password hash (or remove line if it's not necessary)</span>
<span class="c1"># Replace &lt;SSH_PUBKEY&gt; with the SSH public key. Add as many as you wish</span>
<span class="c1"># Download Config Transpiler: https://github.com/coreos/container-linux-config-transpiler/releases/latest</span>
<span class="c1"># Run ct &lt; ignition.yaml &gt; ignition.json to generate the resulting config</span>
<span class="na">passwd</span><span class="pi">:</span>
  <span class="na">users</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">core</span>
      <span class="c1"># https://coreos.com/os/docs/latest/clc-examples.html#generating-a-password-hash</span>
      <span class="c1"># mkpasswd --method=SHA-512 --rounds=4096</span>
      <span class="na">password_hash</span><span class="pi">:</span> <span class="s2">"</span><span class="s">&lt;PWD_HASH&gt;"</span>
      <span class="na">ssh_authorized_keys</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">&lt;SSH_PUBKEY&gt;</span>
<span class="na">update</span><span class="pi">:</span>
  <span class="na">group</span><span class="pi">:</span> <span class="s">stable</span>
<span class="na">locksmith</span><span class="pi">:</span>
  <span class="na">reboot_strategy</span><span class="pi">:</span> <span class="s">reboot</span>
</code></pre></div></div>

<p>If you’re lucky, then CoreOS would be installed on your machine. You can login either with the password, or an SSH key. It doesn’t have much services installed, and there isn’t any package manager to install more. The idea is to spin up containers to do the work.</p>

<p>And the next step would be Kubernetes installation to run containers at scale.</p>]]></content><author><name>Maksym Romanowski</name></author><category term="kubernetes" /><category term="coreos" /><category term="kubernetes" /><category term="kubespray" /><category term="nuc" /><category term="udoo" /><summary type="html"><![CDATA[I always wanted to run a small Kubernetes cluster at home. Why not in cloud? Kubernetes in cloud is still expensive if it’s just for fun. And I have a couple mini computers at home, as well as desire to look how Kubernetes works on the network layer. I was never satisfied with “fat” Linux distributions for running containers, and didn’t want to configure OS auto upgrades. Trying out CoreOS Container Linux sounded like a natural fit for my little pets. Upd 2020-04-23: I have migrated to Flatcar Container Linux, which is more or less a drop-in replacement for CoreOS Container Linux]]></summary></entry></feed>