Operator实战3:Operator开发过程遇到的问题

原创 吴就业 220 0 2023-06-27

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/e2ee4eec386342378a6b3ba81e5a2107

作者:吴就业
链接:https://wujiuye.com/article/e2ee4eec386342378a6b3ba81e5a2107
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

云原生企业级实战笔记专栏

kubebuilder使用helm代替kustomize

kubebuilder默认使用kustomize,因此生成的yaml会存放在config/crd目录下,替换成helm后,如果不想维护多个git仓库,或者一份yaml存两份的话,我们就需要修改kubebuilder生成yaml存放的目录,放到heml chart的templates目录下。

怎么修改?其实搜索一下“config/crd”在哪个文件出现,就能知道是在哪里配置的了。

主要是修改kubebuilder项目的Makefile文件。makefile的代码其实就是kubebuilder提供的命令的实现。

假设我们的Operator项目名是my-operator,在项目下创建一个名为chart的目录,作为my-operator helm chart的根目录。

然后我们需要修改这些地方:

1.修改crd代码生成存放的目录

.PHONY: manifests
manifests: controller-gen 
   $(CONTROLLER_GEN) rbac:roleName=my-operator-manager-role crd webhook paths="./..." output:crd:artifacts:config=chart/crds output:rbac:artifacts:config=chart/templates output:webhook:artifacts:config=chart/templates

其实就是修改make manifests命令,该命令用于生成 WebhookConfiguration、ClusterRole 和 CustomResourceDefinition的yaml文件。 参数说明:

2.install、unstall命令改成helm的install、unstall命令。

.PHONY: install
install: manifests ## 安装helm chart,会检查语法错误、自动打包
   helm lint chart
   helm package chart/
   helm install test my-operator-helm-chart-0.1.0.tgz

.PHONY: uninstall
uninstall: manifests ## 卸载helm chart
   helm uninstall test

3.由于不使用kustomize部署了,需要注释或删掉部署命令。

## 用helm安装部署不能用这些命令
#.PHONY: deploy
#deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
#  cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
#  $(KUSTOMIZE) build config/default | kubectl apply -f -
#
#.PHONY: undeploy
#undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
#  $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f -

4.不需要kustomize了,注释或删掉kustomize的安装命令。

#KUSTOMIZE ?= $(LOCALBIN)/kustomize

#KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"
#.PHONY: kustomize
#kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading.
#$(KUSTOMIZE): $(LOCALBIN)
#  @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \
#     echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \
#     rm -rf $(LOCALBIN)/kustomize; \
#  fi
#  test -s $(LOCALBIN)/kustomize || { curl -Ss $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); }

了解了Makefile之后,我们也可以自己添加一些方便自己开发测试的命令。例如,我将install、unstall命令改为仅安装crd,添加install-chart和uninstall-chart安装和卸载helm chart,添加install simple用于apply demo。

##@ Deployment

ifndef ignore-not-found
  ignore-not-found = false
endif

.PHONY: install
install: manifests ## 生成crd并安装crd
   kubectl apply -f chart/crds

.PHONY: uninstall
uninstall: ## 卸载crd
   kubectl delete --ignore-not-found=$(ignore-not-found) -f chart/crds

.PHONY: install-chart
install-chart: manifests ## 生成crd、生成helm chart,并安装helm chart
   helm lint chart
   helm package chart/
   helm install test my-operator-helm-chart-0.1.0.tgz

.PHONY: uninstall-chart
uninstall-chart: ## 卸载helm chart
   helm uninstall test

##@ Demo
.PHONY: install-simple
install-simple: manifests install ## 安装demo yaml
   kubectl apply -f example

.PHONY: uninstall-simple
uninstall-simple: ## 安装demo yaml
   kubectl delete --ignore-not-found=$(ignore-not-found) -f example

代码改了但似乎没生效-镜像拉取问题

如果是本地kind启动的k8s集群,要使kind启动的k8s集群能够拉取到本地镜像,可以使用命令:kind load docker-image ${IMG},并且不管镜像的版本号是不是latest,每次镜像更新都需要执行一次这个命令。

如果是将镜像推送到远程仓库,那么遇到代码改了但似乎没生效这个问题,一般是由于镜像tag没变,或者imagePullPolicy配置为IfNotPresent,导致镜像没更新。

