Raw manifests with sed replacements don't scale. Helm gives you templating, versioning, and atomic deploys with built-in rollback. This lesson wires Helm into your pipeline.

Why Helm > Raw Manifests in CI/CD

Raw Manifests + sed • sed "s|IMAGE|acr.io/app:sha-x|" deployment.yml • Different files per environment • No rollback tracking • Fails mid-apply? Broken state. Helm • --set image.tag=sha-x (typed values) • values-staging.yaml / values-prod.yaml • helm rollback (release history) • --atomic (auto-rollback on failure!)
--atomic is the killer feature for CI/CD. If any resource in the Helm release fails to become healthy, Helm automatically rolls back to the previous version. Your pipeline never leaves the cluster in a broken state.

Helm Chart Structure (Quick Recap)

📁 helm/cicd-mastery/ ├── Chart.yaml ← name, version, description ├── values.yaml ← default values (all params) ├── values-staging.yaml ← overrides for staging ├── values-production.yaml ← overrides for production └── templates/ ← K8s manifests with {{ .Values.x }} ├── deployment.yaml ├── service.yaml └── hpa.yaml

🏋️ Step 1: Create a Helm Chart

# Generate chart scaffold
helm create helm/cicd-mastery

# Clean out the defaults we don't need
rm -rf helm/cicd-mastery/templates/tests
rm helm/cicd-mastery/templates/ingress.yaml
rm helm/cicd-mastery/templates/serviceaccount.yaml

Edit helm/cicd-mastery/values.yaml:

replicaCount: 2

image:
  repository: acrcicdmastery.azurecr.io/cicd-mastery-api
  tag: "latest"    # Overridden by pipeline
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80
  targetPort: 3000

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi

health:
  path: /health
  port: 3000

env:
  NODE_ENV: production

autoscaling:
  enabled: false
  minReplicas: 2
  maxReplicas: 10
  targetCPU: 70

Create helm/cicd-mastery/values-staging.yaml:

replicaCount: 1
env:
  NODE_ENV: staging
  LOG_LEVEL: debug
resources:
  requests: { cpu: 50m, memory: 64Mi }
  limits:   { cpu: 200m, memory: 128Mi }

Create helm/cicd-mastery/values-production.yaml:

replicaCount: 3
autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 20
env:
  NODE_ENV: production
  LOG_LEVEL: warn
resources:
  requests: { cpu: 200m, memory: 256Mi }
  limits:   { cpu: 1000m, memory: 512Mi }

🏋️ Step 2: Helm Deploy in Pipeline

Replace the sed + kubectl apply approach with Helm:

      - name: Deploy with Helm
        run: |
          helm upgrade --install cicd-mastery ./helm/cicd-mastery \
            --namespace staging \
            --create-namespace \
            --values ./helm/cicd-mastery/values-staging.yaml \
            --set image.tag=${{ env.IMAGE_TAG }} \
            --wait \
            --timeout 5m \
            --atomic

What Each Flag Does

FlagPurposeWhy It Matters
--installInstall if new, upgrade if existsIdempotent — safe to re-run
--create-namespaceCreate ns if missingNo manual setup needed
--values file.yamlLoad environment-specific valuesSame chart, different config
--set image.tag=XOverride one value at deploy timeInject the CI-built image tag
--waitBlock until pods are readyPipeline knows if deploy succeeded
--timeout 5mFail if not ready in 5 minDon't hang forever
--atomicAuto-rollback on failureNever leave broken state

Helm Release History = Deployment Audit Trail

$ helm history cicd-mastery -n staging REVISION UPDATED STATUS DESCRIPTION 1 2024-01-15 10:30:00 superseded Install complete 2 2024-01-15 14:22:00 superseded Upgrade complete 3 2024-01-16 09:15:00 deployed Upgrade complete
Every helm upgrade creates a revision. Rollback to any previous revision instantly.
# Rollback in pipeline on verification failure:
helm rollback cicd-mastery -n staging    # Goes to revision 2

# Or rollback to specific revision:
helm rollback cicd-mastery 1 -n staging  # Goes to revision 1

Pattern: Deploy + Verify + Rollback

      - name: Deploy
        id: deploy
        run: |
          helm upgrade --install cicd-mastery ./helm/cicd-mastery \
            --namespace production \
            --values ./helm/cicd-mastery/values-production.yaml \
            --set image.tag=${{ env.IMAGE_TAG }} \
            --wait --timeout 5m --atomic

      - name: Integration test
        id: verify
        run: |
          # Test critical paths
          curl -f "$APP_URL/health" || exit 1
          curl -f "$APP_URL/" | jq .version || exit 1

      - name: Rollback on failure
        if: failure() && steps.deploy.outcome == 'success'
        run: |
          echo "::error::Verification failed! Rolling back..."
          helm rollback cicd-mastery -n production
          echo "## ⚠️ ROLLBACK EXECUTED" >> $GITHUB_STEP_SUMMARY
This pattern gives you two layers of protection:
  1. --atomic rolls back if pods don't start (infrastructure failure)
  2. The verify step rolls back if the app starts but behaves incorrectly (logic failure)

🧠 Recall Check

  1. What does --atomic do when a Helm upgrade fails?
  2. How do you use different configuration for staging vs production with the same chart?
  3. What's the command to see deployment history in Helm?
  4. Why is --wait important in a pipeline (vs without it)?
Reveal answers
  1. Automatically rolls back to the previous revision. The cluster is never left in a broken state.
  2. Separate values files: --values values-staging.yaml vs --values values-production.yaml. Same templates, different configuration.
  3. helm history <release-name> -n <namespace>
  4. Without --wait, Helm returns immediately after submitting resources (doesn't verify they're healthy). With it, Helm blocks until all pods pass readiness checks — so the pipeline knows the deployment actually worked.

Next lesson: Reusable Workflows — creating pipeline templates that multiple services can share.