<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Home on Nugroho Satrijandi</title><link>https://blog.satrijandi.com/</link><description>Recent content in Home on Nugroho Satrijandi</description><generator>Hugo</generator><language>en</language><lastBuildDate>Wed, 15 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.satrijandi.com/index.xml" rel="self" type="application/rss+xml"/><item><title>The Cost of Not Running Experiments</title><link>https://blog.satrijandi.com/posts/cost-of-not-running-experiments/</link><pubDate>Wed, 15 Jul 2026 00:00:00 +0000</pubDate><guid>https://blog.satrijandi.com/posts/cost-of-not-running-experiments/</guid><description>&lt;p&gt;A bank&amp;rsquo;s costliest decisions rarely look costly on the day they&amp;rsquo;re made. They
look obvious.&lt;/p&gt;
&lt;p&gt;Picture the chart everyone nods at. Customers who get a payment reminder three
days before the due date miss fewer payments. The gap is huge, the line is
clean, and the room agrees in under a minute: send the reminder to everyone.&lt;/p&gt;
&lt;p&gt;The chart is real. The correlation is real. The conclusion is almost certainly
wrong. Customers who opt into reminders, or who happen to be reachable three days
out, are already more organized, more liquid, and more likely to pay on time no
matter what you do. The reminder didn&amp;rsquo;t cause the good behavior; they share a
common cause. Roll it out to everyone and you may spend real money moving
nothing, while congratulating yourself on a number that was always going to be
there.&lt;/p&gt;</description></item><item><title>A Recovery-First Kubernetes Homelab</title><link>https://blog.satrijandi.com/projects/homelab/</link><pubDate>Sat, 11 Jul 2026 00:00:00 +0000</pubDate><guid>https://blog.satrijandi.com/projects/homelab/</guid><description>&lt;p&gt;This site is served by the project it describes: a single-machine Kubernetes
homelab built the way I&amp;rsquo;d build production. The goal was never to run the most
services — it was to run a &lt;em&gt;small&lt;/em&gt; number of services on a foundation that is
&lt;strong&gt;declarative, recoverable, and secure by default&lt;/strong&gt;. Everything below is the
design; nothing here is a secret, a credential, an address, or anything an
attacker could use.&lt;/p&gt;
&lt;h2 id="principles"&gt;Principles&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Declarative over imperative.&lt;/strong&gt; Every change is a file describing desired
state, applied idempotently. Running it twice equals running it once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Git is the source of truth; the runtime is a cache.&lt;/strong&gt; If a change isn&amp;rsquo;t in
Git, it didn&amp;rsquo;t happen — and reconciliation will undo it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recovery-first.&lt;/strong&gt; Anything required to rebuild the cluster lives &lt;em&gt;outside&lt;/em&gt;
the cluster and outside the machine. I designed the restore path before
deploying workloads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Immutable artifacts.&lt;/strong&gt; Container images are pinned by digest; chart and
package versions are pinned. No floating &lt;code&gt;latest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Least privilege &amp;amp; secure by default.&lt;/strong&gt; Workloads run non-root under a
restricted Pod Security baseline; the network defaults to deny; only one
service is exposed to the public internet (this blog).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-stack"&gt;The stack&lt;/h2&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Layer&lt;/th&gt;
 &lt;th&gt;Choice&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Substrate&lt;/td&gt;
 &lt;td&gt;ARM64 Linux on Apple Silicon, single-node &lt;strong&gt;k3s&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Networking&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Cilium&lt;/strong&gt; (kube-proxy replacement) + Kubernetes &lt;strong&gt;Gateway API&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Public ingress&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Cloudflare Tunnel&lt;/strong&gt; (no open inbound ports)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Private access&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Tailscale&lt;/strong&gt; (everything internal is tailnet-only)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;GitOps&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Argo CD&lt;/strong&gt; (app-of-apps, environment overlays)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Internal Git&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Gitea&lt;/strong&gt; on &lt;strong&gt;CloudNativePG&lt;/strong&gt;, push-mirrored to an off-host mirror&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Secrets&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;SOPS + age&lt;/strong&gt; for bootstrap, &lt;strong&gt;External Secrets&lt;/strong&gt; beyond&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Identity&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Kanidm&lt;/strong&gt; as the OIDC provider&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Policy&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Kyverno&lt;/strong&gt; + a restricted Pod Security floor, enforced at admission&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Observability&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Prometheus, Alertmanager, Grafana, Hubble&lt;/strong&gt; + an external dead-man&amp;rsquo;s switch&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Storage &amp;amp; backup&lt;/td&gt;
 &lt;td&gt;Local persistent volumes, &lt;strong&gt;Velero&lt;/strong&gt;, app-native encrypted off-host backups&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Supply chain&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Woodpecker CI&lt;/strong&gt; + rootless &lt;strong&gt;BuildKit&lt;/strong&gt; + &lt;strong&gt;Zot&lt;/strong&gt;, signed with &lt;strong&gt;cosign&lt;/strong&gt;, scanned with &lt;strong&gt;Trivy&lt;/strong&gt;, SBOM-attested, verified at admission&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;DNS as code&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;external-dns&lt;/strong&gt; reconciling records from cluster objects&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="how-it-fits-together"&gt;How it fits together&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Substrate.&lt;/strong&gt; A single ARM64 Linux machine runs k3s with flannel and kube-proxy
disabled, secrets encryption on, and an audit policy that never logs secret
bodies. Keeping the substrate portable (it can be re-created on another ARM64
hypervisor) is part of the recovery story.&lt;/p&gt;</description></item><item><title>Git Is the Source of Truth; the Runtime Is a Cache</title><link>https://blog.satrijandi.com/posts/git-as-source-of-truth/</link><pubDate>Wed, 24 Jun 2026 00:00:00 +0000</pubDate><guid>https://blog.satrijandi.com/posts/git-as-source-of-truth/</guid><description>&lt;p&gt;There&amp;rsquo;s a mental shift that, once it clicks, changes how you operate everything:
&lt;strong&gt;the running system is not the source of truth.&lt;/strong&gt; Git is. The cluster, the
warehouse, the environment — they&amp;rsquo;re caches. A projection of a versioned,
reviewed history that a reconciliation loop keeps in sync.&lt;/p&gt;
&lt;h2 id="why-it-matters"&gt;Why it matters&lt;/h2&gt;
&lt;p&gt;When the runtime is authoritative, every fix is a one-off. Someone SSHes in,
edits a file, restarts a service — and now reality has drifted from any record of
intent. The next person can&amp;rsquo;t tell what&amp;rsquo;s deliberate and what&amp;rsquo;s an accident.&lt;/p&gt;</description></item><item><title>Recovery-First: Design the Restore Before the Deploy</title><link>https://blog.satrijandi.com/posts/recovery-first-infrastructure/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://blog.satrijandi.com/posts/recovery-first-infrastructure/</guid><description>&lt;p&gt;Most infrastructure is designed forward: stand up the cluster, deploy the apps,
and &lt;em&gt;then&lt;/em&gt; — maybe — bolt on backups. I&amp;rsquo;ve come to think that&amp;rsquo;s backwards. The
most honest test of an architecture is whether you can rebuild it from nothing
but a Git history and an off-host backup. If you design the restore path first,
everything else gets healthier almost by accident.&lt;/p&gt;
&lt;h2 id="what-recovery-first-forces-you-to-do"&gt;What &amp;ldquo;recovery-first&amp;rdquo; forces you to do&lt;/h2&gt;
&lt;p&gt;When the restore is the requirement, a few rules stop being optional:&lt;/p&gt;</description></item><item><title>About</title><link>https://blog.satrijandi.com/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.satrijandi.com/about/</guid><description>&lt;p&gt;I&amp;rsquo;m a Head of Data specializing in MLOps, driving end-to-end ML pipeline
development and implementation. I bridge data science innovation with production
systems — collaborating across teams to deliver streamlined solutions that align
with business goals while staying current with emerging AI technologies.&lt;/p&gt;
&lt;p&gt;Across nearly a decade I&amp;rsquo;ve built and led data science teams in fintech and
ride-hailing, shipping the models that underwrite credit, catch fraud, retain
customers, and drive growth — together with the feature platforms, serving, and
monitoring that keep them dependable in production.&lt;/p&gt;</description></item></channel></rss>