前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >为Pod标签编写一个控制器

为Pod标签编写一个控制器

作者头像
CNCF
发布2021-07-07 16:14:28
7820
发布2021-07-07 16:14:28
举报
文章被收录于专栏:CNCF

作者:Arthur Busser(Padok)

Operators[1](操作器)被证明是在 Kubernetes 中运行有状态分布式应用程序的优秀解决方案。Operator SDK[2]等开源工具提供了构建可靠和可维护的操作器的方法,使扩展 Kubernetes 和实现自定义调度变得更容易。

Kubernetes 操作器在集群中运行复杂的软件。开源社区已经为 Prometheus、Elasticsearch 或 Argo CD 等分布式应用构建了许多操作器[3]。即使在开源之外,操作器也可以帮助你为 Kubernetes 集群带来新的功能。

操作器是一组自定义资源[4]和一组控制器[5]。控制器监视 Kubernetes API 中特定资源的变化,并通过创建、更新或删除资源来做出反应。

Operator SDK 最适合构建功能齐全的操作器。尽管如此,你可以使用它来编写单个控制器。这篇文章将带领你在 Go 中编写一个 Kubernetes 控制器,它将为拥有特定注释的 pod 添加一个 pod-name 标签。

为什么需要这样的一个控制器呢?

我最近在一个项目中工作,我们需要创建一个服务,将流量路由到一个 ReplicaSet 中的特定 Pod。问题是服务只能根据标签选择 pod,而 ReplicaSet 中的所有 pod 都有相同的标签。有两种方法可以解决这个问题:

  1. 创建一个没有选择器的服务,并直接管理该服务的 Endpoint 或 EndpointSlice。我们需要编写一个自定义控制器来将 Pod 的 IP 地址插入到这些资源中。
  2. 为 Pod 添加一个具有独特值的标签。然后,我们可以在 Service 的选择器中使用这个标签。同样,我们需要编写一个自定义控制器来添加这个标签。

控制器是跟踪一个或多个 Kubernetes 资源类型的控制循环。上面选项 2 的控制器只需要跟踪 pod,这使得它更容易实现。这是我们将要通过编写一个 Kubernetes 控制器来向我们的 pod 添加一个 pod-name 标签来完成的选项。

StatefulSet 通过向集合中的每个 Pod 添加一个 Pod 名称标签来实现这一点[6]。但如果我们不想或不能使用 StatefulSet 呢?

我们很少直接创建 pod;通常,我们使用 Deployment、ReplicaSet 或其他高级资源。我们可以在 PodSpec 中指定要添加到每个 Pod 的标签,但不能使用动态值,因此没有办法复制 StatefulSet 的 pod-name 标签。

我们试过用mutating admission webhook[7]。当任何人创建一个 Pod,webhook 补丁 Pod 与一个标签包含的名称。令人失望的是,这并不奏效:并不是所有的 pod 在创建之前都有一个名称。例如,当 ReplicaSet 控制器创建一个 Pod 时,它发送一个 namePrefix 给 Kubernetes API 服务器,而不是一个名称。API 服务器在将新的 Pod 持久化到 etcd 之前会生成一个唯一的名称,但只有在调用我们的 admission webhook 之后。所以在大多数情况下,我们无法通过 mutating webhook 知道一个 Pod 的名字。

一旦 Pod 存在于 Kubernetes API 中,它基本上是不可变的,但我们仍然可以添加一个标签。我们甚至可以在命令行中这样做:

代码语言:javascript
复制
kubectl label my-pod my-label-key=my-label-value

我们需要关注 Kubernetes API 中任何 pod 的变化,并添加我们想要的标签。我们将编写一个控制器来代替手动操作。

使用 Operator SDK 引导控制器

控制器是一个协调循环,它从 Kubernetes API 中读取资源的期望状态,并采取行动使集群的实际状态更接近期望状态。

为了尽可能快地编写这个控制器,我们将使用 Operator SDK。如果你没有安装它,请遵循官方文档[8]

代码语言:javascript
复制
$ 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"

让我们创建一个新目录来写入控制器:

代码语言:javascript
复制
mkdir label-operator && cd label-operator

