Kubernetes Manifest Splitter

Paste a multi-document Kubernetes YAML bundle and get it back split per resource — each one named from a filename template, with the document bodies left byte-for-byte intact. Nothing leaves your browser.

Try:
Split manifest

About this tool

helm template, kustomize build and kubectl get -o yaml all hand you one enormous YAML file with dozens of resources jammed together between --- lines. That single stream is awkward to review, impossible to diff usefully, and not the layout you want in Git. This tool takes that stream apart: one resource at a time, each with a filename derived from what the resource actually is.

The split is textual on purpose. Documents are separated on column-0 --- and ... markers, which is exactly what the YAML spec says starts a document, and each body is then carried through byte for byte. Your comments, key ordering, blank lines, anchors and long block scalars come out the way you wrote them — nothing is re-serialised, re-indented or alphabetised behind your back. YAML is parsed only to read apiVersion, kind and metadata, so the filename can be built from them. It all runs as WebAssembly inside this page, so a manifest full of secrets never leaves the browser tab.

A worked example

Paste a two-resource chart render and leave every option alone:

---
# Source: chart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: prod
spec:
  ports:
    - port: 80
---
# Source: chart/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: prod
spec:
  replicas: 2

You get back:

# ===== service-web.yaml =====
# Source: chart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: prod
spec:
  ports:
    - port: 80

# ===== deployment-web.yaml =====
# Source: chart/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: prod
spec:
  replicas: 2

Each # ===== … ===== line is the filename that resource would be written to, and the Helm # Source: comments survived, because the body was never touched. Switch What to render to Index and the same paste becomes an inventory instead:

#  KIND        NAME  NAMESPACE  APIVERSION  LINES  FILE
1  Service     web   prod       v1          9      service-web.yaml
2  Deployment  web   prod       apps/v1     8      deployment-web.yaml

2 documents, 2 kinds

Naming the files

The filename template defaults to {kind}-{name}.yaml, giving deployment-web.yaml. The placeholders are {kind} (lowercased), {Kind} (as written), {name}, {namespace} (default when the resource has none), {apiVersion}, {group} (core for the core group), {version} and {index} — a 1-based, zero-padded counter, which is how you keep the apply order visible in a directory listing: {index}-{kind}-{name}.yaml.

Anything else with a dot in it is read straight out of the document as a field path, so {metadata.labels.app}-{kind}.yaml names files after your app label and {spec.template.spec.containers.0.image} reaches into a list by position. A path that the document does not have, or one that lands on a whole block rather than a single value, is reported rather than silently blanked.

A / in the template makes directories: {namespace}/{kind}-{name}.yaml gives you prod/service-web.yaml. Characters that do not belong in a filename are replaced with -, and if two resources still end up with the same name the second becomes …-2.yaml, the third …-3.yaml, so nothing is ever overwritten.

Five ways to take the output

Filtering and ordering

Keep only these and Drop these take comma-separated selectors. A selector is Kind or Kind/name, case-insensitive, with * and ? wildcards: Deployment,StatefulSet keeps two kinds, Service/web-* keeps services whose name starts with web-, and */*-canary matches by name across every kind. Excludes are applied after includes, so keeping Deployment while dropping */*-canary is a perfectly normal combination.

Output order is document by default — exactly as pasted, which is what you want when the bundle is already in a deliberate order. By kind and by name sort alphabetically. Apply order is the useful one: it ranks by a dependency-safe install sequence — namespaces, quotas and policies, then service accounts, secrets and config maps, then storage, CRDs and RBAC, then services, then workloads, with Ingress and APIService last — so applying the files in the listed order does not fail on a resource that does not exist yet.

Limits and edge cases

FAQ

Will my comments and formatting survive the split?

