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 CNPG Barman plugin | 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/cnpg-barman-plugin-app.yaml |
Application | 3 | CNPG clusters reference the plugin in wave 4 | No, dependency must precede database AppSet |
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/*/*; uses selfHeal: false for DR |
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 my-apps/common/* Components) after core storage and restore foundations |
N/A |
Bootstrap guardrail — observability is not a core dependency¶
No bootstrap-critical app may render monitoring.coreos.com resources (ServiceMonitor, PodMonitor,
PrometheusRule, Probe, AlertmanagerConfig) in its core kustomization. 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 cert-dependent apps (cnpg-barman-plugin,
Wave 3) can start. 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 an Argo CD sync that aborts applies none
of its resources — so unrelated apps stall waiting on Secrets that were never
created.
Two rules follow:
- Fail-closed cluster-scoped webhooks are pinned to the control plane
(
nodeSelector: node-role.kubernetes.io/control-planeplus the matchingNoScheduletoleration). The API server calls them node-locally, so no pod-network fault on any other node can break admission. This applies tocert-managerand thecloudnative-pgoperator. 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 ownapiserver_admission_webhook_*metrics), not in the wave gate.
Notes¶
project-nomadis intentionally managed byappsets/my-apps-appset.yamlas a single bundled app atmy-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.shCheck 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";pathis 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. rustfs-lifecycleis not a standalone Argo entrypoint. It is generated byappsets/infrastructure-appset.yamlfrominfrastructure/storage/rustfs-lifecycleat 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.