接下来,让我们初始化一个新操作器,我们将向其添加一个控制器。为此,你需要指定一个域和一个存储库。域作为自定义 Kubernetes 资源所属组的前缀。因为我们不打算定义自定义资源,所以域并不重要。存储库将是我们将要编写的 Go 模块的名称。按照惯例,这是存储代码的存储库。

作为一个示例,下面是我运行的命令:

代码语言:javascript
复制
# Feel free to change the domain and repo values.
operator-sdk init --domain=padok.fr --repo=github.com/busser/label-operator

接下来,我们需要创建一个新的控制器。这个控制器将处理 pod 而不是自定义资源,所以不需要生成资源代码。让我们运行这个命令来构建我们需要的代码:

代码语言:javascript
复制
operator-sdk create api --group=core --version=v1 --kind=Pod --controller=true --resource=false

现在我们有了一个新文件:controllers/pod_controller.go。该文件包含一个 PodReconciler 类型,其中包含两个需要实现的方法。第一个是 Reconcile,现在看起来是这样的:

代码语言:javascript
复制
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
}

每当创建、更新或删除 Pod 时,都会调用 Reconcile 方法。Pod 的名称和命名空间在 ctrl.Request 方法作为参数接收。

第二个方法是 SetupWithManager,现在看起来是这样的:

代码语言:javascript
复制
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 方法。它告诉操作器框架我们的 PodReconciler 需要监视什么类型。要使用 Kubernetes 内部使用的相同 Pod 类型,我们需要导入它的一些代码。Kubernetes 的所有源代码都是开源的,因此你可以在自己的 Go 代码中导入任何你喜欢的部分。你可以在 Kubernetes 源代码中找到可用软件包的完整列表,也可以在这里的pkg.go.dev[9]中找到。要使用 pod,我们需要 k8s.io/api/core/v1 包。

代码语言:javascript
复制
package controllers

import (
    // other imports...
    corev1 "k8s.io/api/core/v1"
    // other imports...
)

让我们在 SetupWithManager 中使用 Pod 类型来告诉操作器框架我们想要监视 Pod:

代码语言:javascript
复制
func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&corev1.Pod{}).
        Complete(r)
}

在继续之前,我们应该设置控制器需要的 RBAC 权限。在 Reconcile 方法上面,我们有一些默认权限:

代码语言:javascript
复制
// +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。让我们删除不必要的权限,只保留我们需要的:

代码语言:javascript
复制
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;update;patch

现在可以编写控制器的协调逻辑了。

实现协调逻辑

下面是我们想让 Reconcile 方法做的:

  1. 在 ctrl.Request 中使用 Pod 的名称和名称空间从 Kubernetes API 获取 Pod。
  2. 如果 Pod 有一个 add-pod-name-label 注释,添加一个 pod-name 标签到 Pod;如果注释缺失,不要添加标签。
  3. 在 Kubernetes API 中更新 Pod 以保持所做的更改。

让我们为注释和标签定义一些常量:

代码语言:javascript
复制
const (
    addPodNameLabelAnnotation = "padok.fr/add-pod-name-label"
    podNameLabel              = "padok.fr/pod-name"
)

我们的协调函数的第一步是从 Kubernetes API 中获取我们正在工作的 Pod:

代码语言:javascript
复制
// 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
}

当 Pod 被创建、更新或删除时,我们的 Reconcile 方法将被调用。在删除的情况下,对 r.Get 的调用将返回一个特定的错误。让我们导入定义这个错误的包:

代码语言:javascript
复制
package controllers

import (
    // other imports...
    apierrors "k8s.io/apimachinery/pkg/api/errors"
    // other imports...
)

我们现在可以处理这个特定的错误——因为我们的控制器不关心删除的 pods——显式忽略它:

