CRD Design & kubebuilder Scaffolding
Custom Resource Definitions expose your operator's domain language to cluster users as YAML.
Search across all documentation pages
Custom Resource Definitions expose your operator's domain language to cluster users as YAML.
kubebuilder turns Go API types and marker comments into CRD manifests, RBAC rules, and webhook stubs through controller-gen.
Define types under api/v1/ (and future versions) with struct fields tagged for JSON and validation.
Marker comments like +kubebuilder:validation:Minimum=1 become OpenAPI constraints on the CRD.
Run make manifests to regenerate YAML whenever types change.
Plan versioning early: add v2 as a new package, mark one version as storage, implement conversion if fields rename.
Quick-reference recipe card - copy-paste ready.
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced
// +kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=".spec.frontendSize"
// +kubebuilder:printcolumn:name="Ready",type=boolean,JSONPath=".status.ready"
type Guestbook struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec GuestbookSpec `json:"spec,omitempty"`
Status GuestbookStatus `json:"status,omitempty"`
}
type GuestbookSpec struct {
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=10
FrontendSize int32 `json:"frontendSize"`
}When to reach for this:
create apikubectl get columns for support engineers// api/v1/guestbook_types.go
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// GuestbookSpec defines the desired state of Guestbook.
type GuestbookSpec struct {
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=10
FrontendSize int32 `json:"frontendSize"`
// +kubebuilder:validation:Enum=small;medium;large
Tier string `json:"tier,omitempty"`
}
// GuestbookStatus defines the observed state of Guestbook.
type GuestbookStatus struct {
Ready bool `json:"ready,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:storageversion
// +kubebuilder:resource:shortName=gb
// +kubebuilder:printcolumn:name="Tier",type=string,JSONPath=".spec.tier"
// +kubebuilder:printcolumn:name="Ready",type=boolean,JSONPath=".status.ready"
type Guestbook struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec GuestbookSpec `json:"spec,omitempty"`
Status GuestbookStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
type GuestbookList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Guestbook `json:"items"`
}
func init() {
SchemeBuilder.Register(&Guestbook{}, &GuestbookList{})
}# Makefile target (generated by kubebuilder)
controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/basesWhat this demonstrates:
+kubebuilder:storageversion pins etcd storage to this versionmake manifests feeds config/crd/bases/webapp.example.com_guestbooks.yaml+kubebuilder: markers and emits CRD YAML, ClusterRole rules, and webhook configurations.config/crd/bases/; kustomize overlays patch names, namespaces, and webhook CA bundles for install.PROJECT file domain + create api --group flag (webapp.example.com/v1).subresources.status stanza and RBAC */status permissions generated from +kubebuilder:rbac markers on reconcilers.| Pattern | Purpose |
|---|---|
v1alpha1 | Experimental, breaking changes allowed |
v1beta1 | Field set stabilizing, conversion may appear |
v1 | GA-compatible; prefer backward-compatible additions |
| Hub-spoke conversion | Map v1 and v2 fields through a hub type |
| Marker | Effect |
|---|---|
+kubebuilder:validation:Required | Field required in OpenAPI |
+kubebuilder:default=value | Default injected by CRD |
+kubebuilder:validation:Pattern | Regex on strings |
+kubebuilder:rbac:groups=...,resources=...,verbs=... | RBAC rule in config/rbac |
+kubebuilder:webhook:... | Validating or mutating webhook config |
// Reconciler RBAC markers (on controller file)
// +kubebuilder:rbac:groups=webapp.example.com,resources=guestbooks,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=webapp.example.com,resources=guestbooks/status,verbs=get;update;patchmake manifests overwrites changes. Fix: change Go types and markers, then regenerate.v2, implement conversion, keep v1 served.GuestbookList with +kubebuilder:object:root=true.+kubebuilder:storageversion across versions.omitempty on scalars cannot distinguish zero from unset for validation. Fix: use pointers for optional integers or enums when zero is valid.Status().Update returns 403. Fix: add guestbooks/status verbs to rbac markers.| Alternative | Use When | Don't Use When |
|---|---|---|
| kubebuilder + controller-gen | Go operators with typed APIs | CRD is maintained by a separate team in raw YAML |
| Hand-written CRD YAML | Language-agnostic schema source of truth | You want RBAC and webhook codegen from Go |
| Kubebuilder declarative validation only | Simple field constraints | Complex cross-field rules need CEL or webhooks |
| Admission webhooks for validation | Policies beyond OpenAPI | Every simple min/max rule (prefer markers first) |
controller-gen invoked by make manifests reads Go types and kubebuilder markers.
Output is under config/crd/bases/.
Run kubebuilder create api --group webapp --version v2 --kind Guestbook, implement conversion, mark storage version on the canonical version.
The CRD version etcd persists objects as.
Other served versions are converted to and from storage on read/write.
Markers are comments interpreted by controller-gen at codegen time.
Invalid marker syntax fails make manifests, not go build.
They add kubectl get columns without custom plugins.
JSONPath must match published CRD schema fields.
When rules involve multiple fields (spec.end > spec.start) that OpenAPI per-field markers cannot express.
Add +kubebuilder:validation:XValidation rules in newer kubebuilder releases or patch CRD YAML.
Domain, repo path, and layout version kubebuilder uses for scaffolding commands.
Keep it in version control.
Use +kubebuilder:resource:scope=Cluster on cluster-scoped kinds.
RBAC and namespace handling in reconcilers change accordingly.
+kubebuilder:resource:shortName=gb allows kubectl get gb.
Still requires users to know the API group for ambiguous short names.
Yes.
Kubernetes convention and RBAC separation require spec user-writable and status controller-writable.
make installStack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026