如果你想要对 K8S 做二次开发或者说在原有的基础上封装一些功能让开发者更加好用,那么 Operator 的用法你可必须掌握。
我觉得 Operator 真的是 K8S 扩展设计的非常巧妙的一点,它好像一个插件系统,你有了它就好像有了 k8s 的一个扩展操作权,能扩展出各种各样的用法。那什么是 Operator 呢?这需要从 CRD 说起。
首先我们需要知道第一个概念就是 CRD(Custom Resource Define)
,自定义资源定义,顾名思义就是使用者可以通过 CRD 来创建自定义的资源。我们知道在 K8S 中有各种各样的资源 Pod
、Deployment
、StatefulSet
… 在编写 yaml
文件的时候会指定对应的资源类型。
官方文档:Create a CustomResourceDefinition 其中有一个实际的 CustomResourceDefinition 案例
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
# name must match the spec fields below, and be in the form: <plural>.<group>
name: crontabs.stable.example.com
spec:
# group name to use for REST API: /apis/<group>/<version>
group: stable.example.com
# list of versions supported by this CustomResourceDefinition
versions:
- name: v1
# Each version can be enabled/disabled by Served flag.
served: true
# One and only one version must be marked as the storage version.
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
cronSpec:
type: string
image:
type: string
replicas:
type: integer
# either Namespaced or Cluster
scope: Namespaced
names:
# plural name to be used in the URL: /apis/<group>/<version>/<plural>
plural: crontabs
# singular name to be used as an alias on the CLI and for display
singular: crontab
# kind is normally the CamelCased singular type. Your resource manifests use this.
kind: CronTab
# shortNames allow shorter string to match your resource on the CLI
shortNames:
- ct
---- 下面是具体的 object ----
apiVersion: "stable.example.com/v1"
kind: CronTab
metadata:
name: my-new-cron-object
spec:
cronSpec: "* * * * */5"
image: my-awesome-cron-image
然后,有了它,你就可以像操作一个 pod
一样操作这个你定义的对象了,你还可以为它定义一些必要的属性(properties)。那么有了 CRD 之后,我们就有了一个非常强大的能力来扩展 k8s 已有的功能了。但是只有这样还是不够的。因为它仅仅定义了你所需要的资源,但是这个资源如何被操作呢?
有了资源没有人管肯定也不行,那么我们就需要一个 Controller 来控制它的行为和动作了。其实 Controller 本质是一个控制循环。我们知道,k8s 的控制模式其实是基于一个状态模型的,它将监控所有资源的状态,当现在的资源状态不满足用户定义的资源状态的时候,它就会做出调整,想办法让资源调整状态到预期值。
for {
实际状态 := 获取集群中对象 X 的实际状态(Actual State)
期望状态 := 获取集群中对象 X 的期望状态(Expectation State)
if 实际状态 == 期望状态{
什么都不做
} else {
执行编排动作,将实际状态调整为期望状态
}
}
当 Controller Manager 发现资源的实际状态和期望状态有偏差之后,会触发相应 Controller 注册的 Event Handler,让它们去根据资源本身的特点进行调整。
所以,我们可以简单的理解为 Operator = CRD + Controller 也就是说自定义资源加自定义控制器就是 Operator,使用它我们不仅可以自定义我们想要的资源,还可以通过我们想要的逻辑和方式对它进行操作。
那此时你就可以想象的到它是有多万能了。比如:有了自定义资源,你可以定义你想要的各种属性,原来 deployment 只有那些属性,现在你就可以扩展各种你想要的属性了,并且你可以组合一些现有的资源。同时有了自定义控制器,你就可以任意的进行操作了,最重要的是,你能在出现各种情况(重启、异常退出等等)需要调度的时候第一时间知道,并且可以控制如何去调度,调度之后应该配置什么属性等等。
那本文下面就带你来快速制作一个 Demo 来体验一下 Operator,当然前提是你需要有一个可以操作的 k8s 环境。
开发 Operator 并不一定要用 kubebuilder 还可以使用 https://github.com/operator-framework/operator-sdk 我更习惯用 kubebuilder 而已
安装文档见:installation
$ curl -L -o kubebuilder "https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)"
$ chmod +x kubebuilder && mv kubebuilder /usr/local/bin/
$ mkdir opex
$ cd opex
$ kubebuilder init --domain linkinstars.com --repo linkinstars.com/op-ex
$ kubebuilder create api --group example --version v1 --kind ExampleA
# 然后输入两次 y
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
api/v1/examplea_types.go
api/v1/groupversion_info.go
internal/controller/suite_test.go
internal/controller/examplea_controller.go
...
...
此时项目结构已经创建好了,kubebuilder 也为我们创建了对应的 CRD 模板和 Controller 模板。你可以先大致浏览一下项目结构。下面我们就会开始编码的工作。
首先明确一下我们的目标,我们的目标是创建一个 CRD 和 Controller 来体验一下 Operator。我们这次创建的 CRD 扮演一个监察的角色,当整个集群中出现带有指定名称的标签(Label)的对象时,监察就会改变自己的状态,变成监控中。
修改 api/v1/examplea_types.go
文件
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// ExampleASpec defines the desired state of ExampleA
type ExampleASpec struct {
GroupName string `json:"groupName,omitempty"`
}
// ExampleAStatus defines the observed state of ExampleA
type ExampleAStatus struct {
UnderControl bool `json:"underControl,omitempty"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// ExampleA is the Schema for the examplea API
type ExampleA struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ExampleASpec `json:"spec,omitempty"`
Status ExampleAStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// ExampleAList contains a list of ExampleA
type ExampleAList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ExampleA `json:"items"`
}
func init() {
SchemeBuilder.Register(&ExampleA{}, &ExampleAList{})
}
可以看到这里我们主要是定义了 ExampleA
的 Spec
,也就是我们常常在 yaml
文件中写的 spec
属性,其中我们添加了 GroupName
也就是一个组名。
修改 internal/controller/examplea_controller.go
package controller
import (
"context"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
examplev1 "linkinstars.com/op-ex/api/v1"
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/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// ExampleAReconciler reconciles a ExampleA object
type ExampleAReconciler struct {
client.Client
Scheme *runtime.Scheme
}
//+kubebuilder:rbac:groups=example.linkinstars.com,resources=examplea,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=example.linkinstars.com,resources=examplea/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=example.linkinstars.com,resources=examplea/finalizers,verbs=update
// Reconcile
func (r *ExampleAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
logger.Info("开始调用Reconcile方法")
var exp examplev1.ExampleA
if err := r.Get(ctx, req.NamespacedName, &exp); err != nil {
logger.Error(err, "未找到对应的CRD资源")
return ctrl.Result{}, client.IgnoreNotFound(err)
}
exp.Status.UnderControl = false
var podList corev1.PodList
if err := r.List(ctx, &podList); err != nil {
logger.Error(err, "无法获取pod列表")
} else {
for _, item := range podList.Items {
if item.GetLabels()["group"] == exp.Spec.GroupName {
logger.Info("找到对应的pod资源", "name", item.GetName())
exp.Status.UnderControl = true
}
}
}
if err := r.Status().Update(ctx, &exp); err != nil {
logger.Error(err, "无法更新CRD资源状态")
return ctrl.Result{}, err
}
logger.Info("已更新CRD资源状态", "status", exp.Status.UnderControl)
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *ExampleAReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&examplev1.ExampleA{}).
Watches(
&corev1.Pod{},
handler.EnqueueRequestsFromMapFunc(r.podChangeHandler),
).
Complete(r)
}
func (r *ExampleAReconciler) podChangeHandler(ctx context.Context, obj client.Object) []reconcile.Request {
logger := log.FromContext(ctx)
var req []reconcile.Request
var list examplev1.ExampleAList
if err := r.Client.List(ctx, &list); err != nil {
logger.Error(err, "无法获取到资源")
} else {
for _, item := range list.Items {
if item.Spec.GroupName == obj.GetLabels()["group"] {
req = append(req, reconcile.Request{
NamespacedName: types.NamespacedName{Name: item.Name, Namespace: item.Namespace},
})
}
}
}
return req
}
核心逻辑非常简单,就是遍历所有的 pod,如果发现 label 中带有对应 groupName 的 pod 就修改当前 crd 的 UnderControl 状态为 true
if item.GetLabels()["group"] == exp.Spec.GroupName {
logger.Info("找到对应的pod资源", "name", item.GetName())
exp.Status.UnderControl = true
}
其中有几个要点
Watches
方法的第一个参数就是监控的对象类型,第二个参数就是 handler。使用 kubebuilder 的方便就是部署和调试很方便,模板都有,执行下面的命令生成并将 CRD 安装到 k8s 集群中。
$ make manifests
$ make install
安装成功后,查看一下
$ kubectl get crds |grep linkin
exampleas.example.linkinstars.com 2023-08-03T23:02:39Z
error: accumulating resources: accumulation err='accumulating resources from 'bases/example.linkinstars.com_examplea.yaml'
如果出现这样类似的错误,通常是由于生成文件名 s 的问题导致的,修改 config/crd/kustomization.yaml
文件中的 resources:
为 - bases/example.linkinstars.com_exampleas.yaml
对应的正确名称即可
建议新开一个终端窗口来启动,它会前终端中运行并输入对应的日志,方便后续查看
$ make run
2023-08-03T23:07:21+08:00 INFO controller-runtime.metrics Metrics server is starting to listen {"addr": ":8080"}
2023-08-03T23:07:21+08:00 INFO setup starting manager
2023-08-03T23:07:21+08:00 INFO starting server {"path": "/metrics", "kind": "metrics", "addr": "[::]:8080"}
2023-08-03T23:07:21+08:00 INFO Starting server {"kind": "health probe", "addr": "[::]:8081"}
2023-08-03T23:07:21+08:00 INFO Starting EventSource {"controller": "examplea", "controllerGroup": "example.linkinstars.com", "controllerKind": "ExampleA", "source": "kind source: *v1.ExampleA"}
2023-08-03T23:07:21+08:00 INFO Starting EventSource {"controller": "examplea", "controllerGroup": "example.linkinstars.com", "controllerKind": "ExampleA", "source": "kind source: *v1.Pod"}
2023-08-03T23:07:21+08:00 INFO Starting Controller {"controller": "examplea", "controllerGroup": "example.linkinstars.com", "controllerKind": "ExampleA"}
2023-08-03T23:07:21+08:00 INFO Starting workers {"controller": "examplea", "controllerGroup": "example.linkinstars.com", "controllerKind": "ExampleA", "worker count": 1}
2023-08-03T23:08:46+08:00 INFO 开始调用Reconcile方法 {"controller": "examplea", "controllerGroup": "example.linkinstars.com", "controllerKind": "ExampleA", "ExampleA": {"name":"my-opex","namespace":"default"}, "namespace": "default", "name": "my-opex", "reconcileID": "9a03fe8e-8461-4500-b32f-140161095f8b"}
2023-08-03T23:08:46+08:00 INFO 已更新CRD资源状态 {"controller": "examplea", "controllerGroup": "example.linkinstars.com", "controllerKind": "ExampleA", "ExampleA": {"name":"my-opex","namespace":"default"}, "namespace": "default", "name": "my-opex", "reconcileID": "9a03fe8e-8461-4500-b32f-140161095f8b", "status": false}
创建一个 CRD config/samples/example_v1_examplea.yaml
内容如下,指定 groupName 为 business 也就是当出现 business 的 pod 这个 crd 就开始认真监察了。
apiVersion: example.linkinstars.com/v1
kind: ExampleA
metadata:
labels:
app.kubernetes.io/name: examplea
app.kubernetes.io/instance: examplea-sample
app.kubernetes.io/part-of: opex
app.kubernetes.io/managed-by: kustomize
app.kubernetes.io/created-by: opex
name: my-opex
spec:
groupName: business
kubectl apply -f config/samples/example_v1_examplea.yaml
然后,我们查看一下当前的 CRD 状态,可以看到现在状态应该是空的
$ kubectl describe ExampleA my-opex
然后新建一个文件 example_v1_examplea 1.yaml
apiVersion: v1
kind: Pod
metadata:
name: busybox
labels:
group: "business"
spec:
containers:
- name: busybox
image: busybox:latest
command:
- sleep
- "3600"
然后再次查看 CRD 状态
$ kubectl describe ExampleA my-opex
...
Spec:
Group Name: business
Status:
Under Control: true
Events: <none>
...
可以看到控制状态已经变成了 true 了,同时你也可以在控制台的日志中看到资源状态变更的日志
2023-08-03T23:28:21+08:00 INFO 开始调用Reconcile方法 {"controller": "examplea", "controllerGroup": "example.linkinstars.com", "controllerKind": "ExampleA", "ExampleA": {"name":"my-opex","namespace":"default"}, "namespace": "default", "name": "my-opex", "reconcileID": "7578ca2f-2bfe-4e4b-ba1c-3d43ff366ddf"}
2023-08-03T23:28:21+08:00 INFO 已更新CRD资源状态 {"controller": "examplea", "controllerGroup": "example.linkinstars.com", "controllerKind": "ExampleA", "ExampleA": {"name":"my-opex","namespace":"default"}, "namespace": "default", "name": "my-opex", "reconcileID": "7578ca2f-2bfe-4e4b-ba1c-3d43ff366ddf", "status": false}
2023-08-03T23:28:21+08:00 INFO 开始调用Reconcile方法 {"controller": "examplea", "controllerGroup": "example.linkinstars.com", "controllerKind": "ExampleA", "ExampleA": {"name":"my-opex","namespace":"default"}, "namespace": "default", "name": "my-opex", "reconcileID": "e37ac2a6-a769-491c-9d73-e89ea8e43f23"}
2023-08-03T23:28:21+08:00 INFO 已更新CRD资源状态 {"controller": "examplea", "controllerGroup": "example.linkinstars.com", "controllerKind": "ExampleA", "ExampleA": {"name":"my-opex","namespace":"default"}, "namespace": "default", "name": "my-opex", "reconcileID": "e37ac2a6-a769-491c-9d73-e89ea8e43f23", "status": false}
至此,我们的上手工作就已经完成了。之后你就可以摸索更加高级的各种操作了,根据具体的实际业务场景需求来满足不同的需要。
如果需要回收删除对应的资源先使用 kubectl delete -f
删除所有创建的测试。然后直接执行 make uninstall
就可以了。
网上有很多对于 helm 和 Operator 的类比,其实我觉得二者方向就不同。helm 是将所有需要部署的资源统一打包在一起,方便打包和部署。当然 CRD 也可以实现类似的功能并且更加强大。但 helm 是对于已有资源的集合,大多数部署情况 k8s 提供的 deploy/service/… 等等已经足够用了。最最最关键的一点,helm 没法控制循环,Controller 才是 Operator 的灵魂。
我觉得很多人会认为 Operator 复杂或者很难上手,多数情况是不理解 k8s 内部原理导致的。如果你非常清楚他的 Controller Manager 的原理和行为,直到控制循环,其实 Operator 已经封装的非常好了。这样的设计我觉得巧妙的原因是扩展起来真的非常方便。
kubebuilder 官方还有一个 CronJob 的教程,让你快速使用 Operator 实现一个 CronJob 的功能。我觉得对于新手可能还是稍微复杂了一点点,当然你看完本文并且实践之后建议你跑一把玩一玩会更容易理解。