首先检查镜像的tag是不是latest,如果是,则检查imagePullPolicy是不是配置了,且配置为IfNotPresent了,这会导致即使是latest,容器部署的时候也不会拉新的镜像。

如果tag版本保留latest,那么建议删除imagePullPolicy的配置,因为imagePullPolicy默认值会跟据tag是不是latest设置。如果是latest,则imagePullPolicy默认值为Always,否则为IfNotPresent。

如果tag版本号是指定版本号,那么imagePullPolicy需要手动指定为Always,或者就是每次更新镜像都修改一下版本号。

使用ConfigMap替代Apollo配置中心的最少改动方案

如果中间件当前使用apollo作为配置输入,想要改成使用ConfigMap,首先是中间件配置这一块需要重构,将获取和监听配置改变抽象为接口,提供apollo实现和configmap实现,然后通过一个环境变量实现切换选择apollo实现,还是configmap实现。

Configmap实现其实是读本地配置文件,通过将Configmap挂盘使用,让进程直接读文件。这样的好处是中间件不需要像k8s spring cloud框架那样将k8s的client-go也依赖进项目,导致部署在虚拟机上跑不起来,以及中间件本身单元测试或测试需要依赖k8s环境问题。

      containers:
        - name: xxx
          image: {{.Spec.Image}}
          .....
          volumeMounts:
            - name: xxx-configmap-volume
              mountPath: "/etc/config/xxx"
      ......
      volumes:
        - name: xxx-configmap-volume
          configMap:
            name: application
            items:
              - key: application
                path: application.json
        .......

另一个比较坑的问题。当我们一个中间件使用多个configmap的时候,每个configmap的挂盘路径是不能一样的,否则后面的会覆盖前面的,另外subPath也是不能用的,这样会导致configmap内容更新的时候,容器里面的配置文件不会更新。

正确的做法是每个configmap一个独立的挂盘路径,然后配置文件的路径通过环境变量方式传给中间件。

      containers:
        - name: lz-common-bfe
          image: {{.Spec.Image}}
          env:
          - name: CONFIG_PATH_LEGACY_CM
            value: "/etc/config/bfe/legacy-cm/legacy-cm.json"
          - name: CONFIG_PATH_LEGACY_AS
            value: "/etc/config/bfe/legacy-as/legacy-as.json"
            .......
          .....
          volumeMounts:
            - name: bfe-legacy-cm-configmap-volume
              mountPath: "/etc/config/bfe/legacy-cm"
            - name: bfe-legacy-as-configmap-volume
              mountPath: "/etc/config/bfe/legacy-as"
              ......
      ......
      volumes:
        - name: bfe-legacy-cm-configmap-volume
          configMap:
            name: bfe-legacy-cm
            items:
              - key: bfe-legacy-cm
                path: legacy-cm.json
        - name: bfe-legacy-as-configmap-volume
          configMap:
            name: bfe-legacy-as
            items:
              - key: bfe-legacy-as
                path: legacy-as.json
        .......

环境变量的注入以及传递

如果我们当前项目在打包镜像的时候会注入一些环境变量,或者存在一个发布平台,在部署服务时会生成一个Deployment资源,能够通过Deployment资源注入一些环境变量。而改成通过Operator生成Deployment来部署服务的方式,由于Deployment资源是Operator生成的,在打包部署Operator阶段就没办法介入修改这个Deployment。不过可以将环境变量注入到Operator的Deployment资源,再由Operator将环境变量注入到中间件的Deployment资源。

例如,Operator在生成服务的Deployment资源时注入环境变量信息:

func FullEnv(d *appv1.Deployment) {
   for index := 0; index < len(d.Spec.Template.Spec.Containers); index++ {
      envArr := d.Spec.Template.Spec.Containers[index].Env
      envArr = append(envArr,
         v1.EnvVar{
            Name:  "REGION",
            Value: GetRegion(),
         },
         v1.EnvVar{
            Name:  "DEPLOY_ENV",
            Value: GetDeployEnv(),
         },
         v1.EnvVar{
            Name:  "AZ",
            Value: GetAz(),
         },
         v1.EnvVar{
            Name:  "IDC",
            Value: GetIdc(),
         },
         v1.EnvVar{
            Name:      "SERVICE_NAME",
            Value:     GetServiceName(),
            ValueFrom: nil,
         },
      )
      // 数组元素不是指针,所以Env是copy的,只是改copy对象的字段是不生效的
      d.Spec.Template.Spec.Containers[index].Env = envArr
   }
}

