A scalable, production-grade GitOps solution for managing Kubernetes addons across multiple clusters using ArgoCD ApplicationSets, External Secrets Operator, and Helm. Supports both AWS (EKS) and GCP (GKE) clusters.
V2 is a complete rewrite of the original V1 solution. If you're looking for V1, see the
v1tag.
- What's New in V2
- Architecture
- Directory Structure
- Supported Addons
- Prerequisites
- Quick Start
- Configuration Guide
- How It Works
- Advanced Features
- Troubleshooting
- Release Notes
V2 is an evolution of the original open-source solution, battle-tested at enterprise scale managing 50+ EKS clusters with 25+ addons. Key improvements:
| Feature | V1 | V2 |
|---|---|---|
| Structure | Nested app-of-apps/ |
Flat bootstrap/ with clear sync waves |
| Cluster config | Multiple files per cluster per addon | Single YAML per cluster with YAML anchors |
| Values extraction | Manual per-addon config files | Git Files generator (automatic per-addon extraction) |
| Secret management | Mixed AVP + ESO | ESO-only (AVP deprecated) |
| Datadog deployment | 3 separate Applications (apikey, tags, agent) | 1 multi-source Application |
| Version overrides | Per-environment only | Per-cluster via labels (<addon>-version) |
| Node scheduling | Not supported | EKS Auto Mode / Karpenter NodePool support |
| Cloud providers | AWS only | AWS (EKS) and GCP (GKE) |
| Addons supported | 3 (ESO, Datadog, KEDA) | 16+ (Istio, Kyverno, Argo suite, Cert Manager, etc.) |
| Addon migration | Not supported | Zero-downtime adoption mode (inMigration) |
flowchart TB
subgraph GIT["Git Repository (this repo)"]
CFG["configuration/\ncluster-addons.yaml\naddons-catalog.yaml\nper-cluster values"]
end
subgraph MGMT["ArgoCD Management Cluster"]
ROOT["Root Application\ncluster-addons-bootstrap"]
ESO["External Secrets\nOperator"]
CSS["ClusterSecretStore"]
CLAPP["Clusters Application\nExternalSecrets"]
APPSETS["ApplicationSets\nMatrix Generator"]
NP["Karpenter NodePools\n(EKS Auto Mode)"]
end
subgraph SECRETS["Secret Store (AWS / GCP)"]
SM[("AWS Secrets Manager\nor GCP Secret Manager")]
end
subgraph REMOTE["Remote EKS / GKE Clusters"]
C1["Cluster 1\nDatadog · KEDA · Istio"]
C2["Cluster 2\nDatadog · ESO · Kyverno"]
C3["Cluster N\n..."]
end
subgraph HELM["Upstream Helm Repos"]
HR["helm.datadoghq.com\ncharts.external-secrets.io\nistio-release · kedacore\nargoproj · jetstack · ..."]
end
GIT ==>|"ArgoCD syncs"| ROOT
ROOT -->|"Wave -2"| ESO
ROOT -->|"Wave -2"| NP
ESO --> CSS
CSS -.->|"fetch credentials"| SM
ROOT -->|"Wave -1"| CLAPP
CLAPP -.->|"ExternalSecrets"| SM
ROOT -->|"Wave 0+"| APPSETS
APPSETS -.->|"Git Files generator\nreads cluster values"| GIT
APPSETS -->|"deploy addons"| C1
APPSETS -->|"deploy addons"| C2
APPSETS -->|"deploy addons"| C3
APPSETS -.->|"pull charts"| HR
| Wave | Component | What it does |
|---|---|---|
| -2 | ESO + ClusterSecretStore | Deploys External Secrets Operator to management cluster |
| -2 | Karpenter NodePools | Creates infrastructure nodes (if eksAutoMode: "true") |
| -1 | Clusters Application | Registers remote clusters via ExternalSecrets from secret store |
| 0+ | ApplicationSets | Matrix generator (clusters + Git Files) creates per-addon Applications |
├── bootstrap/ # Root Application + bootstrap templates
│ ├── root-app.yaml # Entry point - apply this to bootstrap
│ ├── Chart.yaml # Helm chart metadata
│ ├── repository-secret.yaml # GitHub credentials template
│ └── templates/
│ ├── _helpers.tpl # Helper functions (Datadog, ESO params)
│ ├── addons-appset.yaml # Main ApplicationSet (data-driven)
│ ├── clusters.yaml # Cluster registration Application
│ ├── eso.yaml # ESO bootstrap Application
│ └── karpenter-nodepools-appset.yaml # NodePools for EKS Auto Mode
│
├── charts/ # Local Helm charts
│ ├── clusters/ # ExternalSecret per cluster
│ ├── datadog-apikey/ # Datadog API key ExternalSecret
│ ├── eso-configuration/ # ClusterSecretStore
│ ├── karpenter-nodepools/ # Karpenter NodePool resources
│ └── .empty/ # No-op chart for conditional paths
│
├── configuration/ # All configuration (source of truth)
│ ├── bootstrap-config.yaml # ESO, Git repo, region settings
│ ├── addons-catalog.yaml # Addon definitions (repos, versions)
│ ├── cluster-addons.yaml # Cluster registry + addon labels
│ ├── datadog-project-mappings.yaml # Project-to-env for API key lookup
│ ├── karpenter-nodepools-config.yaml # NodePool definitions
│ ├── addons-global-values/ # Global defaults per addon
│ │ ├── <addon>.yaml # Applied to ALL clusters
│ │ └── nodepools-config-values/ # EKS Auto Mode overrides
│ │ └── <addon>-nodepool-config.yaml
│ ├── addons-clusters-values/ # Per-cluster configurations
│ │ └── <cluster-name>.yaml # Single file per cluster
│ └── .skip/ # No-op path for conditional valueFiles
│
├── scripts/ # Bootstrap automation
│ └── create-github-credentials.sh # Bootstrap GitHub repo access
│
└── docs/ # Documentation (optional)
This solution works with any Helm chart — just add an entry to configuration/addons-catalog.yaml. The repository includes pre-configured examples for common addons:
- Datadog — Multi-source pattern with ExternalSecret for API key
- KEDA — Simple addon pattern
- External Secrets — Multi-source with ClusterSecretStore
- Istio (base, istiod, cni, ingress) — Sync-wave ordering pattern
- Kyverno — ServerSideApply pattern
- Cert Manager, Argo Rollouts, External DNS — Standard patterns
These examples demonstrate different integration patterns. Use them as templates for your own addons.
- ArgoCD >= 2.9.0 (for ApplicationSet matrix generator + multi-source apps)
- ApplicationSet controller enabled (default in ArgoCD >= 2.5)
AWS (EKS):
- AWS Secrets Manager access
- IAM roles for IRSA (ESO, Datadog, KEDA, etc.)
- EKS clusters with OIDC provider configured
GCP (GKE):
- GCP Secret Manager access
- GCP service accounts with Workload Identity
- GKE clusters with Workload Identity enabled
- See docs/GKE-SETUP.md for full GCP setup guide
1. Cluster Credentials (one per cluster, named k8s-<cluster-name>):
{
"clusterName": "my-app-dev",
"host": "https://ABCDEF1234.gr7.eu-west-1.eks.amazonaws.com",
"caData": "LS0tLS1CRUdJTi...",
"accountId": "111111111111",
"region": "eu-west-1",
"dd_tags": "env:dev,region:eu-west-1,project:my-app"
}2. Datadog API Keys (centralized at datadog-api-keys-integration):
{
"my-app-dev": "your-datadog-api-key",
"my-app-staging": "your-datadog-api-key"
}3. GitHub Credentials (optional, for private repos at argocd/<management-cluster>):
{
"github_user": "your-username",
"github_token": "ghp_xxxxxxxxxxxx"
}{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Resource": [
"arn:aws:secretsmanager:*:*:secret:k8s-*",
"arn:aws:secretsmanager:*:*:secret:datadog-api-keys-integration*"
]
}]
}git clone https://github.com/<your-org>/argocd-cluster-addons.git
cd argocd-cluster-addonsUpdate placeholders in these files:
configuration/bootstrap-config.yaml— Set your repo URL, host cluster name, AWS region, ESO IAM rolebootstrap/root-app.yaml— Set your repo URL
Create k8s-<cluster-name> secrets in AWS Secrets Manager for each cluster (see Prerequisites).
Edit configuration/cluster-addons.yaml:
clusters:
- name: my-cluster-dev
labels:
datadog: enabled
keda: enabled
external-secrets: enabledCreate configuration/addons-clusters-values/my-cluster-dev.yaml:
clusterGlobalValues:
env: &env dev
clusterName: &clusterName my-cluster-dev
region: ®ion eu-west-1
projectName: my-project
accountId: 111111111111
datadog:
datadog:
clusterName: *clusterName
external-secrets:
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::111111111111:role/EKS-ESO-my-cluster-dev"
keda:
serviceAccount:
operator:
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::111111111111:role/keda-my-cluster-dev"# For private repos: create GitHub credentials first
./scripts/create-github-credentials.sh
# Deploy the root application
kubectl apply -f bootstrap/root-app.yamlkubectl get applications -n argocd
# Expected: cluster-addons-bootstrap, external-secrets-operator, clusters, <addon>-<cluster> apps- Helm chart defaults — From upstream addon charts
- Global values —
configuration/addons-global-values/<addon>.yaml - Cluster-specific values —
configuration/addons-clusters-values/<cluster>.yaml - EKS Auto Mode overrides —
nodepools-config-values/<addon>-nodepool-config.yaml(ifeksAutoMode: "true") - ApplicationSet parameters — Injected by helpers (Datadog tags, ESO IRSA role)
- Add to
configuration/addons-catalog.yaml:
applicationsets:
- appName: my-addon
repoURL: https://my-addon-charts.example.com
chart: my-addon
version: 1.0.0
# namespace: custom-namespace # Optional, defaults to appName-
Create global defaults at
configuration/addons-global-values/my-addon.yaml -
Enable on clusters via labels:
clusters:
- name: my-cluster
labels:
my-addon: enabled- Create AWS Secrets Manager secret
k8s-<cluster-name> - Add to
configuration/cluster-addons.yamlwith desired addon labels - Create
configuration/addons-clusters-values/<cluster-name>.yaml - Commit and push — ArgoCD handles the rest
clusters:
- name: my-cluster
labels:
datadog: enabled
datadog-version: "3.70.7" # Override default version for this cluster onlyThe core pattern uses a matrix generator combining:
- Cluster generator: Discovers clusters with matching labels
- Git Files generator: Reads per-cluster values YAML and extracts only the relevant addon section
generators:
- matrix:
generators:
- clusters:
selector:
matchLabels:
datadog: enabled
- git:
files:
- path: "configuration/addons-clusters-values/{{.name}}.yaml"Each addon gets only its own config section from the cluster file — no values pollution between addons.
Datadog and ESO use multi-source Applications:
- Source 1: Official Helm chart from upstream repo
- Source 2: Local chart for supporting resources (ExternalSecret, ClusterSecretStore)
- Source 3: Values repo reference for
$valuespaths
Cluster values files use YAML anchors to avoid duplication:
clusterGlobalValues:
env: &env dev
clusterName: &clusterName my-cluster
region: ®ion eu-west-1
datadog:
datadog:
clusterName: *clusterName # References "my-cluster"
keda:
serviceAccount:
operator:
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::111111111111:role/keda-*clusterName"For clusters running EKS Auto Mode:
- Set
eksAutoMode: "true"label on the cluster - A dedicated ApplicationSet deploys Karpenter NodePools at sync-wave -2
- Infrastructure nodes are created with
node-type: infrastructurelabel andinfrastructure=true:NoScheduletaint - Addon nodepool configs automatically add
nodeSelector+tolerations
When migrating addons from another ArgoCD instance, set inMigration: true on the addon in addons-catalog.yaml. This injects ignoreDifferences rules that prevent pod restarts during adoption.
See docs/ADDON-MIGRATION.md for the full step-by-step runbook.
Set bootstrap.cloudProvider: gcp in bootstrap-config.yaml to switch from AWS to GCP. This changes:
- Secret store: GCP Secret Manager instead of AWS Secrets Manager
- Auth: Workload Identity instead of IRSA
- Cluster auth:
argocd-k8s-auth gcpinstead ofargocd-k8s-auth aws
See docs/GKE-SETUP.md for the full setup guide, Terraform examples, and IAM configuration.
Istio components are deployed in order: istio-base (-1) -> istio-cni (0) -> istiod (1) -> istio-ingress (2). ESO is deployed at wave -2. Customize in addons-appset.yaml.
- Verify cluster labels match addon name:
<addon>: enabled - Check cluster secret exists:
kubectl get secrets -n argocd -l argocd.argoproj.io/secret-type=cluster - Verify Git Files generator path:
configuration/addons-clusters-values/<cluster-name>.yamlmust exist
- Check ESO pods:
kubectl get pods -n external-secrets - Verify ClusterSecretStore:
kubectl get clustersecretstore global-secret-store - Check IAM role and IRSA configuration
- Verify AWS secret exists:
aws secretsmanager get-secret-value --secret-id k8s-<cluster-name>
- Check ArgoCD Application status in UI
- For Datadog: verify
ignoreDifferenceshandles operator-generated fields - For Kyverno: ensure
ServerSideApply=truesync option is set - For missing values:
ignoreMissingValueFiles: trueprevents failures
Breaking Changes from V1:
- Directory structure completely changed (
app-of-apps/->bootstrap/,values/->configuration/) - ArgoCD Vault Plugin (AVP) is no longer used — replaced by ESO
- Per-cluster config is now a single file instead of per-addon files
- Root application format changed to multi-source
New Features:
- Data-driven ApplicationSet — Generic template iterates over
addons-catalog.yaml, no hardcoded addon logic - Git Files generator — Automatic per-addon value extraction from single cluster YAML
- Multi-source Applications — Datadog and ESO deploy supporting resources alongside main chart
- EKS Auto Mode support — Karpenter NodePools with infrastructure node separation
- Helper functions —
_helpers.tplextracts addon-specific logic (Datadog params, ESO IRSA, ignoreDifferences) - Sync wave ordering — Ensures correct deployment order (ESO -> Clusters -> NodePools -> Addons)
- 16+ addons — Istio full stack, Kyverno, Argo suite, Cert Manager, External DNS, and more
- Per-cluster version overrides — Via
<addon>-versioncluster labels - Migration mode —
inMigrationflag for zero-downtime addon adoption between ArgoCD instances - YAML anchors — DRY cluster configuration with shared values across addons
- Bootstrap script — Automated GitHub credential setup from AWS Secrets Manager
Migration from V1: If upgrading from V1, you'll need to:
- Back up your current
values/clusters.yamland addon configurations - Recreate cluster definitions in the new
configuration/cluster-addons.yamlformat - Consolidate per-addon value files into single per-cluster files under
configuration/addons-clusters-values/ - Update your root Application to point to
bootstrap/instead ofapp-of-apps/ - Remove AVP plugin configuration from your ArgoCD repo-server
This project is open source. See LICENSE for details.
Contributions are welcome! Please open an issue or submit a pull request.