helm-viz logo
helm-viz Helm charts, drawn
Ingress api.example.com Service ClusterIP · http → 8080 Deployment app=api · 3 replicas Secret not created by this chart

One binary · No cluster · Runs on your machine

See a Helm chart as infrastructure

helm template answers "what YAML does this produce". It does not answer "what does this chart actually build, and how is it wired together" — that question gets answered by scrolling a thousand lines of output and holding the selectors in your head. helm-viz renders the chart the same way helm does, then draws the result.

Single binary · UI embedded · Runs as a helm plugin · Signed and checksummed · Free for small teams

What it looks like

Real charts, rendered by helm and drawn by helm-viz. Click any shot to enlarge it.

The chart, drawn

The wordpress chart rendered with its own helm and laid out in tiers: the ingress routes to the service, the service selects the deployment, and the container, the config and the volume claim hang off it. The ingress is flagged amber because it serves without TLS. The dock carries helm lint and what these values assume about where the chart runs — here an aws-load-balancer-type annotation naming AWS, and eighteen decisions left to whatever cluster it lands on.

What it shows

Every view is the same chart, rendered by your own helm, asked a different question.

Infrastructure

What the chart creates and how traffic reaches it. Ingresses route to services, services select workloads by label, workloads mount config and claim storage, autoscalers scale them. Anything the chart references but never creates — a secret that must already exist — is drawn dashed, because that is the thing that breaks an install at 2am.

Includes

Which template files define which named templates, and who calls whom. Built from the template AST, so it survives nested conditionals, pipelines and }} inside quoted strings — and it still works on a chart helm refuses to render, with helm’s own error carried on the file that failed to parse.

Wiring

What the chart gets wrong about itself. A Service whose named targetPort no selected container declares, an Ingress routing to a port its Service does not expose, an env var reading a key absent from a ConfigMap this chart writes, a mount naming a volume the pod never defines, a probe on a port the container did not declare. The broken link is drawn in red and the object badged.

API versions

Objects written against an API the target Kubernetes no longer serves. helm lint warns the same way whatever cluster you are heading for; here the target decides. On 1.24 a batch/v1beta1 CronJob is a warning, because it still installs. On 1.25 it is an error, because it does not.

Environment

What the chart assumes about where it runs, read off the values it was rendered with. Not "which cloud is this for" — the answerable question is which environment decisions the chart offers and which these values have made: the storage class left empty, the ingress class nobody chose, loki’s s3/gcs/azure branches with one of them picked. Evidence, never a verdict.

Values

Which value produced which line, and what became of every value. Selecting an object lists the values that built it as a tree, each key showing the lines it produced. With nothing selected the dock carries the whole chart: every key sorted into the ones that reached the output, the ones a template reads but that produced nothing this time, the ones read but never set, and the ones nothing refers to.

Diff

--compare renders the chart a second time under another set of values and draws both at once: added, removed and changed objects marked in place, and an object whose name is built from a value shown under its new name with the old one struck through, because it moved rather than being replaced.

Chart lint

helm lint runs against the same chart and the same values, with its findings in the dock — so the output you already read is beside the diagram rather than in another terminal.

Selecting anything fills a resizable dock: every attribute, the connections in and out, and the source behind it — the exact YAML helm emitted, or the template text for a file or define — with syntax highlighting and line numbers. The chart is watched while the server runs, so editing a template redraws the diagram without a reload.

Against what you already run

helm-viz does not replace helm template or helm lint — it runs both and puts their output beside the diagram. What it adds is the question none of them are shaped to answer: whether the objects a chart produces actually point at each other. A one-character typo — targetPort: htttp — passes every column to its left and sends every request nowhere.

