Kubernetes เป็นแพลตฟอร์มที่ทรงพลังสำหรับจัดการ Container แต่มีข้อจำกัดคือ Resource Types ที่มีมาให้ เช่น Pod, Deployment, Service อาจไม่เพียงพอสำหรับการจัดการ Application ที่ซับซ้อน เช่น Database Cluster, Message Queue หรือ Machine Learning Pipeline Kubernetes Operators คือคำตอบ เป็นวิธีขยาย Kubernetes API ด้วย Custom Resource Definition (CRD) และ Custom Controller เพื่อจัดการแอปพลิเคชันที่ซับซ้อนแบบอัตโนมัติ
บทความนี้จะอธิบายทุกอย่างเกี่ยวกับ Kubernetes Operators ตั้งแต่แนวคิดพื้นฐาน CRD Deep Dive Operator Pattern การสร้าง Operator ด้วย Kubebuilder และ Operator SDK รวมถึง Operator ยอดนิยมสำหรับ Database และ Middleware ทั้งหมดอ้างอิงจากแนวปฏิบัติล่าสุดในปี 2026
Kubernetes Operators คืออะไร?
Operator คือ Software Extension ของ Kubernetes ที่ใช้ Custom Resources เพื่อจัดการ Application และ Components ของ Application Operator ทำหน้าที่เป็น "Human Operator" อัตโนมัติ โดยนำความรู้เชิงปฏิบัติการ (Operational Knowledge) ของผู้ดูแลระบบมาเขียนเป็น Code
ตัวอย่างเช่น ถ้าคุณต้องการรัน PostgreSQL Cluster บน Kubernetes แบบเดิมคุณต้องจัดการเอง ทั้ง Replication, Failover, Backup, Scaling แต่ถ้าใช้ PostgreSQL Operator เช่น CloudNativePG คุณแค่เขียน YAML ระบุว่าต้องการ PostgreSQL Cluster กี่ Node มี Backup Policy อย่างไร Operator จะจัดการทุกอย่างให้อัตโนมัติ
หลักการสำคัญของ Operator ประกอบด้วย:
- Extend Kubernetes API — สร้าง Resource Types ใหม่ที่ Kubernetes ไม่มีมาให้ เช่น
PostgresCluster,KafkaTopic,RedisCluster - Automate Day-2 Operations — จัดการงานหลัง Deploy เช่น Backup, Restore, Scaling, Upgrade, Failover โดยอัตโนมัติ
- Domain-Specific Knowledge — Operator ถูกเขียนโดยผู้เชี่ยวชาญที่เข้าใจวิธีจัดการ Application นั้นๆ ดีที่สุด
- Declarative API — ผู้ใช้ระบุ Desired State ใน YAML แล้ว Operator ทำให้ Actual State ตรงกับ Desired State
Custom Resource Definition (CRD) Deep Dive
CRD คือวิธีที่ Kubernetes ให้คุณสร้าง Resource Type ใหม่ เมื่อสร้าง CRD แล้ว คุณสามารถใช้ kubectl จัดการ Custom Resource ได้เหมือน Resource มาตรฐาน เช่น kubectl get postgresclusters หรือ kubectl describe kafkatopic my-topic
โครงสร้างของ CRD
# ตัวอย่าง CRD สำหรับ WebApplication
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: webapplications.app.example.com
spec:
group: app.example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required:
- image
- replicas
properties:
image:
type: string
description: "Docker image สำหรับ Application"
replicas:
type: integer
minimum: 1
maximum: 100
description: "จำนวน Replicas"
port:
type: integer
default: 8080
description: "Port ที่ Application Listen"
resources:
type: object
properties:
cpu:
type: string
default: "100m"
memory:
type: string
default: "128Mi"
autoscaling:
type: object
properties:
enabled:
type: boolean
default: false
minReplicas:
type: integer
default: 1
maxReplicas:
type: integer
default: 10
targetCPU:
type: integer
default: 80
status:
type: object
properties:
availableReplicas:
type: integer
conditions:
type: array
items:
type: object
properties:
type:
type: string
status:
type: string
lastTransitionTime:
type: string
format: date-time
message:
type: string
subresources:
status: {}
additionalPrinterColumns:
- name: Image
type: string
jsonPath: .spec.image
- name: Replicas
type: integer
jsonPath: .spec.replicas
- name: Available
type: integer
jsonPath: .status.availableReplicas
- name: Age
type: date
jsonPath: .metadata.creationTimestamp
scope: Namespaced
names:
plural: webapplications
singular: webapplication
kind: WebApplication
shortNames:
- webapp
- wa
สร้าง Custom Resource จาก CRD
# สร้าง WebApplication Resource
apiVersion: app.example.com/v1
kind: WebApplication
metadata:
name: my-frontend
namespace: production
spec:
image: myregistry.com/frontend:v2.1.0
replicas: 3
port: 3000
resources:
cpu: "500m"
memory: "256Mi"
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 20
targetCPU: 70
# ใช้งานด้วย kubectl
kubectl apply -f webapp.yaml
kubectl get webapplications # หรือ kubectl get webapp
kubectl describe webapp my-frontend
kubectl delete webapp my-frontend
Operator Pattern: Observe - Analyze - Act
หัวใจของ Operator คือ Reconciliation Loop ซึ่งเป็นวงจรที่ทำงานต่อเนื่อง ประกอบด้วย 3 ขั้นตอนหลัก
1. Observe (สังเกต)
Controller ใช้ Kubernetes Watch API เพื่อรับการแจ้งเตือนเมื่อ Custom Resource ถูกสร้าง แก้ไข หรือลบ นอกจากนี้ยังเฝ้าดู Resources ที่เกี่ยวข้อง เช่น Pod, Service, PVC
2. Analyze (วิเคราะห์)
เปรียบเทียบ Desired State (สิ่งที่ผู้ใช้ระบุใน Spec) กับ Actual State (สถานะปัจจุบันของ Resources ในคลัสเตอร์) เพื่อหาความแตกต่าง
3. Act (ดำเนินการ)
ทำให้ Actual State ตรงกับ Desired State เช่น สร้าง Pod เพิ่ม ลบ Pod ที่ไม่ต้องการ อัปเดต Configuration หรือเรียก External API
// Reconciliation Loop (Go pseudocode)
func (r *WebAppReconciler) Reconcile(ctx context.Context,
req ctrl.Request) (ctrl.Result, error) {
// 1. OBSERVE: ดึง Custom Resource
webapp := &appv1.WebApplication{}
if err := r.Get(ctx, req.NamespacedName, webapp); err != nil {
if errors.IsNotFound(err) {
// Resource ถูกลบแล้ว ไม่ต้องทำอะไร
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// 2. ANALYZE: ตรวจสอบ Deployment ปัจจุบัน
deployment := &appsv1.Deployment{}
err := r.Get(ctx, types.NamespacedName{
Name: webapp.Name,
Namespace: webapp.Namespace,
}, deployment)
if errors.IsNotFound(err) {
// 3. ACT: สร้าง Deployment ใหม่
deployment = r.createDeployment(webapp)
if err := r.Create(ctx, deployment); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
}
// 3. ACT: อัปเดต Deployment ถ้า Spec เปลี่ยน
if needsUpdate(deployment, webapp) {
deployment.Spec.Replicas = &webapp.Spec.Replicas
deployment.Spec.Template.Spec.Containers[0].Image = webapp.Spec.Image
if err := r.Update(ctx, deployment); err != nil {
return ctrl.Result{}, err
}
}
// อัปเดต Status
webapp.Status.AvailableReplicas = deployment.Status.AvailableReplicas
if err := r.Status().Update(ctx, webapp); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
Popular Operators ที่ใช้กันมากในปี 2026
Database Operators
| Operator | Database | ความสามารถหลัก | ความนิยม |
|---|---|---|---|
| CloudNativePG | PostgreSQL | HA, Backup/Restore, Monitoring, WAL Archiving | สูงมาก |
| Zalando Postgres Operator | PostgreSQL | Patroni-based HA, Connection Pooling | สูง |
| Percona Operator | MySQL/MongoDB/PG | Multi-DB Support, Backup, Encryption | สูง |
| Oracle MySQL Operator | MySQL | InnoDB Cluster, MySQL Router | กลาง |
| MongoDB Enterprise Operator | MongoDB | Ops Manager Integration, Sharding | สูง |
| Spotahome Redis Operator | Redis | Sentinel, Cluster Mode, Failover | สูง |
Middleware Operators
| Operator | Application | ความสามารถหลัก |
|---|---|---|
| Strimzi | Apache Kafka | Kafka Cluster, Topic, User, Connect, Mirror Maker |
| ECK (Elastic Cloud on K8s) | Elasticsearch | Elasticsearch, Kibana, APM, Fleet |
| RabbitMQ Cluster Operator | RabbitMQ | Cluster, Queue, Policy Management |
| Prometheus Operator | Prometheus | Prometheus, Alertmanager, ServiceMonitor |
| cert-manager | TLS Certificates | Auto-renewal, ACME, Let's Encrypt |
| ArgoCD | GitOps | Application, ApplicationSet, Sync |
ตัวอย่างการใช้ CloudNativePG Operator
# ติดตั้ง CloudNativePG Operator
kubectl apply -f https://raw.githubusercontent.com/cloudnative-pg/
cloudnative-pg/release-1.22/releases/cnpg-1.22.0.yaml
# สร้าง PostgreSQL Cluster 3 Nodes
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: my-postgres
namespace: database
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:16.2
storage:
size: 50Gi
storageClass: fast-ssd
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2"
postgresql:
parameters:
max_connections: "200"
shared_buffers: "512MB"
effective_cache_size: "1536MB"
work_mem: "4MB"
backup:
barmanObjectStore:
destinationPath: "s3://my-backups/postgres/"
s3Credentials:
accessKeyId:
name: aws-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: aws-creds
key: ACCESS_SECRET_KEY
retentionPolicy: "30d"
monitoring:
enablePodMonitor: true
# ตรวจสอบสถานะ
kubectl get cluster -n database
kubectl describe cluster my-postgres -n database
# ดู Instances
kubectl get pods -n database -l cnpg.io/cluster=my-postgres
# Promote Replica เป็น Primary (manual failover)
kubectl cnpg promote my-postgres 2 -n database
ตัวอย่างการใช้ Strimzi Kafka Operator
# ติดตั้ง Strimzi Operator
kubectl create namespace kafka
kubectl apply -f https://strimzi.io/install/latest?namespace=kafka
# สร้าง Kafka Cluster
apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
name: my-kafka
namespace: kafka
spec:
kafka:
version: 3.7.0
replicas: 3
listeners:
- name: plain
port: 9092
type: internal
tls: false
- name: tls
port: 9093
type: internal
tls: true
config:
offsets.topic.replication.factor: 3
transaction.state.log.replication.factor: 3
transaction.state.log.min.isr: 2
default.replication.factor: 3
min.insync.replicas: 2
storage:
type: persistent-claim
size: 100Gi
class: fast-ssd
resources:
requests:
memory: 2Gi
cpu: "1"
limits:
memory: 4Gi
cpu: "2"
zookeeper:
replicas: 3
storage:
type: persistent-claim
size: 20Gi
entityOperator:
topicOperator: {}
userOperator: {}
---
# สร้าง Kafka Topic ผ่าน CRD
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaTopic
metadata:
name: orders
namespace: kafka
labels:
strimzi.io/cluster: my-kafka
spec:
partitions: 12
replicas: 3
config:
retention.ms: 604800000 # 7 days
cleanup.policy: delete
max.message.bytes: 1048576 # 1MB
สร้าง Operator ด้วย Kubebuilder
Kubebuilder เป็น Framework สำหรับสร้าง Kubernetes Operators ด้วยภาษา Go เป็นโปรเจกต์ของ Kubernetes SIG เป็นตัวเลือกยอดนิยมที่สุดสำหรับการสร้าง Operator แบบ Production-grade
# ติดตั้ง Kubebuilder
curl -L -o kubebuilder "https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)"
chmod +x kubebuilder && sudo mv kubebuilder /usr/local/bin/
# สร้างโปรเจกต์ใหม่
mkdir webapp-operator && cd webapp-operator
kubebuilder init --domain example.com --repo github.com/myorg/webapp-operator
# สร้าง API (CRD + Controller)
kubebuilder create api --group app --version v1 --kind WebApplication
# โครงสร้างไฟล์ที่ได้:
# ├── api/v1/
# │ ├── webapplication_types.go # CRD Type Definition
# │ └── zz_generated.deepcopy.go # Auto-generated
# ├── config/
# │ ├── crd/ # CRD YAML
# │ ├── manager/ # Deployment YAML
# │ ├── rbac/ # RBAC YAML
# │ └── samples/ # Sample CR YAML
# ├── controllers/
# │ └── webapplication_controller.go # Reconciliation Logic
# ├── main.go
# ├── Dockerfile
# └── Makefile
กำหนด CRD Types (Go)
// api/v1/webapplication_types.go
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// WebApplicationSpec กำหนด Desired State
type WebApplicationSpec struct {
// Image คือ Container Image ที่จะ Deploy
// +kubebuilder:validation:Required
Image string `json:"image"`
// Replicas คือจำนวน Pod ที่ต้องการ
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=100
// +kubebuilder:default=1
Replicas int32 `json:"replicas"`
// Port คือ Port ที่ Application Listen
// +kubebuilder:default=8080
Port int32 `json:"port,omitempty"`
// Resources กำหนด CPU/Memory
Resources ResourceSpec `json:"resources,omitempty"`
}
type ResourceSpec struct {
CPU string `json:"cpu,omitempty"`
Memory string `json:"memory,omitempty"`
}
// WebApplicationStatus กำหนด Observed State
type WebApplicationStatus struct {
AvailableReplicas int32 `json:"availableReplicas,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
LastUpdated *metav1.Time `json:"lastUpdated,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Image",type=string,JSONPath=`.spec.image`
// +kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=`.spec.replicas`
// +kubebuilder:printcolumn:name="Available",type=integer,JSONPath=`.status.availableReplicas`
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=`.metadata.creationTimestamp`
type WebApplication struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec WebApplicationSpec `json:"spec,omitempty"`
Status WebApplicationStatus `json:"status,omitempty"`
}
เขียน Controller (Reconciliation Logic)
// controllers/webapplication_controller.go
package controllers
import (
"context"
appv1 "github.com/myorg/webapp-operator/api/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type WebAppReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=app.example.com,resources=webapplications,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=app.example.com,resources=webapplications/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
func (r *WebAppReconciler) Reconcile(ctx context.Context,
req ctrl.Request) (ctrl.Result, error) {
log := ctrl.LoggerFrom(ctx)
// ดึง WebApplication CR
webapp := &appv1.WebApplication{}
if err := r.Get(ctx, req.NamespacedName, webapp); err != nil {
if errors.IsNotFound(err) {
log.Info("WebApplication deleted, cleaning up")
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// Reconcile Deployment
if err := r.reconcileDeployment(ctx, webapp); err != nil {
return ctrl.Result{}, err
}
// Reconcile Service
if err := r.reconcileService(ctx, webapp); err != nil {
return ctrl.Result{}, err
}
// อัปเดต Status
return r.updateStatus(ctx, webapp)
}
func (r *WebAppReconciler) reconcileDeployment(ctx context.Context,
webapp *appv1.WebApplication) error {
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: webapp.Name,
Namespace: webapp.Namespace,
},
}
_, err := ctrl.CreateOrUpdate(ctx, r.Client, deployment, func() error {
// กำหนด Owner Reference เพื่อ Garbage Collection
ctrl.SetControllerReference(webapp, deployment, r.Scheme)
labels := map[string]string{"app": webapp.Name}
deployment.Spec = appsv1.DeploymentSpec{
Replicas: &webapp.Spec.Replicas,
Selector: &metav1.LabelSelector{
MatchLabels: labels,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: labels},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "app",
Image: webapp.Spec.Image,
Ports: []corev1.ContainerPort{{
ContainerPort: webapp.Spec.Port,
}},
}},
},
},
}
return nil
})
return err
}
func (r *WebAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&appv1.WebApplication{}).
Owns(&appsv1.Deployment{}).
Owns(&corev1.Service{}).
Complete(r)
}
# Build และ Deploy Operator
make manifests # สร้าง CRD YAML
make install # ติดตั้ง CRD ใน Cluster
make docker-build docker-push IMG=myregistry.com/webapp-operator:v0.1.0
make deploy IMG=myregistry.com/webapp-operator:v0.1.0
# ทดสอบ
kubectl apply -f config/samples/app_v1_webapplication.yaml
kubectl get webapp
kubectl describe webapp webapp-sample
Operator SDK (ทางเลือกอื่นนอกจาก Go)
Operator SDK รองรับ 3 วิธีในการสร้าง Operator ให้เลือกตามความถนัดและความซับซ้อนของ Operator
1. Go-based Operator
เหมือน Kubebuilder (Operator SDK ใช้ Kubebuilder เป็น Foundation) เหมาะกับ Operator ที่ซับซ้อน ต้องการ Logic มาก ประสิทธิภาพสูง
2. Ansible-based Operator
เขียน Reconciliation Logic ด้วย Ansible Playbook เหมาะกับ SysAdmin ที่คุ้นเคย Ansible ไม่ต้องเขียน Go
# watches.yaml
- version: v1
group: app.example.com
kind: WebApplication
role: webapp
# roles/webapp/tasks/main.yml
- name: Create Deployment
kubernetes.core.k8s:
state: present
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: "{{ ansible_operator_meta.name }}"
namespace: "{{ ansible_operator_meta.namespace }}"
spec:
replicas: "{{ replicas | default(1) }}"
selector:
matchLabels:
app: "{{ ansible_operator_meta.name }}"
template:
metadata:
labels:
app: "{{ ansible_operator_meta.name }}"
spec:
containers:
- name: app
image: "{{ image }}"
ports:
- containerPort: "{{ port | default(8080) }}"
3. Helm-based Operator
ใช้ Helm Chart ที่มีอยู่แล้วเป็น Reconciliation Logic เหมาะกับการแปลง Helm Chart เป็น Operator อย่างรวดเร็ว
# watches.yaml
- version: v1
group: app.example.com
kind: WebApplication
chart: helm-charts/webapp
# Operator จะรัน Helm Upgrade ทุกครั้งที่ CR เปลี่ยน
# ค่า Spec ของ CR จะถูก Map เป็น Helm Values อัตโนมัติ
Operator Lifecycle Manager (OLM)
OLM เป็นเครื่องมือสำหรับจัดการ Lifecycle ของ Operator บน Kubernetes ทำหน้าที่คล้ายกับ Package Manager สำหรับ Operator โดยจัดการ Installation, Upgrade, RBAC, Dependencies อัตโนมัติ
# ติดตั้ง OLM
operator-sdk olm install
# ค้นหา Operator ใน OperatorHub
kubectl get packagemanifests
# ติดตั้ง Operator ผ่าน OLM
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
name: cloudnative-pg
namespace: operators
spec:
channel: stable
name: cloudnative-pg
source: operatorhubio-catalog
sourceNamespace: olm
installPlanApproval: Automatic # หรือ Manual สำหรับ Production
OperatorHub.io
OperatorHub.io เป็น Registry กลางสำหรับค้นหาและติดตั้ง Operator มี Operator กว่า 300 ตัว ครอบคลุม Database, Monitoring, Networking, Security, CI/CD และอื่นๆ ทุก Operator บน OperatorHub ผ่านการตรวจสอบคุณภาพก่อนเผยแพร่
Operator Maturity Model (5 Levels)
Operator Capability Model กำหนด 5 ระดับความสามารถ ช่วยให้ประเมินความพร้อมของ Operator ได้อย่างมีหลักเกณฑ์
| Level | ชื่อ | ความสามารถ | ตัวอย่าง |
|---|---|---|---|
| 1 | Basic Install | ติดตั้ง Application ได้อัตโนมัติ สร้าง Resources ที่จำเป็น | Helm-based Operator |
| 2 | Seamless Upgrades | Upgrade Application ได้โดยไม่ Downtime ย้อนกลับได้ | รองรับ Rolling Update |
| 3 | Full Lifecycle | Backup/Restore, Scaling, Configuration Management | CloudNativePG |
| 4 | Deep Insights | Metrics, Alerts, Log Analysis, Performance Tuning | ECK + Monitoring |
| 5 | Auto Pilot | Auto-scaling, Auto-healing, Auto-tuning, Anomaly Detection | Full Auto-ops |
Operator ส่วนใหญ่ในปัจจุบันอยู่ที่ Level 3-4 การไปถึง Level 5 ต้องมี Machine Learning หรือ AI ช่วยวิเคราะห์ซึ่งเริ่มเห็นมากขึ้นในปี 2026 โดยเฉพาะ Database Operator ที่สามารถ Auto-tune Parameters ตาม Workload ได้
Testing Operators
การทดสอบ Operator เป็นสิ่งสำคัญมาก เพราะ Operator มีผลกระทบต่อ Infrastructure ทั้งหมด ถ้า Bug จะทำให้ Database ล่มหรือข้อมูลหาย
Unit Tests
// Go unit test สำหรับ Reconciler
func TestReconcile_CreateDeployment(t *testing.T) {
// Setup
scheme := runtime.NewScheme()
appv1.AddToScheme(scheme)
appsv1.AddToScheme(scheme)
webapp := &appv1.WebApplication{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "default",
},
Spec: appv1.WebApplicationSpec{
Image: "nginx:latest",
Replicas: 3,
Port: 8080,
},
}
client := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(webapp).
Build()
reconciler := &WebAppReconciler{
Client: client,
Scheme: scheme,
}
// Execute
result, err := reconciler.Reconcile(context.Background(),
ctrl.Request{NamespacedName: types.NamespacedName{
Name: "test-app", Namespace: "default",
}})
// Verify
assert.NoError(t, err)
assert.False(t, result.Requeue)
deployment := &appsv1.Deployment{}
err = client.Get(context.Background(), types.NamespacedName{
Name: "test-app", Namespace: "default",
}, deployment)
assert.NoError(t, err)
assert.Equal(t, int32(3), *deployment.Spec.Replicas)
}
Integration Tests (envtest)
// ใช้ envtest สำหรับ Integration Test (มี real API Server)
var _ = Describe("WebApplication Controller", func() {
Context("When creating a WebApplication", func() {
It("Should create a Deployment", func() {
webapp := &appv1.WebApplication{
ObjectMeta: metav1.ObjectMeta{
Name: "test-webapp",
Namespace: "default",
},
Spec: appv1.WebApplicationSpec{
Image: "nginx:1.25",
Replicas: 2,
},
}
Expect(k8sClient.Create(ctx, webapp)).Should(Succeed())
// รอให้ Deployment ถูกสร้าง
Eventually(func() bool {
dep := &appsv1.Deployment{}
err := k8sClient.Get(ctx, types.NamespacedName{
Name: "test-webapp", Namespace: "default",
}, dep)
return err == nil
}, timeout, interval).Should(BeTrue())
})
})
})
E2E Tests
# ใช้ kind (Kubernetes in Docker) สำหรับ E2E Test
# Makefile
test-e2e:
kind create cluster --name operator-test
make install
make deploy IMG=myregistry.com/webapp-operator:test
kubectl apply -f test/e2e/fixtures/
go test ./test/e2e/ -v -count=1
kind delete cluster --name operator-test
Operator Best Practices
- ใช้ Owner References — ตั้ง Owner Reference บน Resources ที่ Operator สร้าง เพื่อให้ Kubernetes ลบอัตโนมัติเมื่อ CR ถูกลบ (Garbage Collection)
- Implement Finalizers — ใช้ Finalizer สำหรับ Cleanup Logic ที่ต้องทำก่อน Resource ถูกลบ เช่น ลบ External Resources, ทำ Backup สุดท้าย
- Status Conditions — อัปเดต Status Conditions ให้สะท้อนสถานะจริง ใช้ Standard Condition Types เช่น Ready, Available, Progressing, Degraded
- Idempotent Reconciliation — Reconcile Function ต้อง Idempotent เรียกซ้ำกี่ครั้งก็ได้ผลเหมือนกัน ใช้ CreateOrUpdate แทน Create
- Rate Limit Reconciliation — อย่า Reconcile ถี่เกินไป ใช้ RequeueAfter แทน Requeue ทันที
- Watch Related Resources — Watch ทั้ง Primary Resource (For) และ Secondary Resources (Owns) เพื่อ Reconcile เมื่อ Resources ที่เกี่ยวข้องเปลี่ยน
- Handle Edge Cases — จัดการกรณีที่ Resource ถูกลบโดยคนอื่น (ไม่ผ่าน Operator) Namespace ถูกลบ หรือ API Server ไม่ตอบ
- RBAC Least Privilege — ให้สิทธิ์ Operator เฉพาะที่จำเป็น ไม่ให้ cluster-admin
- Metrics และ Logging — Expose Prometheus Metrics สำหรับ Reconciliation Duration, Error Count, Queue Depth ใช้ Structured Logging
- CRD Versioning — วางแผน CRD Versioning ตั้งแต่แรก เริ่มที่ v1alpha1 แล้วค่อย Promote เป็น v1beta1 และ v1
เมื่อไหร่ควรใช้ Operator vs Helm Chart
| เกณฑ์ | Helm Chart | Operator |
|---|---|---|
| การติดตั้ง | เพียงพอ | เพียงพอ |
| Day-2 Operations | ต้องทำ Manual | อัตโนมัติ |
| Stateful App | ยาก | เหมาะมาก |
| Backup/Restore | ต้องเขียน Script | ในตัว |
| Auto Failover | ไม่รองรับ | รองรับ |
| ความซับซ้อน Dev | ต่ำ | สูง |
| Maintenance | ต่ำ | สูง (ต้อง maintain controller) |
สรุปง่ายๆ: ถ้าแอปเป็น Stateless ใช้ Helm Chart ก็เพียงพอ แต่ถ้าเป็น Stateful Application ที่ต้องการ Auto-healing, Backup, Failover ควรใช้ Operator หรือหา Operator ที่มีคนทำไว้แล้วจาก OperatorHub
RBAC สำหรับ Operators
Operator ทำงานด้วยสิทธิ์ที่กำหนดผ่าน ServiceAccount, Role/ClusterRole และ RoleBinding/ClusterRoleBinding ต้องให้สิทธิ์เท่าที่จำเป็นเท่านั้น (Principle of Least Privilege)
# ClusterRole สำหรับ WebApp Operator
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: webapp-operator-role
rules:
# สิทธิ์จัดการ CRD
- apiGroups: ["app.example.com"]
resources: ["webapplications"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["app.example.com"]
resources: ["webapplications/status"]
verbs: ["get", "update", "patch"]
- apiGroups: ["app.example.com"]
resources: ["webapplications/finalizers"]
verbs: ["update"]
# สิทธิ์จัดการ Deployment, Service
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# สิทธิ์อ่าน Events
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]
Monitoring Operator Health
การ Monitor สุขภาพของ Operator เป็นสิ่งจำเป็น เพราะถ้า Operator ล่มหรือทำงานผิดปกติ Application ที่มันดูแลจะไม่ได้รับการ Reconcile และอาจเกิดปัญหาตามมา Metric สำคัญที่ต้อง Monitor ได้แก่ Reconciliation Duration ที่บอกว่า Operator ใช้เวลาเท่าไรในการ Reconcile แต่ละรอบ ถ้านานขึ้นเรื่อยๆ อาจมีปัญหา นอกจากนี้ต้อง Monitor Work Queue Depth ซึ่งบอกจำนวนงานที่รอ Reconcile ถ้าสะสมมากอาจหมายความว่า Operator ทำงานไม่ทัน และ Error Rate คือจำนวน Reconciliation ที่ล้มเหลว ควรตั้ง Alert เมื่อสูงเกินไป
# ServiceMonitor สำหรับ Operator (Prometheus)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: webapp-operator-metrics
namespace: operator-system
spec:
selector:
matchLabels:
control-plane: controller-manager
endpoints:
- port: https
path: /metrics
scheme: https
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
tlsConfig:
insecureSkipVerify: true
# Grafana Dashboard Query Examples:
# Reconciliation Rate: rate(controller_runtime_reconcile_total[5m])
# Error Rate: rate(controller_runtime_reconcile_errors_total[5m])
# Queue Depth: workqueue_depth
# Reconcile Duration p99: histogram_quantile(0.99,
# rate(controller_runtime_reconcile_time_seconds_bucket[5m]))
สรุป
Kubernetes Operators เป็นวิธีที่ดีที่สุดในการจัดการ Application ที่ซับซ้อนบน Kubernetes โดยเฉพาะ Stateful Application เช่น Database, Message Queue และ Cache Cluster CRD ช่วยให้สร้าง API ใหม่ที่เหมาะกับ Domain ของคุณ ในขณะที่ Controller จัดการ Reconciliation Loop อัตโนมัติ
สำหรับผู้เริ่มต้น แนะนำให้เริ่มจากการใช้ Operator ที่มีคนทำไว้แล้ว เช่น CloudNativePG สำหรับ PostgreSQL หรือ Strimzi สำหรับ Kafka ก่อน เมื่อเข้าใจแนวคิดแล้วค่อยลองสร้าง Operator ของตัวเองด้วย Kubebuilder เริ่มจาก Operator ง่ายๆ แล้วค่อยเพิ่มความซับซ้อน การเลือกใช้ Operator แทน Helm Chart ควรพิจารณาจากความจำเป็น ถ้าแอปต้องการ Day-2 Operations อัตโนมัติ Operator คือคำตอบที่ถูกต้องในปี 2026
