<?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>kubectl Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/kubectl/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/kubectl/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Sat, 09 May 2026 18:40:33 +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>kubectl Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/kubectl/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<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>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>
	</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-31 19:39:09 by W3 Total Cache
-->