Lab Guide
Three exercises: deploy a LoadBalancer service with cloud annotations, install MetalLB on a local cluster, and build a cloud-agnostic Helm template.
Prerequisites
- A Kubernetes cluster (Minikube, kind, or a cloud cluster). For Exercise 2,
kindor bare-metal is needed. - Helm 3 installed (
helm version) kubectlconfigured and pointing at your cluster- Basic familiarity with Helm chart structure (values.yaml, templates/)
Cloud vs local: Exercise 1 works best on a real cloud cluster (EKS, GKE, AKS). If you only have a local cluster, you can still complete it -- the Service will stay in Pending state, which is expected and educational. Exercise 2 addresses exactly this gap with MetalLB.
Exercise 1 — LoadBalancer Service with Cloud Annotations
Goal
Deploy an NGINX pod behind a LoadBalancer Service, then customize it with cloud-specific annotations for health checks and internal/external visibility.
Step 1 — Create the chart scaffold
mkdir -p ~/helm-labs/lab10 && cd ~/helm-labs/lab10
helm create lb-demo
cd lb-demo
Step 2 — Update values.yaml
Open values.yaml and modify the service section:
# values.yaml (relevant section)
service:
type: LoadBalancer
port: 80
annotations: {}
externalTrafficPolicy: Cluster
sessionAffinity: None
loadBalancerIP: ""
Step 3 — Update the Service template
Edit templates/service.yaml to support annotations, traffic policy, and session affinity:
apiVersion: v1
kind: Service
metadata:
name: {{ include "lb-demo.fullname" . }}
labels:
{{- include "lb-demo.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- if eq .Values.service.type "LoadBalancer" }}
externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }}
sessionAffinity: {{ .Values.service.sessionAffinity }}
{{- if .Values.service.loadBalancerIP }}
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- end }}
{{- end }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "lb-demo.selectorLabels" . | nindent 4 }}
Step 4 — Verify the template renders correctly
# Dry run with default values
helm template lb-demo . | grep -A 20 "kind: Service"
# Dry run with AWS NLB annotations
helm template lb-demo . \
--set service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-type"=nlb \
--set service.externalTrafficPolicy=Local \
| grep -A 25 "kind: Service"
Step 5 — Install the chart
helm install lb-demo . --namespace lb-lab --create-namespace
Step 6 — Check the Service status
# Watch for the external IP assignment
kubectl get svc -n lb-lab -w
# Inspect the full Service object
kubectl describe svc lb-demo -n lb-lab
On a cloud cluster, you should see an external IP or hostname appear within 1-3 minutes. On a local cluster, the EXTERNAL-IP will remain <pending>.
Verify connectivity (cloud only): Once the IP is assigned, run curl http://<EXTERNAL-IP> to confirm you see the NGINX welcome page.
Step 7 — Upgrade with session affinity
helm upgrade lb-demo . --namespace lb-lab \
--set service.sessionAffinity=ClientIP
Confirm the change took effect:
kubectl get svc lb-demo -n lb-lab -o yaml | grep -A 3 sessionAffinity
Exercise 2 — MetalLB on a Local Cluster
Goal
Install MetalLB in a kind or Minikube cluster, configure an IPAddressPool, and verify that LoadBalancer Services get real IPs.
Step 1 — Create a kind cluster (if needed)
kind create cluster --name metallb-lab
Step 2 — Install MetalLB via Helm
helm repo add metallb https://metallb.github.io/metallb
helm repo update
helm install metallb metallb/metallb \
--namespace metallb-system \
--create-namespace \
--wait
Step 3 — Determine the Docker network range (kind only)
MetalLB needs IPs on the same network as your kind nodes:
# Get the kind network subnet
docker network inspect kind -f '{{(index .IPAM.Config 0).Subnet}}'
# Example output: 172.18.0.0/16
# Pick a range that won't conflict, e.g. 172.18.255.200-172.18.255.250
Step 4 — Configure IPAddressPool and L2Advertisement
Create a file called metallb-config.yaml:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: lab-pool
namespace: metallb-system
spec:
addresses:
- 172.18.255.200-172.18.255.250
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: lab-l2
namespace: metallb-system
spec:
ipAddressPools:
- lab-pool
Apply it:
kubectl apply -f metallb-config.yaml
Step 5 — Deploy a LoadBalancer Service
# Re-use the chart from Exercise 1, or deploy a quick test
kubectl create deployment echoserver --image=registry.k8s.io/echoserver:1.10 -n default
kubectl expose deployment echoserver --type=LoadBalancer --port=80 --target-port=8080 -n default
Step 6 — Verify IP assignment
kubectl get svc echoserver -n default
You should see an IP from the 172.18.255.200-250 range in the EXTERNAL-IP column. Test connectivity:
curl http://172.18.255.200
Connectivity note: On macOS with Docker Desktop, you may not be able to reach the MetalLB IP directly from the host because Docker Desktop runs in a Linux VM. Use docker exec into a kind node to test, or use kubectl port-forward as a workaround.
Step 7 — Clean up
kubectl delete svc echoserver -n default
kubectl delete deployment echoserver -n default
Exercise 3 — Cloud-Agnostic Helm Service Template
Goal
Create per-cloud values files and verify the same chart renders correct annotations for AWS, GCP, and on-prem.
Step 1 — Create cloud-specific values files
In your lb-demo chart directory, create three override files:
# values-aws.yaml
service:
type: LoadBalancer
externalTrafficPolicy: Local
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: "HTTP"
service.beta.kubernetes.io/aws-load-balancer-healthcheck-path: "/healthz"
# values-gcp.yaml
service:
type: LoadBalancer
externalTrafficPolicy: Cluster
annotations:
networking.gke.io/load-balancer-type: "Internal"
# values-onprem.yaml
service:
type: LoadBalancer
loadBalancerIP: "192.168.1.200"
externalTrafficPolicy: Local
annotations: {}
Step 2 — Template each variant and compare
# AWS variant
echo "=== AWS ==="
helm template lb-demo . -f values-aws.yaml | grep -A 30 "kind: Service"
# GCP variant
echo "=== GCP ==="
helm template lb-demo . -f values-gcp.yaml | grep -A 30 "kind: Service"
# On-prem variant
echo "=== On-Prem ==="
helm template lb-demo . -f values-onprem.yaml | grep -A 30 "kind: Service"
Step 3 — Validate
For each rendered output, confirm:
- AWS: annotations include
aws-load-balancer-type: nlband health check settings - GCP: annotation includes
networking.gke.io/load-balancer-type: Internal - On-prem:
loadBalancerIPis set, no cloud annotations present - All three:
type: LoadBalanceris consistent
Bonus: Add a helm test that curls the Service endpoint. Define a test pod in templates/tests/test-connection.yaml that runs wget -qO- http://{{ include "lb-demo.fullname" . }}:{{ .Values.service.port }}.
Completion Checklist
- Ex 1: LoadBalancer Service deployed with annotations; session affinity verified via upgrade
- Ex 2: MetalLB installed; LoadBalancer Service received an IP from the configured pool
- Ex 3: Three cloud-specific values files created; helm template output validated for each