Automated Image Updates with Argo CD Image Updater
Watching container registries for new tags and updating Applications automatically - update strategies and Git write-back
Introduction
Pure GitOps means every deployed change traces back to a Git commit - but that leaves a gap for the most common change of all, a new image tag after CI builds one. Someone (or some CI job) still has to open a PR bumping the tag in Git. Argo CD Image Updater is a separate controller (again, its own project alongside core ArgoCD) that closes that gap: it polls configured registries, and when it finds a new tag matching an Application’s update strategy, it updates the image reference itself - either directly against the running Application or by committing back to Git, keeping the automation GitOps-compatible either way.
Installing Image Updater
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj-labs/argocd-image-updater/stable/manifests/install.yaml
It runs as its own Deployment in the argocd namespace, separate from the core argocd-application-controller, argocd-repo-server, etc. - it needs read access to the Applications it manages and (for the Git write-back method) write access to the Git repos it commits to.
Marking an Application for Updates
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: web-app
annotations:
argocd-image-updater.argoproj.io/image-list: webapp=myregistry/web-app
argocd-image-updater.argoproj.io/webapp.update-strategy: semver
argocd-image-updater.argoproj.io/webapp.allow-tags: regexp:^[0-9]+\.[0-9]+\.[0-9]+$
spec:
source:
repoURL: https://github.com/example/web-app-manifests.git
targetRevision: main
path: manifests
helm:
parameters:
- name: image.tag
value: 2.3.0
image-list declares which image(s) to watch and gives each an alias (webapp here) that the rest of the annotations reference. update-strategy: semver picks the highest semantic version among tags matching allow-tags - other strategies are latest (most recently pushed tag, by registry creation timestamp), digest (track a single mutable tag like latest by digest rather than by a changing tag name), and name (lexically greatest tag string, for non-semver schemes).
Update Methods
By default Image Updater updates the live Application object directly (adding an override parameter ArgoCD then syncs) - fast, but that override lives only in the Application resource, not in Git, so it’s invisible to anyone reading the repo and gets lost if the Application itself is ever recreated from Git. The Git write-back method fixes that by committing the change to the actual source repo:
metadata:
annotations:
argocd-image-updater.argoproj.io/write-back-method: git
argocd-image-updater.argoproj.io/git-branch: main
apiVersion: v1
kind: Secret
metadata:
name: git-creds
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repo-creds
stringData:
url: https://github.com/example/web-app-manifests.git
username: image-updater-bot
password: ghp_xxxxxxxxxxxxxxxxxxxx
With write-back-method: git, a detected new tag results in an actual commit to manifests on the main branch, updating the Helm values.yaml (or Kustomize kustomization.yaml image override, depending on the Application’s source type) - the change is now a real Git commit with a real author, restoring the audit trail that direct-to-Application updates skip, at the cost of one extra reconcile round-trip (commit, then ArgoCD’s own Git polling picks it up and syncs).
Registry Authentication
apiVersion: v1
kind: Secret
metadata:
name: image-updater-registry-creds
namespace: argocd
stringData:
registries.conf: |
registries:
- name: My Private Registry
api_url: https://myregistry.example.com
prefix: myregistry.example.com
credentials: secret:argocd/image-updater-registry-creds#creds
credsexpire: 10h
Public registries (Docker Hub, ghcr.io for public images) work without configuration, but a private registry needs credentials configured either per-registry in registries.conf or, on EKS/GKE/AKS, via the node’s own cloud-provider IAM identity if the registry is the cloud’s native one (ECR, Artifact Registry, ACR) and workload identity is already set up.
Kustomize Applications
metadata:
annotations:
argocd-image-updater.argoproj.io/image-list: webapp=myregistry/web-app
argocd-image-updater.argoproj.io/webapp.update-strategy: semver
argocd-image-updater.argoproj.io/webapp.kustomize.image-name: myregistry/web-app
For a Kustomize-sourced Application (rather than Helm), the update targets kustomization.yaml’s images: block instead of Helm values - kustomize.image-name tells Image Updater which entry in that block corresponds to the aliased image.
Best Practices
- Pin
allow-tagsto a real pattern rather than leaving it unset - without it,semverstrategy considers every tag, including ones that aren’t actually semver and get silently skipped, or worse, aren’t your release tags at all (alatestorsha-abc123tag mixed into the same repository). - Prefer Git write-back over direct-Application updates for anything beyond a dev/sandbox environment - the audit trail and rollback-via-
git revertit preserves are usually worth the extra round-trip. - Scope registry and Git credentials to exactly what Image Updater needs - a bot token with write access to unrelated repos, or registry credentials broader than the images actually being watched, is unnecessary blast radius.
- Combine with Argo Rollouts for anything beyond dev - Image Updater changing a tag is just a new desired state; pairing it with a
Rollout’s canary/analysis strategy (rather than a plainDeployment) means an automated image bump also gets automated safety checks before it reaches 100% traffic.
Conclusion
Image Updater is the piece that makes “new image tag” not require a human to open a PR - the meaningful design choice is write-back method: direct-to-Application is fast feedback for low-stakes environments, Git write-back keeps the full GitOps audit trail for anything that matters.