Operator Framework[1]是由CoreOS开发,后被RedHat收购的一个开源工具包,它可以有效的、自动化的和可扩展的方式管理 Kubernetes原生应用程序。该框架下包含Operator SDK,可协助开发人员利用自己的专业知识来引导和构建Operator,而无需了解 Kubernetes API复杂性。今天我们就学习它,用于创建一个基于Ansible的Operator应用(之前小白在《Loki Operator简明教程》中也简单聊到过),它可以利用现有 Ansible playbook和模块来部署和管理Kubernetes资源作为统一应用程序的生命周期,而无需编写任何Go代码。
安装
- Homebrew安装 (MacOS)
如果你使用的是Homebrew,你可以用以下命令安装SDK CLI工具。
代码语言:javascript复制$ brew install operator-sdk
- 二进制安装
$ export ARCH=$(case $(uname -m) in x86_64) echo -n amd64 ;; aarch64) echo -n arm64 ;; *) echo -n $(uname -m) ;; esac)
$ export OS=$(uname | awk '{print tolower($0)}')
$ export OPERATOR_SDK_DL_URL=https://github.com/operator-framework/operator-sdk/releases/download/v1.10.0
#下载operator-sdk
$ curl -LO ${OPERATOR_SDK_DL_URL}/operator-sdk_${OS}_${ARCH}
$ chmod x operator-sdk_${OS}_${ARCH} && sudo mv operator-sdk_${OS}_${ARCH} /usr/local/bin/operator-sdk
- 编译安装
$ git clone https://github.com/operator-framework/operator-sdk
$ cd operator-sdk
$ git checkout master
$ make install
初始化
1. 初始化项目
使用operator-sdk命令来快速初始化这个项目的空间。
代码语言:javascript复制$ mkdir loki-operator
$ cd loki-operator
$ operator-sdk init --plugins=ansible --domain cloudxiaobai.com
这条命令生成的文件中有一个Kubebuilder的PROJECT描述文件。从里面我们可以得知,该工程是一个基于Ansible实现的项目
代码语言:javascript复制$ cat PROJECT
domain: loki.cloudxiaobai.com
layout:
- ansible.sdk.operatorframework.io/v1
plugins:
manifests.sdk.operatorframework.io/v2: {}
scorecard.sdk.operatorframework.io/v2: {}
projectName: cloudxiaobai
version: "3"
2. 创建API
代码语言:javascript复制$ operator-sdk create api
--group=plugins --version=v1 --kind=Loki
--generate-playbook
--generate-role
上面这条命令可以快速帮助我们生成这个项目的脚手架。其中包含如下内容:
- 生成了Loki这个CRD的定义,和一个Loki CR的样例;
- 生成了Controller,用于管理CR的的服务在Kubernetes集群上的状态
- 生成了Ansible Playbook的目录结构
- 生成了一个 watches.yaml文件,它用来将CR资源与Ansible Playbook关联起来。
Ansible Playbook
在完成项目的初始化后,我们在项目下就得到了以下主要的目录。
代码语言:javascript复制├── Dockerfile
├── config
│ ├── samples
│ │ └── plugins_v1_loki.yaml //Loki的CR样例
├── playbooks //playbook的入口
│ └── loki.yml
├── roles //playbook文件
│ └── loki
│ ├── README.md
│ ├── defaults
│ │ └── main.yml
│ ├── files
│ ├── handlers
│ │ └── main.yml
│ ├── meta
│ │ └── main.yml
│ ├── tasks
│ │ └── main.yml
│ ├── templates
│ └── vars
│ └── main.yml
熟悉Ansible的朋友看到这个就比较眼熟了,接下来我们就开始写入一个Loki服务简单playbook文件来演示它的基本逻辑。
1. 修改 roles/loki/tasks/main.yaml
代码语言:javascript复制- name: Loki Operator | Loki | StatefulSet
community.kubernetes.k8s:
definition: "{{ lookup('template', 'statefulset.yaml') | from_yaml }}"
可以看到ansible是通过community.kubernetes[2]的k8s模块来跟kubernetes交互的。上面这条task的意思是让ansible去templates目录下去寻找statefulset.yaml文件,然后将它渲染成yaml文件后提交给kubernetes。
2. 创建 roles/loki/template/statefulset.yaml
statefulset.yaml是真正需要部署的ansible模版文件,我们需要创建的内容如下:
代码语言:javascript复制#jinja2:lstrip_blocks: True
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: '{{ansible_operator_meta.name}}'
namespace: '{{ansible_operator_meta.namespace}}'
labels:
app.kubernetes.io/name: '{{ ansible_operator_meta.name }}-loki-system'
spec:
replicas: {{ replicas }}
serviceName: '{{ansible_operator_meta.name}}-headless'
selector:
matchLabels:
app.kubernetes.io/name: '{{ ansible_operator_meta.name }}-loki-system'
template:
metadata:
labels:
app.kubernetes.io/name: '{{ ansible_operator_meta.name }}-loki-system'
spec:
containers:
- name: loki-system
image: grafana/loki:2.2.1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3100
name: http-3100
protocol: TCP
上面的Ansible应用模版中有几点信息值得强调:
#jinja2:lstrip_blocks: True
保留yaml文件中的空格、缩进等语句{{ansible_operator_meta.name}}
定义了CR里面控制的Loki资源名称{{ansible_operator_meta.namespace}}
定义了CR作用的命名空间{{ replicas }}
自定义的CR变量
可以看到我们声明了一个名replicas的自定义变量,我们需要通过它来控制服务的副本数。Ansible中的变量取值由用户创建的CR来控制。
Ansible的roles文件中task实际上定义了CR的状态,在Kubernetes在创建资源时,由于允许输入任意字段,所以我们不需要在CRD中实际定义CR字段类型的声明。虽然在Operator SDK中它不能被自动生成,不过还是建议在实际使用时最好添加上CRD的字段说明,以便Kubernetes用户在使用CR时可以看到它对应的描述信息。
3. 创建CR
默认情况下Operator SDK会创建一个默认的CR样例文件,它位于config/samples/目录下。在本文中,我们的CR文件名称为plugins_v1_loki.yaml
代码语言:javascript复制apiVersion: plugins.cloudxiaobai.com/v1
kind: Loki
metadata:
name: loki
spec:
replicas: 1
构建与部署
当完成应用的playbook编写后,我们就可以使用make命令进行镜像的构建和上传。我们可以提前编辑Makefile来自定义镜像的名称,
代码语言:javascript复制IMAGE_TAG_BASE ?= cloudxiaobai.com/cloudxiaobai
IMG ?= $(IMAGE_TAG_BASE):$(VERSION)
这样,当我们每次build镜像时,产生固定的镜像名称。
1. 构建Operator镜像
代码语言:javascript复制$ make docker-build docker-push VERSION="0.1"
构建和上传operator镜像。
2. 部署Operator服务
代码语言:javascript复制$ make deploy
# kustomize build config/default | kubectl apply -f -
可以看到在部署时调用了kustomize将config/default目录下的文件进行构建后提交到kubernetes。这里面需要注意的是,默认情况下,kustomize会创建一个名为<project-name>-system
的新命名空间用于部署Operator。
当部署完成后,我们就可以用kubectl进行验证服务状态。
代码语言:javascript复制$ kubectl get deployment -n cloudxiaobai--system
NAME READY UP-TO-DATE AVAILABLE AGE
loki-controller-manager 1/1 1 1 8m
3. 提交CR
将上文的CR声明文件提交到kubernetes
代码语言:javascript复制$ cat config/samples/plugins_v1_loki.yaml
apiVersion: plugins.cloudxiaobai.com/v1
kind: Loki
metadata:
name: loki
spec:
replicas: 1
$ kubectl apply -f config/samples/plugins_v1_loki.yaml
之后我们就可以通过以下命令进行验证服务是否达到预期
代码语言:javascript复制$ kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
cloudxiaobai-31e1d3526-a1klm 1/1 1 1 1m
如果我们要调整Loki的副本数,我们只需有修改CR中的replicas变量即可,也可以通过patch的方式修改。
代码语言:javascript复制$ kubectl patch loki cloudxiaobai -p '{"spec":{"size": 2 }}' --type=merge
4. 删除资源
直接执行下列命令就能分别删除CR和Operator关联的所有资源
代码语言:javascript复制$ kubectl delete -f config/samples/plugins_v1_loki.yaml
$ make undeploy
小技巧
1. Ansible变量转化
自定义CR中的spec字段中所有变量的名称均被操作符转换为snake_case(下横线)命名格式。例如,我们在spec中写入了一个变量serviceAccount,那么在ansible中会被转成service_account。小白建议可以在watches.yaml中将snakeCaseParameters设置为false来禁用这种大小写转换。
2. 使用默认值
为了能将ansible template适配大部分场景,小白建议在模版中使用默认值,避免在CR中没有定义变量而造成的playbook执行报错。例如,我们在CR中定义了Loki的镜像信息如下。
代码语言:javascript复制spec:
image:
repository: grafana/loki
tag: 2.1.1
那我们在template就可以如下编写:
代码语言:javascript复制containers:
- name: loki-system
image: '{{service.cluster.loki.image | default('grafana/loki')}}:{{version | default('latest')}}'
这里,如果我们没在CR中定义image的变量,当playbook执行到这里时,就会采用grafana/loki:latest
镜像。
3. 巧用items
由于community.kubernetes.k8s
模块不允许提交一个allinone的yaml文件,所以当在实际编写playbook时,如果想在一个task时里面完成所有yaml提交时,我们可以使用如下语句:
- name: Loki Operator | Loki | Deploy
community.kubernetes.k8s:
definition: "{{ lookup('template', item) | from_yaml }}"
with_items:
- statefulset.yaml
- service.yaml
4. 控制模块启用或关闭
如果我们的CR中希望能够控制某些服务的启用或关闭时,通常情况下会直接使用enabled: true
来做明确的定义,例如下面这个CR
spec:
redis:
enabled: true
那么,我们在写playbook时,可以通过在state中判断CR中的该变量来决定是否执行,如下:
代码语言:javascript复制- name: Loki Operator | Reids Cache | PVC
community.kubernetes.k8s:
state: '{{"present" if redis is defined and redis.enabled else "absent"}}'
definition: "{{ lookup('template', 'redis/pvc.yaml') | from_yaml }}
5. Owner References
Owner References是Kubernetes中的垃圾删除机制,它能够在删除CR后进行后续清理。默认情况下,ownerReferences由代理在ansible执行时注入。在当CR和被管理的资源在同一个命名空间下时,ownerReferences会在资源中如下显示:
代码语言:javascript复制metadata:
ownerReferences:
- apiVersion: loki.cloudxiaobai.com/v1
kind: loki
name: loki
uid:fda3b711-1f25-4d14-bf84-cf70f48e7ff4
CR和被管理的资源不在一个命名空间下时,ownerReferences便是通过如下的annotation字段来定义的:
代码语言:javascript复制operator-sdk/primary-resource: {metadata.namespace}/{metadata.name}
operator-sdk/primary-resource-type: {kind}.{group}
当然,如果你要禁用ownerReferences的话,如下修改dockerfile即可:
代码语言:javascript复制ENTRYPOINT ["/usr/local/bin/entrypoint", "--inject-owner-ref=false"]
6. 任务并发
默认情况下,ansible调用的runtime.NumCPU()
来设置最大并发协调值,可以看到它取决于你主机的CPU核心数。增加并发协调的数量和允许并发处理事件,可以提高ansible执行任务时的协调性能。我们可以在operator的启动参数来手动设置
- name: manager
args:
- "--max-concurrent-reconciles"
- "3"
总结
本文通过一个小例子介绍了如何通过Operator SDK快速构建、管理Loki应用。并通过6个小技巧让读者在使用过程中尽量避坑。目前小白在实际工作中,将Ansible Operator主要应用在kubernetes平台初始化时各种插件、三方服务的安装管理,如prometheus、loki、grafna等等,希望它也能在其它方面帮助到大家。
参考资料
[1]
Operator Framework: https://sdk.operatorframework.io/docs/overview/
[2]
community.kubernetes: https://docs.ansible.com/ansible/2.10/collections/community/kubernetes/k8s_module.html