operator-sdk
一、Operator SDK 介绍
Operator SDK 是由 Red Hat 开源的用于构建 Kubernetes Operator 的开发框架。它提供了工具、库和标准化模式,帮助开发者高效、一致地创建、测试和打包能够自动化管理 Kubernetes 上复杂应用程序的 Operator。
Operator SDK 基于 Kubernetes controller-runtime 库构建,并通过与 Operator Lifecycle Manager (OLM) 的无缝集成,为 Operator 特定的工具和抽象提供了扩展支持。
为什么需要 Operator SDK
在 Kubernetes 上部署和管理复杂有状态应用时,常常面临生命周期管理复杂、运维门槛高、自动化程度低等挑战。Operator SDK 通过将运维知识编码为软件、提供声明式 API 以及与 kubectl 的一致体验,有效解决了这些问题。
- 将运维专家的知识编码到软件中
- 提供声明式 API 来管理复杂应用
- 支持使用
kubectl操作自定义资源,保持一致的用户体
官方的kube-operator 只支持Go 开发,operator-sdk 可以支持 Go、ansible、helm开发
- Go-based Operators: 基于 Go 的 Operator 直接使用 controller-runtime,适合复杂业务逻辑和高度自定义需求。
- Ansible-based Operators: Ansible Operator 利用 Ansible playbook 和角色,无需 Go 编程,适合已有自动化脚本的场景。
- Helm-based Operators: Helm Operator 基于现有 Helm charts,适合已有 Helm charts 的应用,快速实现 Operator 化。
二、开发示例
https://github.com/operator-framework/operator-sdk/releases
1、安装
🐳 operator-sdk version
operator-sdk version: "v1.42.2", commit: "6001c29067051e1a04e829ea033988b904d1845e", kubernetes version: "1.33.1", go version: "go1.25.7", GOOS: "darwin", GOARCH: "arm64"
概念:
-
CRD (Custom Resource Definition):定义自定义资源(如
MyApp),扩展 Kubernetes API。 -
Controller (控制器):一个运行在集群内的循环进程,不断对比 期望状态 (Spec) 和 实际状态 (Status),并执行操作使两者一致(Reconcile)。
-
Operator:CRD + Controller 的组合,用于自动化运维复杂应用。
2、初始化
mkdir caddy-operator && cd caddy-operator
operator-sdk init --domain example.com --repo github.com/example/caddy-operator
创建api
operator-sdk create api --group web --version v1 --kind Caddy --resource --controller
# 下载依赖
go mod tidy
该命令会自动生成自定义资源定义(CRD)、控制器逻辑及相关测试文件
项目结构说明
├── api/
│ └── v1alpha1/ # API 定义
├── config/
│ ├── crd/ # CRD 配置
│ ├── default/ # 默认配置
│ ├── manager/ # Manager 配置
│ ├── rbac/ # RBAC 配置
│ └── samples/ # 示例资源
├── controllers/ # 控制器逻辑
├── Dockerfile # 容器镜像构建文件
├── Makefile # 构建和部署命令
├── PROJECT # 项目元数据
└── main.go # 主入口文件
自定义CRD
// api/v1/crab_types.go
type GuestbookSpec struct {
Replicas int32 `json:"replicas"` // 对应 YAML 的 `spec.replicas`
Image string `json:"image"` // 对应 YAML 的 `spec.image`
Port int32 `json:"port"` // 应用监听的端口
SvcPort int32 `json:"svcPort"` // Service 暴露的端口
}
type GuestbookStatus struct {
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
AvailableReplicas int32
}
实现业务逻辑
import (
"context"
webappv1 "github.com/serialt/crab-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/util/intstr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
logf "sigs.k8s.io/controller-runtime/pkg/log"
)
// +kubebuilder:rbac:groups=webapp.web.imau.cc,resources=crabs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=webapp.web.imau.cc,resources=crabs/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=webapp.web.imau.cc,resources=crabs/finalizers,verbs=update
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
func (r *CrabReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := logf.FromContext(ctx)
crab := webappv1.Crab{}
if err := r.Get(ctx, req.NamespacedName, &crab); err != nil {
if apierrors.IsNotFound(err) {
// 资源已被物理删除,无需处理
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
labels := map[string]string{
"app": crab.Name,
}
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: crab.Name,
Namespace: crab.Namespace,
Labels: labels,
}}
if err := controllerutil.SetControllerReference(&crab, deploy, r.Scheme); err != nil {
return ctrl.Result{}, err
}
result, err := controllerutil.CreateOrUpdate(ctx, r.Client, deploy, func() error {
deploy.Spec = appsv1.DeploymentSpec{
Replicas: &crab.Spec.Replicas,
Selector: &metav1.LabelSelector{
MatchLabels: labels,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: crab.Name,
Image: crab.Spec.Image,
Ports: []corev1.ContainerPort{
{
ContainerPort: crab.Spec.Port,
},
},
},
},
},
},
}
// 关键操作:设置 OwnerReference
// 确保删除 Guestbook CR 时,自动清理关联的 Deployment
if err := controllerutil.SetControllerReference(&crab, deploy, r.Scheme); err != nil {
return err
}
return nil
})
if err != nil {
logger.Error(err, "Failed to create or update Deployment")
crab.Status.AvailableReplicas = 0
return ctrl.Result{}, err
}
logger.Info("Deployment操作结果", "result", result)
// 构建svc
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: crab.Name,
Namespace: crab.Namespace,
},
}
result, err = controllerutil.CreateOrUpdate(ctx, r.Client, svc, func() error {
svc.Spec = corev1.ServiceSpec{
Selector: labels,
Ports: []corev1.ServicePort{
{
Port: crab.Spec.SvcPort,
TargetPort: intstr.FromInt(int(crab.Spec.Port)),
},
},
}
return controllerutil.SetControllerReference(&crab, svc, r.Scheme)
})
if err != nil {
logger.Error(err, "Failed to create or update Service")
return ctrl.Result{}, err
}
logger.Info("Service操作结果", "result", result)
return ctrl.Result{}, nil
}
func (r *CrabReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&webappv1.Crab{}).
Owns(&appsv1.Deployment{}).
Owns(&corev1.Service{}).
Complete(r)
}
构建和测试
# 生成 manifests (RBAC, CRD 等)
make manifests
# 本地运行 (开发环境)
make install
make run
# 部署到集群
make deploy IMG=controller:latest
测试yaml
# config/samples/webapp_v1_crab.yaml
apiVersion: webapp.web.imau.cc/v1
kind: Crab
metadata:
labels:
app.kubernetes.io/name: crab-op
app.kubernetes.io/managed-by: kustomize
name: crab-app
namespace: dev
spec:
# TODO(user): Add fields here
replicas: 3
image: registry.cn-hangzhou.aliyuncs.com/serialt/nginx:1.28-alpine
port: 80
svcPort: 8080
# 执行后可以看到3个pod
[serialt@Krab web-op]🐳 kubectl apply -f config/samples/webapp_v1_crab.yaml
生产/集成测试
将控制器打包成镜像部署到集群。
# 设置镜像仓库
# export IMG=registry.cn-hangzhou.aliyuncs.com/serialt/crab-op:v0.0.1
# 当前系统与节点系统相同
make docker-build docker-push IMG=registry.cn-hangzhou.aliyuncs.com/serialt/crab-op:v0.0.1
# 构建多架构镜像
make docker-buildx PLATFORMS=linux/amd64 IMG=registry.cn-hangzhou.aliyuncs.com/serialt/crab-op:v0.0.1
#部署到集群
make deploy IMG=registry.cn-hangzhou.aliyuncs.com/serialt/crab-op:v0.0.1
# 修改replicas
kubectl replace -f config/samples/webapp_v1_crab.yaml
chart 打包
# 使用helmify
go install github.com/arttor/helmify/cmd/helmify@latest
makefile增加
CHART_NAME ?= caddy-operator
CHART_DIR ?= dist/caddy-operator
CHART_VERSION ?= ${VERSION}
APP_VERSION ?= latest
HELM_REGISTRY ?= quay.io/serialt
HELM_REPO ?= $(HELM_REGISTRY)/charts
.PHONY: helmify
helmify: $(HELMIFY)
$(HELMIFY): $(LOCALBIN)
@test -s $(HELMIFY) || \
GOBIN=$(LOCALBIN) go install github.com/arttor/helmify/cmd/helmify@latest
.PHONY: helm
helm: manifests kustomize helmify
rm -rf $(CHART_DIR)
mkdir -p $(dir $(CHART_DIR))
$(KUSTOMIZE) build config/default | helmify $(CHART_DIR)
.PHONY: helm-lint
helm-lint: helm
helm lint $(CHART_DIR)
.PHONY: helm-package
helm-package: helm-lint
mkdir -p dist
helm package $(CHART_DIR) \
--destination dist \
--version $(CHART_VERSION) \
--app-version $(APP_VERSION)
.PHONY: helm-push
helm-push: helm-package
helm push \
dist/$(CHART_NAME)-$(CHART_VERSION).tgz \
oci://$(HELM_REPO)
三、基于 helm
[root@dev ansible-op]# operator-sdk init --plugins=helm --domain=example.com
[root@dev ansible-op]# operator-sdk create api --group=web --version=v1 --kind=Caddy
# 需要复制 kustomize 到 bin/kustomize
# 安装crd
[root@dev ansible-op]# make install
# 构建镜像
[root@dev ansible-op]# make docker-build docker-push IMG=registry.cn-hangzhou.aliyuncs.com/serialt/caddy-op:v0.0.1
# 部署
[root@dev ansible-op]# make deploy IMG=registry.cn-hangzhou.aliyuncs.com/serialt/caddy-op:v0.0.1
# config/samples/web_v1_caddy.yaml
apiVersion: web.imau.cc/v1
kind: Caddy
metadata:
name: caddy
namespace: dev
spec:
replicaCount: 5
image:
repository: registry.cn-hangzhou.aliyuncs.com/serialt/nginx
tag: "1.29.6-alpine"
[root@dev ansible-op]# kubectl apply -f config/samples/web_v1_caddy.yaml
# 查看pod
[root@dev samples]# k get pod -n dev
NAME READY STATUS RESTARTS AGE
caddy-6cd4f4c7d7-6v9xf 1/1 Running 0 17m
caddy-6cd4f4c7d7-bzslv 1/1 Running 0 17m
caddy-6cd4f4c7d7-pqnsw 1/1 Running 0 17m
caddy-6cd4f4c7d7-t47lg 1/1 Running 0 17m
caddy-6cd4f4c7d7-wgqgq 1/1 Running 0 17m
四、基于 ansible
[root@dev ansible-op]# operator-sdk init --plugins=ansible --domain=imau.cc
[root@dev ansible-op]# operator-sdk create api --group=webapp --version=v1 --kind=An --generate-role
# 需要复制 kustomize 到 bin/kustomize
# roles/an/vars/main.yml
image: ""
tag: ""
# roles/an/tasks/main.yml
---
- name: 启动 Nginx 部署
k8s:
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: "{{ ansible_operator_meta.name }}-nginx"
namespace: "{{ ansible_operator_meta.namespace }}"
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: "{{ image | default('nginx') }}:{{ tag | default('latest') }}"
ports:
- containerPort: 80
# 安装crd
[root@dev ansible-op]# make install
# 构建镜像
[root@dev ansible-op]# make docker-build docker-push IMG=registry.cn-hangzhou.aliyuncs.com/serialt/ansible-op:v0.0.1
# 部署
[root@dev ansible-op]# make deploy IMG=registry.cn-hangzhou.aliyuncs.com/serialt/ansible-op:v0.0.1
测试文件
# config/samples/webapp_v1_an.yaml
apiVersion: webapp.imau.cc/v1
kind: An
metadata:
labels:
app.kubernetes.io/name: ansible-op
app.kubernetes.io/managed-by: kustomize
name: an-sample
namespace: dev
spec:
# TODO(user): Add fields here
image: registry.cn-hangzhou.aliyuncs.com/serialt/nginx
tag: 1.29.6-alpine
[root@dev ansible-op]# kubectl apply -f config/samples/webapp_v1_an.yaml
[root@dev ansible-op]# k get an -A
NAMESPACE NAME AGE
dev an-sample 32m
[root@dev ansible-op]# k get pod -n dev
NAME READY STATUS RESTARTS AGE
an-sample-nginx-6bfcf94599-vgj77 1/1 Running 0 18m