Asterrr's Handbook

CRDs and operators

Writing a CustomResourceDefinition with a schema, versions and printer columns, working with custom resources through kubectl, and installing and checking an operator.

Exam tasks: 1.8 (understand CRDs, install and configure operators)

The decision: what does a CRD add to the API, what does it take for a custom resource to actually do something, and how do you install, inspect and configure the operator that makes it happen?

CRD, custom resource, controller

PieceWhat it isWithout it
CRDA cluster-scoped object that registers a new resource type, its schema and versionskubectl apply of the custom kind fails with "no matches for kind"
Custom resource (CR)An instance of that type, stored in etcd like any objectNothing to act on
ControllerCode that watches the CRs and reconciles the real world toward .spec, reporting in .statusCRs are stored and listed, but nothing happens
OperatorA controller (plus its CRDs) that encodes how to run one piece of software: install, upgrade, back up, fail over

Writing a CRD

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: backups.vault.lab.io          # must be <plural>.<group>
spec:
  group: vault.lab.io
  scope: Namespaced                   # or Cluster
  names:
    plural: backups
    singular: backup
    kind: Backup
    shortNames: ["bk"]
  versions:
  - name: v1
    served: true
    storage: true                     # exactly one version is the storage version
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            required: ["schedule", "target"]
            properties:
              schedule:
                type: string
              target:
                type: string
              keepLast:
                type: integer
                minimum: 1
                default: 7
            x-kubernetes-validations:
            - rule: "self.target.startsWith('pvc/')"
              message: "target must look like pvc/NAME"
          status:
            type: object
            properties:
              lastRun:
                type: string
    subresources:
      status: {}                      # .status is written through /status only
    additionalPrinterColumns:
    - name: Schedule
      type: string
      jsonPath: .spec.schedule
    - name: Last-Run
      type: string
      jsonPath: .status.lastRun
  • apiextensions.k8s.io/v1 requires a structural schema for every version. Unknown fields are pruned unless you mark a subtree x-kubernetes-preserve-unknown-fields: true.
  • x-kubernetes-validations holds CEL rules the API server checks on create and update. Defaults (default:) are applied on read and write.
  • Several versions can be served, but only one is storage. Conversion between versions uses None (fields must be compatible) or a conversion webhook.

Deleting a CRD

Deleting the CustomResourceDefinition deletes every custom resource of that type in every namespace. If a task says to remove an operator but keep its data, delete the operator Deployment, not the CRD.

Working with custom resources

apiVersion: vault.lab.io/v1
kind: Backup
metadata:
  name: ledger-nightly
  namespace: ledger
spec:
  schedule: "0 2 * * *"
  target: pvc/ledger-data
k get crd | grep vault.lab.io
k api-resources --api-group=vault.lab.io      # kind, short names, namespaced or not
k explain backup.spec                          # schema-driven docs, same as built-in kinds
k apply -f ledger-nightly.yaml
k get bk -n ledger                             # printer columns show up here
k get backups.vault.lab.io -A -o yaml          # fully qualified name avoids clashes
k describe backup ledger-nightly -n ledger     # status, events from the controller

Exam signal

Tasks often ask you to find something about a CRD rather than write one: its group, its versions, whether it's namespaced, or which fields its spec has. k get crd <name> -o yaml, k api-resources --api-group=... and k explain <kind>.spec --recursive answer those without reading the operator's docs.

Installing an operator

MethodLooks likeCheck with
Plain manifestsk apply -f https://.../release/v1.4.0/install.yaml (CRDs, namespace, RBAC, Deployment)k get crd, k get deploy -n <operator-ns>
Helm charthelm install ... --set crds.enabled=true or a chart that ships crds/helm list -A, k get crd
Operator Lifecycle ManagerA Subscription to a catalog; OLM installs and upgrades the operatork get csv -A, k get subscription -A

An operator install always gives you three things to verify:

  1. CRDs registered: k get crd | grep <group>.
  2. RBAC: a ServiceAccount with a ClusterRole that lets it watch its CRs and manage what it creates.
  3. The controller Deployment running: k get pods -n <operator-ns>, and k logs deploy/<name> -n <operator-ns> when CRs don't reconcile.
  • Configure the operator itself through its Deployment arguments, a ConfigMap or Helm values (for example which namespaces it watches). Configure what it manages through the custom resources.
  • Install CRDs before CRs. Applying both in one file can fail on the first run because the CRD isn't established yet; rerunning the apply, or k wait --for=condition=Established crd/<name>, fixes it.
  • Helm installs CRDs from a chart's crds/ folder only on first install and never upgrades or deletes them. Upgrade those CRDs with kubectl apply as the operator's docs describe.

Custom resources that never change status

If CRs are accepted but .status stays empty and nothing gets created, the CRD is fine and the controller isn't: it isn't running, is watching other namespaces, or lacks RBAC. Read the operator's logs for forbidden errors before touching the CR.

Scenarios

Scenario
You apply a manifest containing kind: Backup with apiVersion vault.lab.io/v1 and get: no matches for kind Backup in version vault.lab.io/v1. `kubectl get crd` lists backups.vault.lab.io. What's the most likely cause?
Scenario
A team wants to remove a misbehaving database operator from the cluster, but the 14 Database custom resources it manages must stay so a newer operator version can adopt them next week. What should you delete?

Drill

kubectl config use-context cka-crd

  1. An operator's install manifest is at /opt/op/beacon-operator.yaml. Install it and wait until its CRD beacons.signal.lab.io is established and the operator Pod is running in namespace beacon-system.
  2. Create a Beacon named north in namespace relay with spec.intervalSeconds set to 30 and spec.endpoint set to http://relay-api.relay:8080.
  3. Write the CRD's scope (Namespaced or Cluster) and its storage version to /opt/answers/beacon.txt.

Further reading

On this page