使用 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 峰会。

架构

根据官方提供的组件图可以看出:

  1. Argo Workflows 运行在 Kubernetes 集群里。
  2. 可以利用 Kubernetes API 对 Argo 进行控制。
  3. 用户可以通过 CLI、Kubectl 和 Web UI 三种方式和 Argo 进行交互。
  4. 可以对接外部 idP,让 Argo Workflows 具备单点登录能力
  5. Workflow 也是以 Pod 的形式在集群中运行的。

下图则是对工作流的一个描述。

这里不难发现,Argo Workflow 除了支持工作流之外,还支持了 DAG,它的工作流节点是用多容器 Pod 的形式运行的——每个 Pod 中包含 Wait、Init 和 Main 三个容器。

功能

Argo Workflow 提供了非常丰富的自动化编排能力。流程方面,提供了循环、条件、递归、暂停、恢复等常见内容;容错方面提供了超时、重试、异常捕捉/跳转等能力;另外他还支持脚本执行、变量定义和处理、工件传递等用于应对复杂场景的功能。功能方面,个人评估是略强于 AWS 的 SSM Automation 的。

起步

下文均用目前的 v3.5.6 为例

Argo Workflows 的快速部署方式非常简单,下面两行命令即可:

代码语言:javascript
复制
$ 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:

代码语言:javascript
复制
$ 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

为了简化使用可以进行一点修改:

代码语言:javascript
复制
$ 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:

代码语言:javascript
复制
$ kubectl patch svc argo-server -n argo -p '{"spec": {"type": "NodePort"}}'
service/argo-server patched

这样,就可以在获取端口后,直接浏览器直接访问 Argo UI 了(注意这里默认使用的是 https 协议)。

教程中提供了一个 Hello World 流程,内容如下:

代码语言:javascript
复制
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 工作流定义中的基本元素:

  1. 这是一个 CRD,类型是 argoproj.io/v1alpha1Workflow
  2. 这一清单需要重复使用,因此 metadata 中没有给出 Name,而是给出了 generateName
  3. spec.templates 中保存的步骤的定义,并使用 spec.entrypoint 指定了入口环节。
  4. 仅有的一个步骤中,使用一个容器镜像,并指定了执行命令,输出一段文字。

使用 kubectl create 提交工作流,看看结果:

代码语言:javascript
复制
$ kubectl create -f install.yaml
workflow.argoproj.io/hello-world-fdddc created

用浏览器打开控制台,浏览 workflows 页面,可以看到,出错了:

错误原因也很 Kubernetes,就是 RBAC 权限不足:

代码语言:javascript
复制
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 命名空间里创建:

代码语言:javascript
复制
$ kubectl create -f hello-world.yaml -n argo
workflow.argoproj.io/hello-world-l4q2x created

浏览器控制台可以看到,这次成功运行,并且输出了结果:

argo CLI 也可以方便的查看:

代码语言:javascript
复制
$ 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 实例,并关机。这里需要用到几个能力:

  1. 使用容器模板加载 AWS 凭据,并运行 AWS CLI 的能力
  2. 将 AWS CLI 结果输出为变量的能力
  3. 循环处理列表变量的能力

加载 Secret

假设我们的凭据文件保存在当前目录的 credentials 文件中,我们需要将它创建为 Secret,并在后续的容器模板中进行加载:kubectl create secret generic awskey --from-file=credentials

工作流中想要加载 Secret,跟 Pod 是很相似的,例如我们将会这样编写列出 EC2 实例的环节:

代码语言:javascript
复制
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

这个步骤写完之后,可以运行一下,看看结果:

代码语言:javascript
复制
$ 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 实例的

代码语言:javascript
复制
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 的主要变化:

  • 把原有的单步骤流程拓展成了多步骤
  • 列表中加入了格式化内容,精简输出
  • 将列表结果作为循环变量,传递给了用于关机的后续步骤
代码语言:javascript
复制
arguments:
  parameters:
    - name: ec2id
      value: "{{item}}"
withParam: "{{steps.list.outputs.result}}"

这一段将步骤 list 的控制台输出作为循环变量,传递给 shutdown-ec2 模板的 ec2id 参数,逐个关机。

注意这里的写法,使用 step 的方式对模板进行引用,形成多步骤流程。

运行后,可以看到 Argo 用并发的形式,进行了批量关机操作。

补充

首先是 AWS CLI 提供了丰富的功能,调用起来实在是比 SDK 方便太多,所以这里用这种形式来简化操作。

其次是这里对输出变量的做法,其实 Argo 提供了丰富的内置函数,可以对这些输出内容进行较为复杂的处理,当然,也可以用 Script 步骤进行更加细致的定制工作。

再次,过程中直接加载 AWS 凭据的方法非常不推荐,关于容器环境中的敏感信息管理,已经有很多陈述,这里就不节外生枝了。

最后,Argo 的文档真烂,真的烂。。