Introduction
Kubernetes can be extended with new resource types. A CustomResourceDefinition, usually called a CRD, registers a new API resource with the Kubernetes API server. After that, you can use familiar kubectl commands to create, get, describe, label, and patch objects of that new type.
In this lab, you will create a small namespaced CRD named AppProfile, discover it with Kubernetes API discovery commands, create custom resources from YAML, and update them like regular Kubernetes objects. No controller is needed for this lab; the focus is understanding how custom resource types appear and how to work with their objects.
Prepare a Custom Resource Namespace
In this step, you will prepare a clean namespace for custom resource practice.
Start in /home/labex/project. You will write the CRD and custom resource manifests in this directory.
mkdir -p /home/labex/project
cd /home/labex/project
Check that the Kubernetes API server is reachable. If the VM has just started and the node is not ready yet, wait a few seconds and run the command again.
kubectl get nodes --request-timeout=5s
Expected output includes:
NAME STATUS ROLES AGE VERSION
labex-v135 Ready control-plane ... v1.35...
Wait until the cluster networking Pod is Ready before creating application objects.
kubectl wait --namespace kube-system --for=condition=Ready pod -l k8s-app=calico-node --timeout=120s
Expected output:
pod/calico-node-... condition met
Create a namespace named ckad-prep-customresources:
kubectl create namespace ckad-prep-customresources
Set this namespace as the current context namespace:
kubectl config set-context --current --namespace=ckad-prep-customresources
Confirm the current namespace:
kubectl config view --minify --output 'jsonpath={..namespace}'; echo
Expected output:
ckad-prep-customresources
Register a New Resource Type with a CRD
In this step, you will create a CustomResourceDefinition named AppProfile.
A CRD has two important naming layers. The metadata.name is the plural resource name plus the API group, such as appprofiles.training.labex.io. The spec.names section defines how people will refer to the resource: kind: AppProfile, plural: appprofiles, singular: appprofile, and a short name appprof.
Create app-profile-crd.yaml:
cat > app-profile-crd.yaml <<'EOF'
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: appprofiles.training.labex.io
spec:
group: training.labex.io
scope: Namespaced
names:
plural: appprofiles
singular: appprofile
kind: AppProfile
shortNames:
- appprof
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required:
- image
- replicas
properties:
image:
type: string
replicas:
type: integer
minimum: 1
tier:
type: string
EOF
Apply the CRD:
kubectl apply -f app-profile-crd.yaml
Expected output:
customresourcedefinition.apiextensions.k8s.io/appprofiles.training.labex.io created
Wait until the API server reports that the CRD is established:
kubectl wait --for=condition=Established crd/appprofiles.training.labex.io --timeout=120s
Expected output:
customresourcedefinition.apiextensions.k8s.io/appprofiles.training.labex.io condition met
Inspect the CRD's names and scope:
kubectl get crd appprofiles.training.labex.io -o 'jsonpath={.spec.names.kind}{" "}{.spec.names.plural}{" "}{.spec.scope}{"\n"}'
Expected output:
AppProfile appprofiles Namespaced
Discover and Create Custom Resources
In this step, you will discover the new resource type and create your first AppProfile.
kubectl api-resources lists resource types known to the API server. After a CRD is established, it appears in this discovery output beside built-in resource types.
kubectl api-resources --api-group=training.labex.io
Expected output includes:
NAME SHORTNAMES APIVERSION NAMESPACED KIND
appprofiles appprof training.labex.io/v1 true AppProfile
Use kubectl explain to read the schema Kubernetes learned from the CRD. The spec field is where the application-specific settings live.
kubectl explain appprofile.spec
Expected output includes:
GROUP: training.labex.io
KIND: AppProfile
VERSION: v1
FIELD: spec <Object>
Create frontend-profile.yaml:
cat > frontend-profile.yaml <<'EOF'
apiVersion: training.labex.io/v1
kind: AppProfile
metadata:
name: frontend
labels:
app.kubernetes.io/part-of: storefront
tier: web
spec:
image: nginx:1.27
replicas: 2
tier: web
EOF
Apply the custom resource:
kubectl apply -f frontend-profile.yaml
Expected output:
appprofile.training.labex.io/frontend created
List the custom resources with the plural name:
kubectl get appprofiles
Expected output includes:
NAME AGE
frontend ...
Read fields from the custom resource with jsonpath:
kubectl get appprofile frontend -o 'jsonpath={.spec.image}{" "}{.spec.replicas}{" "}{.spec.tier}{"\n"}'
Expected output:
nginx:1.27 2 web
Update and Select Custom Resources
In this step, you will manage custom resources with the same kubectl patterns you already use for built-in objects.
Patch the frontend custom resource to change its desired replica count. The --type=merge option tells Kubernetes to merge your JSON object into the existing object instead of replacing the whole resource.
kubectl patch appprofile frontend --type=merge -p '{"spec":{"replicas":3}}'
Expected output:
appprofile.training.labex.io/frontend patched
Confirm the updated value:
kubectl get appprofile frontend -o 'jsonpath={.spec.replicas}{"\n"}'
Expected output:
3
Create a second custom resource named worker:
cat > worker-profile.yaml <<'EOF'
apiVersion: training.labex.io/v1
kind: AppProfile
metadata:
name: worker
labels:
app.kubernetes.io/part-of: storefront
tier: worker
spec:
image: busybox:1.36
replicas: 1
tier: worker
EOF
kubectl apply -f worker-profile.yaml
Expected output:
appprofile.training.labex.io/worker created
List only the custom resources that belong to the storefront application. This works because custom resources support normal Kubernetes labels.
kubectl get appprofiles -l app.kubernetes.io/part-of=storefront
Expected output includes:
NAME AGE
frontend ...
worker ...
Use the short name appprof to print each resource name and replica count:
kubectl get appprof -o 'jsonpath={range .items[*]}{.metadata.name}{"="}{.spec.replicas}{"\n"}{end}'
Expected output includes:
frontend=3
worker=1
Summary
You created a CRD, waited for it to become established, discovered the new API resource with kubectl api-resources and kubectl explain, created custom resources, patched a custom resource, selected them by label, and used a short name. These are the same operational skills you will use when CKAD tasks involve resources installed by an operator, controller, or platform extension.


