This page looks best with JavaScript enabled

Event-Driven Workflows with Argo Events

 ·  ☕ 9 min read

1. How Argo Events Works

The diagram above is from the official Argo Events website. An event processing system has three important parts:

  • Ingesting event sources, which corresponds to the Event Source
  • Distributing events, which corresponds to the Event Sensor
  • Consuming events, which corresponds to the Event Trigger

The event messages are stored in the EventBus, which uses NATS by default.

2. Creating ServiceAccounts for the Sensor and the Workflow

  • Create operate-workflow-sa

operate-workflow-sa authorizes the Sensor to operate on Workflows.

 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
kubectl apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
  namespace: argo-events
  name: operate-workflow-sa
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: operate-workflow-role
  namespace: argo-events
rules:
  - apiGroups:
      - argoproj.io
    verbs:
      - "*"
    resources:
      - workflows
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: operate-workflow-role-binding
  namespace: argo-events
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: operate-workflow-role
subjects:
  - kind: ServiceAccount
    name: operate-workflow-sa
    namespace: argo-events
EOF
  • Create workflow-pods-sa

workflow-pods-sa authorizes the Workflow to operate on Pods.

 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
kubectl apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
  namespace: argo-events
  name: workflow-pods-sa
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: argo-events
  name: workflow-pods-role
rules:
  - apiGroups:
      - ""
    verbs:
      - "*"
    resources:
      - pods
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: argo-events
  name: workflow-pods-role-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: workflow-pods-role
subjects:
  - kind: ServiceAccount
    name: workflow-pods-sa
    namespace: argo-events
EOF

It is worth noting that both operate-workflow-sa and workflow-pods-sa are namespace-scoped, so they must be authorized separately in each namespace.

3. Creating an eventbus to Store Events

  • Create a NATS
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
kubectl apply -f - <<EOF
apiVersion: argoproj.io/v1alpha1
kind: EventBus
metadata:
  name: default
  namespace: argo-events
spec:
  nats:
    native:
      replicas: 3
      auth: token
EOF
  • Check the workloads
1
2
3
4
5
kubectl -n argo-events get pod | grep eventbus

eventbus-default-stan-0                                2/2     Running   0             95s
eventbus-default-stan-1                                2/2     Running   0             93s
eventbus-default-stan-2                                2/2     Running   0             92s

4. Creating an API-Triggered Webhook

  • Create a webhook API
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
kubectl apply -f - <<EOF
apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
  name: myproject-webhook
  namespace: argo-events
spec:
  eventBusName: default
  service:
    ports:
      - port: 80
        targetPort: 80
  webhook:
    myproject-start:
      endpoint: /myproject/start
      method: POST
      port: "80"
      url: ""

Here we create a POST endpoint on port 80 at the path /myproject/start for triggering events. If there are multiple API definitions, you can keep adding them under spec.webhook.

  • Check the workloads
1
2
3
kubectl -n argo-events get pod  | grep myproject

myproject-webhook-eventsource-4ws29-697b776fb7-6n9dx   1/1     Running   0   26s

Argo Events creates a pod for each EventSource to receive events, and creates a Service as the entry point for triggering events.

1
2
3
kubectl -n argo-events get svc | grep myproject

myproject-webhook-eventsource-svc   ClusterIP   10.96.129.232   <none>        80/TCP                       26s
  • Expose the service

To make testing easier in a moment, we switch the Service type to NodePort here.

1
kubectl patch svc myproject-webhook-eventsource-svc -n argo-events -p '{"spec":{"type":"NodePort"}}'
1
2
3
kubectl -n argo-events get svc | grep myproject

myproject-webhook-eventsource-svc   NodePort    10.96.129.232   <none>        80:30001/TCP                 14h

5. Creating a sensor to Process Events

 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
kubectl apply -f - <<EOF
apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
  name: myproject-start
  namespace: argo-events
