Skip to content

Argo CD Entrypoints

The review map for everything directly rendered by the root Application from infrastructure/controllers/argocd/apps/.

root.yaml is the manual seed and is not self-managed. Once it exists, Argo CD manages this entrypoint tree, including file moves inside apps/, ApplicationSet edits, and AppProject edits.

Layout

Folder Purpose
bootstrap/ Wave 0 foundation apps required before the rest of GitOps can converge
core-dependencies/ Storage, restore, and bootstrap services that later apps depend on
custom-entrypoints/ Intentional standalone apps kept out of AppSets for ordering or render behavior
appsets/ Broad directory discovery for infrastructure, databases, monitoring, and user apps

Entrypoint Table

Entrypoint Type Wave Reason Can move to AppSet?
projects.yaml AppProjects 0 Project grouping and homelab trust boundary No, foundational Argo CD config
bootstrap/argocd.yaml Application 0 Self-manages the Argo CD Helm chart and values No, self-management entrypoint
bootstrap/cilium-app.yaml Application 0 CNI and Gateway API foundation No, must be healthy before pods and routes
bootstrap/1passwordconnect.yaml Application 0 Secret backend for External Secrets No, secret dependency for later waves
bootstrap/external-secrets.yaml Application 0 ExternalSecret CRDs and controller No, CRDs are required by downstream apps
core-dependencies/cert-manager-app.yaml Application 1 Certificate controller required before webhook-cert consumers in later waves No, required before cert-dependent apps
core-dependencies/longhorn-app.yaml Application 1 Storage foundation before PVC consumers No, required before restore/app PVC flows
core-dependencies/snapshot-controller-app.yaml Application 1 VolumeSnapshot CRDs and controller No, required by backup/restore flows
core-dependencies/kopiur-operator-app.yaml Application 2 kopiur operator (Kopia-native backup): CRDs + controller + webhook + volume populator; no monitoring dependency No, required before managed app PVCs
core-dependencies/kopiur-config-app.yaml Application 3 kopiur repo config: ClusterRepository cluster-kopia + credential fanout + VolumeSnapshotClass longhorn-snapclass No, required before managed app PVCs
custom-entrypoints/keda-app.yaml Application 4 Standalone to isolate its render from the AppSet generator Maybe, if AppSet render stays stable
custom-entrypoints/vertical-pod-autoscaler-app.yaml Application 4 VPA controller (recommender/updater/admission) Maybe, if AppSet render stays stable
custom-entrypoints/vertical-pod-autoscaler-observability-app.yaml Application 6 Optional VPA PodMonitor and alerts after monitoring CRDs exist No, keeps observability out of core
custom-entrypoints/temporal-worker-controller-app.yaml Application 4 Standalone to isolate its render from the AppSet generator Maybe, if AppSet render stays stable
custom-entrypoints/strimzi-app.yaml Application 4 Source lives at my-apps/development/strimzi (excluded from the my-apps AppSet). Kafka CRDs/operator must precede wave-6 Kafka resources; standalone destination kafka also avoids the AppSet basename→namespace assumption (strimzi would be wrong) No, both ordering and destination namespace are exceptional
custom-entrypoints/opentelemetry-operator-app.yaml Application 6 Optional telemetry alongside workloads; health cannot gate workload discovery Maybe; preserve its bootstrap independence
custom-entrypoints/keda-observability-app.yaml Application 6 Optional KEDA ServiceMonitor resources after monitoring CRDs exist No, keeps observability out of core
custom-entrypoints/vpa-system-policies-app.yaml Application 6 VPA policies for the small set of bootstrap/system workloads without a co-located owner No, ownership exception is explicit
custom-entrypoints/opentelemetry-operator-observability-app.yaml Application 6 Optional OpenTelemetry ServiceMonitor after monitoring CRDs exist No, keeps observability out of core
appsets/infrastructure-appset.yaml ApplicationSet 4 Explicit list of core infrastructure directories N/A
appsets/database-appset.yaml ApplicationSet 4 Discovers infrastructure/database/*/* (Redis + shared DB support); fully automated since the CNPG retirement (2026-08-13) N/A
appsets/monitoring-appset.yaml ApplicationSet 5 Discovers monitoring/*/* (metrics/, logs/, tracing/, ai-ops/) after core infra, excluding monitoring/*/_archive N/A
appsets/my-apps-appset.yaml ApplicationSet 6 Discovers my-apps/*/*, excluding Components, my-apps/*/_archive and my-apps/development/strimzi; all automated + self-healing N/A

Generated Application identities are domain-prefixed: infrastructure-<component>, database-<database>, monitoring-<component>, and my-apps-<app>. Namespaces still use the leaf directory name. The prefix prevents operationally dangerous UI ambiguity such as confusing the Temporal database Application with the Temporal server/schema Application.

ApplicationSet waves order creation of the generator objects, not the health of their generated Applications. Those children reconcile independently. See wave scope before treating an AppSet wave as a dependency barrier.

Bootstrap guardrail — observability is not a core dependency

A bootstrap-critical app must render successfully without monitoring.coreos.com CRDs. Argo's Helm chart conditionally renders its monitors only when the installed API capability exists. Other monitoring resources belong in optional observability overlays. Those CRDs do not exist until kube-prometheus-stack (Wave 5); an earlier-wave app shipping them fails dry-run and deadlocks the App-of-Apps wave gate. Put observability CRs in a separate optional app that syncs after Wave 5 (e.g. keda-observability at Wave 6, split out of KEDA's Wave-4 core). Do not install Prometheus Operator CRDs early — SkipDryRunOnMissingResource is an escape hatch / observability-app option only, never a core fix. cert-manager is at Wave 1 (not 4) so early cert-dependent apps can start (historically the Wave-3 cnpg-barman-plugin; the deadlock lesson stands even though that app is retired). Full detail: cluster DR nuke restore runbook.

Bootstrap guardrail — Application health does not prove webhook reachability

Waves gate on Argo CD Application health. For a webhook Deployment that means ready replicas == desired — and kubelet decides readiness by probing the pod locally, on its own node. Neither signal involves the API server. A webhook pod can therefore be Running, Ready, and its Application Healthy, while the API server cannot reach it across the pod network. Every later wave then proceeds into a webhook that cannot be called.

The blast radius is set by failurePolicy, not by the size of the component. A cluster-scoped failurePolicy: Fail webhook rejects every matching write in the cluster while unreachable, and a sync can fail after applying only part of its resources. Unrelated apps can stall waiting for Secrets or other objects rejected by admission.

Two rules follow:

  • Fail-closed cluster-scoped webhooks are pinned to the control plane (nodeSelector: node-role.kubernetes.io/control-plane plus the matching NoSchedule toleration). The API server calls them node-locally, so no pod-network fault on any other node can break admission. This applies to cert-manager. Availability is then coupled to the API server's own availability, which is the correct coupling: if the control plane is down, admission is moot.
  • Do not add a custom Lua health check to probe webhook reachability. Argo CD configuration stays simple; the reachability signal belongs in alerting (monitoring/metrics/prometheus-stack/admission-webhook-alerts.yaml, which reads the API server's own apiserver_admission_webhook_* metrics), not in the wave gate.

Notes

  • project-nomad is intentionally managed by appsets/my-apps-appset.yaml as a single bundled app at my-apps/knowledge/project-nomad. Its child folders are resources inside that app, not generated Argo CD Applications.
  • my-apps/common/* is excluded from the my-apps generator: those directories are shared Kustomize Components (kind: Component), which kustomize builds as an empty render — without the exclude the AppSet generates a phantom zero-resource Application. validate-argocd-apps.sh Check 8 fails CI if a Component dir ever becomes discoverable again.
  • my-apps/development/strimzi is excluded from the my-apps generator: the files live with the dev tools, but custom-entrypoints/strimzi-app.yaml deploys them at wave 4 into namespace kafka.
  • Category folders are for people only. Application names and namespaces come from the last folder, so moving an app between categories (for example my-apps/media/x → my-apps/utility/x) only changes its source path. Renaming the last folder creates a new Application and namespace.
  • The infrastructure and monitoring ApplicationSets set preserveResourcesOnDeletion: true, so their generated Applications carry no resources-finalizer. If a path briefly stops matching (a folder move, a typo, the AppSet spec and repo updating out of order), Argo CD removes only the Application object and recreates it on the next generation; workloads, namespaces and PVCs keep running. The trade-off: removing an infra or monitoring component from git leaves its resources on the cluster. Delete them by hand after the Application is gone; they still carry the argocd.argoproj.io/tracking-id: <app>:… annotation, and kubectl delete -k against the path at the previous commit removes them. Re-adding the finalizer does not work because the AppSet controller rewrites finalizers on every update. The my-apps and database ApplicationSets keep the finalizer because _archive/ relies on the cascade.
  • Change an ApplicationSet's paths and move the folders it matches in separate PRs: first list the new paths alongside the old ones, then move the folders, then drop the old paths. Doing both in one commit let the controller read the new folders with the old spec. Wait for each step to be live, not just merged: root walks the sync waves and can take ~20 minutes to apply an AppSet change. Use the read-only live prerequisite gate: require operationState.phase: Succeeded with operationState.syncResult.revision at the merged commit, then verify the exact live AppSet fields, generated Application ownership/finalizers and preserved resource UIDs. status.sync.revision alone is comparison evidence, not proof that the safety change applied. The user must explicitly authorize each specific PR merge.
  • All four ApplicationSets use strict Go templates (missingkey=error). Git directory fields are objects in Go-template mode. The leading . means "the current generator result"; path is the Git generator's directory object, and the remaining names select fields from it. Therefore use {{ .path.path }} / {{ .path.basename }} rather than the deprecated fasttemplate forms {{ path }} / {{ path.basename }}. Strict missing-key handling deliberately fails generation instead of silently producing an Application with an empty name, source path, or namespace. The render CI also rejects any non-Component kustomization that produces zero Kubernetes objects.
  • Changing a generated Application name replaces the Application object. Never merge an identity rename into a running cluster without a staged finalizer migration. The 2026-07-31 prefix migration is complete; its historical merge-after-destruction step is not part of a normal rebuild.
  • rustfs-lifecycle is not a standalone Argo entrypoint. It is generated by appsets/infrastructure-appset.yaml from infrastructure/backup/rustfs-lifecycle at wave 4.
  • Global ignore rules for HTTPRoute, ExternalSecret, and PVC restore fields live in infrastructure/controllers/argocd/values.yaml; AppSets should only carry app-specific ignore rules.
  • AppProjects are intentionally permissive for a single-operator homelab. They are labels and UI grouping, not hard tenant guardrails.
  • Hook Jobs are acceptable when they are idempotent, bounded, and documented. The RustFS lifecycle Job is a PostSync hook because S3 lifecycle PUTs are idempotent and Kubernetes Jobs are immutable.

Project Nomad App Boundary

Project Nomad is not special to Argo CD; it is special only in repo shape. The my-apps ApplicationSet discovers app directories with my-apps/*/*, so my-apps/knowledge/project-nomad is the generated Application boundary.

