# webapp-operator **Repository Path**: xiaonqedu/webapp-operator ## Basic Information - **Project Name**: webapp-operator - **Description**: k8s CRD+CR+Operator - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-04 - **Last Updated**: 2026-10-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # WebApp Operator > 使用 Kubebuilder 开发的 WebApp 控制器示例:用户提交镜像和副本数,Operator 持续管理 Deployment,并将就绪副本数写回 WebApp。 > > 本文按“原理 → 开发与部署 → 验证”整理。原理部分的代码是简化示意;项目中已完成资源字段和控制器实现,已有项目无需重复初始化或替换代码。 ## 01.Kubebuilder ### 1、概述 > **Kubebuilder**:开发时使用的工具 > > **CRD**:开发产物之一,用来注册 WebApp 类型 > > **Operator**:开发完成后部署运行的管理程序及其配套配置,本例核心是 WebApp 控制器 > > **CR**:用户在使用阶段提交的具体应用要求 - Kubebuilder 就是开发 Kubernetes 自定义控制器 / Operator 的脚手架 - **创建项目骨架**:生成资源类型定义、控制器入口等基础代码 - **生成配套文件**:根据你定义的字段和注释,生成 CRD、权限等配置,并提供构建、部署入口 - **你需要自己写的重点是** - **资源字段**:例如 WebApp 的镜像、副本数 - **控制器逻辑**:例如读取 WebApp CR,创建或更新 Deployment,反馈运行状态 ```text Kubebuilder 搭建项目 ↓ 你定义字段、编写控制器逻辑 ↓ 生成并安装 CRD,编译部署 Operator ↓ 用户提交 CR,Operator 持续处理 ``` - Kubebuilder 是开发工具;部署后真正运行、处理 CR 的是 Operator 中的控制器程序 ### 2、Operator 运行原理 - Operator 的核心原理是:持续观察资源对象,将“实际状态”调整到“用户声明的期望状态” - 下面按照 `准备数据结构 → 启动 Operator → 建立监听 → 用户提交要求 → 控制器处理 → 状态反馈`的顺序梳理 #### ① 定义数据:`api/v1/webapp_types.go` - 这个文件定义 WebApp 对象有哪些字段,让控制器知道如何读取要求、反馈状态 ```go // 用户声明的期望状态 type WebAppSpec struct { Image string `json:"image"` // 使用什么镜像 Replicas *int32 `json:"replicas,omitempty"` // 运行几个副本 } // 控制器反馈的实际状态 type WebAppStatus struct { ReadyReplicas int32 `json:"readyReplicas"` // 当前有几个副本就绪 } ``` - 两者组成 WebApp 对象中的 `spec` 和 `status` ```yaml spec: image: registry.qcraftai.com/oss/nginx:1.26.3-alpine replicas: 3 status: readyReplicas: 3 ``` - 这里只定义数据结构,不执行部署 - `make manifests` 只生成 CRD 清单;执行 `k2 apply -f config/crd/bases/platform.example.com_webapps.yaml` 安装后,API Server 才认识 WebApp 类型 #### ② 启动容器:`config/manager/manager.yaml` - 这个文件定义 `Operator 自身的 Deployment` ```yaml spec: template: spec: containers: - name: manager image: controller:latest # 生成安装清单时替换为你的 Operator 镜像 command: - /manager # 执行编译后的 Go 程序 args: - --leader-elect # 多副本时,通过选主协调工作 ``` - 提交安装清单后,原生 Kubernetes 组件创建 Operator Pod,启动容器中的 `/manager` ```text Operator Deployment ↓ Operator Pod 启动 ↓ 执行 /manager ↓ 进入 cmd/main.go ``` #### ③ 程序启动:`cmd/main.go` - 入口程序负责三件事:**注册类型、配置控制器、启动运行框架** ```go func init() { // 将原生资源和 WebApp 的 Go 类型注册到 scheme, // 供客户端识别、编码和解码资源对象 // 内部可理解为:资源的 Group、Version、Kind → Go 的 reflect.Type。 // 这是内存中的类型映射,不会调用 API Server 创建资源。 clientgoscheme.AddToScheme(scheme) platformv1.AddToScheme(scheme) } func main() { // ① 获取访问 API Server 的连接配置。 config := ctrl.GetConfigOrDie() // ② 创建 Manager 实例,准备客户端、缓存等基础能力。 // 此时还没有启动控制器的处理循环。 mgr, _ := ctrl.NewManager(config, ctrl.Options{ Scheme: scheme, }) // ③ 创建 WebAppReconciler 结构体实例 // & 表示取得实例的指针。 // 将 Manager 提供的客户端和类型注册表赋给它。 reconciler := &controller.WebAppReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), } // ④ 调用该实例的方法,注册监听和处理关系: // 哪些资源变化,需要交给这个实例的 Reconcile 方法处理。 reconciler.SetupWithManager(mgr) // ⑤ 启动 Manager。 // 框架开始同步、监听和处理队列,后续自动调用 Reconcile mgr.Start(ctrl.SetupSignalHandler()) } ``` - 这里的 **Manager 是 Operator 程序内部的管理框架**,不是另外部署的服务 - `&controller.WebAppReconciler{...}` 创建的是内存中的 Go 结构体实例;`Client` 和 `Scheme` 是传给它的依赖,不是保存在 etcd 中的资源对象 ##### `mgr.Start()`:启动框架并持续运行 - 实现来自 `sigs.k8s.io/controller-runtime` 依赖库,项目通过 `main()` 调用它 - `SetupWithManager` 先注册监听和处理关系;`mgr.Start()` 再启动缓存、Watch、控制器和健康检查等运行机制 - 主 goroutine 阻塞在 `mgr.Start()`,其他 goroutine 并发工作;不是整个进程停止,也不是定时调用 `Reconcile` - 启用选主时,控制器协调工作由获得领导权的实例执行;缓存初始同步完成后,工作线程开始处理任务 ```text Operator 进程 ├─ 主 goroutine:调用 mgr.Start(),等待停止或错误 ├─ 缓存与监听:同步资源、接收 Watch 事件、更新缓存并入队 ├─ 控制器工作 goroutine:从队列取出任务,调用 Reconcile └─ 服务 goroutine:处理健康检查、指标等请求 ``` - `ctrl.SetupSignalHandler()` 提供退出上下文;收到 SIGINT 或 SIGTERM 后通知 Manager 停止,`Start()` 完成关闭后返回 - 这些机制由库封装;你负责配置监听资源,并编写 `Reconcile` 中的处理逻辑 #### ④ 配置监听:`internal/controller/webapp_controller.go` - `cmd/main.go` 中的 `reconciler.SetupWithManager(mgr)` 调用下面这个方法 ```go func (r *WebAppReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). // WebApp 发生变化时,安排处理这个 WebApp For(&platformv1.WebApp{}). // Deployment 发生变化时, // 如果它属于某个 WebApp,就安排处理那个 WebApp Owns(&appsv1.Deployment{}). // 指定由当前控制器的 Reconcile 函数处理 Complete(r) } ``` - 这段代码配置监听关系;调用 `mgr.Start()` 后,框架才实际运行 - 调用链:`cmd/main.go` → `controller-runtime` 框架 → 本项目的 `Reconcile()` ```text main.go │ ├─ SetupWithManager(mgr) │ └─ Complete(r):把你的控制器注册给框架 │ └─ mgr.Start(...) └─ 框架启动工作 goroutine └─ 从队列取出任务 └─ 调用 r.Reconcile(ctx, req) ``` - 框架自动完成: ```text 通过 API Server 初始同步资源 ↓ 建立并维护本地缓存 ↓ 通过 API Server 建立 Watch,持续接收变化通知 ↓ 资源变化时,更新缓存并将对应 WebApp 加入工作队列 ↓ 工作线程取出任务,调用 Reconcile ``` - **主要依靠 Watch 事件通知,不是定时拉取所有资源进行比较** - 工作队列主要记录 WebApp 的**命名空间和名称** - Watch 断开重连、缓存维护和失败重试,由框架处理 - Operator 启动前已经存在的 WebApp,也会在初始同步时被发现并安排处理 #### ⑤ 用户提交:`webapp.yaml` - 现在 Operator 已经运行,用户提交一个具体 WebApp ```yaml apiVersion: platform.example.com/v1 kind: WebApp metadata: name: web namespace: webapp-demo spec: image: registry.qcraftai.com/oss/nginx:1.26.3-alpine replicas: 3 ``` ```bash # 将具体应用要求提交给 API Server k2 apply -f webapp.yaml ``` - 随后发生 ```text API Server 校验并保存 WebApp 到 etcd ↓ 通过 Watch 连接向 Operator 发送变化事件 ↓ 框架更新缓存,将下面的对象身份加入队列: namespace: webapp-demo name: web ↓ 工作线程调用 Reconcile(ctx, req) ``` - 收到事件后,不会立即无条件创建 Deployment,而是先进入协调逻辑检查当前状态 #### ⑥ 创建或更新 Deployment:`internal/controller/webapp_controller.go` - `Reconcile` 是你需要编写的核心业务逻辑:**读取要求,检查现状,决定如何调整** ```go func (r *WebAppReconciler) Reconcile( ctx context.Context, req ctrl.Request, ) (ctrl.Result, error) { // ① 本例只处理 webapp-demo 命名空间中的对象 if req.Namespace != "webapp-demo" { return ctrl.Result{}, nil } // ② 按命名空间和名称读取当前 WebApp // 默认通常从框架通过 API 同步维护的缓存中读取 var web platformv1.WebApp if err := r.Get(ctx, req.NamespacedName, &web); err != nil { // 对象已不存在时结束;其他错误交给框架重试 return ctrl.Result{}, client.IgnoreNotFound(err) } // ③ 定位需要管理的同名 Deployment dep := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: web.Name, Namespace: web.Namespace, }, } // ④ 获取已有对象,并按期望配置创建或更新 _, err := controllerutil.CreateOrUpdate(ctx, r.Client, dep, func() error { // 省略容器初始化、默认值处理和已有对象的所属关系检查 dep.Spec.Replicas = web.Spec.Replicas dep.Spec.Template.Spec.Containers[0].Image = web.Spec.Image // 省略标签、节点选择、资源配置和探针设置 // 设置 ownerReferences,说明这个 Deployment 属于哪个 WebApp return controllerutil.SetControllerReference(&web, dep, r.Scheme) }) if err != nil { return ctrl.Result{}, err } // 后面继续进行状态反馈,见第⑧步 // ... } ``` - 其中 `CreateOrUpdate` 根据比较结果决定是否请求 API Server ```text Deployment 不存在 → 调用 API Server 创建 Deployment 已存在,但配置需要调整 → 调用 API Server 更新 Deployment 已符合要求 → 不发送更新请求 ``` - API Server 将创建或更新结果保存到 etcd - 原生 Deployment 控制器不会读取 WebApp 内部的配置,所以需要这一步把要求落实成独立的 Deployment 对象 #### ⑦ 原生 Kubernetes 组件执行部署 - Deployment 保存后,后续由原生组件接手 ```text Deployment 控制器 ↓ 管理 ReplicaSet ReplicaSet 控制器 ↓ 创建、维护 Pod Scheduler ↓ 为 Pod 选择节点 节点上的 kubelet、容器运行时 ↓ Nginx 容器启动,执行就绪检查 ``` - Pod 状态持续通过 API Server 上报,ReplicaSet、Deployment 控制器据此更新各自对象的状态 - WebApp Operator 负责管理 Deployment,不直接启动容器 #### ⑧ 状态回写:`internal/controller/webapp_controller.go` 的 `Reconcile()` - Deployment 就绪副本数变化后,由于配置了 ```text Owns(&appsv1.Deployment{}) ``` - 框架会通过 Deployment 的 `ownerReferences` 找到对应 WebApp,再将它加入队列,重新执行 `Reconcile` - 代码位置:同一文件的 `Reconcile()` 内,位于 `CreateOrUpdate()` 成功之后、函数返回之前 - 此时 Deployment 的配置已经符合要求,通常不需要修改,但可以更新 WebApp 状态 ```go func (r *WebAppReconciler) Reconcile( ctx context.Context, req ctrl.Request, ) (ctrl.Result, error) { // ① 读取 WebApp 对象。 // ... // ② 创建或更新 Deployment。 // controllerutil.CreateOrUpdate(...) // ... // ③ 将 Deployment 的实际就绪副本数写回 WebApp。 if web.Status.ReadyReplicas != dep.Status.ReadyReplicas { web.Status.ReadyReplicas = dep.Status.ReadyReplicas // 通过 API Server 更新 WebApp.status。 if err := r.Status().Update(ctx, &web); err != nil { return ctrl.Result{}, err } } return ctrl.Result{}, nil } ``` - 最终 WebApp 中会显示 ```yaml spec: replicas: 3 # 用户期望 status: readyReplicas: 3 # 控制器反馈的实际状态 ``` - 完整运行链路 ```text manager.yaml:启动 Operator 容器 ↓ cmd/main.go:创建 Manager 和 WebAppReconciler 实例 ↓ SetupWithManager:配置 WebApp、Deployment 的监听关系 ↓ mgr.Start():启动框架,初始同步资源、建立 Watch、运行工作队列 │ 用户提交或修改 webapp.yaml ↓ API Server 保存 WebApp → Watch 通知 → 入队 ↓ Reconcile:读取当前状态 ↓ 按需调用 API Server,创建或更新 Deployment ↓ 原生组件运行 Pod,更新 Deployment 状态 ↓ Watch 通知 → 对应 WebApp 再次入队 ↓ Reconcile:将实际状态写回 WebApp.status ``` ## 02.实战 ### 0、环境准备与项目结构 #### ① 环境说明 - 下面按文档的 **① 注册 CRD → ② 启动 Operator → ③ 创建 CR → ④ 自动部署 Pod** 来操作 - 示例约定: - 集群:`k2` 对应的 ci2 集群 - Operator 命名空间:`webapp-operator-system` - 业务命名空间:`webapp-demo` - 应用:Nginx - 调度节点:沿用 `deploy-type=prometheus2` - **Operator 也部署到集群中运行** - 需要编写的代码只有两处:WebApp 字段定义、WebApp 控制器逻辑 - 本项目 API 组为 `platform.example.com/v1`,Go 模块为 `example.com/webapp-operator` - 控制器仅处理 `webapp-demo` 中的对象;这是 `Reconcile` 内的判断,当前代码没有将缓存和 Watch 限制为这个命名空间 - 本例使用 3 个副本,但没有配置反亲和要求;3 个 Pod 可以运行在同一个符合标签条件的节点上 - 以下命令在交互式终端执行,`k2` 使用你已经配置好的集群连接 #### ② 安装 Kubebuilder - 本例固定使用 Kubebuilder `v4.11.0` - Kubebuilder 负责生成项目框架,后面需要补充资源字段和控制器逻辑 [官方入门说明](https://book.kubebuilder.io/quick-start) ```bash # 检查已有工具 go version # 查看本地基础版本;本项目通过下面的 GOTOOLCHAIN 使用 Go 1.25.3 docker version # 29.1.3 make --version # GNU Make 4.3 # 确认 k2 指向的集群 k2 config current-context # 本例沿用你现有的节点标签 k2 get nodes -l deploy-type=prometheus2 # 使用该项目所需的 Go 工具链;首次运行会下载 export GOTOOLCHAIN=go1.25.3 go version # 安装固定版本的 Kubebuilder,适用于 Linux amd64 mkdir -p "$HOME/.local/bin" curl -fL \ https://github.com/kubernetes-sigs/kubebuilder/releases/download/v4.11.0/kubebuilder_linux_amd64 \ -o "$HOME/.local/bin/kubebuilder" chmod +x "$HOME/.local/bin/kubebuilder" export PATH="$HOME/.local/bin:$PATH" kubebuilder version ``` #### ③ 初始化项目(仅新建项目时执行) - 当前项目已经完成初始化,已有项目跳过这个代码块,直接查看字段、控制器实现和部署步骤 ```bash mkdir -p ~/tmp/webapp-operator cd ~/tmp/webapp-operator # 创建 Operator 项目框架 # --domain example.com:设置自定义 API 组的域名后缀 # --repo example.com/webapp-operator:设置 Go 模块路径 kubebuilder init \ --domain example.com \ --repo example.com/webapp-operator # 生成 WebApp 资源类型和控制器的代码框架 # --group platform:API 组前缀,与 domain 组成 platform.example.com # --version v1:自定义 API 的版本,最终为 platform.example.com/v1 # --kind WebApp:资源类型名称,对应 YAML 中的 kind: WebApp # --resource:生成资源类型定义代码,后续据此生成 CRD # --controller:生成控制器代码框架,具体管理逻辑需要自己编写 # 此命令只生成本地文件,不会向集群注册 CRD 或部署控制器 kubebuilder create api \ --group platform \ --version v1 \ --kind WebApp \ --resource \ --controller ``` - 此时主要文件的作用 ```text cmd/main.go # 程序入口,注册并启动控制器 api/v1/webapp_types.go # 定义 WebApp 的字段 internal/controller/webapp_controller.go # 实现如何管理 Deployment config/crd/bases/ # 生成的 CRD 清单 config/rbac/ # Operator 权限配置 config/manager/ # Operator 自身的 Deployment ``` ### 1、创建 CRD 对象,注册 WebApp 类型 - `kubebuilder create api` 已经生成了基本结构,我们主要补充下面三部分 - 用户可以填写 `image`、`replicas`,控制器回写 `readyReplicas` #### `api/v1/webapp_types.go` #### ① `WebAppSpec` - 定义用户可以填写的要求 ```go type WebAppSpec struct { // 镜像不能为空 // +kubebuilder:validation:MinLength=1 Image string `json:"image"` // 副本数不能小于 0;不填写时默认为 1 // +kubebuilder:validation:Minimum=0 // +kubebuilder:default=1 // +optional Replicas *int32 `json:"replicas,omitempty"` } ``` - 对应用户以后提交的 CR ```yaml spec: image: registry.qcraftai.com/oss/nginx:1.26.3-alpine replicas: 3 ``` #### ② `WebAppStatus` - WebApp 控制器读取 `Deployment.status.readyReplicas` 后,写回 WebApp 对象 - 用户不填写 status;`spec.replicas` 表达期望数量,`status.readyReplicas` 表达实际就绪数量,Running 不等于 Ready ```go type WebAppStatus struct { // 实际就绪的副本数,由控制器更新 // +optional ReadyReplicas int32 `json:"readyReplicas"` } ``` - 对应 CR 中的 ```yaml status: readyReplicas: 3 ``` - 这里只定义字段,不会自动统计副本数;更新逻辑要写在控制器中 #### ③ 注释标记 - 注释标记,配置 CRD 的行为和显示方式,这些 `+kubebuilder:` 注释会被代码生成工具读取 - 标记要放在 `type WebApp struct` 上方,不能随便放在文件中, 它们配置的是 WebApp 这个资源类型 ```go // 告诉代码生成工具:WebApp 是k8s对象,需要为它生成 k8s 客户端处理对象所需的代码 // +kubebuilder:object:root=true // // 让控制器可以单独更新 status,反馈实际运行状态 // +kubebuilder:subresource:status // // 让 WebApp 对象归属于某个命名空间,例如 webapp-demo // +kubebuilder:resource:scope=Namespaced // // 执行 k2 get webapp 时,显示 Desired 列,值来自期望副本数 // +kubebuilder:printcolumn:name="Desired",type=integer,JSONPath=".spec.replicas" // 执行 k2 get webapp 时,显示 Ready 列,值来自实际就绪副本数 // +kubebuilder:printcolumn:name="Ready",type=integer,JSONPath=".status.readyReplicas" type WebApp struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec WebAppSpec `json:"spec"` Status WebAppStatus `json:"status,omitempty"` } ``` ```text # 后两项只控制查询结果的展示,例如 NAME DESIRED READY web 3 3 ``` #### ④ 部署或更新 CRD - 其他的 `WebApp`、`WebAppList`、`SchemeBuilder.Register` 等属于生成的基本结构,保留即可 - 如果生成文件中存在示例字段 `Foo`,将它替换为上面的业务字段 - 修改完执行: ```bash cd ~/tmp/webapp-operator # 生成 DeepCopy 代码和 CRD 等清单 make generate manifests # 创建或更新集群中的 CRD k2 apply -f config/crd/bases/platform.example.com_webapps.yaml # 确认资源类型已注册,字段已更新 k2 get crd webapps.platform.example.com k2 explain webapp.spec ``` - 初次安装到这里,只完成了 WebApp 类型注册;还需要部署下面的 Operator 来处理具体 CR ### 2、编写并部署 WebApp Operator [webapp-operator](https://gitee.com/xiaonqedu/webapp-operator) - 上一步定义了 WebApp 的字段;这一步编写程序,读取 WebApp 的要求并持续管理 Deployment - `kubebuilder create api` 已生成控制器框架,下面按位置修改已有文件 - 本例只处理 `webapp-demo` 命名空间中的 WebApp,业务 Pod 使用节点标签 `deploy-type=prometheus2` #### `internal/controller/webapp_controller.go` #### ① `WebAppReconciler` 与依赖 - 保留文件顶部的版权注释,将 `package`、`import` 和 `WebAppReconciler` 部分整理为下面内容 - `client.Client`:用于读取资源、通过 API Server 创建或更新资源;默认读取通常使用由 API 同步维护的本地缓存 - `Scheme`:记录 Go 类型与 Kubernetes 资源类型的对应关系,设置所属关系时也会使用 ```go package controller import ( "context" "fmt" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" platformv1 "example.com/webapp-operator/api/v1" ) // 这个内容没有做修改 type WebAppReconciler struct { client.Client Scheme *runtime.Scheme } ``` #### ② RBAC 权限标记 - 替换 `Reconcile` 函数上方已有的 `+kubebuilder:rbac` 标记,不要重复追加 - 允许控制器读取 WebApp、更新其状态,以及创建和更新 Deployment - `webapps/finalizers` 的更新权限用于本例设置带删除阻塞标记的所属关系,不代表这里实现了自定义 Finalizer 清理逻辑 - 这些注释由 `make manifests` 生成权限清单,部署该清单后,Operator 才获得相应权限 ```go // 以下标记用于生成 Operator 的 RBAC 权限 // +kubebuilder:rbac:groups=platform.example.com,resources=webapps,verbs=get;list;watch // +kubebuilder:rbac:groups=platform.example.com,resources=webapps/status,verbs=get;update;patch // +kubebuilder:rbac:groups=platform.example.com,resources=webapps/finalizers,verbs=update // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch ``` #### ③ `Reconcile`:读取要求,创建或更新 Deployment - 替换生成的整个 `Reconcile` 函数;发生相关资源变化时,框架将对象的命名空间和名称交给这个函数处理 - 主要处理过程: ```text 读取 WebApp 对象 ↓ 读取 spec.image、spec.replicas ↓ 创建或更新同名 Deployment ├─ 设置镜像、副本数 ├─ 补充标签、节点选择、资源请求和就绪探针 └─ 设置 ownerReferences,指向 WebApp ↓ 读取 Deployment.status.readyReplicas ↓ 通过 API Server 更新 WebApp.status.readyReplicas ``` - Deployment 不存在时创建,存在时调整受管理的配置;返回错误后,框架会按退避机制重试 - 这里创建的是 Deployment 对象,后续 ReplicaSet、Pod 和容器启动仍由原生 Kubernetes 组件负责 ```go func (r *WebAppReconciler) Reconcile( ctx context.Context, req ctrl.Request, ) (ctrl.Result, error) { // 本例只管理这个演示命名空间 if req.Namespace != "webapp-demo" { return ctrl.Result{}, nil } // 读取 WebApp 对象;对象已经删除时结束处理 var web platformv1.WebApp if err := r.Get(ctx, req.NamespacedName, &web); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } if !web.DeletionTimestamp.IsZero() { return ctrl.Result{}, nil } replicas := int32(1) if web.Spec.Replicas != nil { replicas = *web.Spec.Replicas } // Deployment 与 WebApp 使用相同名称、命名空间 dep := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: web.Name, Namespace: web.Namespace, }, } // 不存在则创建,存在则将受管理的配置调整到期望值 _, err := controllerutil.CreateOrUpdate(ctx, r.Client, dep, func() error { // 防止接管用户另外创建的同名 Deployment if !dep.CreationTimestamp.IsZero() && !metav1.IsControlledBy(dep, &web) { return fmt.Errorf( "Deployment %s/%s 已存在,但不属于该 WebApp", dep.Namespace, dep.Name, ) } labels := map[string]string{"app": web.Name} dep.Spec.Replicas = &replicas if dep.Spec.Selector == nil { dep.Spec.Selector = &metav1.LabelSelector{ MatchLabels: labels, } } if dep.Spec.Template.Labels == nil { dep.Spec.Template.Labels = map[string]string{} } dep.Spec.Template.Labels["app"] = web.Name podSpec := &dep.Spec.Template.Spec if podSpec.NodeSelector == nil { podSpec.NodeSelector = map[string]string{} } podSpec.NodeSelector["deploy-type"] = "prometheus2" // 创建时初始化容器;更新时保留 API Server 已填充的默认字段 if len(podSpec.Containers) == 0 { podSpec.Containers = []corev1.Container{{Name: "nginx"}} } container := &podSpec.Containers[0] container.Image = web.Spec.Image container.Ports = []corev1.ContainerPort{{ Name: "http", ContainerPort: 80, Protocol: corev1.ProtocolTCP, }} container.Resources = corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("50m"), corev1.ResourceMemory: resource.MustParse("64Mi"), }, Limits: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("500m"), corev1.ResourceMemory: resource.MustParse("128Mi"), }, } container.ReadinessProbe = &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ TCPSocket: &corev1.TCPSocketAction{ Port: intstr.FromInt(80), }, }, PeriodSeconds: 10, TimeoutSeconds: 1, SuccessThreshold: 1, FailureThreshold: 3, } // 设置 Deployment.ownerReferences,指向 WebApp return controllerutil.SetControllerReference(&web, dep, r.Scheme) }) if err != nil { return ctrl.Result{}, err } // 将 Deployment 就绪副本数回写到 WebApp.status if web.Status.ReadyReplicas != dep.Status.ReadyReplicas { web.Status.ReadyReplicas = dep.Status.ReadyReplicas if err := r.Status().Update(ctx, &web); err != nil { return ctrl.Result{}, err } } return ctrl.Result{}, nil } ``` #### ④ `SetupWithManager`:配置监听对象 - 替换生成的整个 `SetupWithManager` 函数 - `For(&WebApp{})`:WebApp 创建、修改或删除等变化触发协调 - `Owns(&Deployment{})`:所属 Deployment 的变化也触发对应 WebApp 的协调;所属关系由前面的 `SetControllerReference` 设置 - `Complete(r)`:将这些事件交给当前控制器的 `Reconcile` 处理 - 框架通过 API Server 初始同步并建立 Watch,接收变化、维护缓存和工作队列;不是控制器直接监听 etcd ```go // WebApp 变化或所属 Deployment 变化时,触发重新协调 func (r *WebAppReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&platformv1.WebApp{}). Owns(&appsv1.Deployment{}). Complete(r) } ``` - 因此,修改 WebApp 的副本数会推动 Deployment 更新;Deployment 的就绪状态变化又会推动 WebApp 状态更新 - 生成的 `cmd/main.go` 已注册 WebApp 类型和控制器,本例不需要修改入口文件 #### ⑤ 生成清单并编译 - 在项目根目录执行;`make build` 已包含生成清单、生成代码、格式化、静态检查和编译 ```bash cd ~/tmp/webapp-operator # 生成 CRD、RBAC、DeepCopy 等,并编译控制器 make build # 本地编译产物:bin/manager # 此时还没有向集群部署 Operator ``` #### ⑥ `.dockerignore`:放行构建所需文件 - 文件名为项目根目录的 `.dockerignore`;下面是文件内容,不是终端命令 - 显式放行源码目录,避免构建时只有 go.mod、go.sum 被复制,出现找不到 cmd/main.go 的错误 ```text # 默认排除所有内容 ** # 放行编译需要的源码目录和其中的文件 !cmd/ !cmd/** !api/ !api/** !internal/ !internal/** # 排除测试文件 **/*_test.go # 放行 Go 依赖文件 !go.mod !go.sum ``` #### ⑦ 准备命名空间和镜像拉取凭据 - 本机 `docker login` 供本机推送、拉取镜像;节点拉取私有镜像需要单独配置 `imagePullSecrets` - Secret 必须与引用它的 Pod 在同一个命名空间;先创建命名空间,再创建 Secret ```bash cd ~/tmp/webapp-operator # 创建 Operator 和业务命名空间;可以重复执行 k2 create namespace webapp-operator-system \ --dry-run=client -o yaml | k2 apply -f - k2 create namespace webapp-demo \ --dry-run=client -o yaml | k2 apply -f - # 查看是否已有镜像拉取凭据 k2 get secret aliyun-registry-secret -n webapp-operator-system ``` - 如果 Secret 已存在且凭据有效,直接使用;只有缺少或需要更新时才执行下面的命令 - 输入有该镜像拉取权限的仓库账号和密码或令牌,README 不保存实际密码 ```bash # 获取账号及凭据,密码输入时不显示 read -r -p "仓库账号:" REGISTRY_USER read -r -s -p "仓库密码或令牌:" REGISTRY_PASSWORD echo # 创建或更新 Operator 的镜像拉取凭据 k2 create secret docker-registry aliyun-registry-secret \ -n webapp-operator-system \ --docker-server=registry.qcraftai.com \ --docker-username="$REGISTRY_USER" \ --docker-password="$REGISTRY_PASSWORD" \ --dry-run=client -o yaml | k2 apply -f - unset REGISTRY_USER REGISTRY_PASSWORD ``` #### ⑧ `config/manager/manager.yaml`:配置 Operator 自身的 Pod - 当前项目已经设置了下面的字段;如果重新搭建,在 Deployment 的 `spec.template.spec` 下添加 - `nodeSelector`、`imagePullSecrets` 与 `containers` 同级,保留原有的其他配置 ```bash # 编辑 Operator 的部署配置 vim config/manager/manager.yaml ``` ```yaml spec: template: spec: nodeSelector: deploy-type: prometheus2 imagePullSecrets: - name: aliyun-registry-secret # 原有 containers、securityContext 等配置保留 ``` - 这里控制 Operator 自身的 Pod;Nginx Pod 的节点选择由 `internal/controller/webapp_controller.go` 设置 #### ⑨ 构建镜像并部署 - 构建并推送的是 Operator 管理程序的镜像;Nginx 镜像由下一步的 WebApp CR 指定 - 输入的仓库地址必须有推送权限,集群也必须能够拉取该镜像 ```bash cd ~/tmp/webapp-operator # 设置 Operator 镜像地址 export IMG=registry.qcraftai.com/qcraft/webapp-operator:v0.1.0 # 登录镜像仓库;若已经成功登录,可跳过 docker login registry.qcraftai.com # 构建 Operator 镜像(Makefile 中的 docker-build 任务) # 默认等价于 docker build -t "$IMG" . make docker-build IMG="$IMG" # 推送镜像(Makefile 中的 docker-push 任务) # 默认等价于 docker push "$IMG" make docker-push IMG="$IMG" ``` - Dockerfile 在构建阶段下载 Go 基础镜像、编译程序,再将程序放入运行镜像;本地 Go 缓存与镜像内的构建环境相互独立 - 每次修改 Go 代码,需要重新构建并推送镜像;建议使用新的版本标签,并在生成安装清单时传入相同的 `IMG` - 生成并提交安装清单,创建 Operator 的权限配置和 Deployment,再检查启动结果 ```bash # 根据 Go 类型和注释,重新生成 CRD、RBAC 等清单及 DeepCopy 代码 # 将 Operator 的镜像地址设置为 $IMG # 用 Kustomize 合并配置,生成 dist/install.yaml make build-installer IMG="$IMG" # 使用 k2 安装到目标集群 k2 apply -f dist/install.yaml # 等待 Operator 启动 k2 rollout status \ deployment/webapp-operator-controller-manager \ -n webapp-operator-system \ --timeout=180s # 查看 Operator Pod 和日志 k2 get pods -n webapp-operator-system -o wide k2 logs \ -n webapp-operator-system \ deployment/webapp-operator-controller-manager \ -c manager \ --tail=50 ``` - 等待成功且 Pod 显示 `1/1 Running` 后,Operator 已启动;下一步创建 WebApp CR,才开始部署对应的 Nginx 应用 ### 3、创建 WebApp 对象,也就是 CR ```bash cd ~/tmp/webapp-operator cat > webapp.yaml <<'EOF' apiVersion: platform.example.com/v1 kind: WebApp metadata: name: web namespace: webapp-demo spec: image: registry.qcraftai.com/oss/nginx:1.26.3-alpine # 业务应用的镜像 replicas: 3 # 期望运行 3 个副本 EOF # 创建 WebApp CR k2 apply -f webapp.yaml # 查看具体的 WebApp 对象 k2 get webapps.platform.example.com -n webapp-demo # 查看它的配置和状态 k2 get webapp web -n webapp-demo -o yaml ``` - 提交后,API Server 校验并保存 WebApp 对象,Operator 收到变化通知后开始协调 ### 4、Operator 创建 Deployment,原生组件部署 Pod - 这一步自动发生,不用再手动提交 Deployment YAML ```text WebApp/web ↓ WebApp Operator 创建 Deployment/web ↓ Deployment 控制器创建 ReplicaSet ↓ ReplicaSet 控制器创建 3 个 Pod ↓ Scheduler、kubelet、容器运行时 Nginx 运行 ``` - 查看执行结果 ```bash # 持续观察 Deployment 是否已创建;出现后按 Ctrl+C 退出观察 k2 get deployments -n webapp-demo -w # 等待业务 Deployment 就绪 k2 rollout status deployment/web \ -n webapp-demo \ --timeout=180s # 查看完整资源链路 k2 get webapp,deploy,rs,pod -n webapp-demo -o wide # 查看 Deployment 的所有者,应显示 WebApp/web k2 get deployment web \ -n webapp-demo \ -o jsonpath='{range .metadata.ownerReferences[*]}{.kind}{"/"}{.name}{"\n"}{end}' # 查看 Operator 回写的就绪副本数 k2 get webapp web -n webapp-demo ``` - 正常情况下,WebApp 的 DESIRED、READY 均为 `3`,Deployment 的 READY 为 `3/3` - `configured` 只表示对象修改成功,不代表 Pod 已启动;本例就绪探针检查容器 TCP 80 端口 - 当前代码只在就绪数变化时写回 status;初始状态一直为 0 时,WebApp 可能暂时没有 status 字段 - 如果 Pod 没有启动,查看原因 ```bash # 查看 Pod 状态 k2 get pods -n webapp-demo -o wide # 查看调度、镜像拉取、探针等事件 k2 describe pods -n webapp-demo -l app=web # 查看 Operator 的处理错误 k2 logs \ -n webapp-operator-system \ deployment/webapp-operator-controller-manager \ -c manager \ --tail=100 ``` ### 5、访问 Nginx 并验证持续协调 本例使用端口转发进行本地验证: ```bash # 保持该命令运行 k2 port-forward \ -n webapp-demo \ deployment/web \ 18080:80 ``` - 保持端口转发运行,在另一个终端访问: ```bash curl http://127.0.0.1:18080 ``` - 也可以用浏览器访问 `http://127.0.0.1:18080`;本例没有创建 Service 或公网入口,端口转发只用于本地验证 - 修改 CR,验证 Operator 持续管理 - 另开终端,将 WebApp 的期望副本数修改为 `2` ```bash # 修改的是 WebApp CR,不是 Deployment k2 patch webapp web \ -n webapp-demo \ --type=merge \ -p '{"spec":{"replicas":2}}' # 观察 Operator 自动修改 Deployment k2 get deployment web -n webapp-demo -w ``` 看到副本数调整后按 `Ctrl+C`,再执行: ```bash # 查看 WebApp 和 Deployment 的最终状态 k2 get webapp,deploy,pod -n webapp-demo # 恢复 YAML 中定义的 3 个副本 k2 apply -f ~/tmp/webapp-operator/webapp.yaml ``` ## 03.常见问题 - **`docker build -t .` / invalid reference format**:当前终端的 `IMG` 为空,重新执行 `export IMG=registry.qcraftai.com/qcraft/webapp-operator:v0.1.0` - **找不到 cmd/main.go**:检查 `.dockerignore` 是否放行 cmd、api、internal 目录及其中的源码 - **镜像推送 denied**:检查本机 Docker 登录及目标仓库的推送权限 - **Pod 拉取镜像 denied / unauthorized**:检查镜像是否已推送、Pod 是否引用同命名空间下的有效 imagePullSecrets;本机登录不会自动传给节点 - **拉取 docker.io 超时**:修改 WebApp CR 中的镜像,使用集群可以访问的公司仓库镜像;本例已使用 `registry.qcraftai.com/oss/nginx:1.26.3-alpine` - **业务镜像需要认证**:在 `webapp-demo` 创建有效的拉取 Secret,并配置业务 Pod 使用的 default ServiceAccount 的 imagePullSecrets;配置后需让业务 Pod 重新创建,才能继承该设置。Operator 命名空间中的 Secret 不会自动共享给业务 Pod - **Pod Pending**:查看事件,检查 `deploy-type=prometheus2` 节点是否存在、资源是否充足,以及是否有未容忍的污点 - **namespace not found**:先创建对象 metadata.namespace 指定的命名空间 ```bash # 查看业务 Pod 的调度、拉取和就绪检查事件 k2 describe pods -n webapp-demo -l app=web # 查看 Operator 自身的状态和事件 k2 get pods -n webapp-operator-system -o wide k2 describe pods -n webapp-operator-system -l control-plane=controller-manager # 查看控制器日志 k2 logs deployment/webapp-operator-controller-manager \ -n webapp-operator-system -c manager --tail=100 ``` ## License Copyright 2026. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.