iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Kubernetes

從零到 CKA:30 天掌握 Kubernetes 核心觀念與實作系列 第 27

Day 27|CRD 與 Operator — 擴展 Kubernetes API,打造自訂資源與控制器

  • 分享至 

  • xImage
  •  

前言

昨天,我們使用 Gateway API 來管理流量入口。

過程中你一定注意到:我們先安裝了 Gateway API 的 CRD(例如 GatewayClass、Gateway、HTTPRoute),接著再安裝 Controller(Envoy Gateway),讓這些自訂資源真正產生作用。

但你有沒有想過:

這些自訂資源是怎麼定義的?Controller 又是怎麼知道該對這些資源做什麼?

今天我們要從「使用者」進一步變成「建立者」—— 學習如何透過 CRD(CustomResourceDefinition) 擴展 Kubernetes API,再搭配 Controller 實作自動化控制邏輯。

可以先把它簡單理解成:

CRD 定義新的資源,Controller 負責持續觀察並讓實際狀態符合期望狀態。

當這套模式被用來封裝某個應用或領域的自動化維運邏輯時,就形成了我們常說的 Operator Pattern

今天內容包含:

  1. 什麼是 CRD?為什麼需要它?
  2. 實作:建立第一個 CRD
  3. Controller 與 Reconciliation Loop
  4. CRD + Controller 與 Operator Pattern
  5. 實作:用 Shell Script 寫一個簡易 Controller
  6. 認識主流 Operator 開發框架
  7. 常見的 Operator 範例
  8. 常見問題與注意事項

以下操作皆在 master 節點執行。


一、什麼是 CRD?為什麼需要它?

Kubernetes 內建資源的局限

Kubernetes 內建了許多資源類型,例如 Pod、Service、Deployment、ConfigMap。

這些資源適合處理通用的基礎設施需求,但當你需要管理特定領域的物件時,內建資源就不一定足夠。

場景 內建資源能直接表達嗎? CRD 解法
管理 MySQL 叢集 ❌ 沒有 MySQL 專屬資源 定義 MySQLCluster CRD
管理 TLS 憑證 ❌ Secret 本身不會自動申請或續約憑證 cert-manager 的 Certificate CRD
管理進階流量路由 ❌ 內建 Ingress API 能力較有限 Gateway API 的 HTTPRoute CRD
管理監控告警規則 ❌ 沒有內建告警規則資源 Prometheus Operator 的 PrometheusRule CRD

CRD 的本質

CRD(CustomResourceDefinition) 可以讓你替 Kubernetes 定義新的資源類型。

https://ithelp.ithome.com.tw/upload/images/20260821/20181928Q7I8tfXSso.png

安裝 CRD 之後:

  • Kubernetes API Server 會開始提供這類 Custom Resource 的 API
  • 可以透過 kubectl 對自訂資源進行 CRUD
  • Custom Resource 會儲存在 etcd 中
  • 但 CRD 本身只負責「定義資源」,不會自動執行實際的業務邏輯

💡 回想上一篇

我們執行 kubectl apply -f standard-install.yaml 安裝 Gateway API CRD 時,就是把 GatewayClass、Gateway、HTTPRoute 等新的資源類型加入 Kubernetes API。

安裝完成後,API Server 才能識別這些資源,例如:

kubectl get gateway

但光有 CRD 還不夠,真正讓 Gateway 產生 Envoy Proxy、配置路由的,是後面的 Controller


二、實作:建立你的第一個 CRD

Step 1:定義 CRD

我們來定義一個 MyApp 資源,用來描述一個簡單的應用:

