Helm
Your team runs a webhook relay for several customers. Each installation needs the same Deployment and Service, but its own image version and replica count. At first, you copy two YAML files into a folder per customer.
A readiness-probe fix now needs applying to twelve folders. One copy is missed, and the customer with that copy starts receiving traffic before the relay is ready. The common configuration needs one maintained source, with the differences supplied separately.
Helm packages Kubernetes resources into a chart. Templates describe the shared resources, values provide configurable inputs, and an installed instance is a release. The Deis team started Helm in 2015 while moving its platform to Kubernetes, taking inspiration from package managers such as Homebrew and apt.
One chart for the relay
You keep the example chart small enough to inspect. With Helm installed, create its directory:
mkdir -p relay/templatesIn relay/Chart.yaml, the chart's version is separate from the application version:
apiVersion: v2
name: relay
description: Webhook relay deployment
type: application
version: 0.1.0
appVersion: "1.4.0"version identifies this package of templates. appVersion is descriptive metadata; it doesn't select a container image automatically. The image is an explicit value in relay/values.yaml:
replicaCount: 1
image:
repository: registry.example.com/webhook-relay
tag: "1.4.0"Use your own relay image. This example assumes it listens on port 8080 and serves /ready. Save the resources in relay/templates/app.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name | quote }}
template:
metadata:
labels:
app.kubernetes.io/instance: {{ .Release.Name | quote }}
spec:
containers:
- name: relay
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}
spec:
selector:
app.kubernetes.io/instance: {{ .Release.Name | quote }}
ports:
- port: 80
targetPort: 8080.Release.Name comes from the installation name. .Values comes from the merged values. The label connects this release's Service to its Pods; another release in the same namespace gets a different label. The quote function ensures the label renders as a YAML string.
Customer-specific values
For one customer's staging installation, customer-a.yaml contains only the differences:
replicaCount: 2
image:
tag: "1.4.1"The repository is inherited from the defaults. Nested maps merge, while lists generally replace rather than append. Supplying several -f files applies later overrides after earlier ones; --set overrides file values. That precedence is useful until nobody can tell where the final value came from, so you keep the deployment inputs recorded together.
Before reaching a cluster, you lint and render the chart:
helm lint ./relay -f customer-a.yaml
helm template relay-a ./relay --namespace relays \
-f customer-a.yaml > rendered.yamlYou review rendered.yaml, including the image, labels, and ports. Helm's output is ordinary Kubernetes YAML. A valid template can still describe a broken service, so you also ask the intended development cluster to validate it. Replace dev-cluster with a real context; the relays namespace must already exist:
kubectl --context dev-cluster -n relays \
apply --dry-run=server -f rendered.yamlServer dry-run checks the cluster's API and admission rules without persisting these resources. It can't establish that the image exists, /ready behaves correctly, or webhook delivery works.
Installing and rolling back a release
Once the image and configuration have been tested, you install the release:
helm upgrade --install relay-a ./relay \
--kube-context dev-cluster --namespace relays \
-f customer-a.yaml --wait --timeout 5m
helm status relay-a --kube-context dev-cluster -n relays
helm get values relay-a --all --kube-context dev-cluster -n relays
helm get manifest relay-a --kube-context dev-cluster -n relaysrelay-a is the release; ./relay is the chart. upgrade --install installs if that release doesn't exist and upgrades it otherwise. --wait waits for the relevant Kubernetes resources to become ready. You still send a sample webhook and check its delivery record before calling the release successful.
The following upgrade changes a timeout in the app and turns out to be incompatible with one customer's endpoint. You inspect the release revisions:
helm history relay-a --kube-context dev-cluster -n relays
helm get values relay-a --revision 1 --all --kube-context dev-cluster -n relays
helm rollback relay-a 1 --kube-context dev-cluster -n relays --wait --timeout 5mRevision 1 is illustrative; choose the compatible revision from that release's history. Rollback applies the older release configuration as a new revision. It doesn't retract webhooks already delivered or reverse a migration run by an earlier release hook.
Distributing the shared chart
You move the common readiness fix into the chart, increment its package version, and render it against every customer's values in CI. When you distribute the chart itself, helm package ./relay creates a versioned archive; teams can publish charts to a chart repository or OCI registry rather than passing folders around.
The chart can grow to include configuration, service accounts, or dependencies, but credentials don't belong in committed values files. Rendered manifests and release records can contain supplied values too. Reference the team's secret-management system instead of treating templating as encryption.
If Argo CD or another controller deploys this chart, it may use Helm only to render resources and manage the rollout itself. In that setup, the controller's Git source is the change path; a manual helm rollback is not necessarily the owner of anything running there.
The next customer gets a values file instead of another copy of the templates. You can see which chart version and inputs each installation uses, and a shared probe fix has one source to update.