Automate Continuous Integration From Code to Deployment with Argo Events + Argo Workflows + Argo CD

2022-11-03

One of the main benefits of CI is that it helps to identify and fix errors quickly. When code changes are regularly merged into the main branch, this triggers a build and deployment to your cluster, which it is easier to detect errors early on, rather than having to sift through a large change to locate and fix the problem.

In this article, I will walk through how to set up a basic code-to-deployment automation with Argo Events, Argo Workflows, and Argo CD.

Note: This article assumes that you have a Kubernetes cluster running. Since setting up an automated CI/CD is a complex setup depending on various factors, I will only be giving a high-level example walkthrough of the setup and it is not meant to be a quick copy-and-paste guide. You should refer to the official documentation.

Setting up Argo Workflows + Argo Events on your Kubernetes cluster

In order to automate deployment builds, we will be using Argo Workflows to carry out container image build jobs on Kubernetes. The good thing about Argo Workflows is that it works natively with Argo Events, which allows us to trigger workflows based on events, aka. when a new commit is pushed to a Git repository.

1. Install Argo Workflows & Argo Events in your Kubernetes cluster

To install Argo Workflows on your Kubernetes cluster, you can use the following command:

kubectl create namespace argo
kubectl apply -n argo -f https://github.com/argoproj/argo-workflows/releases/download/v3.4.3/install.yaml

Next, install Argo Events:

kubectl create namespace argo-events
kubectl apply -n argo-events -f https://raw.githubusercontent.com/argoproj/argo-events/stable/manifests/install.yaml
# Install with a validating admission controller
kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-events/stable/manifests/install-validating-webhook.yaml

Next, we will need to deploy the eventbus for Argo Events:

kubectl apply -n argo-events -f https://raw.githubusercontent.com/argoproj/argo-events/stable/examples/eventbus/native.yaml

You should now have both Argo Workflows and Argo Events installed on your Kubernetes cluster.

2. Create a Cluster Workflow Template for Container Image Build

Next, we will need to create a Cluster Workflow Template for building our container image. For this, we will be using Kaniko, which is a tool for building container images inside a Kubernetes cluster.

This example template will be used by Argo Workflows to build your container image.

apiVersion: argoproj.io/v1alpha1
kind: ClusterWorkflowTemplate
metadata:
  name: build-container-image
spec:
  templates:
    - name: build-kaniko-git
      inputs:
        parameters:
          - name: app_repo
          - name: ref
          - name: dockerfile
          - name: container_image
          - name: container_tag
      container:
        image: gcr.io/kaniko-project/executor:debug
        args:
          - --context={{inputs.parameters.app_repo}}#{{inputs.parameters.ref}}
          - --dockerfile={{inputs.parameters.dockerfile}}
          - --destination={{inputs.parameters.container_image}}:{{inputs.parameters.container_tag}}
          - --cache=true

        # If you are using a private container registry, you can specify the volume mount here for your repository secret
        # https://github.com/GoogleContainerTools/kaniko#pushing-to-different-registries
        # volumeMounts:
        #   - name: aws-secret
        #     mountPath: /root/.aws/

        # If you are using a private git repository, you can pass in the repository secret as environment variables
        # https://github.com/GoogleContainerTools/kaniko#using-private-git-repository
        # env:
        #   - name: GIT_TOKEN
        #     valueFrom:
        #       secretKeyRef:
        #         name: github-access
        #         key: token

        resources:
          requests:
            cpu: 1.8
            memory: 3Gi
          limits:
            cpu: 2
            memory: 6Gi
      retryStrategy:
        limit: '3'
        retryPolicy: Always

Go ahead and configure the template above and deploy it to your Kubernetes cluster:

kubectl apply -f build-container-image.yaml

3. Create a sensor to trigger the workflow

Next, we will need to create an event source for Argo Events to listen for events from our Git repository.

Set up a webhook on your git repository to send events to Argo Events, instructions here.

In this example, we will be using the GitHub event source.

# Info on GitHub Webhook: https://developer.github.com/v3/repos/hooks/#create-a-hook
apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
  name: github
spec:
  replicas: 1
  service:
    ports:
      - name: github-event
        port: 12000
        targetPort: 12000
  github:
    main:
      repositories:
        - owner: your_github_username_or_org
          names:
            - app_repo_name
      # Github will send events to following port and endpoint
      webhook:
        # endpoint to listen to events on
        endpoint: /push
        # port to run internal HTTP server on
        port: '12000'
        # HTTP request method to allow. In this case, only POST requests are accepted
        method: POST
        # url the event-source will use to register at Github.
        # This url must be reachable from outside the cluster.
        # The name for the service is in `<event-source-name>-eventsource-svc` format.
        # You will need to create an Ingress or Openshift Route for the event-source service so that it can be reached from GitHub.
        url: https://domain-name.com
      # type of events to listen to.
      # following listens to everything, hence *
      # You can find more info on https://developer.github.com/v3/activity/events/types/
      events:
        - '*'

      # apiToken refers to K8s secret that stores the github api token
      # if apiToken is provided controller will create webhook on GitHub repo
      # +optional
      apiToken:
        # Name of the K8s secret that contains the access token
        name: github-access
        # Key within the K8s secret whose corresponding value (must be base64 encoded) is access token
        key: token

      # webhookSecret refers to K8s secret that stores the github hook secret
      # +optional
      webhookSecret:
        # Name of the K8s secret that contains the hook secret
        name: github-access
        # Key within the K8s secret whose corresponding value (must be base64 encoded) is hook secret
        key: secret

      # type of the connection between event-source and Github.
      # You should set it to false to avoid man-in-the-middle and other attacks.
      insecure: false
      # Determines if notifications are sent when the webhook is triggered
      active: true
      # The media type used to serialize the payloads
      contentType: json

Next, we will need to create a sensor to trigger the workflow when a new commit is pushed to a Git repository. The workflow will push the newly built image with the tag 'latest' which will be used for the automated deployment later – you may change this tag to whatever you want.

apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
  name: trigger-build-container-image
spec:
  replicas: 1
  template:
    serviceAccountName: operate-workflow-sa
  dependencies:
    - name: github-eventsource
      eventSourceName: github
      eventName: main
      filters:
        data:
          # Name of the event that triggered the delivery: [pull_request, push, yadayadayada]
          # https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads
          - path: headers.X-Github-Event
            type: string
            value:
              - push
          - path: body.repository.name
            type: string
            comparator: '='
            value:
              # specify the respository name, i.e.
              - (^myapp$)
          - path: body.ref
            type: string
            comparator: '='
            value:
              # specify the branch you want to trigger the workflow, i.e.
              - (^refs\/heads\/branch_name$)
  triggers:
    - template:
        name: build-my-app
        k8s:
          operation: create
          source:
            resource:
              apiVersion: argoproj.io/v1alpha1
              kind: Workflow
              metadata:
                generateName: ci-build-my-app-
                labels:
                  workflows.argoproj.io/archive-strategy: 'false'
              spec:
                entrypoint: build
                podGC:
                  strategy: OnPodCompletion
                volumes:
                  - name: github-access
                    secret:
                      secretName: github-access
                # specify the appropriate secret required to push image
                #   - name: aws-secret
                #     secret:
                #       secretName: aws-test-secret
                #       items:
                #         - key: credentials
                #           path: credentials
                arguments:
                  parameters:
                    - name: repo_name
                    - name: ref
                    - name: tag
                      value: latest
                templates:
                  - name: build
                    inputs:
                      parameters:
                        - name: repo_name
                        - name: ref
                        - name: tag
                    dag:
                      tasks:
                        - name: build-my-app
                          templateRef:
                            name: build-container-image
                            template: build-kaniko-git
                            clusterScope: true
                          arguments:
                            parameters:
                              - name: app_repo
                                value: 'git://github.com/<username_or_org>/{{inputs.parameters.repo_name}}'
                              - name: dockerfile
                                value: Dockerfile
                              - name: container_image
                                value: 'link.com/to/your/container/registry/my-app'
                              - name: ref
                                value: '{{inputs.parameters.ref}}'
                              - name: container_tag
                                value: '{{inputs.parameters.tag}}'
          parameters:
            - src:
                dependencyName: github-eventsource
                dataKey: body.repository.name
              dest: spec.arguments.parameters.0.value
            - src:
                dependencyName: github-eventsource
                dataKey: body.ref
              dest: spec.arguments.parameters.1.value
      retryStrategy:
        steps: 3

Once you have configured your event source and sensor, go ahead and deploy to your Kubernetes cluster:

kubectl apply -f event-source.yaml
kubectl apply -f sensor.yaml

4. Test the workflow

If everything is set up correctly, you should be able to see the container image build workflow triggered when you push a new commit to your repository.

If you have Argo CLI installed on your machine, you can inspect the workflow by running:

argo list -n argo-events

To check the status of the workflow, run:

argo get @latest -n argo-events

Automate Deployment with Argo CD

Now that we have a triggered workflow to build our container image, we can use Argo CD to automate the deployment of our application. For this, we will also be using Argo CD Image Updater to simplify the deployment of newly built container image.

1. Install Argo CD

kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

You may also want to configure Argo CD acces to your private git repository, instruction here.

2. Install Argo CD Image Updater

kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj-labs/argocd-image-updater/stable/manifests/install.yaml

3. Create Argo CD Application

To create an Argo CD application, we will need to create a Kubernetes manifest file that contains the application definition. In this application manifest, we will also specify annotations that will be discovered by Argo CD Image Updater to detect and update the newly built container image.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  annotations:
    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/image-list: >-
      link.com/to/your/registry/image:latest
    argocd-image-updater.argoproj.io/update-strategy: digest
spec:
  destination:
    namespace: default
    server: 'https://kubernetes.default.svc'
  source:
    path: app/to/deployment/manifest.yaml
    repoURL: 'git@github.com:owner/repository_name.git'
    targetRevision: main
  project: default
  syncPolicy:
    # Automatically deploy application when a new image is available
    automated:
      selfheal: true

Once you have configured your application manifest, go ahead and deploy it to your Kubernetes cluster:

kubectl apply -f app.yaml

Test it out

Now that we have everything set up, we can test it out by pushing a new commit to your repository.

Once the commit is pushed, you should be able to see the container image build workflow triggered. Once the workflow is completed, you should be able to see Argo CD Image Updater was able to pick up the latest change and new container image deployed to your cluster automatically.