100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
CI/CD, GitOps, DevSecOps & Observability
55 minintermediate

Practice — package a FastAPI app as a Helm chart with 3 environments

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.

Analogy🏏Cricket
Think of it like cricket: Picture setting up a remote ground management system for a cricket ground in a new city. First, the infrastructure must be installed: the pitch sensors, the scoreboard network, and the broadcast uplink—equivalent to installing ArgoCD on the EKS cluster. Then, the venue configuration must be committed to the central venue management database: the pitch dimensions, the lighting schedule, the boundary positions—equivalent to committing the Kubernetes manifests to the GitOps repository. Then, the venue must be registered with the central management system, which then automatically enforces the declared configuration at the ground—equivalent to creating the ArgoCD Application that connects the repository to the cluster. When a ground manager moves a boundary rope by hand, the sensors detect the drift and alert the management system to restore the declared position—equivalent to ArgoCD detecting and reverting the manual replica scale. This reveals why the lab sequence matters: you cannot verify GitOps until all three components—operator, repository, and Application—are connected and working together.

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.

Analogy🏏Cricket
🏏 Think of it like cricket: Setting up a new academy, you start from the board's standard blueprint but strip out the generic sample sessions and write your own, so you understand every drill you run — exactly as helm create gives you a chart skeleton and you then delete the default templates to author each line yourself. Just as you keep each squad's specific fitness numbers in a separate per-group sheet rather than baking them into the master blueprint, you place environment-specific values in an envs/ directory outside the chart so the chart itself stays generic. Just as one blueprint plus three group sheets covers under-19, state and national squads, one chart plus dev, staging and prod values files covers three environments. Just as an academy that understands every drill can adapt it safely, a chart you wrote line by line holds no opaque defaults to surprise you. The payoff: a clean, fully understood scaffold with variation isolated in values files makes every later change safe and obvious.
bash
# 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.

Analogy🏏Cricket
Think of it like cricket: Think of the values.yaml as the standard training kit that every squad member gets by default: adequate for all standard drills, conservative enough to avoid injuries for new players. The staging and production values files are the specialist upgrades given to players who need them: heavier protective gear for the senior batters facing pace bowlers, advanced pitch-reading devices for the first-choice XI. The default kit does not limit what any player can do; it just ensures everyone has a safe, functional baseline without over-provisioning. Just as the kit manager does not issue full international-grade equipment to every Under-19 trialist, the default values do not provision production-grade resources for every dev environment installation.
bash
# 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
EOF

Step 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.

Analogy🏏Cricket
Think of it like cricket: The _helpers.tpl named templates are the equivalent of the team's standard communication codes—a set of agreed abbreviations that all players use consistently: 'RFC' always means 'rotate the strike', 'DB' always means 'defend the ball'. Just as using the standard code ensures that every player understands the same instruction without ambiguity, using `include india-squad-api.labels` in every resource ensures every resource in the chart has exactly the same label set with no manual copy-paste differences between the Deployment and the Service. Just as a player who invents their own code creates confusion, a chart that computes the labels differently in each template creates selector mismatches that break Kubernetes service discovery.
bash
# 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 }}
EOF

Step 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.

Analogy🏏Cricket
Think of it like cricket: Think of the three values files as the venue-specific match-day briefing documents distributed on top of the standard pre-match preparation. The standard preparation covers everything every player needs regardless of venue. The Mumbai briefing adds: 'the outfield is fast, favour running singles over risky twos; the floodlights create a shadow in the deep third-man region.' The Kolkata briefing adds: 'the pitch has extra bounce in the afternoon session; adjust the bat angle.' Just as the briefing documents contain only the venue-specific adjustments rather than rewriting the full preparation plan, the values files contain only the environment-specific overrides rather than rewriting the full chart values.
bash
# 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
EOF

Step 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.

Analogy🏏Cricket
🏏 Think of it like cricket: Before a match you run graded checks — the kit inspection catches missing or malformed equipment, the dress rehearsal renders the full game plan so you can eyeball it, and a warm-up fixture proves the team actually performs on grass. That is exactly the chart verification ladder: helm lint catches YAML errors and missing required values, helm template dry-run renders the manifests to stdout for visual inspection, and helm test deploys a pod that curls the health endpoint against a dev cluster. Just as each check is cheaper and earlier than discovering the fault mid-match, lint fails in milliseconds before any cluster is touched, the dry-run reveals a bad render before applying, and the test confirms the live pod answers. Just as passing the warm-up gives the captain confidence to name the side, a green helm test gives you confidence to promote the chart. The payoff: layered validation catches defects at the cheapest possible stage.
bash
# 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-staging

Warning: 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.

Lesson 6 of 33
0% complete