vim myapp-crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: myapps.example.com        # 格式:<plural>.<group>
spec:
  group: example.com               # API Group
  versions:
    - name: v1                     # API 版本
      served: true                 # 是否提供此版本的 API
      storage: true                # 是否用此版本儲存(只能有一個 true)
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              required: ["image", "replicas"]
              properties:
                image:
                  type: string
                  description: "容器映像"
                replicas:
                  type: integer
                  minimum: 1
                  maximum: 10
                  description: "副本數量"
                port:
                  type: integer
                  default: 80
                  description: "服務埠號"
            status:
              type: object
              properties:
                availableReplicas:
                  type: integer
                phase:
                  type: string
      additionalPrinterColumns:      # kubectl get 時顯示的額外欄位
        - name: Image
          type: string
          jsonPath: .spec.image
        - name: Replicas
          type: integer
          jsonPath: .spec.replicas
        - name: Phase
          type: string
          jsonPath: .status.phase
        - name: Age
          type: date
          jsonPath: .metadata.creationTimestamp
      subresources:
        status: {}                   # 啟用 /status 子資源
  scope: Namespaced                  # Namespaced 或 Cluster
  names:
    plural: myapps                   # 複數名稱(用於 API 路徑)
    singular: myapp                  # 單數名稱
    kind: MyApp                      # 資源類型名稱
    shortNames:                      # 縮寫(kubectl get ma)
      - ma
kubectl apply -f myapp-crd.yaml

Step 2:確認 CRD 已建立

kubectl get crd myapps.example.com

https://ithelp.ithome.com.tw/upload/images/20260821/20181928g3npYsPEZB.png

現在 Kubernetes 已經認識 MyApp 這個資源類型了!你可以用 kubectl api-resources | grep myapp 確認。

Step 3:建立自訂資源(CR)

vim my-first-app.yaml
apiVersion: example.com/v1
kind: MyApp
metadata:
  name: demo-app
  namespace: default
spec:
  image: nginx:latest
  replicas: 3
  port: 80
kubectl apply -f my-first-app.yaml
# 用完整名稱或縮寫都可以
kubectl get myapps
kubectl get ma

會看到類似這樣的輸出:

https://ithelp.ithome.com.tw/upload/images/20260821/20181928VShgVg1PJP.png

💡 為什麼 Phase 是空的?

因為目前還沒有 Controller 去更新這個 Custom Resource 的 status

CRD 只負責定義資源的結構與 API,資源建立後會被儲存在 etcd 中,但不會因為 CRD 本身就自動執行任何業務邏輯。

可以把它想成:我們已經在 Kubernetes 裡建立了一張表單,但目前還沒有任何 Controller 去讀取這張表單並執行後續動作。

Step 4:驗證 Schema Validation

CRD 的 openAPIV3Schema 會自動驗證資源的格式:

# 試試看:replicas 超出範圍
kubectl apply -f - <<EOF
apiVersion: example.com/v1
kind: MyApp
metadata:
  name: bad-app
spec:
  image: nginx:latest
  replicas: 99
EOF

https://ithelp.ithome.com.tw/upload/images/20260821/20181928uE0K95v718.png

Kubernetes 會拒絕這個請求,因為 replicas 的最大值是 10。

# 試試看:缺少必要欄位
kubectl apply -f - <<EOF
apiVersion: example.com/v1
kind: MyApp
metadata:
  name: bad-app
spec:
  image: nginx:latest
EOF

https://ithelp.ithome.com.tw/upload/images/20260821/20181928lps9d9dzmp.png

也會被拒絕,因為 replicas 是 required 欄位。

💡 CRD 的 Schema Validation

CRD 可以透過 OpenAPI Schema 驗證 Custom Resource 的欄位型別與格式,例如必填欄位、字串、數字、Enum 或數值範圍等。

可以把它理解成 Kubernetes API 的「輸入驗證」,避免使用者送出不符合規格的資源,尤其適合多人協作的環境。


三、Controller 與 Reconciliation Loop

光有 CRD 還不夠

CRD 讓你可以在 Kubernetes 中建立自訂資源,但資源本身不會主動執行任何動作。

要讓這些資源真正產生效果,還需要 Controller 持續觀察資源狀態並執行對應的控制邏輯。