Capability helm template renders YAML helm lint chart conventions --dry-run=server validates on a cluster helm-viz
Renders the chart with your values the same helm, the same merge order ✓ ✓ ✓ ✓
Works with no cluster to talk to and no credentials ✓ ✓ ✕ ✓
Shows how the objects are wired together selectors, ports, mounts, refs — as a diagram ✕ ✕ ✕ ✓
Catches a typo’d named targetPort valid YAML, valid schema, sends every request nowhere ✕ ✕ ✕ ✓
Points an API removal at the template line that wrote it judged against the Kubernetes you name, ready to annotate a PR ✕ ~ ~ ✓
Says which value produced which line and which values produced nothing at all ✕ ✕ ✕ ✓
Diffs the chart under two sets of values renamed objects tracked, not shown as add + remove ✕ ✕ ✕ ✓
Works on a chart that will not render the includes graph is parsed from the templates ✕ ✕ ✕ ✓
Leaves an artifact you can hand to someone one self-contained HTML file, a PNG/SVG, or a draw.io or Mermaid diagram ~ ✕ ✕ ✓
✓full ~partial / with caveats ✕not offered

On API removals the caveat is the same one twice: helm lint warns the same way whatever cluster you are heading for, and --dry-run=server answers only for the cluster it can reach today. helm-viz takes the target as an argument, so the answer is about the upgrade you are planning.

In a pipeline

--check renders the chart, runs the wiring and API-version checks, and exits. Nothing is served. Only errors fail the gate — a removal that is still several releases away is reported as a warning and exits 0, because breaking a pipeline over it is how people learn to stop running the check.

helm-viz --check
$ helm-viz ./charts/api --check --kube-version 1.29
 
error charts/api/templates/service.yaml:11
Service/api port http targets "htttp", which no selected
container declares (declared: http)
 
error charts/api/templates/cronjob.yaml:2
CronJob/api-reaper uses batch/v1beta1, removed in 1.25
 
# exit 1 — the gate fails on errors only
chart.yml
# .github/workflows/chart.yml
- run: |
V=1.2.0 P=linux_amd64 F=helm-viz_${V}_$P
url=$(curl -sSf -X POST https://qfzh8zhy61.execute-api.ap-southeast-2.amazonaws.com/prod/helm-viz/download \
-H 'Content-Type: application/json' \
-d "{\"key\":\"$HELMVIZ_LICENSE\",\"version\":\"$V\"}" \
| jq -r .standalone.$P)
curl -sSfL -o $F.tar.gz "$url"
curl -sSL https://jcrstack.com/dl/helm-viz/v$V/SHA256SUMS | sha256sum -c --ignore-missing
tar xzf $F.tar.gz && sudo mv $F/helm-viz /usr/local/bin/
env:
HELMVIZ_LICENSE: ${{ secrets.HELMVIZ_LICENSE }}
- run: helm-viz ./charts/api --check --format=github
- run: helm-viz ./charts/api --export chart.html
- uses: actions/upload-artifact@v4
with: { path: chart.html }
 
# the annotation lands on the template,
# not on helm output
::error file=charts/api/templates/service.yaml,
line=11::Service/api port http targets "htttp",
which no selected container declares
0 Clean

Nothing found. The JSON document prints all the same, so nothing downstream special-cases success.

1 Problems found

At least one error. Warnings alone do not get here — they are reported and exit 0.

2 Could not tell

The chart would not render, or a flag could not be read. A chart that will not render is not a chart that is wrong.

The standalone archive is the simpler half in CI: the plugin form buys nothing where no one is going to type helm interactively, and one binary on the PATH is one less thing for a runner to get wrong. SHA256SUMS is published beside it, so the download can be checked in the same step it is fetched.

The exit codes and the --format=json document are a contract. The JSON carries a version that goes up when a field changes meaning or disappears, never when one is added. Every finding carries a category — wiring when the chart contradicts itself, apiVersion when it contradicts a Kubernetes release — so a consumer can act on one without the other. An unknown --format exits 2 rather than falling back to text, since a typo would otherwise read as a clean chart.

Something you can hand to someone else

A diagram that only exists while a server is running is a diagram nobody else sees. helm-viz writes the whole thing to one file — the UI, and the payload the live page would have fetched, folded in — so it opens from disk with no server and no network.

Page (HTML)

The graph, the dock, the values index, and the diff if one was asked for. 1.5MB for a small chart, 8MB for kube-prometheus-stack — half a megabyte gzipped either way, which is what an artifact upload costs. Point --export at a directory and it names the file itself, stamped so it sorts by when it was taken.

