<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>2026-08 on Funky Si's Blog (Dev)</title><link>https://blog-dev.funkysi1701.com/2026/08/</link><description>Recent content in 2026-08 on Funky Si's Blog (Dev)</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><managingEditor>funkysi1701@gmail.com (Simon Foster)</managingEditor><webMaster>funkysi1701@gmail.com (Simon Foster)</webMaster><lastBuildDate>Tue, 18 Aug 2026 08:00:00 +0000</lastBuildDate><atom:link href="https://blog-dev.funkysi1701.com/2026/08/index.xml" rel="self" type="application/rss+xml"/><item><title>GitOps with Flux on my k3s cluster</title><link>https://blog-dev.funkysi1701.com/posts/2026/gitops-with-flux-on-my-k3s-cluster/</link><author>funkysi1701@gmail.com (funkysi1701)</author><pubDate>Tue, 18 Aug 2026 08:00:00 +0000</pubDate><guid>https://blog-dev.funkysi1701.com/posts/2026/gitops-with-flux-on-my-k3s-cluster/</guid><category term="Flux">Flux</category><category term="GitOps">GitOps</category><category term="Kubernetes">Kubernetes</category><category term="Helm">Helm</category><category term="k3s">k3s</category><category term="DevOps">DevOps</category><category term="SOPS">SOPS</category><category term="cert-manager">cert-manager</category><media:content medium="image" type="image/png" url="https://blog-dev.funkysi1701.com/images/2026/gitops-with-flux-k3s.png"/><description>&lt;p&gt;I already knew how to put things &lt;em&gt;on&lt;/em&gt; a Kubernetes cluster. &lt;a href="https://blog-dev.funkysi1701.com/posts/2025/deploying-hugo-with-helm/"&gt;Helm&lt;/a&gt;
packages the YAML. &lt;a href="https://blog-dev.funkysi1701.com/posts/2025/learning-kubernetes/"&gt;&lt;code&gt;kubectl apply&lt;/code&gt;&lt;/a&gt;
makes it exist. &lt;a href="https://blog-dev.funkysi1701.com/posts/2025/kubernetes-and-letsencrypt/"&gt;cert-manager&lt;/a&gt;
mints the certificates. What I did not have was a boring answer to “what happens next week, when I have forgotten which laptop I ran that from?”&lt;/p&gt;
&lt;p&gt;GitOps is that answer, at least for &lt;strong&gt;platform&lt;/strong&gt; on my homelab. In practice, GitOps means keeping the desired cluster state in git and having an in-cluster controller continuously reconcile the live cluster back to that state. &lt;strong&gt;&lt;code&gt;simon-cluster&lt;/code&gt;&lt;/strong&gt; is the name I gave the &lt;a href="https://k3s.io/" target="_blank" rel="noopener noreferrer"&gt;k3s&lt;/a&gt;
cluster I run at home — a small Kubernetes estate for shared ingress, certificates, monitoring, and the GitHub Actions runners this blog builds on. &lt;a href="https://fluxcd.io/" target="_blank" rel="noopener noreferrer"&gt;Flux CD&lt;/a&gt;
on that cluster watches a private platform repo and keeps it in line with &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The useful part is not “I put YAML in git.” It is the &lt;strong&gt;split&lt;/strong&gt;. Flux owns MetalLB, Traefik, cert-manager, GitHub runners, the in-cluster registry, Cloudflare Tunnel, and monitoring. Application Helm releases in &lt;code&gt;develop&lt;/code&gt; / &lt;code&gt;main&lt;/code&gt; / &lt;code&gt;test&lt;/code&gt; still come from &lt;strong&gt;their own repos and pipelines&lt;/strong&gt;. Azure DevOps is still in that second column. Flux is not pretending to be the whole estate.&lt;/p&gt;
&lt;h2 id="pull-not-push"&gt;Pull, not push&lt;a class="anchor ms-1" href="#pull-not-push" aria-label="Permalink: Pull, not push"&gt;&lt;i class="fas fa-link" aria-hidden="true"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A lot of “GitOps” talk is really CI with extra steps: a pipeline authenticates to the cluster and runs &lt;code&gt;helm upgrade&lt;/code&gt;. That is a &lt;strong&gt;push&lt;/strong&gt;. It works. I still do it for apps.&lt;/p&gt;
&lt;p&gt;Flux is a &lt;strong&gt;pull&lt;/strong&gt;. The cluster has a &lt;code&gt;GitRepository&lt;/code&gt; pointed at a private platform repo on &lt;code&gt;main&lt;/code&gt;. Child &lt;code&gt;Kustomization&lt;/code&gt; objects apply paths under that repo on a schedule. If someone (or some leftover pipeline) changes a Flux-managed object by hand, the next reconcile puts it back.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;source.toolkit.fluxcd.io/v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;GitRepository&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;flux-system&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;flux-system&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;interval&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1m&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ref&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;branch&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;main&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;url&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;https://github.com/example/example-config.git&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bootstrap is once: install the Flux controllers (&lt;code&gt;gotk-components.yaml&lt;/code&gt;), give Flux a git credential and an age key for SOPS, apply &lt;code&gt;gotk-sync.yaml&lt;/code&gt;. After that I mostly work in git rather than reaching for kubeconfig.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git add -A &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit -m &lt;span style="color:#e6db74"&gt;&amp;#34;…&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git push
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;flux get all -A
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;flux reconcile kustomization flux-system --with-source
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I still keep &lt;code&gt;kubectl&lt;/code&gt; for poking at pods. I try not to use it as the source of truth for anything Flux already owns.&lt;/p&gt;
&lt;h2 id="how-the-repo-is-laid-out"&gt;How the repo is laid out&lt;a class="anchor ms-1" href="#how-the-repo-is-laid-out" aria-label="Permalink: How the repo is laid out"&gt;&lt;i class="fas fa-link" aria-hidden="true"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;clusters/simon-cluster/ # Flux bootstrap + child Kustomizations
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;infrastructure/ # HelmRepositories, HelmReleases, CRs, SOPS
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;clusters/simon-cluster&lt;/code&gt; is the entry point Flux syncs. Each child Kustomization is a named slice of the platform with its own interval, &lt;code&gt;dependsOn&lt;/code&gt;, and (where secrets live) SOPS decryption. &lt;code&gt;infrastructure/&lt;/code&gt; is the actual YAML those slices apply.&lt;/p&gt;
&lt;p&gt;Order matters. Helm charts cannot install until the &lt;code&gt;HelmRepository&lt;/code&gt; objects exist. cert-manager cannot come up until those sources do. A shortened version of the cert-manager slice looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;kustomize.toolkit.fluxcd.io/v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Kustomization&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;cert-manager&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;flux-system&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;interval&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;30m&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;dependsOn&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;sources&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./infrastructure/cert-manager&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;prune&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;wait&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;decryption&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;provider&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;sops&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;secretRef&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;sops-age&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;prune: true&lt;/code&gt; is the bit people skip and then regret. If you delete the manifest and Flux still has prune off, the live object sits there forever looking “fine.” &lt;code&gt;wait: true&lt;/code&gt; plus &lt;code&gt;dependsOn&lt;/code&gt; is the other half of that: cert-manager does not race Traefik because the Kustomization will not move on until the slice is healthy.&lt;/p&gt;
&lt;p&gt;The Helm install itself is a &lt;code&gt;HelmRelease&lt;/code&gt;, not a pipeline task:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;helm.toolkit.fluxcd.io/v2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;HelmRelease&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;cert-manager&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;cert-manager&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;interval&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;30m&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;chart&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;chart&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;cert-manager&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;version&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;v1.21.0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;sourceRef&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;HelmRepository&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jetstack&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;flux-system&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Same idea as &lt;a href="https://blog-dev.funkysi1701.com/posts/2025/deploying-hugo-with-helm/"&gt;deploying with Helm&lt;/a&gt;
, except the cluster is the thing that runs &lt;code&gt;helm&lt;/code&gt;, not me.&lt;/p&gt;
&lt;h2 id="one-change-end-to-end"&gt;One change, end to end&lt;a class="anchor ms-1" href="#one-change-end-to-end" aria-label="Permalink: One change, end to end"&gt;&lt;i class="fas fa-link" aria-hidden="true"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A typical platform change is dull, which is the point.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Edit YAML under &lt;code&gt;infrastructure/…&lt;/code&gt; (chart version, values, a &lt;code&gt;ClusterIssuer&lt;/code&gt;, a runner replica count).&lt;/li&gt;
&lt;li&gt;Open a PR, merge to &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Wait for the Kustomization interval, or &lt;code&gt;flux reconcile&lt;/code&gt; if I am impatient.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flux get hr,ks -A&lt;/code&gt; until the slice is &lt;code&gt;Ready&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If it is not Ready, read the condition before assuming git and the cluster agree. That command is the homelab equivalent of a pipeline log.&lt;/p&gt;
&lt;p&gt;I already wrote about &lt;a href="https://blog-dev.funkysi1701.com/posts/2025/kubernetes-and-letsencrypt/"&gt;Let’s Encrypt on this cluster&lt;/a&gt;
when cert-manager was a &lt;code&gt;kubectl apply&lt;/code&gt; of upstream YAML. That install now lives in git as the HelmRelease above plus &lt;code&gt;ClusterIssuer&lt;/code&gt; manifests. The DNS-01 token is not in those files in plaintext; it is a SOPS-encrypted Secret Flux decrypts on the cluster.&lt;/p&gt;
&lt;p&gt;Grafana followed the same path. I first ran it in &lt;a href="https://blog-dev.funkysi1701.com/posts/2025/setting-up-grafana/"&gt;Docker Compose for .NET metrics&lt;/a&gt;
. The homelab copy now sits in the &lt;code&gt;monitoring&lt;/code&gt; namespace, owned by Flux, with dashboards as ConfigMaps in the same repo. Compose taught me the product; GitOps is how it stays installed when I am not watching.&lt;/p&gt;
&lt;p&gt;This blog is in the picture too, just not as a website on the cluster. Production is still Azure Static Web Apps. What Flux &lt;em&gt;does&lt;/em&gt; provision for this repo is a pool of &lt;a href="https://github.com/actions/actions-runner-controller" target="_blank" rel="noopener noreferrer"&gt;Actions Runner Controller&lt;/a&gt;
runners labelled &lt;code&gt;k8s&lt;/code&gt;, so GitHub Actions can build on the homelab instead of burning hosted minutes. The &lt;code&gt;RunnerDeployment&lt;/code&gt; is YAML in that private platform repo. The workflows stay in &lt;em&gt;this&lt;/em&gt; git repo. That is the split in miniature.&lt;/p&gt;
&lt;h2 id="secrets-in-git-not-in-chat"&gt;Secrets in git, not in chat&lt;a class="anchor ms-1" href="#secrets-in-git-not-in-chat" aria-label="Permalink: Secrets in git, not in chat"&gt;&lt;i class="fas fa-link" aria-hidden="true"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Platform secrets are &lt;code&gt;*.enc.yaml&lt;/code&gt; files encrypted with &lt;a href="https://github.com/getsops/sops" target="_blank" rel="noopener noreferrer"&gt;SOPS&lt;/a&gt;
and &lt;a href="https://age-encryption.org/" target="_blank" rel="noopener noreferrer"&gt;age&lt;/a&gt;
. Flux has a &lt;code&gt;sops-age&lt;/code&gt; Secret in &lt;code&gt;flux-system&lt;/code&gt; (created once, not committed) and Kustomizations that need secrets set &lt;code&gt;decryption.provider: sops&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What &lt;em&gt;is&lt;/em&gt; in git: Cloudflare DNS-01, the ARC GitHub token, Grafana admin, registry pull secrets, tunnel token. What is &lt;strong&gt;not&lt;/strong&gt;: the age private key. If you clone that private platform repo you get ciphertext and the public key in &lt;code&gt;.sops.yaml&lt;/code&gt;. That is enough to &lt;em&gt;add&lt;/em&gt; a secret if you have the private key locally; it is not enough to read the live ones.&lt;/p&gt;
&lt;p&gt;This is not Azure Key Vault. A homelab cluster does not need a cloud HSM to stop me committing a PAT. It needs encryption at rest in git and a bootstrap secret that never hits GitHub. The repo documents how SOPS and age are wired up.&lt;/p&gt;
&lt;h2 id="what-flux-does-not-own"&gt;What Flux does not own&lt;a class="anchor ms-1" href="#what-flux-does-not-own" aria-label="Permalink: What Flux does not own"&gt;&lt;i class="fas fa-link" aria-hidden="true"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I keep an ownership map in that private platform repo because GitOps fails the moment two systems apply the same object. Deciding who is allowed to apply has been more useful than any controller version pin. The short version:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;This repo (Flux)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Namespaces, MetalLB, cert-manager, Traefik, ARC, Zot, cloudflared, monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;App git + CI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Helm releases in &lt;code&gt;develop&lt;/code&gt; / &lt;code&gt;main&lt;/code&gt; / &lt;code&gt;test&lt;/code&gt; (blog chart, TrekRanks, SQL, …)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;k3s / the node&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CoreDNS, local-path, metrics-server, disable bundled Traefik&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Several apps still deploy from Azure DevOps. That is fine. It is &lt;strong&gt;push CI&lt;/strong&gt;, and Flux will not “fix” a Deployment it does not declare. If an old pipeline and a HelmRelease both think they own Grafana, they will fight. I moved monitoring into Flux on purpose so there is one owner.&lt;/p&gt;
&lt;p&gt;k3s itself is not GitOps in this repo either. Node join, versions, and “do not run the bundled Traefik” are documented and applied on the hosts. Flux cannot bootstrap the thing that runs Flux.&lt;/p&gt;
&lt;p&gt;In-cluster &lt;a href="https://docs.renovatebot.com/" target="_blank" rel="noopener noreferrer"&gt;Renovate&lt;/a&gt;
is a small extra: it opens image-bump PRs against that private platform repo. Upgrades become git history too, not SSH-and-hope. One note on Renovate, the Renovate package itself is very noisy, with multiple update PRs raised per day. I have chosen to automerge these, but you may want to consider skipping this package or updating on a schedule.&lt;/p&gt;
&lt;p&gt;I would not start a new cluster by SSHing in and applying twenty manifests. Ingress, certificates, runners, and monitoring are shared, so they belong in git from the start. I would still start a new &lt;em&gt;app&lt;/em&gt; with a pipeline and a Helm chart in &lt;em&gt;that&lt;/em&gt; repo — Flux &lt;code&gt;HelmRelease&lt;/code&gt; is how those charts get onto the cluster, not a replacement for packaging.&lt;/p&gt;
&lt;p&gt;I have not moved every Helm chart into Flux, and I am not pretending that is a moral failing. GitOps earned its place here by making the platform dull. Dull is the goal.&lt;/p&gt;
&lt;p&gt;If you run Kubernetes at home — or you are weighing GitOps against push CI — how do you split &lt;strong&gt;platform&lt;/strong&gt; from &lt;strong&gt;apps&lt;/strong&gt;? Full pull, pipelines only, or a hybrid like mine? I would be interested to hear what works for you in the comments.&lt;/p&gt;
&lt;p&gt;If this is your first step on the same path, these posts are where I started: &lt;a href="https://blog-dev.funkysi1701.com/posts/2025/learning-kubernetes/"&gt;learning Kubernetes&lt;/a&gt;
, &lt;a href="https://blog-dev.funkysi1701.com/posts/2025/deploying-hugo-with-helm/"&gt;deploying with Helm&lt;/a&gt;
, and &lt;a href="https://blog-dev.funkysi1701.com/posts/2025/kubernetes-and-letsencrypt/"&gt;Let&amp;rsquo;s Encrypt on the cluster&lt;/a&gt;
. For a monthly email when I publish something new, &lt;a href="https://blog-dev.funkysi1701.com/newsletter"&gt;subscribe to the newsletter&lt;/a&gt;
.&lt;/p&gt;</description></item><item><title>Pairing with AI in VS Code and Cursor</title><link>https://blog-dev.funkysi1701.com/posts/2026/ai-pair-programming-dotnet/</link><author>funkysi1701@gmail.com (funkysi1701)</author><pubDate>Fri, 07 Aug 2026 12:00:00 +0000</pubDate><guid>https://blog-dev.funkysi1701.com/posts/2026/ai-pair-programming-dotnet/</guid><category term="AI">AI</category><category term="DotNet">DotNet</category><category term="Cursor">Cursor</category><category term="VSCode">VSCode</category><category term="GitHubCopilot">GitHubCopilot</category><category term="Productivity">Productivity</category><category term="Programming">Programming</category><media:content medium="image" type="image/png" url="https://blog-dev.funkysi1701.com/images/2026/ai-pair-programming-dotnet.png"/><description>&lt;p&gt;On a lot of days my pair programmer is not a person. It is Copilot finishing a method, or Cursor proposing a multi-file edit I still have to own.&lt;/p&gt;
&lt;p&gt;Back in April I covered &lt;a href="https://blog-dev.funkysi1701.com/posts/2026/how-i-use-ai-on-side-projects/"&gt;how I use ChatGPT, Cursor, and Copilot on side projects&lt;/a&gt;
. That was mostly about which tool I reach for. Here I want to talk about what that looks like when the code is .NET—C#, Blazor, Azure Functions—and I am working in VS Code or Cursor.&lt;/p&gt;
&lt;p&gt;I am not claiming a universal workflow. These are habits that have helped me in 2026.&lt;/p&gt;
&lt;h2 id="a-thin-tools-map-so-we-can-move-on"&gt;A thin tools map (so we can move on)&lt;a class="anchor ms-1" href="#a-thin-tools-map-so-we-can-move-on" aria-label="Permalink: A thin tools map (so we can move on)"&gt;&lt;i class="fas fa-link" aria-hidden="true"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I still use the same three buckets. The only change is which bucket I reach for when the stack is .NET-heavy.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;What I reach for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framing a design question or learning path&lt;/td&gt;
&lt;td&gt;ChatGPT (browser)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-file change across a solution&lt;/td&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finish this method / test / YAML stanza&lt;/td&gt;
&lt;td&gt;GitHub Copilot in the editor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;Build AI &lt;em&gt;into&lt;/em&gt; the product&amp;rdquo;&lt;/td&gt;
&lt;td&gt;Separate concern — see &lt;a href="https://blog-dev.funkysi1701.com/posts/using-ai/"&gt;Using AI with .NET and Semantic Kernel&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you already read the April post, you can skim this table and skip ahead. The interesting part is not the brand names; it is what happens after a suggestion appears.&lt;/p&gt;
&lt;p&gt;For vocabulary (RAG, LLM, Copilot, and friends), &lt;a href="https://blog-dev.funkysi1701.com/posts/common-ai-copilot-terms/"&gt;Common AI and Copilot Terms&lt;/a&gt;
is still a useful glossary.&lt;/p&gt;
&lt;h2 id="scenario-unfamiliar-code-that-is-still-mine"&gt;Scenario: unfamiliar code that is still “mine”&lt;a class="anchor ms-1" href="#scenario-unfamiliar-code-that-is-still-mine" aria-label="Permalink: Scenario: unfamiliar code that is still “mine”"&gt;&lt;i class="fas fa-link" aria-hidden="true"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.episodeatlas.com/" target="_blank" rel="noopener noreferrer"&gt;Episode Atlas&lt;/a&gt;
grew the way hobby apps do: Blazor on the front, Azure Functions and Cosmos DB behind, enough moving parts that I forget my own choices. When I come back after a few weeks, I do not want a greenfield tutorial from a chat window. I want something that has &lt;em&gt;seen&lt;/em&gt; the repo.&lt;/p&gt;
&lt;p&gt;In Cursor I ask questions like &amp;ldquo;where does per-user progress get written?&amp;rdquo; or &amp;ldquo;what breaks if this Cosmos query assumes a different partition key?&amp;rdquo; The useful answers cite &lt;em&gt;my&lt;/em&gt; folders and types. The useless ones invent a service layer I never wrote.&lt;/p&gt;
&lt;p&gt;What I do next matters more than the first reply:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open the files it named and confirm they are the real path.&lt;/li&gt;
&lt;li&gt;Diff any proposed edit against how neighbouring code already looks.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;dotnet build&lt;/code&gt; (and the bit of the app I touched) before I trust it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The productivity win is not that the model &amp;ldquo;knows Episode Atlas.&amp;rdquo; It is that I spend less time remapping a codebase I already wrote.&lt;/p&gt;
&lt;h2 id="scenario-boring-glue-that-still-has-to-be-correct"&gt;Scenario: boring glue that still has to be correct&lt;a class="anchor ms-1" href="#scenario-boring-glue-that-still-has-to-be-correct" aria-label="Permalink: Scenario: boring glue that still has to be correct"&gt;&lt;i class="fas fa-link" aria-hidden="true"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This blog is Hugo plus Azure Static Web Apps, with GitHub Actions and a pile of Node/Python checks. A lot of the work is glue: a layout partial, a workflow step, a Playwright selector after a theme tweak. Copilot is often enough when I already know the shape—finish the next YAML key, sketch a test that matches the one above.&lt;/p&gt;
&lt;p&gt;Cursor earns its keep when the change spans files: &amp;ldquo;update this partial and the smoke test that asserts the heading,&amp;rdquo; or &amp;ldquo;align the SWA config check with what CI runs.&amp;rdquo; Those are exactly the edits where an incomplete suggestion is worse than no suggestion—half-applied config fails in ways that waste an evening.&lt;/p&gt;
&lt;p&gt;On Blazor and Functions work the same pattern shows up as DTOs, mapping, and tests. Copilot is fast at the third similar test. Cursor is better when the DTO, the endpoint, and the client all need to move together.&lt;/p&gt;
&lt;h2 id="scenario-debugging-when-the-model-is-too-confident"&gt;Scenario: debugging when the model is too confident&lt;a class="anchor ms-1" href="#scenario-debugging-when-the-model-is-too-confident" aria-label="Permalink: Scenario: debugging when the model is too confident"&gt;&lt;i class="fas fa-link" aria-hidden="true"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I have let an assistant send me on a wild goose chase: wrong package version, an API that never existed, a &amp;ldquo;fix&amp;rdquo; that compiled and still left the bug. A human pair who is sure and wrong is more dangerous than one who says &amp;ldquo;I am guessing.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;When something fails, I try to feed the model the error text and a failing test, not a vague description of what I think is wrong. If the first patch does not change the symptom, I stop re-prompting the same story. I read the stack, check docs, or bisect—same as I would without AI.&lt;/p&gt;
&lt;p&gt;Side projects are a forgiving place to learn that discipline. Production systems are not. Secrets, customer data, and employer code stay out of tools unless policy clearly allows them; I wrote about that boundary in the &lt;a href="https://blog-dev.funkysi1701.com/posts/2026/how-i-use-ai-on-side-projects/"&gt;side projects post&lt;/a&gt;
and I still follow it.&lt;/p&gt;
&lt;h2 id="guardrails-that-actually-saved-me-time"&gt;Guardrails that actually saved me time&lt;a class="anchor ms-1" href="#guardrails-that-actually-saved-me-time" aria-label="Permalink: Guardrails that actually saved me time"&gt;&lt;i class="fas fa-link" aria-hidden="true"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;These are the practices that keep AI pairing from becoming AI babysitting:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Read every diff you did not type.&lt;/strong&gt; Especially NuGet names, HTTP routes, auth, and anything touching data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer project conventions over &amp;ldquo;cleaner&amp;rdquo; internet defaults.&lt;/strong&gt; If my solution uses one DI style, I reject the rewrite that introduces another for a one-line fix.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify against official docs when the call looks unfamiliar.&lt;/strong&gt; Models still hallucinate plausible APIs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep the human review bar.&lt;/strong&gt; I treat agent output like a junior&amp;rsquo;s pull request: helpful when it is right, never something I merge on trust alone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Match depth of context to the task.&lt;/strong&gt; Do not paste half a solution into a browser chat for a one-line rename; do not ask inline Copilot to redesign a Functions + Cosmos flow.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="what-productivity-has-meant-for-me"&gt;What “productivity” has meant for me&lt;a class="anchor ms-1" href="#what-productivity-has-meant-for-me" aria-label="Permalink: What “productivity” has meant for me"&gt;&lt;i class="fas fa-link" aria-hidden="true"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I am faster at first drafts and at navigating code I have not opened in a month. I am not faster at &lt;em&gt;skipping&lt;/em&gt; judgment. If anything, the cost of a bad accept is higher because the broken change can look polished.&lt;/p&gt;
&lt;p&gt;That lines up with the longer argument in &lt;a href="https://blog-dev.funkysi1701.com/posts/2026/ai-wont-replace-developers/"&gt;AI Won&amp;rsquo;t Replace Developers, But It Will Redefine Us&lt;/a&gt;
: the work shifts toward directing, reviewing, and verifying. I see that every time I open Cursor or Copilot on Episode Atlas, this blog, or whichever .NET side project I am tinkering with that week.&lt;/p&gt;
&lt;p&gt;If you are doing similar work in C# or Blazor, leave a comment and tell me which tool you reach for first—and what you still refuse to hand to an assistant. I am always curious how other people draw that line.&lt;/p&gt;</description></item></channel></rss>