← Kubernetes← Kubernetes
KubernetesKubernetes19 Th7, 2026Jul 19, 202616 phút đọc14 min read

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:

Vòng lặp reconcile
  1. 01theo dõi objectqua API server / informer
  2. sự kiện: có gì đó thay đổi
    02Reconcile(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:

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:

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:

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.

HelmOperator
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àoLúc install/upgrade (day 1)Liên tục, mãi mãi (day 2)
Xử lý driftKhông — chạy lại helm upgrade thủ côngCó — tự reconcile về lại
Logic vận hànhKhông (chỉ render YAML)Backup, failover, scaling, upgrade mã hóa trong code
Độ phức tạp để dựngThấp (YAML + template)Cao (Go, controller, test, vòng đời)
Hợp choApp stateless, đóng gói config, hầu hết deploymentHệ 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).

MeshGhi chú
IstioNhiề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 MeshDự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 CDLấ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
FluxHướ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:

Serverless trên Kubernetes — chạy function/container với scale-to-zero và autoscaling theo request trên nền K8s:

Định hướng CNCF landscapeCNCF 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

  1. 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 Reconcile phả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.
  2. Làm Reconcile idempotent 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.
  3. 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.
  4. 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).
  5. Đá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.
  6. 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.
  7. Tách spec khỏi status qua subresource /status — người dùng sở hữu spec, controller sở hữu status. 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.
  8. Đặt owner reference lên object được quản lýSetControllerReference cho 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.
  9. 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 describe một object và hiểu vì sao nó kẹt hay không.
  10. 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 failurePolicy có chủ đích, và scope rules/namespaceSelector thật hẹp.
  11. Ư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.
  12. Á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ó.
  13. 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.
  14. 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.
  15. 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.
  16. Test controller bằng envtest và test operator trên cluster thậtenvtest củ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

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:

The reconcile loop
  1. 01watch objectsvia the API server / informer
  2. event: something changed
    02Reconcile(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:

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:

Admission webhooks and aggregated APIs

Beyond adding new objects, you can intercept and extend the API request path:

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.

HelmOperator
What it isA templating + packaging tool (a chart)A running controller with a CRD
When it actsAt install/upgrade time (day 1)Continuously, forever (day 2)
Handles driftNo — re-run helm upgrade manuallyYes — reconciles back automatically
Operational logicNone (just renders YAML)Backups, failover, scaling, upgrades encoded in code
Complexity to buildLow (YAML + templates)High (Go, controller, tests, lifecycle)
Good forStateless apps, config packaging, most deploymentsStateful 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.

MeshNotes
IstioMost feature-rich; Envoy-based; sidecar or the newer sidecar-less “ambient” mode; steep learning curve
LinkerdSimpler, lighter, opinionated; purpose-built Rust “micro-proxy”; CNCF graduated
Cilium Service MesheBPF-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.

ToolNotes
Argo CDUI-centric, application-oriented, very popular; strong multi-cluster/RBAC/visualization
FluxToolkit/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:

Serverless on Kubernetes — run functions/containers with scale-to-zero and request-driven autoscaling on top of K8s:

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

  1. Understand reconciliation before writing any controller — level-triggered convergence is the whole model. If your Reconcile reacts to events instead of re-deriving from current state, it will break on missed events, restarts, and races.
  2. Make Reconcile idempotent 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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.
  7. Separate spec from status via the /status subresource — users own spec, the controller owns status. The subresource keeps RBAC and updates from colliding and prevents a status write from clobbering a concurrent spec change.
  8. Set owner references on managed objectsSetControllerReference gives you automatic garbage collection of children and event-driven re-reconciliation when they change. Without it you leak resources and miss drift.
  9. 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 can kubectl describe an object and understand why it’s stuck.
  10. 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 failurePolicy deliberately, and scope rules/namespaceSelector tightly.
  11. 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.
  12. 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.
  13. Run GitOps so the cluster reflects Git — an Argo CD/Flux agent reconciling from Git gives you drift detection, an audit trail, and git revert rollbacks. It’s the reconciliation pattern applied to delivery and pairs naturally with a declarative, operator-driven platform.
  14. 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.
  15. 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.
  16. Test controllers with envtest and the operator against real clusters — controller-runtime’s envtest runs 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