0%

Workflow 工具 Argo 之部署與概述

argo

Argo介紹

Kubeflow 作為一個 Deep Learning service on Kubernetes 的專案,自然面對最大的問題就是如何將一個 DL 中的每一個 phase 串在一起。而眾多 workflow 工具中, Kubeflow 在這邊選擇使用的便是 - Argo。 Kubeflow 不只將 Argo 應用在 pipeline,也應用在它內部的CI/CD上,因此值得我們好好關注。

Argo 是一個基於 CRD 實作的一個 Workflow 工具,協助使用者管理與控制一系列任務的執行,
同時提供一個簡潔的UI介面觀看每一個任務的執行狀況、順序、結果與 log。
由於 Workflow 本身是一個 CRD,因此建立 workflow 的方法也是透過撰寫一個 yaml 檔,而 Argo 也針對如何描述一個 workflow 提供了許多的規則。

部署Argo

我們這邊是獨立部署,如果是直接安裝Kubeflow,以下這些components已經安裝完畢,只需另外安裝argo client即可。

  1. 首先下載安裝Argo client
    Mac:

    1
    brew install argoproj/tap/argo

    Linux:

    1
    2
    curl -sSL -o /usr/local/bin/argo https://github.com/argoproj/argo/releases/download/v2.2.1/argo-linux-amd64
    chmod +x /usr/local/bin/argo
  2. 接下來安裝Argo Controller 與 UI

    1
    2
    kubectl create ns argo
    kubectl apply -n argo -f https://raw.githubusercontent.com/argoproj/argo/v2.2.1/manifests/install.yaml

    NOTE: 因為上面的insall.yaml會需要在你的k8s中創建clusterrole。因此如果使用的是GKE,這邊需要特別開通權限。

    1
    kubectl create clusterrolebinding YOURNAME-cluster-admin-binding --clusterrole=cluster-admin --user=YOUREMAIL@gmail.com

    安裝好後就會在k8s下面的argo namespace看到以下的components

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    NAME                         DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
    deploy/argo-ui 1 1 1 1 84d
    deploy/workflow-controller 1 1 1 1 84d

    NAME DESIRED CURRENT READY AGE
    rs/argo-ui-65bb8cf7f6 1 1 1 35s
    rs/argo-ui-6bb9c4485f 0 0 0 84d
    rs/workflow-controller-7f4dd6bd9 0 0 0 84d
    rs/workflow-controller-8fb5fb5d5 1 1 1 33s

    NAME READY STATUS RESTARTS AGE
    po/argo-ui-65bb8cf7f6-q2r59 1/1 Running 0 35s
    po/argo-ui-6bb9c4485f-7v2d5 1/1 Terminating 0 42d
    po/workflow-controller-8fb5fb5d5-spq99 1/1 Running 0 33s

    NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
    svc/argo-ui ClusterIP 10.27.255.240 <none> 80/TCP 84d
  3. 在接下來使用 Argo 的過程中,default account 的權限會限制一些功能,因此我們這邊在 default account 綁上 admin privileges。

    1
    kubectl create rolebinding default-admin --clusterrole=admin --serviceaccount=default:default

    也可以在執行的 workflow 時指定使用的是哪一個 service account

    1
    argo submit --serviceaccount <name>

以上即完成 Argo 的部署。

使用Argo UI

Argo UI 可以讓使用者輕鬆關觀察一個 workflow 的狀況,並檢查每一個步驟的結果與錯誤訊息。

kubectl port-forward

1
kubectl -n argo port-forward deployment/argo-ui 8001:8001

透過 http://127.0.0.1:8001 使用

Method 2: kubectl proxy

1
kubectl proxy

透過 http://127.0.0.1:8001/api/v1/namespaces/argo/services/argo-ui/proxy/ 使用

Expose a LoadBalancer

1
2
3
4
5
kubectl patch svc argo-ui -n argo -p '{"spec": {"type": "LoadBalancer"}}'

kubectl get svc argo-ui -n argo
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
argo-ui LoadBalancer 10.19.255.205 35.197.49.167 80:30999/TCP 1m

結果如下圖:

argo-ui

Argo 範例與解析

如同上面所說,Argo 也是透過自己實作一個 operator(CRD + controller) 來完成 Workflow 的功能。而所謂的在 k8s 中執行 workflow 的意思便是利用一個個 pod 來執行我們的每一個步驟。

因此我們這邊先來看幾個簡單的 argo crd,看看我們能設定哪些參數及能做到哪些事情。

首先提供一些常用的指令:

1
2
3
argo list #查看有哪些 workflow
argo get xxx-workflow-name-xxx #查看這個 workflow 的狀態
argo logs xxx-pod-name-xxx #查看這個 pod 的 output

coinflip

這個範例的目標是,先丟一個硬幣,如果是 head 就執行 head 的工作,如果是 tail 就執行 tail 的工作。也就是 workflow 最基礎的 if-else 功能。

首先先利用以下指令來執行coinflip worklow:

1
argo submit --watch https://raw.githubusercontent.com/argoproj/argo/master/examples/coinflip.yaml

接著我們來分析中間發生了哪些事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Name:                coinflip-czw7h
Namespace: default
ServiceAccount: default
Status: Succeeded
Created: Wed Feb 06 23:07:02 +0100 (38 seconds ago)
Started: Wed Feb 06 23:07:02 +0100 (38 seconds ago)
Finished: Wed Feb 06 23:07:40 +0100 (now)
Duration: 38 seconds

STEP PODNAME DURATION MESSAGE
✔ coinflip-czw7h
├---✔ flip-coin coinflip-czw7h-3048772414 23s
└-·-○ heads when 'tails == heads' evaluated false
└-✔ tails coinflip-czw7h-3790732747 13s

這是你會看到的介面。上面呈現了一些基本的訊息,以及這個 workflow 每一個 steps 發生的事情與執行了哪個相對應得 pod。

因此問題便是,我們如何讓每一個 pod 知道上一步驟的結果,與如何傳遞結果給下一個步驟。

我們現在來細看這個coinflips到底怎麼做的,以下是他的yaml file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: coinflip-
spec:
entrypoint: coinflip
templates:
- name: coinflip
steps:
- - name: flip-coin
template: flip-coin
- - name: heads
template: heads
when: "{{steps.flip-coin.outputs.result}} == heads"
- name: tails
template: tails
when: "{{steps.flip-coin.outputs.result}} == tails"

- name: flip-coin
script:
image: python:alpine3.6
command: [python]
source: |
import random
result = "heads" if random.randint(0,1) == 0 else "tails"
print(result)

- name: heads
container:
image: alpine:3.6
command: [sh, -c]
args: ["echo \"it was heads\""]

- name: tails
container:
image: alpine:3.6
command: [sh, -c]
args: ["echo \"it was tails\""]

從上面可以很簡單知道:

  • apiVersion: argoproj.io/v1alpha1kind: Workflow 說明了這是一個CRD
  • specentrypoing 開始執行,在這邊即是從 coinflip 這個 template 開始
  • templates 裡面的每一項描述的都是一個工作,包含
    • 名字
    • 可以執行一個 container 或是執行 script
    • container image
    • commandargs
    • steps : 作用是排定一系列 templates 執行的先後順序
  • coinflip 同時作為一個 entrypoint ,特別可以看到他裡面描述的是 steps ,且包含:
    • 每一步驟的名字
    • 要執行的 template
    • 什麼時候執行

將上面的 yaml 轉成 json 可以視為[[flip-coin], [heads, tails]],因此會先執行 flip-coin。而 flip-coin 透過執行的 script 會有一個 result 作為 output,表示擲出硬幣的結果。
而下面的步驟透過 NaN 來獲得特定步驟的 output 作為判斷的依據,進而可以達成 if-else 的workflow效果 。

Ref: 可另外參考 coinflip-recursive

dag-diamond

在一個 workflow 中除了 if-else 外,有時也需要能夠表達工作之間的相依性,或是有時一些工作需要可以平行執行。而 Argo 便提供 dag 來描述工作的相依性,直接看範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: dag-diamond-
spec:
entrypoint: diamond
templates:
- name: diamond
dag:
tasks:
- name: A
template: echo
arguments:
parameters: [{name: message, value: A}]
- name: B
dependencies: [A]
template: echo
arguments:
parameters: [{name: message, value: B}]
- name: C
dependencies: [A]
template: echo
arguments:
parameters: [{name: message, value: C}]
- name: D
dependencies: [B, C]
template: echo
arguments:
parameters: [{name: message, value: D}]

