<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Platform Engineering Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/platform-engineering/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/platform-engineering/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Fri, 10 Jul 2026 15:38:14 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.4</generator>

<image>
	<url>https://linuxcent.com/wp-content/uploads/2026/04/favicon-512x512-1-150x150.png</url>
	<title>Platform Engineering Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/platform-engineering/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>KubeVirt: Running VMs on Kubernetes — and Why That Still Matters</title>
		<link>https://linuxcent.com/kubevirt-vms-on-kubernetes/</link>
					<comments>https://linuxcent.com/kubevirt-vms-on-kubernetes/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Wed, 29 Jul 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[Kubernetes Ecosystem]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[KubeVirt]]></category>
		<category><![CDATA[Platform Engineering]]></category>
		<category><![CDATA[Virtualization]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=2263</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span>See how KubeVirt runs real VMs alongside pods on the same Kubernetes cluster — and why that migration path still matters in a container-first world.</p>
<p>The post <a href="https://linuxcent.com/kubevirt-vms-on-kubernetes/">KubeVirt: Running VMs on Kubernetes — and Why That Still Matters</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>Kubernetes Ecosystem: From User to Contributor, Episode 10 (Series Finale)</em><br />
<a href="/karpenter-vs-cluster-autoscaler/">← EP09: Karpenter vs Cluster Autoscaler</a> · <strong>EP10: KubeVirt</strong> · <a href="/kubernetes-ecosystem-series/">All Kubernetes Ecosystem Episodes →</a></p>
<p><strong>11 min read</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>KubeVirt runs full virtual machines as first-class Kubernetes objects — a <code class="" data-line="">VirtualMachineInstance</code> wraps a real QEMU/KVM VM inside a special pod, scheduled and managed the same way any other workload is</li>
<li>The real reason to run KubeVirt isn&#8217;t nostalgia for VMs — it&#8217;s giving legacy, non-containerizable, or Windows workloads the same control plane, networking, and operational tooling as everything else, instead of maintaining a separate VM infrastructure stack alongside Kubernetes</li>
<li><strong>Multus</strong> (multiple network interfaces per pod) and <strong>CDI</strong> (Containerized Data Importer, for getting disk images into the cluster) are the supporting pieces that make VM networking and storage work inside a Kubernetes-native model</li>
<li><strong>Live migration</strong> — moving a running VM to another node with no downtime — is KubeVirt&#8217;s answer to node maintenance, and it&#8217;s meaningfully different from how Kubernetes handles pod disruption</li>
<li>Nodes running VM workloads need actual hardware virtualization support (KVM) — the same nested-virtualization requirement that constrained Minikube&#8217;s VM drivers back in EP02, now showing up at the production node level</li>
<li>Contribution opportunity: GPU and device-plugin support for VM workloads specifically lags behind the maturity of the container device-plugin ecosystem — a real, scoped gap</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">CONTAINER POD                                VM VIA KUBEVIRT
──────────────                                ───────────────
Pod                                          VirtualMachineInstance
  └── container                                └── virt-launcher Pod
        runs directly on the                        └── QEMU/KVM process
        node&#039;s kernel via                                running a REAL VM,
        containerd/runc                                    with its own kernel
                                                             (Linux, Windows,
Scheduling, networking,                                     anything QEMU
resource limits — standard                                  supports)
Kubernetes primitives
                                              Scheduling, networking, resource
                                              limits — SAME standard Kubernetes
                                              primitives, wrapping a VM instead
</code></pre>
<p>What is KubeVirt? It&#8217;s the project that makes a virtual machine look like just another workload to Kubernetes — the VM runs inside a <code class="" data-line="">virt-launcher</code> pod, so everything Kubernetes already does for scheduling, networking policy, and resource accounting applies to the VM too, without Kubernetes itself needing to understand virtualization at all.</p>
<hr />
<h2 id="the-core-abstraction-virtualmachine-virtualmachineinstance-and-virt-launcher">The Core Abstraction: VirtualMachine, VirtualMachineInstance, and virt-launcher</h2>
<pre><code class="" data-line="">apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: legacy-app-vm
spec:
  running: true
  template:
    spec:
      domain:
        cpu: {cores: 2}
        memory: {guest: 4Gi}
        devices:
          disks:
          - name: rootdisk
            disk: {bus: virtio}
      volumes:
      - name: rootdisk
        dataVolume: {name: legacy-app-disk}
</code></pre>
<pre><code class="" data-line="">$ kubectl get vmi
NAME            AGE   PHASE     IP            NODENAME
legacy-app-vm   2m    Running   10.244.1.15   worker-03

$ kubectl get pods
NAME                                READY   STATUS
virt-launcher-legacy-app-vm-x7k2l   2/2     Running
#     ^^^^^^^^^^^^ — this IS the pod hosting the real QEMU process
</code></pre>
<p><code class="" data-line="">VirtualMachine</code> is the persistent declaration (should this VM exist, running or stopped); <code class="" data-line="">VirtualMachineInstance</code> is the actual running instance; <code class="" data-line="">virt-launcher</code> is the pod — an ordinary Kubernetes pod from the scheduler&#8217;s point of view — that contains the QEMU process actually executing the VM. This layering mirrors Deployment/ReplicaSet/Pod deliberately, the same pattern CAPI (EP05) uses for Cluster/Machine.</p>
<hr />
<h2 id="why-run-vms-on-kubernetes-at-all">Why Run VMs on Kubernetes At All</h2>
<p>The honest case for KubeVirt isn&#8217;t &#8220;VMs are better than containers&#8221; — it&#8217;s consolidation. Most real infrastructure estates have some workloads that genuinely can&#8217;t be containerized cleanly: legacy applications tightly coupled to a specific OS version, Windows-only software, or workloads with licensing/compliance requirements demanding a VM-level boundary. Without KubeVirt, those workloads need an entirely separate VM infrastructure stack — its own hypervisor management, its own networking, its own monitoring — running alongside Kubernetes rather than on it. KubeVirt collapses that into one control plane, one set of operational tooling, one team&#8217;s expertise, for both categories of workload.</p>
<hr />
<h2 id="networking-and-storage-for-vms-multus-datavolumes-cdi">Networking and Storage for VMs: Multus, DataVolumes, CDI</h2>
<pre><code class="" data-line=""># Multus lets a VM&#039;s pod attach to more than one network —
# often needed for VMs expecting a dedicated network interface,
# unlike a typical single-network container workload
$ kubectl get network-attachment-definitions

# CDI (Containerized Data Importer) gets an existing VM disk image
# into the cluster as a DataVolume
$ kubectl apply -f - &lt;&lt;EOF
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
  name: legacy-app-disk
spec:
  source:
    http:
      url: &quot;https://images.example.com/legacy-app.qcow2&quot;
  pvc:
    accessModes: [&quot;ReadWriteOnce&quot;]
    resources:
      requests: {storage: 20Gi}
EOF
</code></pre>
<p>Regular container images assume a filesystem layer model that doesn&#8217;t map cleanly onto &#8220;import this existing 20GB qcow2 disk image a legacy app already runs from.&#8221; CDI exists specifically to bridge that gap — pulling real VM disk images from HTTP, S3, or a registry into a <code class="" data-line="">DataVolume</code> a <code class="" data-line="">VirtualMachine</code> can boot from.</p>
<hr />
<h2 id="live-migration-kubevirts-answer-to-node-maintenance">Live Migration: KubeVirt&#8217;s Answer to Node Maintenance</h2>
<pre><code class="" data-line="">$ virtctl migrate legacy-app-vm
VM legacy-app-vm was scheduled to migrate

$ kubectl get vmim
NAME                    PHASE       VMI
legacy-app-vm-migrate   Running     legacy-app-vm

# The VM keeps running, on a different node, with no restart —
# genuinely different from how Kubernetes handles pod disruption
</code></pre>
<p>A pod being evicted for node maintenance simply gets rescheduled — it restarts fresh somewhere else. A live-migrated VM keeps running throughout, its in-memory state transferred to the new node without a restart, the same live-migration capability traditional hypervisor platforms have offered for years. This is one behavior category where &#8220;VM on Kubernetes&#8221; is genuinely doing something a plain pod cannot.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>Nodes need real KVM support — nested virtualization if the node itself is already a VM (common on cloud instances).</strong> This is the same underlying requirement that constrained Minikube&#8217;s VM drivers back in EP02, now at the production node level: if your cloud instance type doesn&#8217;t expose nested virtualization, KubeVirt workloads simply won&#8217;t schedule there.</p>
<p><strong>VM disk I/O performance depends heavily on the underlying storage class and driver (<code class="" data-line="">virtio</code> vs alternatives) — don&#8217;t assume container-workload storage benchmarks transfer to VM workloads on the same cluster.</strong></p>
<p><strong>Live migration isn&#8217;t guaranteed to succeed for every VM under every condition</strong> — a VM under heavy memory-write load, or a migration across a congested network path, can fail or take far longer than expected. Test migration behavior under realistic load before depending on it for a real maintenance window.</p>
<p><strong>Windows VMs specifically need the right virtio drivers pre-installed in the image, or networking/storage won&#8217;t work post-boot</strong> — this is a common first-VM stumbling block distinct from anything Linux-VM users hit.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line="">kubectl get vm                       # VirtualMachine declarations
kubectl get vmi                      # running VirtualMachineInstances
kubectl get vmim                     # migration status
virtctl start / stop / restart &lt;vm&gt;  # lifecycle control
virtctl console &lt;vm&gt;                 # serial console access
virtctl migrate &lt;vm&gt;                 # trigger a live migration
kubectl get datavolumes              # CDI-imported disk images
</code></pre>
<hr />
<h2 id="contribution-opportunity-device-plugin-parity-for-vm-workloads">Contribution Opportunity: Device-Plugin Parity for VM Workloads</h2>
<p><strong>The limitation:</strong> Kubernetes&#8217; device-plugin ecosystem for GPUs and other specialized hardware is mature and well-supported for container workloads. Extending that same hardware access cleanly into KubeVirt-managed VMs — GPU passthrough with the same operational ergonomics containers already have — is real, current work with meaningfully more rough edges than the container-side equivalent.</p>
<p><strong>Why it&#8217;s hard to fix:</strong> Passing a physical device through to a VM (as opposed to a container, which can often share the host&#8217;s device more directly) involves IOMMU configuration, PCI passthrough mechanics, and driver considerations inside the guest OS that simply don&#8217;t exist for container device access — it&#8217;s a genuinely harder problem, not a lagging implementation of an equally-easy one.</p>
<p><strong>What a contribution-shaped fix looks like:</strong> KubeVirt&#8217;s own documentation and issue tracker identify specific, scoped GPU/device-passthrough gaps — particular hardware combinations without a documented working configuration, or specific device-plugin integrations that work for containers but haven&#8217;t been extended to the VM path. Reproducing one of these documented gaps on real hardware you have access to, and contributing either the fix or a verified, tested configuration guide back to the project, is exactly the kind of practitioner-shaped contribution this series has pointed toward in every episode — you need access to the hardware and patience to reproduce, not novel research.</p>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>KubeVirt makes a VM look like an ordinary Kubernetes workload by wrapping it in a <code class="" data-line="">virt-launcher</code> pod — the same scheduling, networking, and resource primitives apply to both</li>
<li>The real case for running VMs on Kubernetes is infrastructure consolidation — one control plane for both container and VM workloads, not VM enthusiasm for its own sake</li>
<li>Multus and CDI solve the networking and storage problems that don&#8217;t map cleanly from the container model onto VM disk images and multi-interface networking</li>
<li>Live migration is a genuine capability gap between VMs and plain pods — Kubernetes&#8217; own disruption model for pods has no equivalent</li>
<li>The clearest current contribution opportunity is closing specific, documented device-passthrough gaps for VM workloads — hardware-access work, not abstract feature design</li>
</ul>
<hr />
<h2 id="series-wrap-from-user-to-contributor">Series Wrap: From User to Contributor</h2>
<p>Ten episodes, ten tools, and one repeated pattern: every project in this series exists because something else fell short — k3s and MicroK8s because upstream Kubernetes was too heavy for the edge, Karpenter because Cluster Autoscaler&#8217;s node-group abstraction had a ceiling, Crossplane because Terraform&#8217;s model didn&#8217;t fit a fully Kubernetes-native platform team. Each one of those gaps was found by someone who was using the previous tool closely enough to feel exactly where it stopped working.</p>
<p>That&#8217;s the actual path from user to contributor this series has been pointing at in every Contribution Opportunity section: not a certification, not a novel research breakthrough — close, careful attention to a tool you already run, precise enough to name what&#8217;s actually missing and specific enough to describe what fixing it would look like. Every gap named across these ten episodes is real, documented, and waiting for exactly that kind of attention.</p>
<p>Get future series and episodes in your inbox → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubevirt-vms-on-kubernetes%2F&amp;linkname=KubeVirt%3A%20Running%20VMs%20on%20Kubernetes%20%E2%80%94%20and%20Why%20That%20Still%20Matters" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubevirt-vms-on-kubernetes%2F&amp;linkname=KubeVirt%3A%20Running%20VMs%20on%20Kubernetes%20%E2%80%94%20and%20Why%20That%20Still%20Matters" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubevirt-vms-on-kubernetes%2F&amp;linkname=KubeVirt%3A%20Running%20VMs%20on%20Kubernetes%20%E2%80%94%20and%20Why%20That%20Still%20Matters" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubevirt-vms-on-kubernetes%2F&amp;linkname=KubeVirt%3A%20Running%20VMs%20on%20Kubernetes%20%E2%80%94%20and%20Why%20That%20Still%20Matters" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubevirt-vms-on-kubernetes%2F&amp;linkname=KubeVirt%3A%20Running%20VMs%20on%20Kubernetes%20%E2%80%94%20and%20Why%20That%20Still%20Matters" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubevirt-vms-on-kubernetes%2F&amp;linkname=KubeVirt%3A%20Running%20VMs%20on%20Kubernetes%20%E2%80%94%20and%20Why%20That%20Still%20Matters" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubevirt-vms-on-kubernetes%2F&amp;linkname=KubeVirt%3A%20Running%20VMs%20on%20Kubernetes%20%E2%80%94%20and%20Why%20That%20Still%20Matters" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fkubevirt-vms-on-kubernetes%2F&#038;title=KubeVirt%3A%20Running%20VMs%20on%20Kubernetes%20%E2%80%94%20and%20Why%20That%20Still%20Matters" data-a2a-url="https://linuxcent.com/kubevirt-vms-on-kubernetes/" data-a2a-title="KubeVirt: Running VMs on Kubernetes — and Why That Still Matters"></a></p><p>The post <a href="https://linuxcent.com/kubevirt-vms-on-kubernetes/">KubeVirt: Running VMs on Kubernetes — and Why That Still Matters</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/kubevirt-vms-on-kubernetes/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2263</post-id>	</item>
		<item>
		<title>Crossplane: Kubernetes as the Universal Control Plane</title>
		<link>https://linuxcent.com/crossplane-universal-control-plane/</link>
					<comments>https://linuxcent.com/crossplane-universal-control-plane/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Tue, 21 Jul 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[Kubernetes Ecosystem]]></category>
		<category><![CDATA[Crossplane]]></category>
		<category><![CDATA[Infrastructure as Code]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Platform Engineering]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=2252</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 5</span> <span class="rt-label rt-postfix">minutes</span></span>See how Crossplane turns Kubernetes into a control plane for any cloud resource — and exactly what that composition model costs you operationally.</p>
<p>The post <a href="https://linuxcent.com/crossplane-universal-control-plane/">Crossplane: Kubernetes as the Universal Control Plane</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 5</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>Kubernetes Ecosystem: From User to Contributor, Episode 6</em><br />
<a href="/cluster-api-declarative-lifecycle/">← EP05: Cluster API</a> · <strong>EP06: Crossplane</strong> · <a href="/crossplane-vs-terraform/">EP07: Crossplane vs Terraform →</a></p>
<p><strong>12 min read</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Crossplane extends the exact reconciliation pattern EP05 covered for cluster infrastructure to <em>any</em> cloud resource — an S3 bucket, an RDS instance, a DNS record all become Kubernetes CRDs, continuously reconciled</li>
<li><strong>Managed Resources</strong> represent one real cloud resource each; <strong>Compositions</strong> bundle several Managed Resources behind a single, simpler custom API a platform team defines and app teams consume</li>
<li><strong>Composition Functions</strong> are Crossplane&#8217;s newer, more flexible replacement for its older YAML-based patch-and-transform templating — real code (Go, Python, or others) instead of declarative patches</li>
<li>Crossplane continuously reconciles like any Kubernetes controller — a manual change to a cloud resource outside Crossplane gets reverted on the next reconcile loop, which is a real surprise for teams used to Terraform&#8217;s plan/apply model</li>
<li>Provider CRD counts can bloat a cluster&#8217;s etcd significantly — this drove the ecosystem&#8217;s move toward smaller, split &#8220;provider families&#8221; instead of one monolithic provider per cloud</li>
<li>Contribution opportunity: several providers still haven&#8217;t migrated to the family-split pattern — a real, currently-tracked, achievable upstream contribution</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">App team writes:                    Platform team defined this Composition
                                     once, behind the scenes:
apiVersion: platform.example.com/v1
kind: Database                       XRD &quot;Database&quot; ─── composes ───┐
metadata:                                                             │
  name: my-app-db                                                     ▼
spec:                                                        ┌────────────────┐
  size: small                                                │ RDSInstance    │
                                                                │ SecurityGroup  │
   │                                                            │ ParameterGroup │
   │ app team never sees                                        └────────────────┘
   │ or touches these three                                     each a real Managed
   ▼                                                            Resource, a real
Crossplane reconciles all three,                                cloud API call
continuously, forever
</code></pre>
<p>Crossplane&#8217;s pitch as a universal control plane is literal: instead of app teams filing tickets or writing their own Terraform for a database, they request a <code class="" data-line="">Database</code> — a custom API the platform team designed — and Crossplane&#8217;s controllers translate that into the actual RDS instance, security group, and parameter group underneath, then keep reconciling all three toward the declared state indefinitely.</p>
<hr />
<h2 id="managed-resources-cloud-infrastructure-as-kubernetes-crds">Managed Resources: Cloud Infrastructure as Kubernetes CRDs</h2>
<pre><code class="" data-line="">$ kubectl apply -f - &lt;&lt;EOF
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
  name: app-uploads-prod
spec:
  forProvider:
    region: us-east-1
  providerConfigRef:
    name: aws-prod
EOF

$ kubectl get bucket app-uploads-prod
NAME               READY   SYNCED   AGE
app-uploads-prod   True    True     30s
#                  ^^^^    ^^^^^^ — READY: resource exists and is healthy
#                          SYNCED: Crossplane&#039;s last reconcile succeeded
</code></pre>
<p>Every field under <code class="" data-line="">forProvider</code> maps directly to that cloud API&#8217;s actual parameters — this is a thin, honest translation layer, not an abstraction hiding what&#8217;s actually being created. <code class="" data-line="">READY</code>/<code class="" data-line="">SYNCED</code> becoming <code class="" data-line="">True</code> means an actual S3 bucket now exists in that AWS account, exactly as declared.</p>
<hr />
<h2 id="compositions-and-xrds-building-your-own-abstract-platform-api">Compositions and XRDs: Building Your Own Abstract Platform API</h2>
<p>This is Crossplane&#8217;s real differentiator over just using individual Managed Resources directly:</p>
<pre><code class="" data-line=""># The platform team defines the abstract API app teams will see
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xdatabases.platform.example.com
spec:
  group: platform.example.com
  names:
    kind: XDatabase
    plural: xdatabases
  claimNames:
    kind: Database        # ← this is what app teams actually create
    plural: databases
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        properties:
          spec:
            properties:
              size: {type: string, enum: [&quot;small&quot;, &quot;medium&quot;, &quot;large&quot;]}
</code></pre>
<p>App teams interact only with the simple <code class="" data-line="">Database</code> claim shown in the Big Picture diagram above. The <code class="" data-line="">Composition</code> resource (not shown here for brevity) is what actually maps <code class="" data-line="">size: small</code> to a specific RDS instance class, storage size, and backup configuration — the platform team&#8217;s opinions, encoded once, consumed self-service by every app team afterward.</p>
<hr />
<h2 id="composition-functions-crossplanes-newer-more-flexible-approach">Composition Functions: Crossplane&#8217;s Newer, More Flexible Approach</h2>
<p>Older Crossplane Compositions used a YAML-based &#8220;patch and transform&#8221; templating language to map the abstract API&#8217;s fields onto Managed Resource fields — functional, but limited for anything beyond straightforward field mapping. <strong>Composition Functions</strong> replace that with actual executable code:</p>
<pre><code class="" data-line="">$ crossplane beta render xr.yaml composition.yaml functions.yaml
---
apiVersion: rds.aws.upbound.io/v1alpha1
kind: Instance
metadata:
  name: my-app-db-instance
spec:
  forProvider:
    instanceClass: db.t3.micro   # ← computed by real Go logic based on
                                  #   spec.size, not a static YAML patch
    engine: postgres
</code></pre>
<p>Composition Functions run as small, packaged pieces of logic (often distributed as OCI images) that Crossplane&#8217;s engine invokes during reconciliation — giving platform teams real conditionals, loops, and validation instead of the older templating language&#8217;s more limited patch syntax.</p>
<hr />
<h2 id="providers-and-the-provider-ecosystem">Providers and the Provider Ecosystem</h2>
<p>Each cloud&#8217;s resources are supplied by a separate <strong>provider</strong> — <code class="" data-line="">provider-aws</code>, <code class="" data-line="">provider-gcp</code>, <code class="" data-line="">provider-azure</code>, and increasingly split into smaller <strong>provider families</strong> (<code class="" data-line="">provider-aws-s3</code>, <code class="" data-line="">provider-aws-rds</code>, etc.) rather than one enormous provider per cloud:</p>
<pre><code class="" data-line="">$ kubectl get providers
NAME                   INSTALLED   HEALTHY   AGE
provider-aws-s3        True        True      10d
provider-aws-rds       True        True      10d
#         ^^^^^^ — installing only the families you actually use, instead
#                  of one monolithic provider-aws with every AWS service&#039;s
#                  CRDs installed regardless of whether you use them
</code></pre>
<p>The family split exists specifically because a single monolithic cloud provider can register thousands of CRDs — a real, measurable strain on a cluster&#8217;s etcd and API server that the ecosystem is still in the process of migrating away from.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>Crossplane reconciles continuously — a manual change to a cloud resource outside Crossplane gets reverted on the next loop.</strong> Teams coming from Terraform&#8217;s plan/apply model, where nothing changes until you explicitly run <code class="" data-line="">apply</code> again, are frequently surprised the first time a manual &#8220;quick fix&#8221; in the AWS console gets silently undone minutes later.</p>
<p><strong>Monolithic providers can register thousands of CRDs, and that has a real, measurable etcd and API-server cost.</strong> If you&#8217;re on an older, non-family provider version and seeing API server memory pressure, check CRD count before assuming it&#8217;s an unrelated capacity issue.</p>
<p><strong>Deleting a Composition&#8217;s underlying claim doesn&#8217;t always tear down cleanly if finalizers on the Managed Resources are stuck</strong> — a Managed Resource that failed to delete cleanly from the cloud side (a non-empty S3 bucket, for instance) will block the whole claim&#8217;s deletion until that&#8217;s resolved manually.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line="">kubectl get managed                        # every Managed Resource, all providers
kubectl get compositeresourcedefinitions   # XRDs — the abstract APIs defined
kubectl get compositions                   # the mapping logic behind each XRD
kubectl get providers                       # installed providers + health
crossplane beta render &lt;xr&gt; &lt;comp&gt; &lt;fns&gt;    # render a Composition locally, no cluster needed
kubectl describe &lt;managed-resource-kind&gt; &lt;name&gt;   # sync status + underlying cloud errors
</code></pre>
<hr />
<h2 id="contribution-opportunity-migrating-providers-to-the-family-pattern">Contribution Opportunity: Migrating Providers to the Family Pattern</h2>
<p><strong>The limitation:</strong> Not every Crossplane provider has migrated from the older, monolithic-per-cloud model to the smaller &#8220;provider family&#8221; pattern that registers only the CRDs for services actually in use. Clusters running an un-migrated provider carry the etcd and API-server overhead of thousands of unused CRDs, and this is a known, actively-discussed problem in the Crossplane community — not a hypothetical one.</p>
<p><strong>Why it&#8217;s hard to fix:</strong> Splitting a monolithic provider into families isn&#8217;t a mechanical find-and-replace — it means restructuring code generation, versioning, and release processes for every resource type the provider covers, while keeping a migration path that doesn&#8217;t break existing users who depend on the old provider&#8217;s CRDs. It&#8217;s real, unglamorous engineering work that has to happen provider-by-provider, cloud-by-cloud, and each provider&#8217;s maintainer bandwidth varies.</p>
<p><strong>What a contribution-shaped fix looks like:</strong> The Crossplane and Upbound-maintained provider repositories publicly track which providers still need family-splitting — this is documented, wanted work, not a gap you&#8217;d have to go discover yourself. A concrete starting contribution: pick one still-monolithic provider (checking the project&#8217;s own tracking issues for an unclaimed one), and work through the documented family-split process the already-migrated providers (like <code class="" data-line="">provider-aws</code>) used as a reference implementation. This is real upstream OSS work with an existing template to follow, not a design problem you have to solve from scratch.</p>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Crossplane&#8217;s Managed Resources make individual cloud resources real Kubernetes CRDs, continuously reconciled rather than applied once</li>
<li>Compositions and XRDs are the actual value proposition: platform teams define a simple, opinionated API once; app teams self-serve against it without needing to know what&#8217;s underneath</li>
<li>Composition Functions replace older YAML patch-and-transform templating with real executable logic — a genuinely evolving, more flexible part of the project</li>
<li>Continuous reconciliation means manual out-of-band changes get reverted — a real behavioral difference from Terraform&#8217;s plan/apply model, not just a implementation detail</li>
<li>The provider family migration is documented, wanted, achievable contribution work — not a gap you&#8217;d need to discover on your own</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>Crossplane&#8217;s composition model and Terraform&#8217;s HCL module model solve the same underlying problem — reusable, parameterized infrastructure definitions — from genuinely different architectural starting points. EP07 puts them side by side and gives a clear recommendation for which fits which team.</p>
<p><em>Next: <a href="/crossplane-vs-terraform/">EP07 — Crossplane vs Terraform: Composition vs HCL for Infrastructure as Code</a></em></p>
<p>Get EP07 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fcrossplane-universal-control-plane%2F&amp;linkname=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fcrossplane-universal-control-plane%2F&amp;linkname=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fcrossplane-universal-control-plane%2F&amp;linkname=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fcrossplane-universal-control-plane%2F&amp;linkname=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fcrossplane-universal-control-plane%2F&amp;linkname=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fcrossplane-universal-control-plane%2F&amp;linkname=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fcrossplane-universal-control-plane%2F&amp;linkname=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fcrossplane-universal-control-plane%2F&#038;title=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" data-a2a-url="https://linuxcent.com/crossplane-universal-control-plane/" data-a2a-title="Crossplane: Kubernetes as the Universal Control Plane"></a></p><p>The post <a href="https://linuxcent.com/crossplane-universal-control-plane/">Crossplane: Kubernetes as the Universal Control Plane</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/crossplane-universal-control-plane/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2252</post-id>	</item>
		<item>
		<title>Cluster API: Declarative Cluster Lifecycle — Rancher&#8217;s Foundation Layer</title>
		<link>https://linuxcent.com/cluster-api-declarative-lifecycle/</link>
					<comments>https://linuxcent.com/cluster-api-declarative-lifecycle/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Sun, 19 Jul 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[Kubernetes Ecosystem]]></category>
		<category><![CDATA[CAPI]]></category>
		<category><![CDATA[Cluster API]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Platform Engineering]]></category>
		<category><![CDATA[Rancher]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=2249</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 5</span> <span class="rt-label rt-postfix">minutes</span></span>Learn how Cluster API manages Kubernetes cluster lifecycle declaratively — the provider model Rancher and others build their own tooling on top of.</p>
<p>The post <a href="https://linuxcent.com/cluster-api-declarative-lifecycle/">Cluster API: Declarative Cluster Lifecycle — Rancher&#8217;s Foundation Layer</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 5</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>Kubernetes Ecosystem: From User to Contributor, Episode 5</em><br />
<a href="/rancher-multi-cluster-management/">← EP04: Rancher</a> · <strong>EP05: Cluster API</strong> · <a href="/crossplane-universal-control-plane/">EP06: Crossplane →</a></p>
<p><strong>11 min read</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Cluster API (CAPI) declares Kubernetes clusters themselves — not just workloads running inside them — as Kubernetes objects: <code class="" data-line="">Cluster</code>, <code class="" data-line="">Machine</code>, <code class="" data-line="">MachineDeployment</code>, reconciled by controllers the same way a <code class="" data-line="">Deployment</code> reconciles pods</li>
<li>CAPI itself is infrastructure-agnostic — the actual provisioning logic lives in separate <strong>infrastructure providers</strong> (AWS, Azure, GCP, vSphere, and dozens more), each implementing the same core contract</li>
<li>Bootstrapping is genuinely awkward by necessity: you need a Kubernetes cluster to run CAPI&#8217;s controllers before CAPI can create your real cluster — solved by a temporary &#8220;kind&#8221; cluster and a <strong>pivot</strong> step that moves CAPI&#8217;s own resources into the cluster it just created</li>
<li>Rancher&#8217;s own newer provisioning (EP04) increasingly builds on CAPI patterns rather than reinventing cluster lifecycle management from scratch</li>
<li>Provider version compatibility is a real, ongoing constraint — CAPI core and each infrastructure provider version independently, and not every combination is supported</li>
<li>Contribution opportunity: <code class="" data-line="">clusterctl move</code>, the pivot operation, has well-documented fragility with resources it doesn&#8217;t natively understand — a concrete, scoped gap</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">Cluster (the K8s object, not the K8s cluster itself)
  │
  ├── Represents: this Cluster SHOULD exist
  │
  ▼
MachineDeployment  ──── mirrors Deployment/ReplicaSet/Pod exactly ────┐
  │                                                                     │
  ▼                                                                     │
MachineSet                                                              │
  │                                                                     │
  ▼                                                                     │
Machine  ────────► Infrastructure Provider (AWS/Azure/GCP/vSphere/...)  │
  │                  actually creates the VM/instance                  │
  ▼                                                                     │
Bootstrap Provider (kubeadm, typically)                                │
  actually turns that VM into a working Kubernetes node ────────────────┘
</code></pre>
<p>Cluster API&#8217;s declarative cluster lifecycle model is the same reconciliation pattern Kubernetes already uses for workloads, applied one layer up: instead of a <code class="" data-line="">Deployment</code> controller reconciling <code class="" data-line="">Pod</code> objects into running containers, CAPI&#8217;s controllers reconcile <code class="" data-line="">Machine</code> objects into running cloud instances that then join a cluster as nodes.</p>
<hr />
<h2 id="the-core-abstraction-clusters-and-machines-as-kubernetes-objects">The Core Abstraction: Clusters and Machines as Kubernetes Objects</h2>
<pre><code class="" data-line="">$ kubectl apply -f - &lt;&lt;EOF
apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
  name: prod-us-east
spec:
  clusterNetwork:
    pods:
      cidrBlocks: [&quot;192.168.0.0/16&quot;]
  infrastructureRef:
    apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
    kind: AWSCluster
    name: prod-us-east
EOF

$ kubectl get clusters
NAME           PHASE          AGE
prod-us-east   Provisioning   45s

$ kubectl get machines
NAME                     CLUSTER        PHASE         VERSION
prod-us-east-cp-x7k2l    prod-us-east   Provisioning  v1.28.5
</code></pre>
<p>The <code class="" data-line="">Cluster</code> object is a declaration of intent, not the cluster itself — a management cluster (a separate, already-running Kubernetes cluster whose only job is to run CAPI&#8217;s controllers) watches these objects and does the actual work of calling out to AWS, Azure, or whatever provider is referenced, creating instances, and bootstrapping Kubernetes on them.</p>
<hr />
<h2 id="the-provider-model-how-capi-stays-infrastructure-agnostic">The Provider Model: How CAPI Stays Infrastructure-Agnostic</h2>
<p>CAPI&#8217;s core (<code class="" data-line="">cluster-api</code>) knows nothing about AWS, Azure, or any specific cloud. That knowledge lives in separate, independently-versioned <strong>infrastructure providers</strong>:</p>
<pre><code class="" data-line="">$ clusterctl init --infrastructure aws
Fetching providers
Installing cert-manager
Installing Provider=&quot;cluster-api&quot; Version=&quot;v1.6.2&quot;
Installing Provider=&quot;bootstrap-kubeadm&quot; Version=&quot;v1.6.2&quot;
Installing Provider=&quot;control-plane-kubeadm&quot; Version=&quot;v1.6.2&quot;
Installing Provider=&quot;infrastructure-aws&quot; Version=&quot;v2.4.0&quot;
#                                          ^^^^^^ — versioned independently
#                                          from core CAPI above
</code></pre>
<p>This split — core lifecycle logic separate from provider-specific implementation — is the same architectural pattern CNI and CSI use elsewhere in Kubernetes: a stable core contract, swappable implementations. It&#8217;s also exactly why CAPI&#8217;s ecosystem includes dozens of infrastructure providers (AWS, Azure, GCP, vSphere, OpenStack, Docker, bare metal, and many more) maintained by different teams at different paces.</p>
<hr />
<h2 id="a-management-cluster-managing-itself-and-others-the-pivot">A Management Cluster Managing Itself and Others: The Pivot</h2>
<p>The genuinely awkward part of CAPI&#8217;s bootstrap story: you need a running Kubernetes cluster to host CAPI&#8217;s controllers before CAPI can create your first real cluster. The standard pattern:</p>
<pre><code class="" data-line=""># Step 1: spin up a throwaway local cluster just to run CAPI controllers
$ kind create cluster --name capi-bootstrap
$ clusterctl init --infrastructure aws

# Step 2: use that temporary management cluster to provision the REAL cluster
$ clusterctl generate cluster prod-us-east --infrastructure aws | kubectl apply -f -

# Step 3: move CAPI&#039;s own resources OFF the throwaway cluster and ONTO
# the cluster that was just created — &quot;pivoting&quot; management to itself
$ clusterctl move --to-kubeconfig=./prod-us-east.kubeconfig
Performing move...
Discovering Cluster API objects
Moving Cluster API objects: Clusters=1, Machines=3, ...
</code></pre>
<p>After the pivot, the cluster CAPI created is now managing its own lifecycle (and can go on to manage other clusters too) — the temporary <code class="" data-line="">kind</code> cluster can be torn down. This bootstrap-then-pivot dance is elegant in theory and one of the more fragile operational moments in CAPI&#8217;s lifecycle in practice.</p>
<hr />
<h2 id="how-rancher-and-others-build-on-capi">How Rancher and Others Build On CAPI</h2>
<p>Rancher&#8217;s newer cluster provisioning (EP04) increasingly leans on CAPI patterns rather than maintaining entirely separate provisioning logic — the industry direction across the Kubernetes ecosystem has been toward CAPI as the shared substrate for &#8220;declare a cluster, get a cluster,&#8221; with vendors building their own UX and opinionated defaults on top rather than reinventing the reconciliation model itself.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>Provider version compatibility is a real support matrix, not a &#8220;probably fine&#8221; assumption.</strong> Core CAPI and each infrastructure provider version independently — upgrading one without checking the compatibility matrix for the other is a common source of cryptic reconciliation failures.</p>
<p><strong><code class="" data-line="">clusterctl move</code> is a rare, high-stakes operation — most teams run it once per cluster&#8217;s lifetime, if ever, which means nobody on the team has recent hands-on experience when something goes wrong.</strong> Test the pivot in a non-production scenario before relying on it for anything real.</p>
<p><strong>A <code class="" data-line="">Machine</code> stuck in <code class="" data-line="">Provisioning</code> can mean the infrastructure provider, the bootstrap provider, or the actual cloud API — three different places to look, and the <code class="" data-line="">Machine</code> object&#8217;s own status doesn&#8217;t always make it obvious which.</strong> Check the infrastructure-specific object (<code class="" data-line="">AWSMachine</code>, <code class="" data-line="">AzureMachine</code>, etc.) directly, not just the generic <code class="" data-line="">Machine</code>.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line="">clusterctl init --infrastructure &lt;provider&gt;   # install CAPI + a provider on the management cluster
clusterctl generate cluster &lt;name&gt; --infrastructure &lt;provider&gt;   # generate cluster manifests
kubectl get clusters                           # cluster lifecycle phase
kubectl get machines                           # per-node provisioning phase
kubectl get awsmachines / azuremachines / ...   # provider-specific detail
clusterctl move --to-kubeconfig=&lt;path&gt;          # pivot management to another cluster
clusterctl describe cluster &lt;name&gt;              # human-readable status tree
</code></pre>
<hr />
<h2 id="contribution-opportunity-clusterctl-moves-fragility-with-non-native-resources">Contribution Opportunity: <code class="" data-line="">clusterctl move</code>&#8216;s Fragility With Non-Native Resources</h2>
<p><strong>The limitation:</strong> <code class="" data-line="">clusterctl move</code> knows how to move CAPI&#8217;s own well-known resource types between management clusters cleanly. When a provider or an operator has added custom resources that reference or extend CAPI objects — a common real-world pattern — <code class="" data-line="">move</code> doesn&#8217;t always understand the relationship, and teams have reported needing manual intervention (patching, reapplying, or reordering) to get a full pivot to succeed cleanly. This is documented in multiple open issues against the project, not a rare edge case.</p>
<p><strong>Why it&#8217;s hard to fix:</strong> <code class="" data-line="">move</code>&#8216;s core logic has to correctly identify and preserve object references and ownership across an arbitrary graph of custom resources it wasn&#8217;t necessarily designed to know about — building a fully general solution risks either false confidence (silently missing a reference) or false failure (over-cautiously blocking a move that would have been fine). The CAPI maintainers have to weigh correctness against usability here, and it&#8217;s a genuinely hard design problem, not a simple bug.</p>
<p><strong>What a contribution-shaped fix looks like:</strong> Two realistic, scoped starting points: (1) a <code class="" data-line="">--dry-run</code>-style pre-flight checker for <code class="" data-line="">clusterctl move</code> that specifically scans for custom resources referencing CAPI objects and flags them before the move attempt, rather than discovering the gap mid-operation; or (2) contributing a documented, tested procedure (and ideally a small helper tool) for the specific pattern of &#8220;extra resources referencing Machine/Cluster objects&#8221; that&#8217;s already been reported in the project&#8217;s issue tracker — turning a known, recurring support question into a documented, repeatable procedure.</p>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>CAPI applies Kubernetes&#8217; own reconciliation pattern one layer up — <code class="" data-line="">Cluster</code> and <code class="" data-line="">Machine</code> objects are declarations, reconciled into real infrastructure by provider-specific controllers</li>
<li>The core/provider split keeps CAPI infrastructure-agnostic, at the cost of independent versioning you have to track across a real compatibility matrix</li>
<li>The bootstrap-then-pivot pattern is CAPI&#8217;s most elegant and most operationally fragile moment — rehearse it before you need it for real</li>
<li>Rancher and other platform tools increasingly build their own provisioning UX on top of CAPI&#8217;s reconciliation model rather than replacing it</li>
<li>The clearest contribution opportunity is <code class="" data-line="">clusterctl move</code>&#8216;s handling of non-native custom resources — a documented, scoped gap with real prior art in the issue tracker</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>CAPI treats infrastructure — VMs, networks, load balancers — as the thing being reconciled into existence from Kubernetes objects. EP06 takes that same idea and generalizes it as far as it can go: Crossplane turns Kubernetes into a control plane for effectively any cloud resource, not just the ones needed to run Kubernetes itself.</p>
<p><em>Next: <a href="/crossplane-universal-control-plane/">EP06 — Crossplane: Kubernetes as the Universal Control Plane</a></em></p>
<p>Get EP06 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fcluster-api-declarative-lifecycle%2F&amp;linkname=Cluster%20API%3A%20Declarative%20Cluster%20Lifecycle%20%E2%80%94%20Rancher%E2%80%99s%20Foundation%20Layer" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fcluster-api-declarative-lifecycle%2F&amp;linkname=Cluster%20API%3A%20Declarative%20Cluster%20Lifecycle%20%E2%80%94%20Rancher%E2%80%99s%20Foundation%20Layer" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fcluster-api-declarative-lifecycle%2F&amp;linkname=Cluster%20API%3A%20Declarative%20Cluster%20Lifecycle%20%E2%80%94%20Rancher%E2%80%99s%20Foundation%20Layer" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fcluster-api-declarative-lifecycle%2F&amp;linkname=Cluster%20API%3A%20Declarative%20Cluster%20Lifecycle%20%E2%80%94%20Rancher%E2%80%99s%20Foundation%20Layer" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fcluster-api-declarative-lifecycle%2F&amp;linkname=Cluster%20API%3A%20Declarative%20Cluster%20Lifecycle%20%E2%80%94%20Rancher%E2%80%99s%20Foundation%20Layer" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fcluster-api-declarative-lifecycle%2F&amp;linkname=Cluster%20API%3A%20Declarative%20Cluster%20Lifecycle%20%E2%80%94%20Rancher%E2%80%99s%20Foundation%20Layer" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fcluster-api-declarative-lifecycle%2F&amp;linkname=Cluster%20API%3A%20Declarative%20Cluster%20Lifecycle%20%E2%80%94%20Rancher%E2%80%99s%20Foundation%20Layer" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fcluster-api-declarative-lifecycle%2F&#038;title=Cluster%20API%3A%20Declarative%20Cluster%20Lifecycle%20%E2%80%94%20Rancher%E2%80%99s%20Foundation%20Layer" data-a2a-url="https://linuxcent.com/cluster-api-declarative-lifecycle/" data-a2a-title="Cluster API: Declarative Cluster Lifecycle — Rancher’s Foundation Layer"></a></p><p>The post <a href="https://linuxcent.com/cluster-api-declarative-lifecycle/">Cluster API: Declarative Cluster Lifecycle — Rancher&#8217;s Foundation Layer</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/cluster-api-declarative-lifecycle/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2249</post-id>	</item>
		<item>
		<title>Rancher: Multi-Cluster Kubernetes Management at Scale</title>
		<link>https://linuxcent.com/rancher-multi-cluster-management/</link>
					<comments>https://linuxcent.com/rancher-multi-cluster-management/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Fri, 17 Jul 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[Kubernetes Ecosystem]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Multi-Cluster]]></category>
		<category><![CDATA[Platform Engineering]]></category>
		<category><![CDATA[Rancher]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=2246</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 5</span> <span class="rt-label rt-postfix">minutes</span></span>See how Rancher centralizes multi-cluster Kubernetes management — and where its own control plane becomes the thing you now have to operate.</p>
<p>The post <a href="https://linuxcent.com/rancher-multi-cluster-management/">Rancher: Multi-Cluster Kubernetes Management at Scale</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 5</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>Kubernetes Ecosystem: From User to Contributor, Episode 4</em><br />
<a href="/k3s-vs-microk8s-vs-minikube/">← EP03: k3s vs MicroK8s vs Minikube</a> · <strong>EP04: Rancher</strong> · <a href="/cluster-api-declarative-lifecycle/">EP05: Cluster API →</a></p>
<p><strong>11 min read</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Rancher multi-cluster management means one Rancher server managing many downstream Kubernetes clusters — its own RKE2/k3s clusters, or imported EKS/GKE/AKS clusters — from a single pane of glass</li>
<li>Rancher doesn&#8217;t proxy every API call through itself; it deploys a lightweight agent into each downstream cluster that phones home, then aggregates each cluster&#8217;s API through that agent</li>
<li><strong>Fleet</strong>, Rancher&#8217;s built-in GitOps engine, is what actually pushes manifests to potentially hundreds of clusters from a single git repository — this is the feature that makes &#8220;fleet&#8221; in the product&#8217;s marketing literal, not aspirational</li>
<li>Rancher&#8217;s Projects group namespaces <em>within</em> one cluster for permission management — they are not a cross-cluster grouping, a common misunderstanding</li>
<li>The Rancher server itself becomes something you now have to operate: HA, upgrades, and version compatibility with every downstream cluster&#8217;s Kubernetes version are real, ongoing operational work</li>
<li>Contribution opportunity: Fleet&#8217;s multi-cluster drift visibility has real, specific gaps — covered below</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">                    ┌─────────────────────────┐
                    │      RANCHER SERVER       │
                    │  (itself a K8s cluster,   │
                    │   ideally HA, 3+ nodes)   │
                    └────────────┬────────────┘
                                 │ agents phone home,
                                 │ API aggregated back
              ┌──────────────────┼──────────────────┐
              │                  │                  │
        ┌─────▼─────┐     ┌──────▼──────┐    ┌──────▼──────┐
        │  RKE2      │     │  Imported    │    │  Imported    │
        │  cluster   │     │  EKS cluster │    │  GKE cluster │
        │ (Rancher-  │     │ (Rancher     │    │ (Rancher     │
        │  provisioned)│    │  didn&#039;t      │    │  didn&#039;t      │
        │            │     │  create it)  │    │  create it)  │
        └────────────┘     └─────────────┘    └─────────────┘
</code></pre>
<p>Rancher multi-cluster management works by inverting the connection direction most people assume: Rancher doesn&#8217;t reach out and control downstream clusters directly. Each downstream cluster runs a small agent that establishes an outbound connection back to the Rancher server — which is why Rancher can manage a cluster sitting behind NAT or a restrictive firewall, as long as that cluster can reach out.</p>
<hr />
<h2 id="how-rancher-actually-manages-clusters-it-didnt-create">How Rancher Actually Manages Clusters It Didn&#8217;t Create</h2>
<pre><code class="" data-line=""># Import an existing cluster Rancher never touched at creation time
$ kubectl apply -f https://rancher.example.com/v3/import/&lt;token&gt;.yaml
# This installs the cattle-cluster-agent into the target cluster —
# that agent is the only thing Rancher needs to start managing it

$ kubectl get pods -n cattle-system
NAME                                    READY   STATUS    RESTARTS
cattle-cluster-agent-7d8f9c-x2k9l       1/1     Running   0
</code></pre>
<p>Once the agent is running, Rancher&#8217;s UI and API present that cluster&#8217;s resources as if you were talking to it directly — the agent maintains the tunnel and relays API calls both ways. This is the architectural reason Rancher can manage a genuinely heterogeneous fleet: RKE2, k3s, EKS, GKE, AKS, and on-prem clusters all look identical to Rancher once the same agent is running in each.</p>
<hr />
<h2 id="rke2-and-k3s-ranchers-own-cluster-distributions">RKE2 and k3s: Rancher&#8217;s Own Cluster Distributions</h2>
<p>Rancher can also provision brand-new clusters directly, using its own distributions:</p>
<pre><code class="" data-line=""># Provisioning a new downstream cluster via Rancher&#039;s cluster API
# (typically done through the UI, but expressible as a CR)
$ kubectl apply -f - &lt;&lt;EOF
apiVersion: provisioning.cattle.io/v1
kind: Cluster
metadata:
  name: edge-fleet-01
  namespace: fleet-default
spec:
  kubernetesVersion: v1.28.9+rke2r1
  rkeConfig:
    machinePools:
    - name: pool-01
      quantity: 3
EOF
</code></pre>
<p>RKE2 (&#8220;RKE Government,&#8221; a CIS-hardened, more security-focused distribution) and k3s (the lightweight distribution covered in EP03) are both Rancher/SUSE projects, and Rancher treats them as first-class provisioning targets — this is the direct product connection between &#8220;the lightweight Kubernetes distro you picked in EP03&#8221; and &#8220;the fleet manager covered in this episode.&#8221;</p>
<hr />
<h2 id="fleet-gitops-at-fleet-scale">Fleet: GitOps at Fleet Scale</h2>
<pre><code class="" data-line=""># Fleet watches a git repo and deploys its manifests to a TARGETED
# set of clusters based on label selectors — not necessarily all of them
$ kubectl apply -f - &lt;&lt;EOF
apiVersion: fleet.cattle.io/v1alpha1
kind: GitRepo
metadata:
  name: platform-baseline
  namespace: fleet-default
spec:
  repo: https://github.com/example-org/platform-manifests
  branch: main
  targets:
  - clusterSelector:
      matchLabels:
        env: production
EOF

$ kubectl get gitrepo -n fleet-default
NAME                REPO                                          COMMIT     BUNDLESREADY
platform-baseline   https://github.com/example-org/platform-...   a1b2c3d    12/14
#                                                                              ^^^^^ — 2 clusters
#                                                                              haven&#039;t converged yet
</code></pre>
<p><code class="" data-line="">BUNDLESREADY 12/14</code> is the number that matters at fleet scale — it tells you how many of the targeted clusters have actually converged to the git state, but notice it doesn&#8217;t tell you <em>why</em> the other 2 haven&#8217;t, or which 2 they are, without drilling into each bundle individually. That&#8217;s the exact gap covered in this episode&#8217;s contribution section.</p>
<hr />
<h2 id="projects-and-rbac-ranchers-multi-tenancy-layer">Projects and RBAC: Rancher&#8217;s Multi-Tenancy Layer</h2>
<p>A common misconception worth correcting directly: Rancher&#8217;s <strong>Projects</strong> group namespaces <em>within a single cluster</em> for permission and resource-quota management — they are not a mechanism for grouping resources <em>across</em> clusters. Cross-cluster access control is handled separately, through Cluster-level and Global roles assigned per user or group.</p>
<pre><code class="" data-line="">Global scope        → applies across every cluster Rancher manages
  └── Cluster scope  → applies to all namespaces in one specific cluster
        └── Project scope → applies to a defined subset of namespaces
              within that one cluster (Rancher&#039;s own grouping construct)
</code></pre>
<p>Getting this hierarchy backwards — assuming a Project spans clusters — is one of the most common Rancher RBAC mistakes teams make when first designing their permission model.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>Rancher server itself needs HA, and losing it doesn&#8217;t take down downstream clusters — but it does take down your ability to manage them centrally.</strong> Downstream clusters keep running their workloads fine if Rancher server is unreachable; you just lose the single-pane-of-glass view and Fleet&#8217;s GitOps reconciliation until it&#8217;s back.</p>
<p><strong>Version skew between Rancher server and downstream Kubernetes versions is a real, documented compatibility matrix — not a &#8220;should mostly work&#8221; situation.</strong> Upgrading Rancher server ahead of your downstream clusters&#8217; Kubernetes versions (or vice versa, letting downstream clusters drift too far ahead) can break agent compatibility. Check Rancher&#8217;s official support matrix before any upgrade, not after something breaks.</p>
<p><strong>Agent reconnection storms after a Rancher server upgrade or restart are a known operational event, not a bug report.</strong> If you manage dozens of downstream clusters, expect a burst of reconnection activity immediately after any Rancher server maintenance — plan maintenance windows with that in mind.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line="">kubectl apply -f import.yaml              # import an existing cluster
kubectl get clusters.provisioning.cattle.io -A   # all clusters Rancher manages
kubectl get gitrepo -n fleet-default       # Fleet GitOps repo status
kubectl get bundles -n fleet-default       # per-cluster deployment bundle status
kubectl get pods -n cattle-system          # agent health, on a downstream cluster
</code></pre>
<hr />
<h2 id="contribution-opportunity-fleets-multi-cluster-drift-visibility">Contribution Opportunity: Fleet&#8217;s Multi-Cluster Drift Visibility</h2>
<p><strong>The limitation:</strong> Fleet&#8217;s <code class="" data-line="">BUNDLESREADY</code> count tells you how many targeted clusters have converged, but drilling into <em>why</em> a specific cluster hasn&#8217;t — a stuck rollout, a resource conflict, a cluster that&#8217;s unreachable — still requires checking that cluster&#8217;s bundle status individually. At a fleet of dozens or hundreds of clusters, there&#8217;s no aggregated view that surfaces &#8220;these 3 clusters are all failing for the same underlying reason&#8221; without manual cross-referencing.</p>
<p><strong>Why it&#8217;s hard to fix:</strong> Aggregating meaningful failure reasons across a heterogeneous fleet is genuinely harder than it sounds — a &#8220;failed&#8221; bundle on one cluster might be a transient network blip, on another a real manifest conflict, and on a third a resource quota limit. Building a dashboard that correctly buckets and summarizes those different failure classes without producing a wall of noise is a real UX and data-modeling problem, and it&#8217;s not the kind of thing that gets prioritized ahead of core provisioning reliability work.</p>
<p><strong>What a contribution-shaped fix looks like:</strong> A scoped, achievable starting point: a <code class="" data-line="">fleet</code> CLI plugin or a Rancher UI extension that queries all <code class="" data-line="">Bundle</code> resources across the fleet&#8217;s clusters, groups them by failure-reason similarity (using the existing status conditions Fleet already populates — this is a client-side aggregation problem, not a new backend feature), and surfaces a ranked summary. This is buildable against Fleet&#8217;s existing CRDs and status fields without needing to modify Fleet&#8217;s core reconciliation logic — exactly the kind of contribution an operator who&#8217;s felt this specific pain at scale is positioned to build and upstream.</p>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Rancher manages downstream clusters through an outbound-connecting agent, not by reaching in — this is why it can manage clusters behind NAT or restrictive firewalls</li>
<li>Fleet is the actual mechanism for GitOps at fleet scale, targeting clusters by label selector and reporting convergence via <code class="" data-line="">BUNDLESREADY</code> counts</li>
<li>Projects group namespaces within one cluster, not across clusters — a frequent RBAC design mistake starts from getting this backwards</li>
<li>The Rancher server becomes real infrastructure you operate: HA, version-compatibility matrices, and post-upgrade agent reconnection are ongoing operational realities</li>
<li>The clearest contribution opportunity is Fleet&#8217;s drift-visibility gap at scale — a client-side aggregation problem buildable against existing CRDs, not a core-logic change</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>Rancher&#8217;s own cluster provisioning sits on top of a more general pattern: declaring cluster lifecycle as Kubernetes resources. EP05 covers Cluster API directly — the CNCF project Rancher&#8217;s own provisioning increasingly builds on, and the pattern several other tools in this series also depend on.</p>
<p><em>Next: <a href="/cluster-api-declarative-lifecycle/">EP05 — Cluster API: Declarative Cluster Lifecycle — Rancher&#8217;s Foundation Layer</a></em></p>
<p>Get EP05 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Francher-multi-cluster-management%2F&amp;linkname=Rancher%3A%20Multi-Cluster%20Kubernetes%20Management%20at%20Scale" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Francher-multi-cluster-management%2F&amp;linkname=Rancher%3A%20Multi-Cluster%20Kubernetes%20Management%20at%20Scale" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Francher-multi-cluster-management%2F&amp;linkname=Rancher%3A%20Multi-Cluster%20Kubernetes%20Management%20at%20Scale" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Francher-multi-cluster-management%2F&amp;linkname=Rancher%3A%20Multi-Cluster%20Kubernetes%20Management%20at%20Scale" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Francher-multi-cluster-management%2F&amp;linkname=Rancher%3A%20Multi-Cluster%20Kubernetes%20Management%20at%20Scale" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Francher-multi-cluster-management%2F&amp;linkname=Rancher%3A%20Multi-Cluster%20Kubernetes%20Management%20at%20Scale" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Francher-multi-cluster-management%2F&amp;linkname=Rancher%3A%20Multi-Cluster%20Kubernetes%20Management%20at%20Scale" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Francher-multi-cluster-management%2F&#038;title=Rancher%3A%20Multi-Cluster%20Kubernetes%20Management%20at%20Scale" data-a2a-url="https://linuxcent.com/rancher-multi-cluster-management/" data-a2a-title="Rancher: Multi-Cluster Kubernetes Management at Scale"></a></p><p>The post <a href="https://linuxcent.com/rancher-multi-cluster-management/">Rancher: Multi-Cluster Kubernetes Management at Scale</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/rancher-multi-cluster-management/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2246</post-id>	</item>
		<item>
		<title>Build a Simple Kubernetes Operator with controller-runtime and kubebuilder</title>
		<link>https://linuxcent.com/build-kubernetes-operator-controller-runtime/</link>
					<comments>https://linuxcent.com/build-kubernetes-operator-controller-runtime/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Sat, 25 Apr 2026 18:16:26 +0000</pubDate>
				<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[controller-runtime]]></category>
		<category><![CDATA[CRD]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[kubebuilder]]></category>
		<category><![CDATA[Operator]]></category>
		<category><![CDATA[Platform Engineering]]></category>
		<guid isPermaLink="false">https://linuxcent.com/build-kubernetes-operator-controller-runtime/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 7</span> <span class="rt-label rt-postfix">minutes</span></span>Build a real Kubernetes operator with controller-runtime and kubebuilder — scaffold, reconciler code, RBAC markers, and testing on a local cluster.</p>
<p>The post <a href="https://linuxcent.com/build-kubernetes-operator-controller-runtime/">Build a Simple Kubernetes Operator with controller-runtime and kubebuilder</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 7</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>Kubernetes CRDs &amp; Operators: Extending the API, Episode 7</em><br />
<em><a href="/what-is-kubernetes-crd/">What Is a CRD?</a> · <a href="/kubernetes-custom-resources-examples/">CRDs You Already Use</a> · <a href="/kubernetes-crd-schema-explained/">CRD Anatomy</a> · <a href="/write-kubernetes-crd-yaml-walkthrough/">Write Your First CRD</a> · <a href="/kubernetes-crd-cel-validation/">CEL Validation</a> · <a href="/kubernetes-controller-reconcile-loop/">Controller Loop</a> · </em><em><a href="/build-kubernetes-operator-controller-runtime/">Build an Operator</a></em><em> · <a href="/kubernetes-crd-versioning-conversion-webhook/">CRD Versioning</a> · <a href="/kubernetes-admission-webhooks-explained/">Admission Webhooks</a> · <a href="/kubernetes-crd-production-finalizers-conditions-rbac/">CRDs in Production</a></em></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Building a Kubernetes operator means writing a Go reconciler with <code class="" data-line="">controller-runtime</code> — kubebuilder scaffolds the project structure, RBAC markers, and Makefile targets so you focus on the reconcile logic<br />
  <em>(kubebuilder = a CLI and framework that generates the operator project scaffold; controller-runtime = the Go library that provides the informer cache, work queue, and reconciler interface)</em></li>
<li>The reconciler for <code class="" data-line="">BackupPolicy</code> in this episode creates and manages a <code class="" data-line="">CronJob</code> — it is the behavior layer for the CRD built in EP03–EP05</li>
<li>RBAC is expressed as Go code comments (<code class="" data-line="">//+kubebuilder:rbac:...</code>) — kubebuilder generates the ClusterRole YAML from them</li>
<li>Run the operator locally with <code class="" data-line="">make run</code> during development; no cluster deployment needed until ready</li>
<li>The same project that builds the operator also builds and installs the CRD — <code class="" data-line="">make install</code> applies the CRD YAML generated from your Go types</li>
<li>Testing: the operator ships with <code class="" data-line="">envtest</code> — a local API server + etcd for controller testing without a real cluster</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">  OPERATOR PROJECT STRUCTURE (kubebuilder scaffold)

  backup-operator/
  ├── api/v1alpha1/
  │   ├── backuppolicy_types.go     ← Go types that define CRD schema
  │   └── groupversion_info.go
  ├── internal/controller/
  │   └── backuppolicy_controller.go ← reconcile logic (our main focus)
  ├── config/
  │   ├── crd/                       ← generated CRD YAML
  │   ├── rbac/                      ← generated RBAC YAML
  │   └── manager/                   ← controller Deployment YAML
  ├── cmd/main.go                    ← entrypoint, sets up the manager
  └── Makefile                       ← build, test, install, deploy targets

  FLOW:
  Go types → kubebuilder generate → CRD YAML + RBAC YAML
  Reconcile function → runs in cluster → watches BackupPolicy → manages CronJobs
</code></pre>
<p>Building a Kubernetes operator with <code class="" data-line="">controller-runtime</code> is where CRDs become living infrastructure — the <code class="" data-line="">BackupPolicy</code> objects created in <a href="/write-kubernetes-crd-yaml-walkthrough/">EP04</a> now get actual behavior attached to them.</p>
<hr />
<h2 id="prerequisites">Prerequisites</h2>
<pre><code class="" data-line=""># Go 1.22+
go version

# kubebuilder CLI
curl -L -o kubebuilder \
  https://github.com/kubernetes-sigs/kubebuilder/releases/latest/download/kubebuilder_linux_amd64
chmod +x kubebuilder
sudo mv kubebuilder /usr/local/bin/

# A running cluster (kind works well for development)
kind create cluster --name operator-dev

# Verify kubectl works
kubectl cluster-info --context kind-operator-dev
</code></pre>
<hr />
<h2 id="step-1-scaffold-the-project">Step 1: Scaffold the Project</h2>
<pre><code class="" data-line="">mkdir backup-operator &amp;&amp; cd backup-operator

# Initialize the Go module and project structure
kubebuilder init \
  --domain storage.example.com \
  --repo github.com/example/backup-operator

# Create the API (Go types + controller scaffold)
kubebuilder create api \
  --group storage \
  --version v1alpha1 \
  --kind BackupPolicy \
  --resource \
  --controller
</code></pre>
<p>When prompted:</p>
<pre><code class="" data-line="">Create Resource [y/n]: y
Create Controller [y/n]: y
</code></pre>
<p>The generated directory tree:</p>
<pre><code class="" data-line="">backup-operator/
├── api/
│   └── v1alpha1/
│       ├── backuppolicy_types.go
│       └── groupversion_info.go
├── internal/
│   └── controller/
│       └── backuppolicy_controller.go
├── cmd/
│   └── main.go
├── config/
│   ├── crd/bases/
│   ├── rbac/
│   └── manager/
├── go.mod
├── go.sum
└── Makefile
</code></pre>
<hr />
<h2 id="step-2-define-the-go-types">Step 2: Define the Go Types</h2>
<p>Edit <code class="" data-line="">api/v1alpha1/backuppolicy_types.go</code> to match the schema from <a href="/kubernetes-crd-schema-explained/">EP03</a>:</p>
<pre><code class="" data-line="">package v1alpha1

import (
    metav1 &quot;k8s.io/apimachinery/pkg/apis/meta/v1&quot;
)

// BackupTarget specifies a namespace to include in the backup.
type BackupTarget struct {
    Namespace      string `json:&quot;namespace&quot;`
    IncludeSecrets bool   `json:&quot;includeSecrets,omitempty&quot;`
}

// BackupPolicySpec defines the desired state of BackupPolicy.
type BackupPolicySpec struct {
    // Schedule is a cron expression for when to run backups.
    // +kubebuilder:validation:Pattern=`^(\*|[0-9,\-\/]+) (\*|[0-9,\-\/]+) (\*|[0-9,\-\/]+) (\*|[0-9,\-\/]+) (\*|[0-9,\-\/]+)$`
    Schedule string `json:&quot;schedule&quot;`

    // RetentionDays is how long to keep backup snapshots.
    // +kubebuilder:validation:Minimum=1
    // +kubebuilder:validation:Maximum=365
    RetentionDays int32 `json:&quot;retentionDays&quot;`

    // StorageClass is the storage class to use for backup volumes.
    // +kubebuilder:default=standard
    // +kubebuilder:validation:Enum=standard;premium;encrypted;archive
    StorageClass string `json:&quot;storageClass,omitempty&quot;`

    // Targets lists the namespaces and resources to include.
    // +kubebuilder:validation:MaxItems=20
    Targets []BackupTarget `json:&quot;targets,omitempty&quot;`

    // Suspended pauses backup execution when true.
    // +kubebuilder:default=false
    Suspended bool `json:&quot;suspended,omitempty&quot;`
}

// BackupPolicyStatus defines the observed state of BackupPolicy.
type BackupPolicyStatus struct {
    // Conditions reflect the current state of the BackupPolicy.
    Conditions []metav1.Condition `json:&quot;conditions,omitempty&quot;`

    // LastBackupTime is when the most recent backup completed.
    LastBackupTime *metav1.Time `json:&quot;lastBackupTime,omitempty&quot;`

    // CronJobName is the name of the managed CronJob.
    CronJobName string `json:&quot;cronJobName,omitempty&quot;`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name=&quot;Schedule&quot;,type=string,JSONPath=`.spec.schedule`
// +kubebuilder:printcolumn:name=&quot;Retention&quot;,type=integer,JSONPath=`.spec.retentionDays`
// +kubebuilder:printcolumn:name=&quot;Suspended&quot;,type=boolean,JSONPath=`.spec.suspended`
// +kubebuilder:printcolumn:name=&quot;Ready&quot;,type=string,JSONPath=`.status.conditions[?(@.type==&#039;Ready&#039;)].status`
// +kubebuilder:printcolumn:name=&quot;Age&quot;,type=date,JSONPath=`.metadata.creationTimestamp`

// BackupPolicy is the Schema for the backuppolicies API.
type BackupPolicy struct {
    metav1.TypeMeta   `json:&quot;,inline&quot;`
    metav1.ObjectMeta `json:&quot;metadata,omitempty&quot;`

    Spec   BackupPolicySpec   `json:&quot;spec,omitempty&quot;`
    Status BackupPolicyStatus `json:&quot;status,omitempty&quot;`
}

// +kubebuilder:object:root=true

// BackupPolicyList contains a list of BackupPolicy.
type BackupPolicyList struct {
    metav1.TypeMeta `json:&quot;,inline&quot;`
    metav1.ListMeta `json:&quot;metadata,omitempty&quot;`
    Items           []BackupPolicy `json:&quot;items&quot;`
}

func init() {
    SchemeBuilder.Register(&amp;BackupPolicy{}, &amp;BackupPolicyList{})
}
</code></pre>
<p>Regenerate the CRD YAML and DeepCopy methods:</p>
<pre><code class="" data-line="">make generate   # regenerates zz_generated.deepcopy.go
make manifests  # regenerates CRD YAML under config/crd/bases/
</code></pre>
<hr />
<h2 id="step-3-write-the-reconciler">Step 3: Write the Reconciler</h2>
<p>Edit <code class="" data-line="">internal/controller/backuppolicy_controller.go</code>:</p>
<pre><code class="" data-line="">package controller

import (
    &quot;context&quot;
    &quot;fmt&quot;

    batchv1 &quot;k8s.io/api/batch/v1&quot;
    corev1 &quot;k8s.io/api/core/v1&quot;
    apierrors &quot;k8s.io/apimachinery/pkg/api/errors&quot;
    &quot;k8s.io/apimachinery/pkg/api/meta&quot;
    metav1 &quot;k8s.io/apimachinery/pkg/apis/meta/v1&quot;
    &quot;k8s.io/apimachinery/pkg/runtime&quot;
    &quot;k8s.io/apimachinery/pkg/types&quot;
    ctrl &quot;sigs.k8s.io/controller-runtime&quot;
    &quot;sigs.k8s.io/controller-runtime/pkg/client&quot;
    &quot;sigs.k8s.io/controller-runtime/pkg/log&quot;

    storagev1alpha1 &quot;github.com/example/backup-operator/api/v1alpha1&quot;
)

// BackupPolicyReconciler reconciles BackupPolicy objects.
type BackupPolicyReconciler struct {
    client.Client
    Scheme *runtime.Scheme
}

// RBAC markers — kubebuilder generates ClusterRole YAML from these comments.
//+kubebuilder:rbac:groups=storage.example.com,resources=backuppolicies,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=storage.example.com,resources=backuppolicies/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=storage.example.com,resources=backuppolicies/finalizers,verbs=update
//+kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete

func (r *BackupPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    logger := log.FromContext(ctx)

    // Step 1: Fetch the BackupPolicy
    bp := &amp;storagev1alpha1.BackupPolicy{}
    if err := r.Get(ctx, req.NamespacedName, bp); err != nil {
        if apierrors.IsNotFound(err) {
            // Object deleted before we could reconcile — nothing to do.
            return ctrl.Result{}, nil
        }
        return ctrl.Result{}, fmt.Errorf(&quot;fetching BackupPolicy: %w&quot;, err)
    }

    // Step 2: Define the desired CronJob name
    cronJobName := fmt.Sprintf(&quot;%s-backup&quot;, bp.Name)

    // Step 3: Fetch the existing CronJob (if any)
    existing := &amp;batchv1.CronJob{}
    err := r.Get(ctx, types.NamespacedName{Name: cronJobName, Namespace: bp.Namespace}, existing)
    notFound := apierrors.IsNotFound(err)
    if err != nil &amp;&amp; !notFound {
        return ctrl.Result{}, fmt.Errorf(&quot;fetching CronJob: %w&quot;, err)
    }

    // Step 4: Build the desired CronJob
    desired := r.buildCronJob(bp, cronJobName)

    // Step 5: Create or update
    if notFound {
        logger.Info(&quot;Creating CronJob&quot;, &quot;name&quot;, cronJobName)
        if err := r.Create(ctx, desired); err != nil {
            return ctrl.Result{}, fmt.Errorf(&quot;creating CronJob: %w&quot;, err)
        }
    } else {
        // Update schedule and suspend state if they differ
        if existing.Spec.Schedule != desired.Spec.Schedule ||
            existing.Spec.Suspend != desired.Spec.Suspend {
            existing.Spec.Schedule = desired.Spec.Schedule
            existing.Spec.Suspend = desired.Spec.Suspend
            logger.Info(&quot;Updating CronJob&quot;, &quot;name&quot;, cronJobName)
            if err := r.Update(ctx, existing); err != nil {
                return ctrl.Result{}, fmt.Errorf(&quot;updating CronJob: %w&quot;, err)
            }
        }
    }

    // Step 6: Update status
    bpCopy := bp.DeepCopy()
    meta.SetStatusCondition(&amp;bpCopy.Status.Conditions, metav1.Condition{
        Type:               &quot;Ready&quot;,
        Status:             metav1.ConditionTrue,
        Reason:             &quot;CronJobReady&quot;,
        Message:            fmt.Sprintf(&quot;CronJob %s is configured&quot;, cronJobName),
        ObservedGeneration: bp.Generation,
    })
    bpCopy.Status.CronJobName = cronJobName

    if err := r.Status().Update(ctx, bpCopy); err != nil {
        return ctrl.Result{}, fmt.Errorf(&quot;updating status: %w&quot;, err)
    }

    return ctrl.Result{}, nil
}

func (r *BackupPolicyReconciler) buildCronJob(bp *storagev1alpha1.BackupPolicy, name string) *batchv1.CronJob {
    suspend := bp.Spec.Suspended
    retentionArg := fmt.Sprintf(&quot;--retention-days=%d&quot;, bp.Spec.RetentionDays)

    cj := &amp;batchv1.CronJob{
        ObjectMeta: metav1.ObjectMeta{
            Name:      name,
            Namespace: bp.Namespace,
            Labels: map[string]string{
                &quot;app.kubernetes.io/managed-by&quot;: &quot;backup-operator&quot;,
                &quot;backuppolicy&quot;:                 bp.Name,
            },
        },
        Spec: batchv1.CronJobSpec{
            Schedule: bp.Spec.Schedule,
            Suspend:  &amp;suspend,
            JobTemplate: batchv1.JobTemplateSpec{
                Spec: batchv1.JobSpec{
                    Template: corev1.PodTemplateSpec{
                        Spec: corev1.PodSpec{
                            RestartPolicy: corev1.RestartPolicyOnFailure,
                            Containers: []corev1.Container{
                                {
                                    Name:    &quot;backup&quot;,
                                    Image:   &quot;backup-tool:latest&quot;,
                                    Args:    []string{retentionArg},
                                },
                            },
                        },
                    },
                },
            },
        },
    }

    // Set owner reference — CronJob is garbage-collected when BackupPolicy is deleted
    _ = ctrl.SetControllerReference(bp, cj, r.Scheme)
    return cj
}

// SetupWithManager registers the controller with the manager and declares what to watch.
func (r *BackupPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&amp;storagev1alpha1.BackupPolicy{}).
        Owns(&amp;batchv1.CronJob{}).    // reconcile BackupPolicy when owned CronJob changes
        Complete(r)
}
</code></pre>
<hr />
<h2 id="step-4-install-the-crd-and-run-locally">Step 4: Install the CRD and Run Locally</h2>
<pre><code class="" data-line=""># Install the CRD into the cluster
make install
</code></pre>
<pre><code class="" data-line="">customresourcedefinition.apiextensions.k8s.io/backuppolicies.storage.example.com created
</code></pre>
<pre><code class="" data-line=""># Run the controller locally (outside the cluster)
make run
</code></pre>
<pre><code class="" data-line="">2026-04-25T08:00:00Z  INFO  Starting manager
2026-04-25T08:00:00Z  INFO  Starting workers  {&quot;controller&quot;: &quot;backuppolicy&quot;, &quot;worker count&quot;: 1}
</code></pre>
<p>In a separate terminal:</p>
<pre><code class="" data-line="">kubectl apply -f - &lt;&lt;&#039;EOF&#039;
apiVersion: storage.example.com/v1alpha1
kind: BackupPolicy
metadata:
  name: nightly
  namespace: default
spec:
  schedule: &quot;0 2 * * *&quot;
  retentionDays: 30
EOF
</code></pre>
<p>Watch the controller output:</p>
<pre><code class="" data-line="">2026-04-25T08:01:00Z  INFO  Creating CronJob  {&quot;name&quot;: &quot;nightly-backup&quot;}
</code></pre>
<p>Check the result:</p>
<pre><code class="" data-line="">kubectl get bp nightly
</code></pre>
<pre><code class="" data-line="">NAME      SCHEDULE    RETENTION   SUSPENDED   READY   AGE
nightly   0 2 * * *   30          false       True    10s
</code></pre>
<pre><code class="" data-line="">kubectl get cronjob nightly-backup
</code></pre>
<pre><code class="" data-line="">NAME             SCHEDULE    SUSPEND   ACTIVE   LAST SCHEDULE   AGE
nightly-backup   0 2 * * *   False     0        &lt;none&gt;          10s
</code></pre>
<p>Test self-healing — delete the CronJob and watch the controller recreate it:</p>
<pre><code class="" data-line="">kubectl delete cronjob nightly-backup
# Controller output:
# 2026-04-25T08:02:00Z  INFO  Creating CronJob  {&quot;name&quot;: &quot;nightly-backup&quot;}

kubectl get cronjob nightly-backup
# Back within seconds
</code></pre>
<p>Test suspend:</p>
<pre><code class="" data-line="">kubectl patch bp nightly --type=merge -p &#039;{&quot;spec&quot;:{&quot;suspended&quot;:true}}&#039;
kubectl get cronjob nightly-backup -o jsonpath=&#039;{.spec.suspend}&#039;
# true
</code></pre>
<hr />
<h2 id="step-5-deploy-to-cluster">Step 5: Deploy to Cluster</h2>
<p>When ready for in-cluster deployment:</p>
<pre><code class="" data-line=""># Build and push the controller image
make docker-build docker-push IMG=your-registry/backup-operator:v0.1.0

# Deploy to cluster (creates Deployment, RBAC, CRD)
make deploy IMG=your-registry/backup-operator:v0.1.0
</code></pre>
<pre><code class="" data-line="">kubectl get pods -n backup-operator-system
</code></pre>
<pre><code class="" data-line="">NAME                                          READY   STATUS    RESTARTS   AGE
backup-operator-controller-manager-abc123     2/2     Running   0          30s
</code></pre>
<hr />
<h2 id="understanding-the-rbac-markers">Understanding the RBAC Markers</h2>
<p>The <code class="" data-line="">//+kubebuilder:rbac:...</code> comments in the controller generate the ClusterRole YAML when you run <code class="" data-line="">make manifests</code>:</p>
<pre><code class="" data-line="">//+kubebuilder:rbac:groups=storage.example.com,resources=backuppolicies,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=storage.example.com,resources=backuppolicies/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete
</code></pre>
<p>Generated YAML under <code class="" data-line="">config/rbac/role.yaml</code>:</p>
<pre><code class="" data-line="">apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: manager-role
rules:
  - apiGroups: [&quot;storage.example.com&quot;]
    resources: [&quot;backuppolicies&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;update&quot;, &quot;patch&quot;, &quot;delete&quot;]
  - apiGroups: [&quot;storage.example.com&quot;]
    resources: [&quot;backuppolicies/status&quot;]
    verbs: [&quot;get&quot;, &quot;update&quot;, &quot;patch&quot;]
  - apiGroups: [&quot;batch&quot;]
    resources: [&quot;cronjobs&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;update&quot;, &quot;patch&quot;, &quot;delete&quot;]
</code></pre>
<p>This approach keeps RBAC co-located with the code that needs it — if you add a new resource access in the controller, you add the marker next to it.</p>
<hr />
<h2 id="common-mistakes"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Common Mistakes</h2>
<p><strong>Not setting an owner reference on child resources.</strong> Without <code class="" data-line="">ctrl.SetControllerReference(parent, child, scheme)</code>, deleting the <code class="" data-line="">BackupPolicy</code> leaves orphaned CronJobs. Owner references enable automatic garbage collection of child resources.</p>
<p><strong>Updating the object after <code class="" data-line="">r.Get()</code> without handling conflicts.</strong> If two reconciles run concurrently (possible after a controller restart), both may try to update the same resource. The API server uses resource version for optimistic concurrency — you will get a conflict error. Retry the reconcile on conflict errors rather than failing.</p>
<p><strong>Writing to <code class="" data-line="">bp</code> directly instead of <code class="" data-line="">bp.DeepCopy()</code> for status updates.</strong> If the status update fails and you retry, the original <code class="" data-line="">bp</code> object now has the modified status in memory. Always update a deep copy when writing status so the in-memory state stays consistent with what was actually persisted.</p>
<p><strong>Not watching owned resources.</strong> If you forget <code class="" data-line="">.Owns(&amp;batchv1.CronJob{})</code> in <code class="" data-line="">SetupWithManager</code>, the controller will not reconcile when a CronJob is deleted. Self-healing requires watching the resources you manage.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line=""># Scaffold a new API + controller
kubebuilder create api --group mygroup --version v1alpha1 --kind MyKind

# Regenerate deep copy methods after changing types
make generate

# Regenerate CRD YAML + RBAC from markers
make manifests

# Install CRD into current cluster
make install

# Run controller locally (outside cluster)
make run

# Build + push image, then deploy to cluster
make docker-build docker-push IMG=registry/operator:tag
make deploy IMG=registry/operator:tag

# Uninstall CRD (WARNING: deletes all instances)
make uninstall
</code></pre>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>kubebuilder scaffolds the project; you write the types and the reconcile function</li>
<li>Go struct markers (<code class="" data-line="">//+kubebuilder:...</code>) generate the CRD YAML and RBAC — keep them close to the code they describe</li>
<li><code class="" data-line="">ctrl.SetControllerReference</code> enables automatic garbage collection of child resources</li>
<li>Always deep-copy the object before writing status; retry on conflict errors</li>
<li><code class="" data-line="">make run</code> runs the controller locally — no Docker build needed during development</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p><a href="/kubernetes-crd-versioning-conversion-webhook/">EP08: Kubernetes CRD Versioning</a> covers how to evolve the <code class="" data-line="">BackupPolicy</code> schema from <code class="" data-line="">v1alpha1</code> to <code class="" data-line="">v1</code> without breaking existing clients — storage versions, conversion webhooks, and the hub-and-spoke model for safe API evolution in production clusters.</p>
<p>Get EP08 in your inbox when it publishes → <a href="https://linuxcent.com">subscribe at linuxcent.com</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fbuild-kubernetes-operator-controller-runtime%2F&amp;linkname=Build%20a%20Simple%20Kubernetes%20Operator%20with%20controller-runtime%20and%20kubebuilder" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fbuild-kubernetes-operator-controller-runtime%2F&amp;linkname=Build%20a%20Simple%20Kubernetes%20Operator%20with%20controller-runtime%20and%20kubebuilder" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fbuild-kubernetes-operator-controller-runtime%2F&amp;linkname=Build%20a%20Simple%20Kubernetes%20Operator%20with%20controller-runtime%20and%20kubebuilder" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fbuild-kubernetes-operator-controller-runtime%2F&amp;linkname=Build%20a%20Simple%20Kubernetes%20Operator%20with%20controller-runtime%20and%20kubebuilder" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fbuild-kubernetes-operator-controller-runtime%2F&amp;linkname=Build%20a%20Simple%20Kubernetes%20Operator%20with%20controller-runtime%20and%20kubebuilder" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fbuild-kubernetes-operator-controller-runtime%2F&amp;linkname=Build%20a%20Simple%20Kubernetes%20Operator%20with%20controller-runtime%20and%20kubebuilder" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fbuild-kubernetes-operator-controller-runtime%2F&amp;linkname=Build%20a%20Simple%20Kubernetes%20Operator%20with%20controller-runtime%20and%20kubebuilder" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fbuild-kubernetes-operator-controller-runtime%2F&#038;title=Build%20a%20Simple%20Kubernetes%20Operator%20with%20controller-runtime%20and%20kubebuilder" data-a2a-url="https://linuxcent.com/build-kubernetes-operator-controller-runtime/" data-a2a-title="Build a Simple Kubernetes Operator with controller-runtime and kubebuilder"></a></p><p>The post <a href="https://linuxcent.com/build-kubernetes-operator-controller-runtime/">Build a Simple Kubernetes Operator with controller-runtime and kubebuilder</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/build-kubernetes-operator-controller-runtime/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1693</post-id>	</item>
		<item>
		<title>Write Your First Kubernetes CRD: A Hands-On YAML Walkthrough</title>
		<link>https://linuxcent.com/write-kubernetes-crd-yaml-walkthrough/</link>
					<comments>https://linuxcent.com/write-kubernetes-crd-yaml-walkthrough/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Sat, 25 Apr 2026 18:15:37 +0000</pubDate>
				<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[CRD]]></category>
		<category><![CDATA[Custom Resource]]></category>
		<category><![CDATA[Hands-On Tutorial]]></category>
		<category><![CDATA[kubectl]]></category>
		<category><![CDATA[Platform Engineering]]></category>
		<category><![CDATA[YAML]]></category>
		<guid isPermaLink="false">https://linuxcent.com/write-kubernetes-crd-yaml-walkthrough/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span>Write a Kubernetes CRD from scratch — complete YAML walkthrough with schema, RBAC, kubectl apply, and custom resource operations on a real cluster.</p>
<p>The post <a href="https://linuxcent.com/write-kubernetes-crd-yaml-walkthrough/">Write Your First Kubernetes CRD: A Hands-On YAML Walkthrough</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>Kubernetes CRDs &amp; Operators: Extending the API, Episode 4</em><br />
<em><a href="/what-is-kubernetes-crd/">What Is a CRD?</a> · <a href="/kubernetes-custom-resources-examples/">CRDs You Already Use</a> · <a href="/kubernetes-crd-schema-explained/">CRD Anatomy</a> · </em><em><a href="/write-kubernetes-crd-yaml-walkthrough/">Write Your First CRD</a></em><em> · <a href="/kubernetes-crd-cel-validation/">CEL Validation</a> · <a href="/kubernetes-controller-reconcile-loop/">Controller Loop</a> · <a href="/build-kubernetes-operator-controller-runtime/">Build an Operator</a> · <a href="/kubernetes-crd-versioning-conversion-webhook/">CRD Versioning</a> · <a href="/kubernetes-admission-webhooks-explained/">Admission Webhooks</a> · <a href="/kubernetes-crd-production-finalizers-conditions-rbac/">CRDs in Production</a></em></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Writing a Kubernetes CRD requires five YAML files: the CRD itself, a ClusterRole/ClusterRoleBinding, a namespaced Role/RoleBinding for consumers, and a sample custom resource</li>
<li>The <code class="" data-line="">BackupPolicy</code> CRD built in this episode is the running example throughout the rest of the series — operators, versioning, and production patterns all use it</li>
<li>Apply the CRD, verify it with <code class="" data-line="">kubectl get crds</code>, create a custom resource, and watch the API server validate your spec</li>
<li>RBAC for CRDs follows the same Role/ClusterRole model as built-in resources — the generated resource name is <code class="" data-line="">{plural}.{group}</code></li>
<li>Schema validation fires at apply time: bad field types, missing required fields, and out-of-range values all return clear errors before anything reaches etcd</li>
<li>Without a controller, a <code class="" data-line="">BackupPolicy</code> is stored in etcd but nothing acts on it — that is the topic of EP05 and EP07</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">  WHAT WE&#039;RE BUILDING IN THIS EPISODE

  1. backuppolicies-crd.yaml        ← registers the BackupPolicy type
  2. backuppolicies-rbac.yaml       ← controls who can create/view/delete
  3. nightly-backup.yaml            ← our first custom resource instance

  After applying:

  kubectl get crds | grep backup      ← BackupPolicy type exists
  kubectl get backuppolicies -n demo  ← nightly instance exists
  kubectl describe bp nightly -n demo ← spec visible, status empty
  kubectl apply -f bad-backup.yaml    ← schema validation rejects bad data
</code></pre>
<p>Writing your first Kubernetes CRD is the step that bridges understanding CRDs conceptually to operating them in a real cluster. This episode is hands-on — every block of YAML is something you apply and verify.</p>
<hr />
<h2 id="prerequisites">Prerequisites</h2>
<p>You need a running Kubernetes cluster and <code class="" data-line="">kubectl</code> configured. Any of these work:</p>
<pre><code class="" data-line=""># Local options
kind create cluster --name crd-demo
# or
minikube start

# Verify cluster access
kubectl cluster-info
kubectl get nodes
</code></pre>
<hr />
<h2 id="step-1-write-the-crd">Step 1: Write the CRD</h2>
<p>Save this as <code class="" data-line="">backuppolicies-crd.yaml</code>:</p>
<pre><code class="" data-line="">apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: backuppolicies.storage.example.com
spec:
  group: storage.example.com
  scope: Namespaced
  names:
    plural:     backuppolicies
    singular:   backuppolicy
    kind:       BackupPolicy
    shortNames:
      - bp
    categories:
      - storage
  versions:
    - name: v1alpha1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          required: [&quot;spec&quot;]
          properties:
            spec:
              type: object
              required: [&quot;schedule&quot;, &quot;retentionDays&quot;]
              properties:
                schedule:
                  type: string
                  description: &quot;Cron expression (e.g. &#039;0 2 * * *&#039; for 02:00 daily)&quot;
                retentionDays:
                  type: integer
                  minimum: 1
                  maximum: 365
                  description: &quot;How many days to retain backup snapshots&quot;
                storageClass:
                  type: string
                  default: &quot;standard&quot;
                  description: &quot;StorageClass to use for backup volumes&quot;
                targets:
                  type: array
                  description: &quot;Namespaces and resources to include in the backup&quot;
                  maxItems: 20
                  items:
                    type: object
                    required: [&quot;namespace&quot;]
                    properties:
                      namespace:
                        type: string
                      includeSecrets:
                        type: boolean
                        default: false
                suspended:
                  type: boolean
                  default: false
                  description: &quot;Set to true to pause backup execution&quot;
            status:
              type: object
              x-kubernetes-preserve-unknown-fields: true
      subresources:
        status: {}
      additionalPrinterColumns:
        - name: Schedule
          type: string
          jsonPath: .spec.schedule
        - name: Retention
          type: integer
          jsonPath: .spec.retentionDays
        - name: Suspended
          type: boolean
          jsonPath: .spec.suspended
        - name: Ready
          type: string
          jsonPath: .status.conditions[?(@.type==&#039;Ready&#039;)].status
        - name: Age
          type: date
          jsonPath: .metadata.creationTimestamp
</code></pre>
<p>Apply it:</p>
<pre><code class="" data-line="">kubectl apply -f backuppolicies-crd.yaml
</code></pre>
<p>Verify it registered correctly:</p>
<pre><code class="" data-line="">kubectl get crds backuppolicies.storage.example.com
</code></pre>
<pre><code class="" data-line="">NAME                                    CREATED AT
backuppolicies.storage.example.com      2026-04-25T08:00:00Z
</code></pre>
<p>Check the API server now knows about it:</p>
<pre><code class="" data-line="">kubectl api-resources | grep backuppolic
</code></pre>
<pre><code class="" data-line="">backuppolicies    bp    storage.example.com/v1alpha1    true    BackupPolicy
</code></pre>
<p>Check it is <code class="" data-line="">Established</code>:</p>
<pre><code class="" data-line="">kubectl get crd backuppolicies.storage.example.com \
  -o jsonpath=&#039;{.status.conditions[?(@.type==&quot;Established&quot;)].status}&#039;
</code></pre>
<pre><code class="" data-line="">True
</code></pre>
<p>If you see <code class="" data-line="">False</code> or empty output, wait a few seconds and retry — the API server takes a moment to register new CRDs.</p>
<hr />
<h2 id="step-2-write-rbac">Step 2: Write RBAC</h2>
<p>CRDs follow the same RBAC model as built-in resources. The resource name is <code class="" data-line="">{plural}.{group}</code>.</p>
<p>Save this as <code class="" data-line="">backuppolicies-rbac.yaml</code>:</p>
<pre><code class="" data-line=""># ClusterRole for operators/controllers that manage BackupPolicy objects
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: backuppolicy-controller
rules:
  - apiGroups: [&quot;storage.example.com&quot;]
    resources: [&quot;backuppolicies&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;update&quot;, &quot;patch&quot;, &quot;delete&quot;]
  - apiGroups: [&quot;storage.example.com&quot;]
    resources: [&quot;backuppolicies/status&quot;]
    verbs: [&quot;get&quot;, &quot;update&quot;, &quot;patch&quot;]
  - apiGroups: [&quot;storage.example.com&quot;]
    resources: [&quot;backuppolicies/finalizers&quot;]
    verbs: [&quot;update&quot;]
---
# Role for application teams to manage BackupPolicies in their namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: backuppolicy-editor
rules:
  - apiGroups: [&quot;storage.example.com&quot;]
    resources: [&quot;backuppolicies&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;update&quot;, &quot;patch&quot;, &quot;delete&quot;]
---
# Read-only role for auditors
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: backuppolicy-viewer
rules:
  - apiGroups: [&quot;storage.example.com&quot;]
    resources: [&quot;backuppolicies&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]
</code></pre>
<pre><code class="" data-line="">kubectl apply -f backuppolicies-rbac.yaml
</code></pre>
<p>Verify the roles exist:</p>
<pre><code class="" data-line="">kubectl get clusterrole | grep backuppolicy
</code></pre>
<pre><code class="" data-line="">backuppolicy-controller   2026-04-25T08:01:00Z
backuppolicy-editor       2026-04-25T08:01:00Z
backuppolicy-viewer       2026-04-25T08:01:00Z
</code></pre>
<blockquote>
<p><strong>Note on <code class="" data-line="">backuppolicies/status</code>:</strong> The separate status RBAC rule is only meaningful if you enabled the status subresource (we did). Without it, status and spec share the same update path.</p>
</blockquote>
<hr />
<h2 id="step-3-create-a-namespace-and-your-first-custom-resource">Step 3: Create a Namespace and Your First Custom Resource</h2>
<pre><code class="" data-line="">kubectl create namespace demo
</code></pre>
<p>Save this as <code class="" data-line="">nightly-backup.yaml</code>:</p>
<pre><code class="" data-line="">apiVersion: storage.example.com/v1alpha1
kind: BackupPolicy
metadata:
  name: nightly
  namespace: demo
  labels:
    app.kubernetes.io/managed-by: manual
spec:
  schedule: &quot;0 2 * * *&quot;
  retentionDays: 30
  storageClass: standard
  targets:
    - namespace: production
      includeSecrets: false
    - namespace: staging
      includeSecrets: false
  suspended: false
</code></pre>
<p>Apply it:</p>
<pre><code class="" data-line="">kubectl apply -f nightly-backup.yaml
</code></pre>
<p>Get it back:</p>
<pre><code class="" data-line="">kubectl get backuppolicies -n demo
</code></pre>
<pre><code class="" data-line="">NAME      SCHEDULE    RETENTION   SUSPENDED   READY   AGE
nightly   0 2 * * *   30          false       &lt;none&gt;  5s
</code></pre>
<p>The <code class="" data-line="">Ready</code> column is <code class="" data-line="">&lt;none&gt;</code> because there is no controller writing status yet. The custom resource exists and is stored in etcd, but nothing is acting on it.</p>
<p>Describe it:</p>
<pre><code class="" data-line="">kubectl describe bp nightly -n demo
</code></pre>
<pre><code class="" data-line="">Name:         nightly
Namespace:    demo
Labels:       app.kubernetes.io/managed-by=manual
Annotations:  &lt;none&gt;
API Version:  storage.example.com/v1alpha1
Kind:         BackupPolicy
Metadata:
  Creation Timestamp:  2026-04-25T08:05:00Z
  ...
Spec:
  Retention Days:  30
  Schedule:        0 2 * * *
  Storage Class:   standard
  Suspended:       false
  Targets:
    Include Secrets:  false
    Namespace:        production
    Include Secrets:  false
    Namespace:        staging
Status:
Events:  &lt;none&gt;
</code></pre>
<hr />
<h2 id="step-4-test-schema-validation">Step 4: Test Schema Validation</h2>
<p>The API server now validates every <code class="" data-line="">BackupPolicy</code> against the schema. Try creating an invalid one:</p>
<pre><code class="" data-line="">kubectl apply -f - &lt;&lt;&#039;EOF&#039;
apiVersion: storage.example.com/v1alpha1
kind: BackupPolicy
metadata:
  name: bad-policy
  namespace: demo
spec:
  schedule: &quot;not-a-cron&quot;
  retentionDays: 500
EOF
</code></pre>
<pre><code class="" data-line="">The BackupPolicy &quot;bad-policy&quot; is invalid:
  spec.retentionDays: Invalid value: 500:
    spec.retentionDays in body should be less than or equal to 365
</code></pre>
<p>Missing required field:</p>
<pre><code class="" data-line="">kubectl apply -f - &lt;&lt;&#039;EOF&#039;
apiVersion: storage.example.com/v1alpha1
kind: BackupPolicy
metadata:
  name: missing-schedule
  namespace: demo
spec:
  retentionDays: 7
EOF
</code></pre>
<pre><code class="" data-line="">The BackupPolicy &quot;missing-schedule&quot; is invalid:
  spec.schedule: Required value
</code></pre>
<p>Wrong type:</p>
<pre><code class="" data-line="">kubectl apply -f - &lt;&lt;&#039;EOF&#039;
apiVersion: storage.example.com/v1alpha1
kind: BackupPolicy
metadata:
  name: wrong-type
  namespace: demo
spec:
  schedule: &quot;0 2 * * *&quot;
  retentionDays: &quot;thirty&quot;
EOF
</code></pre>
<pre><code class="" data-line="">The BackupPolicy &quot;wrong-type&quot; is invalid:
  spec.retentionDays: Invalid value: &quot;string&quot;:
    spec.retentionDays in body must be of type integer: &quot;string&quot;
</code></pre>
<p>All validation fires at the API boundary — before etcd, before any controller sees the object.</p>
<hr />
<h2 id="step-5-verify-default-values-apply">Step 5: Verify Default Values Apply</h2>
<p>The schema defines <code class="" data-line="">storageClass: default: &quot;standard&quot;</code> and <code class="" data-line="">suspended: default: false</code>. Verify they are applied even when not specified:</p>
<pre><code class="" data-line="">kubectl apply -f - &lt;&lt;&#039;EOF&#039;
apiVersion: storage.example.com/v1alpha1
kind: BackupPolicy
metadata:
  name: minimal
  namespace: demo
spec:
  schedule: &quot;0 0 * * 0&quot;
  retentionDays: 7
EOF

kubectl get bp minimal -n demo -o jsonpath=&#039;{.spec.storageClass}&#039;
</code></pre>
<pre><code class="" data-line="">standard
</code></pre>
<pre><code class="" data-line="">kubectl get bp minimal -n demo -o jsonpath=&#039;{.spec.suspended}&#039;
</code></pre>
<pre><code class="" data-line="">false
</code></pre>
<p>Defaults are injected by the API server at admission time. They appear in etcd and in every <code class="" data-line="">kubectl get -o yaml</code> output — the stored object includes the defaults even if the user did not specify them.</p>
<hr />
<h2 id="step-6-explore-the-api-endpoints">Step 6: Explore the API Endpoints</h2>
<p>Your custom resource is now available at standard REST endpoints:</p>
<pre><code class="" data-line="">kubectl proxy --port=8001 &amp;

# List all BackupPolicies in the demo namespace
curl -s http://localhost:8001/apis/storage.example.com/v1alpha1/namespaces/demo/backuppolicies \
  | jq &#039;.items[].metadata.name&#039;
</code></pre>
<pre><code class="" data-line="">&quot;nightly&quot;
&quot;minimal&quot;
</code></pre>
<pre><code class="" data-line=""># Get a specific BackupPolicy
curl -s http://localhost:8001/apis/storage.example.com/v1alpha1/namespaces/demo/backuppolicies/nightly \
  | jq &#039;.spec&#039;
</code></pre>
<p>This is how controllers discover and watch custom resources — via the same API server endpoints, using informers that wrap these REST calls with efficient list-and-watch semantics.</p>
<hr />
<h2 id="step-7-clean-up">Step 7: Clean Up</h2>
<pre><code class="" data-line="">kubectl delete namespace demo
kubectl delete -f backuppolicies-rbac.yaml
kubectl delete -f backuppolicies-crd.yaml   # WARNING: deletes all BackupPolicy instances first
</code></pre>
<hr />
<h2 id="common-mistakes"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Common Mistakes</h2>
<p><strong><code class="" data-line="">metadata.name</code> does not match <code class="" data-line="">{plural}.{group}</code>.</strong> The most common error. If you name the CRD <code class="" data-line="">backuppolicy.storage.example.com</code> (singular) but the spec says <code class="" data-line="">plural: backuppolicies</code>, the API server rejects it. The name must always be <code class="" data-line="">{plural}.{group}</code>.</p>
<p><strong>No <code class="" data-line="">required</code> fields on spec.</strong> Without required constraints, <code class="" data-line="">kubectl apply</code> accepts an empty <code class="" data-line="">spec: {}</code>. The controller then receives objects with no configuration and has to handle the nil case. Define required fields in the schema.</p>
<p><strong>Forgetting <code class="" data-line="">subresources: status: {}</code>.</strong> Without this, controllers writing <code class="" data-line="">.status</code> also overwrite <code class="" data-line="">.spec</code> on full PUT updates. This causes status updates to reset user edits. Enable the status subresource from day one.</p>
<p><strong>Not testing validation errors.</strong> Schema validation is the first line of defense. Always explicitly test that your required fields are required, types are enforced, and range constraints work — before deploying the controller.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line=""># All kubectl operations work on custom resources
kubectl get      backuppolicies -n demo
kubectl get      bp -n demo                  # shortName
kubectl describe bp nightly -n demo
kubectl edit     bp nightly -n demo
kubectl delete   bp nightly -n demo

# Output formats
kubectl get bp -n demo -o yaml
kubectl get bp -n demo -o json
kubectl get bp -n demo -o jsonpath=&#039;{.items[*].metadata.name}&#039;

# Watch for changes
kubectl get bp -n demo -w

# List across all namespaces
kubectl get bp -A

# Patch spec
kubectl patch bp nightly -n demo \
  --type=merge -p &#039;{&quot;spec&quot;:{&quot;suspended&quot;:true}}&#039;
</code></pre>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>A working CRD deployment needs: the CRD YAML, RBAC ClusterRoles, and at least one sample custom resource</li>
<li>The API server validates all custom resources against the schema at apply time — errors are surfaced immediately, not inside the controller</li>
<li>Default values in the schema are injected at admission time and appear in every stored object</li>
<li>RBAC for custom resources uses <code class="" data-line="">{plural}.{group}</code> as the resource name — <code class="" data-line="">status</code> and <code class="" data-line="">finalizers</code> are separate sub-resources</li>
<li>Without a controller, custom resources are stored in etcd and serve as validated configuration — nothing acts on them until a controller is deployed</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p><a href="/kubernetes-crd-cel-validation/">EP05: Kubernetes CRD CEL Validation</a> extends schema validation beyond simple type and range checks — cross-field rules (&#8220;if <code class="" data-line="">storageClass</code> is <code class="" data-line="">premium</code>, <code class="" data-line="">retentionDays</code> must be at most 90&#8243;), regex validation beyond <code class="" data-line="">pattern</code>, and immutable field enforcement. All without an admission webhook.</p>
<p>Get EP05 in your inbox when it publishes → <a href="https://linuxcent.com">subscribe at linuxcent.com</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fwrite-kubernetes-crd-yaml-walkthrough%2F&amp;linkname=Write%20Your%20First%20Kubernetes%20CRD%3A%20A%20Hands-On%20YAML%20Walkthrough" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fwrite-kubernetes-crd-yaml-walkthrough%2F&amp;linkname=Write%20Your%20First%20Kubernetes%20CRD%3A%20A%20Hands-On%20YAML%20Walkthrough" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fwrite-kubernetes-crd-yaml-walkthrough%2F&amp;linkname=Write%20Your%20First%20Kubernetes%20CRD%3A%20A%20Hands-On%20YAML%20Walkthrough" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fwrite-kubernetes-crd-yaml-walkthrough%2F&amp;linkname=Write%20Your%20First%20Kubernetes%20CRD%3A%20A%20Hands-On%20YAML%20Walkthrough" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fwrite-kubernetes-crd-yaml-walkthrough%2F&amp;linkname=Write%20Your%20First%20Kubernetes%20CRD%3A%20A%20Hands-On%20YAML%20Walkthrough" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fwrite-kubernetes-crd-yaml-walkthrough%2F&amp;linkname=Write%20Your%20First%20Kubernetes%20CRD%3A%20A%20Hands-On%20YAML%20Walkthrough" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fwrite-kubernetes-crd-yaml-walkthrough%2F&amp;linkname=Write%20Your%20First%20Kubernetes%20CRD%3A%20A%20Hands-On%20YAML%20Walkthrough" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fwrite-kubernetes-crd-yaml-walkthrough%2F&#038;title=Write%20Your%20First%20Kubernetes%20CRD%3A%20A%20Hands-On%20YAML%20Walkthrough" data-a2a-url="https://linuxcent.com/write-kubernetes-crd-yaml-walkthrough/" data-a2a-title="Write Your First Kubernetes CRD: A Hands-On YAML Walkthrough"></a></p><p>The post <a href="https://linuxcent.com/write-kubernetes-crd-yaml-walkthrough/">Write Your First Kubernetes CRD: A Hands-On YAML Walkthrough</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/write-kubernetes-crd-yaml-walkthrough/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1684</post-id>	</item>
		<item>
		<title>Kubernetes CRD Schema Explained: Versions, Validation, and Status Subresource</title>
		<link>https://linuxcent.com/kubernetes-crd-schema-explained/</link>
					<comments>https://linuxcent.com/kubernetes-crd-schema-explained/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Sat, 25 Apr 2026 18:15:21 +0000</pubDate>
				<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[CRD]]></category>
		<category><![CDATA[CRD Schema]]></category>
		<category><![CDATA[OpenAPI]]></category>
		<category><![CDATA[Platform Engineering]]></category>
		<category><![CDATA[spec.versions]]></category>
		<category><![CDATA[Status Subresource]]></category>
		<guid isPermaLink="false">https://linuxcent.com/kubernetes-crd-schema-explained/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span>Understand Kubernetes CRD anatomy: spec.versions, OpenAPI schema validation, scope, and the status subresource — what each YAML field does and why it matters in production.</p>
<p>The post <a href="https://linuxcent.com/kubernetes-crd-schema-explained/">Kubernetes CRD Schema Explained: Versions, Validation, and Status Subresource</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>Kubernetes CRDs &amp; Operators: Extending the API, Episode 3</em><br />
<em><a href="/what-is-kubernetes-crd/">What Is a CRD?</a> · <a href="/kubernetes-custom-resources-examples/">CRDs You Already Use</a> · </em><em><a href="/kubernetes-crd-schema-explained/">CRD Anatomy</a></em><em> · <a href="/write-kubernetes-crd-yaml-walkthrough/">Write Your First CRD</a> · <a href="/kubernetes-crd-cel-validation/">CEL Validation</a> · <a href="/kubernetes-controller-reconcile-loop/">Controller Loop</a> · <a href="/build-kubernetes-operator-controller-runtime/">Build an Operator</a> · <a href="/kubernetes-crd-versioning-conversion-webhook/">CRD Versioning</a> · <a href="/kubernetes-admission-webhooks-explained/">Admission Webhooks</a> · <a href="/kubernetes-crd-production-finalizers-conditions-rbac/">CRDs in Production</a></em></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>The Kubernetes CRD schema is defined in <code class="" data-line="">spec.versions[].schema.openAPIV3Schema</code> — the API server uses it to validate every custom resource create and update before storing in etcd<br />
  <em>(OpenAPI v3 schema = a JSON Schema dialect that describes the structure, types, and constraints of your resource&#8217;s fields)</em></li>
<li><code class="" data-line="">spec.versions</code> is a list — CRDs can serve multiple API versions simultaneously; exactly one version must have <code class="" data-line="">storage: true</code></li>
<li><code class="" data-line="">scope: Namespaced</code> vs <code class="" data-line="">scope: Cluster</code> controls whether custom resources live inside a namespace or at cluster level (like <code class="" data-line="">PersistentVolume</code> vs <code class="" data-line="">PersistentVolumeClaim</code>)</li>
<li><code class="" data-line="">spec.names</code> defines the plural, singular, kind, and optional shortNames used in <code class="" data-line="">kubectl</code> and RBAC</li>
<li>The <strong>status subresource</strong> (<code class="" data-line="">subresources.status: {}</code>) separates user writes (<code class="" data-line="">spec</code>) from controller writes (<code class="" data-line="">status</code>) — enabling optimistic concurrency and <code class="" data-line="">kubectl status</code> support</li>
<li>The <strong>scale subresource</strong> (<code class="" data-line="">subresources.scale</code>) makes your custom resource compatible with <code class="" data-line="">kubectl scale</code> and the HorizontalPodAutoscaler</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">  ANATOMY OF A CUSTOMRESOURCEDEFINITION

  apiVersion: apiextensions.k8s.io/v1
  kind: CustomResourceDefinition
  metadata:
    name: {plural}.{group}        ← MUST be exactly this format
  spec:
    group: {group}                ← API group (e.g. storage.example.com)
    scope: Namespaced | Cluster   ← where instances live
    names:                        ← how kubectl refers to this resource
      plural: backuppolicies
      singular: backuppolicy
      kind: BackupPolicy
      shortNames: [bp]
    versions:                     ← can be a list; one must have storage: true
      - name: v1alpha1
        served: true              ← API server responds to this version
        storage: true             ← etcd stores objects in this version
        schema:
          openAPIV3Schema:        ← validation schema for ALL objects of this type
            type: object
            properties:
              spec: {...}
              status: {...}
        subresources:
          status: {}              ← enables separate status write path
          scale:                  ← enables kubectl scale + HPA
            specReplicasPath: .spec.replicas
            statusReplicasPath: .status.replicas
        additionalPrinterColumns: ← extra columns in kubectl get output
          - name: Schedule
            type: string
            jsonPath: .spec.schedule
</code></pre>
<p>Understanding the Kubernetes CRD schema is the prerequisite for writing a CRD that behaves correctly in production — validation catches bad data at the API boundary, the status subresource prevents controller race conditions, and scope determines your entire RBAC and multi-tenancy model.</p>
<hr />
<h2 id="specgroup-and-metadataname"><code class="" data-line="">spec.group</code> and <code class="" data-line="">metadata.name</code></h2>
<p>The group is a reverse-DNS identifier for your API. Convention:</p>
<pre><code class="" data-line="">storage.example.com     ← domain you control + functional area
monitoring.myteam.io
databases.platform.company.com
</code></pre>
<p>The CRD&#8217;s <code class="" data-line="">metadata.name</code> must be exactly <code class="" data-line="">{plural}.{group}</code>:</p>
<pre><code class="" data-line="">metadata:
  name: backuppolicies.storage.example.com
spec:
  group: storage.example.com
  names:
    plural: backuppolicies
</code></pre>
<p>If these do not match, the API server rejects the CRD with a validation error. This is the most common first-timer mistake.</p>
<hr />
<h2 id="specscope-namespaced-vs-cluster"><code class="" data-line="">spec.scope</code>: Namespaced vs Cluster</h2>
<pre><code class="" data-line="">  SCOPE DETERMINES WHERE INSTANCES LIVE

  Namespaced (scope: Namespaced)       Cluster (scope: Cluster)
  ─────────────────────────────         ──────────────────────────
  kubectl get backuppolicies -n prod    kubectl get clusterbackuppolicies
  kubectl get backuppolicies -A         (no -n flag, no namespace)

  Analogous to: Pod, Deployment,        Analogous to: PersistentVolume,
                ConfigMap                             ClusterRole, Node
</code></pre>
<p><strong>Namespaced</strong>: Use when instances are per-tenant or per-application. Users with namespace-scoped RBAC can manage their own instances without cluster-admin. Most CRDs should be namespaced.</p>
<p><strong>Cluster-scoped</strong>: Use when instances represent cluster-wide configuration — a <code class="" data-line="">ClusterIssuer</code> (cert-manager), <code class="" data-line="">ClusterSecretStore</code> (ESO), a <code class="" data-line="">StorageClass</code>-like concept. Requires cluster-level RBAC to create/modify.</p>
<p>You cannot change scope after a CRD is created without deleting and recreating it (which deletes all instances). Choose carefully.</p>
<hr />
<h2 id="specversions-serving-multiple-api-versions"><code class="" data-line="">spec.versions</code>: Serving Multiple API Versions</h2>
<pre><code class="" data-line="">spec:
  versions:
    - name: v1alpha1
      served: true
      storage: false       # not stored; converted on read
      schema:
        openAPIV3Schema: {...}
    - name: v1beta1
      served: true
      storage: false
      schema:
        openAPIV3Schema: {...}
    - name: v1
      served: true
      storage: true        # etcd stores in this version
      schema:
        openAPIV3Schema: {...}
</code></pre>
<p>Rules:<br />
&#8211; <code class="" data-line="">served: true</code> means the API server accepts requests at this version<br />
&#8211; <code class="" data-line="">served: false</code> means the API server returns <code class="" data-line="">404</code> for that version — use to deprecate<br />
&#8211; Exactly one version must have <code class="" data-line="">storage: true</code> — this is what gets written to etcd<br />
&#8211; When a client requests a non-storage version, the API server converts on the fly (or calls your conversion webhook — see <a href="/kubernetes-crd-versioning-conversion-webhook/">EP08</a>)</p>
<p>Early in development, start with <code class="" data-line="">v1alpha1 storage: true</code>. Promote to <code class="" data-line="">v1</code> when the schema is stable. EP08 covers how to do this without losing data.</p>
<hr />
<h2 id="specnames-what-kubectl-sees"><code class="" data-line="">spec.names</code>: What kubectl Sees</h2>
<pre><code class="" data-line="">spec:
  names:
    plural:     backuppolicies     # kubectl get backuppolicies
    singular:   backuppolicy       # kubectl get backuppolicy (also works)
    kind:       BackupPolicy       # used in YAML apiVersion/kind
    listKind:   BackupPolicyList   # optional; auto-derived if omitted
    shortNames:                    # kubectl get bp
      - bp
    categories:                    # kubectl get all includes this type
      - all
</code></pre>
<p><code class="" data-line="">categories</code> is worth noting: if you add <code class="" data-line="">all</code> to categories, your custom resources appear when someone runs <code class="" data-line="">kubectl get all -n mynamespace</code>. Most CRDs deliberately do not add this — it clutters <code class="" data-line="">get all</code> output. Only add it if your resource is a primary operational concern.</p>
<hr />
<h2 id="schemaopenapiv3schema-validation"><code class="" data-line="">schema.openAPIV3Schema</code>: Validation</h2>
<p>The schema is where you define field types, required fields, constraints, and descriptions. The API server validates every create and update against this schema before writing to etcd.</p>
<pre><code class="" data-line="">schema:
  openAPIV3Schema:
    type: object
    required: [&quot;spec&quot;]
    properties:
      spec:
        type: object
        required: [&quot;schedule&quot;, &quot;retentionDays&quot;]
        properties:
          schedule:
            type: string
            description: &quot;Cron expression for backup schedule&quot;
            pattern: &#039;^(\*|[0-9,\-\/]+)\s+(\*|[0-9,\-\/]+)\s+(\*|[0-9,\-\/]+)\s+(\*|[0-9,\-\/]+)\s+(\*|[0-9,\-\/]+)$&#039;
          retentionDays:
            type: integer
            minimum: 1
            maximum: 365
          storageClass:
            type: string
            default: &quot;standard&quot;        # default value (Kubernetes 1.17+)
          targets:
            type: array
            maxItems: 10
            items:
              type: object
              required: [&quot;name&quot;]
              properties:
                name:
                  type: string
                namespace:
                  type: string
                  default: &quot;default&quot;
      status:
        type: object
        x-kubernetes-preserve-unknown-fields: true   # controllers write arbitrary status
</code></pre>
<h3 id="field-types-available">Field types available</h3>
<table>
<thead>
<tr>
<th>Type</th>
<th>Usage</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="" data-line="">string</code></td>
<td>Text values; supports <code class="" data-line="">format</code>, <code class="" data-line="">pattern</code>, <code class="" data-line="">enum</code>, <code class="" data-line="">minLength</code>, <code class="" data-line="">maxLength</code></td>
</tr>
<tr>
<td><code class="" data-line="">integer</code></td>
<td>Whole numbers; supports <code class="" data-line="">minimum</code>, <code class="" data-line="">maximum</code></td>
</tr>
<tr>
<td><code class="" data-line="">number</code></td>
<td>Floating point</td>
</tr>
<tr>
<td><code class="" data-line="">boolean</code></td>
<td><code class="" data-line="">true</code>/<code class="" data-line="">false</code></td>
</tr>
<tr>
<td><code class="" data-line="">object</code></td>
<td>Nested structure; use <code class="" data-line="">properties</code> to define fields</td>
</tr>
<tr>
<td><code class="" data-line="">array</code></td>
<td>List; use <code class="" data-line="">items</code> to define element schema; supports <code class="" data-line="">minItems</code>, <code class="" data-line="">maxItems</code></td>
</tr>
</tbody>
</table>
<h3 id="x-kubernetes-preserve-unknown-fields-true"><code class="" data-line="">x-kubernetes-preserve-unknown-fields: true</code></h3>
<p>This tells the API server not to prune fields it does not know about. Use it on <code class="" data-line="">status</code> (controllers write whatever they need) and on fields that are intentionally free-form (like a <code class="" data-line="">config</code> field that accepts arbitrary YAML). Avoid it on <code class="" data-line="">spec</code> — it bypasses validation.</p>
<h3 id="validation-behavior-in-practice">Validation behavior in practice</h3>
<pre><code class="" data-line=""># This will fail with a clear error:
kubectl apply -f - &lt;&lt;EOF
apiVersion: storage.example.com/v1alpha1
kind: BackupPolicy
metadata:
  name: bad
  namespace: default
spec:
  schedule: &quot;not-a-cron&quot;    # fails pattern validation
  retentionDays: 500         # fails maximum: 365
EOF
</code></pre>
<pre><code class="" data-line="">The BackupPolicy &quot;bad&quot; is invalid:
  spec.schedule: Invalid value: &quot;not-a-cron&quot;: spec.schedule in body should match
    &#039;^(\*|[0-9,\-\/]+)\s+...&#039;
  spec.retentionDays: Invalid value: 500: spec.retentionDays in body should be
    less than or equal to 365
</code></pre>
<p>Schema validation catches configuration mistakes at apply time, not at runtime inside a pod. This is one of the core advantages of expressing domain configuration as CRDs rather than ConfigMaps.</p>
<hr />
<h2 id="additionalprintercolumns-what-kubectl-get-shows"><code class="" data-line="">additionalPrinterColumns</code>: What kubectl get Shows</h2>
<p>By default, <code class="" data-line="">kubectl get backuppolicies</code> shows only <code class="" data-line="">NAME</code> and <code class="" data-line="">AGE</code>. You can add columns:</p>
<pre><code class="" data-line="">additionalPrinterColumns:
  - name: Schedule
    type: string
    jsonPath: .spec.schedule
    description: Cron schedule for backups
  - name: Retention
    type: integer
    jsonPath: .spec.retentionDays
    priority: 1          # 0 = always shown; 1 = only with -o wide
  - name: Ready
    type: string
    jsonPath: .status.conditions[?(@.type==&#039;Ready&#039;)].status
  - name: Age
    type: date
    jsonPath: .metadata.creationTimestamp
</code></pre>
<p>Result:</p>
<pre><code class="" data-line="">NAME        SCHEDULE      READY   AGE
nightly     0 2 * * *     True    3d
weekly      0 0 * * 0     False   7d
</code></pre>
<p>Good printer columns turn <code class="" data-line="">kubectl get</code> into a useful operational dashboard. Include <code class="" data-line="">Ready</code> (from status conditions) so operators can immediately see which custom resources are healthy without running <code class="" data-line="">kubectl describe</code>.</p>
<hr />
<h2 id="the-status-subresource">The Status Subresource</h2>
<pre><code class="" data-line="">subresources:
  status: {}
</code></pre>
<p>Without the status subresource, <code class="" data-line="">spec</code> and <code class="" data-line="">status</code> are part of the same object. Any user with <code class="" data-line="">update</code> permission on the CRD can modify both. Controllers write status through the same path as users write spec.</p>
<p>With the status subresource enabled:<br />
&#8211; <code class="" data-line="">kubectl apply</code> / <code class="" data-line="">kubectl patch</code> only update <code class="" data-line="">spec</code> — the <code class="" data-line="">status</code> block is stripped<br />
&#8211; Controllers use the <code class="" data-line="">/status</code> subresource endpoint to write status<br />
&#8211; RBAC can grant <code class="" data-line="">update</code> on <code class="" data-line="">backuppolicies</code> (spec) independently from <code class="" data-line="">update</code> on <code class="" data-line="">backuppolicies/status</code></p>
<pre><code class="" data-line="">  WITHOUT status subresource:         WITH status subresource:
  ─────────────────────────            ──────────────────────────
  PUT /backuppolicies/nightly          PUT /backuppolicies/nightly
  → updates spec AND status            → updates spec only

                                       PUT /backuppolicies/nightly/status
                                       → updates status only (controller path)
</code></pre>
<p>Always enable the status subresource on production CRDs. The split between spec and status is fundamental to the Kubernetes API contract. Without it, a controller updating status can accidentally overwrite spec changes made by a user at the same time.</p>
<hr />
<h2 id="the-scale-subresource">The Scale Subresource</h2>
<pre><code class="" data-line="">subresources:
  scale:
    specReplicasPath: .spec.replicas
    statusReplicasPath: .status.replicas
    labelSelectorPath: .status.labelSelector
</code></pre>
<p>This makes your custom resource compatible with:</p>
<pre><code class="" data-line="">kubectl scale backuppolicy nightly --replicas=3
</code></pre>
<p>And with <code class="" data-line="">HorizontalPodAutoscaler</code> targeting your custom resource. If your CRD manages something replica-based (workers, shards, connections), enabling the scale subresource lets it plug into the standard Kubernetes autoscaling ecosystem without extra plumbing.</p>
<hr />
<h2 id="common-mistakes"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Common Mistakes</h2>
<p><strong>Forgetting <code class="" data-line="">x-kubernetes-preserve-unknown-fields: true</code> on status.</strong> If you validate the status field with a strict schema but do not add this, the API server will prune any status fields the controller writes that are not in the schema. The controller&#8217;s status updates will silently lose fields. Either define the full status schema or use <code class="" data-line="">x-kubernetes-preserve-unknown-fields: true</code>.</p>
<p><strong>Using <code class="" data-line="">scope: Cluster</code> for resources that should be namespaced.</strong> Once a CRD is created as cluster-scoped, you cannot make it namespaced without deleting and recreating it. Plan scope before deploying to production.</p>
<p><strong>Not enabling the status subresource.</strong> Without it, controllers writing status can race with users updating spec. It also means <code class="" data-line="">kubectl patch --subresource=status</code> does not work and some tooling behaves unexpectedly. Enable it from the start.</p>
<p><strong>Loose schema with no <code class="" data-line="">required</code> fields.</strong> An <code class="" data-line="">openAPIV3Schema</code> with no <code class="" data-line="">required</code> constraint accepts objects with empty <code class="" data-line="">spec</code>. This usually means your controller gets called with a resource that is missing mandatory configuration. Define required fields and validate them at the API boundary, not inside the controller.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line=""># Inspect the full schema of a CRD
kubectl get crd backuppolicies.storage.example.com -o yaml | \
  yq &#039;.spec.versions[0].schema&#039;

# Check what subresources are enabled
kubectl get crd certificates.cert-manager.io -o jsonpath=\
  &#039;{.spec.versions[0].subresources}&#039;

# See all served versions for a CRD
kubectl get crd prometheuses.monitoring.coreos.com \
  -o jsonpath=&#039;{.spec.versions[*].name}&#039;

# Check which version is the storage version
kubectl get crd certificates.cert-manager.io \
  -o jsonpath=&#039;{.spec.versions[?(@.storage==true)].name}&#039;

# Describe the printer columns for a CRD
kubectl get crd scaledobjects.keda.sh \
  -o jsonpath=&#039;{.spec.versions[0].additionalPrinterColumns}&#039;
</code></pre>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li><code class="" data-line="">spec.versions</code> allows serving and storing multiple API versions; only one version has <code class="" data-line="">storage: true</code></li>
<li><code class="" data-line="">scope</code> (Namespaced vs Cluster) cannot be changed after creation — choose deliberately</li>
<li><code class="" data-line="">openAPIV3Schema</code> validates every CR at the API boundary, before etcd storage</li>
<li>The status subresource separates the user write path (spec) from the controller write path (status) — always enable it</li>
<li><code class="" data-line="">additionalPrinterColumns</code> makes <code class="" data-line="">kubectl get</code> operationally useful; include a <code class="" data-line="">Ready</code> column from status conditions</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p><a href="/write-kubernetes-crd-yaml-walkthrough/">EP04: Write Your First Kubernetes CRD</a> puts the anatomy into practice — a complete hands-on walkthrough building a <code class="" data-line="">BackupPolicy</code> CRD from scratch, applying it to a cluster, creating instances, and verifying validation, RBAC, and status behavior.</p>
<p>Get EP04 in your inbox when it publishes → <a href="https://linuxcent.com">subscribe at linuxcent.com</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-schema-explained%2F&amp;linkname=Kubernetes%20CRD%20Schema%20Explained%3A%20Versions%2C%20Validation%2C%20and%20Status%20Subresource" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-schema-explained%2F&amp;linkname=Kubernetes%20CRD%20Schema%20Explained%3A%20Versions%2C%20Validation%2C%20and%20Status%20Subresource" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-schema-explained%2F&amp;linkname=Kubernetes%20CRD%20Schema%20Explained%3A%20Versions%2C%20Validation%2C%20and%20Status%20Subresource" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-schema-explained%2F&amp;linkname=Kubernetes%20CRD%20Schema%20Explained%3A%20Versions%2C%20Validation%2C%20and%20Status%20Subresource" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-schema-explained%2F&amp;linkname=Kubernetes%20CRD%20Schema%20Explained%3A%20Versions%2C%20Validation%2C%20and%20Status%20Subresource" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-schema-explained%2F&amp;linkname=Kubernetes%20CRD%20Schema%20Explained%3A%20Versions%2C%20Validation%2C%20and%20Status%20Subresource" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-schema-explained%2F&amp;linkname=Kubernetes%20CRD%20Schema%20Explained%3A%20Versions%2C%20Validation%2C%20and%20Status%20Subresource" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-schema-explained%2F&#038;title=Kubernetes%20CRD%20Schema%20Explained%3A%20Versions%2C%20Validation%2C%20and%20Status%20Subresource" data-a2a-url="https://linuxcent.com/kubernetes-crd-schema-explained/" data-a2a-title="Kubernetes CRD Schema Explained: Versions, Validation, and Status Subresource"></a></p><p>The post <a href="https://linuxcent.com/kubernetes-crd-schema-explained/">Kubernetes CRD Schema Explained: Versions, Validation, and Status Subresource</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/kubernetes-crd-schema-explained/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1681</post-id>	</item>
		<item>
		<title>What Is a Kubernetes CRD? How Custom Resources Extend the API</title>
		<link>https://linuxcent.com/what-is-kubernetes-crd/</link>
					<comments>https://linuxcent.com/what-is-kubernetes-crd/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Sat, 25 Apr 2026 18:14:48 +0000</pubDate>
				<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[API Extension]]></category>
		<category><![CDATA[CRD]]></category>
		<category><![CDATA[Custom Resource Definition]]></category>
		<category><![CDATA[kubectl]]></category>
		<category><![CDATA[Operators]]></category>
		<category><![CDATA[Platform Engineering]]></category>
		<guid isPermaLink="false">https://linuxcent.com/what-is-kubernetes-crd/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span>Learn what a Kubernetes CRD is, how custom resources extend the API server, and why every operator depends on them — with real kubectl examples for platform engineers.</p>
<p>The post <a href="https://linuxcent.com/what-is-kubernetes-crd/">What Is a Kubernetes CRD? How Custom Resources Extend the API</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>Kubernetes CRDs &amp; Operators: Extending the API, Episode 1</em><br />
<em><a href="/what-is-kubernetes-crd/">What Is a CRD?</a> · <a href="/kubernetes-custom-resources-examples/">CRDs You Already Use</a> · <a href="/kubernetes-crd-schema-explained/">CRD Anatomy</a> · <a href="/write-kubernetes-crd-yaml-walkthrough/">Write Your First CRD</a> · <a href="/kubernetes-crd-cel-validation/">CEL Validation</a> · <a href="/kubernetes-controller-reconcile-loop/">Controller Loop</a> · <a href="/build-kubernetes-operator-controller-runtime/">Build an Operator</a> · <a href="/kubernetes-crd-versioning-conversion-webhook/">CRD Versioning</a> · <a href="/kubernetes-admission-webhooks-explained/">Admission Webhooks</a> · <a href="/kubernetes-crd-production-finalizers-conditions-rbac/">CRDs in Production</a></em></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>A Kubernetes CRD (Custom Resource Definition) is how you add new resource types to the Kubernetes API — the same way <code class="" data-line="">Deployment</code> and <code class="" data-line="">Service</code> exist natively, you can make <code class="" data-line="">BackupPolicy</code> or <code class="" data-line="">Certificate</code> exist too<br />
  <em>(CRD = the schema/blueprint; Custom Resource = an instance of that schema, just like a Pod is an instance of the Pod schema)</em></li>
<li>Every <code class="" data-line="">kubectl get crds</code> on a real cluster shows dozens of them — cert-manager, KEDA, Prometheus Operator, Crossplane all ship their own CRDs</li>
<li>CRDs are served by the same API server as built-in resources — <code class="" data-line="">kubectl</code>, RBAC, watches, and events all work identically</li>
<li>A CRD alone does nothing — a <strong>controller</strong> watches the custom resources and acts on them; together they form an <strong>Operator</strong></li>
<li>CRDs live in etcd just like Pods and Deployments — they survive API server restarts and cluster upgrades</li>
<li>You do not need to modify Kubernetes source code or restart the API server to add a CRD</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">  HOW KUBERNETES CRDs EXTEND THE API

  ┌──────────────────────────────────────────────────────────────┐
  │  Kubernetes API Server                                       │
  │                                                              │
  │  Built-in resources          Custom resources (via CRD)      │
  │  ─────────────────           ──────────────────────────      │
  │  Pod                         Certificate     (cert-manager)  │
  │  Deployment                  ScaledObject    (KEDA)          │
  │  Service                     ExternalSecret  (ESO)           │
  │  ConfigMap                   BackupPolicy    (your team)     │
  │  ...                         ...                             │
  │                                                              │
  │  All resources: same API, same kubectl, same RBAC, same etcd │
  └──────────────────────────────────────────────────────────────┘
            ▲                          ▲
            │ built in                 │ registered at runtime
            │                         │
         Kubernetes              CustomResourceDefinition
          binary                    (a YAML you apply)
</code></pre>
<p>What is a Kubernetes CRD? It is a resource that defines resources — a schema registration that teaches the API server about a new object type you want to use in your cluster.</p>
<hr />
<h2 id="what-problem-crds-solve">What Problem CRDs Solve</h2>
<p>Kubernetes ships with roughly 50 resource types: Pods, Deployments, Services, ConfigMaps, Secrets, PersistentVolumes, and so on. These cover the general-purpose building blocks for running containerized workloads.</p>
<p>But the moment you operate real infrastructure, you hit the edges. You want to express:</p>
<ul>
<li>&#8220;This database should have three replicas with point-in-time recovery enabled&#8221; — not a Deployment</li>
<li>&#8220;This TLS certificate for api.example.com should renew 30 days before expiry&#8221; — not a Secret</li>
<li>&#8220;This queue consumer should scale to zero when the queue is empty&#8221; — not a HorizontalPodAutoscaler</li>
</ul>
<p>Before CRDs (pre-2017), the only options were: use ConfigMaps as a poor substitute (no schema, no validation, no dedicated RBAC), or fork Kubernetes and add the resource natively (impractical for everyone outside the core team).</p>
<p>CRDs, introduced as stable in Kubernetes 1.16, solved this by letting you register a new resource type with the API server at runtime — without touching Kubernetes source code, without restarting the API server, without any special access beyond being able to create cluster-scoped resources.</p>
<hr />
<h2 id="the-kubernetes-api-a-brief-mental-model">The Kubernetes API: A Brief Mental Model</h2>
<p>Before CRDs make sense, the API model needs to be clear.</p>
<pre><code class="" data-line="">  KUBERNETES API STRUCTURE

  apiVersion: apps/v1       ← API group (apps) + version (v1)
  kind: Deployment          ← resource type
  metadata:
    name: web               ← instance name
    namespace: default      ← namespace scope
  spec:
    replicas: 3             ← desired state
</code></pre>
<p>Every Kubernetes resource has:<br />
&#8211; A <strong>group</strong> (e.g., <code class="" data-line="">apps</code>, <code class="" data-line="">batch</code>, <code class="" data-line="">networking.k8s.io</code>) — or no group for core resources<br />
&#8211; A <strong>version</strong> (e.g., <code class="" data-line="">v1</code>, <code class="" data-line="">v1beta1</code>)<br />
&#8211; A <strong>kind</strong> (e.g., <code class="" data-line="">Deployment</code>, <code class="" data-line="">Pod</code>)<br />
&#8211; A <strong>scope</strong>: namespaced or cluster-wide</p>
<p>The API server is a registry. Each group/version/kind combination maps to a Go struct that knows how to validate, store, and serve that resource type.</p>
<p>A CRD registers a new entry in that registry. You supply the group, version, kind, and schema. The API server handles everything else — serving it via REST, storing it in etcd, exposing it to <code class="" data-line="">kubectl</code>.</p>
<hr />
<h2 id="what-a-crd-looks-like">What a CRD Looks Like</h2>
<p>Here is the smallest possible CRD — it creates a new <code class="" data-line="">BackupPolicy</code> resource type in the <code class="" data-line="">storage.example.com</code> API group:</p>
<pre><code class="" data-line="">apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: backuppolicies.storage.example.com
spec:
  group: storage.example.com
  versions:
    - name: v1alpha1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                schedule:
                  type: string
                retentionDays:
                  type: integer
  scope: Namespaced
  names:
    plural: backuppolicies
    singular: backuppolicy
    kind: BackupPolicy
    shortNames:
      - bp
</code></pre>
<p>Apply it:</p>
<pre><code class="" data-line="">kubectl apply -f backuppolicy-crd.yaml
</code></pre>
<p>Now create an instance:</p>
<pre><code class="" data-line="">apiVersion: storage.example.com/v1alpha1
kind: BackupPolicy
metadata:
  name: nightly
  namespace: default
spec:
  schedule: &quot;0 2 * * *&quot;
  retentionDays: 30
</code></pre>
<pre><code class="" data-line="">kubectl apply -f nightly-backup.yaml
kubectl get backuppolicies
kubectl get bp            # shortName works
kubectl describe bp nightly
</code></pre>
<p>The API server validates the spec against the schema, stores it in etcd, and returns it via all the standard API endpoints — all without a single line of custom code.</p>
<hr />
<h2 id="crd-vs-built-in-resource-what-is-different">CRD vs Built-In Resource: What Is Different?</h2>
<p>Not much, deliberately.</p>
<table>
<thead>
<tr>
<th>Capability</th>
<th>Built-in resource</th>
<th>Custom resource (CRD)</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="" data-line="">kubectl get / describe / delete</code></td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>RBAC (Roles, ClusterRoles)</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Watch (informers, events)</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Stored in etcd</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>OpenAPI schema validation</td>
<td>Yes</td>
<td>Yes (you define the schema)</td>
</tr>
<tr>
<td>Admission webhooks</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Status subresource</td>
<td>Yes</td>
<td>Optional (you enable it)</td>
</tr>
<tr>
<td>Scale subresource</td>
<td>Yes</td>
<td>Optional (you enable it)</td>
</tr>
<tr>
<td>Built-in controller behavior</td>
<td>Yes</td>
<td>No — you write the controller</td>
</tr>
</tbody>
</table>
<p>The last row is the critical one. When you create a <code class="" data-line="">Deployment</code>, the deployment controller immediately starts managing ReplicaSets. When you create a <code class="" data-line="">BackupPolicy</code>, nothing happens — until you write and deploy a <strong>controller</strong> that watches <code class="" data-line="">BackupPolicy</code> objects and acts on them.</p>
<p>That controller + the CRD is what people call an <strong>Operator</strong>.</p>
<hr />
<h2 id="a-real-cluster-what-you-actually-see">A Real Cluster: What You Actually See</h2>
<p>Run this on any cluster running cert-manager, Prometheus Operator, or any other tooling:</p>
<pre><code class="" data-line="">kubectl get crds
</code></pre>
<p>Sample output (abbreviated):</p>
<pre><code class="" data-line="">NAME                                                  CREATED AT
certificates.cert-manager.io                          2024-11-01T08:12:00Z
certificaterequests.cert-manager.io                   2024-11-01T08:12:00Z
issuers.cert-manager.io                               2024-11-01T08:12:00Z
clusterissuers.cert-manager.io                        2024-11-01T08:12:00Z
scaledobjects.keda.sh                                 2024-11-01T08:13:00Z
scaledjobs.keda.sh                                    2024-11-01T08:13:00Z
externalsecrets.external-secrets.io                   2024-11-01T08:14:00Z
prometheuses.monitoring.coreos.com                    2024-11-01T08:15:00Z
servicemonitors.monitoring.coreos.com                 2024-11-01T08:15:00Z
</code></pre>
<p>Every tool that ships as a CRD-based system registers its resource types here first. The count often surprises engineers: a production cluster with a typical toolchain easily has 40–80 CRDs.</p>
<p>Check how many are on your cluster:</p>
<pre><code class="" data-line="">kubectl get crds --no-headers | wc -l
</code></pre>
<hr />
<h2 id="how-the-api-server-handles-a-crd">How the API Server Handles a CRD</h2>
<p>When you apply a CRD, the API server does three things:</p>
<pre><code class="" data-line="">  CRD REGISTRATION FLOW

  kubectl apply -f my-crd.yaml
          │
          ▼
  1. API server validates the CRD manifest
     (is the schema valid OpenAPI v3? are names correct?)
          │
          ▼
  2. CRD stored in etcd
     (under /registry/apiextensions.k8s.io/customresourcedefinitions/)
          │
          ▼
  3. New REST endpoints activated immediately:
     GET  /apis/storage.example.com/v1alpha1/namespaces/{ns}/backuppolicies
     POST /apis/storage.example.com/v1alpha1/namespaces/{ns}/backuppolicies
     ...
</code></pre>
<p>From this point, any <code class="" data-line="">kubectl get backuppolicies</code> or API call to those endpoints is handled exactly like a built-in resource call — the API server serves it from etcd, applies RBAC, runs admission webhooks, and returns standard JSON.</p>
<p>No restart required. The new endpoints appear within seconds.</p>
<hr />
<h2 id="the-difference-between-crd-and-cr">The Difference Between CRD and CR</h2>
<p>Two terms that are easily confused:</p>
<ul>
<li><strong>CRD (CustomResourceDefinition)</strong> — the schema/blueprint. There is one CRD per resource type. <code class="" data-line="">certificates.cert-manager.io</code> is a CRD.</li>
<li><strong>CR (Custom Resource)</strong> — an instance of a CRD. Every <code class="" data-line="">Certificate</code> object you create is a custom resource. You can have thousands of CRs per CRD.</li>
</ul>
<pre><code class="" data-line="">  CRD (one)          →  Custom Resource (many)
  ─────────             ─────────────────────
  certificates          web-tls           (namespace: production)
  .cert-manager.io      api-tls           (namespace: production)
                        admin-tls         (namespace: staging)
                        ...
</code></pre>
<p>The CRD is applied once (usually by the tool&#8217;s Helm chart). Custom resources are created by your users, your CI pipeline, or your GitOps system throughout the life of the cluster.</p>
<hr />
<h2 id="where-crds-fit-in-the-kubernetes-extension-model">Where CRDs Fit in the Kubernetes Extension Model</h2>
<p>CRDs are one of three ways to extend Kubernetes:</p>
<pre><code class="" data-line="">  KUBERNETES EXTENSION MECHANISMS

  1. CRDs + Controllers (Operators)
     Add new resource types + behavior
     → cert-manager, KEDA, Argo CD, Crossplane
     Used for: domain-specific abstractions, infrastructure management

  2. Admission Webhooks
     Intercept API requests to validate or mutate objects
     → OPA/Gatekeeper, Kyverno, Istio injection
     Used for: policy enforcement, sidecar injection, defaulting

  3. API Aggregation (AA)
     Register a fully separate API server behind the main API server
     → metrics-server, custom autoscalers
     Used for: when you need non-CRUD semantics (e.g. exec, attach, streaming)
</code></pre>
<p>For 95% of use cases, CRDs + controllers are the right mechanism. API aggregation is complex and only warranted for non-standard API semantics. Admission webhooks are complementary to CRDs, not an alternative.</p>
<hr />
<h2 id="common-mistakes"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Common Mistakes</h2>
<p><strong>Confusing the CRD with the controller.</strong> The CRD is just a schema registration — it does not execute code. If you apply a CRD but do not deploy its controller, creating custom resources will succeed (the API server accepts them) but nothing will happen. This catches many people the first time they try to use cert-manager by only applying the CRDs without installing the cert-manager controller.</p>
<p><strong>Assuming CRD deletion is safe.</strong> Deleting a CRD deletes all custom resources of that type from etcd. There is no &#8220;are you sure?&#8221; prompt. If you delete the <code class="" data-line="">certificates.cert-manager.io</code> CRD, every <code class="" data-line="">Certificate</code> object in every namespace is gone.</p>
<p><strong>Treating CRDs as ConfigMap replacements.</strong> Some teams store configuration in CRDs purely to get schema validation. This works, but without a controller, the custom resources are inert data. If you only need configuration storage with validation, a CRD is viable — just be explicit that there is no reconciliation loop.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line=""># List all CRDs in the cluster
kubectl get crds

# Inspect a specific CRD&#039;s schema
kubectl get crd certificates.cert-manager.io -o yaml

# List all custom resources of a type
kubectl get certificates -A

# Get details on a specific custom resource
kubectl describe certificate web-tls -n production

# Delete a CRD (WARNING: deletes all instances)
kubectl delete crd backuppolicies.storage.example.com

# Check if a CRD is established (ready to use)
kubectl get crd backuppolicies.storage.example.com \
  -o jsonpath=&#039;{.status.conditions[?(@.type==&quot;Established&quot;)].status}&#039;
# Returns: True
</code></pre>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>A Kubernetes CRD registers a new resource type with the API server — no source code changes, no restart required</li>
<li>Custom resources behave identically to built-in resources: <code class="" data-line="">kubectl</code>, RBAC, watches, etcd, admission webhooks all work the same way</li>
<li>The CRD is just the schema; a controller gives custom resources behavior — together they form an Operator</li>
<li>Every production cluster running modern tooling already uses dozens of CRDs</li>
<li>Deleting a CRD deletes all its instances — treat CRDs as production-critical objects</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p><a href="/kubernetes-custom-resources-examples/">EP02: CRDs You Already Use</a> makes this concrete before we go deeper — we walk through cert-manager&#8217;s <code class="" data-line="">Certificate</code>, KEDA&#8217;s <code class="" data-line="">ScaledObject</code>, and External Secrets&#8217; <code class="" data-line="">ExternalSecret</code> as working examples, so you understand what a well-designed CRD looks like from a user&#8217;s perspective before you design your own.</p>
<p>Get EP02 in your inbox when it publishes → <a href="https://linuxcent.com">subscribe at linuxcent.com</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fwhat-is-kubernetes-crd%2F&amp;linkname=What%20Is%20a%20Kubernetes%20CRD%3F%20How%20Custom%20Resources%20Extend%20the%20API" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fwhat-is-kubernetes-crd%2F&amp;linkname=What%20Is%20a%20Kubernetes%20CRD%3F%20How%20Custom%20Resources%20Extend%20the%20API" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fwhat-is-kubernetes-crd%2F&amp;linkname=What%20Is%20a%20Kubernetes%20CRD%3F%20How%20Custom%20Resources%20Extend%20the%20API" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fwhat-is-kubernetes-crd%2F&amp;linkname=What%20Is%20a%20Kubernetes%20CRD%3F%20How%20Custom%20Resources%20Extend%20the%20API" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fwhat-is-kubernetes-crd%2F&amp;linkname=What%20Is%20a%20Kubernetes%20CRD%3F%20How%20Custom%20Resources%20Extend%20the%20API" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fwhat-is-kubernetes-crd%2F&amp;linkname=What%20Is%20a%20Kubernetes%20CRD%3F%20How%20Custom%20Resources%20Extend%20the%20API" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fwhat-is-kubernetes-crd%2F&amp;linkname=What%20Is%20a%20Kubernetes%20CRD%3F%20How%20Custom%20Resources%20Extend%20the%20API" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fwhat-is-kubernetes-crd%2F&#038;title=What%20Is%20a%20Kubernetes%20CRD%3F%20How%20Custom%20Resources%20Extend%20the%20API" data-a2a-url="https://linuxcent.com/what-is-kubernetes-crd/" data-a2a-title="What Is a Kubernetes CRD? How Custom Resources Extend the API"></a></p><p>The post <a href="https://linuxcent.com/what-is-kubernetes-crd/">What Is a Kubernetes CRD? How Custom Resources Extend the API</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/what-is-kubernetes-crd/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1675</post-id>	</item>
		<item>
		<title>Kubernetes Today: v1.33 to v1.35, In-Place Resize GA, and What Comes Next</title>
		<link>https://linuxcent.com/kubernetes-v1-33-whats-new/</link>
					<comments>https://linuxcent.com/kubernetes-v1-33-whats-new/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Fri, 24 Apr 2026 19:31:44 +0000</pubDate>
				<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Cloud Native]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Kubernetes Security]]></category>
		<category><![CDATA[Kubernetes v1.33]]></category>
		<category><![CDATA[Kubernetes v1.35]]></category>
		<category><![CDATA[Platform Engineering]]></category>
		<guid isPermaLink="false">https://linuxcent.com/kubernetes-v1-33-whats-new/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span>In-place pod resize GA, sidecar containers stable, structured logging mature. Trace what v1.33–v1.35 shipped and where Kubernetes is heading in 2025 and beyond.</p>
<p>The post <a href="https://linuxcent.com/kubernetes-v1-33-whats-new/">Kubernetes Today: v1.33 to v1.35, In-Place Resize GA, and What Comes Next</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<hr />
<h2 id="introduction">Introduction</h2>
<p>Ten years after the first commit, Kubernetes is not exciting in the way it was in 2015. That&#8217;s a compliment. The system is stable. The APIs are mature. The migrations — dockershim, PSP, cloud provider code — are behind us.</p>
<p>What the 1.33–1.35 cycle shows is a project focused on precision: removing edge cases, promoting long-running alpha features to stable, and making the scheduler, storage, and security model more correct rather than more powerful. That&#8217;s what a mature infrastructure platform looks like.</p>
<p>Here&#8217;s what happened and where the project is headed.</p>
<hr />
<h2 id="kubernetes-133-sidecar-resize-in-place-resize-beta-april-2025">Kubernetes 1.33 — Sidecar Resize, In-Place Resize Beta (April 2025)</h2>
<p>Code name: <strong>Octarine</strong></p>
<h3 id="in-place-pod-vertical-scaling-reaches-beta">In-Place Pod Vertical Scaling reaches Beta</h3>
<p>After landing as alpha in 1.27, in-place pod resource resizing became beta in 1.33 — enabled by default via the <code class="" data-line="">InPlacePodVerticalScaling</code> feature gate.</p>
<p>The capability: change CPU and memory requests/limits on a running container without terminating and restarting the pod.</p>
<pre><code class="" data-line=""># Resize a running container&#039;s CPU limit without restart
kubectl patch pod api-pod-xyz --type=&#039;json&#039; -p=&#039;[
  {
    &quot;op&quot;: &quot;replace&quot;,
    &quot;path&quot;: &quot;/spec/containers/0/resources/requests/cpu&quot;,
    &quot;value&quot;: &quot;2&quot;
  },
  {
    &quot;op&quot;: &quot;replace&quot;,
    &quot;path&quot;: &quot;/spec/containers/0/resources/limits/cpu&quot;,
    &quot;value&quot;: &quot;4&quot;
  }
]&#039;

# Verify the resize was applied
kubectl get pod api-pod-xyz -o jsonpath=&#039;{.status.containerStatuses[0].resources}&#039;
</code></pre>
<p><strong>Why this matters operationally</strong>: Before in-place resize, vertical scaling meant terminating the pod, losing in-memory state, waiting for a new pod to become ready. For databases with warm buffer pools, JVM applications with loaded heap caches, or any workload where startup cost is significant, this was a serious limitation. Vertical Pod Autoscaler (VPA) worked around it by restarting pods — acceptable for stateless workloads, problematic for stateful ones.</p>
<p>In 1.33, resizing also works for sidecar containers, combining two 1.32-stable features.</p>
<h3 id="sidecar-containers-full-maturity">Sidecar Containers — Full Maturity</h3>
<p>The first feature to formally combine sidecar and in-place resize: you can now vertically scale a service mesh proxy (Envoy sidecar) without restarting the application pod. For high-traffic services where the proxy itself becomes the CPU bottleneck, this is directly actionable.</p>
<hr />
<h2 id="gateway-api-v14-october-2025">Gateway API v1.4 (October 2025)</h2>
<p>Gateway API continued its rapid iteration with v1.4:</p>
<p><strong>BackendTLSPolicy</strong> (Standard channel): Configure TLS between the gateway and the backend service — not just TLS termination at the gateway, but end-to-end encryption:</p>
<pre><code class="" data-line="">apiVersion: gateway.networking.k8s.io/v1alpha3
kind: BackendTLSPolicy
metadata:
  name: api-backend-tls
spec:
  targetRefs:
  - group: &quot;&quot;
    kind: Service
    name: api-service
  validation:
    caCertificateRefs:
    - name: internal-ca
      group: &quot;&quot;
      kind: ConfigMap
    hostname: api.internal.corp
</code></pre>
<p><strong>Gateway Client Certificate Validation</strong>: The gateway can now validate client certificates — mutual TLS for ingress traffic, not just between services.</p>
<p><strong>TLSRoute to Standard</strong>: TLS routing (based on SNI, not HTTP host headers) graduated to the standard channel — enabling TCP workloads with TLS passthrough through the Gateway API model.</p>
<p><strong>ListenerSet</strong>: Group multiple Gateway listeners — useful for shared infrastructure where multiple teams need to attach routes to the same gateway without managing separate Gateway resources.</p>
<hr />
<h2 id="kubernetes-134-scheduler-improvements-dra-continues-august-2025">Kubernetes 1.34 — Scheduler Improvements, DRA Continues (August 2025)</h2>
<p>The 1.34 release focused on the scheduler and Dynamic Resource Allocation:</p>
<p><strong>DRA structured parameters</strong> stabilization: The Dynamic Resource Allocation API matured its parameter model — resource drivers can expose structured claims that the scheduler understands, enabling topology-aware placement of GPU workloads:</p>
<pre><code class="" data-line="">apiVersion: resource.k8s.io/v1alpha3
kind: ResourceClaim
metadata:
  name: gpu-claim
spec:
  devices:
    requests:
    - name: gpu
      deviceClassName: gpu.nvidia.com
      selectors:
      - cel:
          expression: device.attributes[&quot;nvidia.com/gpu-product&quot;].string() == &quot;A100-SXM4-80GB&quot;
      count: 2
</code></pre>
<p><strong>Scheduler QueueingHint</strong> stable: Plugins can now tell the scheduler when to re-queue a pod for scheduling — instead of the scheduler periodically retrying all unschedulable pods, plugins signal when relevant cluster state has changed. This significantly reduces scheduler CPU consumption in large clusters with many unschedulable pods.</p>
<p><strong>Fine-grained node authorization improvements</strong>: Kubelets can now be restricted from accessing Service resources they don&#8217;t need — further reducing the blast radius of a compromised kubelet.</p>
<hr />
<h2 id="kubernetes-135-in-place-resize-ga-memory-limits-unlocked-december-2025">Kubernetes 1.35 — In-Place Resize GA, Memory Limits Unlocked (December 2025)</h2>
<h3 id="in-place-pod-vertical-scaling-graduates-to-stable">In-Place Pod Vertical Scaling Graduates to Stable</h3>
<p>After landing in alpha (1.27), beta (1.33), in-place resize graduated to GA in 1.35. Two significant improvements accompanied GA:</p>
<p><strong>Memory limit decreases now permitted</strong>: Previously, you could increase memory limits in-place but not decrease them. The restriction existed because the kernel doesn&#8217;t immediately reclaim memory when the limit is lowered — the OOM killer would need to run. 1.35 lifts this restriction with proper handling: the kernel is instructed to reclaim, and the pod status reflects the resize progress.</p>
<p><strong>Pod-Level Resources</strong> (alpha in 1.35): Specify resource requests and limits at the pod level rather than per-container — with in-place resize support. Useful for init containers and sidecar patterns where total pod resources matter more than per-container allocation.</p>
<pre><code class="" data-line="">spec:
  # Pod-level resources (alpha) — total budget for all containers
  resources:
    requests:
      cpu: &quot;4&quot;
      memory: &quot;8Gi&quot;
  containers:
  - name: application
    image: myapp:latest
    # No per-container resources; pod-level applies
  - name: log-collector
    image: fluentbit:latest
    restartPolicy: Always  # sidecar
</code></pre>
<h3 id="other-135-highlights">Other 1.35 Highlights</h3>
<p><strong>Topology Spread Constraints improvements</strong>: Better handling of unschedulable scenarios — <code class="" data-line="">whenUnsatisfiable: ScheduleAnyway</code> now has smarter fallback behavior.</p>
<p><strong>VolumeAttributesClass</strong> stable: Change storage performance characteristics (IOPS, throughput) of a PersistentVolume without re-provisioning — the storage equivalent of in-place pod resize.</p>
<pre><code class="" data-line=""># Change volume IOPS without re-provisioning
kubectl patch pvc database-pvc --type=&#039;merge&#039; -p=&#039;
  {&quot;spec&quot;: {&quot;volumeAttributesClassName&quot;: &quot;high-performance&quot;}}&#039;
</code></pre>
<p><strong>Job success policy improvements</strong>: Declare a Job successful when a subset of pods complete successfully — for distributed training jobs where not all workers need to finish.</p>
<hr />
<h2 id="whats-in-kubernetes-136-april-22-2026">What&#8217;s in Kubernetes 1.36 (April 22, 2026)</h2>
<p>Kubernetes 1.36 is on track for April 22, 2026 release. Based on the enhancement tracking and KEP (Kubernetes Enhancement Proposal) pipeline, expected highlights include:</p>
<ul>
<li><strong>DRA</strong> continuing toward stable</li>
<li><strong>Pod-level resources</strong> moving to beta</li>
<li><strong>Scheduler improvements</strong> for AI/ML workload placement</li>
<li><strong>Further Gateway API integration</strong> as core networking model</li>
</ul>
<p>The project has reached a rhythm: four releases per year, each focused on advancing a predictable set of features through alpha → beta → stable. The drama of the 2019–2022 period (PSP, dockershim, API removals) is behind it.</p>
<hr />
<h2 id="the-state-of-the-ecosystem-in-2026">The State of the Ecosystem in 2026</h2>
<h3 id="control-plane-deployment-models">Control Plane Deployment Models</h3>
<table>
<thead>
<tr>
<th>Model</th>
<th>Examples</th>
<th>Best For</th>
</tr>
</thead>
<tbody>
<tr>
<td>Managed (cloud provider)</td>
<td>GKE, EKS, AKS</td>
<td>Most organizations; no control plane ops</td>
</tr>
<tr>
<td>Self-managed</td>
<td>kubeadm, k3s, Talos</td>
<td>Air-gapped, on-prem, specific compliance requirements</td>
</tr>
<tr>
<td>Managed (platform)</td>
<td>Rancher, OpenShift</td>
<td>Enterprises that need multi-cluster management + vendor support</td>
</tr>
</tbody>
</table>
<h3 id="cni-landscape">CNI Landscape</h3>
<table>
<thead>
<tr>
<th>CNI</th>
<th>Model</th>
<th>Notable Feature</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cilium</td>
<td>eBPF</td>
<td>kube-proxy replacement, network policy at kernel, Hubble observability</td>
</tr>
<tr>
<td>Calico</td>
<td>eBPF or iptables</td>
<td>BGP-based networking, hybrid cloud routing</td>
</tr>
<tr>
<td>Flannel</td>
<td>VXLAN/host-gw</td>
<td>Simple, low overhead, no network policy</td>
</tr>
<tr>
<td>Weave</td>
<td>Mesh overlay</td>
<td>Easy multi-host setup</td>
</tr>
</tbody>
</table>
<p>eBPF-based CNIs (Cilium, Calico in eBPF mode) are now the default recommendation for production clusters. The iptables era of Kubernetes networking is ending.</p>
<h3 id="security-stack-in-2026">Security Stack in 2026</h3>
<p>A hardened Kubernetes cluster in 2026 runs:</p>
<pre><code class="" data-line="">Cluster provisioning:    Cluster API + GitOps (Flux/ArgoCD)
Admission control:       Pod Security Admission (restricted) + Kyverno or OPA/Gatekeeper
Runtime security:        Falco (eBPF-based syscall monitoring)
Network security:        Cilium NetworkPolicy + Cilium Cluster Mesh for multi-cluster
Image security:          Cosign signing in CI + admission webhook for signature verification
Secret management:       External Secrets Operator → HashiCorp Vault or cloud KMS
Observability:           Prometheus + Grafana + Hubble (network flows) + OpenTelemetry
</code></pre>
<hr />
<h2 id="the-permanent-principles-that-havent-changed">The Permanent Principles That Haven&#8217;t Changed</h2>
<p>Looking across twelve years and 35 minor versions, some things have not changed:</p>
<p><strong>The API as the universal interface</strong>: Everything in Kubernetes is a resource. This remains the most important architectural decision — it makes every tool, every controller, every GitOps system work with the same model.</p>
<p><strong>Reconciliation loops</strong>: Every Kubernetes controller watches actual state and drives it toward desired state. The controller pattern from 2014 is unchanged. CRDs and Operators are just more instances of it.</p>
<p><strong>Labels and selectors</strong>: The flexible grouping mechanism from 1.0 is still the primary way Kubernetes components find each other. Services find pods. HPA finds Deployments. Operators find their managed resources.</p>
<p><strong>Declarative, not imperative</strong>: You describe what you want. Kubernetes figures out how to achieve and maintain it. This principle, inherited from Borg&#8217;s BCL configuration, underlies everything from Deployments to Crossplane&#8217;s cloud resource management.</p>
<hr />
<h2 id="whats-coming-the-next-five-years">What&#8217;s Coming: The Next Five Years</h2>
<p><strong>WebAssembly on Kubernetes</strong>: The Wasm ecosystem (wasmCloud, SpinKube) is building toward running WebAssembly workloads as first-class Kubernetes pods — near-native performance, smaller images, stronger isolation than containers. Still early, but gaining real adoption.</p>
<p><strong>AI inference as infrastructure</strong>: LLM serving is becoming a cluster primitive. Tools like KServe and vLLM on Kubernetes are moving from research to production. The scheduler, resource model, and networking will continue adapting to inference workload patterns.</p>
<p><strong>Confidential computing</strong>: AMD SEV, Intel TDX, and ARM CCA provide hardware-level memory encryption for pods. The RuntimeClass mechanism and ongoing kernel work are making confidential Kubernetes workloads operational rather than experimental.</p>
<p><strong>Leaner distributions</strong>: k3s, k0s, Talos, and Flatcar-based minimal Kubernetes distributions are growing in adoption for edge, IoT, and resource-constrained environments. The pressure is toward smaller, more auditable control planes.</p>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>In-place pod vertical scaling went from alpha (1.27) to stable (1.35) — live CPU and memory resize without pod restart changes the economics of stateful workload management</li>
<li>Gateway API v1.4 completes the ingress replacement story: BackendTLSPolicy, client certificate validation, and TLSRoute in standard channel</li>
<li>VolumeAttributesClass stable (1.35): Change storage performance in-place — the storage parallel to pod resource resize</li>
<li>The eBPF era of Kubernetes networking is established: Cilium as default CNI in GKE, growing in EKS/AKS, replacing iptables-based kube-proxy</li>
<li>The Kubernetes project in 2026 is focused on precision — promoting mature features to stable, reducing edge cases, improving scheduler efficiency — not adding new abstractions</li>
<li>WebAssembly, confidential computing, and AI inference scheduling are the frontiers to watch</li>
</ul>
<hr />
<h2 id="series-wrap-up">Series Wrap-Up</h2>
<table>
<thead>
<tr>
<th>Era</th>
<th>Defining Change</th>
</tr>
</thead>
<tbody>
<tr>
<td>2003–2014</td>
<td>Borg and Omega build the playbook internally at Google</td>
</tr>
<tr>
<td>2014–2016</td>
<td>Kubernetes 1.0, CNCF, and winning the container orchestration wars</td>
</tr>
<tr>
<td>2016–2018</td>
<td>RBAC stable, CRDs, cloud providers all-in on managed K8s</td>
</tr>
<tr>
<td>2018–2020</td>
<td>Operators, service mesh, OPA/Gatekeeper — the extensibility era</td>
</tr>
<tr>
<td>2020–2022</td>
<td>Supply chain crisis, PSP deprecated, API removals, dockershim exit</td>
</tr>
<tr>
<td>2022–2023</td>
<td>Dockershim and PSP removed, eBPF networking takes over</td>
</tr>
<tr>
<td>2023–2025</td>
<td>GitOps standard, sidecar stable, DRA, AI/ML workloads</td>
</tr>
<tr>
<td>2025–2026</td>
<td>In-place resize GA, VolumeAttributesClass, Gateway API complete</td>
</tr>
</tbody>
</table>
<p>From 47,501 lines of Go in a 250-file GitHub commit to the operating system of the cloud — and still reconciling.</p>
<hr />
<p><a href="ep07-platform-engineering.md">← EP07: Platform Engineering Era</a></p>
<p><em>Series: Kubernetes: From Borg to Platform Engineering | linuxcent.com</em></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-v1-33-whats-new%2F&amp;linkname=Kubernetes%20Today%3A%20v1.33%20to%20v1.35%2C%20In-Place%20Resize%20GA%2C%20and%20What%20Comes%20Next" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-v1-33-whats-new%2F&amp;linkname=Kubernetes%20Today%3A%20v1.33%20to%20v1.35%2C%20In-Place%20Resize%20GA%2C%20and%20What%20Comes%20Next" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-v1-33-whats-new%2F&amp;linkname=Kubernetes%20Today%3A%20v1.33%20to%20v1.35%2C%20In-Place%20Resize%20GA%2C%20and%20What%20Comes%20Next" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-v1-33-whats-new%2F&amp;linkname=Kubernetes%20Today%3A%20v1.33%20to%20v1.35%2C%20In-Place%20Resize%20GA%2C%20and%20What%20Comes%20Next" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-v1-33-whats-new%2F&amp;linkname=Kubernetes%20Today%3A%20v1.33%20to%20v1.35%2C%20In-Place%20Resize%20GA%2C%20and%20What%20Comes%20Next" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-v1-33-whats-new%2F&amp;linkname=Kubernetes%20Today%3A%20v1.33%20to%20v1.35%2C%20In-Place%20Resize%20GA%2C%20and%20What%20Comes%20Next" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-v1-33-whats-new%2F&amp;linkname=Kubernetes%20Today%3A%20v1.33%20to%20v1.35%2C%20In-Place%20Resize%20GA%2C%20and%20What%20Comes%20Next" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fkubernetes-v1-33-whats-new%2F&#038;title=Kubernetes%20Today%3A%20v1.33%20to%20v1.35%2C%20In-Place%20Resize%20GA%2C%20and%20What%20Comes%20Next" data-a2a-url="https://linuxcent.com/kubernetes-v1-33-whats-new/" data-a2a-title="Kubernetes Today: v1.33 to v1.35, In-Place Resize GA, and What Comes Next"></a></p><p>The post <a href="https://linuxcent.com/kubernetes-v1-33-whats-new/">Kubernetes Today: v1.33 to v1.35, In-Place Resize GA, and What Comes Next</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/kubernetes-v1-33-whats-new/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1662</post-id>	</item>
		<item>
		<title>The Platform Engineering Era: GitOps, AI Workloads, and Leaner Kubernetes (2023–2025)</title>
		<link>https://linuxcent.com/kubernetes-platform-engineering-gitops/</link>
					<comments>https://linuxcent.com/kubernetes-platform-engineering-gitops/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Wed, 15 Apr 2026 19:31:32 +0000</pubDate>
				<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[AI Infrastructure]]></category>
		<category><![CDATA[ArgoCD]]></category>
		<category><![CDATA[Cloud Native]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[GitOps]]></category>
		<category><![CDATA[Platform Engineering]]></category>
		<guid isPermaLink="false">https://linuxcent.com/kubernetes-platform-engineering-gitops/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span>GitOps became the default deployment model. AI workloads hit production clusters. Cluster API matured. Trace how v1.28–v1.32 made Kubernetes the substrate for platform engineering.</p>
<p>The post <a href="https://linuxcent.com/kubernetes-platform-engineering-gitops/">The Platform Engineering Era: GitOps, AI Workloads, and Leaner Kubernetes (2023–2025)</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;<br />
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;<br />
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;<br />
    font-size:.88em;line-height:1.6;border-left:4px solid #555}<br />
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}<br />
pre code{background:transparent;padding:0;color:inherit}<br />
pre[data-lang="bash"],pre[data-lang="sh"],<br />
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}<br />
pre[data-lang="yaml"],pre[data-lang="json"],<br />
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}<br />
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],<br />
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}<br />
pre[data-lang="text"],pre[data-lang="output"],<br />
pre[data-lang="console"]{border-left-color:#888}<br />
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;<br />
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;<br />
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;<br />
    transition:opacity .15s,background .15s;line-height:1.6}<br />
pre:hover .lc-copy-btn{opacity:1}<br />
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}<br />
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}<br />
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;<br />
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;<br />
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}<br />
pre:hover .lc-lang-badge{opacity:1}<br />
table{border-collapse:collapse;width:100%;margin:16px 0}<br />
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}<br />
th{background:#f0f0f0;font-weight:600}<br />
tr:nth-child(even){background:#fafafa}<br />
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<hr />
<h2 id="introduction">Introduction</h2>
<p>By 2023, the question had shifted from &#8220;how do we run Kubernetes?&#8221; to &#8220;how do we let other engineers run their workloads on Kubernetes without becoming a bottleneck?&#8221;</p>
<p>This is the platform engineering problem. And it drove the tooling that defined 2023–2025: GitOps as the deployment standard, Cluster API for Kubernetes-on-Kubernetes provisioning, AI/ML workloads forcing new scheduling capabilities, and the Kubernetes project itself shedding more weight to become faster to release and operate.</p>
<hr />
<h2 id="gitops-principle-becomes-practice">GitOps: Principle Becomes Practice</h2>
<p>GitOps as a term was coined by Weaveworks in 2017. By 2023, it was no longer a debate — it was the default deployment model for organizations running Kubernetes at scale.</p>
<p>The principle: the desired state of your cluster lives in Git. A controller watches the repository and reconciles the cluster state to match. Every deployment is a PR merge. The audit trail is the Git history.</p>
<p><strong>Flux v2</strong> (CNCF graduated) and <strong>ArgoCD</strong> (CNCF incubating) became the two dominant implementations:</p>
<pre><code class="" data-line=""># Flux: GitRepository + Kustomization
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: production-config
  namespace: flux-system
spec:
  interval: 1m
  url: https://github.com/org/k8s-config
  ref:
    branch: main
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: production-apps
  namespace: flux-system
spec:
  interval: 10m
  path: ./clusters/production
  prune: true          # Remove resources deleted from Git
  sourceRef:
    kind: GitRepository
    name: production-config
  healthChecks:
  - apiVersion: apps/v1
    kind: Deployment
    name: api
    namespace: production
</code></pre>
<p>The <code class="" data-line="">prune: true</code> behavior is critical: resources deleted from Git are deleted from the cluster. This is what makes GitOps a security control — unknown resources that aren&#8217;t in Git get removed. No more accumulation of forgotten test deployments, rogue debug pods, or unauthorized configuration changes that outlive the engineer who made them.</p>
<p><strong>ArgoCD&#8217;s Application model</strong> added a UI, synchronization policies, and multi-cluster management:</p>
<pre><code class="" data-line="">apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: production-api
  namespace: argocd
spec:
  project: production
  source:
    repoURL: https://github.com/org/apps
    targetRevision: HEAD
    path: api/production
  destination:
    server: https://kubernetes.default.svc
    namespace: api
  syncPolicy:
    automated:
      prune: true
      selfHeal: true    # Revert manual kubectl changes
    syncOptions:
    - CreateNamespace=true
</code></pre>
<p>The <code class="" data-line="">selfHeal: true</code> option is where GitOps becomes enforceable: any manual change made with kubectl is automatically reverted within the sync interval. For compliance-sensitive environments, this is a configuration drift prevention control.</p>
<hr />
<h2 id="cluster-api-kubernetes-managing-kubernetes">Cluster API: Kubernetes Managing Kubernetes</h2>
<p><strong>Cluster API</strong> (cluster-sigs/cluster-api) flipped the usual model: instead of using tools like Terraform or Ansible to provision Kubernetes clusters, Cluster API lets you manage Kubernetes clusters as Kubernetes resources — using a management cluster to provision and manage workload clusters.</p>
<pre><code class="" data-line=""># Create a new Kubernetes cluster as a Kubernetes resource
apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
  name: workload-cluster-prod
spec:
  clusterNetwork:
    pods:
      cidrBlocks: [&quot;192.168.0.0/16&quot;]
  infrastructureRef:
    apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
    kind: AWSCluster
    name: workload-cluster-prod
  controlPlaneRef:
    apiVersion: controlplane.cluster.x-k8s.io/v1beta1
    kind: KubeadmControlPlane
    name: workload-cluster-prod-control-plane
</code></pre>
<p>Cluster API reconciliation handles cluster provisioning, scaling, upgrades, and deletion — all through the Kubernetes API, with all the tooling (RBAC, audit logging, GitOps integration) that entails. Multi-cluster platform teams could now manage hundreds of workload clusters from a single management cluster.</p>
<hr />
<h2 id="kubernetes-128-sidecar-containers-alpha-august-2023">Kubernetes 1.28 — Sidecar Containers Alpha (August 2023)</h2>
<p>Sidecar containers had been a Kubernetes pattern since 2015 — a helper container in the same pod as the main application. But there was no native sidecar lifecycle management. Sidecars were just regular init containers or additional containers, which meant:<br />
&#8211; Init container sidecars ran before the application and had to block until they succeeded<br />
&#8211; Regular container sidecars had no ordering guarantees at startup<br />
&#8211; At pod termination, sidecars could die before the application finished draining</p>
<p>1.28 introduced native sidecar support: <strong>a new restartPolicy field for init containers</strong>:</p>
<pre><code class="" data-line="">spec:
  initContainers:
  - name: log-collector
    image: fluentbit:latest
    restartPolicy: Always    # This makes it a sidecar
    # Starts before main containers, stays running, stops after main containers exit
  containers:
  - name: application
    image: myapp:latest
</code></pre>
<p>A sidecar container (init container with <code class="" data-line="">restartPolicy: Always</code>):<br />
&#8211; Starts before application containers<br />
&#8211; Stays running throughout the pod lifecycle<br />
&#8211; Terminates automatically after all main containers exit<br />
&#8211; Restarts if it crashes (unlike regular init containers)</p>
<p>This solved the service mesh sidecar problem: Istio and Linkerd injected Envoy proxies as regular containers, leading to race conditions where the proxy hadn&#8217;t started when the application tried to make outbound connections. Native sidecar lifecycle guarantees the proxy is ready before the application starts.</p>
<p>Also in 1.28:<br />
&#8211; <strong>Retroactive default StorageClass assignment</strong>: Existing PVCs without a StorageClass assignment get the default applied retroactively — useful for migrations<br />
&#8211; <strong>Non-graceful node shutdown</strong> stable: Handle node power failures without manual pod cleanup<br />
&#8211; <strong>Recovery from volume expansion failure</strong>: Previously, a failed volume expansion left the PVC in a broken state; 1.28 introduced a mechanism to recover</p>
<hr />
<h2 id="aiml-workloads-force-new-kubernetes-capabilities">AI/ML Workloads Force New Kubernetes Capabilities</h2>
<p>The LLM wave of 2023 drove GPU workloads onto Kubernetes at a scale and urgency the project hadn&#8217;t anticipated. Running LLM inference on Kubernetes required solving problems that CPU-centric cluster scheduling hadn&#8217;t encountered:</p>
<p><strong>GPU topology awareness</strong>: Inference across multiple GPUs requires GPUs connected by NVLink or on the same PCIe switch, not arbitrary GPUs from different nodes or different PCIe buses. The Dynamic Resource Allocation API (1.26 alpha) was designed exactly for this.</p>
<p><strong>Fractional GPU allocation</strong>: NVIDIA&#8217;s time-slicing and MIG (Multi-Instance GPU) allow multiple pods to share a single GPU. The GPU operator (NVIDIA) manages this at the node level:</p>
<pre><code class="" data-line=""># Check GPU resources visible to Kubernetes
kubectl get nodes -o custom-columns=\
  &quot;NODE:.metadata.name,GPU:.status.allocatable.nvidia\.com/gpu&quot;
# NODE       GPU
# gpu-node-1   8
# gpu-node-2   8
</code></pre>
<p><strong>Batch scheduling for training jobs</strong>: Training runs require all workers to start simultaneously — a single missing GPU makes the entire job stall. The Kubernetes Job API doesn&#8217;t guarantee this. Projects like <strong>Volcano</strong> (CNCF incubating) and <strong>Kueue</strong> (Kubernetes SIG Scheduling) added gang scheduling: a job only starts when all requested resources are available.</p>
<pre><code class="" data-line=""># Kueue: queue AI training jobs with resource quotas
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: gpu-queue
spec:
  namespaceSelector: {}
  resourceGroups:
  - coveredResources: [&quot;nvidia.com/gpu&quot;, &quot;cpu&quot;, &quot;memory&quot;]
    flavors:
    - name: a100-80gb
      resources:
      - name: nvidia.com/gpu
        nominalQuota: 16
</code></pre>
<hr />
<h2 id="kubernetes-129-sidecar-to-beta-load-balancer-ip-mode-december-2023">Kubernetes 1.29 — Sidecar to Beta, Load Balancer IP Mode (December 2023)</h2>
<ul>
<li><strong>Sidecar containers</strong> beta: The lifecycle semantics were refined based on 1.28 alpha feedback</li>
<li><strong>Load balancer IP mode</strong> alpha: Distinguish between load balancers that use virtual IPs (kube-proxy handles the traffic) vs. those that handle traffic directly (no need for kube-proxy rules) — important for eBPF-based load balancers</li>
<li><strong>ReadWriteOncePod volume access</strong> stable</li>
</ul>
<hr />
<h2 id="kubernetes-130-structured-authorization-config-april-2024">Kubernetes 1.30 — Structured Authorization Config (April 2024)</h2>
<ul>
<li><strong>Structured authorization configuration</strong> beta: Define multiple authorization webhooks with explicit ordering, failure modes, and connection settings — replacing the flat <code class="" data-line="">--authorization-mode</code> flag</li>
<li><strong>Sidecar containers</strong> beta continues</li>
<li><strong>Node memory swap support</strong> beta: Allow pods to use swap memory — controversial but necessary for workloads with bursty memory patterns that prefer using swap over OOM kill</li>
</ul>
<pre><code class="" data-line=""># Node with swap enabled — kubelet config
kind: KubeletConfiguration
memorySwap:
  swapBehavior: LimitedSwap
</code></pre>
<p>The swap support feature reversed a long-standing Kubernetes hard stance: swap was disabled since 1.0 because its interaction with Kubernetes memory accounting was unpredictable. The 1.30 approach adds proper accounting and policies.</p>
<hr />
<h2 id="kubernetes-131-cloud-provider-code-removal-complete-august-2024">Kubernetes 1.31 — Cloud Provider Code Removal Complete (August 2024)</h2>
<p>1.31 marked the completion of the cloud provider code removal — the 1.5 million line migration that had been running since 1.26. Core binaries are 40% smaller. The API server, controller manager, and scheduler no longer contain vendor-specific code.</p>
<p>Also in 1.31:<br />
&#8211; <strong>Persistent Volume health monitor</strong> stable<br />
&#8211; <strong>AppArmor support</strong> stable: AppArmor profiles for pods using the native Kubernetes field (not annotations)<br />
&#8211; <strong>Traffic distribution for Services</strong> beta: Express topology preferences for Service routing (prefer local node, prefer same zone)</p>
<pre><code class="" data-line=""># Traffic distribution: prefer endpoints in the same zone
apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  trafficDistribution: PreferClose
  selector:
    app: api
  ports:
  - port: 80
    targetPort: 8080
</code></pre>
<hr />
<h2 id="kubernetes-132-sidecar-stable-dra-beta-december-2024">Kubernetes 1.32 — Sidecar Stable, DRA Beta (December 2024)</h2>
<ul>
<li><strong>Sidecar containers</strong> stable: After nearly a decade of workarounds, the sidecar pattern is a first-class Kubernetes primitive</li>
<li><strong>Dynamic Resource Allocation</strong> beta: GPU and specialized hardware scheduling ready for production evaluation</li>
<li><strong>Job API improvements</strong>: Success and failure policies for indexed jobs — granular control over batch workload behavior</li>
<li><strong>Custom Resource field selectors</strong>: Filter CRDs on arbitrary fields — making large CRD-based systems more efficient to query</li>
</ul>
<hr />
<h2 id="crossplane-kubernetes-as-the-control-plane-for-everything">Crossplane: Kubernetes as the Control Plane for Everything</h2>
<p>Crossplane (CNCF graduated) extended the Kubernetes API model beyond the cluster itself. Using CRDs and controllers, Crossplane lets you manage cloud resources (RDS databases, S3 buckets, VPCs, IAM roles) as Kubernetes resources — provisioned, updated, and deleted through the Kubernetes API.</p>
<pre><code class="" data-line=""># Crossplane: provision an RDS PostgreSQL instance as a Kubernetes resource
apiVersion: database.aws.crossplane.io/v1beta1
kind: RDSInstance
metadata:
  name: production-db
spec:
  forProvider:
    region: us-east-1
    dbInstanceClass: db.r6g.xlarge
    masterUsername: admin
    engine: postgres
    engineVersion: &quot;15&quot;
    allocatedStorage: 100
    multiAZ: true
  writeConnectionSecretsToRef:
    name: production-db-credentials
    namespace: production
</code></pre>
<p>For platform teams, Crossplane means a single control plane — the Kubernetes API — for both compute workloads and cloud infrastructure. GitOps tools (Flux, ArgoCD) manage both.</p>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>GitOps (Flux, ArgoCD) became the production deployment standard — not for ideological reasons, but because the audit trail, drift detection, and self-healing properties solve real operational and compliance problems</li>
<li>Cluster API made Kubernetes cluster lifecycle (provisioning, upgrades, deletion) a Kubernetes-native operation — the same API, tooling, and audit trail</li>
<li>Native sidecar containers (1.28 alpha → 1.32 stable) finally resolved the lifecycle ordering problem that service meshes and log collectors had worked around for years</li>
<li>AI/ML workloads drove new scheduling capabilities (DRA, gang scheduling via Kueue/Volcano) and made GPU topology awareness a first-class concern</li>
<li>Crossplane generalized the Kubernetes API model to cloud infrastructure — the cluster is now a control plane for everything, not just containers</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p><a href="ep06-runtime-reckoning.md">← EP06: The Runtime Reckoning</a> | <a href="ep08-kubernetes-today.md">EP08: Kubernetes Today →</a></p>
<p><em>Series: Kubernetes: From Borg to Platform Engineering | linuxcent.com</em></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-platform-engineering-gitops%2F&amp;linkname=The%20Platform%20Engineering%20Era%3A%20GitOps%2C%20AI%20Workloads%2C%20and%20Leaner%20Kubernetes%20%282023%E2%80%932025%29" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-platform-engineering-gitops%2F&amp;linkname=The%20Platform%20Engineering%20Era%3A%20GitOps%2C%20AI%20Workloads%2C%20and%20Leaner%20Kubernetes%20%282023%E2%80%932025%29" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-platform-engineering-gitops%2F&amp;linkname=The%20Platform%20Engineering%20Era%3A%20GitOps%2C%20AI%20Workloads%2C%20and%20Leaner%20Kubernetes%20%282023%E2%80%932025%29" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-platform-engineering-gitops%2F&amp;linkname=The%20Platform%20Engineering%20Era%3A%20GitOps%2C%20AI%20Workloads%2C%20and%20Leaner%20Kubernetes%20%282023%E2%80%932025%29" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-platform-engineering-gitops%2F&amp;linkname=The%20Platform%20Engineering%20Era%3A%20GitOps%2C%20AI%20Workloads%2C%20and%20Leaner%20Kubernetes%20%282023%E2%80%932025%29" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-platform-engineering-gitops%2F&amp;linkname=The%20Platform%20Engineering%20Era%3A%20GitOps%2C%20AI%20Workloads%2C%20and%20Leaner%20Kubernetes%20%282023%E2%80%932025%29" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-platform-engineering-gitops%2F&amp;linkname=The%20Platform%20Engineering%20Era%3A%20GitOps%2C%20AI%20Workloads%2C%20and%20Leaner%20Kubernetes%20%282023%E2%80%932025%29" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fkubernetes-platform-engineering-gitops%2F&#038;title=The%20Platform%20Engineering%20Era%3A%20GitOps%2C%20AI%20Workloads%2C%20and%20Leaner%20Kubernetes%20%282023%E2%80%932025%29" data-a2a-url="https://linuxcent.com/kubernetes-platform-engineering-gitops/" data-a2a-title="The Platform Engineering Era: GitOps, AI Workloads, and Leaner Kubernetes (2023–2025)"></a></p><p>The post <a href="https://linuxcent.com/kubernetes-platform-engineering-gitops/">The Platform Engineering Era: GitOps, AI Workloads, and Leaner Kubernetes (2023–2025)</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/kubernetes-platform-engineering-gitops/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1659</post-id>	</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: linuxcent.com @ 2026-08-26 04:04:42 by W3 Total Cache
-->