Argo Workflows - Up and running using Hera
Argo Workflows - Up and running using Hera
Argo Workflows - Up and running using Hera

Argo Workflows - Up and running using Hera

Table of Contents

Introduction

This tutorial guides you through setting up everything on your local computer that you’ll need to start writing Argo workflows with Hera running on Kubernetes (k3s) and using MinIO for storage. The primary purpose is to get you up and running. After which, you can explore the various tools in more detail on your own.
The following diagram illustrates the interaction between the Kubernetes related software:
notion image
Software
Description
Kubernetes (k3s)
K3s is a certified lightweight Kubernetes distribution that is designed for production workloads in environments with less resources.
Argo Workflows
A workflow manager installed in Kubernetes.
Hera Workflows
A python wrapper for Argo Workflows. It was developed to make Argo workflow construction and submission more accessible by eliminating the need for the developer to understand Argo.
MinIO
MinIO is a high-performance, S3 compatible object store which can be installed in k3s.
Multipass
For creating a virtual machine (VM) where Kubernetes and its packages will be installed and run.
Miniforge
A lightweight alternative to Anaconda or Miniconda, offering a way to manage Python packages and environments.
Podman
For creating container images for the Kubernetes system.

💡
If you click on the logo at the top of each software installation section, you can get more details on that tool.

⚠️
Use commands and code with caution.

 
notion image

Multipass Installation

Multipass is used to run Kubernetes on a virtual machine using the Ubuntu operating system. While there are multiple ways to install and run Kubernetes, we use Multipass in this tutorial to help keep it more platform independent.
Note: Other needed packages are included in this Multipass Installation

Ubuntu

# make sure OS is up to date sudo apt update sudo apt upgrade -y # install Multipass sudo snap install multipass

Fedora

# configure firewalld to trust the Multipass bridge network sudo firewall-cmd --permanent --zone=trusted --add-interface=mpqemubr0 sudo firewall-cmd --permanent --zone=trusted --add-forward sudo firewall-cmd --zone=trusted --list-all sudo systemctl restart firewalld # make sure OS is up to date sudo dnf update -y # install snapd sudo dnf install snapd -y # create the symlink for classic snaps sudo ln -s /var/lib/snapd/snap /snap # enable and start the snap services sudo systemctl enable --now snapd.socket sudo systemctl enable --now snapd.service # install Multipass sudo snap install multipass # add location of Multipass to path echo 'export PATH="$PATH:/snap/bin"' >> ~/.bashrc source ~/.bashrc multipass --version
⚠️
If you see “multipass: command not found…” after executing the above code block, then Mulitipass did not get installed. Executing the above code block again should resolve this issue.
 

MacOS

sudo xcode-select --install /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" echo "export PATH=/opt/homebrew/bin:\$PATH" >> ~/.zshrc source ~/.zshrc brew install --cask multipass -y # install other needed tools brew install wget -y

Launch Multipass

Adjust the 'cpus', 'memory', and 'disk' parameters below to your preferred settings ensuring there are sufficient resources for your local host machine:
multipass launch --name kubemaster --cpus 3 --memory 7G --disk 100G
The VM launched is called “kubemaster”.
Execute:
multipass list
Note the IP address for kubemaster. For example, below shows the IP address for kubemaster to be 10.241.47.55:

notion image

 
Set the IP address to have the name “kubemaster”:
sudo nano /etc/hosts
Add after last line the IP address along with kubemaster. For example:
10.241.47.55 kubemaster
 
Enter the kubemaster VM:
multipass shell kubemaster
 
Update and upgrade the VM:
sudo apt update sudo apt upgrade -y
 
Restart the VM:
sudo reboot now
 
Reenter the VM:
multipass shell kubemaster
 
Open another terminal window so you can execute commands outside of the kubemaster VM on your local host machine.
On your local host machine, create a work directory called “argo” and enter it:
💻
Local host machine
mkdir argo cd argo

 
notion image

Conda Installation (Miniforge)

Conda is an environment manager which allows you to install software packages in an isolated environment. We specifically use Conda to install Python and the Hera Python package. Miniforge is used as it is a lightweight implementation of conda.
Install Conda:
💻
Local host machine
wget https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh bash Miniforge3-$(uname)-$(uname -m).sh -b -p ${HOME}/conda rm -f Miniforge3-$(uname)-$(uname -m).sh source $HOME/conda/bin/activate conda init
 
After installation, create a conda (argodev) environment and activate it:
💻
Local host machine
conda create -n argodev -y conda activate argodev
 
The following installs packages into argodev for use by this tutorial:
💻
Local host machine (argodev)
conda install python -y pip install hera

 
notion image

Kubernetes (k3s) Installation

Install k3s

In the kubemaster VM:
💻
kubemaster VM
sudo curl -sfL https://get.k3s.io | K3S_KUBECONFIG_MODE="644" sh -s -
 
Check your Kubernetes system by executing the following:
💻
kubemaster VM
kubectl get nodes
You should see kubemaster with a status of "Ready”.
 

 
 
notion image
notion image

MinIO Installation

 
Install MinIO:
💻
kubemaster VM
sudo apt install -y dpkg wget https://github.com/minio/operator/releases/download/v5.0.14/kubectl-minio_5.0.14_$(uname | tr '[:upper:]' '[:lower:]')_$(dpkg --print-architecture) -O kubectl-minio chmod +x kubectl-minio sudo mv kubectl-minio /usr/local/bin/. kubectl minio version
Shows the installed version when done.
Install and start MinIO in Kubernetes:
💻
kubemaster VM
kubectl minio init
 
Check the MinIO pods:
💻
kubemaster VM
kubectl get all -n minio-operator
Execute until you see the console and at least one of the minio-operator pods are running.
 
Once the pods are running, create the proxy:
💻
kubemaster VM
kubectl minio proxy -n minio-operator &
You will see something like this:

Starting port forward of the Console UI.
To connect open a browser and go to http://localhost:9090
Current JWT to login: <TOKEN>
Forwarding from 0.0.0.0:9090 -> 9090

Note and record the <TOKEN>. The token is used to log in to the MinIO Operator Console.
 
Open a browser and go to the MinIO operator site:
http://kubemaster:9090
 
Enter the JWT to sign in. You’ll see the following:
notion image
Press "Create a Tenant". Edit the fields marked in red.
Note: "Namespace" must be "minio-operator", but the rest can be set to your desired preferences. Also, make sure the “Total Size” is well within the disk size you set up when launching the kubemaster VM.
When done, press “Create”.
notion image
 
The following screen pops up:
notion image
Note and record the "Access Key" and "Secret Key"
If you wish, press "Download for import". This stores the access key and secret key in the file: credentials.json
 
Check the installation:
💻
kubemaster VM
kubectl get all -n minio-operator
Note the console port of "service/minio": 433:<Console Port> like shown in red below:

notion image

"kubemaster:<Console Port>" is used to access the MinIO object store. This, along with the access key and secret key, allows access to the MinIO repository from the workflow.
 
Create a file that will give the Hera workflow access to MinIO with the following (determined during MinIO installation above):
kubemaster:<console port>
<MinIO Access Key>
<MinIO Secret Key>
💻
kubemaster VM
nano minio_access.txt
For example:
kubemaster:30791 RwDiPu1z4F31mu1E 2pXcivwp7XkWKqHskGFLwk25DqU56eR8

Generate certificates for MinIO

While in the kubemaster VM, get the certificate generator and generate the certificates:
💻
kubemaster VM
wget https://github.com/minio/certgen/releases/latest/download/certgen-$(uname)-$(dpkg --print-architecture) chmod +x certgen-$(uname)-$(dpkg --print-architecture) ./certgen-$(uname)-$(dpkg --print-architecture) -host localhost,kubemaster rm -f certgen-$(uname)-$(dpkg --print-architecture)
This will generate the following files: private.key and public.crt.
 
Copy the certificates from the kubemaster VM:
💻
Local host machine
cd ~/argo multipass transfer kubemaster:public.crt public.crt multipass transfer kubemaster:private.key private.key
 
Go to the MinIO Operator site using the web browser (http://kubemaster:9090). You should see:
notion image
Click on the miniostorage tenet box.
Select Security:
notion image
Custom Certificates: Switch ON.
notion image
MinIO Server Certificates
  • Click the Cert attachment button
    • Find and select the public.crt file you created.
  • Click the Key attachment button
    • Find and select the private.key file you created.
Press “Save”. Then “Restart”:
notion image
 
You should now see the attached certificate. For example:
notion image
 

notion image

Argo Workflows Installation

In the kubemaster VM, create an Argo installation YAML file:
💻
kubemaster VM
nano argo.yaml
with the following contents:
argo.yaml
apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: clusterworkflowtemplates.argoproj.io spec: group: argoproj.io names: kind: ClusterWorkflowTemplate listKind: ClusterWorkflowTemplateList plural: clusterworkflowtemplates shortNames: - clusterwftmpl - cwft singular: clusterworkflowtemplate scope: Cluster versions: - name: v1alpha1 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: type: object x-kubernetes-map-type: atomic x-kubernetes-preserve-unknown-fields: true required: - metadata - spec type: object served: true storage: true --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: cronworkflows.argoproj.io spec: group: argoproj.io names: kind: CronWorkflow listKind: CronWorkflowList plural: cronworkflows shortNames: - cwf - cronwf singular: cronworkflow scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: type: object x-kubernetes-map-type: atomic x-kubernetes-preserve-unknown-fields: true status: type: object x-kubernetes-map-type: atomic x-kubernetes-preserve-unknown-fields: true required: - metadata - spec type: object served: true storage: true --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: workflowartifactgctasks.argoproj.io spec: group: argoproj.io names: kind: WorkflowArtifactGCTask listKind: WorkflowArtifactGCTaskList plural: workflowartifactgctasks shortNames: - wfat singular: workflowartifactgctask scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: type: object x-kubernetes-map-type: atomic x-kubernetes-preserve-unknown-fields: true status: type: object x-kubernetes-map-type: atomic x-kubernetes-preserve-unknown-fields: true required: - metadata - spec type: object served: true storage: true subresources: status: {} --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: workfloweventbindings.argoproj.io spec: group: argoproj.io names: kind: WorkflowEventBinding listKind: WorkflowEventBindingList plural: workfloweventbindings shortNames: - wfeb singular: workfloweventbinding scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: type: object x-kubernetes-map-type: atomic x-kubernetes-preserve-unknown-fields: true required: - metadata - spec type: object served: true storage: true --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: workflows.argoproj.io spec: group: argoproj.io names: kind: Workflow listKind: WorkflowList plural: workflows shortNames: - wf singular: workflow scope: Namespaced versions: - additionalPrinterColumns: - description: Status of the workflow jsonPath: .status.phase name: Status type: string - description: When the workflow was started format: date-time jsonPath: .status.startedAt name: Age type: date - description: Human readable message indicating details about why the workflow is in this condition. jsonPath: .status.message name: Message type: string name: v1alpha1 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: type: object x-kubernetes-map-type: atomic x-kubernetes-preserve-unknown-fields: true status: type: object x-kubernetes-map-type: atomic x-kubernetes-preserve-unknown-fields: true required: - metadata - spec type: object served: true storage: true subresources: {} --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: workflowtaskresults.argoproj.io spec: group: argoproj.io names: kind: WorkflowTaskResult listKind: WorkflowTaskResultList plural: workflowtaskresults singular: workflowtaskresult scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string message: type: string metadata: type: object outputs: properties: artifacts: items: properties: archive: properties: none: type: object tar: properties: compressionLevel: format: int32 type: integer type: object zip: type: object type: object archiveLogs: type: boolean artifactGC: properties: podMetadata: properties: annotations: additionalProperties: type: string type: object labels: additionalProperties: type: string type: object type: object serviceAccountName: type: string strategy: enum: - "" - OnWorkflowCompletion - OnWorkflowDeletion - Never type: string type: object artifactory: properties: passwordSecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object url: type: string usernameSecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object required: - url type: object azure: properties: accountKeySecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object blob: type: string container: type: string endpoint: type: string useSDKCreds: type: boolean required: - blob - container - endpoint type: object deleted: type: boolean from: type: string fromExpression: type: string gcs: properties: bucket: type: string key: type: string serviceAccountKeySecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object required: - key type: object git: properties: branch: type: string depth: format: int64 type: integer disableSubmodules: type: boolean fetch: items: type: string type: array insecureIgnoreHostKey: type: boolean passwordSecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object repo: type: string revision: type: string singleBranch: type: boolean sshPrivateKeySecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object usernameSecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object required: - repo type: object globalName: type: string hdfs: properties: addresses: items: type: string type: array force: type: boolean hdfsUser: type: string krbCCacheSecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object krbConfigConfigMap: properties: key: type: string name: type: string optional: type: boolean required: - key type: object krbKeytabSecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object krbRealm: type: string krbServicePrincipalName: type: string krbUsername: type: string path: type: string required: - path type: object http: properties: auth: properties: basicAuth: properties: passwordSecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object usernameSecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object type: object clientCert: properties: clientCertSecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object clientKeySecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object type: object oauth2: properties: clientIDSecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object clientSecretSecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object endpointParams: items: properties: key: type: string value: type: string required: - key type: object type: array scopes: items: type: string type: array tokenURLSecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object type: object type: object headers: items: properties: name: type: string value: type: string required: - name - value type: object type: array url: type: string required: - url type: object mode: format: int32 type: integer name: type: string optional: type: boolean oss: properties: accessKeySecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object bucket: type: string createBucketIfNotPresent: type: boolean endpoint: type: string key: type: string lifecycleRule: properties: markDeletionAfterDays: format: int32 type: integer markInfrequentAccessAfterDays: format: int32 type: integer type: object secretKeySecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object securityToken: type: string required: - key type: object path: type: string raw: properties: data: type: string required: - data type: object recurseMode: type: boolean s3: properties: accessKeySecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object bucket: type: string createBucketIfNotPresent: properties: objectLocking: type: boolean type: object encryptionOptions: properties: enableEncryption: type: boolean kmsEncryptionContext: type: string kmsKeyId: type: string serverSideCustomerKeySecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object type: object endpoint: type: string insecure: type: boolean key: type: string region: type: string roleARN: type: string secretKeySecret: properties: key: type: string name: type: string optional: type: boolean required: - key type: object useSDKCreds: type: boolean type: object subPath: type: string required: - name type: object type: array exitCode: type: string parameters: items: properties: default: type: string description: type: string enum: items: type: string type: array globalName: type: string name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: type: string optional: type: boolean required: - key type: object default: type: string event: type: string expression: type: string jqFilter: type: string jsonPath: type: string parameter: type: string path: type: string supplied: type: object type: object required: - name type: object type: array result: type: string type: object phase: type: string progress: type: string required: - metadata type: object served: true storage: true --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: workflowtasksets.argoproj.io spec: group: argoproj.io names: kind: WorkflowTaskSet listKind: WorkflowTaskSetList plural: workflowtasksets shortNames: - wfts singular: workflowtaskset scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: type: object x-kubernetes-map-type: atomic x-kubernetes-preserve-unknown-fields: true status: type: object x-kubernetes-map-type: atomic x-kubernetes-preserve-unknown-fields: true required: - metadata - spec type: object served: true storage: true subresources: status: {} --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: workflowtemplates.argoproj.io spec: group: argoproj.io names: kind: WorkflowTemplate listKind: WorkflowTemplateList plural: workflowtemplates shortNames: - wftmpl singular: workflowtemplate scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: type: object x-kubernetes-map-type: atomic x-kubernetes-preserve-unknown-fields: true required: - metadata - spec type: object served: true storage: true --- apiVersion: v1 kind: ServiceAccount metadata: name: argo namespace: argo --- apiVersion: v1 kind: ServiceAccount metadata: name: argo-server namespace: argo --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: argo-role namespace: argo rules: - apiGroups: - coordination.k8s.io resources: - leases verbs: - create - get - update - apiGroups: - "" resources: - secrets verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: rbac.authorization.k8s.io/aggregate-to-admin: "true" name: argo-aggregate-to-admin rules: - apiGroups: - argoproj.io resources: - workflows - workflows/finalizers - workfloweventbindings - workfloweventbindings/finalizers - workflowtemplates - workflowtemplates/finalizers - cronworkflows - cronworkflows/finalizers - clusterworkflowtemplates - clusterworkflowtemplates/finalizers - workflowtasksets - workflowtasksets/finalizers - workflowtaskresults - workflowtaskresults/finalizers verbs: - create - delete - deletecollection - get - list - patch - update - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: rbac.authorization.k8s.io/aggregate-to-edit: "true" name: argo-aggregate-to-edit rules: - apiGroups: - argoproj.io resources: - workflows - workflows/finalizers - workfloweventbindings - workfloweventbindings/finalizers - workflowtemplates - workflowtemplates/finalizers - cronworkflows - cronworkflows/finalizers - clusterworkflowtemplates - clusterworkflowtemplates/finalizers - workflowtaskresults - workflowtaskresults/finalizers verbs: - create - delete - deletecollection - get - list - patch - update - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: rbac.authorization.k8s.io/aggregate-to-view: "true" name: argo-aggregate-to-view rules: - apiGroups: - argoproj.io resources: - workflows - workflows/finalizers - workfloweventbindings - workfloweventbindings/finalizers - workflowtemplates - workflowtemplates/finalizers - cronworkflows - cronworkflows/finalizers - clusterworkflowtemplates - clusterworkflowtemplates/finalizers - workflowtaskresults - workflowtaskresults/finalizers verbs: - get - list - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: argo-cluster-role rules: - apiGroups: - "" resources: - pods - pods/exec verbs: - create - get - list - watch - update - patch - delete - apiGroups: - "" resources: - configmaps verbs: - get - watch - list - apiGroups: - "" resources: - persistentvolumeclaims - persistentvolumeclaims/finalizers verbs: - create - update - delete - get - apiGroups: - argoproj.io resources: - workflows - workflows/finalizers - workflowtasksets - workflowtasksets/finalizers - workflowartifactgctasks verbs: - get - list - watch - update - patch - delete - create - apiGroups: - argoproj.io resources: - workflowtemplates - workflowtemplates/finalizers - clusterworkflowtemplates - clusterworkflowtemplates/finalizers verbs: - get - list - watch - apiGroups: - argoproj.io resources: - workflowtaskresults verbs: - list - watch - deletecollection - apiGroups: - "" resources: - serviceaccounts verbs: - get - list - apiGroups: - argoproj.io resources: - cronworkflows - cronworkflows/finalizers verbs: - get - list - watch - update - patch - delete - apiGroups: - "" resources: - events verbs: - create - patch - apiGroups: - policy resources: - poddisruptionbudgets verbs: - create - get - delete --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: argo-server-cluster-role rules: - apiGroups: - "" resources: - configmaps verbs: - get - watch - list - apiGroups: - "" resources: - secrets verbs: - get - create - apiGroups: - "" resources: - pods - pods/exec - pods/log verbs: - get - list - watch - delete - apiGroups: - "" resources: - events verbs: - watch - create - patch - apiGroups: - "" resources: - serviceaccounts verbs: - get - list - watch - apiGroups: - argoproj.io resources: - eventsources - sensors - workflows - workfloweventbindings - workflowtemplates - cronworkflows - clusterworkflowtemplates verbs: - create - get - list - watch - update - patch - delete --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: argo-binding namespace: argo roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: argo-role subjects: - kind: ServiceAccount name: argo namespace: argo --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: argo-binding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: argo-cluster-role subjects: - kind: ServiceAccount name: argo namespace: argo --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: argo-server-binding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: argo-server-cluster-role subjects: - kind: ServiceAccount name: argo-server namespace: argo --- apiVersion: v1 kind: ConfigMap metadata: name: workflow-controller-configmap namespace: argo --- apiVersion: v1 kind: Service metadata: name: argo-server namespace: argo spec: ports: - name: web port: 2746 targetPort: 2746 selector: app: argo-server --- apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: workflow-controller value: 1000000 --- apiVersion: apps/v1 kind: Deployment metadata: name: argo-server namespace: argo spec: selector: matchLabels: app: argo-server template: metadata: labels: app: argo-server spec: containers: - args: - server - --namespaced - --auth-mode - server - --auth-mode - client env: [] image: quay.io/argoproj/argocli:v3.4.9 name: argo-server ports: - containerPort: 2746 name: web readinessProbe: httpGet: path: / port: 2746 scheme: HTTPS initialDelaySeconds: 10 periodSeconds: 20 securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: true runAsNonRoot: true volumeMounts: - mountPath: /tmp name: tmp nodeSelector: kubernetes.io/os: linux securityContext: runAsNonRoot: true serviceAccountName: argo-server volumes: - emptyDir: {} name: tmp --- apiVersion: apps/v1 kind: Deployment metadata: name: workflow-controller namespace: argo spec: selector: matchLabels: app: workflow-controller template: metadata: labels: app: workflow-controller spec: containers: - args: [] command: - workflow-controller env: - name: LEADER_ELECTION_IDENTITY valueFrom: fieldRef: apiVersion: v1 fieldPath: metadata.name image: quay.io/argoproj/workflow-controller:v3.4.9 livenessProbe: failureThreshold: 3 httpGet: path: /healthz port: 6060 initialDelaySeconds: 90 periodSeconds: 60 timeoutSeconds: 30 name: workflow-controller ports: - containerPort: 9090 name: metrics - containerPort: 6060 securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: true runAsNonRoot: true nodeSelector: kubernetes.io/os: linux priorityClassName: workflow-controller securityContext: runAsNonRoot: true serviceAccountName: argo
 
Create the Argo namespace and install using the install.yaml file:
💻
kubemaster VM
kubectl create ns argo kubectl apply -n argo -f argo.yaml
 
Check the Argo installation:
💻
kubemaster VM
kubectl get pods -n argo
 
Once all pods are running, make Argo available for access outside of the kubemaster VM:
💻
kubemaster VM
kubectl patch svc argo-server -n argo -p '{"spec": {"type": "LoadBalancer"}}'
 
You can access the Argo Workflows page from a web browser on a remote computer:
https://kubemaster:2746
Note: The browser my issue warnings that the site is not secure, but you can safely ignore these warnings and navigate to the site.
Once you get to the site, you may get an introduction page. After getting past it, you should see the following:
notion image

 
notion image

Hera Workflows Installation

Create the Hera install file:
💻
kubemaster VM
nano hera.yaml
 
with the following contents:
apiVersion: v1 kind: Pod metadata: name: hera spec: serviceAccount: hera containers: - image: nginx name: hera
 
Using the hera.yaml file created above, create a Kubernetes role for hera in the argo namespace:
💻
kubemaster VM
kubectl create role hera --verb=list,update,create --resource=workflows.argoproj.io -n argo kubectl create sa hera -n argo kubectl create rolebinding hera --role=hera --serviceaccount=argo:hera -n argo kubectl apply -f hera.yaml -n argo
 
Check the Hera installation:
💻
kubemaster VM
kubectl get pods -n argo
Execute until you see that "hera" has a status of "Running”
 

 
notion image

Create Container Image for Workflows using Podman

Install Podman:
💻
kubemaster VM
sudo apt install podman -y
Create the image that will be used by kubernetes using a container file:
💻
kubemaster VM
nano Containerfile
with the following contents:
FROM docker.io/library/ubuntu:latest # install required system packages RUN apt-get update && apt-get install -y \ wget \ bzip2 \ ca-certificates \ git \ && rm -rf /var/lib/apt/lists/* # set up environment variables ENV CONDA_DIR=/opt/conda ENV PATH=${CONDA_DIR}/bin:${PATH} # install conda RUN wget https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh RUN bash Miniforge3-$(uname)-$(uname -m).sh -b -p ${CONDA_DIR} RUN rm -f Miniforge3-$(uname)-$(uname -m).sh RUN /bin/bash -c "source /opt/conda/bin/activate" RUN conda init # install packages to Conda for workflows RUN conda install python -y RUN pip install minio RUN pip install hera COPY public.crt . COPY minio_access.txt .
This will create an Ubuntu container image that includes the MinIO Python package.
Note: The Python package for Hera is included so that Hera workflows can initiate other Hera workflows. This is not covered in this tutorial, but installing it is beneficial for future projects when you want your Hera workflows to kickoff other Hera workflows within Kubernetes.
 
Build the image using podman and make a image installation file from it:
💻
kubemaster VM
rm -f argodemo.tar podman build -t argodemo:v1 -o argodemo . podman save -o argodemo.tar argodemo:v1
 
 
Install the image installation file and check that was installed successfully:
💻
kubemaster VM
sudo k3s ctr images import argodemo.tar sudo k3s ctr -a /run/k3s/containerd/containerd.sock images ls | grep argodemo
 

Testing the System with a Workflow

 
Create a test workflow:
💻
Local host machine (argodev)
nano workflow_test.py
with the following code:
Workflow Test
#! /usr/bin/env python # This example has one task that stores a file to MinIO which is retrieved # by a second task from hera.shared import global_config from hera.workflows import DAG, Task, Workflow, service, script # define the container image to use global_config.image = "localhost/argodemo:v1" ################### # WARNING # The bearer token and authentication certificate not used for security. # The following disables warnings caused by insecure certificate # verification.For a production system, these should be set up. import urllib3 urllib3.disable_warnings() ################### # Create and upload a file to MinIO @script() def file_uploader(): from minio import Minio from minio.error import S3Error # this tells MinIO where to find the certificate os.environ['SSL_CERT_FILE'] = "public.crt" file = open("minio_access.txt", 'r') endpoint = file.readline().strip() accessKey = file.readline().strip() secretKey = file.readline().strip() file.close() # create a client with the MinIO server, its access key # and secret key. client = Minio( endpoint=endpoint, access_key=accessKey, secret_key=secretKey, secure=True ) try: # Make 'spirit' bucket if not exist. found = client.bucket_exists("spirit") if not found: client.make_bucket("spirit") else: print("Bucket 'spirit' already exists") # create a file to upload to the bucket file = open("/ghost.txt", "w") file.write("abcd\n") file.write("efgh\n") file.close() # upload '/ghost.txt' as object name # 'ghost.txt' to bucket 'spirit' client.fput_object( "spirit", "ghost.txt", "/ghost.txt", ) print( "'/ghost.txt' is successfully uploaded as " "object 'ghost' to bucket 'spirit'." ) except S3Error as exc: print("error occurred.", exc) exit(-1) # read file stored in MinIO by file_uploader() @script() def file_reader(): from minio import Minio from minio.error import S3Error # this tells MinIO where to find the certificate os.environ['SSL_CERT_FILE'] = "public.crt" file = open("minio_access.txt", 'r') endpoint = file.readline().strip() accessKey = file.readline().strip() secretKey = file.readline().strip() file.close() # create a client with the MinIO server, its access key # and secret key. client = Minio( endpoint=endpoint, access_key=accessKey, secret_key=secretKey, secure=True ) try: response = client.get_object( bucket_name = "spirit", object_name = "ghost.txt") print("Contents of file 'ghost.txt'") print(response.data.decode()) except S3Error as exc: print("error occurred.", exc) exit(-1) # define and execute the miniotest argo workflow namespace = "argo" service = service.WorkflowsService(host="https://kubemaster:2746", verify_ssl=False) with Workflow(generate_name="miniotest-", namespace=namespace, automount_service_account_token=True, workflows_service=service, entrypoint="m") as w: with DAG(name="m"): # define the tasks in the workflow t1: Task = file_uploader() t2: Task = file_reader() # execute the tasks sequentially t1 >> t2 # create, execute workflow and wait until workflow is complete w.create()
 
Make the script executable and run it:
💻
Local host machine (argodev)
chmod +x workflow_test.py ./workflow_test.py
This workflow has two tasks. One to create and store a file to MinIO and another that reads the file from MinIO and displays its contents.
 
You can review the workflow’s progress on the Argo Workflows page:
https://localhost:2746
You should see the miniotest workflow:
notion image
 
Once the workflow has completed successfully, you’ll see a green checkmark:
notion image
 
Click within the miniotest workflow box. You should see the following:
notion image
This is a graphical representation of the executed workflow. The top node represents the workflow itself. The middle node represents the file-uploader task and the bottom node represents the file-reader task.
If you click on the “LOGS” button near the top left of the screen, you’ll see the logs generated by the tasks.

Conclusion

You now have what you need to begin developing and testing Argo workflows using Hera. For more information, you can visit Hera’s documentation website: [[https://hera-workflows.readthedocs.io/en/latest/#the-basics]]. Additionally, using workflow_test.py as a template, you can quickly begin writing your own workflows.

Cleanup

This section provides the necessary command for if/when you wish to remove the tools used in this tutorial.

To Remove Multipass

Ubuntu / Fedora

⚠️
This also removes the kubemaster VM along with k3s and all of its packages
multipass delete --purge --all sudo snap remove multipass

MacOS

⚠️
This also removes the kubemaster VM along with k3s and all of its packages
multipass delete --purge --all brew uninstall --zap multipass rm -rf ~/Library/Caches/com.canonical.multipass rm -rf "/Applications/Multipass.app"

To Remove Homebrew

MacOS

⚠️
Don’t remove Homebrew if you’re still using it for applications other than Multipass
sudo /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)" sudo rm -rf /opt/homebrew # Apple Silicon (M1/M2/M3) sudo rm -rf /usr/local/Homebrew # Intel Macs sudo rm -rf ~/.cache/Home brew

To Remove the argodev Conda Environment

# if currently in the argodev environment, you need to deactivate it conda deactivate # remove argodev environment conda env remove -n argodev -y
 

To Remove Snap

Fedora

⚠️
Don’t remove snap if you’re using it for applications other than Multipass
sudo dnf remove snapd -y

 
notion image
 
©️ 2026 Saturnrise. All rights reserved.