- name: echo
inputs:
parameters:
- name: message
container:
image: alpine:3.7
command: [echo, "{{inputs.parameters.message}}"]

可以注意到這邊沒有使用steps,而是使用dag。在dag中的每一項步驟皆可以使用dependencies來描述相依的工作,比方說 B,C 步驟便強迫要在 A 完成後才能執行。而當 A 完成後,B,C 滿足條件便會同時(平行)執行,最後才交由 D 執行。

透過 dag 便可以編排出工作的順序與相依性,或是讓工作平行執行。

dag 與 steps

還記得剛剛的 flip-coin 範例嗎,透過steps也可以達成某程度的平行執行或是順序執行,但是無法表達相依性。什麼意思呢?

熟悉 yaml 的人應該會知道 yaml 跟 json 是等價可以互相轉換的,而 yaml 中的

1
2
3
4
5
6
7
8
9
steps:
- - name: flip-coin
template: flip-coin
- - name: heads
template: heads
when: "{{steps.flip-coin.outputs.result}} == heads"
- name: tails
template: tails
when: "{{steps.flip-coin.outputs.result}} == tails"

中間的 - - 表達的是 list中的list 的意思,也就是 [[flip-coin], [heads, tails]]。當 steps 再寫的時候如果是依序執行一律用- -,若是在同一個 list 內則會平行執行。

因此以下用一個例子比較:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
spec:
entrypoint: diamond
templates:
- name: diamond
steps:
- - name: A
template: echo
arguments:
parameters: [{name: message, value: A}]
- - name: B
template: echo
arguments:
parameters: [{name: message, value: B}]
- name: C
template: echo
arguments:
parameters: [{name: message, value: C}]
- - name: D
template: echo
arguments:
parameters: [{name: message, value: D}]

- name: echo
inputs:
parameters:
- name: message
container:
image: alpine:3.7
command: [echo, "{{inputs.parameters.message}}"]

是等價於

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
spec:
entrypoint: diamond
templates:
- name: diamond
dag:
tasks:
- name: A
template: echo
arguments:
parameters: [{name: message, value: A}]
- name: B
dependencies: [A]
template: echo
arguments:
parameters: [{name: message, value: B}]
- name: C
dependencies: [A]
template: echo
arguments:
parameters: [{name: message, value: C}]
- name: D
dependencies: [B, C]
template: echo
arguments:
parameters: [{name: message, value: D}]

- name: echo
inputs:
parameters:
- name: message
container:
image: alpine:3.7
command: [echo, "{{inputs.parameters.message}}"]

如下圖透過UI看到的結果:

dag-steps-cmp

conditionals

了解了上面如何實現 if-else ,依序執行, 同時執行後,最後在看一個範例解釋了如何在 workflow 執行時動態的給予參數,並影響結果,我們直接看yaml file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: conditional-
spec:
entrypoint: conditional-example
arguments:
parameters:
- name: should-print
value: "false"

templates:
- name: conditional-example
inputs:
parameters:
- name: should-print
steps:
- - name: print-hello
template: whalesay
when: "{{inputs.parameters.should-print}} == true"

- name: whalesay
container:
image: docker/whalesay:latest
command: [sh, -c]
args: ["cowsay hello"]

spec 中可以提供 arguments ,來將參數傳進各個step 中。如上面的範例,預設的參數 should-print 為 “false” 而使得直接執行不會執行 whalesay 步驟,但是在執行 workflow 時使用如下指令,便可以動態傳進參數:

1
argo submit examples/conditionals.yaml -p should-print=true

step 可以使用 NaN 來獲取參數,whalesay這個步驟便會執行。

Summary

以上便是 Argo 的簡易使用分享,想了解更詳細的使用方法建議直接參考 example 來找到最適合自己的範例。
透過 Argo 我們便可以描述 pod 跟 pod 之間的執行順序關係,還可以在彼此之間傳遞結果。
因此 Kubeflow/pipeline 甚至是 Kubeflow 社群的 CI/CD 皆是利用 Argo來完成的。

接下來會利用我自己為 Kubeflow/tf-operator 貢獻的 e2e test 來描述 Kubeflow 如何用 Argo 實作自己的 CI/CD。