Je begint met één ArgoCD Application. Dan vijf. Dan twintig. Voordat je het weet beheer je honderden Applications, en de handmatige overhead doodt je productiviteit.

Het App-of-Apps pattern lost dit op: één root application die alle andere applications beheert.

Dit is hoe ik elke GitOps repository structureer, en het schaalt van homelab tot enterprise.

Het Probleem: Application Sprawl

Wanneer je ArgoCD voor het eerst adopteert, maak je Applications handmatig aan:

kubectl apply -f apps/frontend.yaml
kubectl apply -f apps/backend.yaml
kubectl apply -f apps/database.yaml
# ... herhaal voor elke service

Dit werkt voor kleine deployments. Maar het creëert problemen:

  1. Handmatige creatie: Elke nieuwe service heeft handmatige Application creatie nodig
  2. Geen hiërarchie: Alle applicaties zijn peers, geen logische groepering
  3. Moeilijke onboarding: Nieuwe teamleden weten niet wat er bestaat
  4. Bootstrap probleem: Hoe recreëer je alle Applications na cluster verlies?

De ironie: je gebruikt GitOps voor applicaties maar niet voor de Applications zelf.

De Oplossing: App-of-Apps

App-of-Apps is simpel: Applications die Applications aanmaken.

flowchart TD
    ROOT["Root Application<br/>(beheert alles)"]

    ROOT --> INFRA["Infrastructure App"]
    ROOT --> PLATFORM["Platform App"]
    ROOT --> APPS["Apps App"]

    INFRA --> NS["namespaces"]
    INFRA --> RBAC["rbac"]
    INFRA --> NP["netpols"]

    PLATFORM --> MON["monitoring"]
    PLATFORM --> LOG["logging"]
    PLATFORM --> ING["ingress"]

    APPS --> FE["frontend"]
    APPS --> BE["backend"]
    APPS --> WK["workers"]

De root Application synct een directory met Application manifests. Die Applications syncen de daadwerkelijke workloads.

De Repository Structuur

Dit is hoe ik GitOps repositories organiseer:

gitops-repo/
├── clusters/
│   ├── production/
│   │   ├── root.yaml              # Root application
│   │   ├── infrastructure/
│   │   │   ├── app.yaml           # Infrastructure app-of-apps
│   │   │   ├── namespaces.yaml
│   │   │   ├── rbac.yaml
│   │   │   └── network-policies.yaml
│   │   ├── platform/
│   │   │   ├── app.yaml           # Platform app-of-apps
│   │   │   ├── monitoring.yaml
│   │   │   ├── logging.yaml
│   │   │   └── ingress.yaml
│   │   └── apps/
│   │       ├── app.yaml           # Workloads app-of-apps
│   │       ├── frontend.yaml
│   │       ├── backend.yaml
│   │       └── workers.yaml
│   └── staging/
│       └── ... (vergelijkbare structuur)
├── base/
│   ├── monitoring/
│   │   ├── kustomization.yaml
│   │   └── ...
│   └── ...
└── apps/
    ├── frontend/
    │   ├── base/
    │   └── overlays/
    │       ├── staging/
    │       └── production/
    └── backend/
        └── ...

De Hiërarchie Bouwen

Level 1: De Root Application

De root is de enige Application die je handmatig aanmaakt:

# clusters/production/root.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://github.com/yourorg/gitops-repo.git
    targetRevision: main
    path: clusters/production
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Dit synct de clusters/production directory, die de volgende level apps bevat.

Level 2: Categorie Applications

Groepeer gerelateerde applicaties per categorie:

# clusters/production/infrastructure/app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: infrastructure
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/yourorg/gitops-repo.git
    targetRevision: main
    path: clusters/production/infrastructure
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Dit synct alle YAML bestanden in de infrastructure directory — inclusief de individuele Applications.

Level 3: Leaf Applications

De daadwerkelijke workload applicaties:

# clusters/production/apps/frontend.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: frontend
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/yourorg/gitops-repo.git
    targetRevision: main
    path: apps/frontend/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: frontend
  syncPolicy:
    syncOptions:
      - CreateNamespace=true
    automated:
      prune: true
      selfHeal: true

Sync Waves: Volgorde Controleren

Niet alles kan tegelijk deployen. Databases voor apps. Namespaces voor pods.

ArgoCD sync waves controleren deployment volgorde:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: namespaces
  annotations:
    argocd.argoproj.io/sync-wave: "-10"  # Deploy eerst
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: monitoring
  annotations:
    argocd.argoproj.io/sync-wave: "-5"   # Na namespaces
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: frontend
  annotations:
    argocd.argoproj.io/sync-wave: "0"    # Default, na platform

Lagere nummers syncen eerst. Ik gebruik:

  • -10: Namespaces, RBAC
  • -5: Platform componenten (monitoring, logging, ingress)
  • 0: Applicaties
  • 10: Post-deployment jobs

