What are Argo Workflows?

Tim BerryTim Berry
11 min read

Continuing my efforts to explore and Learn All The Things™ on the CNCF graduated projects page, today I’m going to take a look at Argo Workflows, which is one of 4 tools in the Argo project overall (see my previous post on ArgoCD). Argo Workflows is an open source container-native workflow engine for orchestrating parallel jobs on Kubernetes. It’s is an extremely powerful tool, but I think learning about it could benefit from a bit more scene-setting and context, which I humbly hope to impart in this post.

What is a Workflow?

Before we jump into Argo specifically, I think it will help to understand what we mean by a Workflow.

Many folks may be used to setting up resources in Kubernetes that are designed to keep running - such as Deployments for example. They may change and mutate, or scale up and down, but generally once we’ve declared that we want a service or workload to exist, we’re used to it being there until its eventual end of life. These are basically “long running services”, but not everything we need to accomplish fits that model.

Sometimes we want to run a workload that does something, or maybe even a series of somethings and then stops. It executes successfully, and exits successfully, content in the knowledge of a job well done. This is the workflow pattern, and its used extensively for things like data processing and infrastructure automation.

Kind of like a Job?

A Kubernetes Job is an implementation of this pattern, designed for various batch tasks, but with a very simplistic approach. Kubernetes Jobs typically execute a single task or batch job to completion, but they have very basic error handling and limited scope for dependency management.

By comparison, Argo can handle complex multi-step workflows, with built-in support for dependencies, retry logic and even handling artifacts between workflow steps. Now we’ve set the scene for the problem we’re tying to solve, let’s get hands on and try them out for ourselves!

Prerequisites

To follow along, you’ll need access to a Kubernetes cluster. A simple local dev cluster will do, such as you might run with Kind or Minikube.

Installing Argo Workflows

To get started, we’ll run the following commands to create a dedicated namespace called argo and install the necessary CRDs (just like we did in our previous post!). In this command we’re specifying Argo version 3.7.0, but there may be a newer version available (check their releases page here).

kubectl create ns argo
kubectl apply -n argo -f "https://github.com/argoproj/argo-workflows/releases/download/v3.7.0/quick-start-minimal.yaml"

Along with the CRDs we need to define Argo objects, we’ve also now installed a few components into the argo namespace. These are:

  • argo-server: This is the central component that provides an API for interacting with Argo workflows. It also provides a web UI if you like that sort of thing.

  • workflow-controller: This component is responsible for managing the execution of workflows in Argo. It does things like watch for new workflow submissions, orchestrates their execution and manages their state.

  • httpbin: This is a simple HTTP request and response service. It’s often used for testing workflows.

  • minio: A high-performance, distributed object storage that should probably get its own blog post in this series some day! Argo uses minio to handle artifact storage during workflow execution.

Argo also has its own CLI for Workflows. Whereas the CLI for ArgoCD was called argocd, the CLI for Argo Workflows is just called argo. This doesn’t bother me at all 😬 so let’s go ahead and install it.

You can grab a binary from the releases page, or if you’re using Homebrew run:

brew install argo

The argo CLI only has a handful of simple commands, and this is all we need to interact with the Argo server:

  • To submit a workflow, we use argo submit

  • To list current workflows, we use argo list

  • To see info about a specific workflow, we use argo get

  • To print logs from a workflow, we use argo logs

  • Finally, to delete a workflow, we use argo delete

An example workflow

The Argo docs provide an example “Hello World” workflow to get you started. Before we submit it though, let’s take a look at the YAML so we understand what it’s actually doing:

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: hello-world
  templates:
  - name: hello-world
    container:
      image: busybox
      command: [echo]
      args: ["hello world"]

As you can see, this is a Workflow type of object, as defined by one of the CRDs we just installed.

In the metadata section, we’re using generateName instead of name. This is actually a native Kubernetes feature that you don’t see too often in the wild. Using generateName here means that this object, when created, will use hello-world- as a prefix, and the server will add a unique generated suffix to its name.

Like any other Kubernetes object, the spec then defines the core structure of our workflow. In Argo, we provide this as a list of templates and an entrypoint. Templates are re-usable definitions of some sort of workflow logic which we’ll explore in more detail below, and the entrypoint is just which template we run first when the workflow starts.

Templates

So as we’ve just stated, templates are just re-usable sets of instructions. There are many different types of templates, but they all belong to one of these categories:

  • Template definitions are the types of templates that define work to be done, usually in some sort of container.

  • Template invocators are ways to call other templates, and define execution control (such as ordering and dependencies).

Coming back to our “Hello World” example, we are defining a single template called hello-world, and this template is a container type. This is the most commonly used template type in the definition category, and it simply lets you define a container to run in exactly the same way as you would anywhere else in Kubernetes (such as in a Pod spec).

The expectation is that the container will execute some command, optionally with some arguments, and then exit successfully. The output of the container is stored in an Argo variable, so that you could use it later if you wanted to (we’re not doing that here of course, because we only have a single template in this workflow).

Submitting a Workflow

Let’s submit this example workflow to our Argo service. We’ll add the optional --watch flag to this command, so that we can watch the workflow complete:

argo submit -n argo --watch https://raw.githubusercontent.com/argoproj/argo-workflows/main/examples/hello-world.yaml

Watching the workflow shows us neatly what’s happening: the workflow executes successfully, and the watch exits when it’s finished.

We can see that it took about 20 seconds for the workflow to finish successfully, and that a Pod called hello-world-qnk9t existed for about 9 seconds. Pretty cool!

Other Template Types