Image (PNG / SVG)

The graph as it stands — the current filter and highlight state, the whole laid-out graph rather than the visible viewport. PNG for pasting into a ticket, SVG for staying crisp at any size and opening in Figma. The cards are drawn as real SVG shapes rather than screenshotted, so the file is selectable text and a few tens of KB.

Secrets do not travel

Secret values are replaced with <REDACTED> in an exported file, in the YAML pane and in the diff alike. Key names stay, because the shape of a Secret is most of what the diagram is for. The live view is not redacted — that is your own chart on your own machine. The line is not whether something is sensitive but whether it leaves the machine.

Where it runs

It is one binary. The UI is embedded; there is no server to deploy, no container to run, and nothing leaves your machine. There is also no hosted version — helm-viz shells out to your helm and reads a chart directory off disk, which is not something a website can do for you.

Localhost only

The server binds to localhost. Ctrl+C shuts it down.

No cluster, no credentials

It renders a chart directory. Nothing is installed and no kubeconfig is read.

No account, no telemetry

There is nothing to sign in to and nothing phoning home.

Your helm, your render

helm-viz shells out to the helm on your PATH rather than linking the library, because your helm is the only thing that agrees with your helm about what a chart means.

When you reach for it

Four afternoons that go differently with a picture.

Onboarding

A chart you have never seen, and a page of it is conditionals

Someone leaves and their chart is yours. helm template gives you a thousand lines to scroll and a set of selectors to hold in your head. Open it in helm-viz and the wiring is the picture: which ingress reaches which service, which workload it selects, what it mounts, and which secrets the chart expects to already be in the namespace.

Infrastructure viewDashed external refsSource in the dock
Break-fix

targetPort: htttp — deployed, healthy, and serving nothing

A one-character typo in a named port passes helm lint and passes server-side validation, because it is valid YAML naming a port that is simply never declared. The Service selects the pod, the pod is Ready, and every request goes nowhere. The wiring check proves it from the chart’s own output and draws the broken link in red.

Wiring checksRed edgesTemplate line
Upgrades

The platform team is moving the cluster to 1.29

Point --kube-version at where the cluster is going and the chart renders the way it would there. An API still served today and gone at the target turns from a warning into an error, so the removal is a thing you scheduled rather than a thing you discovered during the upgrade window.

--kube-versionError vs warningGate in CI
Review

What does this values change actually do?

A PR bumps a handful of values. --compare renders the chart both ways and draws the result: what appeared, what went, what changed in place, and which object simply moved to a new name because its name is built from a value. The attribute changes and the line diff behind them are in the dock.

--compareRename trackingLine diff

Install and usage

As a helm plugin, which is where its flags already make sense — everything after viz goes straight to helm-viz, so -f, --set and --compare are spelled the way helm spells them.

Start on the download page. Paste a registration key there for links to any release your year of updates covers, or give an email address and links to v1.2.0 are sent to it. A key's links last fifteen minutes and an emailed link seven days; both give you an install line per platform, like the ones below. The source is not published: helm-viz is a JCR Stack product and the repository is private. SHA256SUMS covers every archive, and each plugin archive carries the .prov helm 4 verifies before it installs. What is in each release →

as a helm plugin
$ helm plugin install https://jcrstack.com/dl/helm-viz/t/<token>/v1.2.0/helm-viz-plugin_1.2.0_darwin_arm64.tar.gz
$ helm helmviz ./charts/api -f values-prod.yaml
 
# copy the line for your platform from /helm-viz/download — the
# link carries the token, and names the OS and architecture
# because helm does not substitute them into a URL.
# the binary is inside the archive; nothing is downloaded at
# install time, and there is no repository to reach.
on helm 4, import the signing key first
$ curl -sSL https://jcrstack.com/dl/helm-viz/jcrstack-release.asc | gpg --import
$ gpg --export > ~/.gnupg/pubring.gpg
 
