Scaling Multi-Environment EKS GitOps with ArgoCD ApplicationSets and Kustomize
How to eliminate YAML duplication and manage multi-cluster Kubernetes deployments across dev, staging, and production using ArgoCD ApplicationSets and Kustomize components.
Managing Kubernetes manifests across multiple environments (dev, staging, prod) often starts with simple Helm values or individual Kustomize overlays. However, as microservices multiply and infrastructure scales across multiple Amazon EKS clusters and regions, maintaining individual ArgoCD Application CRDs quickly becomes an operational nightmare.
Teams inevitably encounter configuration drift, copy-pasted boilerplate, and forgotten environment updates.
The combination of ArgoCD ApplicationSets and Kustomize Components provides a DRY (Donβt Repeat Yourself), scalable pattern for managing hundreds of applications across multiple clusters from a single declarative Git repository.
GitOps Architecture Overview
In this pattern, a single root ApplicationSet controller dynamically discovers application definitions and environments from Git directories, automatically instantiating and synchronizing target ArgoCD applications across development and production EKS clusters.
```mermaid flowchart TD subgraph GitRepo[βπ¦ GitOps Repository (Single Source of Truth)β] AppsDir[βapps/\nβββ web-api/\nβββ worker/β] EnvDir[βenvironments/\nβββ dev/\nβββ staging/\nβββ prod/β] AppSetManifest[βApplicationSet Controller Manifest\n(git generator / directory matrix)β] end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
subgraph ArgoCDControlPlane["β‘ ArgoCD Management Control Plane"]
AppSetEngine["ApplicationSet Controller"]
ArgoApp1["ArgoCD App: web-api-dev"]
ArgoApp2["ArgoCD App: web-api-prod"]
end
subgraph TargetEKS["βΈοΈ Target AWS EKS Clusters"]
subgraph DevCluster["π§ͺ EKS Dev Cluster (us-east-1)"]
DevPods["Pods (1 replica, spot instances)\nIngress: dev.api.internal"]
end
subgraph ProdCluster["π EKS Prod Cluster (us-east-1 multi-AZ)"]
ProdPods["Pods (5 replicas, HPA, PDB)\nIngress: api.company.com"]
end
end
GitRepo -->|Webhook / Polling| AppSetEngine
AppSetEngine -->|Generates CRDs| ArgoApp1
AppSetEngine -->|Generates CRDs| ArgoApp2
ArgoApp1 -->|Syncs Overlays| DevCluster
ArgoApp2 -->|Syncs Overlays| ProdCluster ```text
Repository Structure: Bases, Overlays, and Components
To prevent duplication while allowing environment-specific customizations (like replica counts, ingress hostnames, and resource limits), structure your repository using Kustomize bases and reusable components:
text gitops-fleet/ βββ apps/ β βββ web-service/ β βββ base/ β β βββ deployment.yaml β β βββ service.yaml β β βββ kustomization.yaml β βββ components/ β β βββ hpa/ β β β βββ horizontal-pod-autoscaler.yaml β β β βββ kustomization.yaml β β βββ ingress-tls/ β β βββ ingress.yaml β β βββ kustomization.yaml β βββ overlays/ β βββ dev/ β β βββ patches/ β β β βββ resources.yaml β β βββ kustomization.yaml β βββ staging/ β β βββ patches/ β β β βββ resources.yaml β β βββ kustomization.yaml β βββ prod/ β βββ patches/ β β βββ resources.yaml β βββ kustomization.yaml βββ bootstrap/ βββ applicationset.yaml text
The Base Manifest
The base defines the minimal, shared definition of the service:
```yaml
apps/web-service/base/deployment.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: web-service spec: replicas: 1 selector: matchLabels: app: web-service template: metadata: labels: app: web-service spec: containers: - name: web-service image: ghcr.io/org/web-service:v1.0.0 ports: - containerPort: 8080 readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 10 ```text
Reusable Kustomize Components
Instead of duplicating HPA configurations or Ingress rules across staging and production, create modular Kustomize Components:
```yaml
apps/web-service/components/hpa/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1 kind: Component resources:
- horizontal-pod-autoscaler.yaml ```text
```yaml
apps/web-service/components/hpa/horizontal-pod-autoscaler.yaml
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: web-service-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: web-service minReplicas: 3 maxReplicas: 20 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 75 ```text
Environment Overlay with Components
The production overlay pulls the base, enables the HPA component, and patches replica counts:
```yaml
apps/web-service/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization
resources:
- ../../base
components:
- ../../components/hpa
- ../../components/ingress-tls
patches:
- path: patches/resources.yaml target: kind: Deployment name: web-service ```text
Declarative ApplicationSets: Git Directory Matrix Generator
Instead of manually declaring an ArgoCD Application YAML for every service in every environment, use an ApplicationSet with a Git Directory Generator.
```yaml
bootstrap/applicationset.yaml
apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata: name: fleet-applications namespace: argocd spec: generators: - matrix: generators: # Generator 1: Discover all microservices in apps/ - git: repoURL: https://github.com/ShahzebMahmood/gitops-fleet.git revision: HEAD directories: - path: apps/* # Generator 2: Discover target environment overlays - git: repoURL: https://github.com/ShahzebMahmood/gitops-fleet.git revision: HEAD directories: - path: apps//overlays/
template: metadata: name: β{{path[1]}}-{{path.basename}}β labels: environment: β{{path.basename}}β app: β{{path[1]}}β spec: project: default source: repoURL: https://github.com/ShahzebMahmood/gitops-fleet.git targetRevision: HEAD path: β{{path}}β destination: # Maps environment name to target cluster secret registered in ArgoCD name: βeks-cluster-{{path.basename}}β namespace: β{{path[1]}}β syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true - ApplyOutOfSyncOnly=true retry: limit: 5 backoff: duration: 5s factor: 2 maxDuration: 3m ```text
Secrets Management with External Secrets Operator (ESO)
Storing raw secrets in Git repositories is unacceptable. We integrate External Secrets Operator (ESO) with AWS Secrets Manager, allowing Kustomize to deploy declarative ExternalSecret manifests that dynamically fetch credentials at runtime.
```yaml
apps/web-service/base/external-secret.yaml
apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: web-service-db-credentials spec: refreshInterval: 1h secretStoreRef: name: aws-secretsmanager kind: ClusterSecretStore target: name: db-credentials creationPolicy: Owner data: - secretKey: DB_PASSWORD remoteRef: key: production/web-service/database property: password ```text
This guarantees:
- Zero plaintext tokens or keys exist anywhere in the GitOps repository.
- IAM roles for Service Accounts (IRSA) / EKS Pod Identity authenticate ESO directly to AWS Secrets Manager without static IAM keys.
Preventing Deployment Stampedes with Sync Waves
When deploying complex multi-tier applications (e.g., database schema migrations followed by backend APIs, then frontend ingress), race conditions can cause pods to crash-loop.
Use ArgoCD Sync Waves annotations to order execution:
yaml metadata: annotations: argocd.argoproj.io/sync-wave: "1" # Runs first (e.g. CRDs, DB Migration Job) --- metadata: annotations: argocd.argoproj.io/sync-wave: "2" # Runs after wave 1 succeeds (Deployments) --- metadata: annotations: argocd.argoproj.io/sync-wave: "3" # Runs last (Ingress, DNS) text
Best Practices and Lessons Learned
- Enable
ApplyOutOfSyncOnly=true: In large multi-cluster environments, standard sync attempts re-apply every manifest in the tree. EnablingApplyOutOfSyncOnlyreduces Kubernetes API server load by over 80%. - Use Directory Matrix Generators: Matrix generators eliminate cross-product combinatorial explosions while keeping directory hierarchies intuitive.
- Automate PR Pre-Validation: Run
kustomize buildandkubeconforminside CI checks before merging into the main GitOps branch to catch syntax and schema errors before ArgoCD processes them.
Key Takeaways
- Single Control Plane: Manage hundreds of microservice instances across multiple EKS clusters declaratively.
- DRY Configurations: Kustomize Components isolate shared capabilities (HPA, Ingress, Monitoring) without duplicating YAML files.
- Zero Drift: Continuous self-healing ensures cluster state always matches the audited Git commit history.