Overview
For three weeks I’ve ended every post with the same claim and never once shown my work.
The claim: the CNCF ecosystem already solved continuous delivery, and what the mid-market team is missing isn’t technology, it’s the assembly — a golden path stitched from primitives that already exist. Argo CD reconciles. Kargo promotes. The last post argued you should adopt that road as a product rather than build it as a project.
Fair enough, said a few people, but that’s a slogan until you show the manifests. Why those two tools? And if you’re building a platform layer anyway, why not own the whole pipeline — your own reconciler, your own promotion engine, your own state — instead of taking a dependency on someone else’s CRDs?
This is the post that answers with YAML. The short version: the fastest way to lose is to reinvent continuous delivery. You productize the primitives. You do not rebuild them. Here’s exactly what that looks like on disk.
The temptation to reinvent
When you set out to build a golden-path layer, there’s a seductive voice that says own the whole thing. Write your own controller that watches Git and applies manifests. Write your own promotion logic that moves a build from staging to prod. Keep the state in your own database where you control it. It feels cleaner — one system, your system, no upstream to track.
It’s a trap, and it’s the specific trap that has killed more internal platforms than any budget.
Reconciliation is not a weekend problem. It’s drift detection, sync ordering, health assessment across dozens of resource kinds, pruning, retries, and a decade of edge cases that only show up in someone else’s cluster at 3am. Promotion is not a weekend problem either — it’s provenance, immutability, gating, and the guarantee that the thing you tested is byte-for-byte the thing you shipped. A two- or three-person team that rebuilds these is signing up to re-encounter every bug the ecosystem already fixed, forever.
And here’s the part that should settle it: a better reconciler is not your competitive edge. Nobody adopts a mid-market platform because its home-grown CD engine is marginally different from Argo CD’s. They adopt it because the road is good. So spend your scarce engineering there, and stand the road on primitives that already won.
The bet: reconcile with Argo CD, promote with Kargo
Two jobs, two tools, chosen for boring reasons.
Argo CD for reconciliation. In the 2025 Argo CD End User Survey run by CNCF, 97% of respondents run it in production (up from 93% in 2023), it carries an NPS of 79, and platform engineers are now 37% of its users. When the layer your platform stands on is the single most-adopted GitOps tool in the ecosystem, you inherit three things for free: its operational maturity, its surrounding tooling, and — this one matters — the trust your users already have in it. Betting against Argo CD is betting you understand continuous reconciliation better than the people who defined the category. We don’t, and neither does almost anyone.
Kargo for promotion. Argo CD reconciles a desired state; it deliberately doesn’t decide when one environment’s state should become another’s. That decision — promotion — is the actual gap, and it’s where most teams bolt on a pile of CI scripts. Kargo fills exactly that gap, and it’s built by the Argo CD creators at Akuity — 3.5M+ downloads and ~3,100 stars, up 50% in a year. Standing your promotion story on the tool the Argo team themselves built to solve promotion is about as low-risk as a young dependency gets.
So the layer’s whole job, at the CD tier, is to generate the right Argo CD and Kargo objects from one app definition — and then get out of the way.
What the layer actually generates
Take an app called hello in a project called acme, with staging and prod environments. From that, the layer emits three kinds of object. Labels below are anonymized (platform.example/…, managed-by: your-platform) — in a real install they carry whatever brand the operator configured.
1. A Kargo Warehouse watches the image repository and mints a Freight — an immutable bundle pinning one image digest — every time a new tag appears.
apiVersion: kargo.akuity.io/v1alpha1
kind: Warehouse
metadata:
name: hello
namespace: kargo-acme
labels:
app.kubernetes.io/managed-by: your-platform
platform.example/app: hello
platform.example/project: acme
spec:
freightCreationPolicy: Automatic
subscriptions:
- image:
repoURL: registry.example.com/acme/hello
imageSelectionStrategy: SemVer
constraint: ">=1.0.0"
2. A Kargo Stage per environment requests that Freight and carries the promotion recipe. Staging takes Freight straight from the Warehouse; prod takes it from the staging Stage — so prod can only ever run something staging already ran.
apiVersion: kargo.akuity.io/v1alpha1
kind: Stage
metadata:
name: hello-staging
namespace: kargo-acme
labels:
app.kubernetes.io/managed-by: your-platform
platform.example/app: hello
platform.example/env: staging
platform.example/env-type: staging
spec:
requestedFreight:
- origin:
kind: Warehouse
name: hello
sources:
direct: true # prod uses: sources: { stages: [hello-staging] }
promotionTemplate:
spec:
steps:
- uses: git-clone
config:
repoURL: https://git.example.com/acme/gitops.git
checkout:
- { branch: main, path: ./src }
- uses: yaml-update
config:
path: ./src/envs/staging/acme/hello/values.yaml
updates:
- key: image.tag
value: ${{ quote(imageFrom("registry.example.com/acme/hello").Tag) }}
- uses: git-commit
config: { path: ./src, message: promote acme/hello to staging }
- uses: git-push
config: { path: ./src }
- uses: argocd-update
config:
apps:
- { name: acme-hello-staging, namespace: argocd }
3. An Argo CD Application reconciles the rendered chart into the environment’s own namespace, and never leaves it drifted.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: acme-hello-staging
namespace: argocd
spec:
project: acme
source:
repoURL: https://git.example.com/acme/gitops.git
path: acme/hello/staging
targetRevision: HEAD
helm:
releaseName: hello
valueFiles: [values.yaml]
destination:
server: https://kubernetes.default.svc
namespace: hello-staging
syncPolicy:
automated: { prune: true, selfHeal: true }
syncOptions: [CreateNamespace=true]
Trace one change through all three. An image lands at registry.example.com/acme/hello:1.4.0. The Warehouse mints a Freight for it. A promotion-policy on staging (auto-promote on) fires the staging Stage’s promotionTemplate: it clones the GitOps repo, writes the resolved tag into envs/staging/.../values.yaml, commits, pushes, and pokes Argo CD. Argo CD re-renders the Helm release into hello-staging and self-heals it there. Prod is the same Freight promoted forward — no auto-promotion policy, so it waits for a human — and because it sources from the staging Stage, “it passed staging” means the identical digest, not a rebuilt lookalike.
The two lines that carry the whole argument
Most of that YAML is plumbing. Two details are the actual thesis.
${{ quote(imageFrom(...).Tag) }}. The promotion never rebuilds anything. It resolves the tag from the Freight and writes it into Git. The artifact that Kargo mints once is the artifact every stage runs — build once, promote the same bytes. That’s the guarantee CI-script promotion quietly breaks and the reason to use a promotion tool at all. (The quote() wrapper is a scar, incidentally: without it a version like 1.40 serializes as a YAML float and Helm rejects it. Standing on primitives means you learn their sharp edges — more on that below.)
A platform.example/project label on every object, and a promotion that is just a Git commit. This is the exit story from the earlier posts, made concrete. There is no proprietary control plane holding your pipeline hostage, because there is no control plane — there’s a Warehouse, some Stages, and Applications, all standard CRDs, all labeled, all reconciled from a Git repo you own. If the layer that generated them vanished tomorrow:
kubectl get applications,stages,warehouses -A \
-l platform.example/project=acme
finds every object it ever made — and the workloads it rendered into your clusters carry the standard app.kubernetes.io/managed-by on top, so a second one-liner sweeps those too. Argo CD keeps reconciling the same repo without the layer. Leaving is a Tuesday afternoon. That property is a direct consequence of productizing instead of owning — you can’t be trapped in a control plane that was never built.
What we don’t get for free
I’d be doing the exact thing I criticized if I sold this as costless. Standing on other people’s primitives has a real bill, just a smaller one than reinventing them.
You inherit their release cadence. Kargo’s promotion API changed shape between v0.9 and v1.x — the old promotionMechanisms field was replaced by promotionTemplate.spec.steps, and the v1 admission webhook silently strips the old form. We had to track that migration and pin a minimum version (the quote() fix landed in v1.3.4). When you generate manifests for upstream CRDs, upstream’s breaking changes become your regression suite.
Kargo is young. It’s excellent and it’s from the right team, but it does not have Argo CD’s decade of scars. Fewer Stack Overflow answers, a smaller pattern library, edge cases still being found. Betting on it is a bet on the maintainers more than on the maturity — a defensible bet, but an honest one to name.
Productizing an assembly is still engineering. Keeping generated manifests correct against two projects’ evolving schemas is an ongoing tax. It is genuinely cheaper than owning a reconciler and a promotion engine — but “cheaper than the worst option” is not “free,” and anyone who tells you otherwise is selling.
The trade still lands where it started: this is weeks of assembly plus ongoing upkeep, versus years of reinventing CD badly. For a small team, that’s not close.
Where this is going
You’ll have noticed there is not a single secret anywhere in those manifests. That’s not an accident of the example — it’s the next post. How the layer keeps secret values out of Git entirely, referenced from an external store through a hierarchy of scopes, using External Secrets Operator, so that “GitOps everything” and “never commit a secret” stop being in tension.
Until then, a real question for anyone who has wired Argo CD and Kargo together by hand: where did the schema drift bite you? I’ve named the quote() float bug and the v0.9→v1 promotion rewrite. I’d bet you have two or three of your own, and I’d genuinely like to hear them — that scar list is the most useful documentation this ecosystem doesn’t have.
