雲原生流水線 Argo Workflow 的安裝、使用以及個人體驗

原文: //ryan4yin.space/posts/expirence-of-argo-workflow/

注意:這篇文章並不是一篇入門教程,學習 Argo Workflow 請移步官方文檔 Argo Documentation

Argo Workflow 是一個雲原生工作流引擎,專註於編排並行任務。它的特點如下:

  1. 使用 Kubernetes 自定義資源(CR)定義工作流,其中工作流中的每個步驟都是一個容器。
  2. 將多步驟工作流建模為一系列任務,或者使用有向無環圖(DAG)描述任務之間的依賴關係。
  3. 可以在短時間內輕鬆運行用於機器學習或數據處理的計算密集型作業。
  4. Argo Workflow 可以看作 Tekton 的加強版,因此顯然也可以通過 Argo Workflow 運行 CI/CD 流水線(Pipielines)。

阿里雲是 Argo Workflow 的深度使用者和貢獻者,另外 Kubeflow 底層的工作流引擎也是 Argo Workflow.

一、Argo Workflow 對比 Jenkins

我們在切換到 Argo Workflow 之前,使用的 CI/CD 工具是 Jenkins,下面對 Argo Workflow 和 Jenkins 做一個比較詳細的對比,
以了解 Argo Workflow 的優缺點。

1. Workflow 的定義

Workflow 使用 kubernetes CR 進行定義,因此顯然是一份 yaml 配置。

一個 Workflow,就是一個運行在 Kubernetes 上的流水線,對應 Jenkins 的一次 Build.

而 WorkflowTemplate 則是一個可重用的 Workflow 模板,對應 Jenkins 的一個 Job.

WorkflowTemplate 的 yaml 定義和 Workflow 完全一致,只有 Kind 不同!

WorkflowTemplate 可以被其他 Workflow 引用並觸發,也可以手動傳參以生成一個 Workflow 工作流。

2. Workflow 的編排

Argo Workflow 相比其他流水線項目(Jenkins/Tekton/Drone/Gitlab-CI)而言,最大的特點,就是它強大的流水線編排能力。

其他流水線項目,對流水線之間的關聯性考慮得很少,基本都假設流水線都是互相獨立的。

而 Argo Workflow 則假設「任務」之間是有依賴關係的,針對這個依賴關係,它提供了兩種協調編排「任務」的方法:Steps 和 DAG

再藉助 templateRef 或者 Workflow of Workflows,就能實現 Workflows 的編排了。

我們之所以選擇 Argo Workflow 而不是 Tekton,主要就是因為 Argo 的流水線編排能力比 Tekton 強大得多。(也許是因為我們的後端中台結構比較特殊,導致我們的 CI 流水線需要具備複雜的編排能力)

一個複雜工作流的示例如下:

3. Web UI

Argo Workflow 的 Web UI 感覺還很原始。確實該支持的功能都有,但是它貌似不是面向「用戶」的,功能比較底層。

它不像 Jenkins 一樣,有很友好的使用界面(雖然說 Jenkins 的 UI 也很顯老…)

另外它所有的 Workflow 都是相互獨立的,沒辦法直觀地找到一個 WorkflowTemplate 的所有構建記錄,只能通過 label/namespace 進行分類,通過任務名稱進行搜索。

而 Jenkins 可以很方便地看到同一個 Job 的所有構建歷史。

4. Workflow 的分類

為何需要對 Workflow 做細緻的分類

常見的微服務項目,往往會拆分成眾多 Git 倉庫(微服務)進行開發,眾多的 Git 倉庫會使我們創建眾多的 CI/CD 流水線。
如果沒有任何的分類,這一大堆的流水線如何管理,就成了一個難題。

最顯見的需求:前端和後端的流水線最好能區分一下,往下細分,前端的 Web 端和客戶端最好也能區分,後端的業務層和中台最好也區分開來。

另外我們還希望將運維、自動化測試相關的任務也集成到這個系統中來(目前我們就是使用 Jenkins 完成運維、自動化測試任務的),
如果沒有任何分類,這一大堆流水線將混亂無比。

Argo Workflow 的分類能力

當 Workflow 越來越多的時候,如果不做分類,一堆 WorkflowTemplate 堆在一起就會顯得特別混亂。(沒錯,我覺得 Drone 就有這個問題…)

