如何在十分钟内构建一个k8s operator

381 阅读5分钟

什么是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的工作流程。希望能够给大家带来一些帮助。

代码地址:github.com/819110812/b…

参考文章:betterprogramming.pub/build-a-kub…

更多文章关注公众号: