Posted in gitops on January 1, 2025 by Adrian Wyssmann ‐ 10 min read
Key leanings from GitOps Enterprise: Learn how to use Argo CD in multi-tenant installations, how to create preview environments and more.
At first, it is important to differentiate the possible scenarios, or as the course calls it categories:
| Category | Description | Type | Change Frequency | Target User | What they are for |
|---|---|---|---|---|---|
| 1 | Developer Kubernetes Manifest | Helm, Kustomize or plain manifest in git | Very often | Developers mostly | Describe the state of any application to any of your organization environments (QA/Staging/Production etc) |
| 2 | Developer Argo CD Manifest | Argo CD app and Application Set | Almost never | Operators/Developers | Policy configurations referencing the source of truth for an application i.e Category 1 |
| 3 | Infrastructure Kubernetes manifests | Usually external Helm charts | Sometimes | Operators | Describe the state of any infrastructure application (e.g. logging, monitoring, …) to any of your organization environments (QA/Staging/Production etc) |
| 4 | Infrastructure Argo CD manifests | Argo CD app and Application Set | Almost never | Operators | cy configurations referencing the source of truth for an infrastructure application i.e Category 3 |
Category 1 change very often as due to new releases
There is an artictle How to Model Your GitOps Environments and Promote Releases between Them which specifically talks about these
As these reference the developer manifests (applications), which define the proper state, the manifests in Category 2 should be defined once and then forget about it.
Same as category 1 or 2 respectively, only focusing on infrastructure applications.
Keep in mind, an application shall be installable locally using kustomize or helm, without ArgoCD in place. If that’s possible then you are probably doing it right.
So even so ArgoCD offers the ability to have different types of manifest as part of your Application/Application Set, you shall maintain a very clear separation between Kubernetes (category 1) and Argo CD resources (category 2).
As part of source you can have helm or kustomize. Here some rules
valuesFiles and reference the values file in the git repoFollowing Anti-pattern 1, remember that Argo CD application CRDs should only point to the main Kubernetes manifests/helm charts + value files. So Application CRD shall be static and there should not be a need to even change TargetRevision. Changes should always happen on the underlying Kubernetes manifests. Thus also enables a clear history of events for what an application did.
Templating of Application CRDs should not be used. A classical example would be using a helm chart containing Application CRDs which themselves point to the Helm charts of the Kubernetes manifests. If templating is necessary, use Application Sets instead.
ApplicationSets allow you to generate multiple Applications CRDs. This scales easily if you increase the clusters for example.
The starting point should be a 3-level structure as shown in the image below

Notice how simple this pattern is:
So let’s have a look at https://github.com/kostis-codefresh/many-appsets-demo
├── apps <- Level 3
├── appsets <- Level 2
├── docs
├── README.md
└── root-argocd-app.yml <- Level 1If we look into more details, we can see each app has a folder under apps (apps/<name-of-app>/envs/<name-of-env>) containing the manifests:
├── apps
│ ├── billing
│ │ ├── base
│ │ │ ├── deployment.yml
│ │ │ ├── kustomization.yml
│ │ │ └── service.yml
│ │ └── envs
│ │ ├── prod-eu
│ │ │ ├── deployment.yml
│ │ │ ├── kustomization.yml
│ │ │ ├── replicas.yml
│ │ │ ├── settings.yml
│ │ │ └── version.yml
│ │ └── prod-us
│ │ ├── deployment.yml
│ │ ├── kustomization.yml
│ │ ├── replicas.yml
│ │ ├── settings.yml
│ │ └── version.yml
│ ├── fake-invoices
│ │ ├── base
│ │ │ ...
│ │ └── envs
│ │ └── qa
│ │ ...
│ ├── invoices
│ │ ├── base
│ │ │ ...
│ │ └── envs
│ │ ├── prod-eu
│ │ │ ...
│ │ ├── prod-us
│ │ │ ...
│ │ ├── qa
│ │ │ ...
│ │ └── staging
│ │ ...
│ ├── orders
│ │ ...
│ └── payments
│ ...
...In the appsets folder, we keep all our application sets:
├── appsets
│ ├── my-prod-appset.yml
│ ├── my-qa-appset.yml
│ └── my-staging-appset.ymlIf you check the structure in details you will see
As ArgoCD does not deal with source code, so some concepts/techniques do not make sense. Following the anti-pattern, hence it’s also important to separate source code and deployment manifests.
Considering that, one is tempted to move all manifests into a single code repository. This approach suffers from several scalability and performance issues as the number of Argo CD applications grows
it has several limitations not only in the way that Argo CD detects commits but also how the Git repository deals with conflicts and retries from all the different workflows that interface with the Git repository as a deployment source.
The recommendation is to have multiple Git repositories. Ideally one for each team or each department plus one for infrastructure. Also important is, that you don’t mix infrastructure applications and developer applications in the same Git repo.
Following Kubernetes Deployment Antipatterns the best way to deal with changes, is to have dynamic environments for testing, so a developer can test their new features in isolation. Argo CD offers a way to create dynamic environments by using the Pull Request Generator. It can monitor any Git repository for pull requests and for any feature (in parallel) the following process applies
There are also some caveats with this approach
This is something I want to dig deeper but for now it’s fine to know that the possibility exist.
When using helm there are basically two approaches to choose from - your company should focus on a single one and not mix them:
Chart.yml file) keeps track of the application contained in the Chart.When it comes to define environment specific values, we shall use the values hierarchy supported by Helm: Have multipe value files (from common values to environment specific), which are then combined by helm. This way you don’t duplicate your configuration multiple times. You can always override values in the lower level:
├── values
│ └── common-values.yam
│ └── all-prod-envs.yaml
│ └── specific-prod-cluster.yamlAs for the helm cli, the same applies for Argo CD: Provide the value files in expected order and remember, the last one wins. So in an Application CRD you would specify
...
helm:
valueFiles:
- common-values.yaml
- all-prod-envs.yaml
- specific-prod-cluster.yamlIt’s recommended to consume helm charts from git rather registries, mainly due to the following reasons
However - especially for infrastructure applications - you probably rely on charts from the registry. Then use the multi source feature with at least 2 sources
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: application_name
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- list:
elements:
- cluster: a
clusterid: a
targetRevision: x.x.x.x
- cluster: b
clusterid: b
targetRevision: x.x.x.x
template:
metadata:
name: application_name-{{.env}}
namespace: argocd
spec:
destination:
namespace: application_name
name: "{{.cluster}}"
project: infrastructure
sources:
- repoURL: https://scm.example.com/infrastructure.git
targetRevision: HEAD
ref: values
- repoURL: https://helm.example.com/
targetRevision: "{{.targetRevision}}"
chart: application_name
helm:
releaseName: application_name
valueFiles:
- $values/application_name/{{.env}}/values.yaml
- $values/application_name/values.yamlThe above example shows an ApplicationSet, but same is also possible for Applications. What we usually also do is make the targetRevision of the helm chart parametrized, so combined with the generators list we can individually update the helm chart per cluster.
The generator list in application sets can become very long, especially the more enviornment you have. It may be better to extract this useful information in individual files by using the Git file generator:
generators:
- git:
repoURL: https://github.com/kostis-codefresh/multi-sources-example.git
revision: HEAD
files:
- path: "appsets/4-final/env-config/**/config.json"appsets/4-final/env-config/prod/us/config.json would look like this:
{
"env": "prod-us",
"region": "us",
"type": "prod",
"version": "prod",
"chart": "0.1.0"
}That’s it for now, there are some more stuff to come.