Argo 是完全基於 Kubernetes 的,因此目前它也只能通過 namespace/labels 進行分類。

這樣的分類結構和 Jenkins 的視圖-文件夾體系大相徑庭,目前感覺不是很好用(也可能純粹是 Web UI 的鍋)。

5. 觸發構建的方式

Argo Workflow 的流水線有多種觸發方式:

  • 手動觸發:手動提交一個 Workflow,就能觸發一次構建。可以通過 workflowTemplateRef 直接引用一個現成的流水線模板。
  • 定時觸發:CronWorkflow
  • 通過 Git 倉庫變更觸發:藉助 argo-events 可以實現此功能,詳見其文檔。
    • 另外目前也不清楚 WebHook 的可靠程度如何,會不會因為宕機、斷網等故障,導致 Git 倉庫變更了,而 Workflow 卻沒觸發,而且還沒有任何顯眼的錯誤通知?如果這個錯誤就這樣藏起來了,就可能會導致很嚴重的問題!

6. secrets 管理

Argo Workflow 的流水線,可以從 kubernetes secrets/configmap 中獲取信息,將信息注入到環境變量中、或者以文件形式掛載到 Pod 中。

Git 私鑰、Harbor 倉庫憑據、CD 需要的 kubeconfig,都可以直接從 secrets/configmap 中獲取到。

另外因為 Vault 很流行,也可以將 secrets 保存在 Vault 中,再通過 vault agent 將配置注入進 Pod。

7. Artifacts

Argo 支持接入對象存儲,做全局的 Artifact 倉庫,本地可以使用 MinIO.

使用對象存儲存儲 Artifact,最大的好處就是可以在 Pod 之間隨意傳數據,Pod 可以完全分佈式地運行在 Kubernetes 集群的任何節點上。

另外也可以考慮藉助 Artifact 倉庫實現跨流水線的緩存復用(未測試),提升構建速度。

8. 容器鏡像的構建

藉助 Kaniko 等容器鏡像構建工具,可以實現容器鏡像的分佈式構建。

Kaniko 對構建緩存的支持也很好,可以直接將緩存存儲在容器鏡像倉庫中。

9. 客戶端/SDK

Argo 有提供一個命令行客戶端,也有 HTTP API 可供使用。

如下項目值得試用:

感覺 couler 挺不錯的,可以直接用 Python 寫 WorkflowTemplate,這樣就一步到位,所有 CI/CD 代碼全部是 Python 了。

此外,因為 argo workflow 是 kubernetes 自定義資源 CR,也可以使用 helm/kustomize 來做 workflow 的生成。

目前我們一些步驟非常多,但是重複度也很高的 Argo 流水線配置,就是使用 helm 生成的——關鍵數據抽取到 values.yaml 中,使用 helm 模板 + range 循環來生成 workflow 配置。

二、安裝 Argo Workflow

安裝一個集群版(cluster wide)的 Argo Workflow,使用 MinIO 做 artifacts 存儲:

kubectl apply -f //raw.githubusercontent.com/argoproj/argo/stable/manifests/install.yaml

部署 MinIO:

helm repo add minio //helm.min.io/ # official minio Helm charts
# 查看歷史版本
helm search repo minio/minio -l | head
# 下載並解壓 chart
helm pull minio/minio --untar --version 8.0.9

# 編寫 custom-values.yaml,然後部署 minio
kubectl create namespace minio
helm install minio ./minio -n argo -f custom-values.yaml

minio 部署好後,它會將默認的 accesskeysecretkey 保存在名為 minio 的 secret 中。
我們需要修改 argo 的配置,將 minio 作為它的默認 artifact 倉庫。

在 configmap workflow-controller-configmap 的 data 中添加如下字段:

  artifactRepository: |
    archiveLogs: true
    s3:
      bucket: argo-bucket   # bucket 名稱,這個 bucket 需要先手動創建好!
      endpoint: minio:9000  # minio 地址
      insecure: true
      # 從 minio 這個 secret 中獲取 key/secret
      accessKeySecret:
        name: minio
        key: accesskey
      secretKeySecret:
        name: minio
        key: secretkey

現在還差最後一步:手動進入 minio 的 Web UI,創建好 argo-bucket 這個 bucket.
直接訪問 minio 的 9000 端口(需要使用 nodeport/ingress 等方式暴露此端口)就能進入 Web UI,使用前面提到的 secret minio 中的 key/secret 登錄,就能創建 bucket.

