昨天,我們使用 Gateway API 來管理流量入口。
過程中你一定注意到:我們先安裝了 Gateway API 的 CRD(例如 GatewayClass、Gateway、HTTPRoute),接著再安裝 Controller(Envoy Gateway),讓這些自訂資源真正產生作用。
但你有沒有想過:
這些自訂資源是怎麼定義的?Controller 又是怎麼知道該對這些資源做什麼?
今天我們要從「使用者」進一步變成「建立者」—— 學習如何透過 CRD(CustomResourceDefinition) 擴展 Kubernetes API,再搭配 Controller 實作自動化控制邏輯。
可以先把它簡單理解成:
CRD 定義新的資源,Controller 負責持續觀察並讓實際狀態符合期望狀態。
當這套模式被用來封裝某個應用或領域的自動化維運邏輯時,就形成了我們常說的 Operator Pattern。
今天內容包含:
以下操作皆在 master 節點執行。
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(CustomResourceDefinition) 可以讓你替 Kubernetes 定義新的資源類型。

安裝 CRD 之後:
kubectl 對自訂資源進行 CRUD💡 回想上一篇
我們執行
kubectl apply -f standard-install.yaml安裝 Gateway API CRD 時,就是把 GatewayClass、Gateway、HTTPRoute 等新的資源類型加入 Kubernetes API。安裝完成後,API Server 才能識別這些資源,例如:
kubectl get gateway但光有 CRD 還不夠,真正讓 Gateway 產生 Envoy Proxy、配置路由的,是後面的 Controller。
我們來定義一個 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
kubectl get crd myapps.example.com

現在 Kubernetes 已經認識 MyApp 這個資源類型了!你可以用 kubectl api-resources | grep myapp 確認。
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
會看到類似這樣的輸出:

💡 為什麼 Phase 是空的?
因為目前還沒有 Controller 去更新這個 Custom Resource 的
status。CRD 只負責定義資源的結構與 API,資源建立後會被儲存在 etcd 中,但不會因為 CRD 本身就自動執行任何業務邏輯。
可以把它想成:我們已經在 Kubernetes 裡建立了一張表單,但目前還沒有任何 Controller 去讀取這張表單並執行後續動作。
CRD 的 openAPIV3Schema 會自動驗證資源的格式:
# 試試看:replicas 超出範圍
kubectl apply -f - <<EOF
apiVersion: example.com/v1
kind: MyApp
metadata:
name: bad-app
spec:
image: nginx:latest
replicas: 99
EOF

Kubernetes 會拒絕這個請求,因為 replicas 的最大值是 10。
# 試試看:缺少必要欄位
kubectl apply -f - <<EOF
apiVersion: example.com/v1
kind: MyApp
metadata:
name: bad-app
spec:
image: nginx:latest
EOF

也會被拒絕,因為 replicas 是 required 欄位。
💡 CRD 的 Schema Validation
CRD 可以透過 OpenAPI Schema 驗證 Custom Resource 的欄位型別與格式,例如必填欄位、字串、數字、Enum 或數值範圍等。
可以把它理解成 Kubernetes API 的「輸入驗證」,避免使用者送出不符合規格的資源,尤其適合多人協作的環境。
CRD 讓你可以在 Kubernetes 中建立自訂資源,但資源本身不會主動執行任何動作。
要讓這些資源真正產生效果,還需要 Controller 持續觀察資源狀態並執行對應的控制邏輯。
Controller 大致會做以下幾件事:
這個持續重複的過程,就是 Kubernetes 很重要的設計模式 —— Reconciliation Loop(調諧迴圈)。
可以簡單理解成:

其實我們前面已經使用過很多 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,讓我們也能把這套模式延伸到自己的應用與領域。
可以先把 Operator 簡單理解成:
Custom Resource + Controller + 領域維運邏輯

Operator 會把原本需要人工執行的維運流程,例如部署、升級、備份、故障處理等,寫進 Controller 的 Reconciliation Loop 中。
因此使用者只需要描述「我希望系統變成什麼樣子」,Operator 就會持續調整實際狀態。
假設你要管理一個 MySQL 叢集。
沒有 Operator 時,可能需要手動處理:
有了 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 資源與應用狀態。
為了理解 Controller 的運作原理,這裡先不用 Go 或 Operator SDK,而是用最簡單的 Shell Script 模擬一個 Controller。
我們要讓前面建立的 MyApp Custom Resource 真正「產生作用」。
⚠️ 這只是教學用 Controller
正式環境中的 Controller 通常會使用 Kubernetes Client Library、controller-runtime、Kubebuilder 或 Operator SDK 開發。
這裡使用 Shell Script,主要是為了看清楚「讀取資源 → 比較狀態 → 執行動作」的基本流程。
當使用者建立 MyApp 資源後,Controller 會讀取它的 spec,並建立或更新對應的:
也就是把:
MyApp Custom Resource
↓
Controller
↓
Deployment + Service
轉換成真正可以運行的 Kubernetes 資源。
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
# 在背景執行 Controller
./myapp-controller.sh &

啟動後,Controller 會自動掃描所有已存在的 MyApp 資源並進行 reconcile,然後進入 watch 模式監聽後續變化。
💡 為什麼分兩階段?
我們把這個簡易 Controller 的邏輯拆成兩部分:
- 初始掃描:用
-o jsonpath取得目前已存在的MyApp資源,逐一執行 reconcile,避免 Controller 啟動前建立的資源被漏掉- 持續輪詢:每 5 秒重新檢查
MyApp的期望狀態(spec)與 Deployment 的目前狀態,有差異時再執行 reconcile這種「觀察狀態 → 比較差異 → 執行動作」的流程,就是 Reconciliation Loop 的核心概念。
正式的 Kubernetes Controller 通常會透過 Watch / Cache 等機制接收資源變化事件,而不是固定每幾秒輪詢。這裡使用輪詢,是為了讓 Shell Script 範例更容易理解與實作。
如果在啟動 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 了。
# 修改副本數
kubectl patch myapp demo-app --type merge -p '{"spec":{"replicas":2}}'
# 觀察 Deployment 的副本數是否跟著變
kubectl get deployment myapp-demo-app


# 刪除 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 時,通常不會從零開始處理 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 團隊或較簡單的自訂控制邏輯 |

其中:
make generate:產生 DeepCopy 等 Go 程式碼make manifests:產生 CRD、RBAC 等 YAMLmake docker-build:建立 Controller Imagemake docker-push:推送 Image 到 Registrymake deploy:將 Controller 部署到 Kubernetes💡 Kubebuilder vs Operator SDK
兩者在 Go Operator 的開發方式非常接近,核心都建立在
controller-runtime之上。Operator SDK 額外提供 Ansible / Helm Operator,以及 Operator Framework、OLM 等相關整合。
如果主要使用 Go 開發 Controller,Kubebuilder 與 Operator SDK 都是常見選擇。
實務上,很多常見的 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。
它會透過 Certificate、Issuer 等 Custom Resource 搭配 Controller,自動處理 TLS 憑證的申請、更新與續約。
也就是把今天學到的 CRD + Controller + Reconciliation Loop,真正套用到 Kubernetes 的 HTTPS 憑證管理上!