Inside that directory there is one parent kustomization.yaml. Subdirectories such as mysql/, redis/, qdrant/, embeddings/, kiwix/, protomaps/, cyberchef/, and flatnotes/ are resource folders referenced by the parent kustomization, not independent app directories.

Do not exclude my-apps/knowledge/project-nomad/*; that pattern targets child folders the AppSet does not generate. If Project Nomad should ever become multiple Argo CD Applications, add child kustomization.yaml files deliberately and update the generator/validation model at the same time.

Archived apps

A retired app moves into its category's _archive/ folder, for example my-apps/ai/_archive/ninfer/ or monitoring/ai-ops/_archive/holmesgpt/. The my-apps and monitoring ApplicationSets exclude */_archive, so nothing there is deployed.

Archive an app

  1. Confirm the latest backup succeeded: kubectl -n <ns> get snapshot.
  2. git mv my-apps/<category>/<app> my-apps/<category>/_archive/<app> and merge.

Expected result for my-apps: Argo CD deletes the Application, its namespace and its PVCs through the resources finalizer. Monitoring Applications are removed but their resources remain because that ApplicationSet enables preservation; manual cleanup requires explicit destructive authorization. Kopia snapshots stay in the repository with onNamespaceDelete: Orphan (the current ClusterRepository CRD default); do not change that setting, or archiving an app deletes its backup history.

Bring an app back (rollback)

git mv the folder back up one level and merge. The Application is recreated and restore-before-bind fills each PVC from its latest snapshot, the same path a disaster-recovery rebuild uses.

Leave archived files unchanged. They sit one folder deeper, so their ../../common/kopiur-backup link does not resolve there; CI skips _archive/ when rendering, and the link works again once the folder moves back.