Introduction
The previous deployment labs used raw Kubernetes manifests directly. Helm adds a packaging layer around those manifests. A Helm chart stores related Kubernetes templates together, exposes reusable settings in values.yaml, and installs the rendered objects as a named release.
In this lab, you will build a small local chart instead of downloading one from a repository. You will write the chart metadata, values, Deployment template, and Service template, render the chart before installing it, then upgrade the release by changing values. This keeps the focus on the CKAD skill: understanding how a local chart turns into ordinary Kubernetes objects.
Prepare a Helm Workspace
In this step, you will prepare a namespace and confirm the tools needed for a local Helm chart workflow.
Start in /home/labex/project. This lab keeps the chart directory, generated manifests, and all commands in this workspace.
mkdir -p /home/labex/project
cd /home/labex/project
Check that the Kubernetes API server is reachable. If the VM has just started and the node is not ready yet, wait a few seconds and run the command again.
kubectl get nodes --request-timeout=5s
Expected output includes:
NAME STATUS ROLES AGE VERSION
labex-v135 Ready control-plane ... v1.35...
Wait until the cluster networking Pod is Ready. The chart will create a Service, so application Pods need cluster networking before traffic tests are meaningful.
kubectl wait --namespace kube-system --for=condition=Ready pod -l k8s-app=calico-node --timeout=120s
Expected output:
pod/calico-node-... condition met
Create a namespace named ckad-prep-helm and set it as the current namespace:
kubectl create namespace ckad-prep-helm
kubectl config set-context --current --namespace=ckad-prep-helm
Confirm both the current namespace and the Helm client. Helm 3 works directly with Kubernetes using your current kubeconfig; there is no separate server-side Tiller component.
kubectl config view --minify --output 'jsonpath={..namespace}'; echo
helm version --short
Expected output includes:
ckad-prep-helm
v...
Create and Render a Local Chart
In this step, you will create a minimal local Helm chart and render it before installing anything.
A chart directory has a few important parts. Chart.yaml describes the chart itself. values.yaml contains default settings. Files under templates/ are Kubernetes manifests with template expressions such as {{ .Values.replicaCount }}. Helm replaces those expressions with concrete values when it renders or installs the chart.
Create the chart directory:
mkdir -p local-web-chart/templates
Write Chart.yaml. The chart name is not the same thing as the release name; the release name is chosen later when you install the chart.
cat > local-web-chart/Chart.yaml <<'EOF'
apiVersion: v2
name: local-web
description: A small local chart for CKAD deployment practice
type: application
version: 0.1.0
appVersion: "1.0"
EOF
Write values.yaml. These defaults control the number of Pods, the container image, the Service ports, and the message served by the application.
cat > local-web-chart/values.yaml <<'EOF'
replicaCount: 2
image:
repository: busybox
tag: "1.36"
service:
port: 80
targetPort: 8080
message: helm-v1
EOF
Create the Deployment template. The template uses the release name in object names and labels, so the same chart can be installed under different release names.
cat > local-web-chart/templates/deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-web
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
terminationGracePeriodSeconds: 0
containers:
- name: web
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -c
- |
mkdir -p /www
echo {{ .Values.message | quote }} > /www/index.html
httpd -f -p {{ .Values.service.targetPort }} -h /www
ports:
- containerPort: {{ .Values.service.targetPort }}
readinessProbe:
httpGet:
path: /
port: {{ .Values.service.targetPort }}
initialDelaySeconds: 1
periodSeconds: 2
EOF
Create the Service template. Notice that the Service selector uses the same labels as the Pod template.
cat > local-web-chart/templates/service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
selector:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.service.targetPort }}
EOF
Run helm lint to catch chart structure and template problems:
helm lint ./local-web-chart
Expected output includes:
1 chart(s) linted, 0 chart(s) failed
Render the chart locally. helm template prints the Kubernetes YAML that Helm would send to the API server, but it does not create any objects.
helm template local-web ./local-web-chart | sed -n '1,120p'
Expected output includes a Service named local-web and a Deployment named local-web-web.
Install the Chart as a Release
In this step, you will install the local chart into the ckad-prep-helm namespace.
Installing a chart creates a Helm release. The release name is local-web in this lab. Helm records release metadata, and Kubernetes receives ordinary objects such as a Deployment and Service.
Install the chart:
helm install local-web ./local-web-chart
Expected output includes:
NAME: local-web
STATUS: deployed
Wait for the Deployment created by the chart:
kubectl rollout status deployment/local-web-web --timeout=120s
Expected output:
deployment "local-web-web" successfully rolled out
Inspect the release and the Kubernetes objects it created:
helm list
kubectl get deployment,service -l app.kubernetes.io/instance=local-web
Expected output includes:
local-web
deployment.apps/local-web-web
service/local-web
Create a client Pod and send a request to the Service. This confirms that the chart did not only create objects; the installed application is actually reachable.
kubectl run chart-client --image=busybox:1.36 --restart=Never --command -- sleep 3600
kubectl wait --for=condition=Ready pod/chart-client --timeout=120s
kubectl exec chart-client -- wget -qO- http://local-web
Expected output:
helm-v1
Upgrade the Release with New Values
In this step, you will upgrade the existing release by overriding chart values.
The chart already contains reusable templates. You do not need to edit the template files to change the message or replica count. helm upgrade renders the same chart again with new values, updates the Kubernetes objects, and records a new release revision.
Upgrade the release so it serves helm-v2 from three replicas:
helm upgrade local-web ./local-web-chart --set message=helm-v2 --set replicaCount=3
Expected output includes:
Release "local-web" has been upgraded.
STATUS: deployed
Wait for the upgraded Deployment:
kubectl rollout status deployment/local-web-web --timeout=120s
Expected output:
deployment "local-web-web" successfully rolled out
Inspect the release history and the values currently stored with the release:
helm history local-web
helm get values local-web
Expected output includes two revisions and the overridden values:
REVISION
1
2
message: helm-v2
replicaCount: 3
Check the running Deployment and application response:
kubectl get deployment local-web-web
kubectl exec chart-client -- wget -qO- http://local-web
Expected output includes:
3/3
helm-v2
Summary
You created a local Helm chart from chart metadata, default values, and Kubernetes templates. You rendered the chart before installing it, installed it as a release, tested the Service, and upgraded the release by overriding values. The key idea is that Helm packages repeatable application deployment work while still producing ordinary Kubernetes objects you can inspect with kubectl.


