Skip to content

Custom Resource Definitions (CRDs): Cơ Chế Mở Rộng

Vì Sao Quan Trọng Ở Production

CRD thường được giới thiệu như "một cách định nghĩa resource tùy chỉnh bằng YAML" — mô tả này đúng nhưng che khuất mất phần quan trọng nhất: khi bạn tạo một CRD, API server thực sự sinh ra một REST endpoint mới, hoạt động, ngay lập tức, tuân theo đầy đủ cùng một pipeline đã mô tả ở phần API Server Request Pipeline — cùng authentication, cùng authorization RBAC, cùng admission, cùng cơ chế watch/list, cùng optimistic concurrency. CRD không phải một cơ chế "phụ" tách biệt khỏi API machinery cốt lõi — nó là cùng một cỗ máy, chỉ khác ở chỗ schema và storage handler được sinh ra động từ một object cấu hình thay vì compile cứng vào binary kube-apiserver.

Hiểu đúng cơ chế này giải thích được nhiều hành vi CRD hay gây bất ngờ: vì sao CRD tự động có watch/list giống Pod hay Deployment, vì sao một CRD thiết kế sai schema có thể làm hỏng toàn bộ khả năng kubectl apply (vì admission/schema validation áp dụng y hệt built-in resource), và vì sao đổi version của CRD là một quyết định kiến trúc nghiêm túc, không đơn thuần "sửa số version trong YAML".

Cơ Chế Sinh REST Endpoint Động

Khi một CustomResourceDefinition object được tạo, API server (cụ thể là một thành phần bên trong gọi là apiextensions-apiserver, chạy tích hợp trong cùng binary kube-apiserver) theo dõi chính resource CRD đó qua watch (đúng vòng lặp watch-informer-reconcile đã mô tả xuyên suốt chương này, áp dụng đệ quy lên chính cơ chế mở rộng của nó), và khi phát hiện một CRD mới hoặc thay đổi, nó đăng ký động một REST handler mới vào router HTTP nội bộ:

yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: crontabs.stable.example.com
spec:
  group: stable.example.com
  scope: Namespaced
  names:
    plural: crontabs
    singular: crontab
    kind: CronTab
  versions:
  - name: v1
    served: true
    storage: true
    schema: {...}

