本文发布已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。
编写 Pod 标签的控制器
Operator 被证明是在 Kubernetes 中运行有状态分布式应用程序的绝佳解决方案。诸如 Operator SDK 之类的开源工具提供了构建可靠且可维护的 Operator 的方法,从而更容易扩展 Kubernetes 并实现自定义调度。
Kubernetes Operator 在您的集群内部运行复杂的软件。开源社区已经为诸如 Prometheus、Elasticsearch 或 Argo CD 之类的分布式应用程序构建了许多 Operator。即使在开源之外,Operator 也可以帮助为您的 Kubernetes 集群带来新功能。
一个 Operator 是一组自定义资源和一组控制器。控制器监视 Kubernetes API 中特定资源的变化,并通过创建、更新或删除资源来做出反应。
Operator SDK 最适合构建功能齐全的 Operator。尽管如此,您也可以使用它来编写单个控制器。本文将引导您使用 Go 语言编写一个 Kubernetes 控制器,该控制器将为具有特定注解的 Pod 添加一个 pod-name
标签。
为什么我们需要为此编写一个控制器?
我最近参与的一个项目中,我们需要创建一个 Service,将流量路由到 ReplicaSet 中的特定 Pod。问题是 Service 只能通过标签选择 Pod,而 ReplicaSet 中的所有 Pod 都具有相同的标签。有两种方法可以解决这个问题
- 创建一个没有选择器的 Service,并直接管理该 Service 的 Endpoints 或 EndpointSlices。我们需要编写一个自定义控制器,将我们的 Pod 的 IP 地址插入到这些资源中。
- 为 Pod 添加一个具有唯一值的标签。然后,我们可以在 Service 的选择器中使用这个标签。同样,我们需要编写一个自定义控制器来添加这个标签。
控制器是一个控制循环,它跟踪一个或多个 Kubernetes 资源类型。上面选项 2 中的控制器只需要跟踪 Pod,这使得它更容易实现。我们将通过编写一个 Kubernetes 控制器来完成这个选项,该控制器将为我们的 Pod 添加一个 pod-name
标签。
StatefulSets 原生支持此功能,它会为集合中的每个 Pod 添加一个 pod-name
标签。但是,如果我们不想或不能使用 StatefulSets 怎么办?
我们很少直接创建 Pod;大多数情况下,我们使用 Deployment、ReplicaSet 或其他高级资源。我们可以在 PodSpec 中指定要添加到每个 Pod 的标签,但不能使用动态值,因此无法复制 StatefulSet 的 pod-name
标签。
我们尝试使用修改性准入 Webhook。当任何人创建 Pod 时,Webhook 会使用包含 Pod 名称的标签修补 Pod。令人失望的是,这不起作用:并非所有 Pod 在创建之前都有名称。例如,当 ReplicaSet 控制器创建 Pod 时,它会向 Kubernetes API 服务器发送一个 namePrefix
,而不是一个 name
。API 服务器会在将新的 Pod 持久化到 etcd 之前生成一个唯一的名称,但只有在调用我们的准入 Webhook 之后才会生成。因此,在大多数情况下,我们无法使用修改性 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
接下来,让我们初始化一个新的 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
}
每当创建、更新或删除 Pod 时,都会调用 Reconcile
方法。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)
}
当 Operator 启动时,会调用 SetupWithManager
方法。它的作用是告诉 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...
)
让我们在 SetupWithManager
中使用 Pod
类型来告诉 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
方法执行的操作
- 使用
ctrl.Request
中的 Pod 名称和命名空间从 Kubernetes API 中获取 Pod。 - 如果 Pod 具有
add-pod-name-label
注解,则向 Pod 添加pod-name
标签;如果缺少注解,则不添加标签。 - 更新 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
}
当创建、更新或删除 Pod 时,将调用我们的 Reconcile
方法。在删除的情况下,我们对 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 (Docker 中的 Kubernetes) 在本地启动一个。
只需以下命令即可从您的机器运行 Operator
make run
几秒钟后,您应该会看到 Operator 的日志。请注意,我们控制器的 Reconcile
方法已针对集群中已运行的所有 Pod 调用。
让我们保持 Operator 运行,并在另一个终端中创建一个新的 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 控制器,该控制器能够为集群中的资源添加具有动态值的标签。
无论是大型还是小型的控制器和操作符,都可以成为您 Kubernetes 之旅的重要组成部分。现在编写操作符比以往任何时候都更容易。可能性是无限的。
接下来做什么?
如果您想更进一步,我建议您首先在集群内部署您的控制器或操作符。由 Operator SDK 生成的 Makefile
将完成大部分工作。
在将操作符部署到生产环境时,始终建议实施强大的测试。朝着这个方向迈出的第一步是编写单元测试。 本文档将指导您为您的操作符编写测试。我为我们刚刚编写的操作符编写了测试;您可以在 这个 GitHub 存储库中找到我的所有代码。
如何了解更多?
Operator SDK 文档详细介绍了如何进一步开发和实现更复杂的操作符。
在建模更复杂的用例时,单个控制器作用于内置 Kubernetes 类型可能不够。您可能需要使用自定义资源定义 (CRD)和多个控制器来构建更复杂的操作符。Operator SDK 是帮助您实现这一目标的绝佳工具。
如果您想讨论构建操作符,请加入 Kubernetes Slack 工作区中的 #kubernetes-operator 频道!