代码语言:javascript
复制
    /*
        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,当且仅当我们的注释出现时,动态标签才会出现:

代码语言:javascript
复制
    /*
        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:

代码语言:javascript
复制
    /*
        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 控制器时,我们应该记住,我们并不是集群中唯一的参与者。当这种情况发生时,最好的做法是通过重新排队事件从头开始协调。让我们这样做:

代码语言:javascript
复制
    /*
        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
    }

让我们记住在方法结束时成功返回:

代码语言:javascript
复制
    return ctrl.Result{}, nil
}

这就是了!现在可以在集群上运行控制器了。

在集群上运行控制器

要在集群上运行我们的控制器,我们需要运行操作器。为此,你只需要 kubectl。如果你手边没有 Kubernetes 集群,我建议你用KinD(Docker 中的 Kubernetes)[10]在本地启动一个。

在你的机器上运行这个操作器所需要的就是这个命令:

代码语言:javascript
复制
make run

几秒钟后,你应该会看到操作器的日志。注意,我们的控制器的 Reconcile 方法被集群中已经运行的所有 pod 调用。

让我们保持操作器运行,并在另一个终端中创建一个新的 Pod:

代码语言:javascript
复制
kubectl run --image=nginx my-nginx

操作器应该快速打印一些日志,表明它对 Pod 的创建和随后的状态变化做出了反应:

代码语言:javascript
复制
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 的标签:

代码语言:javascript
复制
$ kubectl get pod my-nginx --show-labels
NAME       READY   STATUS    RESTARTS   AGE   LABELS
my-nginx   1/1     Running   0          11m   run=my-nginx

让我们向 Pod 添加一个注释,这样我们的控制器就知道要向它添加动态标签:

代码语言:javascript
复制
kubectl annotate pod my-nginx padok.fr/add-pod-name-label=true

请注意,控制器立即作出反应,并在其日志中产生了新的一行:

代码语言:javascript
复制
INFO    controllers.Pod adding label    {"pod": "default/my-nginx"}

万岁!你刚刚成功地编写了一个 Kubernetes 控制器,该控制器能够向集群中的资源添加具有动态值的标签。

控制器和操作器,无论大小,可以是你的 Kubernetes 旅程的重要部分。现在编写操作器比以往任何时候都容易。可能性是无止境的。

下一步是什么?

如果你想更进一步,我建议从在集群中部署控制器或操作器开始。Operator SDK 生成的 Makefile 将完成大部分工作。

当将作业者部署到生产中时,实现健壮的测试总是一个好主意。朝着这个方向的第一步是编写单元测试。本文档[11]将指导你为操作器编写测试。我为刚才写的操作器写了测试;你可以在这个 GitHub 仓库[12]找到我的所有代码。

如何了解更多?

Operator SDK 文档[13]详细介绍了如何进一步实现更复杂的操作器。

当建模一个更复杂的用例时,一个作用于内置 Kubernetes 类型的单一控制器可能是不够的。你可能需要使用自定义资源定义(CRD)[14]和多个控制器构建更复杂的操作器。Operator SDK 是一个很好的工具,可以帮助你做到这一点。

如果你想讨论构建操作器,请加入Kubernetes Slack 工作区[15]中的#kubernetes-operator[16]通道!

参考资料

[1]

Operators: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/

[2]

Operator SDK: https://sdk.operatorframework.io/

[3]

许多操作器: https://operatorhub.io/

[4]

自定义资源: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/

[5]

控制器: https://kubernetes.io/docs/concepts/architecture/controller/

[6]

实现这一点: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-name-label

[7]

mutating admission webhook: https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#mutatingadmissionwebhook

[8]

官方文档: https://sdk.operatorframework.io/docs/installation/

[9]

pkg.go.dev: https://pkg.go.dev/k8s.io/api

[10]

KinD(Docker 中的 Kubernetes): https://kind.sigs.k8s.io/docs/user/quick-start/#installation

[11]

本文档: https://sdk.operatorframework.io/docs/building-operators/golang/testing/

[12]

这个 GitHub 仓库: https://github.com/busser/label-operator

[13]

Operator SDK 文档: https://sdk.operatorframework.io/docs/

[14]

自定义资源定义(CRD): https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/

[15]

Kubernetes Slack 工作区: https://slack.k8s.io/

[16]

#kubernetes-operator: https://kubernetes.slack.com/messages/kubernetes-operators

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-06-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 CNCF 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么需要这样的一个控制器呢?
  • 使用 Operator SDK 引导控制器
  • 实现协调逻辑
  • 在集群上运行控制器
  • 下一步是什么?
  • 如何了解更多?
    • 参考资料
    相关产品与服务
    容器服务
    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档