一、背景
最近在学习 Kubernetes Operator 开发,目标是从零实现一个最小可运行的 Operator。
这个 Operator 的目标很简单:
定义一个自定义资源
AppService,用户只需要声明镜像、端口、副本数,Operator 自动创建对应的 Deployment 和 Service。
最终效果类似这样:
apiVersion: app.nebula.io/v1
kind: AppService
metadata:
name: appservice-sample
spec:
image: nginx:1.25
replicas: 2
port: 80
应用这个资源后,Operator 自动生成:
Deployment/appservice-sample
Service/appservice-sample
Pod/appservice-sample-xxx
这篇文章记录完整开发过程,包括环境准备、项目初始化、CRD 定义、Controller 编写、Reconcile 逻辑、踩坑排查和最终运行结果。
二、Operator 是什么
Kubernetes 本身已经有很多内置资源,比如:
Pod
Deployment
Service
Ingress
ConfigMap
Secret
但是在实际业务里,我们经常希望用更贴近业务的方式描述应用。
比如平台用户不一定想直接写 Deployment / Service / Ingress,而是希望写:
kind: AppService
spec:
image: nginx:1.25
replicas: 2
port: 80
然后由平台自动生成底层 Kubernetes 资源。
这就是 Operator 的核心思想:
Operator = CRD + Controller + Reconcile Loop
其中:
| 组件 | 作用 |
|---|---|
| CRD | 扩展 Kubernetes API,定义一种新资源 |
| CR | 用户创建的自定义资源实例 |
| Controller | 监听资源变化 |
| Reconcile | 对齐期望状态和实际状态 |
| status | 回写当前观察到的运行状态 |
| ownerReference | 建立父子资源关系,支持级联管理 |
本项目的核心链路是:
AppService CR
↓
AppService Controller
↓
Reconcile
↓
Deployment + Service
↓
Pod Running
三、本地环境准备
我的本地环境如下:
bash
go version
输出
go version go1.26.2 darwin/arm64
kubectl version
输出
Client Version: v1.34.1
Kustomize Version: v5.7.1
Server Version: v1.33.6+k3s1
docker version
输出
Client Version: 29.4.3
Server Version: 29.4.3
OS/Arch: linux/arm64
一开始本地没有安装 kind 和 kubebuilder
kind version
kubebuilder version
输出:
bash: kind: command not found
bash: kubebuilder: command not found
这里 kind 不是必须的,因为我已经有一个可用的 K3s 集群,kubectl version 能看到 Server Version,说明已经能连接 Kubernetes 集群。
真正需要安装的是 kubebuilder。
四、安装 Kubebuilder
使用 Homebrew 安装:
brew install kubebuilder
安装完成后查查看版本:
kubebuilder version
输出:
KubeBuilder: v4.15.0
Kubernetes: 1.36.0
Git Commit: 034c380389c00396878da8b388d42b17d55f8dd8
Build Date: 2026-06-14T19:16:32Z
Go OS/Arch: darwin/arm64
到这里,基础工具准备完成
五、初始化 Operator 项目
我的代码目录是:
cd /Users/wenjun/data/project/go
mkdir appservice-operator
cd appservice-operator
初始化 kubebuilder 项目:
kubebuilder init \
--domain nebula.io \
--repo github.com/wenjun/appservice-operator
执行完成后,项目结构如下:
appservice-operator
├── Dockerfile
├── Makefile
├── PROJECT
├── README.md
├── cmd
├── config
├── go.mod
├── go.sum
├── hack
├── internal
└── test
其中比较重要的是:
| 目录 | 作用 |
|---|---|
api/ | 后面会存放 CRD 的 Go 类型定义 |
internal/controller/ | Controller 和 Reconcile 逻辑 |
config/crd/ | 生成出来的 CRD YAML |
config/samples/ | 示例 CR |
config/rbac/ | RBAC 权限 |
cmd/main.go | Operator 启动入口 |
六、创建 AppService API
执行:
kubebuilder create api \
--group app \
--version v1 \
--kind AppService
中间会提示:
Create Resource [y/n]
Create Controller [y/n]
都选择:
y
y
生成的关键文件:
api/v1/appservice_types.go
api/v1/groupversion_info.go
internal/controller/appservice_controller.go
internal/controller/appservice_controller_test.go
config/samples/app_v1_appservice.yaml
七、第一次生成并安装 CRD
执行:
make manifests
生成 CRD 文件:
config/crd/bases/app.nebula.io_appservices.yaml
安装 CRD 到集群:
make install
输出:
customresourcedefinition.apiextensions.k8s.io/appservices.app.nebula.io created
查看 CRD:
kubectl get crd | grep appservices
输出:
appservices.app.nebula.io
说明 CRD 已经安装成功。
八、第一次启动 Controller
执行:
make run
第一次启动时遇到了端口冲突:
Failed to start manager {"error": "error listening on :8081: listen tcp :8081: bind: address already in use"}
这是因为 8081 端口已经被占用。
使用下面命令查看端口占用:
lsof -i :8081
输出类似:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
main 46331 wenjun 3u IPv6 ... TCP *:sunproxyadmin (LISTEN)
杀掉进程:
kill -9 46331
再次执行:
make run
启动成功:
INFO setup Starting manager
INFO starting server {"name": "health probe", "addr": "[::]:8081"}
INFO Starting EventSource {"controller": "appservice", "controllerGroup": "app.nebula.io", "controllerKind": "AppService", "source": "kind source: *v1.AppService"}
INFO Starting Controller {"controller": "appservice", "controllerGroup": "app.nebula.io", "controllerKind": "AppService"}
INFO Starting workers {"controller": "appservice", "controllerGroup": "app.nebula.io", "controllerKind": "AppService", "worker count": 1}
说明 Controller 已经启动,并开始监听 AppService 资源。
九、第一次创建 AppService 遇到的问题
查看默认 sample:
cat config/samples/app_v1_appservice.yaml
默认内容是:
apiVersion: app.nebula.io/v1
kind: AppService
metadata:
labels:
app.kubernetes.io/name: appservice-operator
app.kubernetes.io/managed-by: kustomize
name: appservice-sample
spec:
# TODO(user): Add fields here
直接 apply:
kubectl apply -f config/samples/app_v1_appservice.yaml
报错:
The AppService "appservice-sample" is invalid: spec: Required value
原因是当前 CRD 里 spec 是必填的,但是 sample 里面的 spec 为空。
于是我把 sample 改成:
apiVersion: app.nebula.io/v1
kind: AppService
metadata:
labels:
app.kubernetes.io/name: appservice-operator
app.kubernetes.io/managed-by: kustomize
name: appservice-sample
spec:
image: nginx:1.25
replicas: 2
port: 80
再次执行:
kubectl apply -f config/samples/app_v1_appservice.yaml
又报错:
strict decoding error: unknown field "spec.image", unknown field "spec.port", unknown field "spec.replicas"
这个问题的原因是:
sample 里写了
image / replicas / port,但是 Go 类型定义AppServiceSpec里还没有定义这些字段,所以生成的 CRD schema 不认识这些字段。
十、定义 AppService Spec 和 Status
打开:
vim api/v1/appservice_types.go
修改 AppServiceSpec:
type AppServiceSpec struct {
// Image is the container image of the application.
Image string `json:"image"`
// Replicas is the desired number of pods.
// +kubebuilder:validation:Minimum=1
// +kubebuilder:default=1
Replicas int32 `json:"replicas,omitempty"`
// Port is the container port exposed by the application.
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=65535
Port int32 `json:"port"`
}
修改 AppServiceStatus:
type AppServiceStatus struct {
// ReadyReplicas is the number of ready pods.
ReadyReplicas int32 `json:"readyReplicas,omitempty"`
// Phase represents the current phase of the application.
Phase string `json:"phase,omitempty"`
// Message describes current status details.
Message string `json:"message,omitempty"`
}
AppService 结构体保持 Kubebuilder v4.15 默认风格即可:
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// AppService is the Schema for the appservices API.
type AppService struct {
metav1.TypeMeta `json:",inline"`
// metadata is a standard object metadata
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
// spec defines the desired state of AppService
// +required
Spec AppServiceSpec `json:"spec"`
// status defines the observed state of AppService
// +optional
Status AppServiceStatus `json:"status,omitzero"`
}
注意这里有一行比较重要:
// +kubebuilder:subresource:status
它表示该 CRD 支持 status 子资源,后续 Controller 可以单独更新状态。
十一、重新生成并安装 CRD
修改 Go 类型之后,需要重新生成 CRD:
make manifests
检查 CRD 里是否已经生成新字段:
grep -n "image:" config/crd/bases/app.nebula.io_appservices.yaml
grep -n "replicas:" config/crd/bases/app.nebula.io_appservices.yaml
grep -n "port:" config/crd/bases/app.nebula.io_appservices.yaml
输出:
46: image:
56: replicas:
53: port:
然后重新安装 CRD:
make install
再次创建 AppService:
kubectl delete appservice appservice-sample --ignore-not-found
kubectl apply -f config/samples/app_v1_appservice.yaml
输出:
appservice.app.nebula.io/appservice-sample created
查看资源:
kubectl get appservice appservice-sample -o yaml
输出中可以看到:
spec:
image: nginx:1.25
port: 80
replicas: 2
到这里,自定义资源已经可以正常创建。
十二、此时还没有创建 Deployment / Service
查看 Deployment:
kubectl get deploy appservice-sample
输出:
Error from server (NotFound): deployments.apps "appservice-sample" not found
查看 Service:
kubectl get svc appservice-sample
输出:
Error from server (NotFound): services "appservice-sample" not found
这是正常的。
因为此时 Controller 还只是 Kubebuilder 默认骨架,Reconcile 里还没有创建 Deployment / Service 的业务逻辑。
十三、编写 Controller import
打开:
vim internal/controller/appservice_controller.go
默认 import 类似:
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
appv1 "github.com/wenjun/appservice-operator/api/v1"
)
为了创建 Deployment 和 Service,需要改成:
import (
"context"
appv1 "github.com/wenjun/appservice-operator/api/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
)
这些包的作用:
| 包 | 作用 |
|---|---|
appsv1 | 创建 Deployment |
corev1 | 创建 Service、PodTemplate、Container |
apierrors | 判断 NotFound |
metav1 | ObjectMeta、LabelSelector |
types | NamespacedName |
intstr | Service TargetPort |
ctrl | controller-runtime 核心包 |
client | Kubernetes API 客户端 |
十四、增加 RBAC 权限
在 Reconcile 方法上面增加 RBAC marker:
// +kubebuilder:rbac:groups=app.nebula.io,resources=appservices,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=app.nebula.io,resources=appservices/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=app.nebula.io,resources=appservices/finalizers,verbs=update
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
这里的意思是:
允许 Controller 读取、监听、更新 AppService
允许 Controller 更新 AppService status
允许 Controller 管理 Deployment
允许 Controller 管理 Service
本地 make run 时使用的是本地 kubeconfig 权限,RBAC 问题不明显。
但后续如果把 Operator 部署到集群里运行,就必须依赖这些 RBAC 权限。
十五、编写 Reconcile 逻辑
Reconcile 是 Operator 最核心的逻辑。
它要做的事情是:
1. 根据 namespace/name 查询 AppService
2. 如果 AppService 不存在,说明被删除了,直接返回
3. 根据 AppService.spec 构造 Deployment
4. 如果 Deployment 不存在,则创建
5. 如果 Deployment 已存在,则检查 replicas、image、port 是否需要更新
6. 根据 AppService.spec 构造 Service
7. 如果 Service 不存在,则创建
8. 如果 Service 已存在,则忽略
第一版代码如下:
func (r *AppServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := logf.FromContext(ctx)
var app appv1.AppService
if err := r.Get(ctx, req.NamespacedName, &app); err != nil {
if apierrors.IsNotFound(err) {
logger.Info("AppService not found, may have been deleted", "name", req.Name, "namespace", req.Namespace)
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
logger.Info("Reconciling AppService", "name", app.Name, "namespace", app.Namespace)
deploy := buildDeployment(&app)
if err := ctrl.SetControllerReference(&app, deploy, r.Scheme); err != nil {
return ctrl.Result{}, err
}
var existingDeploy appsv1.Deployment
if err := r.Get(ctx, types.NamespacedName{Name: deploy.Name, Namespace: deploy.Namespace}, &existingDeploy); err != nil {
if apierrors.IsNotFound(err) {
if err := r.Create(ctx, deploy); err != nil {
if apierrors.IsAlreadyExists(err) {
logger.Info("Deployment already exists", "name", deploy.Name, "namespace", deploy.Namespace)
} else {
return ctrl.Result{}, err
}
} else {
logger.Info("Created Deployment", "name", deploy.Name, "namespace", deploy.Namespace)
}
} else {
return ctrl.Result{}, err
}
} else {
needUpdate := false
if existingDeploy.Spec.Replicas == nil || *existingDeploy.Spec.Replicas != app.Spec.Replicas {
existingDeploy.Spec.Replicas = &app.Spec.Replicas
needUpdate = true
}
if len(existingDeploy.Spec.Template.Spec.Containers) > 0 {
container := &existingDeploy.Spec.Template.Spec.Containers[0]
if container.Image != app.Spec.Image {
container.Image = app.Spec.Image
needUpdate = true
}
if len(container.Ports) > 0 && container.Ports[0].ContainerPort != app.Spec.Port {
container.Ports[0].ContainerPort = app.Spec.Port
needUpdate = true
}
}
if needUpdate {
if err := r.Update(ctx, &existingDeploy); err != nil {
return ctrl.Result{}, err
}
logger.Info("Updated Deployment", "name", existingDeploy.Name, "namespace", existingDeploy.Namespace)
}
}
svc := buildService(&app)
if err := ctrl.SetControllerReference(&app, svc, r.Scheme); err != nil {
return ctrl.Result{}, err
}
var existingSvc corev1.Service
if err := r.Get(ctx, types.NamespacedName{Name: svc.Name, Namespace: svc.Namespace}, &existingSvc); err != nil {
if apierrors.IsNotFound(err) {
if err := r.Create(ctx, svc); err != nil {
if apierrors.IsAlreadyExists(err) {
logger.Info("Service already exists", "name", svc.Name, "namespace", svc.Namespace)
} else {
return ctrl.Result{}, err
}
} else {
logger.Info("Created Service", "name", svc.Name, "namespace", svc.Namespace)
}
} else {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
这里我额外处理了:
apierrors.IsAlreadyExists(err)
因为实际测试时出现过这种日志:
Reconciler error ... services "appservice-sample" already exists
原因通常是 Reconcile 被多次触发,或者创建 Service 后又因为资源变化重新进入 Reconcile。Controller 逻辑应该保持幂等,所以对 AlreadyExists 应该容忍,而不是直接认为失败。
十六、构造 Deployment
增加 helper 方法:
func buildDeployment(app *appv1.AppService) *appsv1.Deployment {
labels := map[string]string{
"app": app.Name,
}
replicas := app.Spec.Replicas
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: app.Name,
Namespace: app.Namespace,
Labels: labels,
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{
MatchLabels: labels,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: app.Name,
Image: app.Spec.Image,
Ports: []corev1.ContainerPort{
{
Name: "http",
ContainerPort: app.Spec.Port,
},
},
},
},
},
},
},
}
}
这里做了几件事:
Deployment 名字 = AppService 名字
Deployment namespace = AppService namespace
Pod label = app: AppService 名字
副本数来自 spec.replicas
镜像来自 spec.image
容器端口来自 spec.port
十七、构造 Service
增加 helper 方法:
func buildService(app *appv1.AppService) *corev1.Service {
labels := map[string]string{
"app": app.Name,
}
return &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: app.Name,
Namespace: app.Namespace,
Labels: labels,
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeClusterIP,
Selector: labels,
Ports: []corev1.ServicePort{
{
Name: "http",
Port: app.Spec.Port,
TargetPort: intstr.FromInt(int(app.Spec.Port)),
},
},
},
}
}
Service 的 selector 是:
selector:
app: appservice-sample
Deployment 生成的 Pod label 也是:
labels:
app: appservice-sample
所以 Service 可以选中对应 Pod。
十八、设置 Watch 关系
修改 SetupWithManager:
func (r *AppServiceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&appv1.AppService{}).
Owns(&appsv1.Deployment{}).
Owns(&corev1.Service{}).
Named("appservice").
Complete(r)
}
这里的意思是:
监听 AppService
同时监听该 AppService 拥有的 Deployment
同时监听该 AppService 拥有的 Service
关键在于前面的:
ctrl.SetControllerReference(&app, deploy, r.Scheme)
ctrl.SetControllerReference(&app, svc, r.Scheme)
它会给 Deployment / Service 加上 ownerReference。
这样有两个好处:
1. AppService 删除后,Deployment / Service 可以被 Kubernetes GC 自动回收;
2. Deployment / Service 发生变化时,也能反向触发 AppService 的 Reconcile。
十九、重新运行 Controller
先停止之前的 make run:
Ctrl + C
重新生成:
make manifests
重新运行:
make run
启动日志:
INFO setup Starting manager
INFO starting server {"name": "health probe", "addr": "[::]:8081"}
INFO Starting EventSource {"source": "kind source: *v1.Service"}
INFO Starting EventSource {"source": "kind source: *v1.AppService"}
INFO Starting EventSource {"source": "kind source: *v1.Deployment"}
INFO Starting Controller
INFO Starting workers
这里可以看到,Controller 已经监听了:
AppService
Deployment
Service
二十、创建 AppService 并验证
重新创建 AppService:
kubectl delete appservice appservice-sample --ignore-not-found
kubectl apply -f config/samples/app_v1_appservice.yaml
输出:
appservice.app.nebula.io/appservice-sample created
查看 AppService:
kubectl get appservice
输出:
NAME AGE
appservice-sample 6s
查看 Deployment:
kubectl get deploy appservice-sample
输出:
NAME READY UP-TO-DATE AVAILABLE AGE
appservice-sample 2/2 2 0 20s
查看 Service:
kubectl get svc appservice-sample
输出:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
appservice-sample ClusterIP 10.43.83.8280/TCP 30s
查看 Pod:
kubectl get pod -l app=appservice-sample
输出:
NAME READY STATUS RESTARTS AGE
appservice-sample-75dbd48454-lnffg 1/1 Running 0 56s
appservice-sample-75dbd48454-mxtbm 1/1 Running 0 56s
这说明 AppService Operator 已经成功完成了:
AppService
↓
Deployment
↓
ReplicaSet
↓
Pod
↓
Service
二十一、运行日志分析
Controller 日志中可以看到:
Reconciling AppService
Created Deployment
Created Service
也可以看到多次:
Reconciling AppService
这是正常的。
因为 Reconcile 会被多种事件触发:
AppService 创建
Deployment 创建
Service 创建
Pod 状态变化
Deployment 状态变化
Operator 的核心要求是:
Reconcile 必须是幂等的。
也就是说,同一个 AppService 被 Reconcile 多次,最终结果都应该是稳定的:
Deployment 存在就不重复创建
Service 存在就不重复创建
字段不一致才更新
字段一致就不处理
这也是为什么要处理:
apierrors.IsNotFound(err)
apierrors.IsAlreadyExists(err)
二十二、本次踩坑总结
1. 端口 8081 被占用
报错:
error listening on :8081: bind: address already in use
排查:
lsof -i :8081
处理:
kill -9
也可以换端口启动:
go run ./cmd/main.go --health-probe-bind-address=:18081
2. sample 里 spec 为空
报错:
The AppService "appservice-sample" is invalid: spec: Required value
原因:
spec:
# TODO(user): Add fields here
解决:
spec:
image: nginx:1.25
replicas: 2
port: 80
3. unknown field spec.image
报错:
unknown field "spec.image", unknown field "spec.port", unknown field "spec.replicas"
原因:
YAML 里写了字段
但是 AppServiceSpec 里没有定义字段
CRD schema 里也没有这些字段
解决:
修改 api/v1/appservice_types.go
执行 make manifests
执行 make install
重新 apply sample
4. Service already exists
报错:
services "appservice-sample" already exists
原因:
Reconcile 可能被多次触发
第一次创建 Service 成功
后续某次 Reconcile 又尝试创建
解决:
if err := r.Create(ctx, svc); err != nil {
if apierrors.IsAlreadyExists(err) {
logger.Info("Service already exists", "name", svc.Name, "namespace", svc.Namespace)
} else {
return ctrl.Result{}, err
}
}
更进一步可以使用 controllerutil.CreateOrUpdate 简化创建和更新逻辑。
二十三、当前项目完成度
当前已经完成:
1. 使用 Kubebuilder 初始化 Operator 项目
2. 定义 AppService CRD
3. 定义 spec.image / spec.replicas / spec.port
4. 安装 CRD 到 K3s 集群
5. 编写 Controller Reconcile 逻辑
6. 根据 AppService 自动创建 Deployment
7. 根据 AppService 自动创建 Service
8. 设置 ownerReference
9. 监听 AppService / Deployment / Service
10. 成功拉起 nginx 双副本 Pod
目前还没做:
1. status 状态回写
2. conditions 设计
3. finalizer 删除前清理
4. Deployment 更新策略完善
5. Service 端口变更处理
6. ConfigMap / Secret 注入
7. Ingress 生成
8. 镜像发布状态观测
9. 单元测试和 envtest
10. Operator 镜像构建和集群内部署
二十四、下一步规划
下一阶段可以继续增强:
1. 增加 status 回写
目标:
status:
phase: Ready
readyReplicas: 2
message: All replicas are ready
这样用户可以直接通过:
kubectl get appservice appservice-sample -o yaml
看到业务应用状态。
2. 增加 conditions
比单纯的 phase 更标准:
status:
conditions:
- type: Available
status: "True"
reason: DeploymentAvailable
message: Deployment is available
3. 使用 controllerutil.CreateOrUpdate
当前代码手动判断:
Get
NotFound
Create
Exists
Update
后面可以优化成:
controllerutil.CreateOrUpdate(ctx, r.Client, deploy, func() error {
// mutate deployment
return ctrl.SetControllerReference(&app, deploy, r.Scheme)
})
这样代码更清晰,也更符合 controller-runtime 常见写法。
4. 增加 Finalizer
用于删除前清理:
AppService 删除
↓
deletionTimestamp 非空
↓
执行外部清理逻辑
↓
移除 finalizer
↓
资源真正删除
5. 和自己的平台结合
后续可以逐步从 demo 变成平台能力:
AppService CRD
↓
Deployment
Service
Ingress
ConfigMap
Secret
HPA
Canary
NetworkProfile
最终可以把平台里的应用发布模型 Kubernetes 原生化。
标题:从零开始写一个 Kubernetes Operator:基于 Kubebuilder 实现 AppService Operator
作者:awen100
地址:https://blog.douwen.top/articles/2026/07/06/1783351110816.html