ServiceAccount 配置

Argo Workflow 依賴於 ServiceAccount 進行驗證與授權,而且默認情況下,它使用所在 namespace 的 default ServiceAccount 運行 workflow.

default 這個 ServiceAccount 默認根本沒有任何權限!所以 Argo 的 artifacts, outputs, access to secrets 等功能全都會因為權限不足而無法使用!

為此,Argo 的官方文檔提供了兩個解決方法。

方法一,直接給 default 綁定 cluster-admin ClusterRole,給它集群管理員的權限,只要一行命令(但是顯然安全性堪憂):

kubectl create rolebinding default-admin --clusterrole=admin --serviceaccount=<namespace>:default -n <namespace>

方法二,官方給出了Argo Workflow 需要的最小權限的 Role 定義,方便起見我將它改成一個 ClusterRole:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: argo-workflow-role
rules:
# pod get/watch is used to identify the container IDs of the current pod
# pod patch is used to annotate the step's outputs back to controller (e.g. artifact location)
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - get
  - watch
  - patch
# logs get/watch are used to get the pods logs for script outputs, and for log archival
- apiGroups:
  - ""
  resources:
  - pods/log
  verbs:
  - get
  - watch

創建好上面這個最小的 ClusterRole,然後為每個名字空間,跑一下如下命令,給 default 賬號綁定這個 clusterrole:

kubectl create rolebinding default-argo-workflow --clusterrole=argo-workflow-role  --serviceaccount=<namespace>:default -n <namespace>

這樣就能給 default 賬號提供最小的 workflow 運行權限。

或者如果你希望使用別的 ServiceAccount 來運行 workflow,也可以自行創建 ServiceAccount,然後再走上面方法二的流程,但是最後,要記得在 workflow 的 spec.serviceAccountName 中設定好 ServiceAccount 名稱。

Workflow Executors

Workflow Executor 是符合特定接口的一個進程(Process),Argo 可以通過它執行一些動作,如監控 Pod 日誌、收集 Artifacts、管理容器生命周期等等…

Workflow Executor 有多種實現,可以通過前面提到的 configmap workflow-controller-configmapcontainerRuntimeExecutor 這個參數來選擇。

可選項如下:

  1. docker(默認): 目前使用範圍最廣,但是安全性最差。它要求一定要掛載訪問 docker.sock,因此一定要 root 權限!
  2. kubelet: 應用非常少,目前功能也有些欠缺,目前也必須提供 root 權限
  3. Kubernetes API (k8sapi): 直接通過調用 k8sapi 實現日誌監控、Artifacts 手機等功能,非常安全,但是性能欠佳。
  4. Process Namespace Sharing (pns): 安全性比 k8sapi 差一點,因為 Process 對其他所有容器都可見了。但是相對的性能好很多。

在 docker 被 kubernetes 拋棄的當下,如果你已經改用 containerd 做為 kubernetes 運行時,那 argo 將會無法工作,因為它默認使用 docker 作為運行時!

我們建議將 workflow executore 改為 pns,兼顧安全性與性能。

三、使用 Argo Workflow 做 CI 工具

官方的 Reference 還算詳細,也有提供非常多的 examples 供我們參考,這裡提供我們幾個常用的 workflow 定義。

使用 Kaniko 構建容器鏡像:

