Kubernetes 自定义 CRD 开发完整指南:从零开发 ConfigMap 热更新 Operator
·
以下是完整的 ConfigMap 热更新 Operator 开发步骤和代码实现,从零开始手把手带你完成。我们将使用 Operator SDK(Kubebuilder 风格) 和 Go 语言 开发。
环境准备
-
安装工具:
# 1. 安装 Operator SDK curl -LO https://github.com/operator-framework/operator-sdk/releases/download/v1.28.0/operator-sdk_linux_amd64 chmod +x operator-sdk_linux_amd64 sudo mv operator-sdk_linux_amd64 /usr/local/bin/operator-sdk # 2. 安装 kubebuilder(可选,Operator SDK 已集成) # 3. 确保已安装 Go (1.24+) 和 Kubernetes 集群(如 Minikube) -
验证工具:
operator-sdk version # 输出类似:operator-sdk version: "v1.28.0"
步骤 1:初始化 Operator 项目
# 创建项目目录
mkdir configmap-reload-operator && cd configmap-reload-operator
# 初始化项目(使用 Kubebuilder 风格)
operator-sdk init \
--domain example.com \
--repo github.com/your-username/configmap-reload-operator \
--project-name configmap-reload-operator
# 创建 API 和 Controller(CRD + 控制器)
operator-sdk create api \
--group ops \
--version v1 \
--kind ConfigMapReload \
--resource \
--controller
步骤 2:定义 CRD 结构
编辑 api/v1/configmapreload_types.go:
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// ConfigMapReloadSpec 定义用户期望的状态
type ConfigMapReloadSpec struct {
// 需要监听的 ConfigMap 名称
ConfigMapName string `json:"configMapName"`
// ConfigMap 所在的命名空间(默认与 CR 相同)
// +optional
ConfigMapNamespace string `json:"configMapNamespace,omitempty"`
// 关联的工作负载类型(Deployment/StatefulSet)
WorkloadType string `json:"workloadType"`
// 关联的工作负载名称
WorkloadName string `json:"workloadName"`
}
// ConfigMapReloadStatus 定义观测到的状态
type ConfigMapReloadStatus struct {
// 最后一次触发 reload 的时间
// +optional
LastReloadTime string `json:"lastReloadTime,omitempty"`
// ConfigMap 内容的哈希值(用于检测变化)
// +optional
LastConfigMapHash string `json:"lastConfigMapHash,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// ConfigMapReload 是 CRD 的定义
type ConfigMapReload struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigMapReloadSpec `json:"spec,omitempty"`
Status ConfigMapReloadStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// ConfigMapReloadList 是 ConfigMapReload 的列表
type ConfigMapReloadList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ConfigMapReload `json:"items"`
}
func init() {
SchemeBuilder.Register(&ConfigMapReload{}, &ConfigMapReloadList{})
}
步骤 3:实现 Controller 逻辑
编辑 controllers/configmapreload_controller.go:
package controllers
import (
"context"
"crypto/sha256"
"fmt"
"time"
"github.com/go-logr/logr"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
opsv1 "git.woa.com/devcloud/test-env/configmap-reload-operator/api/v1"
)
// ConfigMapReloadReconciler reconciles a ConfigMapReload object
type ConfigMapReloadReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=ops.example.com,resources=configmapreloads,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=ops.example.com,resources=configmapreloads/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;update;patch
// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch
func (r *ConfigMapReloadReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
if r.Log.GetSink() == nil {
r.Log = ctrl.Log.WithName("controllers").WithName("ConfigMapReload")
}
log := r.Log.WithValues("ConfigMapReload", req.NamespacedName)
// 1. 获取 ConfigMapReload 实例
reloader := &opsv1.ConfigMapReload{}
if err := r.Get(ctx, req.NamespacedName, reloader); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 2. 获取目标 ConfigMap
configMapNamespace := reloader.Spec.ConfigMapNamespace
if configMapNamespace == "" {
configMapNamespace = reloader.Namespace // 默认使用 CR 的命名空间
}
configMap := &corev1.ConfigMap{}
configMapKey := client.ObjectKey{
Namespace: configMapNamespace,
Name: reloader.Spec.ConfigMapName,
}
if err := r.Get(ctx, configMapKey, configMap); err != nil {
log.Error(err, "无法获取 ConfigMap")
return ctrl.Result{}, err
}
// 3. 计算 ConfigMap 内容的哈希值
currentHash := computeConfigMapHash(configMap.Data)
// 4. 如果哈希值未变化,则无需处理
if reloader.Status.LastConfigMapHash == currentHash {
return ctrl.Result{}, nil
}
// 5. 获取关联的 Deployment
deployment := &appsv1.Deployment{}
deploymentKey := client.ObjectKey{
Namespace: reloader.Namespace,
Name: reloader.Spec.WorkloadName,
}
if err := r.Get(ctx, deploymentKey, deployment); err != nil {
log.Error(err, "无法获取 Deployment")
return ctrl.Result{}, err
}
// 6. 修改 Deployment 的 Annotation 以触发滚动重启
if deployment.Spec.Template.ObjectMeta.Annotations == nil {
deployment.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
}
deployment.Spec.Template.ObjectMeta.Annotations["configmap.reload/trigger"] = currentHash
if err := r.Update(ctx, deployment); err != nil {
log.Error(err, "更新 Deployment 失败")
return ctrl.Result{}, err
}
// 7. 更新 Status
reloader.Status.LastConfigMapHash = currentHash
reloader.Status.LastReloadTime = time.Now().Format(time.RFC3339)
if err := r.Status().Update(ctx, reloader); err != nil {
log.Error(err, "更新 Status 失败")
return ctrl.Result{}, err
}
log.Info("成功触发滚动重启",
"configMap", configMap.Name,
"deployment", deployment.Name,
"hash", currentHash)
return ctrl.Result{}, nil
}
// 计算 ConfigMap 数据的哈希值
func computeConfigMapHash(data map[string]string) string {
hasher := sha256.New()
for k, v := range data {
hasher.Write([]byte(k + v))
}
return fmt.Sprintf("%x", hasher.Sum(nil))
}
// 将 ConfigMap 事件映射到关联的 ConfigMapReload
func (r *ConfigMapReloadReconciler) mapConfigMapToReloader(obj client.Object) []reconcile.Request {
configMap := obj.(*corev1.ConfigMap)
var reloaders opsv1.ConfigMapReloadList
if err := r.List(context.TODO(), &reloaders); err != nil {
return []reconcile.Request{}
}
requests := []reconcile.Request{}
for _, reloader := range reloaders.Items {
configMapNamespace := reloader.Spec.ConfigMapNamespace
if configMapNamespace == "" {
configMapNamespace = reloader.Namespace
}
if reloader.Spec.ConfigMapName == configMap.Name &&
configMapNamespace == configMap.Namespace {
requests = append(requests, reconcile.Request{
NamespacedName: client.ObjectKey{
Namespace: reloader.Namespace,
Name: reloader.Name,
},
})
}
}
return requests
}
func (r *ConfigMapReloadReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&opsv1.ConfigMapReload{}).
Watches(
&source.Kind{Type: &corev1.ConfigMap{}},
handler.EnqueueRequestsFromMapFunc(r.mapConfigMapToReloader),
).
Complete(r)
}
编辑main.go函数,添加日志初始化
if err = (&controllers.ConfigMapReloadReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Log: ctrl.Log.WithName("controllers").WithName("ConfigMapReload"), // 必须初始化
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ConfigMapReload")
os.Exit(1)
}
步骤 4:生成 CRD 和安装 Operator
# 生成 CRD 的 YAML 文件
make manifests
# 安装 CRD 到集群
make install
# 本地运行 Operator(测试用)
make run
# 或构建镜像并部署到集群(生产用)
make docker-build docker-push IMG=your-repo/configmap-reload-operator:v1
make deploy IMG=your-repo/configmap-reload-operator:v1
步骤 5:测试 Operator
-
创建示例 Deployment 和 ConfigMap:
# configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: myapp-config data: app.conf: | key1=value1 key2=value2 # deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: myapp spec: replicas: 2 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: containers: - name: nginx image: nginx:alpine volumeMounts: - name: config mountPath: /etc/config volumes: - name: config configMap: name: myapp-configkubectl apply -f configmap.yaml -f deployment.yaml -
创建 ConfigMapReload 实例:
# reloader.yaml apiVersion: ops.example.com/v1 kind: ConfigMapReload metadata: name: myapp-reloader spec: configMapName: myapp-config workloadType: Deployment workloadName: myappkubectl apply -f reloader.yaml -
测试 ConfigMap 更新:
# 修改 ConfigMap kubectl edit configmap myapp-config # 将 key2=value2 改为 key2=value3 # 观察 Pod 重启 kubectl get pods -w # 输出应显示 Pod 被重新创建(AGE 重置)
关键点解析
-
事件监听:
- Controller 同时监听
ConfigMapReloadCR 和ConfigMap资源的变化。 - 通过
mapConfigMapToReloader将 ConfigMap 事件映射到关联的 CR。
- Controller 同时监听
-
滚动重启原理:
- 修改 Deployment 的
spec.template.metadata.annotations会触发 K8s 的滚动更新机制。 - 哈希值(
configmap.reload/trigger)确保每次 ConfigMap 内容变化都会触发更新。
- 修改 Deployment 的
-
幂等性保障:
- 通过比较
LastConfigMapHash避免不必要的重启。
- 通过比较
其他:
为了方便测试,不需要将opeartor服务部署到集群也可以测试
本地执行make run (这里会直接关联到集群上)
通过这个完整的实现,你已经拥有了一个生产可用的 ConfigMap 热更新 Operator!
更多推荐


所有评论(0)