Skip to content
Go back

Kustomize vs Jsonnet for K8s Manifests

By SumGuy 9 min read
Kustomize vs Jsonnet for K8s Manifests

Intro hook

Kubernetes manifests are either “copy, paste, pray” or “write clever code and argue about it at standup.” Kustomize is the clipboard and tape approach that actually survives a 2 AM crisis. Jsonnet is the clever engineer’s toolkit — real code, real power, and real potential for future-you to groan when the internals age out.

This isn’t a holy war. It’s about the right trade-offs: composition vs programmability, YAML-friendly workflows vs code-driven generation, ease-of-change vs long-term comprehension.

What Kustomize is

Kustomize treats YAML like paper you can layer and patch. You define a base set of manifests, then create overlays that apply patches or additions. No templating language, no loops, no functions — just declarative composition.

Kustomize’s philosophy is “patch, don’t templatize.” You can keep your manifests human-readable YAML and let kustomize build variants (dev, staging, prod) by applying overlays. It’s especially friendly when your team already thinks in YAML and you want minimal cognitive load.

What Jsonnet is

Jsonnet is a real programming language for JSON (and by extension YAML through conversion). It has imports, functions, conditionals, loops, and libraries. You’re not writing templates — you’re writing code that emits configuration.

That gives you power: reuse via functions and libs, DRY patterns that actually scale, and the ability to generate complex structures programmatically. The downside: it’s code. It needs code practices — reviews, tests, and someone who understands the indirection.

Kustomize in practice: base + overlay example

Kustomize uses a base + overlay pattern. Put shared resources in a base/ folder and environment-specific changes in overlays/dev/ and overlays/prod/.

Example repo layout:

sumguy-app/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml
└── overlays/
├── dev/
│ └── kustomization.yaml
└── prod/
└── kustomization.yaml

base/kustomization.yaml:

base/kustomization.yaml
resources:
- deployment.yaml
- service.yaml
configMapGenerator:
- name: app-config
literals:
- LOG_LEVEL=info
commonLabels:
app: sumguy-app

A strategic merge patch example (overlay adds replica count and image tag):

overlays/prod/kustomization.yaml
resources:
- ../../base
patchesStrategicMerge:
- deployment-patch.yaml
overlays/prod/deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: sumguy-app
spec:
replicas: 5
template:
spec:
containers:
- name: sumguy-app
image: ghcr.io/kingpin/sumguy-app:stable

Kustomize also supports JSON6902 patches (you can patch by JSON patch ops), components (reusable kustomize pieces), and generators like configMapGenerator and secretGenerator to create ConfigMaps/Secrets from literals or files.

Generators are convenient but remember: they create resources at build-time (not runtime), and the output is just YAML — keep them in your kustomization files, not in random scripts.

Jsonnet in practice: function generating a Deployment

Jsonnet lets you write a function that returns a Deployment, then call it with different parameters for dev/prod. Save reusable snippets as libsonnet files and import them.

Example layout:

sumguy-app-jsonnet/
├── main.jsonnet
├── lib/
│ └── deployment.libsonnet
└── vendor/

lib/deployment.libsonnet:

lib/deployment.libsonnet
local k = import 'k.libsonnet';
// deployment(name, image, replicas)
{
deployment(name, image, replicas):: {
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: { name: name },
spec: {
replicas: replicas,
selector: { matchLabels: { app: name } },
template: {
metadata: { labels: { app: name } },
spec: {
containers: [
{
name: name,
image: image,
ports: [{ containerPort: 8080 }],
}
]
}
}
}
}
}

main.jsonnet:

main.jsonnet
local dep = import 'lib/deployment.libsonnet';
{
dev:: dep.deployment('sumguy-app', 'ghcr.io/kingpin/sumguy-app:canary', 1),
prod:: dep.deployment('sumguy-app', 'ghcr.io/kingpin/sumguy-app:stable', 5),
}

Render with the jsonnet CLI:

Terminal window
jsonnet -J vendor -o manifests.json main.jsonnet
# or to get YAML, pipe through yq or use jsonnetfmt then convert
jsonnet -J vendor main.jsonnet | yq -P e - > manifests.yaml

Jsonnet libraries (like the Kubernetes k.libsonnet) help encode common K8s patterns so you don’t hand-roll every field.

The history: ksonnet’s death, Tanka as “Jsonnet for grown-ups”

