You've probably heard the phrase "King of Kings" in movies and series. A ruler so powerful, other rulers bow to them.
Well, in the world of ArgoCD — meet the App of Apps. An application so powerful, it manages other applications.
The Problem: kubectl apply is Not GitOps
If you're running ArgoCD, you're already doing GitOps for your actual workloads — your Helm charts, your Deployments, your Services. ArgoCD watches your Git repo and syncs everything automatically.
But here's the irony: how do you deploy the ArgoCD Application CRDs themselves?
kubectl apply -f app1-qc.yaml
kubectl apply -f app2-qc.yaml
kubectl apply -f another-app-uat.yaml
# ... repeat for every app, on every cluster, forever
Or manually click "New App" in the Argo UI, fill in the form, hit save. Same problem, different interface.
Every time a new service is onboarded, someone has to switch context to the cluster where ArgoCD is running and manually apply the manifest. That's not GitOps. That's just... work.
Now imagine you have hundreds of apps across prod and nonprod clusters. Here's what that actually looks like in practice:
Your manager walks in on a Monday and says "we're migrating from EKS to GKE." You have 200 Application manifests spread across 7 clusters. Someone has to kubectl get applications -n argocd, export each one, re-apply them on the new GKE cluster, verify nothing was missed(hopefully), and somehow do this without an outage. That's days of work — and one typo away from a production incident.
Or your security team decides prod should require manual sync approvals — no more autosync. Someone is now editing Application CRDs in the cluster one by one at 11pm, hoping they got all 80 of them.
Or nonprod environments are getting polluted with orphaned resources because prune: true was never set consistently. Half your apps have it, half don't. You don't even know which ones anymore.
Or a new xuat environment spins up and every one of your 40 services needs a new Application manifest pointing at it — by end of sprint.
You're looking at hundreds of manual changes, on live clusters, without a meaningful audit trail, and no easy way to roll back if something goes wrong.
That's the problem. The App of Apps is the solution.
What is App of Apps?
The App of Apps pattern is elegantly simple. An ArgoCD Application whose source points to a folder — and that folder contains other ArgoCD Application YAMLs.
app-of-apps/
├── argo-apps-prod/
│ ├── app1/
│ │ └── prod.yaml
│ └── app2/
│ └── prod.yaml
└── argo-apps-nonprod/
├── app1/
│ ├── qc.yaml
│ └── uat.yaml
├── app2/
│ ├── qc.yaml
│ └── uat.yaml
└── new-app/
└── qc.yaml
The parent Application watches this folder. When you drop a YAML in — ArgoCD creates the app. When you edit it — ArgoCD updates it. When you delete it — ArgoCD removes it.
Git is now the source of truth for your Application CRDs too. Not just your workloads.
How It Works
Step 1: Create the folder structure
In your git repo, create two folders — one per cluster:
app-of-apps/
├── argo-apps-prod/ ← apps for prod cluster
└── argo-apps-nonprod/ ← apps for nonprod cluster
Step 2: Drop your Application YAMLs in
Each file is just a regular ArgoCD Application manifest:
# argo-apps-nonprod/app1/qc.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: app1-qc
namespace: argocd
spec:
project: my-project
source:
repoURL: https://github.com/your-org/app1-cd.git
path: helm
targetRevision: HEAD
helm:
valueFiles:
- values.qc.yaml
destination:
server: https://your-cluster-api-url.eks.amazonaws.com
namespace: app1-qc
syncPolicy:
automated:
prune: true
selfHeal: true
Step 3: Create the parent Application (one-time bootstrap)
Most setups have a single ArgoCD instance managing all apps — all Applications live in the argocd namespace on that one cluster. In that case, you apply the parent just once, and ArgoCD takes it from there.
If you have separate ArgoCD instances (e.g., one for prod, one for nonprod), you'd apply a parent on each. But for most teams — one apply, done.
# parent.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: app-of-apps
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/your-cd-repo.git
path: app-of-apps/argo-apps-nonprod
targetRevision: HEAD
directory:
recurse: true # picks up files in subfolders
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
selfHeal: true
prune: true
Apply it:
kubectl apply -f parent.yaml
That's it. From this point on, you never run kubectl apply again for app onboarding.
Real World Scenarios
Onboarding a new service
A new service called new-app is ready to go live in QC. You create:
argo-apps-nonprod/new-app/qc.yaml
Push to git. Within minutes, new-app-qc appears in the Argo UI — synced, healthy, managed. No one touched the cluster. No Slack message to the platform team saying "can you apply this manifest?" Just a git push.
Cluster migration
Your team is migrating from an old EKS cluster to a new one. Old approach: kubectl get applications -n argocd, export each one, re-apply them manually on the new cluster, hope you didn't miss one.
New approach: update destination.server in your YAML files, open a PR, merge. ArgoCD points all your apps at the new cluster. And you have a git history proving exactly when and who made the change.
Changing the target branch across all apps
Your team decides to pin all nonprod apps to a develop branch instead of tracking HEAD. Previously you'd have to edit every Application manifest in the cluster one by one.
Now a single find-and-replace across the folder, one commit, one push. Every app updated.
Adding sync notifications to all prod apps
Your team wants email alerts whenever a prod app syncs. Add the annotation to your prod YAML files:
metadata:
annotations:
notifications.argoproj.io/subscribe.on-sync-failed.email: platform@your-org.com
notifications.argoproj.io/subscribe.on-sync-succeeded.email: platform@your-org.com
Push. Every prod app gets the notification config on next sync. No kubectl edit, no manual patching.
Rolling out a new environment
Your team adds a new environment — auat. Every service needs a new Application pointing to values.auat.yaml. Copy, update the name and values file, drop in the folder:
argo-apps-nonprod/app1/auat.yaml
argo-apps-nonprod/app2/auat.yaml
argo-apps-nonprod/new-app/auat.yaml
Push once. All AUAT apps appear in Argo simultaneously.
Decommissioning an app
app2-qc is being retired. Delete argo-apps-nonprod/app2/qc.yaml from git and push. ArgoCD removes the Application CRD — and with prune: true on the child app, it tears down the Deployments, Services, and Ingresses in the cluster too.
No orphaned resources. No forgotten namespaces. No "wait, is that app still running?" six months later.
What Does It Look Like in the UI?
The parent app-of-apps shows up in the Argo UI like any other app. Click into it and instead of Pods and Deployments, you see your child Applications as its resources:
app-of-apps
├── app1-qc
├── app1-uat
├── app2-qc
├── new-app-qc
└── another-app-prod
Each child app is fully clickable — drill into it, see its own Deployments, Services, Pods, health status, and sync history. Everything works exactly the same as before, just with an extra layer of management on top.
What Happens if the Parent Goes Down?
If ArgoCD restarts — nothing happens. The child apps are independent Kubernetes resources that keep running on their own.
If the parent is deleted — it depends on how you delete it. Some methods cascade (deleting all child apps along with the parent), and some don't. Be very careful here. Understand whether your deletion method cascades before you run it in production. When in doubt, use --cascade=false to delete only the parent and leave the children untouched.
Multi-Cluster From a Single Parent
Here's where it gets really powerful. The parent app lives on one cluster, but each child app can point to a completely different cluster via its destination.server:
# argo-apps-nonprod/app1/qc.yaml → deploys to QC cluster
destination:
server: https://qc-cluster-api-url.eks.amazonaws.com
# argo-apps-nonprod/app1/uat.yaml → deploys to UAT cluster
destination:
server: https://uat-cluster-api-url.eks.amazonaws.com
# argo-apps-prod/app1/prod.yaml → deploys to prod cluster
destination:
server: https://prod-cluster-api-url.eks.amazonaws.com
One parent. One git repo. Apps deployed across as many clusters as you need. The parent doesn't care where the child deploys — it just makes sure the Application CRD exists and is configured correctly.
The Audit Trail You Never Had
Before App of Apps, if someone asked "when did app2 get added to the QC cluster?" — good luck. It was a kubectl apply someone ran from their laptop six months ago. No record. No context. No idea.
With App of Apps, every change is a git commit:
- Who added a new app → git author
- When a branch was changed → commit timestamp
- Why an app was removed → commit message
- What the configuration looked like at any point in time → git history
For teams in regulated industries or with compliance requirements, this is significant. Your entire application lifecycle is now fully auditable — without any extra tooling.
What About Existing Apps?
If you already have hundreds of apps running in your cluster that were manually applied, don't worry. The parent only manages what it applies from git. ArgoCD uses tracking labels to know what it owns — your existing manually-applied apps have no such label, so the parent doesn't know they exist and won't touch them.
Migrate gradually: add apps to the folder one by one, and the parent adopts them. The rest stay untouched until you're ready.
Conclusion
The App of Apps pattern closes the last GitOps gap — the Application CRDs themselves. Once the parent is bootstrapped (one kubectl apply, one time), your entire application lifecycle is managed through git.
Push to onboard. Edit to update. Delete to decommission.
No manual cluster access. No error-prone kubectl commands. No "who applied that?" questions in a post-incident review.
Just Git, ArgoCD, and peace of mind.
Find more of my posts at rajduwal.com.np/blog. If you have any queries or just want to discuss how you're using it, drop me a mail at root@rajduwal.com.np.