Controller 大致會做以下幾件事:

  1. 監聽(Watch) 資源的建立、更新或刪除
  2. 比較期望狀態(Desired State)與目前觀察到的狀態(Observed State)
  3. 執行動作(Act),讓實際狀態逐步接近期望狀態
  4. 必要時更新資源的 Status

這個持續重複的過程,就是 Kubernetes 很重要的設計模式 —— Reconciliation Loop(調諧迴圈)

可以簡單理解成:

https://ithelp.ithome.com.tw/upload/images/20260821/20181928392wOdAS5C.png

內建 Controller 也是這樣運作

其實我們前面已經使用過很多 Controller:

Controller 主要監聽的資源 做的事情
Deployment Controller Deployment 建立或更新 ReplicaSet,讓 Deployment 朝期望版本與副本配置前進
ReplicaSet Controller ReplicaSet 建立或刪除 Pod,維持指定的副本數
Service Controller type: LoadBalancer 的 Service 在支援的雲端環境中協調建立或更新外部 Load Balancer
Envoy Gateway GatewayClass / Gateway / HTTPRoute 等 根據 Gateway API 資源產生並更新 Envoy Proxy 與相關路由設定

💡 Kubernetes 的設計哲學

Kubernetes 很核心的一個設計方式就是宣告式管理

使用者描述「我希望系統最後變成什麼樣子」,Controller 則透過持續的 Reconciliation,讓實際狀態逐步接近期望狀態。

CRD 加上對應的 Controller,讓我們也能把這套模式延伸到自己的應用與領域。


四、CRD + Controller 與 Operator Pattern

Operator 模式

可以先把 Operator 簡單理解成:

Custom Resource + Controller + 領域維運邏輯

https://ithelp.ithome.com.tw/upload/images/20260821/20181928BtGIuGMB2h.png

Operator 會把原本需要人工執行的維運流程,例如部署、升級、備份、故障處理等,寫進 Controller 的 Reconciliation Loop 中。

因此使用者只需要描述「我希望系統變成什麼樣子」,Operator 就會持續調整實際狀態。

一個具體的例子

假設你要管理一個 MySQL 叢集。

沒有 Operator 時,可能需要手動處理:

  1. 建立 StatefulSet、Service、ConfigMap
  2. 設定資料庫叢集或複寫
  3. 處理備份與還原
  4. 執行升級或故障處理
  5. 調整副本數與其他設定

有了 MySQL Operator 後,可以透過一個 Custom Resource 描述期望狀態:

apiVersion: database.example.com/v1
kind: MySQLCluster
metadata:
  name: my-db
spec:
  replicas: 3
  version: "8.0"
  backup:
    schedule: "0 2 * * *"

💡 這裡是概念範例

不同 MySQL Operator 會定義不同的 CRD、apiVersion 與欄位格式,實際使用時需要以該 Operator 的文件為準。

Operator 看到這個 Custom Resource 後,就會透過 Controller 執行對應的自動化流程,建立或管理底層的 Kubernetes 資源與應用狀態。


五、實作:用 Shell Script 寫一個簡易 Controller

為了理解 Controller 的運作原理,這裡先不用 Go 或 Operator SDK,而是用最簡單的 Shell Script 模擬一個 Controller。

我們要讓前面建立的 MyApp Custom Resource 真正「產生作用」。

⚠️ 這只是教學用 Controller

正式環境中的 Controller 通常會使用 Kubernetes Client Library、controller-runtime、Kubebuilder 或 Operator SDK 開發。

這裡使用 Shell Script,主要是為了看清楚「讀取資源 → 比較狀態 → 執行動作」的基本流程。

這個 Controller 會做什麼?

當使用者建立 MyApp 資源後,Controller 會讀取它的 spec,並建立或更新對應的:

  • Deployment
  • Service

也就是把:

MyApp Custom Resource
        ↓
    Controller
        ↓
Deployment + Service

轉換成真正可以運行的 Kubernetes 資源。

Step 1:撰寫 Controller 腳本

