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

## 架构

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745825094360/0f156294-e126-4431-bd3c-382919d53c4a.png align="center")

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

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

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745825174668/a2656a36-1c00-43e2-b52c-bfe2f4c7ee01.jpeg align="center")

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

## 功能

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

## 起步

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

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

```plaintext
$ 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：

```plaintext
$ 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
```

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

```plaintext
$ 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：

```plaintext
$ kubectl patch svc argo-server -n argo -p '{"spec": {"type": "NodePort"}}'
service/argo-server patched
```

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

教程中提供了一个 `Hello World` 流程，内容如下：

```yaml
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/v1alpha1` 的 `Workflow`。
    
2. 这一清单需要重复使用，因此 `metadata` 中没有给出 Name，而是给出了 `generateName`。
    
3. `spec.templates` 中保存的步骤的定义，并使用 `spec.entrypoint` 指定了入口环节。
    
4. 仅有的一个步骤中，使用一个容器镜像，并指定了执行命令，输出一段文字。
    

使用 `kubectl create` 提交工作流，看看结果：

```plaintext
$ kubectl create -f install.yaml
workflow.argoproj.io/hello-world-fdddc created
```

用浏览器打开控制台，浏览 `workflows` 页面，可以看到，出错了：

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745825198090/729c43f6-1bc3-4232-af7e-f369a35e40b8.png align="center")

错误原因也很 Kubernetes，就是 RBAC 权限不足：

```plaintext
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 命名空间里创建：

```yaml
$ kubectl create -f hello-world.yaml -n argo
workflow.argoproj.io/hello-world-l4q2x created
```

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745825218631/94e94897-8e1a-48dd-904a-93cad7d9dbc0.png align="center")

用 `argo` CLI 也可以方便的查看：

```plaintext
$ 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 实例的环节：

```yaml
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
```

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

```plaintext
$ 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 实例的

```yaml
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 的主要变化：

* 把原有的单步骤流程拓展成了多步骤
    
* 列表中加入了格式化内容，精简输出
    
* 将列表结果作为循环变量，传递给了用于关机的后续步骤
    

```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 的文档真烂，真的烂。。
