[教學][book] CRD + CR + Operator
===
###### tags: `K8s`
###### tags: `Kubernetes`, `k8s`, `CRD`, `CR`, `Operator`, `教學範例`, `Kubebuilder`
<br>
[TOC]
<br>
## 快速查詢指令
```bash=
# apply CRD: book
$ kubectl apply -f crd-book.yaml # no checksum
$ kubectl apply -f crd-book-checksum.yaml # with checksum
# apply CR: book
$ kubectl apply -f cr-book.yaml
# 修改前 vs 修改後
$ kubectl get book/great-book -o yaml
$ kubectl patch book/great-book --type merge -p '{"status": {"checksum":"123"}}' --subresource=status
$ kubectl get book/great-book -o yaml
# 使用 vi 編輯
$ kubectl edit book/great-book
# 常見 type 錯誤訊息
$ kubectl patch book/great-book --type merge -p '{"spec": {"edition":"2"}}'
The Book "great-book" is invalid:
spec.edition: Invalid value: "string":
spec.edition in body must be of type integer: "string"
$ kubectl patch book/great-book --type merge -p '{"spec": {"edition":2}}'
```
<br>
<hr>
<br>
## 討論
### 是否能用 book (書名、作者、出版日期、第幾刷) 為例子,實作 CR / CRD / Operator(如果有再版,則發出通知)?(cht)
- ### gpt-o3-high (2025/02/04)
> Book Operator Example
本文件提供一個以 **Book**(書名、作者、出版日期、及第幾刷)為例,展示如何實作 CR / CRD / Operator 的完整範例,並在再版時發出通知。
---
## 1. 定義 Book 的 CRD
首先,需要定義一個 CRD 來描述 Book 的結構。這裡的範例 CRD 定義了四個欄位:`title`、`author`、`publicationDate`(以 ISO 日期格式表示)與 `edition`(整數型態),並將此資源定義在 example.com API 群組下。
```yaml=
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: books.example.com
spec:
group: example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
title:
type: string
author:
type: string
publicationDate:
type: string
format: date
edition:
type: integer
required:
- title
- author
- publicationDate
- edition
# 可選:如果想讓 Operator 更新狀態,可以啟用 status 子資源
subresources:
status: {}
scope: Namespaced
names:
plural: books
singular: book
kind: Book
shortNames:
- bk
```
使用以上 YAML 建立 CRD:
```bash
kubectl apply -f book-crd.yaml
```
---
## 2. 建立 Book 的自訂資源(CR)
在 CRD 建立之後,可以創建一個或多個 Book 物件。下面是一個初始版本的 Book 資源實例:
```yaml=
apiVersion: example.com/v1
kind: Book
metadata:
name: great-book
spec:
title: "The Great Book"
author: "John Doe"
publicationDate: "2021-01-01"
edition: 1
```
將此檔案存成 `book-sample.yaml` 並套用:
```bash
kubectl apply -f book-sample.yaml
```
---
## 3. 編寫 Operator
Operator 的工作是監控 Book 資源的變更,並在檢測到再版(例如 edition 從 1 變成 2 或更高)時,發出通知(可以是更新狀態、發送事件、調用外部 API 等)。
以下提供一個簡單的 Operator 邏輯示例(以 Go 語言和 Kubebuilder 為例):
### 3.1 Operator Reconcile 範例邏輯(Go 語言)
假設我們在 Book 的 status 中新增兩個欄位:
- `lastNotifiedEdition`:記錄上次通知時的 edition 值
- `notificationMessage`:通知訊息(例如可供查詢或日誌記錄)
下面是簡化的 Reconcile 函數範例:
```go=
package controllers
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/log"
examplev1 "your_module_path/api/v1"
)
type BookReconciler struct {
client.Client
}
// Reconcile 會被持續呼叫以檢查 Book 資源的狀態
func (r *BookReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
var book examplev1.Book
if err := r.Get(ctx, req.NamespacedName, &book); err != nil {
if errors.IsNotFound(err) {
// 資源已被刪除
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// 從 status 中取得上次通知的 edition 值
lastNotifiedEdition := book.Status.LastNotifiedEdition
// 若目前的 edition 大於上次通知的 edition,則代表有再版情況
if book.Spec.Edition > lastNotifiedEdition {
// 構造通知訊息
notification := fmt.Sprintf("Book '%s' has a new edition: %d (previous: %d)",
book.Spec.Title, book.Spec.Edition, lastNotifiedEdition)
logger.Info(notification)
// 模擬發送通知:例如可以整合發送 Email、Slack 訊息或其他處理邏輯
// 更新 status 記錄
book.Status.LastNotifiedEdition = book.Spec.Edition
book.Status.NotificationMessage = notification
if err := r.Status().Update(ctx, &book); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// SetupWithManager 設定 Controller 與 Manager
func (r *BookReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&examplev1.Book{}).
Complete(r)
}
```
### 3.2 說明
- **監控變更:**
Operator 持續監控 Book CR 的變更,當 `spec.edition` 大於 `status.lastNotifiedEdition` 時,代表有再版情況,則觸發通知邏輯。
- **通知機制:**
範例中僅記錄日誌並更新 Book 的狀態。實際應用中,可以進一步整合發送 Email、Webhook、Slack 等通知服務。
- **狀態更新:**
更新 `status.lastNotifiedEdition` 能夠避免重複通知,確保每次通知僅在真正再版時觸發。
---
## 4. 小結
1. **CRD 與 CR 建立:**
- 使用 YAML 定義 Book 的 CRD,包含必要的欄位與驗證規則。
- 建立對應的 Book 自訂資源實例。
2. **Operator 編寫:**
- 實作一個 Operator 持續監控 Book 資源的變更。
- 當檢測到 `edition` 升級時(即再版),執行通知邏輯,並更新資源狀態以避免重複通知。
此範例展示了如何利用 Kubernetes 的擴展機制結合 CRD 與 Operator 實作一個簡單的業務邏輯,根據需求可進一步擴展通知與狀態管理功能。
<br>
### 是否能用 book (書名、作者、出版日期、第幾刷) 為例子,實作 CR / CRD / Operator(如果有再版,則發出通知)?(en)
- ### gpt-o3-high (2025/02/04)
> Book Operator Example: CR/CRD/Operator with Notification on New Edition
This markdown document provides an example of implementing a custom resource (CR), a custom resource definition (CRD), and an operator for managing "Book" objects in Kubernetes. The operator monitors changes to the "Book" resource, and if a new edition (reprint) is detected (i.e., if the edition number increases), a notification is issued.
## 1. Define the Book CRD
First, define a CRD to describe the structure of a Book. The CRD defines the fields: `title`, `author`, `publicationDate` (as an ISO date string), and `edition` (an integer). This CRD is defined in the `example.com` API group.
```yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: books.example.com
spec:
group: example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
title:
type: string
author:
type: string
publicationDate:
type: string
format: date
edition:
type: integer
required:
- title
- author
- publicationDate
- edition
subresources:
status: {}
scope: Namespaced
names:
plural: books
singular: book
kind: Book
shortNames:
- bk
```
Apply the CRD with:
```bash
kubectl apply -f book-crd.yaml
```
## 2. Create a Book Custom Resource (CR)
After the CRD is established, create an instance of the Book resource. Below is an example of a Book resource in its initial version:
```yaml
apiVersion: example.com/v1
kind: Book
metadata:
name: great-book
spec:
title: "The Great Book"
author: "John Doe"
publicationDate: "2021-01-01"
edition: 1
```
Save this file as `book-sample.yaml` and apply it with:
```bash
kubectl apply -f book-sample.yaml
```
## 3. Implement the Operator
The operator monitors changes to the Book resource and issues a notification when a new edition (i.e., an increase in the `edition` field) is detected.
### 3.1 Example Operator Reconcile Logic (using Go and Kubebuilder)
Below is a simplified example of a Reconcile function for the operator. The operator checks if `spec.edition` is greater than `status.lastNotifiedEdition` and, if so, logs a notification and updates the status.
Assume that the Book status includes the following fields:
- `lastNotifiedEdition`: the edition number when the last notification was sent.
- `notificationMessage`: the notification message.
```go
package controllers
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/log"
examplev1 "your_module_path/api/v1"
)
type BookReconciler struct {
client.Client
}
func (r *BookReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
var book examplev1.Book
if err := r.Get(ctx, req.NamespacedName, &book); err != nil {
if errors.IsNotFound(err) {
// Resource not found, could have been deleted after reconcile request.
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// Get the last notified edition from the status
lastNotifiedEdition := book.Status.LastNotifiedEdition
// If the current edition is greater than the last notified edition, a new edition is detected
if book.Spec.Edition > lastNotifiedEdition {
notification := fmt.Sprintf("Book '%s' has a new edition: %d (previous: %d)",
book.Spec.Title, book.Spec.Edition, lastNotifiedEdition)
logger.Info(notification)
// Simulate sending a notification (e.g., email, Slack, webhook, etc.)
// Update the status with the new edition and notification message
book.Status.LastNotifiedEdition = book.Spec.Edition
book.Status.NotificationMessage = notification
if err := r.Status().Update(ctx, &book); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
func (r *BookReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&examplev1.Book{}).
Complete(r)
}
```
### 3.2 Explanation
- **Monitoring Changes:**
The operator continuously watches for changes in the Book custom resource. When it detects that `spec.edition` is greater than `status.lastNotifiedEdition`, it triggers the notification process.
- **Notification Mechanism:**
In this example, the operator simply logs the notification and updates the status of the Book resource. In a production scenario, you might integrate email, Slack, or another notification service.
- **Status Update:**
The status update ensures that subsequent reconcile loops do not re-send notifications for the same edition.
## 4. Summary
1. **Define CRD and CR:**
- Create a CRD to define the Book structure (title, author, publication date, and edition).
- Create a Book custom resource instance.
2. **Implement the Operator:**
- Write an operator that monitors the Book resource.
- Detect if a new edition is published by comparing `spec.edition` with `status.lastNotifiedEdition`.
- Trigger a notification process (e.g., log, external call) and update the resource status accordingly.
This example demonstrates how to extend Kubernetes using CRDs and an Operator to manage business logic—in this case, monitoring a Book resource for new editions and issuing notifications accordingly.
<br>
### [補充] 關於在 CRD 中沒有定義 `subresources:`**,會有什麼問題發生?
- 對使用 `kuberctl patch ... --subresource=status` ,仍然可以正常更新
- 若是使用 Python/kubernetes 套件,則會有下面的錯誤訊息:
- **code**
`cust_api.patch_namespaced_custom_object_status(group='example.com', version='v1', namespace='default', plural='books', name='great-book', body={'status': {'checksum': 'abc123'}})`
- **error**
`kubernetes.client.exceptions.ApiException: (404)`
`Reason: Not Found`
`HTTP response headers: HTTPHeaderDict({'Audit-Id': '2d9e119e-9b64-4470-99fb-afbb93f227ac', 'Cache-Control': 'no-cache, private', 'Content-Type': 'application/json', 'X-Kubernetes-Pf-Flowschema-Uid': '959071ea-09b5-42fd-941c-5418a73fa170', 'X-Kubernetes-Pf-Prioritylevel-Uid': '5dd10805-93fc-4442-93c5-f374456c66d3', 'Date': 'Wed, 05 Feb 2025 09:57:48 GMT', 'Content-Length': '224'})`
`HTTP response body: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"books.example.com \"great-book\" not found","reason":"NotFound","details":{"name":"great-book","group":"example.com","kind":"books"},"code":404}`
- 404 NotFound 錯誤通常代表:
- 在 default 命名空間中,雖然物件 great-book 存在,但 API Server 上並沒有對應的 /status 子資源 endpoint。
- 這可能是因為 CRD 定義中沒有正確啟用 status 子資源,或是物件是在啟用 status 子資源之前建立的。
- **小結**:
- 在某些 CRD 中,`/status` 子資源僅在 CRD 的版本部分包含 "subresources: {status: {}}" 時可用。
- 定義 `subresources:` 是指:在 API Server 上,建立對應的 `/status` 子資源 endpoint
- CLI 指令有效,但使用 Python/kubernetes 套件會失敗
- **補充**:
- **為什麼不能直接帶入 checksum 資訊?**
- **分離更新的機制**:
當 CRD 啟用了 `subresources: { status: {} }` 時,status 部分只能透過針對 `/status` 子資源的 API 進行更新。也就是說,初次建立時,任何在 YAML 檔中設定的 status 資訊都會被忽略。
- **預期流程**:
Kubernetes 的設計原則是讓 Controller 或 Operator 根據實際狀態去更新 status。使用者在建立資源時只需提供期望的狀態(即 spec 部分),而 status 則由系統動態填入、更新。
<br>
### Operator 要如何執行?
- ### gpt-o3-high (2025/02/04)
> Operator 執行指南
## 1. 本地開發與測試階段
在 Operator 開發初期,通常會在本地機器上運行 Operator 進行調試。常見的方法包括:
### 1.1 使用 `make run`
如果你使用 Kubebuilder 或 Operator SDK 等工具生成 Operator 專案,通常專案根目錄下會有 Makefile,可以透過以下命令啟動 Operator:
```bash
make run
```
這會在你的本機上啟動 Operator 程式(通常透過你本機的 kubeconfig 連線到集群),便於觀察日誌、進行除錯及測試 Reconcile 邏輯。
### 1.2 直接執行 Operator Binary
編譯 Operator 程式後,也可以直接在命令列執行:
```bash
./operator-binary --kubeconfig ~/.kube/config
```
這種方式同樣可用於本地除錯與測試。
---
## 2. 正式部署到 Kubernetes 集群
在 Operator 開發完成並測試通過後,就可以把它打包成容器映像,並部署到集群內部運行。步驟大致如下:
### 2.1 建構 Operator 容器映像
#### 撰寫 Dockerfile
在專案中撰寫 Dockerfile,把 Operator 程式打包進映像中。例如:
```Dockerfile=
FROM golang:1.19 as builder
WORKDIR /workspace
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -o manager main.go
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]
```
#### 編譯與推送映像
執行以下命令構建映像並推送至你的容器 Registry:
```bash
docker build -t <your-registry>/operator:latest .
docker push <your-registry>/operator:latest
```
### 2.2 部署 Operator 到集群
#### 配置 RBAC 與 Deployment
Kubebuilder 或 Operator SDK 通常會生成一組 YAML 檔案,包含了以下內容:
- **ServiceAccount**:運行 Operator 的身份
- **Role / ClusterRole**:定義 Operator 可以存取哪些資源
- **RoleBinding / ClusterRoleBinding**:將權限綁定給 ServiceAccount
- **Deployment**:描述 Operator 的 Pod 如何運行
這些檔案一般位於 `config/` 或 `deploy/` 目錄中。你可以使用如下命令一次性部署所有資源(以 Kubebuilder 項目為例):
```bash
kubectl apply -k config/default
```
在 Deployment 裡,你需要確保容器使用剛才推送的映像 `<your-registry>/operator:latest`。
#### 驗證部署狀態
使用以下命令檢查 Operator 是否成功運行:
```bash
kubectl get deployment -n <operator-namespace>
kubectl get pods -n <operator-namespace>
```
查看 Operator Pod 的日誌,確認其正在監控 CR 並根據 Reconcile 邏輯處理事件:
```bash
kubectl logs <operator-pod-name> -n <operator-namespace>
```
---
## 3. Operator 運作流程
一旦 Operator 被部署到集群中,它就會:
- **監控 CR**:通過 Kubernetes API,監聽你定義的 CRD(例如以 Book 為例的 CRD)上所有的建立、更新或刪除事件。
- **Reconcile 邏輯**:每當 CR 發生變化時,Operator 的 Reconcile 迴圈就會被觸發。你的程式邏輯(例如檢測 `edition` 是否變更並發出通知)會在這裡執行。
- **狀態更新或通知**:根據業務邏輯更新 CR 的狀態或發送外部通知(如 Email、Slack 訊息、Webhook 呼叫等)。
---
## 小結
- **本地測試**:可以透過 `make run` 或直接執行 Operator binary,在本地環境中連線集群測試 Operator 的邏輯。
- **正式部署**:將 Operator 打包成容器映像,並透過 Kubernetes 的 Deployment、RBAC 等資源部署到集群中,讓 Operator 長期運行並監控相應的 CR。
這樣,Operator 就會自動運行並根據你的設計處理 Book 資源的再版事件,達到自動化管理與通知的目的。
{%hackmd vaaMgNRPS4KGJDSFG0ZE0w %}