Free lesson · GenAI Agent Engineering
Create a Helm chart for the LLM chat application
Scaffold a Helm chart and move the chat API, PostgreSQL, and Redis manifests into templates. Parameterize image tags, replica counts, and resource limits.
Course: Kubernetes Essentials for GenAI Engineers · Chapter 9 · Packaging with Helm & Kustomize
Free to read — no subscription required.
Introduction
When you have spent eight chapters writing one YAML manifest at a time — a Deployment here, a StatefulSet there, three ConfigMaps and a quota — installing the full LLM chat stack means applying ten-plus files in the right order, hand-editing every one of them between dev and prod, and praying nothing drifts. Miss a file and the chat API boots without its Redis cache; mistype a replica count and a 200-replica prod release ships as 2. By the end of this lesson you will be able to scaffold a Helm chart for the LLM chat application, move your existing manifests into parameterized templates under templates/, factor naming and labels through _helpers.tpl, and install the whole stack with one helm install command.
Key Terminology
- Chart — a versioned bundle of Kubernetes manifests plus a
values.yamlfile; the unit Helm installs, upgrades, and rolls back as one release. Bundling the LLM chat API, PostgreSQL StatefulSet, Redis StatefulSet, and proxy sidecars into one chart is the whole point of this lesson. - Template — a Go-templated YAML file under
templates/that Helm renders into a real Kubernetes manifest by substituting values fromvalues.yaml. Every manifest you wrote in earlier chapters becomes a template here. - values.yaml — the chart's default configuration file. Templates reference fields like
.Values.chatApi.replicaCountso you can swap dev defaults for prod overrides without editing any template. - Release — one installation of a chart into a cluster, identified by a name (e.g.
chat-prod). Helm tracks every release's history sohelm rollbackcan restore the previous revision. - Named template (helper) — a reusable snippet defined in
templates/_helpers.tplvia{{- define "name" -}}…{{- end }}and called withinclude. Used here to generate consistent resource names and labels across the API Deployment, PostgreSQL StatefulSet, and Redis StatefulSet.
Concepts
Chart layout
helm create llm-chat scaffolds the directory shape every chart shares: Chart.yaml carries metadata, values.yaml carries defaults, templates/ carries the manifests, and templates/_helpers.tpl carries named templates. For the LLM chat stack you delete the scaffold's generic deployment.yaml/service.yaml/hpa.yaml and replace them with one template per real component — chat-api-deployment.yaml, postgresql-statefulset.yaml, redis-statefulset.yaml, plus matching Services. The layout is non-negotiable: Helm finds templates by directory, not by listing them anywhere.
Templating with .Values
A template is a manifest with the environment-specific bits replaced by {{ .Values.path.to.field }} references. .Values.chatApi.replicaCount reads from values.yaml; .Values.chatApi.image.tag lets you ship a new image without touching the template. The pipeline functions you will use most often are quote (force a string into quotes — critical for numeric env vars), toYaml (dump a nested structure as YAML — used for resources: blocks), and nindent N (indent each line by N spaces — pairs with toYaml). Conditionals like {{- if .Values.geminiProxy.enabled }} let one template serve a dev release with no proxy sidecar and a prod release with all three (see Code Walkthrough).
Named templates in _helpers.tpl
Three resources — the API Deployment, PostgreSQL StatefulSet, Redis StatefulSet — all need the same release-scoped name prefix and the same label set. Duplicating that logic in three templates is how charts drift. _helpers.tpl defines llm-chat.fullname (combines .Release.Name with the chart name, truncated to 63 chars for Kubernetes), llm-chat.labels (the standard app.kubernetes.io/* set plus the chart version), and llm-chat.selectorLabels (the minimal subset used in selector.matchLabels). Every template calls them with {{ include "llm-chat.fullname" . }} and {{- include "llm-chat.labels" . | nindent 4 }}.
Chart.yaml and release lifecycle
Chart.yaml declares apiVersion: v2 (Helm 3), name, version (the chart version — bump when templates change), and appVersion (the chat application's own version, independent of the chart). Once installed, the release is tracked: helm upgrade chat-prod ./llm-chat rolls forward, helm rollback chat-prod 1 rolls back to revision 1, and helm uninstall chat-prod removes every resource the chart installed in one command. This is the lifecycle benefit you cannot get from kubectl apply -f.
Code Walkthrough
The two snippets below demonstrate the concepts together: snippet 1 is the API Deployment template (chart layout + .Values templating + a conditional sidecar + helper includes); snippet 2 is _helpers.tpl + values.yaml showing what those helpers expand to and where the values come from.
Code snippetyaml
1# llm-chat/templates/chat-api-deployment.yaml 2apiVersion: apps/v1 3kind: Deployment 4metadata: 5 name: {{ include "llm-chat.fullname" . }}-api 6 labels: 7 {{- include "llm-chat.labels" . | nindent 4 }} 8 app.kubernetes.io/component: api 9spec: 10 replicas: {{ .Values.chatApi.replicaCount }} 11 selector: 12 matchLabels: 13 {{- include "llm-chat.selectorLabels" . | nindent 6 }} 14 app.kubernetes.io/component: api 15 template: 16 metadata: 17 labels: 18 {{- include "llm-chat.selectorLabels" . | nindent 8 }} 19 app.kubernetes.io/component: api 20 spec: 21 containers: 22 - name: chat-api 23 image: "{{ .Values.chatApi.image.repository }}:{{ .Values.chatApi.image.tag }}" 24 imagePullPolicy: {{ .Values.chatApi.image.pullPolicy }} 25 ports: 26 - name: http 27 containerPort: {{ .Values.chatApi.port }} 28 env: 29 - name: DATABASE_URL 30 valueFrom: 31 secretKeyRef: 32 name: {{ include "llm-chat.fullname" . }}-secrets 33 key: database-url 34 - name: REDIS_URL 35 value: "redis://{{ include "llm-chat.fullname" . }}-redis:6379" 36 - name: MODEL_NAME 37 value: {{ .Values.chatApi.modelName | quote }} 38 - name: MAX_TOKENS 39 value: {{ .Values.chatApi.maxTokens | quote }} 40 resources: 41 {{- toYaml .Values.chatApi.resources | nindent 12 }} 42 {{- if .Values.geminiProxy.enabled }} 43 - name: gemini-proxy 44 image: "{{ .Values.geminiProxy.image.repository }}:{{ .Values.geminiProxy.image.tag }}" 45 ports: 46 - name: proxy 47 containerPort: {{ .Values.geminiProxy.port }} 48 resources: 49 {{- toYaml .Values.geminiProxy.resources | nindent 12 }} 50 {{- end }}
include "llm-chat.fullname" and include "llm-chat.labels" are calls into _helpers.tpl — the same prefix and label set will appear on the PostgreSQL and Redis templates too, so a single helper change reskins the whole chart. .Values.chatApi.image.tag and .Values.chatApi.replicaCount parameterize what changes between environments; quote and toYaml | nindent 12 are the pipeline-function pair that makes env vars and resources: render as valid YAML. The {{- if .Values.geminiProxy.enabled }} block is how one template serves dev (proxy off) and prod (proxy on) without forking the file.
Code snippetyaml
1# llm-chat/templates/_helpers.tpl 2{{- define "llm-chat.fullname" -}} 3{{- if .Values.fullnameOverride }} 4{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 5{{- else }} 6{{- $name := default .Chart.Name .Values.nameOverride }} 7{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 8{{- end }} 9{{- end }} 10 11{{- define "llm-chat.labels" -}} 12helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} 13app.kubernetes.io/managed-by: {{ .Release.Service }} 14{{- include "llm-chat.selectorLabels" . }} 15{{- end }} 16 17{{- define "llm-chat.selectorLabels" -}} 18app.kubernetes.io/name: {{ .Chart.Name }} 19app.kubernetes.io/instance: {{ .Release.Name }} 20{{- end }} 21--- 22# llm-chat/values.yaml 23chatApi: 24 replicaCount: 2 25 image: 26 repository: ghcr.io/example/chat-api 27 tag: "1.0.0" 28 pullPolicy: IfNotPresent 29 port: 8080 30 modelName: gemini-1.5-flash 31 maxTokens: 2048 32 resources: 33 requests: { cpu: 200m, memory: 512Mi } 34 limits: { cpu: 1, memory: 1Gi } 35geminiProxy: 36 enabled: false 37 image: { repository: ghcr.io/example/gemini-proxy, tag: "1.0.0" } 38 port: 9090 39 resources: 40 requests: { cpu: 50m, memory: 64Mi }
You'll know it works when helm lint ./llm-chat reports no errors, helm template chat-dev ./llm-chat prints valid Kubernetes YAML (every {{ … }} substituted), helm install chat-dev ./llm-chat creates the release and the API pod reaches Running, and helm install chat-prod ./llm-chat --set geminiProxy.enabled=true produces a two-container pod (chat-api + gemini-proxy) while the dev release stays single-container.
Do's and Don'ts
Do's
- ✓Do run
helm lintandhelm templatebefore every install — lint catches schema errors andtemplateshows the rendered YAML so you can confirm{{ .Values.* }}substitutions before they hit the cluster. - ✓Do centralize naming and labels in
_helpers.tpl— every chart template (API Deployment, PostgreSQL StatefulSet, Redis StatefulSet, all Services) should callinclude "llm-chat.fullname"andinclude "llm-chat.labels"so a single edit reskins the release. - ✓Do version
Chart.yamlseparately fromappVersion— bumpversionwhen templates change, bumpappVersionwhen the chat application image changes; keeping them independent is what makeshelm rollbackmeaningful.
Don'ts
- ✗Don't hardcode environment-specific values in templates — replica counts, image tags, model names, and resource limits belong in
values.yaml(or avalues-prod.yamloverride), never inline intemplates/*.yaml. - ✗Don't skip
quoteon string env vars that look numeric —MAX_TOKENS: 2048renders as an integer and Kubernetes rejects it;{{ .Values.chatApi.maxTokens | quote }}produces"2048"and passes validation. - ✗Don't apply chart manifests with
kubectl apply -f— bypassinghelm install/helm upgrademeans no release history, nohelm rollback, and orphaned resourceshelm uninstallcannot clean up.
This lesson is free to read. Its hands-on lab — real code, in a cloud IDE — is part of the GenAI Agent Engineering subscription.
From · cancel anytime
More free lessons in Kubernetes Essentials for GenAI Engineers
- Ch 1Use Docker Compose to run the LLM app with supporting services
- Ch 2Deploy the LLM app as your first Kubernetes pod
- Ch 4Manage deployment lifecycle with kubectl rollout
- Ch 9Create a Helm chart for the LLM chat applicationYou are here
- Ch 9Use Kustomize bases and overlays for the LLM app
- Ch 9Use Kustomize patches and generators
- Ch 12Use kubectl debug and ephemeral containers for live debugging