1.2.2 Operator 应用案例
前面介绍了基于 CR 和相应的自定义资源控制器,我们可以自定义扩展 Kubernetes 原生的模型元素,这样的自定义模型可以加入到原生 Kubernetes API 管理;同时 Operator开发者可以像使用原生 API 进行应用管理一样,通过声明式的方式定义一组业务应用的期状态,并且根据业务应用的自身特点编写相应控制器逻辑,以此完成对应用运行时刻生命周期的管理,并持续维护与期望状态的一致性。
在本节我们将介绍如何使用 Kubebuilder 工具,快速构建一个 Kubernetes Operator,通过创建 CRD 完成一个简单的 Web 应用部署,通过编写控制器相关业务逻辑,完成对CRD 的自动化管理。
1. Kubebuilder 介绍
Kubebuilder 是一个用 Go 语言构建 Kubernetes API 控制器和 CRD 的脚手架工具,通过使用 Kubebuilder,用户可以遵循一套简单的编程框架,编写 Operator 应用实例。
(1)依赖条件
① go version v1.13+。
② docker version 17.03+。
③ kubectl version v1.11.3+。
④ Access to a Kubernetes v1.11.3+ cluster。
(2)安装 Kubebuilder
在安装 Kubebuilder 前,首先需要安装 Go 语言环境,针对不同的操作系统,安装方法可参考 Go 语言官方文档。
安装完成后,我们通过如下命令验证是否安装完成:
$ go version
查看命令是否正确回显当前安装的 Go 语言版本,输入如下命令查看 Go 语言环境变量:
$ go env
我们可以看到 GOOS 及 GOARCH 等常用环境变量配置 ,然后安装 Kubebuilder,具体 shell 命令见代码清单 1-2。
代码清单 1-2
os=$(go env GOOS) arch=$(go env GOARCH) # download kubebuilder and extract it to tmp curl -L https://go.kubebuilder.io/dl/2.3.1/${os}/${arch} | tar -xz -C /tmp/ # move to a long-term location and put it on your path # (you'll need to set the KUBEBUILDER_ASSETS env var if you put it somewhere else) sudo mv /tmp/kubebuilder_2.3.1_${os}_${arch} /usr/local/kubebuilder export PATH=$PATH:/usr/local/kubebuilder/bin
执行完成后,输入如下命令检查是否正确:
$ kubebuilder version
至此 Kubebuilder 已安装完成,使用 kubebuilder -h 可以查看帮助文档。
2. Welcome 案例介绍
Welcome 案例主要实现使用 Operator 和 CRD 部署一套完整的应用环境,可以实现根据自定义类型创建资源,通过创建一个 Welcome 类型的资源,后台自动创建 Deployment和 Service,通过 Web 页面访问 Service 呈现应用部署,通过自定义控制器方式进行控制管理,整体流程如图 1-10 所示。
图 1-10 案例应用交互流程
本案例中,我们需要创建 Welcome 自定义资源及对应的 Controllers,最终我们可以通过类似代码清单 1-3 的 Yaml 文件部署简单的 Web 应用。
代码清单 1-3
apiVersion: webapp.demo.welcome.domain/v1 kind: Welcome metadata: name: welcome-sample spec: name: myfriends
(1) Web 应用介绍
本案例中,我们使用 Go 语言 http 模块创建了一个 Web 服务,用户访问页面后会自动加载 NAME 及 PORT 环境变量并渲染 index.html 静态文件,代码逻辑见代码清单 1-4。
代码清单 1-4
func main() { name := os.Getenv("NAME") hello := fmt.Sprintf("Hello %s ", name) http.Handle("/hello/", http.StripPrefifix("/hello/", http.FileServer(http. Dir("static")))) f, err := os.OpenFile("./static/index.html", os.O_APPEND|os.O_ WRONLY|os.O_CREATE, 0600) if err != nil { panic(err) } defer f.Close() if _, err = f.WriteString(hello); err != nil { panic(err) } port := os.Getenv("PORT") if port == "" { port = "8080" } }
其中,NAME 环境变量通过我们在 Welcome 中定义的 name 字段获取,我们在下面的控制器编写中会详细介绍获取字段的详细方法。我们将 index.html 放在 Static 文件夹下,并将工程文件打包为 Docker 镜像,Dockerfile 见代码清单 1-5。
代码清单 1-5
FROM golang:1.12 as builder # Copy local code to the container image. WORKDIR / COPY . . COPY static # Build the command inside the container. RUN CGO_ENABLED=0 GOOS=linux go build -v -o main # Use a Docker multi-stage build to create a lean production image. FROM alpine RUN apk add --no-cache ca-certifificates # Copy the binary to the production image from the builder stage. COPY --from=builder /main /usr/local/main COPY --from=builder static /static # Run the web service on container startup. CMD ["/usr/local/main"]
本案例中 Docker 镜像文件已上传至 dockerhub,可以通过 docker pull sdfcdwefe/welcome demo:v1 进行下载。
(2)项目初始化
接下来,我们使用代码清单 1-6 中的 Kubebuilder 命令进行项目初始化工作。
代码清单 1-6
$ mkdir demo $ cd demo $ go mod init welcome_demo.domain $ kubebuilder init --domain demo.welcome.domain
初始化项目后,Kubebuilder 会自动生成 main.go 文件等一系列配置和代码框架(见代码清单 1-7)。
代码清单 1-7
. ├── bin │ └── manager ├── confifig │ ├── certmanager │ │ ├── certifificate.yaml │ │ ├── kustomization.yaml │ │ └── kustomizeconfifig.yaml │ ├── default │ │ ├── kustomization.yaml │ │ ├── manager_auth_proxy_patch.yaml │ │ ├── manager_webhook_patch.yaml │ │ └── webhookcainjection_patch.yaml │ ├── manager │ │ ├── kustomization.yaml │ │ └── manager.yaml │ ├── prometheus │ │ ├── kustomization.yaml │ │ └── monitor.yaml │ ├── rbac │ │ ├── auth_proxy_client_clusterrole.yaml │ │ ├── auth_proxy_role_binding.yaml │ │ ├── auth_proxy_role.yaml │ │ ├── auth_proxy_service.yaml │ │ ├── kustomization.yaml │ │ ├── leader_election_role_binding.yaml │ │ ├── leader_election_role.yaml │ │ └── role_binding.yaml │ └── webhook │ ├── kustomization.yaml │ ├── kustomizeconfifig.yaml │ └── service.yaml ├── Dockerfifile ├── go.mod ├── go.sum ├── hack │ └── boilerplate.go.txt ├── main.go ├── Makefifile └── PROJECT
接下来我们使用代码清单 1-8 创建“Welcome”Kind 和其对应的控制器。
代码清单 1-8
$ kubebuilder create api --group webapp --kind Welcome --version v1 Create Resource [y/n] y Create Controller [y/n] y
输入两次 y,Kubebuilder 分别创建了资源和控制器的模板,此处的 group、version、kind 这 3 个属性组合起来标识一个 k8s 的 CRD,创建完成后,Kubebuilder 添加文件见代码清单 1-9。
代码清单 1-9
├── api │ └── v1 │ ├── groupversion_info.go │ ├── welcome_types.go // 自定义 CRD 结构需修改的文件 │ └── zz_generated.deepcopy.go ├── bin │ └── manager ├── config │ ├── certmanager │ │ ├── certificate.yaml │ │ ├── kustomization.yaml │ │ └── kustomizeconfig.yaml │ ├── crd │ │ ├── bases │ │ │ └── webapp.demo.welcome.domain_welcomes.yaml │ │ ├── kustomization.yaml │ │ ├── kustomizeconfig.yaml │ │ └── patches │ │ ├── cainjection_in_welcomes.yaml │ │ └── webhook_in_welcomes.yaml │ ├── default │ │ ├── kustomization.yaml │ │ ├── manager_auth_proxy_patch.yaml │ │ ├── manager_webhook_patch.yaml │ │ └── webhookcainjection_patch.yaml │ ├── manager │ │ ├── kustomization.yaml │ │ └── manager.yaml │ ├── prometheus │ │ ├── kustomization.yaml │ │ └── monitor.yaml │ ├── rbac │ │ ├── auth_proxy_client_clusterrole.yaml │ │ ├── auth_proxy_role_binding.yaml │ │ ├── auth_proxy_role.yaml │ │ ├── auth_proxy_service.yaml │ │ ├── kustomization.yaml │ │ ├── leader_election_role_binding.yaml │ │ ├── leader_election_role.yaml │ │ ├── role_binding.yaml │ │ ├── role.yaml │ │ ├── welcome_editor_role.yaml │ │ └── welcome_viewer_role.yaml │ ├── samples │ │ └── webapp_v1_welcome.yaml // 简单的自定义资源 Yaml 文件 │ └── webhook │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── service.yaml ├── controllers │ ├── suite_test.go │ └── welcome_controller.go // CRD Controller 核心逻辑 ├── Dockerfile ├── go.mod ├── go.sum ├── hack │ └── boilerplate.go.txt ├── main.go ├── Makefile └── PROJECT
后续需要执行两步操作:
① 修改 Resource Type;
② 修改 Controller 逻辑。
(3)修改 Resource Type
此处 Resource Type 为需要定义的资源字段,用于在 Yaml 文件中进行声明,本案例中需要新增 name 字段用于“Welcome”Kind 中的 Web 应用,见代码清单 1-10。
代码清单 1-10
/api/v1/welcome_types.go type WelcomeSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file // Foo is an example field of Welcome. Edit Welcome_types.go to remove/update // Foo string `json:"foo,omitempty"` Name string `json:"name,omitempty"` }
(4)修改 Controller 逻辑
在 Controller 中需要通过 Reconcile 方法完成 Deployment 和 Service 部署,并最终达到期望的状态。
Controller 中的代码见代码清单 1-11,我们需要在其中加入业务逻辑。
代码清单 1-11
// +kubebuilder:rbac:groups=webapp.demo.welcome.domain,resources=welcomes,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=webapp.demo.welcome.domain,resources=welcomes/ status,verbs=get;update;patch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;get; patch;create;update // +kubebuilder:rbac:groups=core,resources=services,verbs=list;watch;get;patch; create;update func (r *WelcomeReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() log := r.Log.WithValues("welcome", req.NamespacedName) log.Info("reconciling welcome")
此处有两组“+”标识,第一组用于 Operator 更新 Welcome 资源对象,第二组用于创
建 Deployment 和 Service。接下来完成 Welcome 类型控制器的部分代码的实现(见
代码清单 1-12)。
代码清单 1-12
deployment, err := r.createWelcomeDeployment(welcome) if err != nil { return ctrl.Result{}, err } log.Info("create deployment success!") svc, err := r.createService(welcome) if err != nil { return ctrl.Result{}, err } log.Info("create service success!") applyOpts := []client.PatchOption{client.ForceOwnership, client. FieldOwner("welcome_controller")} err = r.Patch(ctx, &deployment, client.Apply, applyOpts...) if err != nil { return ctrl.Result{}, err } err = r.Patch(ctx, &svc, client.Apply, applyOpts...) if err != nil { return ctrl.Result{}, err }
在控制器部分需要完成 Deployment 和 Service 的创建,并完成两者的关联,在上述代码中,我们分别通过调用 createWelcomeDeployment 和 createService 方法完成对象的创建,接下来我们完成上述方法的具体实现(见代码清单 1-13)。
代码清单 1-13
func (r *WelcomeReconciler) createWelcomeDeployment(welcome webappv1.Welcome) (appsv1.Deployment, error) { defOne := int32(1) name := welcome.Spec.Name if name == "" { name = "world" } depl := appsv1.Deployment{ TypeMeta: metav1.TypeMeta{APIVersion: appsv1.SchemeGroupVersion. String(), Kind: "Deployment"}, ObjectMeta: metav1.ObjectMeta{ Name: welcome.Name, Namespace: welcome.Namespace, }, Spec: appsv1.DeploymentSpec{ Replicas: &defOne, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"welcome": welcome.Name}, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"welcome": welcome.Name}, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "welcome", Env: []corev1.EnvVar{ {Name: "NAME", Value: name}, }, Ports: []corev1.ContainerPort{ {ContainerPort: 8080, Name: "http", Protocol: "TCP"}, }, Image: "sdfcdwefe/operatordemo:v1", Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceCPU: *resource. NewMilliQuantity(100, resource.DecimalSI), corev1.ResourceMemory: *resource. NewMilliQuantity(100000, resource.BinarySI),
在上述代码中,我们在 Deployment 使用了之前制作的 Docker 镜像,将 Types 中获得的 NAME 字段作为环境变量传入镜像中,在镜像执行 main 函数时,即可获得 NAME字段并修改 index 文件,在文件中插入 NAME,并默认开启 8080 监听端口,用户通过Web 访问时即可获得最终的期望值。接下来,我们完成 Service 部分代码的实现(见代码清单 1-14)。
代码清单 1-14
func (r *WelcomeReconciler) createService(welcome webappv1.Welcome) (corev1. Service, error) { svc := corev1.Service{ TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion. String(), Kind: "Service"}, ObjectMeta: metav1.ObjectMeta{ Name: welcome.Name, Namespace: welcome.Namespace, }, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ {Name: "http", Port: 8080, Protocol: "TCP", TargetPort: intstr.FromString("http")}, }, Selector: map[string]string{"welcome": welcome.Name}, Type: corev1.ServiceTypeLoadBalancer, }, }
在本例中,我们创建了 LoadBalancer 类型的 Service。通过 kubectl get svc 命令可以获取 URL 地址,也可以访问 Web 应用。
(5) Welcome 应用部署
接下来,我们部署前面步骤中更新的 Type 和 Controller 文件,并创建 Welcome 类型资源(见代码清单 1-15)。
代码清单 1-15
$ kubectl create -f config/crd/bases/ $ kubectl create -f config/samples/webapp_v1_welcome.yaml
此时,我们通过kubectl get crd命令可以看到自定义对象已经生效(见代码清单1-16)。
代码清单 1-16
$ kubectl get crd NAME CREATED AT crontabs.stable.example.com 2021-02-18T06:23:11Z welcomes.webapp.demo.welcome.domain 2021-03-10T13:06:37Z
通过 kubectl get welcome 命令可以看到创建的 welcome 对象(见代码清单 1-17)。
代码清单 1-17
$ kubectl get welcome NAME AGE welcome-sample 3s
此时 CRD 并不会完成任何工作,只是在 ETCD 中创建了一条记录,我们需要运行
Controller 才能帮助我们完成调谐工作并最终达到 welcome 定义的状态。
$ make run
以上方式在本地启动控制器,方便调试和验证,最终显示见代码清单 1-18。
代码清单 1-18
2021-03-11T21:04:56.904+0800 INFO controller-runtime.metrics metrics server is starting to listen {"addr": ":8080"} 2021-03-11T21:04:56.904+0800 INFO setup starting manager 2021-03-11T21:04:56.904+0800 INFO controller-runtime.manager starting metrics server {"path": "/metrics"} 2021-03-11T21:04:56.905+0800 INFO controller-runtime.controller Starting EventSource {"controller": "welcome", "source": "kind source: /, Kind="} 2021-03-11T21:04:57.005+0800 INFO controller-runtime.controller Starting Controller {"controller": "welcome"} 2021-03-11T21:04:57.005+0800 INFO controller-runtime.controller Starting workers {"controller": "welcome", "worker count": 1} 2021-03-11T21:04:57.006+0800 INFO controllers.Welcome reconciling welcome {"welcome": "default/welcome-sample"} 2021-03-11T21:04:57.006+0800 INFO controllers.Welcome create deployment success! {"welcome": "default/welcome-sample"} 2021-03-11T21:04:57.056+0800 INFO controllers.Welcome create service success! {"welcome": "default/welcome-sample"} 2021-03-11T21:04:57.056+0800 INFO controllers.Welcome create deploy and service success! {"welcome": "default/welcome-sample"} 2021-03-11T21:04:57.056+0800 DEBUG controller-runtime.controller Successfully Reconciled {"controller": "welcome", "request": "default/ welcome-sample"}
此时我们通过代码清单 1-19 验证控制器是否完成对象创建及状态更新。
代码清单 1-19
$ kubectl get deploy NAME READY UP-TO-DATE AVAILABLE AGE welcome-sample 1/1 1 1 3m2s
通过代码清单 1-20 可以看到,Deployment 已经创建成功,并且达到期望的副本数量。
代码清单 1-20
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 212d welcome-sample LoadBalancer 10.96.242.198 <pending> 8080:32181/ TCP 4m12s
通过代码清单 1-21 可以看到,此时 Service 已经创建成功,并且分配到集群 IP,我
们通过集群 IP 访问应用查看。
代码清单 1-21
$ curl -L 10.96.242.198:8080/hello/ <html ng-app="redis"> <head> <title>Hello </title> </head> <body> <div style="width: 50%; margin-left: 20px"> <h2>Welcome! This is an Opetator Demo</h2> </div> </body> </html> Hello myfriends