spec:
  template:
    serviceAccountName: operate-workflow-sa
  dependencies:
    - name: myproject-start
      eventSourceName: myproject-webhook
      eventName: myproject-start
  triggers:
    - template:
        name: webhook-workflow-trigger
        argoWorkflow:
          group: argoproj.io
          version: v1alpha1
          resource: workflows
          operation: submit
          source:
            resource:
              apiVersion: argoproj.io/v1alpha1
              kind: Workflow
              metadata:
                generateName: webhook-
              spec:
                serviceAccountName: workflow-pods-sa
                ttlStrategy:
                  secondsAfterCompletion: 600
                  secondsAfterSuccess: 600
                  secondsAfterFailure: 600
                entrypoint: whalesay
                arguments:
                  parameters:
                    - name: message
                    - name: who
                templates:
                  - name: whalesay
                    inputs:
                      parameters:
                        - name: message
                          value: "hello(input)"
                        - name: who
                          value: "world(input)"
                    container:
                      image: docker/whalesay:latest
                      command: [cowsay]
                      args:
                        [
                          "{{inputs.parameters.message}} {{inputs.parameters.who}}",
                        ]
          parameters:
            - src:
                dataTemplate: "{{ .Input.body.message }}"
                dependencyName: myproject-start
              dest: spec.arguments.parameters.0.value
            - src:
                dataTemplate: "{{ .Input.body.who }}"
                dependencyName: myproject-start
              dest: spec.arguments.parameters.1.value
EOF

At this point Argo Events also creates a Pod to process events.

1
2
3
kubectl -n argo-events get pod  | grep sensor

myproject-start-sensor-pzkf9-658cbd5c7d-xv9zf          1/1     Running   0             56s

There are a few settings here worth noting:

1
2
3
4
dependencies:
  - name: myproject-start
    eventSourceName: myproject-webhook
    eventName: myproject-start

This must be associated with the definition in the EventSource, which is how events are received.

1
operation: submit

If the operation is create, what you get in parameters is a complete event description with the data Base64-encoded. If the operation is submit, what you get in parameters is a payload you can use directly.

1
2
3
4
ttlStrategy:
  secondsAfterCompletion: 600
  secondsAfterSuccess: 600
  secondsAfterFailure: 600

After a Workflow finishes executing it is not deleted immediately; it is deleted according to the definition in ttlStrategy.

1
entrypoint: whalesay

entrypoint specifies the entry point for Workflow execution. If it is not specified, it defaults to main, i.e. the value of spec.entrypoint.

1
2
3
4
5
6
7
8
9
parameters:
   - src:
         dataTemplate: "{{ .Input.body.message }}"
         dependencyName: myproject-start
      dest: spec.arguments.parameters.0.value
   - src:
         dataTemplate: "{{ .Input.body.who }}"
         dependencyName: myproject-start
      dest: spec.arguments.parameters.1.value

dataTemplate specifies the event data to depend on, dest specifies the parameter in the Workflow, and dependencyName specifies the name of the event depended upon. Here it extracts the parameters from the API body and passes them to the Workflow parameters, overriding the default values.

6. Calling the API Webhook to Trigger an Event

  • Call the API endpoint to trigger it
1
curl -d '{"message":"hello", "who": "world"}' -H "Content-Type: application/json" -X POST http://localhost:30001/myproject/start -v
  • Check the created Workflow
1
2
3
4
kubectl -n argo-events get workflows

NAME            STATUS      AGE    MESSAGE
webhook-5z7k5   Succeeded   119s
  • Check the Workflow logs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
kubectl -n argo-events logs webhook-5z7k5  -f
 _____________
