Operator: CRD Cộng Vòng Lặp Reconcile

K
Kai··5 min read

Bài 57 dựng CRD Widget, nhưng tạo một Widget xong thì không có gì xảy ra — CRD chỉ là cấu trúc dữ liệu, không ai phản ứng với nó. Operator là phần còn thiếu: một controller chạy vòng lặp, theo dõi custom resource và hành động để đưa thực tế về khớp với spec. Đây là cách Kubernetes tự vận hành mọi thứ (Deployment tạo ReplicaSet, ReplicaSet tạo Pod ở Bài 24), giờ áp cho kiểu của riêng ta. Bài này dựng một operator thật từ đầu, không dùng framework, để thấy rõ vòng lặp đó.

Operator là gì

Operator gói hai thứ: một CRD định nghĩa kiểu, và một controller theo dõi kiểu đó:

   user tạo/sửa/xóa  Echo (custom resource)
            │
            ▼  watch
   ┌──────────────── controller (chạy trong pod) ─────────────────┐
   │  vòng lặp reconcile:                                          │
   │    đọc spec mong muốn  ──►  so với thực tế  ──►  hành động     │
   │    (message, replicas)      (Deployment hiện có)  (tạo/sửa)    │
   │    rồi báo lại status                                         │
   └───────────────────────────────────────────────────────────────┘
            │  tạo/sửa
            ▼
        Deployment (resource thật, do operator quản)

Controller là một client của API server, không khác gì kubectl — nó đọc custom resource qua API, rồi tạo/sửa/xóa resource khác để khớp. Ta sẽ làm một operator quản kiểu Echo: mỗi Echo sinh ra một Deployment chạy http-echo với đúng message và số replicas khai trong spec.

CRD Echo

Dùng lại khuôn Bài 57. Echospec.message, spec.replicas, và status.ready để operator báo lại:

spec:
  group: kkloud.io
  names: {plural: echoes, singular: echo, kind: Echo, shortNames: [ec]}
  versions:
  - name: v1
    served: true
    storage: true
    subresources: {status: {}}
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            required: [message]
            properties:
              message:  {type: string}
              replicas: {type: integer, minimum: 1, maximum: 5, default: 1}
          status:
            properties: {ready: {type: string}}

Controller chạy trong pod

Controller ở đây là một vòng lặp shell dùng kubectl — đủ để thấy bản chất reconcile mà không cần framework. Với mỗi Echo, nó apply một Deployment mang ownerReferences trỏ về Echo đó, rồi cập nhật status.ready:

while true; do
  for name in $(kubectl get echoes -n $NS -o jsonpath='{.items[*].metadata.name}'); do
    msg=$(kubectl get echo $name -n $NS -o jsonpath='{.spec.message}')
    reps=$(kubectl get echo $name -n $NS -o jsonpath='{.spec.replicas}')
    uid=$(kubectl get echo $name -n $NS -o jsonpath='{.metadata.uid}')
    kubectl apply -f - <<YAML
    # Deployment echo-$name, replicas=$reps, http-echo "-text=$msg"
    # ownerReferences: {kind: Echo, name: $name, uid: $uid, controller: true}
    YAML
    ready=$(kubectl get deploy echo-$name -n $NS -o jsonpath='{.status.readyReplicas}')
    kubectl patch echo $name -n $NS --subresource=status -p "{\"status\":{\"ready\":\"$ready/$reps\"}}"
  done
  sleep 5
done

Chạy nó trong cluster như một operator thật: một ServiceAccount với RBAC vừa đủ (watch echoes, ghi echoes/status, quản deployments), và một Deployment dùng SA đó. kubectl trong pod tự dùng token của SA (Bài 53) nên không cần cấu hình gì thêm:

rules:
- {apiGroups: ["kkloud.io"], resources: ["echoes"],        verbs: ["get","list","watch"]}
- {apiGroups: ["kkloud.io"], resources: ["echoes/status"], verbs: ["get","update","patch"]}
- {apiGroups: ["apps"],      resources: ["deployments"],    verbs: ["get","list","create","update","patch","delete"]}

RBAC tối thiểu này (Bài 52) là một điểm: operator chỉ được đụng đúng resource nó quản, không hơn.

Reconcile khi tạo Echo

Tạo một Echo và xem operator phản ứng:

kubectl -n op-demo apply -f - <<'EOF'
apiVersion: kkloud.io/v1
kind: Echo
metadata: {name: hello}
spec: {message: "xin chao operator", replicas: 2}
EOF
# chờ ~10s
kubectl -n op-demo get ec
kubectl -n op-demo get deploy echo-hello
NAME    MESSAGE             REPLICAS   READY
hello   xin chao operator   2          2/2

NAME         READY   UP-TO-DATE   AVAILABLE   AGE
echo-hello   2/2     2            2           8s

Ta chỉ tạo một Echo; operator dựng Deployment echo-hello 2 replica và ghi status.ready=2/2 lên Echo. Deployment mang ownerReferences trỏ về Echo:

kubectl -n op-demo get deploy echo-hello \
  -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name} controller={.metadata.ownerReferences[0].controller}'
Echo/hello controller=true

Reconcile khi sửa, và GC khi xóa

Vòng lặp reconcile nghĩa là sửa spec thì operator đưa thực tế theo. Đổi replicas 2→4:

kubectl -n op-demo patch echo hello --type=merge -p '{"spec":{"replicas":4}}'
# chờ vòng reconcile kế
kubectl -n op-demo get deploy echo-hello
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
echo-hello   4/4     4            4           29s

Operator thấy spec đổi và scale Deployment lên 4. Xóa Echo thì Deployment đi theo — không phải vì operator dọn, mà nhờ ownerReferences + garbage collection (Bài 29):

kubectl -n op-demo delete echo hello
kubectl -n op-demo get deploy echo-hello
echo.kkloud.io "hello" deleted
Error from server (NotFound): deployments.apps "echo-hello" not found

Deployment biến mất ngay khi Echo bị xóa, vì nó là con của Echo qua ownerReferences — GC dọn con khi cha mất, đúng cơ chế Bài 29. Operator không cần code xử lý xóa; nó để quan hệ sở hữu lo việc đó.

🧹 Dọn dẹp

kubectl delete crd echoes.kkloud.io      # xóa CRD + mọi Echo còn lại
kubectl delete namespace op-demo          # xóa operator, SA, RBAC

Manifest và script reconcile ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 59-operator.

Tổng kết

Operator ghép CRD (Bài 57) với một controller chạy vòng lặp reconcile: đọc spec mong muốn, so với thực tế, hành động để khớp, báo lại status. Ta dựng một operator thật — CRD Echo và một controller shell chạy trong pod với ServiceAccount + RBAC tối thiểu — và thấy nó tự tạo Deployment khi ta tạo Echo (status.ready=2/2), tự scale khi ta sửa replicas lên 4, và để Deployment bị GC khi ta xóa Echo nhờ ownerReferences (Bài 29). Controller chỉ là một client API như kubectl, chạy trong cluster bằng token của SA (Bài 53), được RBAC bó vào đúng resource nó quản (Bài 52). Đó là mô hình mọi controller gốc của Kubernetes dùng, áp cho kiểu của riêng bạn — và là cách các operator thật (database, message queue, cert-manager) đóng gói kiến thức vận hành.

CRD và operator mở rộng API server bằng cách thêm dữ liệu và controller. Bài 60 mở rộng theo cách sâu hơn: API aggregation — gắn hẳn một API server thứ hai vào sau API server chính, phục vụ một nhóm API do server riêng đó cấp. Cụm ta đã có sẵn một ví dụ chạy từ Bài 39: metrics-server.