Kubernetes Helm Chart Generator

Scaffold a Helm chart with Chart.yaml, values.yaml and templates that use nindent, required and the 63 character name truncation correctly. A chart is a directory, so the output is one document with a header per file, and every trap it avoids is explained beside it.

Chart

Chart.yaml describes the packaging. None of it describes what runs.

Defaults in values.yaml

These are defaults, not settings. Anything here can be replaced by -f or --set at install time.

Template behaviour

required fails the render with your message. Without it a missing value renders as an empty string and the manifest is wrong rather than rejected.

The template pipes the block through toYaml | nindent 12 either way. This decides whether the default is a real block or an empty map.

Shows the hook weight and the delete policy. A hook without a delete policy is never cleaned up, by anything.

chart scaffold

updates as you type

    Common mistakes

    These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

    1. Putting environment values in values.yaml

      That file is the chart's defaults and ships with it. Environment specifics belong in a separate file passed with -f.

      Instead:Keep values.yaml generic and override per environment.

    2. Templating without quoting values

      A version like 1.10 renders as 1.1 and a string of digits becomes a number, which then fails validation in the manifest.

      Instead:Use the quote function on anything that must stay a string.

    3. Forgetting the chart version is separate from appVersion

      Helm tracks changes by chart version. Bumping only appVersion means helm upgrade sees no change to the chart.

      Instead:Bump version on every chart change.

    What a Helm chart actually is, and where it goes wrong

    A chart is a directory of Go templates plus a values file, and the templating is the part that surprises people. Go templating is not Jinja and it is not YAML-aware: it produces text, and the YAML parser only ever sees the result. So indentation is the chart author's problem, whitespace control is a language feature you have to use, and a chart can render perfectly for the values you tested and fail to parse for somebody else's.

    version is the chart, appVersion is the application

    Two version numbers with two lifecycles. version describes the packaging and is what helm package writes into the .tgz filename and what helm history shows. appVersion describes the code inside, and it is a string. Unquoted it is a YAML float, so 1.20 arrives as 1.2 with the trailing zero gone, and a chart that defaults its image tag from appVersion then points at a tag that does not exist.

    apiVersion: v2
    name: my-app
    version: "0.4.2"      # this chart packaging
    appVersion: "1.20.0"  # the code it deploys
    
    # unquoted, these break differently:
    #   version: 0.4      -> the float 0.4
    #   appVersion: 1.20  -> the float 1.2, tag "1.2"
    #
    # apiVersion: v1 is the Helm 2 format. Helm 3 still
    # installs it, and it keeps Helm 2 semantics: the
    # dependencies below are read from requirements.yaml
    # instead, and a dependencies: block here does nothing.

    indent and nindent, which is the bug you will actually hit

    The template engine emits text at whatever column the text happens to be in. A block coming out of toYaml or include starts at column zero, so it has to be indented to fit where you put it. nindent is indent with a leading newline, and after a key on its own line that newline is the whole point. Get it backwards and the parse error points at the line after the mistake, which is why it takes so long to find.

    resources:
      {{- toYaml .Values.resources | nindent 4 }}
    
    # nindent 4 renders:
    # resources:
    #     requests:
    #       cpu: 100m
    
    # indent 4 in the same place renders:
    # resources:    requests:
    #       cpu: 100m
    #                  ^ still "valid" text, and the
    #                    parser complains two lines down

    {{- and -}} delete whitespace, in one direction each

    A control action on its own line leaves a blank line behind unless the dash trims it. The dash goes on the side you want removed, and it eats all whitespace including newlines. This matters most inside a list: an untrimmed action can leave a dangling indent that turns the next item into a continuation of the previous value, and only for the values that take that branch.

    {{- if .Values.enabled }}        <- trims before
    key: value
    {{- end }}
    
    # without the dashes:
    #
    # <blank line where the if was>
    # key: value
    # <blank line where the end was>
    #
    # harmless at the top level, and inside a nested
    # block the blank line is where a parse error comes
    # from for one value of .Values.enabled and not
    # the other.

    values.yaml is merged, except lists, which are replaced

    Overrides are merged key by key, so a file that sets one key leaves every other default in place. Lists do not work that way. A list in an override replaces the whole list, element for element, and nothing warns you that the defaults went away. This is the most common surprise in the whole system, because merging is what the rest of the file taught you to expect.

    # values.yaml
    imagePullSecrets:
      - name: registry-a
      - name: registry-b
    resources:
      requests:
        cpu: 100m
        memory: 128Mi
    
    # -f prod.yaml
    imagePullSecrets:
      - name: registry-c
    resources:
      requests:
        cpu: 500m
    
    # result:
    #   imagePullSecrets: [registry-c]   a and b are gone
    #   resources.requests.cpu:    500m
    #   resources.requests.memory: 128Mi  still there

    .Values, .Chart and .Release, and why $ exists

    Three separate scopes hang off the root context. Inside a range or a with block the dot is rebound to the current item, so the root is no longer reachable through it. $ is the root, always, and it is how you get back to .Release.Name from inside a loop. The error when you forget is a nil pointer message that reads like a missing value rather than a scope problem.

    .Values   values.yaml after every override is merged
    .Chart    .Chart.Name, .Chart.Version, .Chart.AppVersion
    .Release  .Release.Name, .Release.Namespace,
              .Release.Service, .Release.IsUpgrade
    
    {{- range .Values.hosts }}
      - host: {{ . }}              <- the item
        secret: {{ $.Release.Name }}-tls
    {{- end }}
    
    # .Release.Name inside the range:
    #   nil pointer evaluating interface {}.Release

    63 characters, because names become DNS labels

    A Kubernetes object name ends up in a DNS label, and a label stops at 63 characters. The fullname helper is release name plus chart name, so a long release name overflows it. Truncating alone is not enough: the cut can land exactly on a hyphen, and a label cannot start or end with one. That is what trimSuffix is doing there, and without it the chart installs for short release names and is rejected for long ones.

    {{- define "my-app.fullname" -}}
    {{- printf "%s-%s" .Release.Name $name
        | trunc 63 | trimSuffix "-" -}}
    {{- end -}}
    
    # helm install a-very-long-release-name-here my-app
    #   trunc 63 alone -> "...-name-here-my-"
    #   the API server rejects it:
    #   must be a valid DNS-1123 subdomain

    Hooks are not part of the release

    Helm creates a hook resource, waits for it, and then stops tracking it. It is not in the release manifest, so helm uninstall does not delete it, helm rollback does not know it ran, and the next upgrade collides with the object still sitting there. The delete policy annotation is the only thing that cleans it up. Weights order hooks within a phase, lowest first, and they are strings even though they look like numbers.

    annotations:
      "helm.sh/hook": pre-upgrade
      "helm.sh/hook-weight": "-5"      <- a string
      "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
    
    # before-hook-creation  delete the previous run first
    # hook-succeeded        delete it when it passes
    # hook-failed           delete it when it fails, which
    #                       throws away the logs you wanted
    
    # with no policy at all: the Job stays forever, and
    # the next upgrade fails on "already exists".

    What each command actually checks

    None of them is validation against your cluster. helm lint reads the chart structure and a few conventions, and says nothing about whether the rendered YAML is a legal object. helm template renders locally with no API server at all. helm install --dry-run does reach the API server and catches more, and still not everything. For schema validation against a specific Kubernetes version, render the chart and pipe it through kubeconform.

    helm lint ./my-app
      chart structure, Chart.yaml fields, icon,
      values against values.schema.json.
      Nothing about the rendered manifests.
    
    helm template ./my-app
      renders locally. Catches template errors and
      YAML that does not parse. Knows nothing about
      which apiVersions your cluster has.
    
    helm install --dry-run
      sends it to the API server: schema, admission,
      webhooks. Does not run initContainers, does not
      prove the image exists.
    
    helm template ./my-app | kubeconform -strict \
      -kubernetes-version 1.30.0

    More kubernetes tools

    Kubernetes YAML Generator Answer a few questions, get a manifest Kubernetes YAML Validator Is this YAML valid, and where is it wrong? Kubernetes Rollout & Probe Timing Restart loops, capacity dips and stuck drains Kubernetes Service Checker Will this Service have any endpoints? Kubernetes Network Policy Viewer What can reach what, and what just broke Kubernetes Manifest Diff What actually changed Kubernetes Resource Calculator What these manifests actually reserve Kubernetes RBAC Viewer Who can actually do what Kubernetes Deployment Generator See the capacity dip before you deploy Kubernetes Service Generator port, targetPort, and which one you meant Kubernetes StatefulSet Generator Stable names, and volumes that outlive you Kubernetes QoS Class Calculator And where you sit in the eviction order Kubernetes Node Capacity Planner Allocatable is not capacity Kubernetes Max Pods per Node Calculator The ENI limit, not the 110 default Kubernetes Label Selector Tester Which key failed, not just that one did Kubernetes Taint & Toleration Tester Tolerations permit, they do not attract Kubernetes Resource Name Validator RFC 1123 subdomain, label, or 1035 Kubernetes Label & Annotation Validator Why your image tag is not a legal label Kubernetes CrashLoopBackOff Guide The status is a timer, not a cause Kubernetes TLS Certificate Decoder When it expires, and what it covers Kubernetes ServiceAccount Token Decoder Bound or legacy, and when it dies Kubernetes imagePullSecret Generator The registry URL nobody gets right Kubernetes cert-manager Certificates Issuer scope, and the rate limit that costs a week Kubernetes Flux HelmRelease Generator With the source it cannot work without Kubernetes Argo CD Application Generator The defaults that are off, turned on Kubernetes Ingress Generator pathType, and the TLS secret that fails quietly Kubernetes HPA Generator Why it says unknown and never scales Kubernetes SecurityContext Generator Pod level, container level, and which wins Kubernetes Probe Generator How long it gets to start before liveness kills it Kubernetes PVC Generator Access modes your storage can actually do Kubernetes ResourceQuota Generator And the LimitRange that stops it breaking deploys Kubernetes CronJob Generator The fields that decide whether it runs Kubernetes NetworkPolicy Generator With the DNS rule already in it Kubernetes RBAC Generator And what it actually grants Kubernetes PodDisruptionBudget Generator Can this budget ever be satisfied? Kubernetes API Deprecation Checker What breaks on the next upgrade Kubernetes Kubeconfig Viewer Read it without handing it to anyone Kubernetes Quantity Parser What 512Mi, 100m and 1e3 actually mean Kubernetes Multi-Document YAML Splitter One file per object, in apply order Kubernetes .env to ConfigMap & Secret Split the credentials out on the way Kubernetes Secret Decoder See what is actually in there Kubernetes Ingress to Gateway API Which annotations silently stop working Kubernetes Manifest Visualizer What points at what, and what points at nothing Kubernetes Patch Tester Which --type, and what it deletes Kubernetes kubectl JSONPath Tester kubectl's dialect, not generic JSONPath Kubernetes Node Affinity Tester Required excludes, preferred only scores Kubernetes Helm Values Diff A missing key is a third value Kubernetes Pod and Service CIDR Fixed at cluster creation Kubernetes Secret Encoder base64 is not encryption Kubernetes API Version Checker Will this apply on that cluster Kubernetes Go Template Tester Go truthiness is not JavaScript's Kubernetes Cluster Cost Estimator At your rates, not a price table Kubernetes Kustomize Build Transformers run in kustomize's order Kubernetes Helm Chart Validator version is SemVer, appVersion is not

    Elsewhere on the site