< hello world >
 -------------
    \
     \
      \
                    ##        .
              ## ## ##       ==
           ## ## ## ##      ===
       /""""""""""""""""___/ ===
  ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ /  ===- ~~~
       \______ o          __/
        \    \        __/
          \____\______/

The trigger succeeded, as expected.

7. Creating Workflows with WorkflowTemplate

Putting the full definition inside the Workflow’s templates every time is very tedious, so Argo provides WorkflowTemplate for orchestrating Workflows.

  • Create the WorkflowTemplates
 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
39
40
41
42
43
44
45
46
47
kubectl apply -f - <<EOF
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: whalesay-template
  namespace: argo-events
spec:
  templates:
    - name: whalesay
      inputs:
        parameters:
          - name: message
      container:
        image: docker/whalesay
        command: [cowsay]
        args: ["{{inputs.parameters.message}}"]
---
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: congratulations-template
  namespace: argo-events
spec:
  templates:
    - name: congratulations
      container:
        image: shaowenchen/demo:ubuntu
        command: [sh, -c]
        args: ["echo Congratulations!"]
---
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: random-status-template
  namespace: argo-events
spec:
  templates:
    - name: random-status
      script:
        image: python:alpine3.6
        command: [python]
        source: |
          import random
          exit_code = 0 if random.choice([True, False]) else 1
          import sys
          sys.exit(exit_code)
EOF

Three WorkflowTemplates are defined here: whalesay-template prints the input parameters, congratulations-template prints a fixed string, and random-status-template produces a random status.

  • Create a Workflow
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
kubectl apply -f - <<EOF
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  name: combined-workflow
  namespace: argo-events
spec:
  entrypoint: combined-template
  serviceAccountName: workflow-pods-sa
  templates:
  - name: combined-template
    steps:
    - - name: whalesay
        templateRef:
          name: whalesay-template
          template: whalesay
        arguments:
          parameters:
          - name: message
            value: hello world
    - - name: congratulations
        templateRef:
          name: congratulations-template
          template: congratulations

The Workflow now looks much simpler, because the concrete operations of each task are defined in the WorkflowTemplate; the Workflow only needs to specify the WorkflowTemplate names and parameters.

8. Orchestrating Complex Dependencies with DAG Workflow

An ordinary Workflow can only execute tasks in sequence, whereas a DAG Workflow can handle complex task dependencies and status dependencies to orchestrate tasks. Here is an example:

  • Create a DAG Workflow
 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
39
40
41
42
43
44
45
46
47
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  name: status-workflow
  namespace: argo-events
spec:
  entrypoint: dag
  serviceAccountName: workflow-pods-sa
  templates:
    - name: dag
      dag:
        tasks:
          - name: whalesay
            templateRef:
              name: whalesay-template
              template: whalesay
            arguments:
              parameters:
                - name: message
                  value: hello world
          - name: check-status
            templateRef:
              name: random-status-template
              template: random-status
            dependencies: [whalesay]
            continueOn:
              failed: true
          - name: success-path
            templateRef:
              name: whalesay-template
              template: whalesay
            arguments:
              parameters:
                - name: message
                  value: success-path
            dependencies: [check-status]
            when: "{{tasks.check-status.status}} == 'Succeeded'"
          - name: failure-path
            templateRef:
              name: whalesay-template
              template: whalesay
            arguments:
              parameters:
                - name: message
                  value: failure-path
            dependencies: [check-status]
            when: "{{tasks.check-status.status}} == 'Failed'"

A DAG Workflow is defined here:

  1. whalesay runs first
  2. check-status waits for whalesay to finish, then randomly produces a status
  3. If the status is success, success-path runs; otherwise failure-path runs
  • Check the workloads
1
2
3
4
5
kubectl get pod -n argo-events |grep status

status-workflow-random-status-2060967154       0/2     Error       0          40s
status-workflow-whalesay-261193263             0/2     Completed   0          60s
status-workflow-whalesay-437390993             0/2     Completed   0          30s

status-workflow-random-status-2060967154 produced a random error status, so failure-path runs.

  • Check the Workflow status
1
2
3
4
kubectl get workflow -n argo-events

NAME                STATUS      AGE   MESSAGE
status-workflow     Failed      56s

Because a task failed, the Workflow is marked as Failed.

9. Summary

Recently I needed to build a workflow for AI Infra together with Argo, so this post is mainly some notes from studying Argo Events\Workflow. The main contents are:

  • An introduction to how Argo Events works
  • An example of triggering a Workflow from a webhook
  • An example of orchestrating a Workflow with WorkflowTemplate
  • An example of orchestrating a DAG Workflow with WorkflowTemplate

Argo also has an object very similar to WorkflowTemplate called ClusterWorkflowTemplate, i.e. a cluster-level WorkflowTemplate, which can be reused in every namespace.

For a platform team, Argo mainly offers two capabilities:

  • Rapid integration: event triggering based on EventSource - Sensor - Workflow, quickly stacking up API features with yaml
  • Workflow orchestration: orchestrating Workflows based on ClusterWorkflowTemplate, providing higher-level orchestration capability

Here is a model of mine:

ClusterWorkflowTemplate corresponds to a Plugin, and Workflow corresponds to a Pipeline. Although a Workflow is executed immediately once created, business systems usually have their own databases, so we only need to park the Workflow in the database and hand it off to Argo at runtime to execute the pipeline.


微信公众号
WRITTEN BY
微信公众号