使用 Operator SDK 为 Pod 标签编写Controller

语言: CN / TW / HK

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 存储库中找到我的所有代码。

https://github.com/busser/label-operator

如何学习更多?

Operator SDK 文档详细介绍了如何进一步实现更复杂的 operator

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

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

如果您想讨论构建 operator ,请加入 Kubernetes Slack 工作区中的 #kubernetes-operator 频道!

https://slack.k8s.io/
https://kubernetes.slack.com/messages/kubernetes-operators

更多文档

请关注微信公众号 云原生CTO [1]

参考资料

[1]

参考地址: https://kubernetes.io/blog/2021/06/21/writing-a-controller-for-pod-labels/