Ksonnet tried to be “Jsonnet for Kubernetes” and failed spectacularly — it mixed CLI, env concepts, and opinionated patterns, then the community quietly moved on. Ksonnet’s death taught a lesson: building a platform around a DSL is harder than the DSL itself.

Tanka (by Grafana Labs) picked up the torch as a more practical Jsonnet-based approach for Kubernetes. It focuses on environments, clear structuring, and integrates better with GitOps patterns. Think of Tanka as Jsonnet with seat belts: same power, less chance of derailing a cluster when someone writes a dubious loop.

Meanwhile Kustomize was adopted upstream by kubectl (kubectl apply -k) and became the low-friction choice for many teams.

Debugging UX: kustomize edit, jsonnet -e, readability

Kustomize debugging is straightforward: kustomize build overlays/prod spits YAML. If something’s wrong, the YAML is still plain and patchable. You can use kubectl apply -k and kustomize edit helpers to add resources.

Jsonnet debugging is code debugging. jsonnet -e 'import "main.jsonnet"' or jsonnet main.jsonnet produces JSON — then convert to YAML if needed. Errors point to locations in .jsonnet files which can be cryptic if you rely heavily on imports and eval-time indirection.

Readability-wise, Kustomize wins for ops folks who prefer to read YAML. Jsonnet wins for code maintainers who like functions and abstractions. The trade is obvious: Jsonnet is more expressive but can hide what the final YAML looks like until you run it.

The abstraction cost: when Jsonnet is too clever

There’s a slippery slope. Start with a tiny function to avoid repetition, then someone writes a factory that reads a couple dozen parameters and performs stringly-typed magic. Six months later, new hires face a main.jsonnet that looks like a feature-complete application, and they spend a day reverse-engineering why the Deployment has an unexpected label.

Abstraction costs are real:

Rule of thumb: use Jsonnet when you need programmatic generation that saves you real effort. Don’t use it to avoid typing three fields in a single deployment manifest.

Testing/linting story for each

Kustomize:

Jsonnet:

Both require a render-and-validate CI step. The difference is Jsonnet projects often need unit tests for library functions if you go deep into logic.

The Helm + Kustomize post-render pattern

Helm templates are powerful and many teams standardize on Helm charts. Kustomize can be used as a post-renderer for Helm — render a Helm chart, then run kustomize overlays against the result to inject tenancy-specific patches or apply company-wide labels.

Pattern:

  1. Helm template → YAML
  2. Kustomize post-render → overlays applied

This gives you Helm’s packaging and values file ergonomics and Kustomize’s patch-based overlays for cluster-specific changes. It’s useful when you want the best of both: chart reuse and non-invasive per-cluster tweaks.

Helm has a --post-renderer flag and there are community helpers to chain these together. Use carefully: the complexity stack grows quickly.

Argo CD / Flux compatibility

Both Kustomize and Jsonnet fit well into GitOps tooling.

Argo CD:

Flux:

If your GitOps pipeline needs to be declarative and hands-off, Kustomize is the path of least resistance. Jsonnet requires a bit more wiring but integrates fine once set up.

Decision matrix by use case

Here’s a tiny cheat-sheet:

Use Kustomize: small-medium apps, ops-first, GitOps-native.
Use Jsonnet: large fleets, generated resources, heavy reuse, when "code" wins.

SumGuy-voice conclusion picking a winner

If you want something that survives a 2 AM panic without invoking a developer, Kustomize is the sensible forklift: it lifts YAML into place, doesn’t try to be clever, and your on-call self will thank you. It’s the clipboard and duct tape that still runs the cluster.

If you’re building a factory of resources where patterns repeat and human typing becomes a liability, Jsonnet (or Tanka if you need structure) is a precision lathe: it carves exact parts and pays off over time — until it doesn’t, and then you need tests and discipline.

So: for most teams, start with Kustomize. If you hit real pain from repetition or need the expressive power, move to Jsonnet/Tanka — but treat it like code: lint, test, document, and don’t let one clever refactor become the thing nobody can change.

Winner: Kustomize for ops-first reliability; Jsonnet for power users who accept the maintenance bill.

Happy patching. Don’t be the person who writes a Jsonnet macro at 11:45 PM and disappears from Slack until Monday.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
iperf3 + nload: Network Diagnosis

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts