Featured image of post CI/CD pipeline with GitHub Actions and ArgoCD Image Updater

CI/CD pipeline with GitHub Actions and ArgoCD Image Updater

In a previous post “GitHub Actions Runner on your Kubernetes cluster”, I set up a GitHub Actions runner on my Kubernetes cluster, and used it to automatically deploy my applications whenever the pipeline was triggered.

Although it worked, I soon realized it was not the ideal way to set up this kind of automation. It creates a tight coupling between the GitHub repository and the deployment. It is not a huge problem for some small projects that I develop alone, but I just wanted to do it the right way, so here we go.

Overview

The goal of this post is to set up a complete CI/CD pipeline for a project on GitHub. The pipeline consists of the following steps:

  1. Define application manifest using Kustomize
  2. Manage the deployment with ArgoCD
  3. GitHub Actions: Build and push the Docker image
  4. ArgoCD Image Updater: Automatically update the image in the deployment

The core of this setup is ArgoCD Image Updater. It is a tool that automatically updates the image in a deployment when a new image is pushed to the container registry. However, to make it work, there are a few prerequisites.

Prerequisites

  1. A Kubernetes cluster
  2. A dockerized application to deploy
  3. ArgoCD installed in the cluster

In addition to above, from the documentation of ArgoCD Image Updater:

  • The applications you want container images to be updated must be managed using Argo CD. There is no support for workloads not managed using Argo CD.
  • Argo CD Image Updater can only update container images for applications whose manifests are rendered using either Kustomize or Helm and - especially in the case of Helm - the templates need to support specifying the image’s tag (and possibly name) using a parameter (i.e. image.tag).

There are two options here: Kustomize and Helm. Per my understanding, Kustomize is easier to set up, and helm is more geared towards distributing applicationsto customers or other users. So I chose Kustomize for my stuff.

Define application manifest using Kustomize

When using Kustomize, there are three important components to understand:

  1. Base: The base directory contains the base resources that are common to all environments.
  2. Overlays: The overlays directory contains the environment-specific resources.
  3. Kustomization file: The kustomization.yaml file found in base and each overlay directory.

Basically it serves the purpose of “customizing” the base resources to deploy to different environments. With a kustomization.yml file, it combines multiple resource yml files into one application, and it can also deploy additional resources or override existing ones.

An example directory structure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
├── base
│   ├── deployment.yml
│   ├── kustomization.yml
│   └── service.yml
└── overlays
    └── stg
        ├── ingress.yml
        ├── kustomization.yml
        └── volume.yml
    └── prd
        ├── certificate.yml
        ├── ingress.yml
        ├── kustomization.yml
        └── volume.yml

I’m not going to show the content of all those yml files here, but let’s go over the kustomization.yml files.

base/kustomization.yml

1
2
3
4
5
6
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - deployment.yml
  - service.yml

This is a simple example of a kustomization file. It packages together the deployment.yml and service.yml files into one application.

overlays/prd/kustomization.yml

 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
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

# image tag to use for the production environment (updated by ArgoCD Image Updater)
images:
- name: jyking99/review-planner
  newTag: prd-20250209

# resources specific to the production environment
resources:
- ../../base
- volume.yml
- ingress.yml
- certificate.yml

# namespace for prod environment
namespace: review

# only the production environment needs 2 replicas
replicas:
- name: review
  count: 2

# secrets to be generated for the production environment
secretGenerator:
- name: review-db-creds
  literals:
  - username=user
  - password=pass

This is an example of an overlay file, which customizes the base resources for the production environment.

Notice that there are additional manifest files here (resources section) that do not exist in the base. This is where you define environment-specific resources such as persistent volumes, ingress, and TLS certificates for HTTPS. \

The namespace and having two replicas are also specific to the production environment.

Another cool feature is the secretGenerator section. It generates a secret with the specified fields without having to manually encode them in base64.

Deploy

After setting up the application manifest, you can deploy it to your cluster with the following command:

1
kubectl apply -k overlays/prd

Don’t forget to push the manifeset files to your git repository, since ArgoCD will watch the repository for changes.

Manage the deployment with ArgoCD

If you’ve used ArgoCD, you probably know how to deploy applications with it. Just create a git application and register the repository URL and path to the manifest files.

For example, I have an application with repository URL as “git@github.com:jywang99/homelab.git” and path as “kube/blog/overlays/prd” (it is the application for this blog!).

Install ArgoCD Image Updater

Installing ArgoCD Image Updater is as simple as this (from the official documentation):

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

This deploys a pod called argocd-image-updater in the argocd namespace.

Give instructions to ArgoCD Image Updater

ArgoCD Image Updater by itself does not do anything yet. We have to give it some instructions, by means of adding annotations to the applications managed by ArgoCD.

Note

The annotations are added to the ArgoCD application, not the deployment that is managed by ArgoCD.

For example:

1
kubectl edit app blog -n argocd

and add these annotations:

1
2
3
4
5
  annotations:
    argocd-image-updater.argoproj.io/image-list: review=jyking99/blog
    argocd-image-updater.argoproj.io/myimage.allow-tags: regexp:^prd-*
    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/write-back-target: kustomization

This tells ArgoCD Image Updater to:

  1. Update the image of the review container in the deployment with the image from jyking99/blog repository.
  2. Only consider tags that start with prd-.
  3. Update the image tag in the kustomization.yml file on git repository.

GitHub Actions

Now let’s set up an automatic way to update the image, whenever there is a change in the master branch of our application repository.

For this, I used a slightly modified version of the workflow file from a previous post: GitHub Actions Runner on your Kubernetes cluster.

 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
name: Build and Push Docker Image

on:
  push:
    branches:
      - master

env:
  DOCKER_REPO: ${{ secrets.DOCKER_USERNAME }}/blog

jobs:
  build:
    runs-on: jylab-runner-set

    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        with:
          submodules: true

      - name: Log in to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build, tag, and push
        run: |
          DATE_TAG=$(date +'%Y%m%d')
          docker build -t $DOCKER_REPO:release -t $DOCKER_REPO:$DATE_TAG .
          docker push $DOCKER_REPO:release && docker push $DOCKER_REPO:$DATE_TAG          

Moment of truth

Now, whenever you push a change to the master branch,

  1. GitHub Actions builds and pushes the new image to the container registry.
  2. ArgoCD Image Updater detects the new image and updates the image tag in the kustomization.yml file.
  3. ArgoCD detects the change in the git repository and applies the new image to the deployment.

In short, any change to the master branch is automatically deployed to the production environment!

Try it out by pushing something to the master branch of your application repository.

Conclusion

Honestly, this setup is a bit overkill for the small personal projects that I work on, especially since I only have one environment (prod). However, having this kind of automated setup just feels good.

Built with Hugo
Theme Stack designed by Jimmy