Helm in CI/CD Pipelines
Integrate Helm into your CI/CD workflows. Automate chart testing, building, and deployment with GitHub Actions and more.
Helm in CI/CD Pipelines
In the previous tutorial, we learned how to test Helm charts thoroughly. Now let's wire everything into CI/CD pipelines so charts are tested, published, and deployed automatically. No more manual helm upgrade in production. Ever.
The CI/CD Workflow
Here's what a complete Helm CI/CD pipeline looks like:
PR Opened / Push to Main
│
├── Lint & Validate
│ ├── helm lint --strict
│ ├── helm template | kubeconform
│ └── helm unittest
│
├── Integration Test (on PR)
│ ├── Deploy to test cluster
│ ├── helm test
│ └── Cleanup
│
├── Package & Publish (on main)
│ ├── helm package
│ └── Push to OCI / chart repo
│
└── Deploy (on main / tag)
├── Deploy to staging (auto)
└── Deploy to production (manual approval)
GitHub Actions — Chart Testing
Lint and Unit Test on PRs
# .github/workflows/chart-test.yml
name: Chart Testing
on:
pull_request:
paths:
- 'charts/**'
jobs:
lint-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Helm
uses: azure/setup-helm@v3
with:
version: v3.14.0
- name: Install helm-unittest
run: helm plugin install https://github.com/helm-unittest/helm-unittest
- name: Lint charts
run: |
for chart in charts/*/; do
echo "Linting $chart..."
helm lint "$chart" --strict
done
- name: Run unit tests
run: |
for chart in charts/*/; do
if [ -d "$chart/tests" ]; then
echo "Testing $chart..."
helm unittest "$chart"
fi
done
- name: Validate rendered manifests
run: |
brew install kubeconform
for chart in charts/*/; do
echo "Validating $chart..."
helm template test "$chart" | kubeconform -strict -kubernetes-version 1.28.0
done
Integration Test with Kind
For a full integration test, spin up a local Kubernetes cluster:
# .github/workflows/chart-integration.yml
name: Chart Integration Test
on:
pull_request:
paths:
- 'charts/**'
jobs:
integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Helm
uses: azure/setup-helm@v3
- name: Create Kind cluster
uses: helm/kind-action@v1
- name: Set up chart-testing
uses: helm/chart-testing-action@v2
- name: Detect changed charts
id: changed
run: |
changed=$(ct list-changed --target-branch main)
if [ -n "$changed" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
fi
- name: Lint and install changed charts
if: steps.changed.outputs.changed == 'true'
run: ct lint-and-install --target-branch main
GitHub Actions — Release and Publish
Publish to OCI Registry
# .github/workflows/release-oci.yml
name: Release Charts (OCI)
on:
push:
branches: [main]
paths:
- 'charts/**'
jobs:
release:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v3
- name: Login to GHCR
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \
helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Package and push charts
run: |
for chart in charts/*/; do
chart_name=$(basename "$chart")
chart_version=$(grep '^version:' "$chart/Chart.yaml" | awk '{print $2}')
echo "Packaging $chart_name v$chart_version..."
helm package "$chart" --dependency-update
echo "Pushing to GHCR..."
helm push "${chart_name}-${chart_version}.tgz" \
oci://ghcr.io/${{ github.repository_owner }}/charts
done
Publish to GitHub Pages
Using chart-releaser:
# .github/workflows/release-pages.yml
name: Release Charts (GitHub Pages)
on:
push:
branches: [main]
paths:
- 'charts/**'
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure Git
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Set up Helm
uses: azure/setup-helm@v3
- name: Run chart-releaser
uses: helm/chart-releaser-action@v1
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
GitHub Actions — Deployment
Deploy to Kubernetes
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v3
- name: Configure kubectl
uses: azure/k8s-set-context@v3
with:
method: kubeconfig
kubeconfig: ${{ secrets.KUBE_CONFIG_STAGING }}
- name: Deploy to staging
run: |
helm upgrade --install my-app ./charts/my-app \
--namespace staging \
--create-namespace \
-f charts/my-app/values-staging.yaml \
--set image.tag=${{ github.sha }} \
--atomic \
--timeout 10m \
--wait
- name: Run integration tests
run: helm test my-app --namespace staging --timeout 5m
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # Requires manual approval in GitHub
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v3
- name: Configure kubectl
uses: azure/k8s-set-context@v3
with:
method: kubeconfig
kubeconfig: ${{ secrets.KUBE_CONFIG_PRODUCTION }}
- name: Deploy to production
run: |
helm upgrade --install my-app ./charts/my-app \
--namespace production \
--create-namespace \
-f charts/my-app/values-production.yaml \
--set image.tag=${{ github.sha }} \
--atomic \
--timeout 10m \
--wait
Environment-Specific Values
Structure your values files by environment:
charts/my-app/
├── Chart.yaml
├── values.yaml # Defaults (dev-friendly)
├── values-staging.yaml # Staging overrides
├── values-production.yaml # Production overrides
└── templates/
# values.yaml (defaults — dev)
replicaCount: 1
image:
repository: my-app
tag: "latest"
resources:
limits:
cpu: 100m
memory: 128Mi
# values-staging.yaml
replicaCount: 2
resources:
limits:
cpu: 250m
memory: 256Mi
# values-production.yaml
replicaCount: 5
resources:
limits:
cpu: 1000m
memory: 1Gi
ingress:
enabled: true
hosts:
- host: api.example.com
paths:
- path: /
pathType: Prefix
autoscaling:
enabled: true
minReplicas: 5
maxReplicas: 20
Helmfile — Managing Multiple Releases
When you deploy many services, managing individual helm upgrade commands gets tedious. Helmfile provides a declarative way to manage multiple Helm releases:
# helmfile.yaml
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
environments:
staging:
values:
- environments/staging.yaml
production:
values:
- environments/production.yaml
releases:
- name: api
chart: ./charts/api
namespace: "{{ .Environment.Name }}"
values:
- charts/api/values.yaml
- charts/api/values-{{ .Environment.Name }}.yaml
set:
- name: image.tag
value: "{{ requiredEnv \"IMAGE_TAG\" }}"
- name: worker
chart: ./charts/worker
namespace: "{{ .Environment.Name }}"
values:
- charts/worker/values.yaml
- charts/worker/values-{{ .Environment.Name }}.yaml
- name: redis
chart: bitnami/redis
version: 18.4.0
namespace: "{{ .Environment.Name }}"
values:
- config/redis-{{ .Environment.Name }}.yaml
# Deploy everything to staging
helmfile -e staging apply
# Deploy to production
helmfile -e production apply
# Diff before applying
helmfile -e production diff
# Sync a specific release
helmfile -e staging -l name=api apply
Helmfile in CI
# .github/workflows/deploy-helmfile.yml
- name: Deploy with Helmfile
run: |
helmfile -e ${{ inputs.environment }} apply
env:
IMAGE_TAG: ${{ github.sha }}
ArgoCD — GitOps Deployment
For GitOps workflows, ArgoCD watches your Git repo and automatically syncs Helm releases:
# argo-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/deployments.git
targetRevision: main
path: charts/my-app
helm:
valueFiles:
- values-production.yaml
parameters:
- name: image.tag
value: "latest" # Or use image updater
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
With ArgoCD:
- Merge to main triggers a sync
- ArgoCD renders the Helm chart
- Diffs against live state
- Applies changes automatically (or waits for approval)
- Monitors health and reports status
Best Practices for Helm in CI/CD
-
Pin chart and app versions — Never deploy unversioned charts. Tag images with git SHA, not
latest. -
Use
--atomic— Auto-rollback on failure. There's no reason not to. -
Use
--wait— Don't mark a deploy as successful until pods are ready. -
Store values in Git — Track environment config alongside your chart for full auditability.
-
Use environments with approvals — Production deploys should require human approval (GitHub Environments, ArgoCD sync windows).
-
Namespace per environment — Isolate staging from production.
-
Test before deploy — The pipeline should run lint → unit test → integration test → deploy staging → deploy production.
-
Helm diff before apply — In non-automated flows, always review what will change.
What's Next?
You've made it! You've gone from "what is Helm?" to running production CI/CD pipelines. Here's what you covered across this entire series:
- Basics: What Helm is, installing it, creating your first chart
- Structure: Chart anatomy, values, built-in objects
- Templating: Functions, pipelines, flow control, named templates
- Advanced: Dependencies, hooks, releases, rollbacks
- Distribution: Repositories, packaging, OCI registries
- Quality: Linting, unit tests, integration tests
- Automation: CI/CD pipelines, Helmfile, GitOps
From here, real mastery comes from practice. Build charts for your own applications, contribute to open-source charts, and explore the Helm documentation for edge cases.
Happy Helming! 🎉