4. Manifest generation
Argo CD · 4. Manifest generationThe repo server has one job: turn a git reference into a flat list of Kubernetes objects. Everything about how your manifests are authored is contained in this step, and several of Argo CD’s more confusing behaviours are really Helm’s or Kustomize’s behaviours arriving through it.
The rule that explains Helm behaviour
Section titled “The rule that explains Helm behaviour”Argo CD never runs
helm installorhelm upgrade. It runshelm templateand applies the output.
This one sentence resolves most Helm-related confusion in Argo CD.
HELM CLI ARGO CD
helm install my-app ./chart helm template my-app ./chart → renders → renders → applies then Argo CD applies → RECORDS A RELEASE nothing is recorded in a SecretThe consequences are direct:
There are no Helm releases. helm list shows nothing. Argo CD tracks state by comparing git to the cluster, not by storing release history.
helm rollback does not exist. Rollback is git revert, or Argo CD’s own history (argocd app rollback), which re-syncs a previous rendered revision.
Helm hooks are translated, not executed by Helm. Argo CD maps helm.sh/hook: pre-install onto its own PreSync hooks. The mapping is good but not perfect — chapter 7 covers where it differs.
Chart tests and lifecycle hooks that depend on release state do not work. Anything reading .Release.IsUpgrade or expecting a release Secret will misbehave, because there is no release.
--wait semantics come from Argo CD’s health assessment, not from Helm. Chapter 8.
This is a deliberate design choice, not a limitation. Helm’s release state is a second source of truth living in a Secret in the cluster, and the entire point of Argo CD is that git is the only source of truth. Removing the release removes the conflict.
The Helm guide covers charts in full; its chapter 12 covers the four chart features that break silently under helm template. in an Application
source: repoURL: https://github.com/acme/manifests.git targetRevision: main path: charts/checkout helm: releaseName: checkout valueFiles: - values.yaml - values-prod.yaml values: | replicaCount: 5 image: tag: v1.4.2 parameters: - name: image.tag value: v1.4.2 fileParameters: - name: config path: files/config.json version: v3 skipCrds: false ignoreMissingValueFiles: falsePrecedence, lowest to highest: the chart’s own values.yaml, then valueFiles in order, then the inline values block, then parameters. parameters are --set, so they take the same string-typing pitfalls with them — a value of 1.10 becomes 1.1 unless you use stringParameters.
skipCrds matters more than it looks. Helm installs CRDs from crds/ only on install, never on upgrade. Argo CD applies them every time, which is usually what you want and occasionally surprising if a CRD is large enough to hit the annotation limit — chapter 6.
ignoreMissingValueFiles is how you have one Application definition across environments where only some have an override file. Without it, a missing file is a hard render failure.
Values from another repository
Section titled “Values from another repository”The multi-source pattern from chapter 3 is the right answer for using an upstream chart:
sources: - repoURL: https://charts.bitnami.com/bitnami chart: postgresql targetRevision: 16.2.1 # PIN IT helm: valueFiles: - $values/envs/prod/postgresql.yaml - repoURL: https://github.com/acme/manifests.git targetRevision: main ref: valuesThe alternative — a wrapper chart that declares the upstream as a dependency — also works and gives you somewhere to put extra resources. Both are fine; the multi-source version is less machinery.
Kustomize
Section titled “Kustomize”Detected by kustomization.yaml. Argo CD runs kustomize build and can inject overrides:
source: path: overlays/prod kustomize: namePrefix: prod- nameSuffix: -v2 images: - acme/checkout:v1.4.2 # override the image tag replicas: - name: checkout count: 5 commonLabels: env: prod commonAnnotations: team: payments patches: - target: { kind: Deployment, name: checkout } patch: |- - op: replace path: /spec/template/spec/containers/0/resources/limits/memory value: 2GiThe images override is the important one, because it is what most image-updating automation writes to. It is also worth a warning: overrides in the Application make the Application a second place where configuration lives. A reviewer reading the repository sees one thing; the running system reflects the repository plus whatever is in the Application spec. Prefer putting configuration in the overlay and keeping the Application boring.
Remote bases (resources: pointing at a git URL) work, and mean your render depends on a third-party repository being reachable and unchanged. Vendor anything you depend on for production.
Plain YAML
Section titled “Plain YAML”No Chart.yaml, no kustomization.yaml — Argo CD reads the .yaml, .yml and .json files in the path.
source: path: manifests directory: recurse: true include: '*.yaml' exclude: 'test/**' jsonnet: {}recurse is off by default. A directory of subdirectories syncs nothing and reports success, which is a quietly confusing first experience.
Plain YAML is underrated. If you are not templating anything, a directory of manifests is the most readable and reviewable option there is, and “we adopted Helm because that is what people do” has produced a great deal of unnecessary complexity.
Config management plugins
Section titled “Config management plugins”For anything else — jsonnet with custom logic, cdk8s, Terraform-generated manifests, a homegrown templating system — a config management plugin (CMP) runs your own binary in a sidecar of the repo server.
# a ConfigMap mounted into a CMP sidecarapiVersion: v1kind: ConfigMapmetadata: name: cmp-plugindata: plugin.yaml: | apiVersion: argoproj.io/v1alpha1 kind: ConfigManagementPlugin metadata: name: my-generator spec: discover: fileName: "./generate.sh" generate: command: ["sh", "-c", "./generate.sh"]The contract is simple: write Kubernetes YAML to stdout. Argo CD applies whatever comes out.
Two warnings.
A CMP executes arbitrary code from your repository inside the repo server. That is a genuine security boundary, and it is why plugins run in sidecars with their own restricted service accounts rather than in the repo server process. Never enable a CMP for a repository you do not fully control.
A CMP that is slow makes everything slow. Rendering happens on every refresh for every Application using it. A generator taking thirty seconds turns a hundred Applications into a repo server that never catches up.
The older configManagementPlugins field in argocd-cm is removed in Argo CD 2.8+. Use sidecar plugins.
Caching, and why the first sync is slow
Section titled “Caching, and why the first sync is slow”The repo server caches aggressively in Redis:
key: repo + revision + path + rendering parameters value: the rendered manifestsSo a rendered manifest for a given commit is computed once and reused by every Application that needs it. This is why the first refresh after a push takes seconds and every subsequent one is instant.
Three things invalidate the cache: a new revision, changed rendering parameters (a different values file, a Kustomize image override), or a manual hard refresh.
argocd app get checkout --hard-refresh # ignore cache, re-renderHard refresh is the tool for the specific confusion of “I pushed a change and Argo CD is showing the old manifests.” That almost always means one of:
- the branch you pushed to is not
targetRevision - a mutable tag —
targetRevision: v1.2where the tag was moved, so the resolved SHA looks unchanged - a Helm chart version resolved from cache
- a git submodule that was not updated
The mutable-tag case is the most common and the most annoying. Argo CD caches by resolved revision; if the tag moved but Argo CD has not re-resolved it, nothing appears to change. Pin to SHAs, or accept the hard refresh.
What comes out
Section titled “What comes out”Whatever the path, the output is a flat list of Kubernetes objects — no Helm release, no Kustomize state, no plugin state. The renderer’s identity is forgotten immediately after rendering. From the application controller’s perspective there is only a list of desired objects, which is why chapters 6, 7 and 8 never mention Helm or Kustomize again.
That flattening is why you can switch from Helm to Kustomize without Argo CD noticing anything except that the manifests changed. It is also why Helm-specific machinery — release history, helm rollback, chart tests — simply is not available: it never entered the system.
What to take from this chapter
Section titled “What to take from this chapter”- Argo CD runs
helm template, neverhelm install. There are no releases, nohelm list, nohelm rollback, and hooks are translated to Argo CD hooks. - Helm value precedence: chart
values.yaml→valueFilesin order → inlinevalues→parameters(--set, with its string-typing traps). - Pin chart versions. A semver range means your desired state changes without a commit.
- Use multiple sources to pair an upstream chart with your own values file.
- Kustomize overrides in the Application spec create a second place configuration lives. Prefer keeping it in the overlay.
- Plain YAML is a legitimate and underrated choice — but
recurseis off by default, so a tree of subdirectories silently syncs nothing. - Config management plugins run arbitrary code from your repository in a repo server sidecar. Only for repositories you control, and a slow plugin starves everything.
- Rendering is cached in Redis by repo + revision + parameters.
--hard-refreshis the fix when a moved mutable tag or a submodule makes changes appear not to arrive. - The output is a flat list of objects with no memory of how it was generated — which is why nothing after this chapter mentions Helm again.
Next: the reconciliation loop traced end to end, from a commit landing to a pod running.