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

於清樂發表於2021-01-27

原文: https://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 https://raw.githubusercontent.com/argoproj/argo/stable/manifests/install.yaml

部署 MinIO:

helm repo add minio https://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:
#
# * https://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: git@gitlab.svc.local: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 程式碼實現的,目前沒有在社群找到類似的解決方案。

參考文件

視訊:

相關文章