# USAGE:
#
# push 鏡像需要一個 config.json, 這個 json 需要被掛載到 `kaniko/.docker/config.json`.
# 為此,你首先需要構建 config.json 文件,並使用它創建一個 kubernetes secret:
#
#    export DOCKER_REGISTRY="registry.svc.local"
#    export DOCKER_USERNAME=<username>
#    export DOCKER_TOKEN='<password>'   # 對於 harbor 倉庫而言,token 就是賬號的 password.
#    kubectl create secret generic docker-config --from-literal="config.json={\"auths\": {\"$DOCKER_REGISTRY\": {\"auth\": \"$(echo -n $DOCKER_USERNAME:$DOCKER_TOKEN|base64)\"}}}"
#
# clone git 倉庫也需要 git credentails,這可以通過如下命令創建:
# 
#    kubectl create secret generic private-git-creds --from-literal=username=<username> --from-file=ssh-private-key=<filename>
# 
# REFERENCES:
#
# * //github.com/argoproj/argo/blob/master/examples/buildkit-template.yaml
#
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: build-image
spec:
  arguments:
    parameters:
      - name: repo  # 源碼倉庫
        value: [email protected]:ryan4yin/my-app.git
      - name: branch
        value: main
      - name: context-path
        value: .
      - name: dockerfile
        value: Dockerfile
      - name: image  # 構建出的鏡像名稱
        value: registry.svc.local/ryan4yin/my-app:latest
      - name: cache-image
        # 注意,cache-image 不能帶 tag! cache 是直接通過 hash 值來索引的!
        value: registry.svc.local/build-cache/my-app
  entrypoint: main
  templates:
    - name: main
      steps:
      - - name: build-image
          template: build-image
          arguments:
            artifacts:
              - name: git-repo
                git:
                  repo: "{{workflow.parameters.repo}}"
                  revision: "{{workflow.parameters.branch}}"
                  insecureIgnoreHostKey: true
                  usernameSecret:
                    name: private-git-creds
                    key: username
                  sshPrivateKeySecret:
                    name: private-git-creds
                    key: ssh-private-key
            parameters:
              - name: context-path
                value: "{{workflow.parameters.context-path}}"
              - name: dockerfile
                value: "{{workflow.parameters.dockerfile}}"
              - name: image
                value: "{{workflow.parameters.image}}"
              - name: cache-image
                value: "{{workflow.parameters.cache-image}}"
    # build-image 作為一個通用的 template,不應該直接去引用 workflow.xxx 中的 parameters/artifacts
    # 這樣做的好處是復用性強,這個 template 可以被其他 workflow 引用。
    - name: build-image
      inputs:
        artifacts:
          - name: git-repo
        parameters:
          - name: context-path
          - name: dockerfile
          - name: image
          - name: cache-image
      volumes:
        - name: docker-config
          secret:
            secretName: docker-config
      container:
        image: gcr.io/kaniko-project/executor:v1.3.0
        # 掛載 docker credential
        volumeMounts:
          - name: docker-config
            mountPath: /kaniko/.docker/
        # 以 context 為工作目錄
        workingDir: /work/{{inputs.parameters.context-path}}
        args:
          - --context=.
          - --dockerfile={{inputs.parameters.dockerfile}}
          # destination 可以重複多次,表示推送多次
          - --destination={{inputs.parameters.image}}
          # 私有鏡像倉庫,可以考慮不驗證 tls 證書(有安全風險)
          - --skip-tls-verify
          # - --skip-tls-verify-pull
          # - --registry-mirror=<xxx>.mirror.aliyuncs.com
          - --reproducible #  Strip timestamps out of the image to make it reproducible
          # 使用鏡像倉庫做遠程緩存倉庫
          - --cache=true
          - --cache-repo={{inputs.parameters.cache-image}}

四、常見問題

1. workflow 默認使用 root 賬號?

workflow 的流程默認使用 root 賬號,如果你的鏡像默認使用非 root 賬號,而且要修改文件,就很可能遇到 Permission Denined 的問題。

解決方法:通過 Pod Security Context 手動設定容器的 user/group:

安全起見,我建議所有的 workflow 都手動設定 securityContext,示例:

apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: xxx
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000

或者也可以通過 workflow-controller-configmapworkflowDefaults 設定默認的 workflow 配置。

2. 如何從 hashicorp vault 中讀取 secrets?

參考 Support to get secrets from Vault

hashicorp vault 目前可以說是雲原生領域最受歡迎的 secrets 管理工具。
我們在生產環境用它做為分佈式配置中心,同時在本地 CI/CD 中,也使用它存儲相關的敏感信息。

現在遷移到 argo,我們當然希望能夠有一個好的方法從 vault 中讀取配置。

目前最推薦的方法,是使用 vault 的 vault-agent,將 secrets 以文件的形式注入到 pod 中。

通過 valut-policy – vault-role – k8s-serviceaccount 一系列認證授權配置,可以制定非常細粒度的 secrets 權限規則,而且配置信息閱後即焚,安全性很高。

3. 如何在多個名字空間中使用同一個 secrets?

使用 Namespace 對 workflow 進行分類時,遇到的一個常見問題就是,如何在多個名字空間使用 private-git-creds/docker-config/minio/vault 等 workflow 必要的 secrets.

