使用 Argo Workflow 组织跨云运维的可能性
在微服务、容器化和 IaC 等概念普及之前,自动化通常是使用过程性操作进行的,例如摘流——升级——恢复的过程。为了运维方便,通常这些操作序列会由所谓的运维流程编排工具完成,例如 AWS 的 SSM Automation,或者阿里云的 OOS 等。随着运维自动化的要求逐步提高,这些工具的编排能力也逐步扩展,出现了插件扩展、循环、跳转等更复杂的行为,甚至还出现了人工审批等蜜汁操作。自动化的编排复杂度也不断延伸——AWS 公开的作业脚本中已经出现了超过 3000 行 50 个步骤的庞然大物。
古时候的自动化运维通常是围绕着虚拟机进行的——管你是谁家的机器,只要你开了 SSH,或者装了我家的 Agent,你就跟我姓了。但是随着公有云服务能力的不断扩展,虚拟机的运维操作占比就逐步降低了,围绕 API 进行的运维能力逐步超过了虚拟机,成为主流。
不管有用没用,多云已经成为部分架构师的口头禅了。再加上前面的两个情况—— SRE 平台需要有一个能跨云的、面向 API 的、具备复杂编排能力并且能用编程方式进行扩展的自动化工具了,另外随着面对资源规模的不同,必要的并发能力和横向扩展的能力也是必要的。经过一番比对,我觉得 Argo Workflow 可能是个合适的选择。
Argo 大概于 2017 年以 GitOps 工具的形态,由 Intuit 发布,2020 年进入 CNCF 孵化,2022 年毕业,现在已经成长为包含 Argo CD、Argo Workflows、Argo Events 以及 Argo Rollouts 的生态群,并在 2022 年开始有了 Argo Con 峰会。
架构
根据官方提供的组件图可以看出:
- Argo Workflows 运行在 Kubernetes 集群里。
- 可以利用 Kubernetes API 对 Argo 进行控制。
- 用户可以通过 CLI、Kubectl 和 Web UI 三种方式和 Argo 进行交互。
- 可以对接外部 idP,让 Argo Workflows 具备单点登录能力
- Workflow 也是以 Pod 的形式在集群中运行的。
下图则是对工作流的一个描述。
这里不难发现,Argo Workflow 除了支持工作流之外,还支持了 DAG,它的工作流节点是用多容器 Pod 的形式运行的——每个 Pod 中包含 Wait、Init 和 Main 三个容器。
功能
Argo Workflow 提供了非常丰富的自动化编排能力。流程方面,提供了循环、条件、递归、暂停、恢复等常见内容;容错方面提供了超时、重试、异常捕捉/跳转等能力;另外他还支持脚本执行、变量定义和处理、工件传递等用于应对复杂场景的功能。功能方面,个人评估是略强于 AWS 的 SSM Automation 的。
起步
下文均用目前的
v3.5.6
为例
Argo Workflows 的快速部署方式非常简单,下面两行命令即可:
$ kubectl create namespace argo
namespace/argo created
$ kubectl apply -n argo -f https://github.com/argoproj/argo-workflows/releases/download/v3.5.6/install.yaml
...
priorityclass.scheduling.k8s.io/workflow-controller created
deployment.apps/argo-server created
deployment.apps/workflow-controller created
当然,这只是一个测试环境的玩法,项目也用 Helm Chart 的方式提供了用于生产环境的部署途径。
服务启动后,可以看到两个 Pod:
$ kubectl get po -n argo
NAME READY STATUS RESTARTS AGE
workflow-controller-5bb8788d57-sxnv2 1/1 Running 0 29s
argo-server-67bcf4bb48-sq9jp 1/1 Running 0 29s
为了简化使用可以进行一点修改:
$ kubectl patch deployment \
argo-server \
--namespace argo \
--type='json' \
-p='[{"op": "replace", "path": "/spec/template/spec/containers/0/args", "value": [
"server",
"--auth-mode=server"
]}]'
默认的认证方式需要使用 Service Account,并且需要进行较多的 RBAC 配置,有些复杂,所以这里改成了服务侧自行认证。
然后把服务改成 NodePort:
$ kubectl patch svc argo-server -n argo -p '{"spec": {"type": "NodePort"}}'
service/argo-server patched
这样,就可以在获取端口后,直接浏览器直接访问 Argo UI 了(注意这里默认使用的是 https 协议)。
教程中提供了一个 Hello World
流程,内容如下:
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: hello-world-
labels:
workflows.argoproj.io/archive-strategy: "false"
annotations:
workflows.argoproj.io/description: |
This is a simple hello world example.
spec:
entrypoint: whalesay
templates:
- name: whalesay
container:
image: docker/whalesay:latest
command: [cowsay]
args: ["hello world"]
这个简单的 YAML 可以看到 Argo 工作流定义中的基本元素:
- 这是一个 CRD,类型是
argoproj.io/v1alpha1
的Workflow
。 - 这一清单需要重复使用,因此
metadata
中没有给出 Name,而是给出了generateName
。 spec.templates
中保存的步骤的定义,并使用spec.entrypoint
指定了入口环节。- 仅有的一个步骤中,使用一个容器镜像,并指定了执行命令,输出一段文字。
使用 kubectl create
提交工作流,看看结果:
$ kubectl create -f install.yaml
workflow.argoproj.io/hello-world-fdddc created
用浏览器打开控制台,浏览 workflows
页面,可以看到,出错了:
错误原因也很 Kubernetes,就是 RBAC 权限不足:
Error (exit code 1): pods "hello-world-fdddc" is forbidden: User "system:serviceaccount:default:default" cannot patch resource "pods" in API group "" in the namespace "default"
看来这里用到的什么修改 Pod 的功能,看一下命名空间中的 hello-world
,会看到它的内容和我们在模板中指定的简单几行完全不同,多出了 initContainer 和 Sidecar。主容器的命令也被加入了新的内容。
这里偷个懒,直接借用 Argo 明明空间里的 Argo SA,用法很简单,在 YAML 的 entrypoint 字段后加入同级元素 serviceAccountName: argo
,并且在 Argo 命名空间里创建:
$ kubectl create -f hello-world.yaml -n argo
workflow.argoproj.io/hello-world-l4q2x created
浏览器控制台可以看到,这次成功运行,并且输出了结果:
用 argo
CLI 也可以方便的查看:
$ argo list -A
NAMESPACE NAME STATUS AGE DURATION PRIORITY MESSAGE
argo hello-world-l4q2x Succeeded 7h 10s 0
default hello-world-fdddc Error 8h 10s 0 Error (exit c
场景
用户可以通过 Restful API、SDK、CLI 和 Web 控制台来访问 AWS 服务,自动化操作通常会使用 SDK 或者 CLI 的方式。这里我们设置一个场景:查询当前账户的 EC2 实例,并关机。
这里需要用到几个能力:
- 使用容器模板加载 AWS 凭据,并运行 AWS CLI 的能力
- 将 AWS CLI 结果输出为变量的能力
- 循环处理列表变量的能力
加载 Secret
假设我们的凭据文件保存在当前目录的 credentials
文件中,我们需要将它创建为 Secret,并在后续的容器模板中进行加载:kubectl create secret generic awskey --from-file=credentials
。
工作流中想要加载 Secret,跟 Pod 是很相似的,例如我们将会这样编写列出 EC2 实例的环节:
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: shutdown-ec2-
labels:
workflows.argoproj.io/archive-strategy: "false"
spec:
serviceAccountName: argo
entrypoint: list-instances
volumes:
- name: aws-secret
secret:
secretName: awskey
templates:
- name: list-instances
container:
image: amazon/aws-cli:2.15.43
args:
- "ec2"
- "describe-instances"
- "--output"
- "json"
- "--region"
- "ap-northeast-1"
- "--query"
- "Reservations[].Instances[].InstanceId"
volumeMounts:
- name: aws-secret
mountPath: /root/.aws
这个步骤写完之后,可以运行一下,看看结果:
$ argo submit -n argo --watch aws-list-ec2.yaml
...
STEP TEMPLATE PODNAME DURATION MESSAGE
✔ shutdown-ec2-7ngl9 list-instances shutdown-ec2-7ngl9 4s
查看日志会发现,成功返回了一个 JSON 数组,其中包含了我们需要的实例 ID 列表。
循环关闭
接下来把这个工作流改为多模板的模式,便于我们加入参数和循环能力。
实际上 AWS CLI 是直接支持用数组方式关闭多个 EC2 实例的
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: shutdown-ec2-
labels:
workflows.argoproj.io/archive-strategy: "false"
spec:
serviceAccountName: argo
entrypoint: shutdown-all-ec2
volumes:
- name: aws-secret
secret:
secretName: awskey
templates:
- name: shutdown-all-ec2
steps:
- - name: list
template: list-instances
- - name: shut
template: shutdown-ec2
arguments:
parameters:
- name: ec2id
value: "{{item.InstanceId}}"
withParam: "{{steps.list.outputs.result}}"
- name: list-instances
container:
image: amazon/aws-cli:2.15.43
command: ["aws"]
args:
- --output
- json
- --region
- ap-northeast-1
- ec2
- describe-instances
- --query
- "Reservations[].Instances[]"
volumeMounts:
- name: aws-secret
mountPath: /root/.aws
- name: shutdown-ec2
inputs:
parameters:
- name: ec2id
container:
image: amazon/aws-cli:2.15.43
command: ["aws"]
args:
- "ec2"
- "stop-instances"
- --region
- ap-northeast-1
- "--instance-ids"
- "{{inputs.parameters.ec2id}}"
volumeMounts:
- name: aws-secret
mountPath: /root/.aws
上面的 YAML 的主要变化:
- 把原有的单步骤流程拓展成了多步骤
- 列表中加入了格式化内容,精简输出
将列表结果作为循环变量,传递给了用于关机的后续步骤
arguments: parameters: - name: ec2id value: "{{item}}" withParam: "{{steps.list.outputs.result}}"
这一段将步骤 list
的控制台输出作为循环变量,传递给 shutdown-ec2
模板的 ec2id
参数,逐个关机。
注意这里的写法,使用 step
的方式对模板进行引用,形成多步骤流程。
运行后,可以看到 Argo 用并发的形式,进行了批量关机操作。
补充
首先是 AWS CLI 提供了丰富的功能,调用起来实在是比 SDK 方便太多,所以这里用这种形式来简化操作。
其次是这里对输出变量的做法,其实 Argo 提供了丰富的内置函数,可以对这些输出内容进行较为复杂的处理,当然,也可以用 Script 步骤进行更加细致的定制工作。
再次,过程中直接加载 AWS 凭据的方法非常不推荐,关于容器环境中的敏感信息管理,已经有很多陈述,这里就不节外生枝了。
最后,Argo 的文档真烂,真的烂。。