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 Kafka CRDs/operator must precede wave-6 Kafka resources; standalone destination kafka also avoids the infrastructure AppSet's basename→namespace assumption (strimzi would be wrong) No, both ordering and destination namespace are exceptional
custom-entrypoints/opentelemetry-operator-app.yaml Application 5 Core operator after cert-manager; ServiceMonitor kept out of core Maybe, if cert-manager dependency is otherwise enforced
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/* after core infra N/A
appsets/my-apps-appset.yaml ApplicationSet 6 Discovers my-apps/*/* (excluding Components); 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/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/home/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.
  • 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/storage/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/home/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/home/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.