What You'll Build
In this exercise you will package an existing FastAPI application as a Helm chart and deploy it to three environments—dev, staging, and production—each with environment-specific values. You will write the chart's templates from scratch rather than using the helm create defaults, giving you full understanding of the template structure. The finished chart will have a Deployment, Service, optional Ingress, optional HPA, a _helpers.tpl for consistent naming, a Helm test that verifies the health endpoint, and a lint-clean structure validated by helm lint.
You will also create three environment values files that progressively increase replica count, enable autoscaling, switch to ECR images, and configure ingress hostnames. By the end, a single `helm upgrade --install` command with the appropriate values file deploys the correct configuration to each environment without any template duplication, and `helm diff` shows exactly what will change before any upgrade is applied.
Prerequisites
- Helm 3.x installed locally: brew install helm on macOS or the Linux installer script from helm.sh.
- A running Kubernetes cluster for testing: kind or minikube is sufficient for the dev environment steps.
- Docker installed to build the FastAPI image for the dev environment local registry.
- Familiarity with Helm chart structure from Lesson 11: Chart.yaml, values.yaml, and the templates/ directory layout.
- helm diff plugin installed for the diff step: helm plugin install https://github.com/databus23/helm-diff.
Setup & Project Structure
Create the FastAPI application and scaffold the chart directory. The helm create command creates a chart skeleton with example templates; we immediately remove the default templates to write our own, which gives full understanding of every line in the chart rather than inheriting opaque defaults. The envs/ directory holds the environment-specific values files outside the chart itself, making it easy to commit them to the GitOps repository separately from the chart.
# Project setup — create the FastAPI app and Helm chart scaffold.
mkdir india-squad-helm && cd india-squad-helm
# Create the FastAPI application
mkdir -p app && cat > app/main.py << 'APPEOF'
from fastapi import FastAPI
app = FastAPI(title='India Squad Analytics API')
@app.get('/health')
def health(): return {'status': 'ok', 'service': 'india-squad-api'}
@app.get('/squad')
def squad(): return {'players': ['Rohit Sharma', 'Virat Kohli', 'Jasprit Bumrah']}
APPEOF
# Create Dockerfile
cat > Dockerfile << 'DFEOF'
FROM python:3.12-slim
WORKDIR /app
RUN pip install fastapi uvicorn --no-cache-dir
COPY app/ .
CMD ['uvicorn', 'main:app', '--host', '0.0.0.0', '--port', '8000']
DFEOF
# Scaffold the Helm chart (creates Chart.yaml, values.yaml, templates/)
helm create india-squad-api
# Remove the default Helm starter templates we will rewrite
rm -rf india-squad-api/templates/*
rm india-squad-api/values.yaml
# Create directories for environment values
mkdir -p envs/{dev,staging,prod}
echo 'Scaffold complete. Tree:'
tree india-squad-api/ envs/Step 1 — Chart Metadata and Default Values
Write the `Chart.yaml` with the chart version, app version, and name. Then write `values.yaml` with conservative single-replica defaults suitable for development: small resource requests, no ingress, and `pullPolicy: IfNotPresent` so the local image is used without a registry pull. Every field in `values.yaml` is optional—if a consuming values file omits it, the default applies—so defaults should represent the safest, most minimal configuration.
# Step 1: Write Chart.yaml and values.yaml defaults.
# india-squad-api/Chart.yaml
cat > india-squad-api/Chart.yaml << 'EOF'
apiVersion: v2
name: india-squad-api
description: India Squad Analytics FastAPI service
type: application
version: 0.1.0
appVersion: '1.0.0'
EOF
# india-squad-api/values.yaml (conservative development defaults)
cat > india-squad-api/values.yaml << 'EOF'
replicaCount: 1
image:
repository: india-squad-api
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 8000
ingress:
enabled: false
host: ''
tlsSecret: ''
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilization: 70
env:
LOG_LEVEL: info
ENVIRONMENT: dev
EOFStep 2 — Templates
Write the three core templates: `_helpers.tpl` for the `fullname` and `labels` named templates, `deployment.yaml` that uses those helpers and renders the replica count conditionally on autoscaling, and `service.yaml`. The conditional `{{- if not .Values.autoscaling.enabled }}` block ensures the Deployment does not override the HPA-managed replica count when autoscaling is enabled—a common mistake that causes the HPA to fight the Deployment controller.
# Step 2: Write the Deployment, Service, and _helpers.tpl templates.
# india-squad-api/templates/_helpers.tpl
cat > india-squad-api/templates/_helpers.tpl << 'EOF'
{{- define "india-squad-api.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "india-squad-api.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{- end }}
EOF
# india-squad-api/templates/deployment.yaml
cat > india-squad-api/templates/deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "india-squad-api.fullname" . }}
labels:
{{- include "india-squad-api.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
containers:
- name: {{ .Chart.Name }}
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports: [{containerPort: {{ .Values.service.targetPort }}}]
env:
- {name: LOG_LEVEL, value: {{ .Values.env.LOG_LEVEL | quote }}}
- {name: ENVIRONMENT, value: {{ .Values.env.ENVIRONMENT | quote }}}
resources:
{{- toYaml .Values.resources | nindent 12 }}
EOF
# india-squad-api/templates/service.yaml
cat > india-squad-api/templates/service.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
name: {{ include "india-squad-api.fullname" . }}
labels: {{- include "india-squad-api.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.service.targetPort }}
selector:
app.kubernetes.io/instance: {{ .Release.Name }}
EOFStep 3 — Environment Values Files
Create the three environment values files, each containing only the fields that differ from the chart defaults. The dev file switches to a locally built image with `pullPolicy: Never`. The staging file adds two replicas, enables the ingress with a staging hostname, and switches to ECR with the staging tag. The production file enables autoscaling with a three to fifteen replica range, increases resource limits, and sets the log level to warning to reduce log volume.
# Step 3: Create values files for dev, staging, and production.
# envs/dev/values.yaml — minimal, uses local image
cat > envs/dev/values.yaml << 'EOF'
image:
repository: india-squad-api
tag: dev
pullPolicy: Never # use locally built image in kind cluster
env:
LOG_LEVEL: debug
ENVIRONMENT: dev
EOF
# envs/staging/values.yaml — two replicas, real ECR image, ingress enabled
cat > envs/staging/values.yaml << 'EOF'
replicaCount: 2
image:
repository: 123456789.dkr.ecr.ap-south-1.amazonaws.com/india-squad-api
tag: staging
pullPolicy: Always
ingress:
enabled: true
host: api.staging.india-squad.example.com
tlsSecret: india-squad-staging-tls
env:
LOG_LEVEL: info
ENVIRONMENT: staging
EOF
# envs/prod/values.yaml — five replicas, autoscaling, production ECR tag
cat > envs/prod/values.yaml << 'EOF'
replicaCount: 5
image:
repository: 123456789.dkr.ecr.ap-south-1.amazonaws.com/india-squad-api
tag: v1.0.0
pullPolicy: IfNotPresent
ingress:
enabled: true
host: api.india-squad.example.com
tlsSecret: india-squad-prod-tls
resources:
requests: { cpu: 250m, memory: 128Mi }
limits: { cpu: 1000m, memory: 512Mi }
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 15
env:
LOG_LEVEL: warning
ENVIRONMENT: prod
EOFStep 4 — Test, Lint and Verify
Add a Helm test pod that curls the health endpoint and validate the complete chart with helm lint, helm template dry-run, and helm test against a dev cluster. The lint step catches YAML syntax errors and missing required values before any deployment attempt. The template dry-run renders the templates to stdout, allowing visual inspection of the rendered production manifests before applying them to a cluster.
# Step 4: Add a Helm test and lint with ct.
# india-squad-api/tests/test-health.yaml
mkdir -p india-squad-api/tests
cat > india-squad-api/tests/test-health.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
name: {{ include "india-squad-api.fullname" . }}-test-health
annotations:
helm.sh/hook: test
spec:
restartPolicy: Never
containers:
- name: test
image: curlimages/curl:8.7.1
command:
- sh
- '-c'
- |
curl -f -s http://{{ include "india-squad-api.fullname" . }}:{{ .Values.service.port }}/health
echo 'Health check passed for india-squad-api'
EOF
# Lint the chart (catches YAML errors and template issues)
helm lint india-squad-api/
helm lint india-squad-api/ -f envs/staging/values.yaml
helm lint india-squad-api/ -f envs/prod/values.yaml
# Dry-run: render templates and show what would be applied
helm template india-squad-api india-squad-api/ -f envs/prod/values.yaml
# Install to a local kind cluster (dev environment)
helm upgrade --install india-squad-api india-squad-api/ \
-f envs/dev/values.yaml \
--namespace squad-dev --create-namespace
# Run the Helm test
helm test india-squad-api --namespace squad-dev
# Expected: Pod india-squad-api-test-health completed successfully
# Diff before staging upgrade
helm diff upgrade india-squad-api india-squad-api/ \
-f envs/staging/values.yaml --namespace squad-stagingWarning: The `image.pullPolicy: Never` setting in the dev values file works only when the image has been built and loaded into the kind cluster's local registry with `kind load docker-image india-squad-api:dev`. If the image is not loaded, the pod will fail with `ErrImageNeverPull`. Always run `docker build -t india-squad-api:dev .` and `kind load docker-image india-squad-api:dev` before running the dev helm install. A `Makefile` target that runs build, load, and install in sequence makes this workflow repeatable.
Extension Challenge: Add an HPA template to the chart, gated on `autoscaling.enabled`. Combine it with the conditional replica count in the Deployment so that when autoscaling is enabled the Deployment has no explicit replica count (allowing the HPA to manage it freely) and when disabled the Deployment uses `values.replicaCount`. Test the HPA template by running `helm template` with both the dev (autoscaling disabled) and prod (autoscaling enabled) values files and confirming that the HPA appears only in the prod output and the replica count appears only in the dev output.