This lesson assumes you already know Kubernetes, Helm, and AKS basics. The focus here is the Azure DevOps delivery orchestration and operational patterns that connect to those platforms.
Deploy to AKS with Helm
Implement a real Azure DevOps delivery path: build container image, push to Azure Container Registry, then deploy to Azure Kubernetes Service with Helm using controlled multi-environment promotion.
🧒 Simple Explanation (ELI5)
Think of your app as a product going through a shipping company. First the factory builds it. Then it gets stored in a warehouse. Then a delivery truck brings it to the customer using clear delivery instructions.
- Build creates the container image.
- ACR stores that image like a warehouse.
- AKS runs the application for users.
- Helm is the instruction sheet telling AKS how to install and configure the app.
The Azure DevOps pipeline coordinates the full journey automatically and records exactly what was shipped where.
🔧 Technical Explanation
Target Delivery Flow
Core Dependencies
| Dependency | Purpose | Example |
|---|---|---|
| Service connection | Authenticate pipeline to Azure | Azure Resource Manager connection |
| ACR | Store built image | contosoregistry.azurecr.io |
| AKS | Run workloads | aks-prod-eastus |
| Helm chart | Package Kubernetes manifests | charts/webapp |
| Variable groups | Store environment-specific config | namespace, ingress host, chart path |
Build and Push Stage
stages:
- stage: Build
jobs:
- job: BuildImage
pool:
vmImage: ubuntu-latest
steps:
- task: Docker@2
displayName: 'Build and push image'
inputs:
command: buildAndPush
repository: 'webapp'
dockerfile: 'Dockerfile'
containerRegistry: 'sc-acr-prod'
tags: |
$(Build.BuildId)
latestIn production, prefer immutable tags like $(Build.SourceVersion) or $(Build.BuildId). Keep latest only as a convenience tag, never as the source of truth for release selection.
Deploy Stage with Helm
- stage: Deploy_Prod
dependsOn: Build
jobs:
- deployment: HelmDeployProd
environment: production
strategy:
runOnce:
deploy:
steps:
- checkout: self
- task: HelmInstaller@1
inputs:
helmVersionToInstall: 'latest'
- task: AzureCLI@2
displayName: 'AKS get-credentials'
inputs:
azureSubscription: 'sc-aks-prod'
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az aks get-credentials \
--resource-group $(resourceGroup) \
--name $(clusterName) \
--overwrite-existing
- script: |
helm upgrade --install webapp ./charts/webapp \
--namespace $(namespace) \
--create-namespace \
--set image.repository=$(acrLoginServer)/webapp \
--set image.tag=$(Build.BuildId) \
--set ingress.host=$(ingressHost) \
--wait --timeout 10m
displayName: 'Helm upgrade'Deployment Strategy Choices
| Strategy | How It Works | Best For |
|---|---|---|
| Rolling update | Gradually replaces pods | Default low-complexity deployments |
| Blue-green | Deploys a parallel environment, then switches traffic | Low-risk cutover with fast rollback |
| Canary | Sends limited traffic to new version first | High-confidence progressive delivery |
The Azure DevOps part is mostly about orchestrating validation and environment promotion. Helm and ingress configuration handle the platform-specific rollout details.
Production-Grade Real-World Pipeline
The minimal example above proves the concept. A real team usually wants stronger guardrails: PR validation, immutable image tags, staging smoke tests, explicit artifact flow, and production approval through environments.
trigger:
branches:
include:
- main
paths:
include:
- src/*
- charts/*
- Dockerfile
pr:
branches:
include:
- main
variables:
- group: aks-shared
- name: imageRepository
value: 'webapp'
- name: imageTag
value: '$(Build.SourceVersion)'
stages:
- stage: Validate
displayName: 'Lint and test'
jobs:
- job: AppChecks
pool:
vmImage: ubuntu-latest
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
- script: npm ci
displayName: 'Install dependencies'
- script: npm run lint
displayName: 'Run lint'
- script: npm test -- --ci
displayName: 'Run unit tests'
- stage: Build_Image
displayName: 'Build and push image to ACR'
dependsOn: Validate
condition: succeeded()
jobs:
- job: BuildPush
pool:
vmImage: ubuntu-latest
steps:
- task: Docker@2
displayName: 'Build and push image'
inputs:
command: buildAndPush
containerRegistry: 'sc-acr-prod'
repository: '$(imageRepository)'
dockerfile: 'Dockerfile'
tags: |
$(imageTag)
$(Build.BuildId)
- publish: charts/webapp
artifact: helm-chart
- stage: Deploy_Staging
displayName: 'Deploy to staging AKS'
dependsOn: Build_Image
jobs:
- deployment: StagingDeploy
environment: staging
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: helm-chart
- task: HelmInstaller@1
inputs:
helmVersionToInstall: 'latest'
- task: AzureCLI@2
inputs:
azureSubscription: 'sc-aks-staging'
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az aks get-credentials --resource-group $(resourceGroup) --name $(stagingClusterName) --overwrite-existing
helm upgrade --install webapp $(Pipeline.Workspace)/helm-chart \
--namespace staging \
--create-namespace \
--set image.repository=$(acrLoginServer)/$(imageRepository) \
--set image.tag=$(imageTag) \
--set ingress.host=$(stagingIngressHost) \
--wait --timeout 10m
- stage: Smoke_Test
displayName: 'Validate staging release'
dependsOn: Deploy_Staging
jobs:
- job: SmokeTest
pool:
vmImage: ubuntu-latest
steps:
- bash: |
curl --fail --retry 5 --retry-delay 10 https://$(stagingIngressHost)/health
displayName: 'Run smoke test'
- stage: Deploy_Production
displayName: 'Deploy to production AKS'
dependsOn: Smoke_Test
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: ProductionDeploy
environment: production
strategy:
runOnce:
deploy:
steps:
- task: HelmInstaller@1
inputs:
helmVersionToInstall: 'latest'
- task: AzureCLI@2
inputs:
azureSubscription: 'sc-aks-prod'
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az aks get-credentials --resource-group $(resourceGroup) --name $(prodClusterName) --overwrite-existing
helm upgrade --install webapp ./charts/webapp \
--namespace production \
--create-namespace \
--set image.repository=$(acrLoginServer)/$(imageRepository) \
--set image.tag=$(imageTag) \
--set ingress.host=$(prodIngressHost) \
--wait --timeout 10m| Design Choice | Why It Is Used |
|---|---|
$(Build.SourceVersion) tag | Creates an immutable mapping from deployed image back to the exact commit. |
| Separate staging and production service connections | Limits blast radius and keeps production access isolated. |
| Smoke test stage before production | Stops promotion if the release is alive technically but unhealthy functionally. |
| Environment-based production deployment | Allows approvals and checks outside the editable YAML file. |
| Published chart artifact | Ensures later stages use the exact build output instead of rebuilding. |
🛠️ Hands-on
Minimal End-to-End Multi-Stage Example
trigger:
- main
variables:
- group: aks-prod-shared
stages:
- stage: Verify
jobs:
- job: VerifyApp
pool:
vmImage: ubuntu-latest
steps:
- script: npm ci
- script: npm test
- stage: Build
dependsOn: Verify
jobs:
- job: BuildPush
steps:
- task: Docker@2
inputs:
command: buildAndPush
containerRegistry: 'sc-acr-prod'
repository: 'webapp'
dockerfile: 'Dockerfile'
tags: '$(Build.BuildId)'
- stage: Deploy
dependsOn: Build
jobs:
- deployment: Prod
environment: production
strategy:
runOnce:
deploy:
steps:
- task: AzureCLI@2
inputs:
azureSubscription: 'sc-aks-prod'
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az aks get-credentials --resource-group $(resourceGroup) --name $(clusterName) --overwrite-existing
- script: |
helm upgrade --install webapp ./charts/webapp \
--namespace $(namespace) \
--set image.repository=$(acrLoginServer)/webapp \
--set image.tag=$(Build.BuildId) \
--wait --timeout 10mRollbacks
- Use
helm historyandhelm rollbackfor release-level rollback. - Keep image tags immutable so you can redeploy a prior known-good version quickly.
- Use environment history in Azure DevOps to identify which run deployed which version.
🐛 Debugging Scenarios
Scenario 1: Pipeline Can Push to ACR but AKS Cannot Pull Image
- Check AKS-to-ACR integration or image pull secret configuration.
- Inspect pod events for
ImagePullBackOff. - Verify the exact image tag exists in ACR.
Scenario 2: Helm Upgrade Times Out
- Run
kubectl get pods -n <namespace>andkubectl describe pod. - Check readiness and liveness probe failures.
- Confirm the new image actually starts in the target environment.
Scenario 3: Azure CLI Task Fails with Authorization Error
- Review the Azure service connection identity and role assignments.
- Confirm access to both the AKS cluster and ACR resources.
- Check whether the subscription or resource group changed since the service connection was created.
Many deployment failures are Kubernetes or application failures surfaced by the pipeline. Distinguish authentication problems, image distribution problems, and cluster health problems before changing the pipeline itself.
📋 Interview Questions
Beginner
ACR stores the built container images so AKS can pull and run them. It is the registry layer between build and cluster deployment.
Helm packages Kubernetes manifests into reusable charts, making environment-specific deployment inputs easier to manage than raw manifest duplication.
az aks get-credentials do?It fetches cluster access credentials and writes kubeconfig so kubectl and Helm can communicate with the AKS cluster.
Immutable tags provide traceability, safer rollback, and certainty about exactly what version was deployed.
Yes. YAML multi-stage pipelines are the modern and preferred approach.
Intermediate
Pushing to ACR publishes a container image into the registry. Deploying to AKS updates the cluster workload to reference and run that image.
I focus on the delivery chain: artifact traceability, service connections, environment approvals, Helm parameterization, and troubleshooting handoff between the pipeline and the platform.
I keep shared defaults in the chart and inject environment-specific values via values files or explicit pipeline inputs, avoiding copy-paste chart forks.
The image should be built and tested, lower-environment validation should complete, and environment checks or approvals should pass.
Use Helm release history and rollback or redeploy a previous immutable image tag, depending on the root cause and operational preference.
Scenario-Based
I verify ingress, service, pod health, probes, logs, and any config changes introduced by the release. The green pipeline only proves the orchestration completed, not that the application is healthy.
The Helm values may still reference the old tag, the deployment may not have rolled out, or a failed release may have left the previous ReplicaSet active. I inspect the live deployment spec and rollout status.
I use least-privilege service connections, protected environments, restricted variable groups, approval checks, and strong auditing around who can edit deploy logic or approve production releases.
When the application is highly critical, rollback speed matters, and the platform can afford the extra environment or routing complexity.
Azure DevOps validates the app, builds an immutable container, stores it in ACR, and orchestrates a controlled Helm-based rollout into AKS environments with auditability and rollback options.
🌍 Real-World Usage
This pattern is common in enterprise Azure delivery: code in Repos, CI in Azure Pipelines, package in ACR, runtime on AKS, and chart-driven deployment with Helm. The delivery system becomes the glue that connects source control, security, and operations without duplicating Kubernetes concepts already defined elsewhere.
🧾 Summary
Deploying to AKS with Helm from Azure DevOps is about connecting validated builds to controlled cluster releases. The operational quality comes from immutable tags, strong authentication, environment checks, and clear separation between pipeline orchestration and platform diagnosis.