While containers are most commonly used for templates, there are other types too within the definition category:

  • script is basically a convenience wrapper around container, which adds a source field to allow you to define a script in-place (for example, to run an in-line Python script on a python:alpine container)

  • resource allows you to perform resource operations on a Kubernetes cluster, for example to create a ConfigMap. This is super useful when you realise you can pass variables and parameters between templates in a workflow.

  • suspend allows you to suspend the execution of a workflow until it is resumed manually.

  • plugin allows you to reference external plugins.

  • containerset lets you use multiple containers within a single Pod.

  • http lets you execute HTTP requests and store the results as a variable to use elsewhere.

Invocators and execution control

Now we’re getting to the good stuff, and hopefully things will start to make more sense!

In the “Hello World” example, we have a single template acting as a single step in a workflow. This is a great starting point, but it can make the terminology confusing. Why is a step called a template?

The answer is that a template is designed to be re-used. We should use templates to create functions that can be referenced multiple times (applying the principle of Don’t Repeat Yourself).

We can then arrange calls to these functions using one of the invocator categories of templates.

So, within this category, you have 2 options:

steps is the most straightforward invocator type, and lets you define your tasks in a series of steps. The steps will run one by one, but you can nest them - outer lists will run sequentially, and inner lists will run in parallel. There are also advanced synchronisation and conditional options for steps, but they’re a bit beyond our scope for today.

Here’s an example of using steps in a workflow:

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: steps-example-
spec:
  entrypoint: data-workflow
  templates:
  - name: data-workflow
    steps:
    - - name: step1
        template: prepare-data
    - - name: step2a
        template: process-data
      - name: step2b
        template: notify-data-team

In this snippet, we create a workflow object with an entrypoint of data-workflow. The data-workflow template is a steps type, so it defines a set of steps to run.

  • step1 runs first, and it calls a template called prepare-data (we would declare later on in this YAML file what prepare-data actually is and what it does, but we’re omitting it for now to keep this simple).

  • Once that step is completed, step2a and step2b run in parallel as they’re in a nested list. So the process-data and notifiy-data-team templates would both be called at the same time.

If you have a more complex set of dependencies for steps in a workflow, you can specify these using a directed acyclic graph, or DAG, with the dag invocator template type. A DAG is basically a data structure that consists of nodes connected by directed edges, where the edges have a specific direction, and there are no cycles (you cannot return to a node once you leave it). They’re used a lot in workflow orchestration, and pop up in other tools like Apache Airflow.

Let’s look at this example:

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: dag-example-
spec:
  entrypoint: data-workflow
  templates:
  - name: data-workflow
    dag:
      tasks:
      - name: A
        template: echo
      - name: B
        dependencies: [A]
        template: echo
      - name: C
        dependencies: [A]
        template: echo
      - name: D
        dependencies: [B, C]
        template: echo

For the sake of simplicity, every step here is referencing the same dummy template called echo - the important thing is the order of how these steps are run, and how they depend upon each other.

  • A runs first

  • B and C will run in parallel, as this is the default, but only if A has completed successfully because of the dependency we’ve defined.

  • D can then run, but again only if its dependency on the successful completion of B and C is met.

Templates and parameters

Passing data or artifacts between steps is super useful too. Let’s take a look at another steps example:

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: messages-
spec:
  entrypoint: my-workflow
  templates:
  - name: my-workflow
    steps:
    - - name: hi-bob
        template: print-message
        arguments:
          parameters:
          - name: message
            value: "Bob"
    - - name: hi-emily
        template: print-message
        arguments:
          parameters:
          - name: message
            value: "Emily"
    - - name: hi-again
        template: print-message
        arguments:
          parameters:
          - name: message
            value: "{{steps.hi-emily.outputs.result}}"

  - name: print-message
    inputs:
      parameters:
      - name: message
    container:
      image: busybox
      command: [echo]
      args: ["Hi {{inputs.parameters.message}}"]

In this YAML our workflow object will get a name starting with messages- and we invoke the workflow with the my-workflow template (specified as our entrypoint). Also, my-workflow is of the steps type. Each step will run in order, as we don’t have any nested lists here. This should all be starting to make sense now 😄

Each step references the same template, called print-message. So we’re re-using that function multiple times. You can see it defined at the bottom of the YAML file, where it just uses the busybox container to echo a string to standard output. A nice convention here is to leave a blank line in the YAML before the print-message template, reminding us that it’s a function that gets called by steps in the workflow, but it’s not a part of the workflow on its own.

Let’s walkthrough my-workflow:

  • In each step we’re demonstrating how we can parameterise our templates. In the hi-bob and hi-emily steps, we’re using arguments to pass a parameter to the template. The parameter is called message, and as you can see we can use any value we want in each step.

  • In the final step, rather than directly specify the value of message, we actually reference the output of the previous step!

So what does this look like when we run it? Let’s give it a try. We’ll write this to a file called messages.yaml and submit it to Argo:

argo submit -n argo --watch messages.yaml

Here’s what the watch looks like:

We can also see the logs of a workflow like this:

argo logs -n argo @latest

This output is helpfully colour-coded! We can see the standard output of each step, saying “Hi Bob”, “Hi Emily” and of course, “Hi Hi Emily” (because we passed “Hi Emily” as the name to say Hi to!) 😄

Please tell me there’s a UI

Of course there is! We can port-forward the UI and then connect to https://localhost:2746. You will get some scary self-signed TLS errors however, and if you go on to use Argo in production you should definitely configure TLS properly.

kubectl -n argo port-forward service/argo-server 2746:2746

Summary

We’ve now had a quick tour of Argo Workflows, and hopefully you’ve got a good idea of its basic functionality as well as its potential use cases. To get to grips with the advanced features of Argo, I’d definitely recommend browsing the user guide and playing with some more examples. Stay tuned, as next time we’ll be tackling Argo Rollouts!

0
Subscribe to my newsletter

Read articles from Tim Berry directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tim Berry
Tim Berry