Ngay khi CRD này được API server tiếp nhận, endpoint /apis/stable.example.com/v1/namespaces/*/crontabs tồn tại thực sự, hỗ trợ đầy đủ GET/LIST/WATCH/CREATE/UPDATE/PATCH/DELETE — không cần restart API server, không cần deploy thêm binary nào. Điều này khác căn bản với built-in resource (nơi handler được compile cứng vào kube-apiserver từ mã nguồn Go) — CRD sinh ra handler hoàn toàn generic, dựa trên schema khai báo, lưu trữ dữ liệu dưới dạng gần như tùy ý (thường là JSON được encode) trong cùng etcd với mọi resource khác, dùng chung mod_revision/resourceVersion semantics đã mô tả ở phần Resource Versioning.

Structural Schema: Điều Kiện Bắt Buộc, Không Phải Tùy Chọn

Từ apiextensions.k8s.io/v1 (API version ổn định hiện tại), mọi CRD bắt buộc phải có một structural schema — một schema OpenAPI v3 tuân theo các ràng buộc nghiêm ngặt hơn OpenAPI v3 thuần túy:

  1. Mọi field phải khai báo type tường minh — root object, mỗi property, mỗi phần tử mảng — trừ trường hợp đặc biệt dùng x-kubernetes-int-or-string: true (field chấp nhận cả số lẫn chuỗi, ví dụ cổng dịch vụ có thể là số hoặc tên named port) hoặc x-kubernetes-preserve-unknown-fields: true (field chấp nhận nội dung tùy ý không kiểm soát trước).
  2. Field bên trong allOf/anyOf/oneOf/not phải được khai báo lại cả bên ngoài các toán tử logic đó — schema chỉ dùng logic junctor để thu hẹp thêm ràng buộc, không được dùng để định nghĩa field mới mà không xuất hiện ở cấp schema chính.
  3. Các thuộc tính mô tả (description, type, default, nullable) không được đặt bên trong logic junctor.

Lý do đằng sau các ràng buộc tưởng chừng phiền phức này: schema OpenAPI v3 thuần túy đủ linh hoạt để biểu diễn logic mà không có một type xác định duy nhất tại mỗi vị trí — điều này khiến API server không thể suy luận chắc chắn kiểu dữ liệu để phục vụ các tính năng cấp thấp cần biết chính xác cấu trúc: pruning field không xác định, tính toán default value, và merge strategy cho PATCH. Structural schema là tập con của OpenAPI v3 đủ hẹp để đảm bảo mỗi vị trí trong document luôn có đúng một type xác định, cho phép API server xử lý CRD với cùng mức độ tin cậy như built-in resource có Go struct tường minh.

Pruning Và Default: Hệ Quả Trực Tiếp Của Structural Schema

Nhờ có structural schema, API server có thể tự động:

  • Prune (loại bỏ) field không nằm trong schema khỏi object trước khi lưu — trừ khi field đó hoặc field cha của nó đánh dấu x-kubernetes-preserve-unknown-fields: true. Đây là lý do gửi lên một field "gõ nhầm tên" trong custom resource sẽ âm thầm biến mất sau khi lưu, không có lỗi nào cả — hành vi này thường gây bối rối cho người mới làm việc với CRD, khác với built-in resource nơi unknown field trong strict mode sẽ bị từ chối tường minh.
  • Áp default value khai báo trong schema ngay tại thời điểm ghi, trước khi persist — đúng vị trí trong pipeline admission đã mô tả ở phần Request Pipeline (schema validation/defaulting nằm giữa mutating và validating admission phase).

Versioning: Nhiều Version Được Serve, Chỉ Một Version Lưu Trữ

CRD hỗ trợ đồng thời nhiều version API cho cùng một Kind:

yaml
spec:
  versions:
  - name: v1
    served: true
    storage: true      # <- version thực sự dùng để lưu trong etcd
  - name: v1beta1
    served: true
    storage: false      # <- vẫn phục vụ client, nhưng KHÔNG lưu trữ trực tiếp

Ràng buộc cứng: đúng một và chỉ một version được đánh dấu storage: true tại một thời điểm — đây là version thực sự quyết định cách object được encode và ghi vào etcd. Mọi version khác có served: true vẫn nhận request từ client bình thường, nhưng mỗi lần đọc/ghi đều phải đi qua một bước convert giữa version client yêu cầu và storage version thực tế đang nằm trong etcd.

Hai Chiến Lược Conversion

  • strategy: None — chỉ hợp lệ khi mọi version served có schema giống hệt nhau về cấu trúc (không có field nào đổi tên, đổi type, hay đổi vị trí giữa các version) — API server chỉ đơn thuần gắn nhãn version khác nhau mà không biến đổi dữ liệu thực sự.
  • strategy: Webhook — cần thiết khi các version có khác biệt cấu trúc thực sự (ví dụ v1beta1 có field spec.replica (số ít) trong khi v1 đổi thành spec.replicas (số nhiều)). API server gọi một webhook HTTP bên ngoài, gửi ConversionReview chứa object ở version nguồn, nhận về object đã convert sang version đích. Webhook này chạy đồng bộ trên mọi request đọc/ghi liên quan tới version không phải storage version — một webhook conversion chậm hoặc down sẽ trực tiếp làm chậm hoặc chặn đứng mọi client vẫn đang dùng version cũ, tương tự cách một admission webhook failurePolicy: Fail có thể chặn write path đã mô tả ở phần đầu chương.

Việc thiết kế conversion webhook đúng đòi hỏi tư duy giống hệt thiết kế migration schema cơ sở dữ liệu có zero-downtime: conversion phải là song ánh có thể đảo ngược đủ tốt để một client cũ đọc dữ liệu do client mới ghi (và ngược lại) không bị mất thông tin quan trọng — nếu v1 có field mới không tồn tại ở v1beta1, convert v1 → v1beta1 buộc phải quyết định rõ ràng: bỏ field đó (chấp nhận mất thông tin với client cũ) hay tìm cách biểu diễn tương đương.

Subresource: Tách statusscale Khỏi spec

yaml
spec:
  subresources:
    status: {}
    scale:
      specReplicasPath: .spec.replicas
      statusReplicasPath: .status.replicas
      labelSelectorPath: .status.labelSelector

Bật subresources.status tách /status thành một endpoint REST riêng biệt (PUT/PATCH .../crontabs/my-crontab/status), với hệ quả quan trọng: một request UPDATE/PATCH thông thường lên object chính sẽ không thể sửa status, dù status vẫn nằm trong cùng object khi đọc. Đây chính là cơ chế kỹ thuật hiện thực hóa nguyên tắc tách biệt trách nhiệm cốt lõi của Kubernetes: client (người dùng, GitOps pipeline) sở hữu spec — thể hiện trạng thái mong muốn; controller sở hữu status — thể hiện trạng thái quan sát được. Không có subresource status, không gì ngăn được một kubectl apply vô tình ghi đè status mà một controller vừa cập nhật, gây ra race condition giữa ý định người dùng và quan sát thực tế của hệ thống.

subresources.scale cho phép CRD tương thích với kubectl scaleHorizontal Pod Autoscaler — bằng cách khai báo rõ đường dẫn JSON nào trong custom resource đóng vai trò spec.replicas (HPA ghi vào) và status.replicas (HPA đọc ra để tính toán), cho phép HPA điều khiển một custom resource hoàn toàn không biết gì về cấu trúc nội tại của nó — HPA chỉ cần đúng ba đường dẫn chuẩn hóa này để hoạt động, bất kể Kind thực sự là gì.

CEL Validation Rules: Ràng Buộc Vượt Ngoài Khả Năng Của JSON Schema Thuần

OpenAPI schema thuần chỉ diễn tả được ràng buộc cấu trúc (type, format, min/max) — không diễn tả được ràng buộc quan hệ giữa các field (ví dụ minReplicas phải nhỏ hơn hoặc bằng maxReplicas). x-kubernetes-validations cho phép nhúng biểu thức CEL (Common Expression Language) chạy ngay trong API server tại thời điểm admission, không cần webhook ngoài:

yaml
x-kubernetes-validations:
- rule: "self.spec.minReplicas <= self.spec.maxReplicas"
  message: "minReplicas must be less than or equal to maxReplicas"

self tham chiếu object hiện tại đang được validate; với rule chạy trên UPDATE, oldSelf cho phép so sánh với trạng thái trước đó — hữu ích để enforce field bất biến (immutable) mà không cần một webhook riêng chỉ để kiểm tra "field này không được đổi sau khi tạo". Việc CEL chạy trong tiến trình API server (thay vì gọi webhook ra ngoài) là khác biệt hiệu năng quan trọng: nó tránh hoàn toàn round-trip network, loại bỏ luôn rủi ro failurePolicy/timeoutSeconds vốn là gánh nặng vận hành của mọi webhook admission truyền thống.

CRD vs Aggregated API Server: Ranh Giới Khi Nào Dùng Cái Nào

CRD đủ cho tuyệt đại đa số nhu cầu mở rộng — nhưng nó có giới hạn cứng: storage backend luôn là etcd (thông qua chính API server đang chạy CRD đó), schema validation giới hạn trong khả năng của OpenAPI structural schema cộng CEL, và không có khả năng cắm logic nghiệp vụ tùy ý vào chính request path (mọi logic tùy chỉnh phải đi qua webhook, tức là một round-trip network riêng biệt).

Aggregated API server (APIService object trỏ tới một API server tùy chỉnh chạy như một Deployment riêng) loại bỏ toàn bộ giới hạn đó — nó là một API server thực sự, có thể dùng bất kỳ backend lưu trữ nào (không bắt buộc etcd), tự triển khai logic validate/convert trực tiếp trong code thay vì qua webhook, và có toàn quyền kiểm soát hành vi request. Cái giá phải trả là độ phức tạp vận hành tăng vọt: phải tự implement toàn bộ REST semantics (bao gồm cả watch, list với pagination, optimistic concurrency) đúng chuẩn Kubernetes API convention, tự quản lý một Deployment/Service/TLS cert riêng, và tự chịu trách nhiệm cho tính sẵn sàng của chính API server đó — một sự cố ở aggregated API server có thể ảnh hưởng trực tiếp tới trải nghiệm gọi API cho resource đó, tương tự cách chính kube-apiserver là điểm chịu lỗi trung tâm.

Nguyên tắc quyết định thực dụng: mặc định chọn CRD. Chỉ cân nhắc aggregated API server khi có nhu cầu rõ ràng vượt ngoài khả năng CRD — ví dụ cần backend lưu trữ không phải etcd (dữ liệu quá lớn hoặc cần transaction phức tạp hơn), hoặc cần logic validate quá nặng để chạy hợp lý trong một webhook đồng bộ trên mọi request.

Ràng Buộc Và Failure Mode Ở Scale

  • Conversion webhook down làm bất khả dụng mọi version không phải storage version, không chỉ ảnh hưởng riêng version đó — nếu client cũ (một Helm chart lâu năm, hoặc một Operator chưa nâng cấp) vẫn dùng v1beta1 trong khi storage version đã chuyển sang v1, webhook down đồng nghĩa client đó hoàn toàn không thể đọc/ghi resource, dù storage version v1 hoàn toàn khỏe mạnh.
  • Xóa một version served quá sớm (trước khi mọi client đã migrate) là lỗi vận hành phổ biến khi nâng cấp CRD — khác với API deprecation của Kubernetes core (có chính sách hỗ trợ nhiều version rất dài), một CRD tự quản không có cơ chế bảo vệ nào ngăn đội ngũ xóa version đang được client cũ sử dụng.
  • x-kubernetes-preserve-unknown-fields: true dùng tùy tiện để "tránh phải viết schema đầy đủ" vô hiệu hóa hoàn toàn lợi ích validate và pruning tại field đó — về bản chất biến field đó thành một blob JSON tùy ý mà API server không còn kiểm soát được nữa, đẩy toàn bộ trách nhiệm validate sang phía controller đọc dữ liệu, dễ dẫn tới silent data corruption nếu controller không tự validate cẩn thận.
  • CEL rule phức tạp trên object lớn tính toán tại mỗi lần admission có chi phí CPU thực sự trong pipeline request đồng bộ — không miễn phí như cảm giác "chỉ là một biểu thức". Với CRD có số lượng object lớn và tần suất update cao, CEL rule cần được thiết kế đơn giản và tránh duyệt sâu vào cấu trúc lồng nhau lớn, nếu không sẽ trở thành nguồn latency ẩn trong write path.

References