vim myapp-controller.sh
#!/bin/bash
# MyApp Simple Controller
# 監聽 MyApp 資源,自動建立對應的 Deployment 和 Service

echo "MyApp Controller 啟動中..."

# ====== Reconcile 函式:處理單一 MyApp 資源 ======
reconcile() {
  local NAME=$1 NAMESPACE=$2

  # 即時取得最新的 spec
  IMAGE=$(kubectl get myapp "$NAME" -n "$NAMESPACE" -o jsonpath='{.spec.image}')
  REPLICAS=$(kubectl get myapp "$NAME" -n "$NAMESPACE" -o jsonpath='{.spec.replicas}')
  PORT=$(kubectl get myapp "$NAME" -n "$NAMESPACE" -o jsonpath='{.spec.port}')
  PORT=${PORT:-80}
  UID_VAL=$(kubectl get myapp "$NAME" -n "$NAMESPACE" -o jsonpath='{.metadata.uid}')

  [ -z "$IMAGE" ] && return

  echo "Reconciling MyApp:$NAMESPACE/$NAME (image=$IMAGE, replicas=$REPLICAS)"

  # 建立或更新 Deployment
  kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-${NAME}
  namespace: ${NAMESPACE}
  labels:
    app: myapp-${NAME}
    managed-by: myapp-controller
  ownerReferences:
    - apiVersion: example.com/v1
      kind: MyApp
      name: ${NAME}
      uid: ${UID_VAL}
spec:
  replicas: ${REPLICAS}
  selector:
    matchLabels:
      app: myapp-${NAME}
  template:
    metadata:
      labels:
        app: myapp-${NAME}
    spec:
      containers:
        - name: app
          image: ${IMAGE}
          ports:
            - containerPort: ${PORT}
EOF

  # 建立或更新 Service
  kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: myapp-${NAME}
  namespace: ${NAMESPACE}
  labels:
    managed-by: myapp-controller
spec:
  selector:
    app: myapp-${NAME}
  ports:
    - port: ${PORT}
      targetPort: ${PORT}
EOF

  # 更新 MyApp 的 status
  sleep 2
  AVAILABLE=$(kubectl get deployment "myapp-${NAME}" -n "$NAMESPACE" -o jsonpath='{.status.availableReplicas}' 2>/dev/null)
  kubectl patch myapp "$NAME" -n "$NAMESPACE" --type merge --subresource=status \
    -p "{\"status\":{\"availableReplicas\":${AVAILABLE:-0},\"phase\":\"Running\"}}" 2>/dev/null

  echo "已同步 MyApp:$NAMESPACE/$NAME"
}

# ====== 第一階段:先 reconcile 所有現有的 MyApp ======
echo "🔍 掃描現有 MyApp 資源..."
kubectl get myapps --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}' | while IFS='/' read -r ns name; do
  [ -z "$name" ] && continue
  reconcile "$name" "$ns"
done

# ====== 第二階段:定期輪詢,偵測變化 ======
echo "👀 開始監聽 MyApp 變化(每 5 秒輪詢)..."
while true; do
  kubectl get myapps --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}/{.spec.image}/{.spec.replicas}{"\n"}{end}' | while IFS='/' read -r ns name image replicas; do
    [ -z "$name" ] && continue
    # 檢查 Deployment 是否存在且 replicas / image 一致
    CURRENT_REPLICAS=$(kubectl get deployment "myapp-${name}" -n "$ns" -o jsonpath='{.spec.replicas}' 2>/dev/null)
    CURRENT_IMAGE=$(kubectl get deployment "myapp-${name}" -n "$ns" -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null)
    if [ "$CURRENT_REPLICAS" != "$replicas" ] || [ "$CURRENT_IMAGE" != "$image" ]; then
      reconcile "$name" "$ns"
    fi
  done
  sleep 5
done
chmod +x myapp-controller.sh

Step 2:啟動 Controller

# 在背景執行 Controller
./myapp-controller.sh &

