Rendered Manifests¶
The rendered manifests pattern is an advanced GitOps workflow in which Kustomize (or Helm) templates are executed at promotion time and the rendered YAML is committed directly to Git. Argo CD and Flux sync from the rendered output, not from the source templates.
This pattern is the standard for large Argo CD deployments (50+ applications) and is used in production at organizations that need PR reviewers to see exact YAML diffs rather than template changes.
Why Render Manifests at Promotion Time¶
Benefit 1: Visible PR diffs¶
In a standard GitOps setup, a PR that changes image.tag in values.yaml from v1.28.0 to v1.29.0 is a 1-line diff. The reviewer has no idea what the rendered output looks like without running helm template locally.
With rendered manifests, the same promotion PR shows the exact YAML change:
Every change — including environment variables, resource limits, Ingress rules, security contexts — is visible as a plain diff.
Benefit 2: GitOps agent performance¶
When an Argo CD Application tracks plain YAML, the agent only applies it. When it tracks a Helm chart or Kustomize base, the agent runs helm template or kustomize build on every reconciliation loop, on every cluster. At 200+ applications, this causes CPU spikes and reconciliation lag. Pre-rendering eliminates this entirely.
Benefit 3: CODEOWNERS enforcement on rendered output¶
CODEOWNERS rules can be placed on the rendered environment branches. A change to a production Ingress requires review from the security team — even if the source template change looked innocuous.
Benefit 4: Auditability¶
The Git history of env/prod is the exact sequence of manifests that ran in production. No reconstruction needed.
Repository Structure¶
main branch (DRY source):
base/
deployment.yaml
service.yaml
kustomization.yaml
environments/
dev/
kustomization.yaml # patches over base
staging/
kustomization.yaml
prod/
kustomization.yaml
env/dev branch (rendered):
deployment.yaml # plain YAML, no Kustomize references
service.yaml
env/staging branch (rendered):
deployment.yaml
service.yaml
env/prod branch (rendered):
deployment.yaml
service.yaml
kardinal-promoter reads from the source branch, runs kustomize-set-image then kustomize-build, and commits the result to the environment branch via a PR (for pr-review environments) or a direct push (for auto environments).
Pipeline Configuration¶
Configure layout: branch on each environment that should use the rendered manifests pattern:
apiVersion: kardinal.io/v1alpha1
kind: Pipeline
metadata:
name: my-app
spec:
git:
url: https://github.com/myorg/gitops-repo
branch: source # DRY source branch where overlays live
provider: github
secretRef: { name: github-token }
environments:
- name: dev
path: overlays/dev # overlay path within the source branch
approval: auto
layout: branch # use rendered manifests branch layout
health:
type: resource
- name: staging
path: overlays/staging
approval: auto
layout: branch
health:
type: argocd
- name: prod
path: overlays/prod
approval: pr-review
layout: branch
health:
type: argocd
When layout: branch is set, the default step sequence becomes:
git-clone → kustomize-set-image → kustomize-build → git-commit → git-push
→ [open-pr → wait-for-merge] # for pr-review environments
→ health-check
The kustomize-build step: 1. Runs kustomize build <env.path> in the cloned working directory 2. Writes the rendered YAML to rendered-<env>.yaml in the work directory 3. Stores the path in Outputs["renderedManifestPath"] for subsequent steps
The git-commit step picks up the rendered manifest file and commits it to the environment branch (env/<name> by convention).
See examples/rendered-manifests/ for a complete working example.
When the branchPrefix field is set, the default step sequence auto-infers the branch names. Manual steps configuration is only needed for non-standard naming schemes.
Argo CD Configuration¶
Argo CD Applications must track the rendered branch, not the source branch:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app-prod
namespace: argocd
spec:
source:
repoURL: https://github.com/myorg/gitops-repo
targetRevision: env/prod # rendered branch, not main
path: . # root of the rendered branch (all files are plain YAML)
destination:
server: https://kubernetes.default.svc
namespace: my-app-prod
syncPolicy:
automated:
prune: true
selfHeal: true
Because the rendered branch contains only plain YAML (no Kustomize base references or Helm chart), Argo CD uses the Directory source type automatically. This is significantly faster than Kustomize or Helm source types on large clusters.
Branch Protection¶
The env/prod rendered branch should be protected:
- Require pull request reviews before merging (CODEOWNERS or required reviewers)
- Require status checks (Argo CD Application health, custom validation scripts)
- Disallow direct pushes — only kardinal-promoter's PR branch merges are permitted
# GitHub branch protection (in Terraform or GitHub UI)
branch_protection:
pattern: "env/*"
required_pull_request_reviews:
required_approving_review_count: 1
require_status_checks:
strict: true
contexts:
- "argocd-health/my-app-prod"
CODEOWNERS on rendered branches is enforced by GitHub without any kardinal-promoter configuration. The controller does not bypass CODEOWNERS.
CODEOWNERS Integration¶
Place a CODEOWNERS file in the rendered branch:
# env/prod CODEOWNERS
/deployment.yaml @myorg/security-reviewers
/ingress.yaml @myorg/platform-team @myorg/security-reviewers
* @myorg/platform-team
Any promotion PR to env/prod that touches deployment.yaml will require approval from @myorg/security-reviewers. kardinal-promoter waits for the PR to be approved and merged before advancing the PromotionStep.
Comparison: Directory Layout vs Branch Layout¶
| Aspect | Directory layout (default) | Branch layout (rendered) |
|---|---|---|
| PR diff | Template source diff (values.yaml) | Rendered YAML diff |
| Argo CD source type | Kustomize or Helm | Directory (plain YAML) |
| Argo CD performance | Template runs on every reconcile | No template run |
| CODEOWNERS granularity | Overlay directory | Individual YAML files |
| Git history | All envs on one branch | Each env has its own history |
| Complexity | Lower | Higher |
| Best for | Small teams, simple apps | Enterprise, security requirements |
Anti-Pattern: Do Not Use targetRevision Updates¶
A common shortcut in some GitOps tools is to update the Argo CD Application's spec.source.targetRevision field to point to a new commit SHA, bypassing Git entirely. This is sometimes called "Pseudo-GitOps."
Do not use this pattern. It breaks the core GitOps guarantee that Git is the source of truth. You cannot recover the cluster state from Git if targetRevision was never committed. The Argo CD Application object becomes your source of truth instead of Git, which defeats auditability and disaster recovery.
kardinal-promoter never mutates Argo CD Application objects directly. All promotions write to Git first.
Examples¶
See examples/rendered-manifests/ for a complete working example including: - GitOps repo structure with source and env branches - Pipeline CRD with renderManifests: true - Argo CD ApplicationSet targeting rendered branches - CODEOWNERS file for the env/prod branch