Home > Blog > tech

Kubernetes Operators และ CRD คืออะไร? สอนสร้าง Custom Controller สำหรับ K8s 2026

kubernetes operators crd guide
Kubernetes Operators CRD Guide 2026
2026-04-08 | tech | 3500 words

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 ประกอบด้วย:

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
CRD vs ConfigMap: หลายคนสงสัยว่าทำไมไม่ใช้ ConfigMap เก็บ Configuration แล้วให้ Controller อ่าน คำตอบคือ CRD ให้ Type Safety (Schema Validation), Versioning, Subresources (Status), RBAC ระดับ Resource Type และ kubectl Support ที่ ConfigMap ไม่มี

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
}
Idempotency สำคัญมาก: Reconcile Function อาจถูกเรียกหลายครั้งสำหรับ Event เดียวกัน ดังนั้นต้องเขียนให้ Idempotent คือ ไม่ว่าจะเรียกกี่ครั้งก็ได้ผลลัพธ์เหมือนกัน ตรวจสอบก่อนเสมอว่า Resource มีอยู่แล้วหรือยัง ก่อนสร้างใหม่

Popular Operators ที่ใช้กันมากในปี 2026

Database Operators

OperatorDatabaseความสามารถหลักความนิยม
CloudNativePGPostgreSQLHA, Backup/Restore, Monitoring, WAL Archivingสูงมาก
Zalando Postgres OperatorPostgreSQLPatroni-based HA, Connection Poolingสูง
Percona OperatorMySQL/MongoDB/PGMulti-DB Support, Backup, Encryptionสูง
Oracle MySQL OperatorMySQLInnoDB Cluster, MySQL Routerกลาง
MongoDB Enterprise OperatorMongoDBOps Manager Integration, Shardingสูง
Spotahome Redis OperatorRedisSentinel, Cluster Mode, Failoverสูง

Middleware Operators

OperatorApplicationความสามารถหลัก
StrimziApache KafkaKafka Cluster, Topic, User, Connect, Mirror Maker
ECK (Elastic Cloud on K8s)ElasticsearchElasticsearch, Kibana, APM, Fleet
RabbitMQ Cluster OperatorRabbitMQCluster, Queue, Policy Management
Prometheus OperatorPrometheusPrometheus, Alertmanager, ServiceMonitor
cert-managerTLS CertificatesAuto-renewal, ACME, Let's Encrypt
ArgoCDGitOpsApplication, 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ชื่อความสามารถตัวอย่าง
1Basic Installติดตั้ง Application ได้อัตโนมัติ สร้าง Resources ที่จำเป็นHelm-based Operator
2Seamless UpgradesUpgrade Application ได้โดยไม่ Downtime ย้อนกลับได้รองรับ Rolling Update
3Full LifecycleBackup/Restore, Scaling, Configuration ManagementCloudNativePG
4Deep InsightsMetrics, Alerts, Log Analysis, Performance TuningECK + Monitoring
5Auto PilotAuto-scaling, Auto-healing, Auto-tuning, Anomaly DetectionFull 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

  1. ใช้ Owner References — ตั้ง Owner Reference บน Resources ที่ Operator สร้าง เพื่อให้ Kubernetes ลบอัตโนมัติเมื่อ CR ถูกลบ (Garbage Collection)
  2. Implement Finalizers — ใช้ Finalizer สำหรับ Cleanup Logic ที่ต้องทำก่อน Resource ถูกลบ เช่น ลบ External Resources, ทำ Backup สุดท้าย
  3. Status Conditions — อัปเดต Status Conditions ให้สะท้อนสถานะจริง ใช้ Standard Condition Types เช่น Ready, Available, Progressing, Degraded
  4. Idempotent Reconciliation — Reconcile Function ต้อง Idempotent เรียกซ้ำกี่ครั้งก็ได้ผลเหมือนกัน ใช้ CreateOrUpdate แทน Create
  5. Rate Limit Reconciliation — อย่า Reconcile ถี่เกินไป ใช้ RequeueAfter แทน Requeue ทันที
  6. Watch Related Resources — Watch ทั้ง Primary Resource (For) และ Secondary Resources (Owns) เพื่อ Reconcile เมื่อ Resources ที่เกี่ยวข้องเปลี่ยน
  7. Handle Edge Cases — จัดการกรณีที่ Resource ถูกลบโดยคนอื่น (ไม่ผ่าน Operator) Namespace ถูกลบ หรือ API Server ไม่ตอบ
  8. RBAC Least Privilege — ให้สิทธิ์ Operator เฉพาะที่จำเป็น ไม่ให้ cluster-admin
  9. Metrics และ Logging — Expose Prometheus Metrics สำหรับ Reconciliation Duration, Error Count, Queue Depth ใช้ Structured Logging
  10. CRD Versioning — วางแผน CRD Versioning ตั้งแต่แรก เริ่มที่ v1alpha1 แล้วค่อย Promote เป็น v1beta1 และ v1

เมื่อไหร่ควรใช้ Operator vs Helm Chart

เกณฑ์Helm ChartOperator
การติดตั้งเพียงพอเพียงพอ
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


Back to Blog | iCafe Forex | SiamLanCard | Siam2R