https://ithelp.ithome.com.tw/upload/images/20260821/20181928sNhR7QZ0gt.png

啟動後,Controller 會自動掃描所有已存在的 MyApp 資源並進行 reconcile,然後進入 watch 模式監聽後續變化。

💡 為什麼分兩階段?

我們把這個簡易 Controller 的邏輯拆成兩部分:

  1. 初始掃描:用 -o jsonpath 取得目前已存在的 MyApp 資源,逐一執行 reconcile,避免 Controller 啟動前建立的資源被漏掉
  2. 持續輪詢:每 5 秒重新檢查 MyApp 的期望狀態(spec)與 Deployment 的目前狀態,有差異時再執行 reconcile

這種「觀察狀態 → 比較差異 → 執行動作」的流程,就是 Reconciliation Loop 的核心概念。

正式的 Kubernetes Controller 通常會透過 Watch / Cache 等機制接收資源變化事件,而不是固定每幾秒輪詢。這裡使用輪詢,是為了讓 Shell Script 範例更容易理解與實作。

Step 3:建立 MyApp 資源,觀察 Controller 的反應

如果在啟動 Controller 之前就已經建立了 MyApp 資源,Controller 啟動時會自動處理。否則可以現在建立:

kubectl apply -f my-first-app.yaml
# 查看 Controller 是否建立了 Deployment 和 Service
kubectl get deployment myapp-demo-app
kubectl get svc myapp-demo-app
kubectl get pods -l app=myapp-demo-app

應該能看到 Controller 自動建立了 3 個 Pod 的 Deployment 和對應的 Service!

# 查看 MyApp 的 status
kubectl get myapps

現在 Phase 欄位應該顯示 Running 了。

Step 4:測試更新

# 修改副本數
kubectl patch myapp demo-app --type merge -p '{"spec":{"replicas":2}}'

# 觀察 Deployment 的副本數是否跟著變
kubectl get deployment myapp-demo-app

https://ithelp.ithome.com.tw/upload/images/20260821/20181928lcm1b4gXma.png
https://ithelp.ithome.com.tw/upload/images/20260821/201819280EGsmoIYFx.png

Step 5:測試刪除(ownerReferences 的威力)

# 刪除 MyApp 資源
kubectl delete myapp demo-app

# 如果 Deployment 和 Service 都正確設定 ownerReferences,
# Kubernetes Garbage Collector 會自動清理這些相依資源
kubectl get deployment myapp-demo-app
kubectl get svc myapp-demo-app
# 停止背景執行的 Controller
kill %1 2>/dev/null

💡 為什麼會自動刪除?

因為 Deployment 和 Service 的 ownerReferences 指向 MyApp

MyApp 被刪除後,Kubernetes 的 Garbage Collector 會依照 Owner / Dependent 關係,自動清理這些相依資源。

⚠️ 這只是教學範例

正式的 Kubernetes Controller 通常不會使用 Shell Script 開發。

這個範例的重點,是理解 Controller 的核心流程:

Watch → Compare → Act → Update Status

實際開發通常會使用 Go 搭配 controller-runtime、Kubebuilder 或 Operator SDK 等工具。


六、認識主流 Operator 開發工具

實際開發 Operator 時,通常不會從零開始處理 Watch、Work Queue、Leader Election、RBAC 等底層細節,而是使用專門的開發框架或工具來建立專案骨架。

工具 語言 / 方式 特色 適合場景
Kubebuilder Go 基於 controller-runtime,提供 CRD、Controller、Webhook、RBAC 等 scaffolding 使用 Go 開發正式 Controller / Operator
Operator SDK Go / Ansible / Helm 同樣基於 controller-runtime,並整合 Operator Framework 與 OLM 相關工具 想使用 Go、Ansible 或 Helm 開發 Operator
Metacontroller 任意語言 透過 Webhook 撰寫 Reconcile 邏輯,不必直接處理 Kubernetes Client 快速原型、非 Go 團隊或較簡單的自訂控制邏輯

