<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>writing on harsha pathuri</title>
    <link>https://dlibre.com/writing/</link>
    <description>Recent writing on harsha pathuri</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-US</language>
    <managingEditor>harsha@dlibre.com (harsha pathuri)</managingEditor>
    <webMaster>harsha@dlibre.com (harsha pathuri)</webMaster>
    <copyright>© fuck copyright</copyright>
    <lastBuildDate>Sun, 17 May 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://dlibre.com/writing/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>how the dlibre cluster is put together</title>
      <link>https://dlibre.com/writing/cluster-01-how-the-dlibre-cluster-works/</link>
      <pubDate>Sun, 17 May 2026 00:00:00 +0000</pubDate>
      <author>harsha@dlibre.com (harsha pathuri)</author>
      <guid>https://dlibre.com/writing/cluster-01-how-the-dlibre-cluster-works/</guid>
      <description>&lt;p&gt;this post is the first draft in a short series about the cluster behind dlibre.com. the goal is not to write a generic kubernetes explainer. it is to document the actual shape of this cluster, why it is set up this way, and what happens when someone opens this blog in a browser.&lt;/p&gt;
&lt;p&gt;the current setup is intentionally simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one oci vm&lt;/li&gt;
&lt;li&gt;one k3s server&lt;/li&gt;
&lt;li&gt;argocd as the gitops controller&lt;/li&gt;
&lt;li&gt;traefik as ingress&lt;/li&gt;
&lt;li&gt;metallb for the service ip&lt;/li&gt;
&lt;li&gt;external-dns for dns records&lt;/li&gt;
&lt;li&gt;cert-manager for certificates&lt;/li&gt;
&lt;li&gt;longhorn for persistent storage&lt;/li&gt;
&lt;li&gt;dex for oidc on the admin side&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;that means this is not pretending to be a highly available multi-node platform. it is a small, reproducible cluster that can be rebuilt from git, and that tradeoff matters for understanding the rest of the design.&lt;/p&gt;
&lt;h2 id=&#34;the-base-machine&#34;&gt;the base machine&lt;/h2&gt;
&lt;p&gt;the cluster starts as a single server with k3s installed from the checked-in config in the cluster repo.&lt;/p&gt;
&lt;p&gt;that config does a few important things up front:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it adds &lt;code&gt;dlibre&lt;/code&gt;, &lt;code&gt;dlibre.com&lt;/code&gt;, and the public ip as tls sans for the api server&lt;/li&gt;
&lt;li&gt;it disables &lt;code&gt;local-storage&lt;/code&gt; because longhorn is used for persistent volumes instead&lt;/li&gt;
&lt;li&gt;it disables &lt;code&gt;servicelb&lt;/code&gt; because ingress exposure is handled with metallb and traefik&lt;/li&gt;
&lt;li&gt;it wires the kubernetes api server to dex for oidc auth&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;so the machine is not just &amp;ldquo;running kubernetes.&amp;rdquo; it is already opinionated toward gitops, external ingress, and cluster-wide login through dex.&lt;/p&gt;
&lt;h2 id=&#34;bootstrap-and-gitops&#34;&gt;bootstrap and gitops&lt;/h2&gt;
&lt;p&gt;after k3s is up, the cluster is bootstrapped with a small argocd seed from &lt;code&gt;argocd/bootstrap&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;the bootstrap step does three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;installs argocd itself&lt;/li&gt;
&lt;li&gt;adds the private github repo as an argocd repository using an ssh deploy key&lt;/li&gt;
&lt;li&gt;creates the root application that points at &lt;code&gt;argocd/apps&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;from that point on, the cluster converges from git. that is the key design choice in this setup.&lt;/p&gt;
&lt;p&gt;instead of manually installing components one by one, argocd reads the repo and continuously applies the desired state. if traefik, dex, external-dns, or the dlibre site drift away from what the repo says, argocd pulls them back.&lt;/p&gt;
&lt;p&gt;the practical consequence is that rebuilding the cluster is mostly a matter of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reinstalling k3s&lt;/li&gt;
&lt;li&gt;restoring the sealed-secrets keys&lt;/li&gt;
&lt;li&gt;bootstrapping argocd&lt;/li&gt;
&lt;li&gt;waiting for applications to sync&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;the-platform-services&#34;&gt;the platform services&lt;/h2&gt;
&lt;p&gt;once the root application is active, argocd installs the cluster building blocks in waves.&lt;/p&gt;
&lt;h3 id=&#34;dex&#34;&gt;dex&lt;/h3&gt;
&lt;p&gt;dex is the oidc issuer for the admin surface. argocd, the kubernetes api, kubernetes dashboard, and longhorn trust dex. google oauth is the upstream identity provider, and the &lt;code&gt;cluster-admins@dlibre.com&lt;/code&gt; group is used for authorization.&lt;/p&gt;
&lt;p&gt;this is mostly about operator access, not end-user auth for public services.&lt;/p&gt;
&lt;h3 id=&#34;metallb&#34;&gt;metallb&lt;/h3&gt;
&lt;p&gt;because this is not running behind a cloud kubernetes load balancer, metallb advertises the ingress address inside the cluster network. right now the pool is deliberately tiny: it is a single public ip, &lt;code&gt;144.24.140.57/32&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;that fits the current design. there is one node, one ingress tier, and one public edge.&lt;/p&gt;
&lt;h3 id=&#34;traefik&#34;&gt;traefik&lt;/h3&gt;
&lt;p&gt;traefik is installed as a &lt;code&gt;LoadBalancer&lt;/code&gt; service. metallb gives that service the public ingress ip, and traefik then becomes the entry point for http and https traffic.&lt;/p&gt;
&lt;p&gt;traefik is the component that turns hostname-based routing into actual backend service selection inside the cluster.&lt;/p&gt;
&lt;h3 id=&#34;external-dns&#34;&gt;external-dns&lt;/h3&gt;
&lt;p&gt;external-dns watches dns resources in the cluster and writes records to cloudflare. that means dns is also described in git and reconciled from the cluster side, instead of being managed manually in a dashboard.&lt;/p&gt;
&lt;p&gt;for &lt;code&gt;dlibre.com&lt;/code&gt;, there is a &lt;code&gt;DNSEndpoint&lt;/code&gt; that publishes an &lt;code&gt;A&lt;/code&gt; record to the ingress ip. for some other services, records can point at the shared traefik ingress hostname instead.&lt;/p&gt;
&lt;h3 id=&#34;cert-manager&#34;&gt;cert-manager&lt;/h3&gt;
&lt;p&gt;cert-manager handles tls using a letsencrypt cluster issuer with cloudflare dns challenges. there is a wildcard certificate for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dlibre.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*.dlibre.com&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;the certificate secret is reflected across namespaces, so workloads can reuse the same wildcard cert without manually duplicating tls material in each app namespace.&lt;/p&gt;
&lt;h3 id=&#34;longhorn-and-postgres&#34;&gt;longhorn and postgres&lt;/h3&gt;
&lt;p&gt;longhorn is the persistent storage layer for stateful workloads. postgres exists as shared platform data infrastructure for services like dex. this matters less for the blog itself, but it explains why the cluster is set up to support more than static sites.&lt;/p&gt;
&lt;h2 id=&#34;what-happens-when-someone-opens-dlibrecom&#34;&gt;what happens when someone opens dlibre.com&lt;/h2&gt;
&lt;p&gt;this is the simplest way to think about the request path:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;a browser resolves &lt;code&gt;dlibre.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;cloudflare returns the public ip managed by the cluster dns setup&lt;/li&gt;
&lt;li&gt;that ip lands on the traefik &lt;code&gt;LoadBalancer&lt;/code&gt; service&lt;/li&gt;
&lt;li&gt;traefik matches the &lt;code&gt;Host&lt;/code&gt; header for &lt;code&gt;dlibre.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;traefik forwards the request to the &lt;code&gt;dlibre&lt;/code&gt; service in the &lt;code&gt;dlibre&lt;/code&gt; namespace&lt;/li&gt;
&lt;li&gt;the pod serves the static site over port 80&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;the tls certificate presented at the edge is the wildcard cert issued by cert-manager and made available to the app through the cluster certificate setup.&lt;/p&gt;
&lt;h2 id=&#34;how-this-blog-is-deployed&#34;&gt;how this blog is deployed&lt;/h2&gt;
&lt;p&gt;the blog itself is not built inside the cluster repo. the cluster repo only declares how the already-built site should run.&lt;/p&gt;
&lt;p&gt;the actual site deployment is managed by the &lt;code&gt;dlibre&lt;/code&gt; argocd application, which pulls a helm chart from &lt;code&gt;https://dlibre.github.io/charts&lt;/code&gt; and fills it with the checked-in values from the cluster repo.&lt;/p&gt;
&lt;p&gt;those values tell argocd to deploy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one replica&lt;/li&gt;
&lt;li&gt;image &lt;code&gt;ghcr.io/dlibre/dlibre.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;a pinned image tag&lt;/li&gt;
&lt;li&gt;an image pull secret for ghcr&lt;/li&gt;
&lt;li&gt;a clusterip service on port 80&lt;/li&gt;
&lt;li&gt;an ingress for &lt;code&gt;dlibre.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;tls using &lt;code&gt;letsencrypt-wildcard-cert-dlibre.com&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;so the cluster repo is the runtime contract for the site, not the site source itself.&lt;/p&gt;
&lt;p&gt;inside this repo, the site builds to static output and the container image is very small: the generated files are copied into an &lt;code&gt;nginx:alpine&lt;/code&gt; image and served from there. that gives the deployment a straightforward runtime model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;build static files&lt;/li&gt;
&lt;li&gt;publish image to ghcr&lt;/li&gt;
&lt;li&gt;update the deployed image tag in the cluster repo&lt;/li&gt;
&lt;li&gt;let argocd reconcile the new version&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;why-this-shape-works-well&#34;&gt;why this shape works well&lt;/h2&gt;
&lt;p&gt;for a small personal platform, this setup buys a lot without requiring a very large control plane:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the entire cluster can be reasoned about from git&lt;/li&gt;
&lt;li&gt;ingress, dns, and tls are part of the same declarative workflow&lt;/li&gt;
&lt;li&gt;admin auth is centralized&lt;/li&gt;
&lt;li&gt;stateful services can be added without redesigning the whole stack&lt;/li&gt;
&lt;li&gt;static sites like this one remain cheap to run&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;the obvious tradeoff is failure domain. one server still means one server. if the vm is down, the cluster is down. if the ingress node is down, everything public is down. none of that is hidden by kubernetes.&lt;/p&gt;
&lt;h2 id=&#34;what-i-want-to-document-next&#34;&gt;what i want to document next&lt;/h2&gt;
&lt;p&gt;this is the overview post. the next drafts in the series should probably split the details into smaller pieces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;how argocd bootstraps the cluster and keeps it converged&lt;/li&gt;
&lt;li&gt;how ingress, dns, and wildcard tls fit together&lt;/li&gt;
&lt;li&gt;how app delivery works from this repo to ghcr to the running pod&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;that breakdown is probably easier to maintain than one giant post, especially as the cluster grows beyond the current single-node design.&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
