[Ep.17] Closing the gap between CI and CD with the Argo CD Image Updater
Argo CD is very good at one thing: making sure that whatever is stored in Git is what is running on the cluster. It compares both things and updates the cluster accordingly. It is not good at noticing that a CI pipeline has just published a new container image (into the registry). That is the gap between Continuous Integration and Continuous Delivery, and most of us have closed it with a small piece of glue: a pipeline task that clones the configuration repository, patches a tag and pushes the change. I did exactly that in Step 8 of the Secure Supply Chain series with a Tekton task.
With OpenShift GitOps 1.21 the Argo CD Image Updater became generally available. This controller aims to close the gap between CI and CD.
Instead of a hand-written pipeline task, you describe what you would like to do in an ImageUpdater custom resource: which applications to watch, which images to track, which versions are acceptable and where the result should be written.
I tried to create a full walkthrough with this article. We will build a tiny container image, deploy it with Argo CD, let the Image Updater track it, and then push a new version and watch what happens.
What the Image Updater does
The Image Updater is a controller running next to Argo CD. It runs a reconciliation loop that watches the Argo CD applications you selected and queries the container registries for newer tags of the images you configured. When it finds a version that satisfies your constraints, it instructs Argo CD to use the new image.
There are two properties that should be considered before we start.
The Image Updater does not deploy anything. It only changes the desired state, either the Application resource or a file in Git. Whether that change is rolled out depends on the sync policy of your application. With an automated sync policy the new image goes live. Without it, the application turns OutOfSync and waits for manual input.
The Image Updater needs a parameterised manifest, such as Kustomize or Helm. Plain YAML with a hard-coded image in a Deployment is not supported, because there is no parameter for the controller to override.
The supported registries that can be observed for changes, include Docker Hub, Red Hat Quay, GitHub Container Registry, GitLab Container Registry, Google Container Registry, Azure Container Registry, JFrog Artifactory and anything else that implements the Docker Registry v2 API.
Why not just use a pipeline task?
A pipeline task that patches a tag and pushes the change works. I used it myself and it is fine for a single pipeline. But as the number of applications and environments grows, the approach has a few rough edges:
The CI pipeline needs write access to the configuration repository. That is a privilege most pipelines should not have. A compromised build could push any image tag it wants. With the Image Updater, only the controller writes to the configuration repository and the CI pipeline only needs push access to the registry.
Every pipeline has to carry the same glue code. Cloning a second repository, running
kustomize edit set imageoryq, committing and pushing: that logic lives in every pipeline, often copy-pasted, and it drifts over time. The Image Updater replaces all of it with a single custom resource.The pipeline only runs when you trigger it. If somebody pushes an image outside of the pipeline, nothing happens. The Image Updater polls the registry independently, so it catches every new tag that matches the constraint, no matter who pushed it.
Version constraints are built in. A pipeline task blindly writes the tag it just built. The Image Updater evaluates semantic version ranges, tag filters and ignore lists before it acts. You define what is acceptable once and the controller enforces it.
Pull request support comes for free. Opening a pull request from a pipeline task means scripting the GitHub or GitLab API. The Image Updater has it built in.
In short: the pipeline task is imperative glue that couples CI to the configuration repository. The Image Updater is a declarative controller that removes that coupling.
A word about the setup
Everything below was done on a single OpenShift cluster with the default openshift-gitops Argo CD instance. You need:
OpenShift Container Platform with the Red Hat OpenShift GitOps Operator 1.21 or later. The
ImageUpdatercustom resource does not exist in earlier versions.cluster-admin privileges, or at least permission to modify the ArgoCD custom resource.
A Git repository for the configuration. I use https://github.com/tjungbauer/argocd-image-updater-demo.git in this article. Replace it with your own.
A registry account you can push to. I use Quay.io, because you can create a public repository in a few clicks and push a new tag whenever you want to trigger an update.
podman (or docker) on your workstation.
| Do not run OpenShift GitOps 1.21.0. Several issues relevant to this article, including a goroutine leak in the Image Updater, were fixed in 1.21.1. Check your version with oc get csv -n openshift-gitops-operator | grep gitops. |
Building a test image
The first thing we need to see an update happen is an image whose tags we can control. This can be any image. I have created a simple image for testing purposes: BusyBox with its built-in HTTP server, serving a single page that displays its own version.
FROM docker.io/library/busybox:1.37
ARG APP_VERSION=0.0.0 (1)
RUN mkdir -p /www && \
printf '<html><body style="font-family:sans-serif">\n<h1>simple-app</h1>\n<p>version: %s</p>\n</body></html>\n' \
"${APP_VERSION}" > /www/index.html && \
chmod 0755 /www && chmod 0644 /www/index.html (2)
EXPOSE 8080
USER 1001 (3)
ENTRYPOINT ["/bin/busybox", "httpd", "-f", "-v", "-p", "8080", "-h", "/www"] (4)| 1 | The version is baked in at build time so that you can see in the browser which image is running. |
| 2 | The files must be readable by any user ID. OpenShift runs the container with a random UID from the namespace range, not with the UID from the image. |
| 3 | A non-root user. OpenShift would enforce this anyway through the restricted-v2 security context constraint, but declaring it makes the image usable elsewhere as well. |
| 4 | BusyBox httpd in foreground mode on port 8080. Ports below 1024 would require root privileges, which you do not get. |
Build and push the first version. Replace <tjungbau> with your own namespace. (If you use a different registry, replace the registry name accordingly):
podman build --build-arg APP_VERSION=1.0.0 --platform linux/amd64 -t quay.io/tjungbau/simple-app:1.0.0 .
podman push quay.io/tjungbau/simple-app:1.0.0| Make the Quay repository public. A private repository works too, but then you need a pull secret for the cluster and registry credentials for the Image Updater, which adds two moving parts to the first test. There is a section on private registries further down. |
Verify that the image runs locally before you involve a cluster:
podman run --rm -p 8080:8080 quay.io/tjungbau/simple-app:1.0.0
curl -s http://localhost:8080It should return the HTML page with the version number.
<html><body style="font-family:sans-serif">
<h1>simple-app</h1>
<p>version: 1.0.0</p>
</body></html>The Kubernetes manifests
The configuration repository https://github.com/tjungbauer/argocd-image-updater-demo.git holds a Kustomize overlay. The layout is deliberately small:
argocd-image-updater-demo/
└── overlays
└── dev
├── deployment.yaml
├── service.yaml
├── route.yaml
└── kustomization.yaml| Change the repository URL to your own, if you want to use a different repository. |
apiVersion: apps/v1
kind: Deployment
metadata:
name: simple-app
labels:
app: simple-app
spec:
replicas: 1
selector:
matchLabels:
app: simple-app
template:
metadata:
labels:
app: simple-app
spec:
containers:
- name: simple-app
image: quay.io/tjungbau/simple-app:1.0.0 (1)
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /
port: http
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
securityContext: (2)
allowPrivilegeEscalation: false
runAsNonRoot: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault| 1 | The tag written here is only the starting point. Kustomize overrides it, and later the Image Updater overrides the Kustomize value. |
| 2 | This block satisfies the restricted-v2 security context constraint explicitly. OpenShift would inject most of it, but if the namespace enforces the restricted Pod Security Standard, the manifest has to carry it. |
apiVersion: v1
kind: Service
metadata:
name: simple-app
spec:
selector:
app: simple-app
ports:
- name: http
port: 8080
targetPort: httpapiVersion: route.openshift.io/v1
kind: Route
metadata:
name: simple-app
spec:
to:
kind: Service
name: simple-app
port:
targetPort: http
tls:
termination: edge
insecureEdgeTerminationPolicy: RedirectapiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: image-updater-demo
resources:
- deployment.yaml
- service.yaml
- route.yaml
images: (1)
- name: quay.io/tjungbau/simple-app
newTag: "1.0.0"| 1 | This is the parameter the Image Updater will modify. Without an images section, Kustomize has nothing to override and the Image Updater has nothing to write to. |
Be sure that the above manifests are pushed to your repository.
The Argo CD application
The Argo CD application is a normal one. No annotations, no special fields. As repository, we use the one we just created.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: image-updater-demo
namespace: openshift-gitops (1)
spec:
project: default
source:
repoURL: https://github.com/tjungbauer/argocd-image-updater-demo.git
targetRevision: main
path: overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: image-updater-demo
syncPolicy:
automated: (2)
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true| 1 | Remember this namespace. The ImageUpdater resource has to live in the same namespace as the applications it references. |
| 2 | With an automated sync policy an image update is rolled out immediately. Leave it out if you want a human to press the button. |
Apply the application and check that it comes up:
oc apply -f application.yaml
oc get application image-updater-demo -n openshift-gitops
oc get pods,route -n image-updater-demoOpen the route in a browser. It should show version: 1.0.0.
Enabling the Argo CD Image Updater
The controller is enabled per Argo CD instance in the ArgoCD custom resource. By default, it is not enabled.
apiVersion: argoproj.io/v1beta1
kind: ArgoCD
metadata:
name: openshift-gitops
namespace: openshift-gitops
spec:
imageUpdater:
enabled: true (1)| 1 | Deploys the Image Updater controller for this Argo CD instance. |
If you manage the Argo CD instance declaratively, and you should (see Ep.3), add the block to your existing manifest. For a quick test, patch the resource:
oc patch argocd openshift-gitops -n openshift-gitops \
--type merge \
-p '{"spec":{"imageUpdater":{"enabled":true}}}'Wait for the controller to appear. The Operator names the deployment after the Argo CD instance, so for the default instance it is openshift-gitops-argocd-image-updater-controller.
oc get deployment -n openshift-gitops | grep image-updateropenshift-gitops-argocd-image-updater-controller 1/1 1 1 2m| If you use a differently named Argo CD instance, the deployment name changes with it. The commands in this article use the default name. Store it in a variable if you work on a custom instance. |
Keep the log open in a second terminal for the rest of this article:
oc logs -n openshift-gitops \
deployment/openshift-gitops-argocd-image-updater-controller -fWhich namespaces are watched
By default the controller only looks at the namespace it was installed in. As soon as you use applications in any namespace, you have to tell it where else to look. This is done with environment variables in the same block:
apiVersion: argoproj.io/v1beta1
kind: ArgoCD
metadata:
name: openshift-gitops
namespace: openshift-gitops
spec:
imageUpdater:
enabled: true
env:
- name: IMAGE_UPDATER_LOGLEVEL (1)
value: info
- name: IMAGE_UPDATER_WATCH_NAMESPACES (2)
value: "openshift-gitops,image-updater-demo"| 1 | Log level of the controller. Valid values are debug, info, warn and error. Set it to debug while you build your first ImageUpdater resources. It tells you which tags were considered and why they were rejected. |
| 2 | Comma-separated list of namespaces watched for ImageUpdater resources and for Argo CD applications. |
The first ImageUpdater resource
The ImageUpdater custom resource answers three questions:
which applications
which images
where does the result go
apiVersion: argocd-image-updater.argoproj.io/v1alpha1
kind: ImageUpdater
metadata:
name: image-updater-demo
namespace: openshift-gitops (1)
spec:
applicationRefs:
- namePattern: "image-updater-demo" (2)
images:
- alias: "simple-app" (3)
imageName: "quay.io/tjungbau/simple-app:~1.0" (4)| 1 | The controller uses metadata.namespace to decide where to search for applications. A resource in openshift-gitops will never find an application in image-updater-demo. |
| 2 | Glob pattern matching the application name. |
| 3 | A name for this image inside the resource. You need it when you map Helm parameters later. |
| 4 | The image, including a version constraint. ~1.0 allows patch releases inside 1.0, so 1.0.1 and 1.0.7 are accepted while 1.1.0 is not. |
Apply it and check the status:
oc apply -f imageupdater.yaml
oc get imageupdater image-updater-demo -n openshift-gitops -o yamlIf this returns 1, the selector works. If it returns 0 or nothing, the resource is in the wrong namespace or the name pattern does not match.
| Always set a version constraint. Without one, the Image Updater may move to any newer version it finds, including a major release. In 1.21 the field spec.namespace was removed from this API. A resource that still contains it fails validation. |
| A second rule that is easy to break in a large repository: never let two ImageUpdater resources match the same application. Both will update it and overwrite each other, and the image version will keep flipping back and forth. |
Triggering the first update
Now build and push a new patch version:
podman build --build-arg APP_VERSION=1.0.1 --platform linux/amd64 -t quay.io/tjungbau/simple-app:1.0.1 .
podman push quay.io/tjungbau/simple-app:1.0.1The controller polls the registry, so the update appears after the reconciliation interval. Watch the log, then confirm the result on the Application resource:
oc get application image-updater-demo -n openshift-gitops \
-o jsonpath='{.spec.source.kustomize.images}{"\n"}'["quay.io/tjungbau/simple-app:1.0.1"]This is the default write-back method at work: the controller wrote a Kustomize parameter override into the Application resource on the cluster. Because the application has an automated sync policy, Argo CD rolls it out. Reload the route and the page shows version: 1.0.1.
The update history in the status tells you what happened and when:
oc get imageupdater image-updater-demo -n openshift-gitops -o jsonpath='{.status.recentUpdates}{"\n"}'[{"alias":"simple-app","applicationsUpdated":1,"image":"quay.io/tjungbau/simple-app","message":"Updated from 1.0.0 to 1.0.1.","newVersion":"1.0.1","updatedAt":"2026-09-15T15:36:07Z"}]Now look at the repository. Nothing changed there. The kustomization.yaml still says 1.0.0, and the cluster runs 1.0.1. Argo CD simply overrides the value with the new one. That is the reason the next section exists.
Write-back methods
Method 1: Argo CD API, the default
The controller writes the new image into the Application resource, which is what you just saw. No configuration is needed. If you omit writeBackConfig, this is what you get.
It is quick and it is simple. It also has a failure mode that costs people an afternoon: if the Application itself is managed in Git, for example through an App-of-Apps pattern (see Ep.4), the next sync from Git overwrites the parameter override. The cluster falls back to the old image and the reason is not obvious.
Use this method for applications created through the UI or the CLI, and for experiments like the one above.
| Do not treat this method as a production-ready solution. |
Method 2: Git write-back
The Git method fits the idea that the repository is the single source of truth. The controller clones the repository, checks out the branch, writes the change and pushes it.
Here a secret must be created that holds credentials with write access to the configuration repository. For HTTPS the secret needs the keys username and password, and the password should be a personal access token:
oc create secret generic git-creds -n openshift-gitops \
--from-literal=username=my-bot-user \
--from-literal=password=<personal_access_token>Then extend the ImageUpdater:
apiVersion: argocd-image-updater.argoproj.io/v1alpha1
kind: ImageUpdater
metadata:
name: my-app-updater
namespace: openshift-gitops
spec:
applicationRefs:
- namePattern: "my-app-dev"
images:
- alias: "simple-app"
imageName: "quay.io/tjungbau/simple-app:~1.0"
writeBackConfig:
method: "git:secret:openshift-gitops/git-creds" (1)
gitConfig:
repository: "https://github.com/tjungbauer/argocd-image-updater-demo.git" (2)
branch: "main" (3)| 1 | Use the Git method with the credentials from the referenced secret. Plain method: "git" reuses the credentials Argo CD already has for the repository, which are often read-only. |
| 2 | The repository that receives the commit. |
| 3 | The target branch. |
| The secret has to live in the same namespace as the ImageUpdater resource (here: openshift-gitops). Cross-namespace references are rejected, so that a team cannot read a secret it does not own. |
Push version 1.0.2 and watch the repository:
podman build --build-arg APP_VERSION=1.0.2 --platform linux/amd64 -t quay.io/tjungbau/simple-app:1.0.2 .
podman push quay.io/tjungbau/simple-app:1.0.2By default the controller does not touch your kustomization.yaml. It creates or updates a file called .argocd-source-image-updater-demo.yaml in the application path. That file contains a parameter override which Argo CD merges on top of the rendered manifests. Your own files stay untouched, which is safe, but it also means the repository now contains two statements about the image version: kustomization.yaml says 1.0.0 and the generated file says 1.0.2.
If you prefer the commit to change the source of truth itself, set a write-back target:
writeBackConfig:
method: "git:secret:openshift-gitops/git-creds"
gitConfig:
repository: "https://github.com/mycompany/my-app-config.git"
branch: "main"
writeBackTarget: "kustomization" (1)| 1 | The change is committed as a kustomize edit set image operation, so kustomization.yaml carries the new tag. |
For Helm applications the equivalent target is a values file:
writeBackConfig:
method: "git:secret:openshift-gitops/git-creds"
gitConfig:
branch: "main"
writeBackTarget: "helmvalues:/helm/config/values.yaml"| after switching from the API method to a Git method, remove the leftover override from the Application resource once, otherwise the old value stays in spec.source.kustomize.images and keeps winning: oc patch application my-app-dev -n openshift-gitops --type json -p '[{"op":"remove","path":"/spec/source/kustomize/images"}]' |
Method 3: Pull request or merge request
If your main branch is protected, which makes sense, the controller cannot push to it. The pull request method extends the Git method. The commit goes to a generated branch named image-updater-<namespace>-<appName>-<sha256>, and a pull or merge request is opened against the base branch. If a request for the same branch pair already exists, no duplicate is created.
writeBackConfig:
method: "git:secret:openshift-gitops/git-creds"
gitConfig:
repository: "https://github.com/mycompany/my-app-config.git"
branch: "main"
pullRequest:
github: {} (1)| 1 | For GitLab, use gitlab: {} instead. |
Push version 1.0.3 and watch the repository:
podman build --build-arg APP_VERSION=1.0.3 --platform linux/amd64 -t quay.io/tjungbau/simple-app:1.0.3 .
podman push quay.io/tjungbau/simple-app:1.0.3And once the Controller has created the pull request, you can see it in the GitHub UI:

This pull request can now be reviewed and merged (or rejected).
| Creating a pull request requires credentials with a bearer token, so a personal access token or GitHub App credentials. SSH keys do not work here, because the controller has to call the SCM API over HTTP and an SSH key provides no token for that. |
Choosing a method
| Method | Use it when | Consequence |
|---|---|---|
Argo CD API | Playgrounds, demos, applications not managed in Git | A sync from Git reverts the update |
Git, direct push | Development and test environments | A bot account with write access to the configuration repository |
Git, pull request | Production and protected branches | Somebody has to merge the request, otherwise nothing is deployed |
Update strategies
A tag like v1.0.0-3f2a1b is not a semantic version, and latest never changes its name. Four strategies are available, configured with commonUpdateSettings.updateStrategy:
| Strategy | Description |
|---|---|
semver (default) | Updates to the highest version matching the semantic version constraint. The right choice when the pipeline produces release tags, as in the example above. |
newest-build | Updates to the tag with the most recent creation timestamp, regardless of version numbering. This is the strategy for pipelines that tag with a build ID or a commit SHA. |
alphabetical | Updates to the last tag when sorted alphabetically. Useful for date-based tags such as |
digest | Tracks a mutable tag such as |
In practice a strategy is combined with a tag filter, because registries contain plenty of tags you usually do not want to deploy:
spec:
applicationRefs:
- namePattern: "image-updater-demo"
images:
- alias: "simple-app"
imageName: "quay.io/tjungbau/simple-app"
commonUpdateSettings:
updateStrategy: "newest-build" (1)
allowTags: "regexp:^build-[0-9]+$" (2)| 1 | Take the most recently built image instead of the highest number. |
| 2 | Only tags matching this regular expression are considered. |
Two parameters control the filtering:
- allowTags
A match function applied to every tag. Supports regexp:<expression> and any, which is the default.
- ignoreTags
A comma-separated list of glob patterns that are excluded.
commonUpdateSettings:
allowTags: "regexp:^[0-9]+\\.[0-9]+\\.[0-9]+$"
ignoreTags: "*-rc*"| If a filter does not behave as expected, set IMAGE_UPDATER_LOGLEVEL to debug. The log shows which tags were fetched and which ones survived the filter. That is faster than guessing at the regular expression. |
To try the newest-build strategy with the demo image, push a few tags in a row:
for i in 1 2 3; do
podman build --build-arg APP_VERSION="build-${i}" -t quay.io/tjungbau/simple-app:build-${i} .
podman push quay.io/tjungbau/simple-app:build-${i}
doneSelecting applications
Name patterns are convenient, but in a fleet generated by an ApplicationSet, labels scale better:
spec:
applicationRefs:
- labelSelectors:
matchLabels:
tier: "frontend"
matchExpressions:
- key: env
operator: In
values:
- staging
- production
images:
- alias: "simple-app"
imageName: "quay.io/<your_quay_account>/simple-app:~1.0"Name pattern and label selector can be combined, in which case both have to match.
There is a third variant meant for ApplicationSets. If every generated application should carry its own image configuration, set useAnnotations: true. The controller then reads the configuration from the argocd-image-updater.argoproj.io/image-list annotation of each application instead of from the resource:
spec:
applicationRefs:
- namePattern: "generated-app-*"
useAnnotations: true (1)| 1 | With this enabled, an images block in the resource is ignored for this applicationRef. Only namePattern and labelSelectors remain effective. |
Combining useAnnotations: true with namePattern: "*" and no label selector means every application in the namespace is processed. That works, but it costs performance on a large instance. |
Private registries
Public registries work without configuration. For a private one you have two options.
Per image, reference a pull secret in the ImageUpdater:
spec:
applicationRefs:
- namePattern: "my-app-dev"
images:
- alias: "simple-app"
imageName: "registry.example.com/mycompany/simple-app:~1.0"
pullSecret: "pullsecret:openshift-gitops/myregistry-pull-secret" (1)| 1 | Uses a Docker-style pull secret with a .dockerconfigjson key. |
The well-known registries are supported out of the box.
For a registry that is not known out of the box, describe it once in the argocd-image-updater-config config map in the namespace of the Argo CD instance:
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-image-updater-config
namespace: openshift-gitops
data:
registries.conf: |
registries:
- name: My Private Registry
prefix: myregistry.example.com (1)
api_url: https://myregistry.example.com (2)
credentials: secret:openshift-gitops/registry-creds#creds (3)
default: false (4)| 1 | The prefix that image names start with. |
| 2 | The registry API endpoint. |
| 3 | Credentials for querying the registry API. |
| 4 | Whether this registry is used for images without a registry prefix. |
Practical considerations
These are the questions that come up in customer workshops.
- Who is committing to our repository?
A bot account with a personal access token. Treat it like any other privileged identity: a dedicated account, minimal scope, rotated regularly, and no administrator rights. If branch protection requires signed commits, the git command line inside the controller has to sign them, which means providing a GPG or SSH signing key to the pod. Upstream this is configured with git.commit-signing-key and git.commit-signing-method in the argocd-image-updater-config config map. I was not able to test this yet, but it should be considered for production use.
- Does this break our Secure Supply Chain?
No, but it changes where the gate sits. Previously the pipeline was the only writer, and it had already verified signatures, scanned the image with ACS and generated an SBOM (see the Secure Supply Chain series). If the Image Updater now picks images directly from the registry, the tag itself becomes the contract. Let the pipeline push a release tag such as 1.2.3 only after all checks passed, and restrict the Image Updater with allowTags: "regexp:^\\.[0-9]\\.[0-9]+$". Everything that failed a check never gets a tag that the Image Updater accepts.
- Can we roll back?
With a Git write-back method, a rollback is a git revert. With the API method there is nothing to revert.
Limitations
Only Kustomize-rendered and Helm-rendered manifests are supported. A Helm chart has to expose parameters for the image.
Image pull secrets have to exist on the cluster where the controller runs.
One application has to be targeted by exactly one ImageUpdater resource.
Summary
The Argo CD Image Updater replaces a piece of glue that most of us wrote ourselves, and it does so declaratively. An ImageUpdater resource states which applications are watched, which images are tracked, which versions are acceptable and where the result is persisted. Since OpenShift GitOps 1.21 it is generally available and enabled with one flag in the ArgoCD custom resource.
If you take one thing from this article, let it be the write-back decision. The default API method is the fastest way to a working demo and the fastest way to a confusing incident. For anything that matters, write back to Git: directly on a development branch, and through a pull request wherever the branch is protected. The repository stays the source of truth, a rollback stays a git revert, and the audit trail stays intact.
And as always: If it is not in Git, it does not exist.
Copyright © 2020 - 2026 Toni Schmidbauer & Thomas Jungbauer
![image from [Ep.17] Closing the gap between CI and CD with the Argo CD Image Updater](https://blog.stderr.at/gitopscollection/images/logos/ep-17-gitops.png)
![Featured image for [Ep.16] Tuning OpenShift GitOps for performance at scale](/gitopscollection/images/logos/ep-16-gitops.png)
![Featured image for [Ep.15] OpenShift GitOps - Argo CD Agent](/gitopscollection/images/agent/Logo-ArgoCDAgent.png)
Discussion
Comments are powered by GitHub Discussions. To participate, you'll need a GitHub account.
By loading comments, you agree to GitHub's Privacy Policy. Your data is processed by GitHub, not by this website.