常見的方法是把 secrets 在所有名字空間 create 一次。

但是也有更方便的 secrets 同步工具:

比如,使用 kyverno 進行 secrets 同步的配置:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: sync-secrets
spec:
  background: false
  rules:
  # 將 secret vault 從 argo Namespace 同步到其他所有 Namespace
  - name: sync-vault-secret
    match:
      resources:
        kinds:
        - Namespace
    generate:
      kind: Secret
      name: regcred
      namespace: "{{request.object.metadata.name}}"
      synchronize: true
      clone:
        namespace: argo
        name: vault
  # 可以配置多個 rules,每個 rules 同步一個 secret

上面提供的 kyverno 配置,會實時地監控所有 Namespace 變更,一但有新 Namespace 被創建,它就會立即將 vault secret 同步到該 Namespace.

或者,使用專門的 secrets/configmap 複製工具:kubernetes-replicator

4. Argo 對 CR 資源的驗證不夠嚴謹,寫錯了 key 都不報錯

待研究

5. 是否應該盡量使用 CI/CD 工具提供的功能?

我從同事以及網絡上,了解到部分 DevOps 人員主張盡量自己使用 Python/Go 來實現 CI/CD 流水線,CI/CD 工具提供的功能能不使用就不要使用。

因此有此一問。下面做下詳細的分析:

盡量使用 CI/CD 工具提供的插件/功能,好處是不需要自己去實現,可以降低維護成本。
但是相對的運維人員就需要深入學習這個 CI/CD 工具的使用,另外還會和 CI/CD 工具綁定,會增加遷移難度。

而盡量自己用 Python 等代碼去實現流水線,讓 CI/CD 工具只負責調度與運行這些 Python 代碼,
那 CI/CD 就可以很方便地隨便換,運維人員也不需要去深入學習 CI/CD 工具的使用。
缺點是可能會增加 CI/CD 代碼的複雜性。

我觀察到 argo/drone 的一些 examples,發現它們的特徵是:

  1. 所有 CI/CD 相關的邏輯,全都實現在流水線中,不需要其他構建代碼
  2. 每一個 step 都使用專用鏡像:golang/nodejs/python
    1. 比如先使用 golang 鏡像進行測試、構建,再使用 kaniko 將打包成容器鏡像

那是否應該盡量使用 CI/CD 工具提供的功能呢?
其實這就是有多種方法實現同一件事,該用哪種方法的問題。這個問題在各個領域都很常見。

以我目前的經驗來看,需要具體問題具體分析,以 argo workflow 為例:

  1. 流水線本身非常簡單,那完全可以直接使用 argo 來實現,沒必要自己再搞個 python 腳本
    1. 簡單的流水線,遷移起來往往也非常簡單。沒必要為了可遷移性,非要用 argo 去調用 python 腳本。
  2. 流水線的步驟之間包含很多邏輯判斷/數據傳遞,那很可能是你的流水線設計有問題!
    1. 流水線的步驟之間傳遞的數據應該儘可能少!複雜的邏輯判斷應該盡量封裝在其中一個步驟中!
    2. 這種情況下,就應該使用 python 腳本來封裝複雜的邏輯,而不應該將這些邏輯暴露到 argo workflow 中!
  3. 我需要批量運行很多的流水線,而且它們之間還有複雜的依賴關係:那顯然應該利用上 argo wrokflow 的高級特性。
    1. argo 的 dag/steps 和 workflow of workflows 這兩個功能結合,可以簡單地實現上述功能。

使用體驗

目前已經使用 Argo Workflow 一個月多了,總的來說,最難用的就是 Web UI。

其他的都是小問題,只有 Web UI 是真的超難用,感覺根本就沒有好好做過設計…

急需一個第三方 Web UI…

畫外

Argo 相比其他 CI 工具,最大的特點,是它假設「任務」之間是有依賴關係的,因此它提供了多種協調編排「任務」的方法。

但是貌似 Argo CD 並沒有繼承這個理念,Argo CD 部署時,並不能在 kubernetes 資源之間,通過 DAG 等方法定義依賴關係。

微服務的按序更新,我們目前是自己用 Python 代碼實現的,目前沒有在社區找到類似的解決方案。

參考文檔

視頻: