ArgoCD - How to Automate Deployments
Disclaimer: This post was originally written for my company’s blog . I’ve translated it and added a personal touch for this space.
In the previous ArgoCD article, we walked through installing ArgoCD and using it to deploy a Helm-based application across multiple environments.
What I deliberately skipped was the natural follow-up question: How can new container images be automatically rolled out without manually triggering syncs or touching the cluster?
That’s exactly what this article is about. We’ll look at common ways to integrate CI/CD pipelines with ArgoCD and then dive deeper into one of the most practical options: the ArgoCD Image Updater, including a step-by-step setup.
Automation Strategies
The goal of our setup is a clean GitOps-style workflow: Our GitLab pipeline builds new container images, pushes them to the GitLab Container Registry, and ArgoCD ensures those images end up running in the cluster — automatically and reliably.
There are three common ways to achieve this.
Git Commit from the Pipeline
The CI/CD pipeline updates the image tag directly in Git (e.g., in the values.yaml file) and commits the change. ArgoCD detects the Git update and deploys it. This method is fully Git-driven and traceable - but it requires Git access and commit rights for the pipeline. This adds a bit of complexity and is also not super desirable from a security perspective.
Trigger an ArgoCD Sync from the Pipeline
Here, the new image tag is already present in Git (e.g., via dynamic tags like latest, dev-*, etc.). After pushing the image, the pipeline explicitly triggers a sync of the ArgoCD Application via API or CLI. While this is easy and quick to implement, it comes with a downside: The deployment happens without a corresponding Git change. That breaks the core GitOps idea of Git being the single source of truth.
Use the ArgoCD Image Updater
The ArgoCD Image Updater is a controller running inside the cluster that regularly checks container registries for new tags and reacts accordingly.
If it finds a new tag, it can update the Git repository accordingly. These Git changes are picked up by ArgoCD and deployed. Alternatively, it can also trigger deployments without modifying Git. Either way, this setup requires some additional configuration - but with the benefit of keeping the pipeline simple and bringing the deployment logic back to ArgoCD.
Since this article focuses on ArgoCD itself, let’s take a closer look at its native solution - the Image Updater.
Prerequisites
If you want to follow along, you’ll need the setup from the previous ArgoCD article, including:
- a running ArgoCD instance
- the Helm charts in GitLab
- a CI pipeline and images pushed to the GitLab Container Registry
To keep things simple for now, we’ll work with plain Application YAMLs for a frontend and backend - and skip ApplicationSets and AppProjects.
Updating the Existing Configurations
Before installing the Image Updater, we need to prepare our existing setup:
values.yaml
In each values.yaml, replace the image tag with a placeholder like this:
repository: <Registry link>
tag: "abc123"
The Image Updater will later overwrite this value.
In our Application files, we’ll add annotations so the Image Updater knows what to track and how to update:
frontend.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: frontend-dev
namespace: argocd
annotations:
argocd-image-updater.argoproj.io/image-list: frontend=<Registry Link>
argocd-image-updater.argoproj.io/frontend.update-strategy: newest-build
argocd-image-updater.argoproj.io/frontend.helm.image-tag: image.tag
argocd-image-updater.argoproj.io/frontend.helm.image-name: image.repository
spec:
project: default
source:
repoURL: <Repository Link>
targetRevision: main
path: <Path to Helm Chart>
helm:
valueFiles:
- values/dev.yaml
destination:
server: https://kubernetes.default.svc
namespace: dev
syncPolicy:
automated:
prune: true
selfHeal: true
backend.yaml follows the same pattern.
We can now apply the updated Applications with:
kubectl apply -f frontend.yaml
kubectl apply -f backend.yaml
Creating a Registry Secret
To allow the Image Updater to read from the GitLab Container Registry, it needs appropriate credentials. We’ll use a GitLab Deploy Token with Read Registry permissions.
We create the token under Settings → Repository → Deploy Tokens, then prepare the credentials.
Base64-encode the credentials:
echo -n "<Deploy Token Name>:<Deploy Token Password>" | base64
Create the .dockerconfigjson file:
{
"auths": {
"<Registry Link>": {
"username": "<Deploy Token Name>",
"password": "<Deploy Token Password>",
"email": "REGISTRY_EMAIL",
"auth": "Base64-Credentials from above"
}
}
}
Encode the file:
cat .dockerconfigjson | base64
Create the Kubernetes Secret:
apiVersion: v1
kind: Secret
type: kubernetes.io/dockerconfigjson
metadata:
name: registry-credentials
namespace: argocd
data:
.dockerconfigjson: <base64 output from encoding the file>
Now we can apply the secret to our cluster.
Installing the ArgoCD Image Updater
With everything prepared, we can now install the ArgoCD Image Updater:
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj-labs/argocd-image-updater/master/manifests/install.yaml
This creates various resources, including RBAC roles and ConfigMaps - which we’ll adjust next.
Configuring Registry Access
To work with your registry, the Image Updater needs access to the secret we just created. There are two options to configure this:
- Direct annotation in each Application:
argocd-image-updater.argoproj.io/backend.pull-secret: pullsecret:argocd/registry-credentials
- Global reference in the Image Updater’s ConfigMap
Direct annotations are useful if different applications use different registries - this keeps access fine-grained. For shared credentials, the global option is simpler and more efficient in small or homogeneous setups.
We’ll go with the second approach here. Create a configmap.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-image-updater-config
namespace: argocd
data:
registries.conf: |
registries:
- name: GitLab Registry
api_url: <Registry URL>
prefix: <Registry URL without https>
credentials: pullsecret:argocd/registry-credentials
default: true
insecure: false
Deploy it with:
kubectl apply -f configmap.yaml
At this point you should no longer see any registry-related errors in the logs.
Seeing it in action
Now let’s test the setup: Trigger your pipeline so it builds and pushes a new image to the registry. Important: if you’ve been using generic tags like latest, you should switch to a unique tag like the commit short SHA ($CI_COMMIT_SHORT_SHA). This avoids caching issues and makes deployments traceable.
Once the pipeline is done, check your cluster again. After a short wait, you should see a deployment with the new image – no manual deploy step needed. 🎉
Optimization Options
To keep things simple, we used a fairly basic setup. But the Image Updater offers many customization options:
- filtering or excluding tags
- writing changes back to Git (write-back-method: git)
- updating Kustomize files instead of Helm values
This makes it easy to adapt the controller’s behavior to the structure of your repository. More details on configuration can be found in the official docs .
Update (after this article was written)
With the v1.0.0 release, the Image Updater has moved from annotation-based configuration to a CRD-based config. I might play around with this later - and keep you posted then :)
