什么是k8s operator
大多数人在使用k8s时,都是通过使用原生资源来部署他们的应用程序,如pod、部署、服务等。然而,有需要扩展软件的能力,将其逻辑纳入,以满足特定的需求的情况,这些情况下原生资源可能无法满足。这就需要我们的operator来实现这些功能。
operator主要就是将程序员的一些操作变成可执行的代码然后放到k8s集群中自动化的去操作一些资源。operator使得很多重复性的工作得以自动化,比如:Mysql或者是ES的维护,启停gitlab等。
在这篇文章里面,我们主要是通过一个很简单的demo来带大家了解一个自定义的operator是如何运行的。
开始正餐
1. 构建环境
- go版本 v1.19+
- docker版本17.03+
- kubectl版本 v1.11.3+
- 能够访问一个Kubernetes v1.11.3+集群
接下来我们就需要安装kubebuilder
来帮助我们快速开发:
curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH) && chmod +x kubebuilder && mv kubebuilder /usr/local/bin/
当下载结束之后,就可以通过下面的命令来确认你是否安装成功了
$ kubebuilder version
Version: main.version{KubeBuilderVersion:"3.9.0", KubernetesVendor:"1.26.0", GitCommit:"26f605e889b2215120f73ea42b081efac99f5162", BuildDate:"2023-01-16T17:21:30Z", GoOs:"darwin", GoArch:"amd64"}
2. 创建一个简单的operator
接下来就是简单创建一个名叫foo
的operator,它除了演示一下operator的基础功能外没有其他实质性的功能。我们可以通过运行下面的命令来帮我们快速初始化一个项目,然后在这个项目的基础上进行我们自己的开发。
$ go mod init jerry.org/tutorial
$ kubebuilder init --plugins go/v3 --domain jerry.org --owner 'jerry'
当你执行了以上的命令之后,你会看到kubebuilder帮你下载了很多的依赖包下来,并且在你当前的目录下已经有一个项目模板自动创建好了,建好的项目目录结构如下:
├── Dockerfile // 当前项目构建镜像的dockerfile
├── Makefile
├── PROJECT
├── README.md
├── config // 所有需要的manifest
│ ├── default
│ │ ├── kustomization.yaml
│ │ ├── manager_auth_proxy_patch.yaml
│ │ └── manager_config_patch.yaml
│ ├── manager
│ │ ├── kustomization.yaml
│ │ └── manager.yaml
│ ├── prometheus
│ │ ├── kustomization.yaml
│ │ └── monitor.yaml
│ └── rbac
│ ├── auth_proxy_client_clusterrole.yaml
│ ├── auth_proxy_role.yaml
│ ├── auth_proxy_role_binding.yaml
│ ├── auth_proxy_service.yaml
│ ├── kustomization.yaml
│ ├── leader_election_role.yaml
│ ├── leader_election_role_binding.yaml
│ ├── role_binding.yaml
│ └── service_account.yaml
├── go.mod
├── go.sum
├── hack
│ └── boilerplate.go.txt
└── main.go // 项目入口
接下来我们就开始创建一个名叫FOO
的 CRD,并为这个CRD创建一个api controller, 运行下面的命令来创建一个CRD:
$ kubebuilder init --domain jerry.org --owner 'jerry' --skip-go-version-check
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/controller-runtime@v0.14.1
Update dependencies:
$ go mod tidy
Next: define a resource with:
$ kubebuilder create api
(base) ➜ k8s-operator kubebuilder create api --group tutorial --version v1beta1 --kind Foo
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
api/v1beta1/foo_types.go
controllers/foo_controller.go
Update dependencies:
$ go mod tidy
Running make:
$ make generate
mkdir -p /Users/Documents/k8s-operator/bin
test -s /Users/Documents/k8s-operator/bin/controller-gen && /Users/Documents/k8s-operator/bin/controller-gen --version | grep -q v0.11.1 || \
GOBIN=/Users/Documents/k8s-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.11.1
/Users/Documents/k8s-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:
$ make manifests
温馨提示:如果你当前目录路径中有空格,可能会报错~
3. 编写我们的CRD的controller逻辑
下面是我们刚刚创建好的并稍作修改后的foo
api,
package v1beta1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// FooSpec defines the desired state of Foo
type FooSpec struct {
Name string `json:"name"`
}
// 当Foo 找到朋友后,状态happy就会设为true
type FooStatus struct {
Happy bool `json:"happy,omitempty"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// Foo is the Schema for the foos API
type Foo struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec FooSpec `json:"spec,omitempty"`
Status FooStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// FooList contains a list of Foo
type FooList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Foo `json:"items"`
}
func init() {
SchemeBuilder.Register(&Foo{}, &FooList{})
}
接下来我们就实现我们的一个简单的逻辑。我们获取所有foo资源,获取所有与Foo
朋友具有相同名字的pod,如果我们找到了有这么一个pod存在,我们就把foo
的happy状态设置成true,否则就是false。
还有一点要注意的就是,这个状态更新应该具备实时性,因为controller
本身也会相应pod的时间,当有pod创建更新或者删除时,我们也应该能够实时的去更新status。
以下是我们的一些核心实现:
func (r *FooReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
logger.Info("reconciling foo custom resource")
// Get the Foo resource that triggered the reconciliation request
var foo tutorialv1beta1.Foo
if err := r.Get(ctx, req.NamespacedName, &foo); err != nil {
logger.Error(err, "unable to fetch Foo")
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Get pods with the same name as Foo's friend
var podList v1.PodList
var friendFound bool
if err := r.List(ctx, &podList); err != nil {
logger.Error(err, "unable to list pods")
} else {
for _, item := range podList.Items {
if item.GetName() == foo.Spec.Name {
logger.Info("pod linked to a foo custom resource found", "name", item.GetName())
friendFound = true
}
}
}
// Update Foo' happy status
foo.Status.Happy = friendFound
if err := r.Status().Update(ctx, &foo); err != nil {
logger.Error(err, "unable to update foo's happy status", "status", friendFound)
return ctrl.Result{}, err
}
logger.Info("foo's happy status updated", "status", friendFound)
logger.Info("foo custom resource reconciled")
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
// 注册我们的 controller 到 manager 中
func (r *FooReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&tutorialv1beta1.Foo{}).
Watches(&source.Kind{Type: &v1.Pod{}}, handler.EnqueueRequestsFromMapFunc(r.mapPodsReqToFooReq)).
Complete(r)
}
// 用于将 Pod 的变化映射到 Foo 的变化
func (r *FooReconciler) mapPodsReqToFooReq(obj client.Object) []reconcile.Request {
ctx := context.Background()
logger := log.FromContext(ctx)
// List all the Foo custom resource
req := []reconcile.Request{}
var list tutorialv1beta1.FooList
if err := r.Client.List(context.TODO(), &list); err != nil {
logger.Error(err, "unable to list foo custom resources")
} else {
// Only keep Foo custom resources related to the Pod that triggered the reconciliation request
for _, item := range list.Items {
if item.Spec.Name == obj.GetName() {
req = append(req, reconcile.Request{
NamespacedName: types.NamespacedName{Name: item.Name, Namespace: item.Namespace},
})
logger.Info("pod linked to a foo custom resource issued an event", "name", obj.GetName())
}
}
}
return req
}
运行make manifests
来生成我们当前资源的manifest,准备下一步的运行调试
4. 运行调试
运行make install
将我们的资源注册到我们的k8s集群中。当你看到下面的结果是,就是注册成功了。
customresourcedefinition.apiextensions.k8s.io/foos.tutorial.jerry.org created
运行kubectl get crds
来查看创建好的CRD:
kubectl get crds
NAME CREATED AT
foos.tutorial.jerry.org 2023-01-25T02:07:44Z
现在本地运行我们的controller:
使用make run
来在本地运行,然后我们创建几个CRD资源然后apply上去:
apiVersion: tutorial.jerry.org/v1beta1
kind: Foo
metadata:
name: foo-01
spec:
name: jack
---
apiVersion: tutorial.jerry.org/v1beta1
kind: Foo
metadata:
name: foo-02
spec:
name: joe
$ kubectl apply -f .
foo.tutorial.jerry.org/foo-01 created
foo.tutorial.jerry.org/foo-02 created
从我们的控制台中应该也能看到,我们的资源的更新日志
2023-01-25T10:18:41+08:00 INFO reconciling foo custom resource {"controller": "foo", "controllerGroup": "tutorial.jerry.org", "controllerKind": "Foo", "Foo": {"name":"foo-02","namespace":"default"}, "namespace": "default", "name": "foo-02", "reconcileID": "8f5907d0-d1cc-4705-a035-885c2dc08279"}
2023-01-25T10:18:41+08:00 INFO foo's happy status updated {"controller": "foo", "controllerGroup": "tutorial.jerry.org", "controllerKind": "Foo", "Foo": {"name":"foo-02","namespace":"default"}, "namespace": "default", "name": "foo-02", "reconcileID": "8f5907d0-d1cc-4705-a035-885c2dc08279", "status": false}
2023-01-25T10:18:41+08:00 INFO foo custom resource reconciled {"controller": "foo", "controllerGroup": "tutorial.jerry.org", "controllerKind": "Foo", "Foo": {"name":"foo-02","namespace":"default"}, "namespace": "default", "name": "foo-02", "reconcileID": "8f5907d0-d1cc-4705-a035-885c2dc08279"}
这个时候我们检查下资源的status字段:
$ kubectl describe foos
Name: foo-01
Namespace: default
Labels: <none>
Annotations: <none>
API Version: tutorial.jerry.org/v1beta1
Kind: Foo
Metadata:
Creation Timestamp: 2023-01-25T02:18:40Z
Generation: 1
Managed Fields:
API Version: tutorial.jerry.org/v1beta1
Fields Type: FieldsV1
Manager: kubectl-client-side-apply
Operation: Update
API Version: tutorial.jerry.org/v1beta1
Fields Type: FieldsV1
fieldsV1:
f:status:
Manager: main
Operation: Update
Subresource: status
Spec:
Name: jack
Status:
Events: <none>
Name: foo-02
Namespace: default
Labels: <none>
Annotations: <none>
API Version: tutorial.jerry.org/v1beta1
Kind: Foo
Metadata:
Creation Timestamp: 2023-01-25T02:18:40Z
Generation: 1
Managed Fields:
API Version: tutorial.jerry.org/v1beta1
Fields Type: FieldsV1
Manager: kubectl-client-side-apply
Operation: Update
API Version: tutorial.jerry.org/v1beta1
Fields Type: FieldsV1
Manager: main
Operation: Update
Subresource: status
Time: 2023-01-25T02:18:40Z
Resource Version: 2005426
UID: cf85e2f9-54d7-469d-9b71-1550ad06119e
Spec:
Name: joe
Status:
Events: <none>
其实可以看到目前这两个字段还是空的,那么让我们把这俩字段值变成true把。根据上述的逻辑描述,我们的集群里面需要一个同名的资源才能够触发我们的Happy=true的情况,那么我们就需要再部署一个名字叫jack的pod
apiVersion: v1
kind: Pod
metadata:
name: jack
spec:
containers:
- name: ubuntu
image: ubuntu:latest
# Just sleep forever
command: [ "sleep" ]
args: [ "infinity" ]
同样运行kubectl describe foos
这个时候再看第一个资源的状态就已经是true了:
$ kubectl describe foos
Name: foo-01
Namespace: default
Labels: <none>
Annotations: <none>
API Version: tutorial.jerry.org/v1beta1
Kind: Foo
Metadata:
Generation: 1
Managed Fields:
API Version: tutorial.jerry.org/v1beta1
Fields Type: FieldsV1
Manager: kubectl-client-side-apply
Operation: Update
Time: 2023-01-25T02:18:40Z
API Version: tutorial.jerry.org/v1beta1
Manager: main
Operation: Update
Subresource: status
Time: 2023-01-25T02:23:40Z
Resource Version: 2005893
UID: f30d385c-080d-486f-8bba-af39469e2566
Spec:
Name: jack
Status:
Happy: true
Events: <none>
总结
在这篇文章里面主要是讲述了我们如何使用kubecbuilder
来创建一个operator,并通过一个简单的场景来演绎整个operator的工作流程。希望能够给大家带来一些帮助。
参考文章:betterprogramming.pub/build-a-kub…
更多文章关注公众号: