使用 Operator SDK 为 Pod 标签编写Controller
关 注 微 信 公 众 号 《 云 原 生 C T O 》 更 多 云 原 生 干 货 等 你 来 探 索
专 注 于 云原生技术
分 享
提 供 优 质 云原生开发
视 频 技 术 培 训
面试技巧
, 及 技术疑难问题
解答
云 原 生 技 术 分 享 不 仅 仅 局 限 于 Go
、 Rust
、 Python
、 Istio
、 containerd
、 CoreDNS
、 Envoy
、 etcd
、 Fluentd
、 Harbor
、 Helm
、 Jaeger
、 Kubernetes
、 Open Policy Agent
、 Prometheus
、 Rook
、 TiKV
、 TUF
、 Vitess
、 Arg
o
、 Buildpacks
、 CloudEvents
、 CNI
、 Contour
、 Cortex
、 CRI-O
、 Falco
、 Flux
、 gRPC
、 KubeEdge
、 Linkerd
、 NATS
、 Notary
、 OpenTracing
、 Operator Framework
、 SPIFFE
、 SPIRE
和 Thanos
等
使用 Operator SDK 为 Pod 标签编写Controller
Operator
被证明是在 Kubernetes
中运行有状态分布式应用程序的绝佳解决方案。 Operator SDK
等开源工具提供了构建可靠且可维护的 Operator
的方法,使扩展 Kubernetes
和实现自定义调度变得更加容易。
Kubernetes operator
在您的集群内运行复杂的软件。开源社区已经为 Prometheus
、 Elasticsearch
或 Argo CD
等分布式应用程序构建了许多 operator
。即使在开源之外,运维人员也可以帮助为您的 Kubernetes
集群带来新功能。
operator
是一组自定义资源和一组控制器。控制器监视 Kubernetes API
中特定资源的更改,并通过创建、更新或删除资源做出反应。
Operator SDK
最适合构建功能齐全的 Operator
。尽管如此,您可以使用它来编写单个控制器。这篇文章将引导您在 Go
中编写一个 Kubernetes
控制器,该控制器将为 pod-name
具有特定注释的 pod
添加标签。
为什么我们需要一个控制器呢?
我最近在一个项目中工作,我们需要创建一个服务,将流量路由到 ReplicaSet
中的特定 Pod
。问题是一个 Service
只能按标签选择 pod
,而 ReplicaSet
中的所有 pod
都具有相同的标签。有两种方法可以解决这个问题:
创建没有选择器的服务并直接管理该服务的端点或端点切片。我们需要编写一个自定义控制器来将 Pod
的 IP
地址插入到这些资源中。
为 Pod
添加一个具有唯一值的标签。然后我们可以在我们的服务选择器中使用这个标签。同样,我们需要编写一个自定义控制器来添加这个标签。
控制器是跟踪一个或多个 Kubernetes
资源类型的控制循环。上面选项 n°2
中的控制器只需要跟踪 pod
,这使其更易于实现。这是我们将通过编写一个 Kubernetes
控制器来 pod-name
为我们的 pod
添加标签的选项。
StatefulSets
通过为集合中的每个 Pod
添加 标签来本地执行此操作。 pod-name
但是如果我们不想或不能使用 StatefulSets
怎么办?
我们很少直接创建 pod
;大多数情况下,我们使用 Deployment
、 ReplicaSet
或其他高级资源。我们可以在 PodSpec
中指定要添加到每个 Pod
的标签,但不能使用动态值,因此无法复制 StatefulSet
的 pod-name
标签。
我们尝试使用 mutating admission webhook
。当任何人创建 Pod
时, webhook
会使用包含 Pod
名称的标签来修补 Pod
。令人失望的是,这不起作用:并非所有 pod
在创建之前都有名称。例如,当 ReplicaSet
控制器创建 Pod
时,它会向 namePrefixKubernetes API
服务器发送 a
而不是 name
. API
服务器在将新 Pod
持久化到 etcd
之前生成一个唯一名称,但仅在调用我们的 admission webhook
之后。所以在大多数情况下,我们无法通过 mutating webhook
知道 Pod
的名称。
一旦 Pod
存在于 Kubernetes API
中,它大部分是不可变的,但我们仍然可以添加标签。我们甚至可以从命令行这样做:
kubectl label my-pod my-label-key=my-label-value
我们需要观察 Kubernetes API
中任何 pod
的变化并添加我们想要的标签。与其手动执行此操作,我们将编写一个控制器来为我们执行此操作。
使用 Operator SDK 引导控制器
控制器是一个协调循环,它从 Kubernetes API
读取资源的所需状态,并采取措施使集群的实际状态更接近所需状态。
为了尽快编写此控制器,我们将使用 Operator SDK
。如果您没有安装它,请按照 官方文档。
$ operator-sdk version operator-sdk version: "v1.4.2", commit: "4b083393be65589358b3e0416573df04f4ae8d9b", kubernetes version: "v1.19.4", go version: "go1.15.8", GOOS: "darwin", GOARCH: "amd64"
让我们创建一个新目录来写入我们的控制器:
mkdir label-operator && cd label-operator
接下来,让我们初始化一个新的运算符,我们将向其中添加一个控制器。为此,您需要指定域和存储库。域作为您的自定义 Kubernetes
资源所属的组的前缀。因为我们不会定义自定义资源,所以域无关紧要。存储库将是我们要编写的 Go
模块的名称。按照惯例,这是您将存储代码的存储库。
例如,这是我运行的命令:
# Feel free to change the domain and repo values. operator-sdk init --domain=padok.fr --repo=github.com/busser/label-operator
接下来,我们需要创建一个新的控制器。此控制器将处理 pod
而不是自定义资源,因此无需生成资源代码。让我们运行这个命令来搭建我们需要的代码:
operator-sdk create api --group=core --version=v1 --kind=Pod --controller=true --resource=false
我们现在有一个新文件: controllers/pod_controller.go
. 该文件包含一个 PodReconciler
类型,其中包含我们需要实现的两个方法。第一个是 Reconcile
,现在看起来像这样:
func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = r.Log.WithValues("pod", req.NamespacedName) // your logic here return ctrl.Result{}, nil }
Reconcile
每当创建、更新或删除 Pod
时都会调用该方法。 Pod
的名称和命名空间在 ctrl.Request
作为参数接收的方法中。
第二种方法是 SetupWithManager
,现在它看起来像这样:
func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). // Uncomment the following line adding a pointer to an instance of the controlled resource as an argument // For(). Complete(r) }
该 SetupWithManager
方法在 operator
启动时被调用。它用于告诉 operator
框架我们 PodReconciler
需要观察哪些类型。要使用 Kubernetes
内部使用的相同 Pod
类型,我们需要导入它的一些代码。所有 Kubernetes
源代码都是开源的,因此您可以在自己的 Go
代码中导入您喜欢的任何部分。您可以在 Kubernetes
源代码中或在 pkg.go.dev
上找到可用包的完整列表。要使用 pod
,我们需要 k8s.io/api/core/v1
包。
package controllers import ( // other imports... corev1 "k8s.io/api/core/v1" // other imports... )
让我们使用 Podin
类型 SetupWithManager
告诉 operator
框架我们要监视 pod
:
func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&corev1.Pod{}). Complete(r) }
在继续之前,我们应该设置控制器需要的 RBAC
权限。在方法之上 Reconcile
,我们有一些默认权限:
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=core,resources=pods/status,verbs=get;update;patch // +kubebuilder:rbac:groups=core,resources=pods/finalizers,verbs=update
我们不需要所有这些。我们的控制器永远不会与 Pod
的状态或其终结器交互。它只需要读取和更新 pod
。让我们删除不必要的权限,只保留我们需要的:
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;update;patch
我们现在准备编写控制器的协调逻辑。
协调逻辑
这是我们希望我们的 Reconcile
方法执行的操作:
-
使用
Pod
的名称和命名空间从ctrl.RequestKubernetes API
获取Pod
。 -
如果
Pod
有add-pod-name-label
注解,pod-name
则给Pod
添加标签;如果注释丢失,请不要添加标签。 -
更新
Kubernetes API
中的Pod
以保留所做的更改。 让我们为注解和标签定义一些常量:
const ( addPodNameLabelAnnotation = "padok.fr/add-pod-name-label" podNameLabel = "padok.fr/pod-name" )
我们协调功能的第一步是从 Kubernetes API
获取我们正在处理的 Pod
:
// Reconcile handles a reconciliation request for a Pod. // If the Pod has the addPodNameLabelAnnotation annotation, then Reconcile // will make sure the podNameLabel label is present with the correct value. // If the annotation is absent, then Reconcile will make sure the label is too. func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("pod", req.NamespacedName) /* Step 0: Fetch the Pod from the Kubernetes API. */ var pod corev1.Pod if err := r.Get(ctx, req.NamespacedName, &pod); err != nil { log.Error(err, "unable to fetch Pod") return ctrl.Result{}, err } return ctrl.Result{}, nil }
Reconcile
创建、更新或删除 Pod
时将调用我们的方法。在删除的情况下,我们的调用 r.Get
将返回一个特定的错误。让我们导入定义此错误的包:
package controllers import ( // other imports... apierrors "k8s.io/apimachinery/pkg/api/errors" // other imports... )
我们现在可以处理这个特定的错误,并且——因为我们的控制器不关心已删除的 pod
——明确地忽略它:
/* Step 0: Fetch the Pod from the Kubernetes API. */ var pod corev1.Pod if err := r.Get(ctx, req.NamespacedName, &pod); err != nil { if apierrors.IsNotFound(err) { // we'll ignore not-found errors, since we can get them on deleted requests. return ctrl.Result{}, nil } log.Error(err, "unable to fetch Pod") return ctrl.Result{}, err }
接下来,让我们编辑我们的 Pod
,以便当且仅当我们的注解存在时,我们的动态标签才存在:
/* Step 1: Add or remove the label. */ labelShouldBePresent := pod.Annotations[addPodNameLabelAnnotation] == "true" labelIsPresent := pod.Labels[podNameLabel] == pod.Name if labelShouldBePresent == labelIsPresent { // The desired state and actual state of the Pod are the same. // No further action is required by the operator at this moment. log.Info("no update required") return ctrl.Result{}, nil } if labelShouldBePresent { // If the label should be set but is not, set it. if pod.Labels == nil { pod.Labels = make(map[string]string) } pod.Labels[podNameLabel] = pod.Name log.Info("adding label") } else { // If the label should not be set but is, remove it. delete(pod.Labels, podNameLabel) log.Info("removing label") }
最后,让我们将更新后的 Pod
推送到 Kubernetes API
:
/* Step 2: Update the Pod in the Kubernetes API. */ if err := r.Update(ctx, &pod); err != nil { log.Error(err, "unable to update Pod") return ctrl.Result{}, err }
将更新后的 Pod
写入 Kubernetes API
时,存在自我们第一次读取 Pod
以来已更新或删除的风险。在编写 Kubernetes
控制器时,我们应该记住,我们不是集群中唯一的参与者。发生这种情况时,最好的办法是通过重新排队事件从头开始协调。让我们这样做:
/* Step 2: Update the Pod in the Kubernetes API. */ if err := r.Update(ctx, &pod); err != nil { if apierrors.IsConflict(err) { // The Pod has been updated since we read it. // Requeue the Pod to try to reconciliate again. return ctrl.Result{Requeue: true}, nil } if apierrors.IsNotFound(err) { // The Pod has been deleted since we read it. // Requeue the Pod to try to reconciliate again. return ctrl.Result{Requeue: true}, nil } log.Error(err, "unable to update Pod") return ctrl.Result{}, err }
让我们记住在方法结束时成功返回:
return ctrl.Result{}, nil }
就是这样!我们现在准备在我们的集群上运行控制器。
在集群上运行控制器
要在您的集群上运行我们的控制器,我们需要运行 operator
。为此,您只需要 kubectl
. 如果您手头没有 Kubernetes
集群,我建议您使用 KinD (Kubernetes in Docker)
在本地启动一个。
从您的机器上运行 operator
所需要的只是以下命令:
make run
几秒钟后,您应该会看到 operator
的日志。请注意,我们的控制器 Reconcile
方法已为集群中已经运行的所有 Pod
调用。
让我们让 oeprator
继续运行,并在另一个终端中创建一个新 Pod
:
kubectl run --image=nginx my-nginx
operator
应该快速打印一些日志,表明它对 Pod
的创建和随后的状态变化做出了反应:
INFO controllers.Pod no update required {"pod": "default/my-nginx"} INFO controllers.Pod no update required {"pod": "default/my-nginx"} INFO controllers.Pod no update required {"pod": "default/my-nginx"} INFO controllers.Pod no update required {"pod": "default/my-nginx"}
让我们检查 Pod
的标签:
$ kubectl get pod my-nginx --show-labels NAME READY STATUS RESTARTS AGE LABELS my-nginx 1/1 Running 0 11m run=my-nginx
让我们为 Pod
添加一个注解,以便我们的控制器知道向它添加我们的动态标签:
kubectl annotate pod my-nginx padok.fr/add-pod-name-label=true
请注意,控制器立即做出反应并在其日志中生成了一个新行:
INFO controllers.Pod adding label {"pod": "default/my-nginx"} $ kubectl get pod my-nginx --show-labels NAME READY STATUS RESTARTS AGE LABELS my-nginx 1/1 Running 0 13m padok.fr/pod-name=my-nginx,run=my-nginx
太棒了!您刚刚成功编写了一个 Kubernetes
控制器,该控制器能够为集群中的资源添加具有动态值的标签。
控制器和 operator
,无论大小,都可以成为您 Kubernetes
之旅的重要组成部分。现在编写 operator
比以往任何时候都容易。可能性是无止境。
接下来是什么?
如果您想更进一步,我建议您首先在集群中部署控制器或 operator
。 Operator SDK
生成的 Makefile
将完成大部分工作。
将 operator
部署到生产环境时,实施稳健的测试始终是一个好主意。朝着这个方向迈出的第一步是编写单元测试。 本文档将指导您为 operator
编写测试。我为我们刚刚编写的 operator
编写了测试;你可以在这个 GitHub
存储库中找到我的所有代码。
http://github.com/busser/label-operator
如何学习更多?
Operator SDK
文档详细介绍了如何进一步实现更复杂的 operator
。
Operator SDK
文档: http://sdk.operatorframework.io/docs/
在对更复杂的用例进行建模时,作用于内置 Kubernetes
类型的单个控制器可能还不够。您可能需要使用自定义资源定义 (CRD)
和多个控制器构建更复杂的 operator
。 Operator SDK
是一个很好的工具,可以帮助您做到这一点。
如果您想讨论构建 operator
,请加入 Kubernetes Slack
工作区中的 #kubernetes-operator
频道!
http://slack.k8s.io/
http://kubernetes.slack.com/messages/kubernetes-operators
更多文档
请关注微信公众号 云原生CTO [1]
参考资料
参考地址: http://kubernetes.io/blog/2021/06/21/writing-a-controller-for-pod-labels/
- Go 中的 Kubernetes GraphQL 动态查询
- 揭开云原生数据管理的神秘面纱:操作层级
- 云原生数据库,激活数智创新之力
- 企业考虑云原生分布式数据库的三个原因
- 一组用于 Kubernetes 的现代 Grafana 仪表板
- Go 中的构建器模式
- 让我们使用 Go 实现基本的服务发现
- 云原生下一步的发展方向是什么?
- 用更云原生的方式做诊断|大规模 K8s 集群诊断利器深度解析
- 多个维度分析k8s多集群管理工具,到底哪个才真正适合你
- 使用 Kube-capacity CLI 查看 Kubernetes 资源请求、限制和利用率
- 使用 Go 在 Kubernetes 中构建自己的准入控制器
- 云原生数仓如何破解大规模集群的关联查询性能问题?
- 云原生趋势下的迁移与灾备思考
- 2022 年不容错过的六大云原生趋势!
- 使用 Prometheus 监控 Golang 应用程序
- 云原生时代下的机遇与挑战 DevOps如何破局
- 如何在云原生格局中理解Kubernetes合规性和安全框架
- 设计云原生应用程序的15条基本原则
- 使用 Operator SDK 为 Pod 标签编写Controller