Health Checks en Dependencies

ArgoCD wacht tot applicaties healthy zijn voordat het doorgaat:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: database
spec:
  # ... source en destination
  syncPolicy:
    automated:
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
  # Health bepaalt wanneer "ready"

De database Application is healthy wanneer zijn pods draaien. Downstream applicaties in latere sync waves wachten hierop.

Multi-Cluster met App-of-Apps

Voor meerdere clusters, breid de hiërarchie uit:

gitops-repo/
├── clusters/
│   ├── production-eu/
│   │   └── root.yaml
│   ├── production-us/
│   │   └── root.yaml
│   └── staging/
│       └── root.yaml

Elk cluster heeft zijn eigen root. Ze kunnen base configuraties delen via Kustomize overlays.

Een Nieuwe Application Toevoegen

De kracht van App-of-Apps: een nieuwe applicatie toevoegen is gewoon een bestand:

# 1. Maak het Application manifest
cat > clusters/production/apps/new-service.yaml <<EOF
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: new-service
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/yourorg/gitops-repo.git
    path: apps/new-service/overlays/production
    targetRevision: main
  destination:
    server: https://kubernetes.default.svc
    namespace: new-service
  syncPolicy:
    syncOptions:
      - CreateNamespace=true
    automated:
      prune: true
      selfHeal: true
EOF

# 2. Maak de applicatie manifests
mkdir -p apps/new-service/overlays/production
# ... voeg je Kustomization toe

# 3. Commit en push
git add .
git commit -m "Add new-service"
git push

ArgoCD detecteert het nieuwe Application manifest en deployt het automatisch. Geen kubectl. Geen UI klikken.

ApplicationSets: Het Volgende Level

Voor echt dynamische application generatie, overweeg ApplicationSets:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: apps
  namespace: argocd
spec:
  generators:
    - git:
        repoURL: https://github.com/yourorg/gitops-repo.git
        revision: main
        directories:
          - path: apps/*
  template:
    metadata:
      name: '{{path.basename}}'
    spec:
      project: default
      source:
        repoURL: https://github.com/yourorg/gitops-repo.git
        targetRevision: main
        path: '{{path}}/overlays/production'
      destination:
        server: https://kubernetes.default.svc
        namespace: '{{path.basename}}'
      syncPolicy:
        automated:
          prune: true
          selfHeal: true

Dit genereert een Application voor elke directory onder apps/. Voeg een directory toe, krijg een Application.

Veelvoorkomende Fouten

Fout 1: Te Diepe Nesting

Drie levels is meestal genoeg. Meer creëert verwarring:

✗ root → category → subcategory → team → service → component
✓ root → category → service

Fout 2: Finalizers Vergeten

Zonder finalizers laat het verwijderen van een app-of-apps orphan applications achter:

metadata:
  finalizers:
    - resources-finalizer.argocd.argoproj.io

Dit zorgt dat child applications verwijderd worden wanneer parent verwijderd wordt.

Fout 3: Circulaire Dependencies

Laat Application A niet afhangen van Application B die afhangt van Application A.

Fout 4: Alles in Één Sync Wave

Als alles tegelijk deployt, krijg je race conditions. Gebruik sync waves.

Mijn Standaard Structuur

Na jaren itereren, dit is mijn go-to structuur:

clusters/{env}/
├── root.yaml
├── infrastructure/           # Sync wave -10
│   ├── app.yaml
│   ├── namespaces.yaml
│   ├── rbac.yaml
│   └── network-policies.yaml
├── platform/                 # Sync wave -5
│   ├── app.yaml
│   ├── argocd.yaml          # ArgoCD beheert zichzelf
│   ├── monitoring.yaml
│   ├── logging.yaml
│   ├── ingress.yaml
│   └── cert-manager.yaml
├── data/                     # Sync wave -2
│   ├── app.yaml
│   └── databases.yaml
└── apps/                     # Sync wave 0
    ├── app.yaml
    └── {service}.yaml

Deze volgorde zorgt:

  1. Namespaces bestaan voordat iets deployt
  2. Platform tools klaar voordat apps ze nodig hebben
  3. Databases draaien voordat apps connecten

Het Bootstrap Probleem Opgelost

Herinner je GitOps Disaster Recovery? App-of-Apps maakt recovery triviaal:

# 1. Nieuw cluster
# 2. Installeer ArgoCD
# 3. Apply ÉÉN bestand
kubectl apply -f clusters/production/root.yaml
# 4. Alles recreëert

Eén commando bootstrapt je hele cluster. Dat is de kracht van App-of-Apps.


App-of-Apps transformeert GitOps van “bestanden die dingen deployen” naar “zelf-beschrijvende infrastructuur.” Je Git repository wordt de complete specificatie van wat zou moeten bestaan.