Use Helm values schemas to reject invalid overrides before rendering

John Burns

A Helm chart can render successfully for its default values.yaml and still fail at the point where a deployment pipeline supplies its environment-specific override. That is where an unquoted number becomes a string, a required image tag is omitted, or a nested value is given the wrong shape. Template review alone does not make that boundary explicit. The chart needs a contract for the values that operators and CI are allowed to provide.

This guide adds a values.schema.json contract to a small chart and uses Helm 4.2.4 to test it. The success condition is concrete: helm lint and helm template accept a reviewed override with an integer replica count, while both return a nonzero status when the same field is supplied as the string "two". The validation was run in an isolated Linux AMD64 directory with a generated chart and synthetic values. It did not contact a Kubernetes cluster, registry, or production release.

Helm 4.2.4 was released on 13 August 2026. The release is a useful time to rehearse a chart validation gate, but it is not by itself a reason to upgrade a deployment process. Record the Helm version that performs the check, especially where a CI image, platform chart controller, or workstation package determines the executable.

Treat overrides as an interface

A chart’s values are an interface between its templates and the people or automation that install it. Without a schema, a template may coerce a value unexpectedly, emit a Kubernetes manifest that is structurally valid but operationally wrong, or fail only after several templates have been evaluated. A schema moves straightforward type, presence, and range requirements closer to the supplied input.

The following example uses an integer replica count of at least one and requires an image object with a nonempty tag. Keep the schema narrow at first. Require values that the chart genuinely needs, not every optional setting that a caller might reasonably inherit from values.yaml.

{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "replicaCount": {
      "type": "integer",
      "minimum": 1
    },
    "image": {
      "type": "object",
      "properties": {
        "tag": {
          "type": "string",
          "minLength": 1
        }
      },
      "required": ["tag"]
    }
  },
  "required": ["replicaCount", "image"]
}

Save this file as values.schema.json beside Chart.yaml and values.yaml. Helm recognizes that filename as the optional values schema for a chart. It applies the schema to the final values it receives, so an override can satisfy a required field left blank in the chart defaults. That makes it practical to require a release-specific image tag without hard-coding one into the reusable chart.

A schema is not a replacement for Kubernetes admission control, policy, image verification, or an integration test. It cannot prove that an image exists, that a Service selects ready Pods, or that a cluster supports an API version. Its useful scope is smaller: reject values whose shape and basic constraints are already known before rendering creates a large manifest set.

Verify the Helm archive before running it

Download the release archive and its adjacent checksum file from the official Helm distribution site. The file name below is for Linux AMD64; select the platform that matches the runner rather than reusing it on another architecture.

set -eu
version=4.2.4
archive="helm-v${version}-linux-amd64.tar.gz"
base="https://get.helm.sh"

curl -fsSLO "$base/$archive"
curl -fsSLO "$base/$archive.sha256sum"
sha256sum -c "$archive.sha256sum"
tar -xzf "$archive"
chmod 700 linux-amd64/helm
./linux-amd64/helm version

The checksum command must print OK before extracting or executing the archive. In the isolated validation run, the downloaded archive matched the published SHA-256 value. The resulting executable reported Helm v4.2.4, Git commit 3900f434fd3ef2b84065dc04508df48f288dba00, Go 1.26.5, and Kubernetes client version v1.36. A system package or CI image can report different build metadata, so capture its own helm version output with the change rather than copying this result.

Do not install an unreviewed binary into a shared runner merely to perform this test. A temporary directory with an explicit executable path keeps the validation version visible and avoids changing the version used by unrelated jobs.

Create a known-good override

For a new chart, helm create <chart-name> is a convenient way to obtain a disposable structure to test. For an existing chart, work in a branch or temporary copy and retain its actual templates. Put a reviewed environment override in its own file rather than editing the chart defaults during validation:

# review-values.yaml
replicaCount: 2
image:
  tag: "1.27.0"

The value 2 is an integer in YAML, while the image tag is deliberately a string. Quoting image-like values avoids accidental numeric interpretation and preserves a tag such as 01 exactly as supplied. The schema permits this small override because it meets the minimum replica count and provides the required tag.

Run both a chart check and a render check:

helm lint ./validation-chart --values review-values.yaml
helm template validation-chart ./validation-chart --values review-values.yaml > rendered.yaml

helm lint checks the chart and reports chart-level problems. helm template evaluates the templates without installing anything, leaving a manifest file that can be passed to the next approved local or cluster-aware validation step. In the validation run, lint exited zero and reported one chart linted with zero failures. The template command also exited zero and generated Deployment, Service, and ServiceAccount manifests. Neither command required a cluster connection for this chart-local check.

Do not treat a rendered file as proof that it can be applied. A separate authorized stage should validate the target Kubernetes version and APIs, policy constraints, namespace permissions, custom resources, and any server-side defaulting. The point here is to stop malformed value inputs before that more expensive boundary.

Make a bad override fail on purpose

A positive check is only useful if the rule also rejects an input that violates it. Copy the reviewed values and change just the replica count into a string:

# invalid-values.yaml
replicaCount: "two"
image:
  tag: "1.27.0"

Run the same two commands against the disposable invalid file:

helm lint ./validation-chart --values invalid-values.yaml
helm template validation-chart ./validation-chart --values invalid-values.yaml

Helm 4.2.4 rejected both commands in the isolated run. helm lint exited status 1 and reported at '/replicaCount': got string, want integer; it ended with 1 chart(s) linted, 1 chart(s) failed. helm template also exited status 1 before producing a manifest, with the same schema diagnosis. That second result matters: the contract is enforced during render as well as the explicit lint stage, but CI should keep both commands. Lint gives a focused chart report, and template produces the exact input for later checks only after schema validation succeeds.

Do not weaken the schema or add || true to make a pipeline proceed past this failure. Correct the override when the intended count is a number. If a chart genuinely accepts an autoscaling mode instead of an explicit replica count, model that as a documented alternative in the schema and templates, then add a separate positive and negative fixture for that path.

The completed gate is intentionally local and limited. It verifies the exact Helm binary, makes the values contract reviewable, accepts a typed override, and proves that a simple type regression stops both lint and rendering. Put it before any command that has cluster credentials or an installation side effect, then use cluster-aware checks for the things a JSON schema cannot observe.

Sources