Skip to content

4. Manifest generation

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

Argo CD never runs helm install or helm upgrade. It runs helm template and 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 Secret

The 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: false

Precedence, 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.

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: values

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

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: 2Gi

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

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.

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 sidecar
apiVersion: v1
kind: ConfigMap
metadata:
name: cmp-plugin
data:
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.

The repo server caches aggressively in Redis:

key: repo + revision + path + rendering parameters
value: the rendered manifests

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

Terminal window
argocd app get checkout --hard-refresh # ignore cache, re-render

Hard 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.2 where 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.

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.

  • Argo CD runs helm template, never helm install. There are no releases, no helm list, no helm rollback, and hooks are translated to Argo CD hooks.
  • Helm value precedence: chart values.yaml → valueFiles in order → inline values → 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 recurse is 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-refresh is 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.

Written by Sepuri Sai Krishna

© 2026CC BY 4.0Source