<?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>Hands-On Tutorial Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/hands-on-tutorial/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/hands-on-tutorial/</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>Hands-On Tutorial Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/hands-on-tutorial/</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>
	</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 10:39:25 by W3 Total Cache
-->