Kubebuilder 的典型工作流程

https://ithelp.ithome.com.tw/upload/images/20260821/2018192836vuunpkyw.png

其中:

  • make generate:產生 DeepCopy 等 Go 程式碼
  • make manifests:產生 CRD、RBAC 等 YAML
  • make docker-build:建立 Controller Image
  • make docker-push:推送 Image 到 Registry
  • make deploy:將 Controller 部署到 Kubernetes

💡 Kubebuilder vs Operator SDK

兩者在 Go Operator 的開發方式非常接近,核心都建立在 controller-runtime 之上。

Operator SDK 額外提供 Ansible / Helm Operator,以及 Operator Framework、OLM 等相關整合。

如果主要使用 Go 開發 Controller,Kubebuilder 與 Operator SDK 都是常見選擇。


七、常見的 Operator 範例

實務上,很多常見的 Kubernetes 工具其實都採用了 Operator Pattern。

Operator 管理的資源 做了什麼
cert-manager Certificate、Issuer 自動申請與續約 TLS 憑證
Prometheus Operator Prometheus、ServiceMonitor 等 管理 Prometheus 監控元件與相關設定
Strimzi Kafka、KafkaTopic 等 管理 Kafka 叢集的部署、擴縮與升級
MySQL Operator MySQL 相關 Custom Resource 管理 MySQL 叢集、備份與故障處理

💡 去哪找 Operator?

OperatorHub.io 收錄了許多社群與廠商提供的 Operator,可以依照資料庫、監控、網路、安全等類型搜尋。


八、常見問題與注意事項

問題 原因與解法
CRD 建了但 kubectl get 報錯 確認 CRD 是否已成功建立,以及 metadata.name 是否符合 <plural>.<group> 格式,例如 myapps.example.com
CR 建立失敗,出現 Schema Validation 錯誤 檢查 Custom Resource YAML 是否符合 CRD 的 openAPIV3Schema,也可以用 kubectl explain 查看欄位定義
刪除 CRD 會怎樣? 刪除 CRD 也會刪除該類型的 Custom Resource,屬於破壞性操作,操作前要特別小心
Controller 掛了會怎樣? Custom Resource 仍會保存在 Kubernetes 中,但暫時不會有新的 Reconcile 動作;Controller 恢復後會再次處理資源
ownerReferences 是什麼? 用來建立 Owner / Dependent 關係,Owner 被刪除後,Dependent 資源可由 Kubernetes Garbage Collector 自動清理

小結

今天我們從 CRD 開始,一路理解到 Controller 與 Operator Pattern,知道 Kubernetes 是如何透過自訂資源與 Reconciliation Loop,把原本需要人工執行的維運流程自動化。

重點 說明
CRD 為 Kubernetes 定義新的資源類型,擴展 Kubernetes API
Custom Resource 根據 CRD 建立的資源實例,可以透過 Kubernetes API 與 kubectl 管理
Controller 持續觀察資源狀態,透過 Reconciliation 讓實際狀態接近期望狀態
Operator Pattern 將 Custom Resource、Controller 與領域維運邏輯結合,實現自動化管理
Schema Validation 驗證 Custom Resource 的欄位型別、必填欄位與其他格式規則
ownerReferences 建立資源間的 Owner / Dependent 關係,搭配 Garbage Collection 自動清理相依資源

下一篇我們會直接看一個很實際的例子 —— cert-manager

它會透過 CertificateIssuer 等 Custom Resource 搭配 Controller,自動處理 TLS 憑證的申請、更新與續約。

也就是把今天學到的 CRD + Controller + Reconciliation Loop,真正套用到 Kubernetes 的 HTTPS 憑證管理上!


參考資源


上一篇
Day 26|Gateway API — Kubernetes 的下一代流量入口管理
下一篇
Day 28|cert-manager — 自動申請、管理與續約 Kubernetes TLS 憑證
系列文
從零到 CKA:30 天掌握 Kubernetes 核心觀念與實作28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言