Configuration, Templating & Packaging
Every workload on your platform is, in the end, a pile of Kubernetes YAML. The interesting question is how you produce that YAML — because the same app has to run in five environments, keep its secrets out of Git, stay DRY without becoming unreadable, and be safe for a hundred teams to change every day. This is the craft of turning one intention into many correct manifests: raw YAML and where it snaps, Helm’s package model, Kustomize’s template-free overlays, the new wave of typed and programmatic config, how all of it renders in a GitOps world, and the workload abstractions that finally let a developer describe an app once and let the platform realise it.
Imagine you draw one picture of a robot, but you need slightly different robots for your bedroom, your classroom, and the big science fair — a small one, a medium one, and a giant one. You could redraw the whole robot three times (and make three times the mistakes), or you could draw it once and write tiny sticky notes that say “this one’s bigger” or “this one’s red.” Configuration tools are those clever ways of drawing the robot once and sticking on the little notes, so all three robots come out right — and nobody has to redraw the arms every single time.
The configuration problem
☺ Like you’re 10: One app has to run in lots of slightly different places, and copying it everywhere is how mistakes sneak in.
Before you can judge Helm against Kustomize against anything else, you have to feel the problem they all exist to solve. A single service is never deployed once. It runs in dev (tiny, chatty logging, a throwaway database), in staging (production-shaped, but safe to break), and in prod (many replicas, real secrets, strict limits) — often multiplied again across regions and clusters. The shape of the app is identical everywhere; only a handful of values change. Configuration management is the discipline of expressing “mostly the same, but different in these specific ways” without drowning in copies.
One app, many environments — the N × M explosion
Count the axes. You have M services and N environments, and every cell in that grid needs a valid set of manifests. Naïvely, that’s M × N hand-maintained copies, and each copy drifts a little further from its siblings every week. The goal of every tool on this page is to collapse that grid back down to one definition plus a small set of per-environment deltas, so the number of things a human actually writes grows like M + N, not M × N.
DRY vs clarity — the central tension
Here is the argument that never dies. DRY (“don’t repeat yourself”) says: factor out everything shared so a change happens in exactly one place. Taken too far, DRY produces config that is correct but unreadable — a template so parameterised that you can’t tell what any given environment actually gets without running the renderer in your head. The opposing value is clarity: a reader should be able to open the prod folder and see what prod is, without decoding a maze of overrides. Every tool on this page sits somewhere on that spectrum, and mature platform teams deliberately choose a point on it rather than maximising DRY blindly. A good heuristic: factor out the things that must never differ (labels, security context, probes), and keep visible the things a human will want to check (replica counts, image tags, resource limits).
Configuration is not a “make it DRY” problem — it’s a “make the right things easy to change and the important things easy to read” problem. The best layout minimises both duplication and surprise. If a reviewer can’t predict what a change does to prod by looking at the diff, your config is too clever.
Configuration drift and the single source of truth
Drift is what happens when the running cluster stops matching the config you think describes it — someone runs kubectl edit at 2am, a mutating webhook rewrites a field, or two environments were “kept in sync by hand” and quietly diverged. The antidote is a single source of truth: one place — a Git repository — that authoritatively answers “what is supposed to be running?”, and a reconciler that continually drives reality back to it. Configuration tooling is the layer that turns that source of truth from a wall of duplicated YAML into a maintainable set of definitions. The tool you pick strongly affects how easy drift is to spot: a small, structured overlay diffs cleanly in a pull request; a deeply templated chart can hide a meaningful change inside whitespace.
Separating configuration from secrets
☺ Like you’re 10: The recipe card can live on the fridge for everyone to read, but the key to the safe does not get written on it.
Config and secrets look alike — both are key/value settings an app reads at start-up — but they have opposite handling rules. Config is meant to be read, reviewed, and versioned in Git forever. Secrets must never land in Git in plaintext, because a repository is world-readable to anyone who can clone it and its history is effectively permanent. The discipline is to keep a reference to a secret in your config (“inject the value named db-password”) while the plaintext lives elsewhere — sealed to a cluster key, or synced at runtime from a vault. Every tool below has to play nicely with that split; none of them is a place to store a password.
Do not let a templating engine tempt you into “just this once” inlining a token so a chart renders. The moment a secret is committed it is compromised — rotate it, don’t just delete the commit. Keep secrets in Sealed Secrets, the External Secrets Operator, or SOPS-encrypted values, and pass only references through your config. The full playbook lives in Secrets Management.
Raw YAML and where it breaks down
☺ Like you’re 10: Plain YAML is like writing every robot out longhand — fine for one, painful for ten.
The honest starting point is plain, hand-written Kubernetes manifests applied with kubectl apply -f. For a single tiny service it is the right answer: no tool to learn, no rendering step, the file is exactly what runs. Everyone should be able to read raw manifests, because raw manifests are what every other tool eventually produces. The trouble is not that raw YAML is wrong — it’s that it does not scale across environments, and it fails in three specific ways.
The duplication tax
Multiply one service across dev and prod with plain files and you get two near-identical documents that differ in maybe four lines. Change a label, a probe, or a volume mount, and you must remember to change it in both — forever. This is the duplication tax, and it compounds: the more environments and services you add, the more places a single logical change has to be repeated, and the more likely one of them is missed.
# dev/deployment.yaml # prod/deployment.yaml is 95% identical…
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout
spec:
replicas: 1 # ← prod wants 6
template:
spec:
containers:
- name: checkout
image: acme/checkout:1.4.2 # ← prod pins a different tag
resources:
requests: { cpu: 50m, memory: 64Mi } # ← prod requests more
env:
- name: LOG_LEVEL
value: debug # ← prod wants info
# …every other line must be kept byte-for-byte in sync, by hand, in both files.No parameters, no logic
YAML is a data format, not a language. It has no variables, no conditionals, no loops, and no way to say “this value is derived from that one.” So you cannot express “give staging and prod an anti-affinity rule but not dev,” or “generate one CronJob per region in this list,” without physically writing out every case. The absence of logic is a feature for readability and a curse for anything that legitimately varies in structure rather than just in a value.
No validation, no types
Nothing stops you writing replicas: "3" (a string), misspelling resouces, or indenting a block one space wrong. Some mistakes are caught by the API server at apply time — after the change has already started rolling out — and some (a typo’d annotation, an env var that’s silently ignored) are never caught at all. Plain YAML gives you no schema, no types, and no pre-flight check. Client-side tools like kubeconform or kubectl apply --dry-run=server help, but they are bolt-ons; the format itself knows nothing about what a valid Deployment looks like. These three gaps — duplication, no logic, no validation — are precisely the holes the rest of this page fills.
Helm — the package manager
☺ Like you’re 10: Helm is like an app-store bundle: someone packs up all the YAML with knobs on the front, and you install it by turning the knobs.
Helm is the de-facto package manager for Kubernetes and the reason you can install something as sprawling as Prometheus or an ingress controller with a single command. Its unit is the chart: a versioned bundle of templated manifests plus a set of default values. Helm’s superpower is distribution — thousands of third-party charts exist, so you rarely hand-write config for off-the-shelf software — and its central trade-off is that it templates YAML using a text templating engine, which is powerful but blind to YAML’s structure.
Charts, templates & the Go template language
A chart is a directory: Chart.yaml (name, version, dependencies), values.yaml (default settings), a templates/ folder of manifests written in Go’s text/template language (extended with the Sprig function library), and optionally charts/ for bundled dependencies and crds/ for CRDs installed before anything else. At install time Helm walks templates/, substitutes values, and emits plain manifests. The template language gives you the logic raw YAML lacks: {{ .Values.x }} to interpolate, {{ if }}/{{ range }} for conditionals and loops, named templates via define/include (usually in _helpers.tpl), and the {{- -}} whitespace-chomping syntax that keeps indentation legal.
# templates/deployment.yaml — one template serves every environment
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "checkout.fullname" . }}
labels: {{- include "checkout.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
template:
spec:
containers:
- name: checkout
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
env:
- name: LOG_LEVEL
value: {{ .Values.logLevel | quote }}
{{- if .Values.probes.enabled }}
readinessProbe:
httpGet: { path: /healthz, port: http }
{{- end }}
resources: {{- toYaml .Values.resources | nindent 12 }}Because Helm manipulates strings, not YAML nodes, a single wrong indent or a missing nindent produces structurally broken output that only fails when the API server rejects it. Get comfortable with helm template . and helm template . | kubeconform - to render and validate before you ship. The engine will happily generate invalid YAML; your job is to catch it.
values.yaml and value overrides
Every knob a chart exposes has a default in values.yaml. You reshape a release by overriding those defaults, and the override order is the whole trick to multi-environment config: base values.yaml is lowest priority, then each -f values-<env>.yaml you pass (left to right), and finally --set flags on the command line win over everything. This lets one chart serve every environment — a shared base of values plus a thin per-environment file that changes only what differs.
# base defaults live in the chart; prod overrides only what changes
$ cat values-prod.yaml
replicaCount: 6
logLevel: info
image: { tag: "1.4.3" }
resources:
requests: { cpu: 250m, memory: 256Mi }
# install/upgrade the prod release: later -f wins, --set beats files
$ helm upgrade --install checkout ./charts/checkout \
--namespace checkout --create-namespace \
-f values.yaml -f values-prod.yaml \
--set image.tag=1.4.4 # hotfix pin, overrides the fileReleases & release history
Installing a chart creates a release: a named, running instance of the chart in a namespace. Helm 3 removed the old in-cluster Tiller component; it now stores each release’s rendered state as a Kubernetes Secret in the release’s namespace, one per revision. That history is what makes Helm feel like a package manager: helm history checkout lists every upgrade, and helm rollback checkout 3 re-applies a previous revision’s manifests. Note the mental model — a “release” is Helm’s own record of what it applied, which is why running Helm imperatively and running it through a GitOps engine behave differently (more on that in the GitOps section).
Repositories, OCI charts, dependencies & hooks
Charts are shared through repositories: classically an HTTP server with an index.yaml, and — since Helm 3.8 — OCI registries, so a chart is pushed and pulled from the same registry as your container images (helm push / oci://). Charts compose through dependencies declared in Chart.yaml (pulled into charts/ with helm dependency update, and toggled with conditions/tags), so an umbrella chart can bundle a database subchart. Finally, hooks — annotated helm.sh/hook resources — run at lifecycle points (pre-install, post-upgrade, pre-delete, test, …) to do things like run a schema migration Job before the new version rolls. Hooks are powerful and a little dangerous: they run outside the normal reconciliation model, so a GitOps engine may treat them differently than helm install does.
| Helm — where it shines | Helm — where it bites |
|---|---|
| Huge ecosystem of ready-made third-party charts | Text templating is blind to YAML structure |
| Real packaging: versioned, distributable, OCI-native | Values sprawl; deep charts become hard to reason about |
| One command installs complex, multi-object apps | No static types — errors surface at render/apply time |
Built-in revisions, history & rollback | Whitespace bugs and {{ }} soup hurt readability |
Kustomize — template-free overlays
☺ Like you’re 10: Instead of a template with knobs, Kustomize keeps real YAML and lays little “change this bit” patches on top.
Kustomize takes the opposite philosophy to Helm: no templating at all. You keep plain, valid Kubernetes YAML and describe changes as structured patches layered on top. Because you are always editing real YAML nodes rather than rendering strings, the output is valid by construction and every environment diffs cleanly against its base. Kustomize is built right into kubectl (kubectl apply -k) and also ships as a standalone CLI, which is why it’s the default reach for per-environment variation without adopting a package manager.
Bases & overlays
The core pattern is a base (the shared, environment-neutral manifests plus a kustomization.yaml that lists them) and one overlay per environment (a kustomization.yaml that references the base and applies patches). Promoting dev → staging → prod becomes a small, reviewable diff in one overlay, and there is exactly one copy of everything shared.
# base/kustomization.yaml
resources:
- deployment.yaml
- service.yaml
commonLabels: { app.kubernetes.io/name: checkout }
---
# overlays/prod/kustomization.yaml
resources:
- ../../base
replicas:
- name: checkout
count: 6
images:
- name: acme/checkout
newTag: "1.4.3"
patches:
- path: resources-patch.yaml # bump requests/limits for prod
target: { kind: Deployment, name: checkout }Strategic-merge & JSON 6902 patches
Kustomize offers two patch styles. A strategic-merge patch is a partial manifest that Kustomize merges into the target using Kubernetes’ own merge semantics — it understands list merge keys, so patching one container in a list doesn’t clobber the others. A JSON patch (RFC 6902) is a precise list of operations (op: replace, add, remove at a path) for surgical edits where you need to target an exact position. Strategic merge reads naturally for “change these fields”; JSON patch wins for “remove this one element” or fields with no merge key.
# strategic-merge: looks like the object, merges by field
# overlays/prod/resources-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata: { name: checkout }
spec:
template:
spec:
containers:
- name: checkout # matched by name, others untouched
resources:
requests: { cpu: 250m, memory: 256Mi }
---
# JSON 6902: surgical ops by path (inline form)
patches:
- target: { kind: Deployment, name: checkout }
patch: |-
- op: replace
path: /spec/template/spec/containers/0/env/0/value
value: infoGenerators & the “no templating” philosophy
Kustomize generates some resources rather than templating them. configMapGenerator and secretGenerator build ConfigMaps/Secrets from literals or files and — crucially — append a content hash suffix to the name, so changing a config value renames the object and forces a rolling update of anything that mounts it (no more “I changed the ConfigMap but the pods kept the old value”). Add name prefixes/suffixes, common labels/annotations, namespace, and image-tag overrides, and you can reshape a base substantially without ever writing a template. The philosophy is deliberate: parameters and loops are refused on purpose, so the config stays plain YAML you can read and Git can diff.
Components
A Component is Kustomize’s answer to “optional, reusable features.” It is a kustomization fragment (kind Component) that an overlay can opt into — say, an observability component that adds a sidecar and a ServiceMonitor, or an ha component that adds a PodDisruptionBudget and anti-affinity. Overlays list the components they want, so instead of copying the same patch into three environments you define it once and switch it on where needed. Components are how Kustomize recovers a little of the “compose features” power that its no-parameters stance otherwise gives up.
| Dimension | Helm | Kustomize |
|---|---|---|
| Mechanism | Text templates + values | Structured patches over real YAML |
| New language? | Go templates + Sprig | None — just YAML |
| Output validity | Can render invalid YAML | Valid by construction |
| Distribution | Packages via repos/OCI | Not a package manager |
| Logic (loops/conditionals) | Yes | No (by design) |
| Best at | Shipping & installing third-party apps | Per-environment overlays of your own apps |
Helm and Kustomize are not really rivals — they answer different questions. Helm answers “how do I package and distribute an app with knobs?” Kustomize answers “how do I vary my own manifests across environments without a template language?” Plenty of teams run both: install third-party software with Helm, and overlay their own services with Kustomize.
Programmatic & typed configuration
☺ Like you’re 10: Instead of stitching text together, these tools use a real language that can say “that’s not allowed” before you ship.
Helm templates strings and Kustomize patches YAML; both ultimately treat config as text or loosely-typed data. A newer family says: describe config in a real language with real types and schemas, so the tool can catch “that’s not a valid Deployment” or “replicas can’t be negative” at compile time — long before the API server ever sees it. The shift is from string templating to typed generation, and it trades a steeper learning curve for far stronger guarantees.
jsonnet & the data-templating languages
Jsonnet is a small, purpose-built data templating language — a superset of JSON with variables, functions, imports, and object composition. You write functions that return Kubernetes objects and compose them, so shared structure lives in one function and environments differ by argument. Grafana’s Tanka wraps jsonnet for Kubernetes, adding libraries and an apply workflow. Jsonnet is a big step up in expressiveness from templating, especially for large, highly-repetitive config (think fleets of dashboards or alerting rules), though it is still dynamically typed — it catches logic errors, not schema errors.
// checkout.jsonnet — a function returns a Deployment; envs pass args
local deployment(env, replicas, tag) = {
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: { name: 'checkout', labels: { env: env } },
spec: {
replicas: replicas,
template: { spec: { containers: [{
name: 'checkout',
image: 'acme/checkout:' + tag,
env: [{ name: 'LOG_LEVEL', value: if env == 'prod' then 'info' else 'debug' }],
}] } },
},
};
{
dev: deployment('dev', 1, '1.4.2'),
prod: deployment('prod', 6, '1.4.3'), // one function, two environments
}cdk8s & Pulumi — general-purpose languages
Why learn a config language at all when you already know TypeScript, Python, or Go? cdk8s (a CNCF project) lets you define resources as objects in a real programming language and synthesises plain Kubernetes YAML from them. You get everything a language offers — loops, functions, unit tests, IDE autocomplete, and imported types generated from CRD schemas so a wrong field is a compile error. Pulumi takes the same “real language” idea further into infrastructure-as-code: it manages Kubernetes objects and cloud resources together with a state backend, which places it closer to IaC & control planes than to pure config. The common thread: your config gets to reuse your language’s tooling and testing.
// cdk8s (TypeScript): typed constructs synth to plain YAML
import { App, Chart } from 'cdk8s';
import { KubeDeployment } from './imports/k8s'; // types from the k8s schema
for (const [env, replicas] of [['dev', 1], ['prod', 6]] as const) {
const chart = new Chart(app, env);
new KubeDeployment(chart, 'checkout', {
spec: {
replicas, // number — a string won't compile
template: { spec: { containers: [{ name: 'checkout', image: 'acme/checkout:1.4.3' }] } },
},
});
}KCL, CUE & Timoni — typed config with schemas
Between jsonnet and full programming sit dedicated typed configuration languages. KCL (a CNCF sandbox project) is a config language with static types, schema definitions, and constraints — you can declare that replicas >= 1 and the compiler enforces it. CUE takes a striking approach: types and values are the same thing, unified together, so a schema and the data validate each other automatically and conflicts are compile errors. Timoni builds a Helm-like package manager on top of CUE — modules are distributed as OCI artifacts and applied with server-side apply, giving you Helm’s packaging story with CUE’s validation instead of text templates. This is where the field is heading: config that can’t be structurally wrong because a schema proves it first.
| Tool | Language / model | Typing & validation | Sweet spot |
|---|---|---|---|
| jsonnet (+ Tanka) | Purpose-built data-templating | Dynamic; logic errors only | Large, repetitive config (dashboards, rules) |
| cdk8s | TS / Python / Go / Java | Language types + imported CRD types | Teams who’d rather write code & tests |
| KCL | Dedicated config language | Static types + constraints | Typed, validated config at scale |
| CUE / Timoni | CUE unification (Timoni packages it) | Schema = data; strong validation | Typed packaging as a Helm alternative |
| Pulumi | General-purpose IaC languages | Language types + provider schemas | Apps + cloud infra in one program |
Choosing an approach
☺ Like you’re 10: Pick the tool that matches your team and your problem — there’s no single “best,” just “best for you right now.”
There is no universally correct choice; there is a choice that fits your team, your software, and your risk appetite. The mistake is adopting a tool because it’s fashionable rather than because it answers a question you actually have. Three questions settle most decisions.
The three questions
1. What are your team’s skills? A team fluent in Go or TypeScript may be far happier (and make fewer mistakes) with cdk8s than with Go-template whitespace rules; a YAML-only team gets huge mileage from Kustomize with nothing new to learn. 2. How much third-party software do you install? If you deploy a lot of off-the-shelf components, Helm is almost unavoidable because that’s how vendors ship — so the real question is what you use for your own apps on top. 3. How much do you need validation and safety? A regulated platform where a bad manifest is expensive leans toward typed tools (KCL, CUE/Timoni, cdk8s) that reject errors at compile time; a small team optimising for readability leans toward Kustomize.
A decision table
| If you value… | Reach for | Because |
|---|---|---|
| Installing third-party apps fast | Helm | The ecosystem ships as charts; one command, versioned & rollback-able |
| Readable per-env overlays of your own apps | Kustomize | No new language; valid-by-construction; clean Git diffs |
| DRY across huge, repetitive config | jsonnet | Functions & composition without object explosion |
| Compile-time types & tests in a real language | cdk8s | Reuse your language’s tooling; typed CRDs catch errors early |
| Schema-validated config that can’t be wrong | KCL / CUE + Timoni | Constraints & unification reject bad config before apply |
| One program for apps and cloud infra | Pulumi | Kubernetes and cloud resources share a state & language |
Combining tools
The choices aren’t mutually exclusive, and the strongest platforms mix them. A common and pragmatic pattern is Helm for packaging, Kustomize for per-environment overrides: render a vendor chart to plain YAML and then post-process it with a Kustomize overlay, so you get the ecosystem and clean env diffs. Kustomize can even invoke Helm as a chart inflator; Argo CD and Flux both support “Helm then Kustomize” pipelines directly. The spectrum below is the mental map — from format to real language — and “where should this platform sit?” is a deliberate decision, not a default.
Configuration in a GitOps world
☺ Like you’re 10: Whatever tool you use, a robot inside the cluster turns it into plain YAML and keeps the cluster matching it.
None of these tools deploy anything by themselves in a modern platform — they feed a GitOps reconciler. The reconciler’s job is to take whatever’s in Git, render it to plain Kubernetes manifests, and continuously drive the cluster to match. So the practical question becomes: how does Argo CD or Flux turn your Helm chart, Kustomize overlay, or jsonnet into the objects it applies?
How Argo CD and Flux render your config
Argo CD renders in its repo-server: it natively runs kustomize build, helm template (note: template, not helm install — Argo renders the chart to manifests and manages state itself, so there’s no Helm release secret), and jsonnet, and it handles anything else through a Config Management Plugin (which is how teams wire in KCL, cdk8s, or Timoni). Flux splits the work across controllers: kustomize-controller runs kustomize build (decrypting SOPS secrets on the way), while helm-controller performs an actual Helm release from a HelmRelease resource. That difference matters — Argo flattens Helm to manifests; Flux keeps a real Helm release — and it changes how hooks and release history behave.
The “rendered manifests” pattern
A powerful discipline is to stop letting the reconciler render at apply time and instead render in CI. In the rendered manifests pattern, a pipeline runs helm template / kustomize build and commits the resulting plain YAML to a rendered branch or directory; the reconciler then applies dumb, fully-expanded manifests. The payoff is enormous for reviewability: a pull request shows the exact objects that will hit the cluster — no “what will this template actually produce?” guesswork — so diffs are honest, rollbacks are trivial (revert to the previous rendered commit), and your GitOps engine is decoupled from whichever templating tool you happen to use. The cost is an extra CI step and a busier Git history.
# CI renders each environment to plain YAML and commits it — # the reconciler then applies fully-expanded manifests, no surprises. helm template checkout ./charts/checkout -f values-prod.yaml \ | kubeconform -strict - # validate before committing helm template checkout ./charts/checkout -f values-prod.yaml \ > rendered/prod/checkout.yaml kustomize build overlays/prod > rendered/prod/all.yaml git add rendered/ && git commit -m "render: prod checkout 1.4.3"
Per-environment overlays & promotion
In a GitOps repo, environments are usually directories (an overlay per env) rather than long-lived branches, because directories diff and promote more cleanly and avoid merge drift between environments. Promotion — moving a tested version from dev to staging to prod — becomes a tiny, auditable change: bump an image tag or a values file in the next environment’s overlay, open a PR, merge, and the reconciler rolls it out. Because every promotion is a commit, you get a complete history of “what went where, when, and who approved it,” which is exactly the audit trail security and operating-model reviews want. This is where configuration management and GitOps become one workflow rather than two.
Workload abstractions above config
☺ Like you’re 10: The best trick is to let a developer say what they want — “an app with a database” — and let the platform figure out the YAML.
Every tool so far still asks a developer to think in Kubernetes objects. The frontier of platform engineering is to raise the abstraction one more level: let a developer describe a workload — “a container that needs 2 CPUs, a Postgres database, and a public route” — once, in platform-neutral terms, and let the platform translate that into whatever Helm/Kustomize/CRD soup the target environment needs. This is where configuration meets self-service and platform APIs.
Score — describe a workload once
Score (a CNCF sandbox project) is an open, platform-agnostic workload specification. A developer writes a single score.yaml describing their app’s containers, resource needs, service ports, and abstract dependencies (a database, a DNS route), with no Kubernetes-specific detail. A “Score implementation” — score-compose for local Docker Compose, score-k8s or score-helm for the cluster — translates that one file into the right config for each target. The developer writes the same spec whether they’re running locally or shipping to prod; the platform owns the translation.
# score.yaml — one platform-neutral description of a workload
apiVersion: score.dev/v1b1
metadata: { name: checkout }
containers:
checkout:
image: acme/checkout:1.4.3
variables:
DB_URL: "${resources.db.host}:${resources.db.port}" # abstract, resolved by the platform
resources:
db:
type: postgres # "I need a Postgres" — not "here is a StatefulSet"
route:
type: route # "expose me" — the platform decides Ingress vs Gateway“I don’t want to learn Helm and Kustomize and which ingress this cluster uses. I want to say ‘here’s my container, it needs a database and a URL,’ and get exactly that — the same file on my laptop and in prod. When the platform hides the templating from me, I ship features instead of fighting YAML. That’s the whole point of a golden path.”
The Open Application Model & KubeVela
OAM (the Open Application Model) is a specification that splits an application into components (the workloads) and traits (operational features like autoscaling, ingress, or a sidecar), with a clean division of labour: app developers assemble components and traits, while the platform team defines what those components and traits mean. KubeVela is OAM’s engine on Kubernetes: platform teams write ComponentDefinition and TraitDefinition objects (using CUE templates under the hood), and a developer’s Application resource references them by name. KubeVela renders and delivers the underlying objects — so the platform team encodes the golden path once, and developers compose from a menu of approved building blocks.
# A KubeVela Application: developer composes approved components + traits
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata: { name: checkout }
spec:
components:
- name: checkout
type: webservice # a component the platform team defined
properties: { image: acme/checkout:1.4.3, port: 8080 }
traits:
- type: scaler # a trait: platform decides how it's realised
properties: { replicas: 6 }
- type: gateway
properties: { domain: checkout.acme.com }Where this meets self-service
These abstractions are the top layer of the same stack this whole page has been climbing. Raw YAML is the atoms; Helm and Kustomize package and vary the atoms; typed tools validate them; GitOps reconciles them; and Score/OAM/KubeVela let a developer request an outcome while the platform decides which of the lower tools produce it. That translation — a friendly request in, a validated set of manifests out — is exactly what a self-service platform sells, often surfaced through a portal and backed by custom resources and operators. Configuration management is not one tool; it’s the whole ladder from a hand-written Deployment to a one-line workload request.
Take one Deployment + Service and express it three ways on a throwaway cluster. (1) Raw YAML in dev/ and prod/ folders — feel the duplication. (2) A Kustomize base/ plus a prod overlay that changes only replicas, image tag, and log level — run kustomize build overlays/prod and diff it. (3) A minimal Helm chart with a values-prod.yaml — run helm template . -f values-prod.yaml and compare. Now run each render through kubeconform -strict. You’ll feel exactly where raw YAML hurts, why Kustomize diffs so cleanly, and why Helm’s power comes with whitespace peril.
Foxy: Wait — if Helm, Kustomize, and KCL all just make YAML in the end, why not pick one and ban the others?
Benny: Because they answer different questions. Helm ships other people’s apps; Kustomize varies ours across environments; typed tools prove config is valid before it ever applies. Right tool, right job.
Gizmo: Ugh, so much ceremony. Just copy the dev folder to a prod folder, tweak a couple numbers, and ship. Who’s gonna maintain it — future you? 🤑
Timmy: Future us, at 2am, when the two folders have silently drifted and prod is missing a probe. One base, small overlays, and a schema check — that’s how we go fast safely.
Mira: And the developer shouldn’t see any of it. Give me one score.yaml — “a container that needs a database and a route” — and I’ll hide Helm, Kustomize, and the ingress choice behind a single button.
Dot: Yes. Same file on my laptop and in prod, and I never learn what a strategic-merge patch is. That’s the platform I actually want.
Configuration, templating, and packaging are the quiet machinery under every deploy on your platform — the layer that turns one intention into many correct, validated, reconciled manifests. Master it and everything above it, from GitOps to self-service golden paths, gets simpler; skip it and you inherit a swamp of drifting copies. Next, see how these renders ride the delivery machinery in CI/CD & Progressive Delivery, and how the platform’s own infrastructure gets the same treatment in IaC & Control Planes.
1. Name the three specific ways raw YAML breaks down across many environments. 2. In one sentence each, what different question do Helm and Kustomize answer? 3. Why is Kustomize output “valid by construction” while Helm can render invalid YAML? 4. What does the “rendered manifests” pattern give you that render-at-apply-time doesn’t? 5. What problem do Score and KubeVela solve that Helm and Kustomize don’t?
Check your answers
- Duplication (near-identical copies kept in sync by hand), no parameters or logic (YAML is data — no variables, conditionals, or loops), and no validation or types (typos and wrong types aren’t caught before apply).
- Helm answers “how do I package and distribute an app with configurable knobs?”; Kustomize answers “how do I vary my own manifests across environments without a template language?”
- Kustomize patches structured YAML nodes (strategic-merge / JSON 6902), so it edits real objects and always emits valid YAML; Helm templates text, so a bad indent or missing
nindentcan produce structurally broken output. - It commits fully-expanded plain manifests, so a pull request shows the exact objects that will hit the cluster — honest diffs, trivial rollback, and the GitOps engine decoupled from the templating tool.
- They raise the abstraction above Kubernetes objects: a developer describes a workload once (containers + abstract dependencies), and the platform translates it into the right config — the bridge to self-service.