Operator และ Custom Resource
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”CustomResourceDefinition ต่อขยาย Kubernetes API ด้วย object Kind ใหม่ ส่วน Operator คือ CRD นั้นจับคู่กับ controller ที่คอย reconcile สถานะจริงให้ตรงกับที่ประกาศไว้ตลอดเวลา
CustomResourceDefinition: สอน API server ให้รู้จัก Kind ใหม่
หัวข้อที่มีชื่อว่า “CustomResourceDefinition: สอน API server ให้รู้จัก Kind ใหม่”CustomResourceDefinition (CRD) ลงทะเบียน object Kind ใหม่เข้ากับ Kubernetes API server พอติดตั้งแล้ว instance ของ Kind นั้นจะทำงานเหมือน built-in object ทุกอย่าง kubectl get/kubectl apply ใช้ได้ตามปกติ object เหล่านี้ถูกเก็บใน etcd เหมือนของอื่น ๆ และ watch ผ่าน API ได้เหมือน Pod หรือ Deployment ตัว CRD เองกำหนด schema ของ Kind นั้นด้วย OpenAPI v3 schema ทำให้ API server validate instance ตอนเขียนได้
apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: name: databases.example.comspec: group: example.com versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: engine: type: string enum: ['postgres', 'mysql'] storageGB: type: integer minimum: 1 scope: Namespaced names: plural: databases singular: database kind: Database shortNames: ['db']# An instance of the new Kind, just like any other manifestapiVersion: example.com/v1kind: Databasemetadata: name: orders-dbspec: engine: postgres storageGB: 20kubectl apply -f orders-db.yamlkubectl get databaseskubectl get db orders-db -o yamlCRD เดี่ยว ๆ คือแค่ schema controller ต่างหากที่ทำให้กลายเป็น Operator
หัวข้อที่มีชื่อว่า “CRD เดี่ยว ๆ คือแค่ schema controller ต่างหากที่ทำให้กลายเป็น Operator”การ apply object Database นั้นไม่ทำอะไรด้วยตัวเอง API server แค่เก็บลง etcd แล้ว validate โครงสร้าง แต่ไม่มีอะไรไป provision database จริงให้ CRD เพิ่มแค่ schema กับที่เก็บสำหรับ Kind ใหม่ ไม่มี behavior ใด ๆ Operator pattern คือ CRD นั้นจับคู่กับ controller ที่คอย watch instance ของ custom resource แล้วขับเคลื่อน infrastructure จริงให้ตรงกับสิ่งที่อธิบายไว้ ใช้ reconciliation loop แบบเดียวกัน (watch, diff, act) ที่ built-in controller ของ Kubernetes เองใช้กับ Deployment กับ ReplicaSet เพียงแต่เล็งไปที่ความรู้เฉพาะแอปหรือเฉพาะงาน operation แทนที่จะเป็นการ schedule Pod ทั่วไป database Operator อาจ provision database instance จริงตอนที่ resource Database ถูกสร้าง ทำ backup เป็นระยะเพราะ resource บอกไว้แบบนั้น และจัดการ failover อัตโนมัติเมื่อ replica ตัวไหนไม่ healthy ถ้าไม่มี controller ตัวนี้ CRD ก็เป็นแค่รูปแบบที่ kubectl ยอมรับเท่านั้นเอง
ทำไมตรงนี้ programmatic client ถึงคุ้มค่าจริง ๆ
หัวข้อที่มีชื่อว่า “ทำไมตรงนี้ programmatic client ถึงคุ้มค่าจริง ๆ”ที่อื่นในคอร์สนี้ kubectl apply เพียงพออยู่แล้วเพราะมนุษย์รันครั้งเดียวแล้ว built-in controller ก็ทำต่อเอง ส่วน controller ของ Operator ต่างออกไป คือต้องรันต่อเนื่อง ตอบสนองต่อทุก add, update, delete ของ custom resource ตราบเท่าที่ cluster ยังอยู่ นี่คือสถานการณ์ที่คำสั่ง CLI ครั้งเดียวรองรับไม่ได้ แต่ client แบบ programmatic ที่ watch อยู่ตลอดรองรับได้ การใช้ class Watch ของ @kubernetes/client-node เล็งไปที่ API path ของ custom resource เป็นวิธีที่สมจริงในการสร้าง reconciliation loop ที่เป็นหัวใจของ Operator
import * as k8s from '@kubernetes/client-node';
const kc = new k8s.KubeConfig();kc.loadFromDefault();
const watch = new k8s.Watch(kc);
// Path for a namespace-scoped custom resource: /apis/{group}/{version}/namespaces/{namespace}/{plural}const path = '/apis/example.com/v1/namespaces/default/databases';
async function reconcile(database: any) { const name = database.metadata?.name; const engine = database.spec?.engine; const storageGB = database.spec?.storageGB; console.log(`reconciling Database ${name}: engine=${engine} storageGB=${storageGB}`); // Real controller: provision or update the actual database instance here}
const req = await watch.watch( path, {}, (type, apiObj) => { if (type === 'ADDED' || type === 'MODIFIED') { reconcile(apiObj); } else if (type === 'DELETED') { console.log(`Database ${apiObj.metadata?.name} deleted, tearing down real instance`); } }, (err) => { console.error('watch ended', err); },);
// Later, on shutdown:// req.abort();ทุก event ADDED หรือ MODIFIED จะรัน reconcile step ใหม่อีกครั้ง ที่เป็นแก่นของ Operator pattern เลย คือคอยเทียบ desired state (custom resource) กับ real state (infrastructure จริง) แล้วแก้ส่วนต่างไปเรื่อย ๆ โดยไม่ต้องให้มนุษย์มารันคำสั่งซ้ำ
flowchart LR
cr["Database custom resource (desired state)"] -->|watch| ctrl["Operator controller"]
ctrl -->|diff| decide{"Does real state match spec?"}
decide -->|no| act["Provision, resize, or repair"]
decide -->|yes| noop["Do nothing this cycle"]
act --> infra["Real PostgreSQL instance"]
infra -->|status| ctrl