# helm 4 verifies a plugin signature against your own keyring
# before installing. the second line looks redundant and is not:
# helm reads keyrings with Go’s OpenPGP, which cannot parse the
# keybox format GnuPG has used since 2.1.
# --verify=false skips it. helm 3 does not verify at all.
or a standalone binary
$ curl -sSL -o helm-viz_1.2.0_linux_amd64.tar.gz '<link from /helm-viz/download>'
$ tar xzf helm-viz_1.2.0_linux_amd64.tar.gz
$ helm-viz_1.2.0_linux_amd64/helm-viz ./charts/api
 
# one directory holding the binary, LICENSE and the README.
# also darwin_arm64, darwin_amd64, linux_arm64, and a
# windows_amd64 zip — there is no plugin archive for Windows,
# so take this one there.
the things you will actually type
$ helm-viz ./helm
$ helm-viz ./helm -f values-prod.yaml --set ingress.enabled=true
$ helm-viz ./helm -f values-dev.yaml --compare -f values-prod.yaml
$ helm-viz ./helm --check --format=github
$ helm-viz ./helm --check --kube-version 1.29
$ helm-viz ./helm --export chart.html
 
# everything after `helm helmviz` goes straight through, so the
# plugin takes the same flags spelled the same way.

Upgrading is uninstall and install

helm plugin update cannot work on a plugin fetched from a URL — there is no repository to pull from, and both helm 3 and helm 4 fail with "cannot get information about plugin source". Run helm plugin uninstall helmviz — or viz, coming from 1.0.0, which installed under that name — and install the new version's URL.

If helm 3 says bin: file exists

helm 3 keeps the extracted archive in ~/.cache/helm/plugins (~/Library/Caches/helm/plugins on macOS) and does not clear it, so installing the same URL twice fails. Delete that directory. A genuine upgrade fetches a different version's URL and never hits it.

Flag What it does
-f <file> Values file. Repeatable, and merged the way helm merges it.
--set k=v Value override. Repeatable.
--compare Everything after this is a second set of values, rendered and diffed against the first.
--check Report problems and exit; serves nothing. 0 clean, 1 problems found, 2 could not tell.
--format <fmt> With --check: text (default), github for workflow annotations, or json.
--kube-version <v> Kubernetes to render and check apiVersions against (default 1.32). Passed to helm too.
--export <path> Write one self-contained HTML file and exit. A directory gets a stamped filename inside it.
--release <name> Release name to render with (default "release").
--port <n> Port to listen on; 0 picks a free one (default 3000). Binds to localhost only.
--no-open Do not open a browser.
--version Print the version and exit.

Needs helm on your PATH — helm-viz shells out to yours rather than linking the library. The chart directory defaults to . and must contain a Chart.yaml. Everything works without a key — all the views, the local server, --export, and --check. It is free under 250 staff and A$15M revenue; above either, a key per user (A$149 for a year of updates).

Frequently asked questions

What teams ask before they run it on a chart that matters.

What does helm-viz need to run?

One binary and a helm on your PATH. The UI is embedded in it, so there is no server to deploy, no container to run, and no cluster to talk to — helm-viz renders a chart directory off disk and serves the diagram on localhost. The chart directory defaults to . and must contain a Chart.yaml. Ctrl+C shuts it down.

Why shell out to helm instead of importing the library?

Because your helm is the only thing that agrees with your helm about what a chart means. Linking the library pins helm-viz to one version of the renderer and quietly renders your chart differently from the command you are about to run. Shelling out means the YAML in the diagram is the YAML helm gave it, and --kube-version is passed straight through so the chart renders the way it would on the cluster you are targeting.

Does my chart or my values leave the machine?

No. helm-viz runs locally, binds the server to localhost, and makes no outbound calls — there is no account, no telemetry, and no hosted version to send anything to. The only files it reads are the chart directory and the values files you point it at, and the only thing it writes is an export you asked for.

Is it safe to attach an exported page to a ticket?

Secret values are replaced with <REDACTED> in an exported file, in the YAML pane and in the diff alike. Key names stay, because the shape of a Secret is most of what the diagram is for — but the values cannot travel, and a chart that generates its own passwords with randAlphaNum writes a live credential on every render. The live view is not redacted: that is your own chart on your own machine, and seeing what helm actually emitted is the point of it. The line is not whether something is sensitive but whether it leaves the machine. One thing to know before publishing an export more widely: it embeds the absolute path of the chart directory it was taken from, which usually means a home directory.

