Extending Kubernetes & Hệ sinh tháiExtending Kubernetes & the Ecosystem
Thuộc bộ tài liệu kiến thức theo Kubernetes Roadmap.
Tổng quan
Kubernetes đi kèm một tập object dựng sẵn cố định — Pod, Deployment, Service, v.v. — nhưng sức mạnh thực sự của nó là bản thân API có thể mở rộng. Bạn có thể dạy Kubernetes biết về những loại object hoàn toàn mới (một Database, một Certificate, một KafkaCluster) và viết phần mềm quản lý chúng theo đúng cách khai báo, self-healing như các controller dựng sẵn quản lý Pod. Đây là cách hệ sinh thái bùng nổ: cert-manager, Prometheus Operator, Istio, ArgoCD, Crossplane, và hàng trăm thứ khác không phải là fork của Kubernetes — chúng là phần mở rộng xây trên cùng các primitive.
Ý tưởng xuyên suốt tất cả là mẫu controller / reconciliation. Một controller là một vòng lặp theo dõi object, so sánh trạng thái mong muốn (spec) với trạng thái quan sát được (status), và hành động để thu hẹp khoảng cách — lặp đi lặp lại, mãi mãi. Controller Deployment dựng sẵn làm điều này cho Pod; một Operator tùy chỉnh làm điều đó cho domain ứng dụng của bạn. Một khi thấm được “mọi thứ trong Kubernetes là một control loop”, việc mở rộng nó hết còn bí ẩn: bạn định nghĩa một loại object mới (một CRD) và viết một controller reconcile nó.
Trang này bao quát hai tầng. Thứ nhất, cách mở rộng Kubernetes: CRD, mẫu Operator và các công cụ dựng chúng (Kubebuilder, Operator SDK, controller-runtime), cùng admission webhook và aggregated API server. Thứ hai, hệ sinh thái bạn cắm vào: service mesh, GitOps, quản lý multi-cluster, và serverless — bức tranh CNCF landscape bạn sẽ điều hướng với vai operator. Xuyên suốt, một câu hỏi thực tế lặp lại: khi nào thực sự nên viết một Operator thay vì chỉ dùng Helm?
Kiến thức nền tảng
Mẫu controller / reconciliation
Mọi controller trong Kubernetes — dựng sẵn hay tùy chỉnh — chạy cùng một vòng lặp level-triggered:
- 01theo dõi objectqua API server / informer
- sự kiện: có gì đó thay đổi02Reconcile(request)1. đọc trạng thái mong muốn (spec) — 2. đọc trạng thái thực tế (thế giới thật) — 3. nếu khác nhau, thực hiện MỘT bước để hội tụ, rồi cập nhật status — 4. return (requeue nếu còn việc)
Hai đặc tính khiến nó bền vững:
- Level-triggered, không phải edge-triggered. Controller không phản ứng với sự kiện (“một Pod bị xóa”); nó phản ứng với trạng thái (“mong muốn 3, thực tế 2 → tạo một”). Nên nó khôi phục đúng từ sự kiện bị bỏ lỡ, restart, và lỗi bộ phận — nó luôn tái suy ra hành động đúng từ thực tại hiện tại.
- Idempotent và hội tụ.
Reconcilecó thể được gọi nhiều lần cho cùng một object; mỗi lần gọi tiến về mục tiêu hoặc không làm gì. Bạn không bao giờ giả định đã thấy mọi trạng thái trung gian.
Custom Resource Definition (CRD)
Một CRD đăng ký một kind object mới với API server. Sau khi bạn apply một CRD, kubectl get databases hoạt động y hệt kubectl get pods — cùng kho lưu trữ (etcd), cùng RBAC, cùng kubectl, cùng label/annotation. Bản thân một CRD chỉ là kho lưu có cấu trúc kèm validation; nó không làm gì cho tới khi một controller theo dõi nó.
Custom Resource + một controller = một Operator
Mẫu Operator đơn giản là: một CRD (API) + một controller tùy chỉnh (hành vi) mã hóa kiến thức vận hành của một chuyên gia con người. Trong khi controller Deployment biết cách giữ N Pod chạy, một operator PostgresCluster biết cách provision một primary và các replica, chụp backup, failover, và chạy upgrade phiên bản — tự động hóa việc mà một DBA lẽ ra làm bằng tay. Operator chính là SRE, được biểu đạt thành code.
Bộ công cụ dựng
Bạn hiếm khi viết vòng watch client-go thô. Stack chuẩn:
- controller-runtime — thư viện Go nằm dưới mọi thứ; cung cấp Manager, informer/cache, client, và interface
Reconcile. - Kubebuilder — scaffold một project: sinh kiểu CRD, RBAC, khung controller, và manifest. Cách khởi đầu de facto.
- Operator SDK — xây trên Kubebuilder và thêm operator dựa Helm và Ansible (không cần Go cho ca đơn giản) cùng đóng gói OLM (Operator Lifecycle Manager).
Admission webhook và aggregated API
Ngoài việc thêm object mới, bạn có thể chặn và mở rộng đường đi của request API:
- Admission webhook chạy trong lúc một write API, sau authn/authz nhưng trước khi object được lưu. Webhook mutating có thể sửa object (chèn sidecar, thêm default); webhook validating có thể từ chối nó (thực thi policy). Đây là cách Istio chèn proxy và cách Kyverno/Gatekeeper thực thi luật.
- Aggregated API server đăng ký một API server hoàn toàn riêng phía sau apiserver chính cho một API group nhất định — dùng khi bạn cần storage hoặc hành vi tùy chỉnh mà CRD không biểu đạt được (API
metrics.k8s.iocủa metrics-server là ví dụ kinh điển). Nặng hơn CRD; hiếm khi cần đến.
Khái niệm chính
Một CRD tối thiểu
Đây là một CRD nhỏ định nghĩa kind CronTab trong group stable.example.com, kèm schema OpenAPI (validation) và printer column:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: crontabs.stable.example.com # phải là <plural>.<group>
spec:
group: stable.example.com
scope: Namespaced # hoặc Cluster
names:
plural: crontabs
singular: crontab
kind: CronTab
shortNames: [ct]
versions:
- name: v1
served: true # được expose qua API
storage: true # phiên bản NÀY được lưu trong etcd
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
cronSpec: { type: string, pattern: '^(\S+\s+){4}\S+$' }
image: { type: string }
replicas: { type: integer, minimum: 1, default: 1 }
required: [cronSpec, image]
status:
type: object
properties:
lastScheduleTime: { type: string, format: date-time }
subresources:
status: {} # bật subresource /status
additionalPrinterColumns:
- { name: Spec, type: string, jsonPath: .spec.cronSpec }
- { name: Image, type: string, jsonPath: .spec.image }
Sau khi apply, người dùng tạo instance như bất kỳ object dựng sẵn nào:
apiVersion: stable.example.com/v1
kind: CronTab
metadata: { name: my-cron }
spec:
cronSpec: "* * * * */5"
image: my-cron-image:1.2.0
replicas: 2
kubectl apply -f crontab-crd.yaml
kubectl get crontabs # hoặc `kubectl get ct`
kubectl describe ct my-cron
Những điểm quan trọng trong production: phiên bản storage: true là phiên bản được ghi vào etcd — khi bạn thêm v2, bạn cung cấp một conversion webhook để cả hai phiên bản cùng phục vụ; subresources.status tách spec (người dùng sở hữu) khỏi status (controller sở hữu) để RBAC và update không xung đột; và schema OpenAPI là validation của bạn — hãy chặt (required, pattern, luật CEL x-kubernetes-validations) để object xấu bị từ chối ngay tại admission.
Một reconcile loop khái niệm
Controller trao hành vi cho CRD CronTab. Đây là pseudocode theo hình dạng Reconcile của controller-runtime:
// Reconcile được gọi mỗi khi một CronTab (hoặc thứ nó sở hữu) thay đổi.
func (r *CronTabReconciler) Reconcile(ctx context.Context, req Request) (Result, error) {
// 1. LẤY trạng thái mong muốn. Nếu không còn, nó đã bị xóa — không làm gì.
var cron stablev1.CronTab
if err := r.Get(ctx, req.NamespacedName, &cron); err != nil {
return Result{}, client.IgnoreNotFound(err) // NotFound = đã dọn xong
}
// 2. QUAN SÁT trạng thái thực: CronJob ta quản lý đã tồn tại chưa?
var existing batchv1.CronJob
err := r.Get(ctx, childName(&cron), &existing)
// 3. DIFF + HÀNH ĐỘNG: tạo nếu thiếu, update nếu bị drift.
desired := r.buildCronJob(&cron) // dịch spec → object thật
ctrl.SetControllerReference(&cron, desired, r.Scheme) // quyền sở hữu cho GC + watch
switch {
case apierrors.IsNotFound(err):
r.Create(ctx, desired) // hội tụ: tạo
case !equalSpec(&existing, desired):
r.Update(ctx, desired) // hội tụ: sửa drift
}
// 4. BÁO CÁO: ghi thực tại quan sát được vào .status (update subresource).
cron.Status.LastScheduleTime = now()
r.Status().Update(ctx, &cron)
// 5. Return. Requeue nếu muốn re-sync định kỳ; nếu không, chờ sự kiện kế.
return Result{RequeueAfter: 5 * time.Minute}, nil
}
Các ý then chốt được cụ thể hóa: lấy desired, quan sát actual, thực hiện một bước hội tụ, ghi status, requeue. Vì nó level-triggered, nếu controller crash giữa chừng reconcile rồi restart, lần Reconcile kế đơn giản tái suy ra hành động đúng. SetControllerReference khiến Kubernetes garbage-collect CronJob con khi CronTab bị xóa và re-trigger reconcile khi con thay đổi — cơ chế của quyền sở hữu.
Khi nào viết Operator vs dùng Helm
Đây là quyết định thực tế thường gặp nhất. Chúng giải quyết những vấn đề khác nhau và thường kết hợp.
| Helm | Operator | |
|---|---|---|
| Là gì | Công cụ templating + đóng gói (một chart) | Một controller đang chạy kèm một CRD |
| Hành động lúc nào | Lúc install/upgrade (day 1) | Liên tục, mãi mãi (day 2) |
| Xử lý drift | Không — chạy lại helm upgrade thủ công | Có — tự reconcile về lại |
| Logic vận hành | Không (chỉ render YAML) | Backup, failover, scaling, upgrade mã hóa trong code |
| Độ phức tạp để dựng | Thấp (YAML + template) | Cao (Go, controller, test, vòng đời) |
| Hợp cho | App stateless, đóng gói config, hầu hết deployment | Hệ thống stateful cần automation vận hành liên tục |
Quy tắc ngón tay cái: dùng Helm (hoặc Kustomize) cho đại đa số workload — bất cứ thứ gì mà vận hành day-2 là “đổi image mới” đều không cần operator. Viết hoặc dùng một Operator khi phần mềm cần quyết định vận hành liên tục không thể gói trong YAML tĩnh: database (backup/restore/failover), hệ thống messaging (rebalancing), vòng đời certificate, hay bất cứ thứ gì mà “một chuyên gia phải canh và can thiệp”. Đừng dựng operator để deploy một web app stateless — đó là việc của Helm, và operator ở đó là chi phí thừa. Cũng nên dùng một operator đã được kiểm chứng (cert-manager, Prometheus Operator, CloudNativePG) hơn là tự viết.
Hệ sinh thái: bạn cắm vào cái gì
Kubernetes là nền tảng; CNCF landscape là thứ chạy trên nó. Định hướng các tầng mà hầu hết operator chạm tới:
Service mesh — một tầng hạ tầng chuyên cho lưu lượng service-to-service, thêm mTLS, retry, timeout, chia lưu lượng, và telemetry giàu mà không đổi code ứng dụng, thường qua proxy sidecar (hoặc ngày càng nhiều mode không-sidecar).
| Mesh | Ghi chú |
|---|---|
| Istio | Nhiều tính năng nhất; dựa Envoy; sidecar hoặc mode “ambient” mới không-sidecar; đường học dốc |
| Linkerd | Đơn giản hơn, nhẹ hơn, có quan điểm; “micro-proxy” Rust chuyên dụng; CNCF graduated |
| Cilium Service Mesh | Dựa eBPF, chạy được không-sidecar ở mức kernel |
Áp dụng mesh khi bạn thực sự cần mTLS ở mọi nơi, kiểm soát lưu lượng tinh (canary theo header, mirroring), hoặc observability L7 xuyên nhiều service — không phải mặc định, vì nó thêm chi phí vận hành và latency thật.
GitOps — Git là nguồn sự thật duy nhất cho trạng thái cluster; một agent trong cluster liên tục reconcile cluster thực về đúng thứ đã commit. Đó là mẫu reconciliation áp dụng cho chính việc deployment.
| Công cụ | Ghi chú |
|---|---|
| Argo CD | Lấy UI làm trung tâm, hướng application, rất phổ biến; multi-cluster/RBAC/trực quan hóa mạnh |
| Flux | Hướng toolkit/controller, Git-native, composable; CNCF graduated |
Cả hai phát hiện và sửa drift, biến Git thành nhật ký audit, và biến rollback thành git revert — và kết hợp tự nhiên với một platform nhiều operator vì mọi thứ đều khai báo.
Multi-cluster & fleet management — khi cluster nhân lên (theo region, môi trường, team), bạn quản lý chúng như một fleet:
- Cluster API (CAPI) — vòng đời cluster khai báo: cluster, machine, và hạ tầng tự chúng là CRD được controller reconcile. Bạn
kubectl applymộtClustervà CAPI provision nó — mẫu operator áp dụng cho cluster. - Fleet / Rancher, Karmada, Open Cluster Management — lập lịch và lan truyền workload cùng policy xuyên nhiều cluster từ một điểm điều khiển trung tâm.
Serverless trên Kubernetes — chạy function/container với scale-to-zero và autoscaling theo request trên nền K8s:
- Knative — khối xây dựng hàng đầu: Serving (scale-to-zero, revision, chia lưu lượng cho workload theo request) và Eventing (mô hình pub/sub và source/sink dựa CloudEvents). Đây là cách bạn có trải nghiệm kiểu Lambda mà vẫn ở trên cluster của mình.
- KEDA — autoscaling hướng sự kiện (kể cả scale-to-zero) cho Deployment thường, điều khiển bởi độ sâu queue, Kafka lag, cron, v.v. Nhẹ hơn Knative khi bạn chỉ cần scaling theo sự kiện chứ không cần cả runtime serverless.
Định hướng CNCF landscape — CNCF Landscape sắp xếp hàng trăm project vào các nhóm (runtime, orchestration, observability, security, delivery). Đừng cố học hết; hãy học các nhóm và một hai project graduated mỗi nhóm mà bạn thực sự cần. Ưu tiên graduated > incubating > sandbox để phụ thuộc production, và kiểm tra project còn được bảo trì tích cực trước khi đặt cược.
Best Practices
- Hiểu reconciliation trước khi viết bất kỳ controller nào — hội tụ level-triggered là toàn bộ mô hình. Nếu
Reconcilephản ứng với sự kiện thay vì tái suy ra từ trạng thái hiện tại, nó sẽ vỡ khi sự kiện bị bỏ lỡ, khi restart, và khi có race. - Làm
Reconcileidempotent và an toàn side-effect — nó sẽ được gọi lặp lại (controller-runtime tuần tự hóa các lần gọi theo từng object, nhưng re-entry qua sự kiện, restart, requeue là chắc chắn). Mỗi lần chạy phải hoặc hội tụ một bước hoặc an toàn không làm gì; đừng bao giờ giả định nó chạy đúng một lần. - Dùng operator có sẵn trước khi tự dựng — cert-manager, Prometheus Operator, CloudNativePG, và nhiều thứ khác mã hóa nhiều năm kiến thức vận hành đúc kết. Viết và bảo trì một operator production là cam kết kỹ thuật nghiêm túc, liên tục; đừng làm cho một vấn đề đã được giải.
- Dùng Helm/Kustomize cho app stateless; dành Operator cho logic vận hành day-2 — nếu nhu cầu vòng đời duy nhất của app là “deploy image mới”, operator là chi phí thừa. Operator đáng giá khi phần mềm cần quyết định vận hành liên tục, chuyên gia (backup, failover, scaling).
- Đánh version cho CRD và cung cấp conversion webhook — API bạn ship là một hợp đồng. Thêm phiên bản mới (
v1beta1 → v1) kèm conversion để object hiện có tiếp tục chạy; đừng bao giờ phá vỡ một phiên bản đang served dưới chân người dùng. - Viết schema OpenAPI/CEL chặt cho CRD — validation tại admission (
required, pattern,x-kubernetes-validations) từ chối object xấu trước khi lưu, biến cả lớp lỗi thành lỗi rõ ràng và sớm thay vì bất ngờ runtime bên trong controller. - Tách spec khỏi status qua subresource /status — người dùng sở hữu
spec, controller sở hữustatus. Subresource giữ RBAC và update khỏi xung đột và ngăn một lần ghi status đè lên một thay đổi spec đồng thời. - Đặt owner reference lên object được quản lý —
SetControllerReferencecho bạn garbage collection tự động của con và re-reconcile theo sự kiện khi chúng đổi. Không có nó bạn rò rỉ tài nguyên và bỏ lỡ drift. - Phát Event và status condition có ý nghĩa — bộc lộ controller đang làm gì qua Kubernetes Event và
status.conditions. Tính vận hành được của một operator được đánh giá bằng việc con người có thểkubectl describemột object và hiểu vì sao nó kẹt hay không. - Giữ admission webhook nhanh, fail-safe, và phạm vi hẹp — một webhook nằm trong đường tới hạn của mọi write API khớp; webhook chậm hoặc chết có thể chặn mọi write. Đặt timeout chặt, chọn
failurePolicycó chủ đích, và scoperules/namespaceSelectorthật hẹp. - Ưu tiên CRD hơn aggregated API server — CRD phủ đại đa số nhu cầu mở rộng với gánh nặng vận hành thấp hơn nhiều. Chỉ tìm tới aggregated API server khi bạn thực sự cần storage hoặc xử lý request tùy chỉnh mà CRD không biểu đạt được.
- Áp dụng service mesh vì nhu cầu thật, không phải mặc định — mesh mang lại mTLS, kiểm soát lưu lượng, và telemetry L7 với chi phí sidecar, latency, và phức tạp vận hành. Thêm nó khi bạn gọi được tên yêu cầu; bắt đầu với mesh nhẹ nhất đáp ứng nó.
- Chạy GitOps để cluster phản ánh Git — một agent Argo CD/Flux reconcile từ Git cho bạn phát hiện drift, một dấu vết audit, và rollback bằng
git revert. Đó là mẫu reconciliation áp dụng cho delivery và kết hợp tự nhiên với platform khai báo, hướng operator. - Quản lý nhiều cluster khai báo (Cluster API / công cụ fleet) — khi có hơn vài cluster, hãy coi vòng đời cluster và policy xuyên cluster là tài nguyên khai báo, được reconcile thay vì các cluster snowflake dựng tay.
- Chọn project hệ sinh thái theo độ chín CNCF và mức hoạt động — ưu tiên graduated hơn incubating hơn sandbox cho phụ thuộc production, và xác nhận còn bảo trì tích cực. Landscape khổng lồ; đào sâu vài project chọn kỹ hơn là rải mỏng bề rộng.
- Test controller bằng envtest và test operator trên cluster thật —
envtestcủa controller-runtime chạy logic reconcile của bạn với apiserver+etcd thật; thêm test đầu-cuối trên kind. Logic reconciliation chưa test là một gánh nặng rò rỉ, gây drift nằm ngay trong đường tới hạn của platform.
Tài liệu tham khảo
- Extending Kubernetes (concepts)
- Custom Resources
- CustomResourceDefinitions (reference)
- Operator pattern
- Dynamic Admission Control (webhooks)
- Aggregation Layer
- Kubebuilder Book
- Operator SDK · controller-runtime
- OperatorHub.io
- Istio · Linkerd · Cilium
- Argo CD · Flux
- Cluster API · Karmada
- Knative · KEDA
- CNCF Landscape · CNCF Graduated & Incubating Projects
- Sách: Programming Kubernetes (Hausenblas & Schimanski — O’Reilly) · Kubernetes Operators (Dobies & Wood — O’Reilly)
- roadmap.sh — Kubernetes
Part of the Kubernetes Roadmap knowledge base.
Overview
Kubernetes ships with a fixed set of built-in objects — Pods, Deployments, Services, and so on — but its real power is that the API itself is extensible. You can teach Kubernetes about entirely new kinds of objects (a Database, a Certificate, a KafkaCluster) and write software that manages them the same declarative, self-healing way the built-in controllers manage Pods. This is how the ecosystem exploded: cert-manager, Prometheus Operator, Istio, ArgoCD, Crossplane, and hundreds of others are not forks of Kubernetes — they are extensions built on the same primitives.
The unifying idea behind all of it is the controller / reconciliation pattern. A controller is a loop that watches objects, compares desired state (spec) to observed state (status), and takes action to close the gap — over and over, forever. The built-in Deployment controller does this for Pods; a custom Operator does it for your application’s domain. Once you internalize “everything in Kubernetes is a control loop,” extending it stops being mysterious: you define a new object type (a CRD) and write a controller that reconciles it.
This page covers two layers. First, how to extend Kubernetes: CRDs, the Operator pattern and the tools that build them (Kubebuilder, Operator SDK, controller-runtime), plus admission webhooks and aggregated API servers. Second, the ecosystem you plug into: service mesh, GitOps, multi-cluster management, and serverless — the CNCF landscape you’ll navigate as an operator. Throughout, one practical question recurs: when should you actually write an Operator versus just using Helm?
Fundamentals
The controller / reconciliation pattern
Every controller in Kubernetes — built-in or custom — runs the same level-triggered loop:
- 01watch objectsvia the API server / informer
- event: something changed02Reconcile(request)1. read desired state (spec) — 2. read actual state (the real world) — 3. if they differ, take ONE step to converge, then update status — 4. return (requeue if more work remains)
Two properties make this robust:
- Level-triggered, not edge-triggered. The controller doesn’t react to events (“a Pod was deleted”); it reacts to state (“desired 3, actual 2 → create one”). So it recovers correctly from missed events, restarts, and partial failures — it always re-derives the right action from current reality.
- Idempotent and convergent.
Reconcilemay be called many times for the same object; each call moves toward the goal or does nothing. You never assume you saw every intermediate state.
Custom Resource Definitions (CRDs)
A CRD registers a new object kind with the API server. After you apply a CRD, kubectl get databases works exactly like kubectl get pods — the same storage (etcd), the same RBAC, the same kubectl, the same labels/annotations. A CRD by itself is just structured storage with validation; it does nothing until a controller watches it.
Custom Resources + a controller = an Operator
The Operator pattern is simply: a CRD (the API) + a custom controller (the behavior) that encodes the operational knowledge of a human expert. Where a Deployment controller knows how to keep N Pods running, a PostgresCluster operator knows how to provision a primary and replicas, take backups, fail over, and run version upgrades — automating what a DBA would otherwise do by hand. The operator is the SRE, expressed as code.
The build toolchain
You rarely write raw client-go watch loops. The standard stack:
- controller-runtime — the Go library underneath everything; provides the Manager, informers/caches, clients, and the
Reconcileinterface. - Kubebuilder — scaffolds a project: generates the CRD types, RBAC, the controller skeleton, and manifests. The de facto way to start.
- Operator SDK — builds on Kubebuilder and adds Helm-based and Ansible-based operators (no Go required for simple cases) plus OLM (Operator Lifecycle Manager) packaging.
Admission webhooks and aggregated APIs
Beyond adding new objects, you can intercept and extend the API request path:
- Admission webhooks run during an API write, after authn/authz but before the object is persisted. Mutating webhooks can modify the object (inject a sidecar, add defaults); validating webhooks can reject it (enforce policy). This is how Istio injects proxies and how Kyverno/Gatekeeper enforce rules.
- Aggregated API servers register an entirely separate API server behind the main apiserver for a given API group — used when you need custom storage or behavior that CRDs can’t express (the
metrics.k8s.ioAPI from metrics-server is the classic example). Heavier than CRDs; reach for them rarely.
Key Concepts
A minimal CRD
Here is a small CRD defining a CronTab kind in the stable.example.com group, with an OpenAPI schema (validation) and printer columns:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: crontabs.stable.example.com # must be <plural>.<group>
spec:
group: stable.example.com
scope: Namespaced # or Cluster
names:
plural: crontabs
singular: crontab
kind: CronTab
shortNames: [ct]
versions:
- name: v1
served: true # exposed via the API
storage: true # THIS version is persisted in etcd
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
cronSpec: { type: string, pattern: '^(\S+\s+){4}\S+$' }
image: { type: string }
replicas: { type: integer, minimum: 1, default: 1 }
required: [cronSpec, image]
status:
type: object
properties:
lastScheduleTime: { type: string, format: date-time }
subresources:
status: {} # enables the /status subresource
additionalPrinterColumns:
- { name: Spec, type: string, jsonPath: .spec.cronSpec }
- { name: Image, type: string, jsonPath: .spec.image }
Once applied, users create instances like any built-in object:
apiVersion: stable.example.com/v1
kind: CronTab
metadata: { name: my-cron }
spec:
cronSpec: "* * * * */5"
image: my-cron-image:1.2.0
replicas: 2
kubectl apply -f crontab-crd.yaml
kubectl get crontabs # or `kubectl get ct`
kubectl describe ct my-cron
Notes that matter in production: the storage: true version is the one written to etcd — when you add a v2, you provide a conversion webhook so both versions serve; subresources.status separates spec (user-owned) from status (controller-owned) so RBAC and updates don’t collide; and the OpenAPI schema is your validation — be strict (required, patterns, x-kubernetes-validations CEL rules) so bad objects are rejected at admission.
A conceptual reconcile loop
The controller that gives the CronTab CRD behavior. This is pseudocode in the shape of a controller-runtime Reconcile:
// Reconcile is called whenever a CronTab (or something it owns) changes.
func (r *CronTabReconciler) Reconcile(ctx context.Context, req Request) (Result, error) {
// 1. FETCH the desired state. If it's gone, it was deleted — nothing to do.
var cron stablev1.CronTab
if err := r.Get(ctx, req.NamespacedName, &cron); err != nil {
return Result{}, client.IgnoreNotFound(err) // NotFound = already cleaned up
}
// 2. OBSERVE actual state: does the CronJob we manage already exist?
var existing batchv1.CronJob
err := r.Get(ctx, childName(&cron), &existing)
// 3. DIFF + ACT: create it if missing, update it if it drifted.
desired := r.buildCronJob(&cron) // translate spec → real object
ctrl.SetControllerReference(&cron, desired, r.Scheme) // ownership for GC + watches
switch {
case apierrors.IsNotFound(err):
r.Create(ctx, desired) // converge: create
case !equalSpec(&existing, desired):
r.Update(ctx, desired) // converge: correct drift
}
// 4. REPORT: write observed reality back to .status (a subresource update).
cron.Status.LastScheduleTime = now()
r.Status().Update(ctx, &cron)
// 5. Return. Requeue if you want periodic re-sync; else wait for the next event.
return Result{RequeueAfter: 5 * time.Minute}, nil
}
The key ideas made concrete: fetch desired, observe actual, take one convergent step, write status, requeue. Because it’s level-triggered, if the controller crashes mid-reconcile and restarts, the next Reconcile simply re-derives the correct action. SetControllerReference makes Kubernetes garbage-collect the child CronJob when the CronTab is deleted and re-trigger reconcile when the child changes — the mechanics of ownership.
When to write an Operator vs use Helm
This is the most common real decision. They solve different problems and often combine.
| Helm | Operator | |
|---|---|---|
| What it is | A templating + packaging tool (a chart) | A running controller with a CRD |
| When it acts | At install/upgrade time (day 1) | Continuously, forever (day 2) |
| Handles drift | No — re-run helm upgrade manually | Yes — reconciles back automatically |
| Operational logic | None (just renders YAML) | Backups, failover, scaling, upgrades encoded in code |
| Complexity to build | Low (YAML + templates) | High (Go, controller, tests, lifecycle) |
| Good for | Stateless apps, config packaging, most deployments | Stateful systems needing ongoing operational automation |
Rule of thumb: use Helm (or Kustomize) for the vast majority of workloads — anything whose day-2 operations are “roll a new image” doesn’t need an operator. Write or adopt an Operator when the software needs ongoing operational decisions that can’t be captured in static YAML: databases (backup/restore/failover), messaging systems (rebalancing), certificate lifecycles, or anything where “an expert has to watch and intervene.” Don’t build an operator to deploy a stateless web app — that’s Helm’s job, and an operator there is pure overhead. Also prefer adopting a battle-tested operator (cert-manager, Prometheus Operator, CloudNativePG) over writing your own.
The ecosystem: what you plug into
Kubernetes is the substrate; the CNCF landscape is what runs on it. Orientation of the layers most operators touch:
Service mesh — a dedicated infrastructure layer for service-to-service traffic, adding mTLS, retries, timeouts, traffic splitting, and rich telemetry without changing application code, usually via sidecar (or, increasingly, sidecar-less) proxies.
| Mesh | Notes |
|---|---|
| Istio | Most feature-rich; Envoy-based; sidecar or the newer sidecar-less “ambient” mode; steep learning curve |
| Linkerd | Simpler, lighter, opinionated; purpose-built Rust “micro-proxy”; CNCF graduated |
| Cilium Service Mesh | eBPF-based, can run sidecar-free at the kernel level |
Adopt a mesh when you genuinely need mTLS everywhere, fine-grained traffic control (canaries by header, mirroring), or L7 observability across many services — not by default, since it adds real operational and latency cost.
GitOps — Git as the single source of truth for cluster state; an in-cluster agent continuously reconciles the live cluster toward what’s committed. It’s the reconciliation pattern applied to deployment itself.
| Tool | Notes |
|---|---|
| Argo CD | UI-centric, application-oriented, very popular; strong multi-cluster/RBAC/visualization |
| Flux | Toolkit/controller-oriented, Git-native, composable; CNCF graduated |
Both detect and correct drift, make Git the audit log, and turn rollbacks into git revert — and pair naturally with an operator-heavy platform since everything is declarative.
Multi-cluster & fleet management — as clusters multiply (per region, per environment, per team), you manage them as a fleet:
- Cluster API (CAPI) — declarative cluster lifecycle: clusters, machines, and infrastructure are themselves CRDs reconciled by controllers. You
kubectl applyaClusterand CAPI provisions it — the operator pattern applied to clusters. - Fleet / Rancher, Karmada, Open Cluster Management — schedule and propagate workloads and policy across many clusters from a central control point.
Serverless on Kubernetes — run functions/containers with scale-to-zero and request-driven autoscaling on top of K8s:
- Knative — the leading building block: Serving (scale-to-zero, revisions, traffic splitting for request-driven workloads) and Eventing (a CloudEvents-based pub/sub and source/sink model). It’s how you get a Lambda-like experience while staying on your own cluster.
- KEDA — event-driven autoscaling (including scale-to-zero) for regular Deployments, driven by queue depth, Kafka lag, cron, etc. Lighter than Knative when you just need event-based scaling, not a full serverless runtime.
CNCF landscape orientation — the CNCF Landscape maps hundreds of projects into categories (runtime, orchestration, observability, security, delivery). Don’t try to learn all of it; learn the categories and one or two graduated projects per category you actually need. Prefer graduated > incubating > sandbox for production reliance, and check that a project is actively maintained before betting on it.
Best Practices
- Understand reconciliation before writing any controller — level-triggered convergence is the whole model. If your
Reconcilereacts to events instead of re-deriving from current state, it will break on missed events, restarts, and races. - Make
Reconcileidempotent and side-effect-safe — it will be called repeatedly (controller-runtime serializes calls per object, but re-entry across events, restarts, and requeues is guaranteed). Every run must either converge one step or safely do nothing; never assume it runs exactly once. - Adopt an existing operator before building your own — cert-manager, Prometheus Operator, CloudNativePG, and others encode years of hard-won operational knowledge. Writing and maintaining a production operator is a serious, ongoing engineering commitment; don’t do it for a solved problem.
- Use Helm/Kustomize for stateless apps; reserve Operators for day-2 operational logic — if your app’s only lifecycle need is “deploy a new image,” an operator is pure overhead. Operators earn their cost when software needs continuous, expert operational decisions (backup, failover, scaling).
- Version your CRDs and provide conversion webhooks — the API you ship is a contract. Add new versions (
v1beta1 → v1) with conversion so existing objects keep working; never break a served version out from under users. - Write strict OpenAPI/CEL schemas for CRDs — validation at admission (
required, patterns,x-kubernetes-validations) rejects bad objects before they’re stored, turning class-of-bug problems into clear, early errors instead of runtime surprises inside your controller. - Separate spec from status via the /status subresource — users own
spec, the controller ownsstatus. The subresource keeps RBAC and updates from colliding and prevents a status write from clobbering a concurrent spec change. - Set owner references on managed objects —
SetControllerReferencegives you automatic garbage collection of children and event-driven re-reconciliation when they change. Without it you leak resources and miss drift. - Emit events and meaningful status conditions — surface what the controller is doing via Kubernetes Events and
status.conditions. Operability of an operator is judged by whether a human cankubectl describean object and understand why it’s stuck. - Keep admission webhooks fast, fail-safe, and narrowly scoped — a webhook is in the critical path of every matching API write; a slow or down webhook can block all writes. Set tight timeouts, choose
failurePolicydeliberately, and scoperules/namespaceSelectortightly. - Prefer CRDs over aggregated API servers — CRDs cover the vast majority of extension needs with far less operational burden. Only reach for an aggregated API server when you genuinely need custom storage or request handling CRDs can’t express.
- Adopt a service mesh for a real need, not by default — mesh delivers mTLS, traffic control, and L7 telemetry at the cost of sidecars, latency, and operational complexity. Add it when you can name the requirement; start with the lightest mesh that meets it.
- Run GitOps so the cluster reflects Git — an Argo CD/Flux agent reconciling from Git gives you drift detection, an audit trail, and
git revertrollbacks. It’s the reconciliation pattern applied to delivery and pairs naturally with a declarative, operator-driven platform. - Manage many clusters declaratively (Cluster API / fleet tools) — once you have more than a couple of clusters, treat cluster lifecycle and cross-cluster policy as declarative, reconciled resources rather than snowflake, hand-built clusters.
- Choose ecosystem projects by CNCF maturity and activity — prefer graduated over incubating over sandbox for production dependencies, and confirm active maintenance. The landscape is huge; depth on a few well-chosen projects beats shallow breadth.
- Test controllers with envtest and the operator against real clusters — controller-runtime’s
envtestruns your reconcile logic against a real apiserver+etcd; add end-to-end tests on kind. Untested reconciliation logic is a leaking, drift-inducing liability sitting in the critical path of your platform.
References
- Extending Kubernetes (concepts)
- Custom Resources
- CustomResourceDefinitions (reference)
- Operator pattern
- Dynamic Admission Control (webhooks)
- Aggregation Layer
- Kubebuilder Book
- Operator SDK · controller-runtime
- OperatorHub.io
- Istio · Linkerd · Cilium
- Argo CD · Flux
- Cluster API · Karmada
- Knative · KEDA
- CNCF Landscape · CNCF Graduated & Incubating Projects
- Book: Programming Kubernetes (Hausenblas & Schimanski — O’Reilly) · Kubernetes Operators (Dobies & Wood — O’Reilly)
- roadmap.sh — Kubernetes