k8wiz
All graphs

Argo CD graph

Argo CD: ApplicationSet, AppProject and the Applications it generates

How Argo CD turns one ApplicationSet into an Application per app directory, which AppProject limits what they may deploy, and where the repository and cluster credentials live.

An Argo CD Application says: take this path in this repo and keep it in sync with this namespace on this cluster. Writing one per app gets old fast, so an ApplicationSet generates them. Its Git directory generator creates one Application for every folder under apps/.

Two kinds of Secret do the plumbing. A Secret labeled argocd.argoproj.io/secret-type: repository holds Git credentials, and one labeled secret-type: cluster registers a cluster. The AppProject draws the security boundary: which repos, clusters, namespaces and kinds its Applications may use.

Interactive graph

Click a resource to see what it does, or follow the setup step by step.

Outside the clusterYou write itCreated by a controllerComes with an add-ontrafficreferencescreates

How it works

All 10 resources and what they do

Git repository github.com/acme/team-a-apps
Holds one folder per app under apps/, each with plain YAML, a Kustomize overlay or a Helm chart. Adding a folder is all it takes to deploy a new app.
Secret repo-team-a-appsv1
The label argocd.argoproj.io/secret-type: repository makes Argo CD treat it as repository credentials. The repo server and the ApplicationSet controller use it for every Application pointing at this URL.
Secret cluster-prod-euv1
Registers the prod-eu cluster with its API server URL and credentials. argocd cluster add writes one of these for you. The Applications refer to the cluster by name.
AppProject team-aargoproj.io/v1alpha1
Limits what team A's Applications can do: only this repo, only namespaces starting with team-a- on prod-eu, and no cluster-scoped objects. An Application asking for more fails with a permission error instead of syncing.
ApplicationSet team-aargoproj.io/v1alpha1
Lists the folders under apps/ and renders the template once per folder, with {{ .path.basename }} as the app name. Folders added or removed in Git turn into Applications created or deleted.
Application team-a-billingargoproj.io/v1alpha1
Generated from apps/billing. The ApplicationSet owns it, so a manual edit is overwritten on the next generator run. Its status shows Synced and Healthy once the cluster matches Git.
Application team-a-searchargoproj.io/v1alpha1
Generated from apps/search, the same way. Each Application syncs on its own, so a broken search manifest doesn't block billing.
StatefulSet argocd-application-controllerapps/v1
Compares each Application's rendered manifests, which it gets from the repo server, with the live objects on the target cluster, then applies the difference. Large installs shard clusters across its replicas.
Deployment billingapps/v1
Applied to prod-eu by the application controller. Argo CD tracks it with the argocd.argoproj.io/tracking-id annotation, the default tracking method since Argo CD 3.0.
Deployment searchapps/v1
Applied to prod-eu from apps/search. With selfHeal: true, a kubectl edit here is reverted within a few seconds.

Common mistakes

  • The ApplicationSet owns the Applications it generates. Edits to them, including changes from the Argo CD UI, are reverted on the next run. Change the template instead.
  • An Application whose destination the AppProject doesn't allow gets an InvalidSpecError condition, and a kind it doesn't allow fails the sync with "not permitted in project". Compare argocd proj get team-a with what the Application asks for.
  • With goTemplate: true, missingkey=error turns a typo like {{ .path.basname }} into an error. Without it, the typo renders as <no value> and creates an Application with a broken name.
  • Deleting the ApplicationSet deletes every Application it generated, and with them their resources. Remove the generator's targets one at a time, or orphan them first with kubectl delete applicationset team-a -n argocd --cascade=orphan.
  • The default AppProject allows everything. Give each team its own project before you give them the ability to create Applications.

The manifests you write

Everything above that is marked "You write it", in one file. The rest comes from add-ons and controllers.

apiVersion: v1
kind: Secret
metadata:
  name: repo-team-a-apps
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repository
stringData:
  type: git
  url: https://github.com/acme/team-a-apps.git
  username: argocd
  password: <read-only token>
---
apiVersion: v1
kind: Secret
metadata:
  name: cluster-prod-eu
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: cluster
    env: prod
stringData:
  name: prod-eu
  server: https://prod-eu.k8s.example.com
  config: |
    {
      "bearerToken": "<service account token>",
      "tlsClientConfig": { "caData": "<base64 CA>" }
    }
---
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: team-a
  namespace: argocd
spec:
  sourceRepos:
    - https://github.com/acme/team-a-apps.git
  destinations:
    - name: prod-eu
      namespace: team-a-*
  clusterResourceWhitelist: []
  namespaceResourceBlacklist:
    - group: ""
      kind: ResourceQuota
---
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: team-a
  namespace: argocd
spec:
  goTemplate: true
  goTemplateOptions: ["missingkey=error"]
  generators:
    - git:
        repoURL: https://github.com/acme/team-a-apps.git
        revision: main
        directories:
          - path: apps/*
  template:
    metadata:
      name: "team-a-{{ .path.basename }}"
    spec:
      project: team-a
      source:
        repoURL: https://github.com/acme/team-a-apps.git
        targetRevision: main
        path: "{{ .path.path }}"
      destination:
        name: prod-eu
        namespace: "team-a-{{ .path.basename }}"
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true

Keep going

More graphs