How big is an exported page?

About 1.5MB for a small chart and 8MB for kube-prometheus-stack — half a megabyte gzipped either way, which is what an artifact upload costs. It folds the UI and the payload the live page would have fetched into one file, so it opens from disk with no server and no network: the graph, the dock, the values index, and the diff if one was asked for. The image exports are smaller again, because the cards are drawn as real SVG shapes rather than screenshotted, so an SVG is selectable text and a few tens of KB.

What exactly does --check report, and will it break my build?

It renders the chart, runs the wiring and API-version checks, and exits without serving anything. Only errors fail the gate — a removal that is still several releases away is reported as a warning and exits 0, because breaking a pipeline over it is how people learn to stop running the check.

  • → 0 — clean.
  • → 1 — problems found.
  • → 2 — could not tell. A chart that will not render is not a chart that is wrong.

The exit codes and the --format=json document are a contract. The JSON carries a version that goes up when a field changes meaning or disappears, never when one is added, and it prints the same document whether or not anything was found, so nothing downstream has to special-case success. Every finding carries a category — wiring when the chart contradicts itself, apiVersion when it contradicts a Kubernetes release — so a consumer can act on one without the other. An unknown --format exits 2 rather than falling back to text, since a typo would otherwise read as a clean chart.

Do the CI annotations point at the rendered YAML or at my templates?

At your templates. --format=github emits workflow annotations against the template file and line that wrote the object, because provenance maps one to the other and the rendered YAML is not a file anyone committed. An annotation on a line of helm output would have nothing to attach to in the diff view of a pull request.

Will it report findings on a chart that is fine?

Only things provable from the chart’s own output are reported, so the checks are quiet on a chart that is not wrong. The same discipline applies to the environment view, which reports evidence rather than a verdict: an alb ingress class means the AWS Load Balancer Controller is installed, not that the cluster is EKS. And a comparison of a chart against itself reports nothing — anything built with randAlphaNum regenerates on every render, so the base side is rendered twice and whatever moved on its own is left out.

Does it work on a chart helm refuses to render?

The includes view does. It is parsed from the template AST rather than from helm’s output, so the file/define call graph still draws when the render fails, and the file that failed carries helm’s own error. Calls whose target is computed at render time — include $var, tpl — are counted rather than guessed at, and a name nothing in the chart defines is marked as coming from a subchart or a library chart. The views that need rendered objects cannot draw without a render, which is what exit code 2 exists to say.

Do I have to restart it while I work?

Not for chart edits. The chart is watched while the server runs, so editing a template redraws the diagram without a reload. The compare side is a picker in the toolbar listing the values files in the chart directory, so a comparison can be started and changed without a restart. Keys and flags in the toolbar, or ? from anywhere, lists the shortcuts and the two things that still need a relaunch.

Is the source available?

No. helm-viz is a JCR Stack product and the repository is private, so there is nothing to clone and no install path that runs through a git host — helm installs the plugin straight from an HTTPS tarball, and the binary is inside that archive rather than fetched at install time. What is published is the binary — from the download page, with a registration key or an emailed link — and its signature and checksum: a .prov sits beside every archive, SHA256SUMS covers all of them, and helm 4 verifies the signature before it installs. Bug reports and feature requests go to hello@jcrstack.com rather than to an issue tracker.

What does it cost?

Nothing for individuals, education, open source, or organisations with fewer than 250 employees and under A$15 million in annual revenue: every view, the local server, --check, --compare and --export work in full. A copy without a key says it is on the free licence, with a label in the toolbar and a footer on exported images. An organisation over either threshold needs a key for each person who uses it, after a 30-day evaluation: A$149 per user for a year of updates. It is a one-off payment, not a subscription, and the releases a key covered stay licensed after the year ends. Keys are checked offline, so a key sends nothing off the machine either. Buy one at jcrstack.com/helm-viz/buy.

Run it on the chart you are least sure about

One archive, your own helm, and a chart directory. Nothing to deploy, no cluster to reach, and nothing leaves the machine you run it on.