<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[clstBlog]]></title><description><![CDATA[clstBlog]]></description><link>https://clstb.sh</link><generator>RSS for Node</generator><lastBuildDate>Mon, 13 Apr 2026 21:49:19 GMT</lastBuildDate><atom:link href="https://clstb.sh/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Managing data models in Go]]></title><description><![CDATA[Once upon a time a developer created a new Go service.
He started by defining the service interface as protobuf spec, generated code, and saw it was good.
The need to store data creeped up to him. Being a smart hackernews reader he chooses sqlc to ge...]]></description><link>https://clstb.sh/managing-data-models-in-go</link><guid isPermaLink="true">https://clstb.sh/managing-data-models-in-go</guid><category><![CDATA[Go Language]]></category><dc:creator><![CDATA[Claas Störtenbecker]]></dc:creator><pubDate>Thu, 04 Mar 2021 18:59:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/ufnhDMFOZ_M/upload/v1645556359669/OPf7WCV7a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Once upon a time a developer created a new Go service.
He started by defining the service interface as protobuf spec, generated code, and saw it was good.
The need to store data creeped up to him. Being a smart hackernews reader he chooses <a target="_blank" href="https://github.com/kyleconroy/sqlc">sqlc</a> to generate CRUD and model code from sql.
Then the product owner reached out to implement new business logic.
Striving for a clean code base the developer creates a new data structure once again, adding methods to the new struct.
After awhile inconsistency in the code base started to show.  </p>
<blockquote>
<p>In how many places does code break if we touch files code is generated from? Many.</p>
</blockquote>
<p>Let's make some observations.</p>
<ol>
<li>The database schema is most likely the root of the datamodel.</li>
<li>Protocol buffers exist mainly to define an interface, used for GRPC communication.</li>
<li>Business logic doesn't have to be a method of a struct, though it makes sense in many cases.</li>
</ol>
<p>The goal should be to have a single struct for each data model which is used throughout the entire codebase. To achieve this we make use of struct embedding and methods that handle transcoding.</p>
<pre><code class="lang-go"><span class="hljs-comment">// generated</span>

<span class="hljs-comment">// /pkg/pb/account.pb.go</span>

<span class="hljs-keyword">type</span> Account <span class="hljs-keyword">struct</span> {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Id   <span class="hljs-keyword">string</span> <span class="hljs-string">`protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`</span>
    Name <span class="hljs-keyword">string</span> <span class="hljs-string">`protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`</span>
}
</code></pre>
<pre><code class="lang-go"><span class="hljs-comment">// generated</span>

<span class="hljs-comment">// /pkg/db/model.go</span>

<span class="hljs-keyword">type</span> Account <span class="hljs-keyword">struct</span> {
    ID   uuid.UUID <span class="hljs-string">`json:"id"`</span>
    Name <span class="hljs-keyword">string</span>    <span class="hljs-string">`json:"name"`</span>
}
</code></pre>
<pre><code class="lang-go"><span class="hljs-comment">// /pkg/business/account.go</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">"pkg/db"</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">"pkg/pb"</span>

<span class="hljs-keyword">type</span> Account <span class="hljs-keyword">struct</span> {
    db.Account
} 

<span class="hljs-comment">// This is pretty straight forward.</span>
<span class="hljs-comment">// Generated query functions will return a db.Account and we can transcode swiftly.</span>
<span class="hljs-comment">// Nothing can break here.</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">AccountFromDB</span><span class="hljs-params">(a db.Account)</span> <span class="hljs-title">Account</span></span> {
    <span class="hljs-keyword">return</span> Account{Account: a}
}

<span class="hljs-comment">// Arguably the most complicated function.</span>
<span class="hljs-comment">// We need to transcode a protobuf struct to our business logic struct.</span>
<span class="hljs-comment">// Writing a generator for std data types shouldn't be to complicated.</span>
<span class="hljs-comment">// How to extend it to custom types needs some thought.</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">AccountFromPB</span><span class="hljs-params">(a *pb.Account)</span> <span class="hljs-params">(Account, error)</span></span> {
    id, err := uuid.FromStringOrNil(a.Id)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> Account{}, err
    }

    account := Account{}
    account.ID = id
    account.Name = a.Name

    <span class="hljs-keyword">return</span> account, <span class="hljs-literal">nil</span>
}