Kubebuilder单测跑不起来

测试用例编写参考:https://cloudnative.to/kubebuilder/cronjob-tutorial/writing-tests.html#%E7%BC%96%E5%86%99%E6%8E%A7%E5%88%B6%E5%99%A8%E6%B5%8B%E8%AF%95%E7%A4%BA%E4%BE%8B、https://github.com/Azure/azure-databricks-operator

如果直接在idea点run执行单元测试错误:"error": "fork/exec /usr/local/kubebuilder/bin/etcd: no such file or directory",然后手动安装了etcd之后执行单元测试错误:fork/exec /usr/local/kubebuilder/bin/kube-apiserver: no such file or directory

解决方案一:使用已有集群测试

我们使用kind或minikube安装一个集群,避免影响公共的测试集群。

然后将~/.kube/config文件改为kind/minikube集群的config,测完后再改回测试环境k3s的config。 然后启动单元测试时需要添加环境变量:USE_EXISTING_CLUSTER=true,意思是使用存在的k8s集群测试,而不需要搭建一个本地集群,也就不需要安装etcd、kube-apiserver了。

参考:https://cloudnative.to/kubebuilder/reference/envtest.html

解决方案二:使用make test执行单测

执行make test命令会自动下载etcd、kubectl、kube-apiserver到项目bin/k8s目录下。 make testmake manifestsmake generatemake envtestgo test的组合,其中make envtest命令会下载setup-envtest二进制到项目的bin目录下,最后make test还会执行:

KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out

这条命令包括了设置KUBEBUILDER_ASSETS环境变量,然后执行go test命令,而设置环境变量又隐藏了一条命令:

$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path

替换变量后就是:

./bin/setup-envtest use 1.26.0 --bin-dir ./bin -p path

这条命令就是使用setup-envtest去下载etcd、kubectl、kube-apiserver这些工具,-p path输出的路径是“./bin/k8s/1.26.0-darwin-amd64”。

Helm chart和finalizer特性冲突问题

在使用finalizer时发现一个问题,在helm uninstall的时候,operator会卸载掉,这个时候自定义的资源(如bfe)变成删除状态后,由于没有operator处理finalizer,就会一直删不掉,所以中间件部署和operator部署不应打包在一个helm chart中。

参考开源项目:https://github.com/mysql/mysql-operator,该项目分两个helm chart打包operator和innodb mysql cluster。我们可以这样分:CRD和Operator打包到一个chart,CR则打包到另一个chart。

#云原生

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

terraform篇02:为私有云开发一个terraform provider

很多企业内部为了不与云厂商绑定,避免上云容易下云难的尴尬,以及企业内部可能也会做私有云,或者封装一个混合云平台,因此不能直接用云厂商提供的provider。

terraform篇01:基础设施即代码,初识terraform,用代码减少沟通成本

通常申请基础设施,我们需要向运维描述我们需要什么基础设施、什么规格,运维根据我们的描述去检查是否已经申请过这样的资源,有就会直接给我们使用基础设施的信息,没有再帮我们申请,然后告诉我们使用基础设施的信息,例如mysql的jdbc和用户名、密码。如果将描述代码化,基础设施的申请自动化,就能实现“基础设施即代码”。而terraform就是实现“将描述代码化”的工具软件。

Operator实战4:如何获取已经被删除的pod的日记

在Job场景,如果Job达到backoffLimit还是失败,而且backoffLimit值很小,很快就重试完,没能及时的获取到容器的日记。而达到backoffLimit后,job的controller会把pod删掉,这种情况就没办法获取pod的日记了,导致无法得知job执行失败的真正原因,只能看到job给的错误:"Job has reached the specified backoff limit"。

Operator实战2:实现webhook修改Job的最大重试次数

terraform-controller默认Job会一直重试,导致重复申请资源。

Operator实战1:使用kubebuilder开发一个部署web服务的Operator

举一个非常简单的需求场景,仅用于介绍如何使用kubebuilder开发一个Operator,非真实需求场景。

中间件云原生利器:Operator,Operator是什么?

新的云原生中间件很难短时间内覆盖到企业项目中,企业走云原生这条道路,还需要考虑传统中间件如何上云的问题。最需要解决的是如何容器化部署,以及自动化运维。这就不得不借助Operator了。