# 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.

Interactive graph: https://k8wiz.com/graphs/argo-cd-applicationset-appproject

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.

## Resources

### Git repository github.com/acme/team-a-apps

outside the cluster

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-apps

you write it, v1, namespace argocd

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-eu

you write it, v1, namespace argocd

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-a

you write it, argoproj.io/v1alpha1, namespace argocd

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-a

you write it, argoproj.io/v1alpha1, namespace argocd

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-billing

created by a controller, argoproj.io/v1alpha1, namespace argocd

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-search

created by a controller, argoproj.io/v1alpha1, namespace argocd

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-controller

comes with an add-on, apps/v1, namespace argocd

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 billing

created by a controller, apps/v1, namespace team-a-billing

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 search

created by a controller, apps/v1, namespace team-a-search

Applied to `prod-eu` from `apps/search`. With `selfHeal: true`, a `kubectl edit` here is reverted within a few seconds.

## How the resources connect

- ApplicationSet team-a → Git repository github.com/acme/team-a-apps: lists apps/* (traffic)
- ApplicationSet team-a → Secret repo-team-a-apps: uses credentials (references)
- ApplicationSet team-a → Application team-a-billing: generates (creates)
- ApplicationSet team-a → Application team-a-search: generates (creates)
- Application team-a-billing → AppProject team-a: project (references)
- Application team-a-search → AppProject team-a: project (references)
- AppProject team-a → Secret cluster-prod-eu: allowed destination (references)
- Application team-a-billing → Secret cluster-prod-eu: destination.name (references)
- Application team-a-search → Secret cluster-prod-eu: destination.name (references)
- StatefulSet argocd-application-controller → Application team-a-billing: reconciles (references)
- StatefulSet argocd-application-controller → Application team-a-search: reconciles (references)
- StatefulSet argocd-application-controller → Git repository github.com/acme/team-a-apps: renders via repo server (traffic)
- StatefulSet argocd-application-controller → Deployment billing: applies (creates)
- StatefulSet argocd-application-controller → Deployment search: applies (creates)

## How it works

1. **Argo CD gets credentials.** The repository Secret and the cluster Secret are ordinary Secrets in `argocd`. Their `argocd.argoproj.io/secret-type` label is what makes Argo CD pick them up, with no restart needed.
2. **The AppProject sets the boundary.** `team-a` allows one repo, the `prod-eu` cluster, namespaces matching `team-a-*` and no cluster-scoped kinds. Every Application in the project is checked against these rules before it syncs.
3. **The ApplicationSet generates Applications.** The Git directory generator lists `apps/*` on `main` and finds `billing` and `search`. For each, it renders the template and creates or updates an Application named `team-a-billing` or `team-a-search`.
4. **The application controller syncs each one.** For each Application, the controller asks the repo server to render the manifests at `main`, compares them with the live objects on `prod-eu` and, with automated sync, applies the difference. `CreateNamespace=true` creates the target namespace first.
5. **Adding and removing apps.** Commit a new folder `apps/payments` and the next generator run creates `team-a-payments`. Delete a folder and the ApplicationSet deletes that Application, which by default also deletes what it deployed. Set `preserveResourcesOnDeletion: true` in the ApplicationSet's `syncPolicy` to keep the resources.

## 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

```yaml
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
```