<span class="hljs-comment">// This is pretty simple again and could easily be generated.</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(a Account)</span> <span class="hljs-title">PB</span><span class="hljs-params">()</span> *<span class="hljs-title">pb</span>.<span class="hljs-title">Account</span></span> {
    <span class="hljs-keyword">return</span> &amp;pb.Account{
        Id:   a.ID.String(),
        Name: a.Name,
    }
}
</code></pre>
<p>When investing the effort to write robust generators one time, maintenance headache in the code base is reduced significantly.
Mentioned generator could even be used universally over services.</p>
]]></content:encoded></item><item><title><![CDATA[Secure HA Kubernetes on bare metal using k3s]]></title><description><![CDATA[This walkthrough will leave you with a kubeconfig to a fully functional, secure cluster.

What is k3s?
Kubernetes is was too hard for the solo developer.
Using k3s you can deploy Kubernetes in a couple of commands per node.
The Kubernetes distributio...]]></description><link>https://clstb.sh/secure-ha-kubernetes-on-bare-metal-using-k3s</link><guid isPermaLink="true">https://clstb.sh/secure-ha-kubernetes-on-bare-metal-using-k3s</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[k8s]]></category><dc:creator><![CDATA[Claas Störtenbecker]]></dc:creator><pubDate>Mon, 21 Dec 2020 19:04:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/M5tzZtFCOfs/upload/v1645556590487/Oje7eF5-U0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p> This walkthrough will leave you with a <code>kubeconfig</code> to a fully functional, secure cluster.</p>
</blockquote>
<h1 id="heading-what-is-k3shttpsgithubcomk3s-iok3s">What is <a target="_blank" href="https://github.com/k3s-io/k3s">k3s</a>?</h1>
<p>Kubernetes is was too hard for the solo developer.
Using k3s you can deploy Kubernetes in a couple of commands per node.
The Kubernetes distribution is:</p>
<ul>
<li>fully conformant</li>
<li>production-ready</li>
<li>lightweight</li>
<li>packaged in a single binary</li>
</ul>
<p><em>I personally run 3 SKVM-4G from <a target="_blank" href="https://www.maxkvm.com">MaxKVM</a> for 18$/month with student discount.
Each node has 2 EPYC cores, 4GB ECC, 3TB traffic @ 1GBs, 75GB NVME; unmatched for that price.</em></p>
<h1 id="heading-1-setup-node-iptables">1. Setup node iptables</h1>
<p>To isolate network communication we need to setup some firewall rules on each node.
This tutorial will use <a target="_blank" href="https://help.ubuntu.com/community/UFW">ufw</a> to configure iptables but you may use any other tool.
I assume that each node will have two distinct network interfaces:</p>
<ul>
<li><code>eth0</code> assigned with a public IP used for internet communication</li>
<li><code>eth1</code> assigned with a private IP used for intranet communication</li>
</ul>
<p>We will start by blocking incoming and allowing outgoing traffic.</p>
<pre><code class="lang-shell">sudo ufw default allow outgoing
sudo ufw default deny incoming
</code></pre>
<p>Further we need to enable <code>ssh</code> access to the node.</p>
<pre><code class="lang-shell">sudo ufw allow ssh
</code></pre>
<p>We allow communication through the intranet. You can also add each node IP seperately.
In this example <code>eth1</code> is part of the subnet <code>172.16.0.0/12</code>.</p>
<pre><code class="lang-shell">sudo ufw allow from 172.16.0.0/12
</code></pre>
<p>We allow communication to the CNI (Container Network Interface) from the Pod CIDR.</p>
<pre><code class="lang-shell">sudo ufw allow in on cni0 from 10.42.0.0/16
</code></pre>
<p>We allow communication to the Kubernetes API.</p>
<pre><code class="lang-shell">sudo ufw allow 6443/tcp
</code></pre>
<h1 id="heading-2-install-kubernetes">2. Install Kubernetes</h1>
<p>To join nodes k3s needs a shared secret. We can generate one with:</p>
<pre><code class="lang-shell">openssl rand -base64 32
</code></pre>
<h3 id="heading-21-first-node">2.1 First node</h3>
<p>We configure necassary installation parameters on the server:</p>
<ul>
<li><code>K3S_TOKEN</code> <em>the secret we generated in step 2</em></li>
<li><code>INSTALL_K3S_EXEC</code> <em>arguments the k3s installer is called with</em><ul>
<li><code>--cluster-init</code> <em>this node initializes a completely new cluster</em></li>
<li><code>-i {eth1_ip}</code> <em>this node uses the internal IP</em></li>
<li><code>--flannel-iface eth1</code> <em>the cluster network is build on the intranet</em></li>
<li><code>--disable traefik</code> <em>k3s ships with outdated traefik as ingress</em></li>
</ul>
</li>
</ul>
<pre><code class="lang-shell">export K3S_TOKEN="{generated_secret}" &amp;
export INSTALL_K3S_EXEC="server \ 
    --cluster-init \
    -i {eth1_ip} \
    --flannel-iface eth1 \
    --disable traefik"
</code></pre>
<p>Now we execute the install script.</p>
<pre><code class="lang-shell">curl -sfL https://get.k3s.io | sh -
</code></pre>
<h3 id="heading-22-other-nodes">2.2 Other nodes</h3>
<p>Configuration is similar to the first node.
Now we do not initialize a cluster, but join the cluster that was created in <strong>2.1</strong>.</p>
<pre><code class="lang-shell">export K3S_TOKEN="{generated_secret}" &amp;
export INSTALL_K3S_EXEC="server \ 
    --server https://{node1_internal_ip}:6443 \
    -i {eth1_ip} \
    --flannel-iface eth1 \
    --disable traefik"
</code></pre>
<p>Also execute the install script.</p>
<pre><code class="lang-shell">curl -sfL https://get.k3s.io | sh -
</code></pre>
<h3 id="heading-23-finalize">2.3 Finalize</h3>
<p>On any node run <code>kubectl get nodes</code> to check if your cluster was build sucessfully.
Now remove from <code>/etc/systemd/system/k3s.service</code> on every node:</p>
<ul>
<li><code>--cluster-init</code> <em>if it is the first node</em></li>
<li><code>--server</code>  <em>else</em></li>
</ul>
<p>at the bottom of the file.</p>
<p>Reload and check the node after editing.</p>
<pre><code class="lang-shell">systemctl daemon-reload
service k3s restart
kubectl get nodes
</code></pre>
<p><strong>Copy <code>/etc/rancher/k3s/k3s.yaml</code> to your machine.
After that replace <code>localhost</code> with a node or loadbalancer IP and you are done.</strong></p>
]]></content:encoded></item></channel></rss>