Kubernetes CRDs & Operators: Extending the API, Episode 2
What Is a CRD? · CRDs You Already Use · CRD Anatomy · Write Your First CRD · CEL Validation · Controller Loop · Build an Operator · CRD Versioning · Admission Webhooks · CRDs in Production
TL;DR
- cert-manager, KEDA, and External Secrets Operator are all CRD-based systems — understanding their custom resources shows you what a well-designed CRD looks like before you build one
- cert-manager’s
CertificateCRD expresses desired TLS state; the cert-manager controller reconciles that state by issuing, renewing, and storing certificates in Secrets - KEDA’s
ScaledObjectextends the HorizontalPodAutoscaler with external metrics (queue depth, Kafka lag, Prometheus queries) — the KEDA operator translates ScaledObjects into native HPA objects - External Secrets Operator’s
ExternalSecretabstracts over secret backends (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager) — the controller pulls values and writes Kubernetes Secrets - All three follow the same pattern: you describe desired state in a custom resource; the operator reconciles actual state to match
- Kubernetes custom resources examples like these are the fastest way to internalize the CRD mental model before writing your own
The Big Picture
THREE CRD-BASED OPERATORS AND WHAT THEY MANAGE
┌─────────────────────────────────────────────────────────────┐
│ cert-manager │
│ Certificate CR → controller issues cert → TLS Secret │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ KEDA │
│ ScaledObject CR → controller creates HPA → Pod count │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ External Secrets Operator │
│ ExternalSecret CR → controller pulls → K8s Secret │
│ from Vault/AWS/GCP │
└─────────────────────────────────────────────────────────────┘
In every case:
User creates CR → Operator watches CR → Operator acts → Status updated
Kubernetes custom resources examples from real tools like these reveal the design pattern you will use in every CRD you build: express desired state declaratively, let the controller bridge the gap to actual state, surface the outcome in the status subresource.
Why Look at Existing CRDs First?
Before designing your own CRD, you want to understand what good CRD design looks like from the user’s perspective. The engineers at Jetstack (cert-manager), KEDACORE (KEDA), and External Secrets contributors have collectively solved the same problems you will face:
- What goes in
specvsstatus? - How do you reference other Kubernetes objects?
- How do you handle secrets and credentials securely?
- What does a healthy vs unhealthy custom resource look like?
Studying these before writing your own saves you from the most common first-timer mistakes.
cert-manager: The Certificate CRD
cert-manager is the most widely deployed CRD-based system in Kubernetes. It manages TLS certificates from Let’s Encrypt, internal CAs, and cloud providers.
The core CRDs
kubectl get crds | grep cert-manager
certificates.cert-manager.io
certificaterequests.cert-manager.io
challenges.acme.cert-manager.io
clusterissuers.cert-manager.io
issuers.cert-manager.io
orders.acme.cert-manager.io
The one you interact with most is Certificate. Here is a real example:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: api-tls
namespace: production
spec:
secretName: api-tls-cert # cert-manager writes the TLS Secret here
duration: 2160h # 90 days
renewBefore: 720h # renew 30 days before expiry
subject:
organizations:
- example.com
dnsNames:
- api.example.com
- api-internal.example.com
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
What happens after you apply this:
- cert-manager controller sees the new
Certificateobject - It contacts the referenced
ClusterIssuer(Let’s Encrypt in this case) - It completes the ACME challenge, obtains the certificate
- It writes the certificate and private key into the
api-tls-certSecret - It updates the
Certificateobject’sstatusto reflect success
kubectl describe certificate api-tls -n production
Status:
Conditions:
Last Transition Time: 2026-04-10T08:00:00Z
Message: Certificate is up to date and has not expired
Reason: Ready
Status: True
Type: Ready
Not After: 2026-07-09T08:00:00Z
Not Before: 2026-04-10T08:00:00Z
Renewal Time: 2026-06-09T08:00:00Z
What this teaches you about CRD design
spec.secretName— the CR references an output object by name. The controller creates or updates that object.spec.issuerRef— the CR references another custom resource (ClusterIssuer) by name. This is a common pattern for separating configuration concerns.status.conditions— the standard Kubernetes condition pattern:type,status,reason,message. You will use the same structure in your own CRDs.- The controller owns
status— users ownspec. This separation is a core convention.
KEDA: The ScaledObject CRD
KEDA (Kubernetes Event-Driven Autoscaling) extends Kubernetes autoscaling beyond CPU and memory. It can scale deployments based on queue depth, Kafka consumer lag, Prometheus metric values, and dozens of other event sources.
The core CRDs
kubectl get crds | grep keda
clustertriggerauthentications.keda.sh
scaledjobs.keda.sh
scaledobjects.keda.sh
triggerauthentications.keda.sh
A ScaledObject ties a Deployment to an external scaler:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: order-processor-scaler
namespace: production
spec:
scaleTargetRef:
name: order-processor # the Deployment to scale
minReplicaCount: 0 # scale to zero when idle
maxReplicaCount: 50
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.us-east-1.amazonaws.com/123456789/orders
queueLength: "5" # target: 5 messages per pod
awsRegion: us-east-1
authenticationRef:
name: keda-sqs-auth # TriggerAuthentication for AWS credentials
What KEDA does with this:
- KEDA controller sees the
ScaledObject - It creates a native
HorizontalPodAutoscalerobject targeting theorder-processorDeployment - KEDA’s metrics adapter polls the SQS queue depth and exposes it as a custom metric
- The HPA uses that metric to scale replicas — including to zero when the queue is empty
kubectl get scaledobject order-processor-scaler -n production
NAME SCALETARGETKIND SCALETARGETNAME MIN MAX TRIGGERS READY ACTIVE
order-processor-scaler apps/Deployment order-processor 0 50 aws-sqs-queue True True
What this teaches you about CRD design
spec.scaleTargetRef— targeting another object by name. The controller acts on that object, not on the CR itself.spec.triggers— a list of trigger specifications. Lists of typed sub-objects are a recurring CRD pattern.spec.minReplicaCount: 0— expressing scale-to-zero as a first-class concept in the API. Built-in HPA does not support this; KEDA’s CRD extends the vocabulary of what is expressible.- The KEDA operator translates
ScaledObject→ native HPA. The CRD is an abstraction over a more complex Kubernetes object. This “translate and manage child resources” pattern is extremely common in operators.
External Secrets Operator: The ExternalSecret CRD
External Secrets Operator (ESO) solves a specific problem: secrets live in external systems (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager), but Kubernetes workloads need them as Kubernetes Secrets. ESO bridges the gap.
The core CRDs
kubectl get crds | grep external-secrets
clusterexternalsecrets.external-secrets.io
clustersecretstores.external-secrets.io
externalsecrets.external-secrets.io
secretstores.external-secrets.io
A SecretStore defines the backend connection:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets-manager
namespace: production
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: eso-sa # uses IRSA/workload identity
An ExternalSecret defines what to pull and how to map it:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-creds
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: database-secret # Kubernetes Secret to create/update
creationPolicy: Owner
data:
- secretKey: username # key in the K8s Secret
remoteRef:
key: prod/database # path in AWS Secrets Manager
property: username # property within that secret
- secretKey: password
remoteRef:
key: prod/database
property: password
After ESO reconciles this:
kubectl get secret database-secret -n production -o jsonpath='{.data.username}' | base64 -d
# outputs: db_user
kubectl describe externalsecret database-creds -n production
Status:
Conditions:
Last Transition Time: 2026-04-10T08:00:00Z
Message: Secret was synced
Reason: SecretSynced
Status: True
Type: Ready
Refresh Time: 2026-04-10T09:00:00Z
Synced Resource Version: 1-abc123
What this teaches you about CRD design
spec.secretStoreRef— referencing a configuration CRD (SecretStore) from an operational CRD (ExternalSecret). This layering of CRDs to separate concerns is a mature pattern.spec.refreshInterval— the CR expresses a desired behavior (periodic sync), not just a desired state snapshot. CRDs can express temporal behaviors.spec.target.creationPolicy: Owner— ESO will set an owner reference on the created Secret, so deleting theExternalSecretcascades to deleting the Secret. This is how controllers manage lifecycle.- Sensitive values never appear in the CR — only paths and references. The controller handles the actual secret retrieval. This is a key security pattern in CRD design.
The Common Pattern Across All Three
OPERATOR PATTERN (cert-manager / KEDA / ESO / every other operator)
User applies CR
│
▼
Controller watches CRDs
(informer cache, events queue)
│
▼
Controller reconciles:
actual state ──→ compare ──→ desired state
│ │
│ (gap found)
│ │
▼ ▼
Takes action Updates status
(issue cert, conditions in CR
create HPA,
sync Secret)
│
└──── loops back, watches for next change
The design contract:
– Users write spec — what they want
– Controllers read spec, write status — what actually happened
– Status conditions are truth — Ready: True/False with reason and message tell operators what the controller knows
This pattern, explained in depth in EP06, is why CRDs and controllers are designed the way they are.
⚠ Common Mistakes
Installing CRDs without the controller. If you install cert-manager’s CRDs from the crds.yaml manifest without installing cert-manager itself, Certificate objects will be accepted by the API server but never reconciled. The Ready condition will never appear. Always install the operator alongside its CRDs.
Editing status fields directly. Many teams try kubectl patch or kubectl edit to update a custom resource’s status to work around a stuck controller. Most well-written controllers overwrite status every reconcile loop — your manual change will be wiped. Fix the underlying issue, not the status display.
Assuming CRD deletion is safe. Covered in EP01 but worth repeating: deleting a CRD cascades to deleting all instances. If you kubectl delete crd certificates.cert-manager.io, every Certificate object in every namespace is gone and cert-manager will stop issuing. Back up CRDs and their instances before any CRD deletion.
Quick Reference
# See all CRDs installed by cert-manager
kubectl get crds | grep cert-manager.io
# Get all Certificates across all namespaces
kubectl get certificates -A
# Watch cert-manager reconcile a new Certificate
kubectl get certificate api-tls -n production -w
# See all ScaledObjects and their current state
kubectl get scaledobjects -A
# Check ESO sync status for all ExternalSecrets
kubectl get externalsecrets -A
# Inspect what APIs a CRD exposes
kubectl api-resources | grep cert-manager
Key Takeaways
- cert-manager, KEDA, and ESO are canonical examples of well-designed CRD-based operators
- All three follow the same pattern: user writes
spec, controller reconciles to actual state, status reflects outcome specexpresses desired state declaratively; the controller figures out how to achieve it- Status conditions (
type,status,reason,message) are the standard way to surface controller outcomes - Sensitive values never appear in the CR — controllers retrieve them from external systems using references and credentials
What’s Next
EP03: CRD Anatomy opens the YAML of a CRD itself — spec.versions, OpenAPI schema properties, scope, names, and subresources. You have seen CRDs from the outside; next we look at how they are structured on the inside.
Get EP03 in your inbox when it publishes → subscribe at linuxcent.com