Two environments, one ApplicationSet
Most GitOps setups I inherit look the same: one ArgoCD application per environment, a values file per environment, and a production deployment that happens the moment someone merges to the right branch. The pipeline is honest about what it does. It is just not honest about what a mistake costs.
On my own platform I wanted three things at once: one definition of the deployment, a development environment that moves on its own, and a production environment that cannot change without a human deciding to change it.
One ApplicationSet, two environments
The whole delivery is a single ApplicationSet with a list generator:
generators:
- list:
elements:
- env: development
namespace: interview-challenger-development
valuesFile: envs/development/values.yaml
- env: production
namespace: interview-challenger-production
valuesFile: envs/production/values.yaml
Each element produces an application name, a namespace, and the values file to apply. Adding an environment is one list entry, not a new manifest to copy and drift.
The chart lives in the application repository, next to the code it deploys. The values live in a separate configuration repository, because they change for reasons the code does not: a new image tag, a replica count, a feature flag.
That split needs a multi-source application:
sources:
- repoURL: https://github.com/…/interview-challenger.git
targetRevision: main
path: deploy/charts/interview-challenger
helm:
valueFiles:
- $configrepo/envs/development/values.yaml
- repoURL: https://github.com/…/interview-challenger-gitops.git
targetRevision: main
ref: configrepo
ref: configrepo is the part people miss. Without it, the chart cannot read
values from another repository, and the two repos end up back in one.
Where the gate lives
Both applications auto-sync, with pruning and self-healing on. That sounds irresponsible for production, until you look at where the approval sits.
Production is not protected by stopping the sync. It is protected because the only thing that changes production is a pull request on the configuration repository, and that pull request needs a human to merge it. Once merged, ArgoCD does what it is good at: converging the cluster to the file.
I prefer this to manual sync in ArgoCD, and I would argue the case against it:
- Manual sync hides drift. A cluster that has not synced looks identical to a cluster that is up to date, in every dashboard you own.
- Manual sync moves the gate into a tool that is not reviewable. An approval buried in a UI leaves no diff, no author, no reason.
- A merge request has all three. Who, what, why — and it can be reverted.
The rule I follow: gate the change, not the mechanism that applies it.
What the pipeline does before the PR exists
The release workflow builds the images, tags them with the commit SHA — never
latest — pushes to the registry, and then opens the deploy pull requests
itself. Development is set to auto-merge. Production is opened and left waiting.
Nothing is deployed by a human typing a tag into a form. The tag is a fact about the code, and the pull request is the only place where the decision happens.
When this is too much
Two environments, one chart and a config repo is roughly the right size for a
product with real users and one team. It is too much for a side project that
deploys once a month: there, a single docker compose up and a backup are more
honest.
The pattern is worth copying when three conditions hold: more than one environment, more than one person able to merge, and a cost to being wrong in production. If one of those is false, the ceremony buys nothing.