Yes. Document bodies are copied through byte for byte — comments (including the # Source: chart/templates/… lines Helm adds), key order, indentation style, anchors and block scalars all come out exactly as they went in. The YAML is parsed only to read apiVersion, kind and metadata for the filename, and that parse never feeds back into the output. The single exception is a kind: List wrapper when list expansion is on: its items have to be re-serialised to become standalone documents, so comments inside a List are lost. Untick Expand kind: List wrappers into their items if you need that wrapper intact.

How do I actually get the files onto disk?

Choose the Shell script output and save it as split.sh, then sh split.sh in an empty directory — it writes each resource with a heredoc and creates any directories your filename template implies. If you would rather not run a generated script, use the Files output and copy each block under its # ===== filename ===== header, or use the JSON output and let your own script write content to file. The same split is available offline in the command-line tool, which is the better route when the manifest is already a file on disk.

Can I name the files after a label instead of the kind?

Yes — any placeholder containing a dot is read as a field path into the document itself, so {metadata.labels.app}/{kind}-{name}.yaml groups the output into one directory per app label. Numeric steps index into lists, which is how {spec.template.spec.containers.0.image} gets the first container's image. If a document is missing the path you asked for, the run stops and names the placeholder rather than writing a file with a hole in the name — usually the sign that one resource in the bundle is missing the label you assumed everything had.

Why are two of my resources sharing a filename?

They are not — the second one gets a -2 before the extension, the third a -3, and so on. It happens when the template does not include something that actually distinguishes the two resources: the same ConfigMap name in two namespaces collides under {kind}-{name}.yaml because the namespace is nowhere in the name. Add it: {namespace}-{kind}-{name}.yaml, or {namespace}/{kind}-{name}.yaml to put each namespace in its own directory. Use the Index output to see every assigned filename at a glance.

What does apply order actually sort by?

A fixed dependency-safe sequence, not alphabet: Namespace first, then policies and quotas, then ServiceAccount, Secret and ConfigMap, then storage classes and volumes, then CustomResourceDefinition and the RBAC kinds, then Service, then the workload kinds (DaemonSet, Deployment, StatefulSet, Job, CronJob), with Ingress and APIService last. Kinds it does not know — your own custom resources — sort after the known ones, which is normally right, because a custom resource usually needs its CRD and its operator to exist first. Combine it with an {index}- prefix in the filename template and apply -f on the directory replays that order.

Does it validate the manifests?

No, and deliberately so. Each document must be parseable YAML — a syntax error is reported with the number of the document it appears in — but nothing checks that apiVersion exists, that the kind is real, or that required fields are present. A bundle you cannot apply will split perfectly happily into files you cannot apply. Keep kubectl apply --dry-run=server or a schema linter in the loop for that; this tool's job is to preserve exactly what you gave it while reorganising where it lives.

Is anything uploaded?

No. The split is a WebAssembly module running inside this page, so the manifest stays in the browser tab — which matters, since a rendered chart routinely carries Secret resources with real credentials in them. Load the page, disconnect from the network, and it still works.

Developer & Automation Access

Run it from the terminal

Same engine as this page, headless — via the gizza CLI:

gizza tool k8s-manifest-splitter "apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  ports:
    - port: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2"

New to the CLI? Get gizza →

Open it by URL

Pre-fill and auto-run this tool with query parameters — the names match the API/CLI:

https://gizza.ai/tools/k8s-manifest-splitter/?manifest=apiVersion%3A%20v1%0Akind%3A%20Service%0Ametadata%3A%0A%20%20name%3A%20web%0Aspec%3A%0A%20%20ports%3A%0A%20%20%20%20-%20port%3A%2080%0A---%0AapiVersion%3A%20apps%2Fv1%0Akind%3A%20Deployment%0Ametadata%3A%0A%20%20name%3A%20web%0Aspec%3A%0A%20%20replicas%3A%202&output=files&filename_template=%7Bkind%7D-%7Bname%7D.yaml&include=Deployment%2CService%2Fweb-%2A&exclude=Secret%2C%2A%2F%2A-test&sort=document&skip_non_k8s=true&expand_lists=true&include_triple_dash=true

Machine-readable descriptor: tool.json — title + parameters JSON Schema for agents.