Right, let’s talk about getting your entire fleet of applications under ArgoCD’s control without losing your mind clicking buttons in the UI. If you’re manually creating an Application resource for every single microservice, you’re not doing GitOps; you’re just moving the data entry from kubectl apply to a YAML file. The real power move is the Application of Applications pattern. It’s exactly what it sounds like: an Application resource that points not to a Helm chart or Kustomize overlay of your app, but to a repository full of other Application resources. It’s declarative inception, and it’s how you bootstrap an entire environment with a single definition.

Think of it as the root node of your deployment tree. You tell ArgoCD about this one app, and it goes, “Ah, I see,” and then proceeds to discover and manage everything else you’ve defined. This is how you achieve true environment-as-code. Your entire stack’s desired state is versioned in git, and a single change to your App of Apps can roll out a new service across ten clusters.

Defining the Root Application

Here’s what your root Application, the grand poobah, looks like. You’ll typically kubectl apply this one yourself to get the ball rolling. Notice it uses a directory with recurse: true. This is the magic sauce—it tells Argo, “Go into this folder in my repo and please manage every YAML file in there that looks like an Application.”

# 1-app-of-apps.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: production-apps
  namespace: argocd # This lives in the ArgoCD namespace, not your app's
spec:
  project: default
  source:
    repoURL: 'https://github.com/your-org/your-gitops-repo.git'
    targetRevision: HEAD
    path: production # The path to the directory full of child Applications
    directory:
      recurse: true # This is the key!
  destination:
    server: 'https://kubernetes.default.svc' # Deploys to the same cluster
    namespace: argocd # Child apps will be created in the argocd namespace
  syncPolicy:
    automated:
      selfHeal: true # If you're not using this, you're babysitting
      prune: true # Clean up if something gets deleted from the repo

Now, in the ./production directory of that repo, you’d have a bunch of other Application definitions. ArgoCD will find these and manage them.

# production/redis-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: redis-production
spec:
  project: default
  source:
    repoURL: 'https://github.com/your-org/helm-charts.git'
    targetRevision: main
    chart: redis
    helm:
      values: |-
        architecture: standalone
        master:
          persistence:
            size: 10Gi
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: data-services

Why This Beats a Helm Chart Full of Apps

You might be thinking, “Couldn’t I just use a Helm chart that templates out a bunch of Application resources?” Sure, you could stick a fork in a toaster, too. It works, but it’s a bad idea.

The App of Apps pattern using a directory is superior because it’s transparent and composable. You can see every individual application definition in plain YAML. Adding a new service is as simple as adding a new file to a directory—no Helm templating magic, no wondering if your indentation is right inside a range loop. This approach plays nicely with code review and git workflows. A PR to add a new app is just a PR adding one new file. It’s simple, and in platform engineering, simple is robust.

The Crucial Caveat: Sync Waves and Dependencies

Here’s where everyone trips up. You create this beautiful App of Apps, hit sync, and watch everything deploy in a random order. Your app tries to start before the database or Redis is ready, and it all explodes. Congratulations, you’ve discovered the problem of orchestration.

ArgoCD has a brilliant solution for this: sync waves and resource hooks. You explicitly tell Argo what order to do things in. You add two annotations to your Application resources:

# production/my-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-web-app
  annotations:
    argocd.argoproj.io/sync-wave: "2" # Wait for wave 0 and 1 to finish first
    argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
spec:
  source:
    repoURL: 'https://git.com/myapp.git'
    targetRevision: main
    path: kustomize/production
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: web-apps

Wave -5 is for things like namespaces and CRDs. Wave 0 is for databases, Redis, and other foundational services. Wave 1 might be for service accounts and config maps. Wave 2 is for your actual application deployments. This way, ArgoCD will sync your database App (wave 0) and wait for it to be Healthy before it even starts syncing your web app App (wave 2). It’s declarative dependency management, and it’s non-negotiable for any serious setup.

Best Practices and Pitfalls

First, namespace your Applications carefully. The root App of Apps and your child Applications will likely all be created in the argocd namespace. That’s fine. The destination for the resources they deploy is what actually matters. Your redis-production App manages resources in the data-services namespace. Keep your management plane (the Application resources) separate from your workload plane.

Second, use projects. That project: default line is doing a lot of heavy lifting. For the love of all that is holy, create projects to enforce source repos, destinations, and cluster resources. Don’t just throw everything in default; that’s how a developer accidentally deletes the production database from a fork.

Finally, embrace the diff. The App of Apps pattern makes ArgoCD the undeniable source of truth. If someone goes behind its back and does a kubectl edit, ArgoCD will notice and revert the change. This is a feature, not a bug. It forces all changes through git, which means they’re reviewed, audited, and tested. You’re not just deploying software; you’re enforcing a culture of discipline. And that, frankly, is the whole point.