Turning a Pile of values Into an API
In one sentence
A CRD is not a device for adding new functionality. It lends the capabilities the API server already has to a type you define.
Why it was needed
Imagine a Helm chart that deploys an internal service. A values.yaml that started out with two entries, image and replicas, grows to forty lines a year later. And the same things keep happening.
- Someone wrote
replicas: "3"with quotes. The template still renders, the deployment succeeds, and the Pod never starts. - Someone made a typo,
tier: prd. Nothing catches it, and a wrong label is quietly attached. - A wiki page appears to explain "our chart's values schema," and that page is always older than the code.
- To find out what values are running in the cluster right now, you have to run
helm get valuesfor every release. You cannot select them all at once by label.
There is a single root cause. To the API server, values.yaml is just a blob of text. Nothing validates it, it is stored inside a release Secret and is hard to query, the audit log does not record who changed what and when at the level of individual resources, and you cannot use RBAC to grant permission to change just one value.
A CRD turns this problem around: "Then let's make it a real API object."
How it works
Once you apply a single CRD, the API server opens an endpoint at /apis/<그룹>/<버전>/namespaces/<ns>/<복수형> (group, version, and plural name), and from that moment the following come for free.
| Capability | values.yaml | CR |
|---|---|---|
| Schema validation | None (found only after rendering) | Rejected at apply time by OpenAPI v3 |
| Default injection | The default function inside the template |
The API server fills in the schema default |
| Unknown fields | Silently ignored | Pruned, or explicitly preserved |
| Storage | Release Secret | As an object in etcd |
| Query | helm get values |
kubectl get, label selectors, custom columns |
| Change watching | None | Watch stream |
| Permission separation | Per chart | RBAC per resource and subresource |
| Audit | Pipeline logs | Per object in the API audit log |
One more thing comes with this. You can use CEL rules inside the schema. Cross-field constraints such as self.tier != 'prod' || self.replicas >= 2 are checked inside the API server, without a webhook server. What used to require running a validating webhook is now one line in the schema.
And remember this without fail. A CRD alone makes nothing happen. When you apply a CR, the validated data is simply stored in etcd; no Pod starts and no backup runs. Only when a controller is attached that watches that type and reconciles does it become an Operator.
CRD = 새 어휘 (무엇을 원하는지 말하는 언어)
컨트롤러 = 그 어휘를 현실로 만드는 두뇌
오퍼레이터 = CRD + 컨트롤러
What you see in the field
First, self-service for platform teams. Most teams building an internal developer platform sell their abstractions through CRDs. A developer writes a single WebService, and the platform translates it into a Deployment, Service, Ingress, HPA, and NetworkPolicy. The surface a developer has to learn shrinks from 40 lines of values to a 5-line CR.
Second, the Prometheus Operator's ServiceMonitor. Instead of people editing one huge Prometheus configuration file, each team creates a small CR and declares, "scrape my service's metrics." The conflicts that came from many teams editing one configuration file turn into per-resource ownership.
Third, there are clear cases where you should not use one. If any of the following applies, a CRD is overkill.
- It is a stateless app that ends with a Deployment and an HPA. You only add an abstraction layer and make debugging harder.
- Once installed, there are almost no operational actions afterward. That is the place for Helm and GitOps.
- Nobody will maintain it over the long term. A CRD is code, and you have to keep up as Kubernetes versions advance.
- A well-maintained official Operator already exists. Look for one before building your own.
Fourth, a CRD is an API contract, not code. Controller code can be redeployed at any time, but you cannot casually change the thousands of CRs already stored in etcd or the manifests users have committed to Git. If you create one field wrongly, it follows you for years, from v1alpha1 to v1. That is why you start with a small API surface and pave the way for evolution in advance.
What to look for in the next check
In the quiz that follows, you will confirm why a CRD must be designed not as a mere YAML format but as an API contract with long-term compatibility. After reviewing the criteria for names, scope, schema, and version conversion, you will define the WebService type in the apps.labhub.io group yourself in the next module.