Helm Charts for Azure Kubernetes Service
Introduction
Helm is the package manager for Kubernetes, enabling you to define, install, and upgrade complex Kubernetes applications. For Azure Kubernetes Service (AKS), Helm charts streamline deployments with Azure-specific configurations like managed identities, Key Vault integration, and Azure Monitor. This guide covers creating production-ready Helm charts for AKS.
Chart Structure
myapp/
├── Chart.yaml
├── values.yaml
├── values-dev.yaml
├── values-prod.yaml
├── templates/
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── hpa.yaml
│ ├── serviceaccount.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ ├── pdb.yaml
│ └── tests/
│ └── test-connection.yaml
└── charts/
Chart.yaml
apiVersion: v2
name: myapp
description: A Helm chart for deploying my application to AKS
type: application
version: 1.0.0
appVersion: "1.0.0"
keywords:
- dotnet
- api
- azure
maintainers:
- name: Platform Team
email: platform@company.com
dependencies:
- name: redis
version: "17.x.x"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled
Values Configuration
values.yaml (Default)
# Default values for myapp
replicaCount: 2
repository: myacr.azurecr.io/myapp
pullPolicy: IfNotPresent
tag: "" # Overridden by appVersion
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations:
azure.workload.identity/client-id: ""
name: ""
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "80"
prometheus.io/path: "/metrics"
podSecurityContext:
fsGroup: 1000
securityContext:
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
service:
type: ClusterIP
port: 80
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: api.myapp.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: api-tls
hosts:
- api.myapp.com
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
nodeSelector:
kubernetes.io/os: linux
tolerations: []
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- myapp
topologyKey: topology.kubernetes.io/zone
# Azure-specific configuration
azure:
keyVault:
enabled: true
name: kv-myapp-prod
tenantId: ""
secrets:
- secretName: connection-string
keyVaultKey: DatabaseConnectionString
- secretName: api-key
keyVaultKey: ExternalApiKey
appInsights:
enabled: true
connectionString: ""
managedIdentity:
enabled: true
clientId: ""
# Application configuration
config:
aspnetcoreEnvironment: Production
logLevel: Information
# Feature flags
features:
enableSwagger: false
enableHealthUI: true
# Redis subchart configuration
redis:
enabled: true
architecture: standalone
auth:
enabled: true
existingSecret: redis-secret
values-prod.yaml
replicaCount: 3
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
minReplicas: 3
maxReplicas: 20
ingress:
hosts:
- host: api.myapp.com
paths:
- path: /
pathType: Prefix
azure:
keyVault:
name: kv-myapp-prod
managedIdentity:
clientId: "prod-client-id"
config:
aspnetcoreEnvironment: Production
logLevel: Warning
features:
enableSwagger: false
Template Files
_helpers.tpl
{{/*
Expand the name of the chart.
*/}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "myapp.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "myapp.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ include "myapp.chart" . }}
{{ include "myapp.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "myapp.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "myapp.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
{{- if .Values.azure.managedIdentity.enabled }}
azure.workload.identity/use: "true"
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "myapp.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 80
protocol: TCP
env:
- name: ASPNETCORE_ENVIRONMENT
value: {{ .Values.config.aspnetcoreEnvironment | quote }}
- name: Logging__LogLevel__Default
value: {{ .Values.config.logLevel | quote }}
{{- if .Values.azure.appInsights.enabled }}
- name: APPLICATIONINSIGHTS_CONNECTION_STRING
valueFrom:
secretKeyRef:
name: {{ include "myapp.fullname" . }}-secrets
key: appinsights-connection-string
{{- end }}
{{- if .Values.redis.enabled }}
- name: Redis__ConnectionString
valueFrom:
secretKeyRef:
name: redis-secret
key: redis-password
{{- end }}
envFrom:
- configMapRef:
name: {{ include "myapp.fullname" . }}-config
{{- if .Values.azure.keyVault.enabled }}
volumeMounts:
- name: secrets-store
mountPath: "/mnt/secrets-store"
readOnly: true
{{- end }}
livenessProbe:
httpGet:
path: /health/live
port: http
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if .Values.azure.keyVault.enabled }}
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: {{ include "myapp.fullname" . }}-secrets
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
secretproviderclass.yaml (Key Vault Integration)
{{- if .Values.azure.keyVault.enabled }}
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: {{ include "myapp.fullname" . }}-secrets
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
provider: azure
parameters:
usePodIdentity: "false"
useVMManagedIdentity: "false"
clientID: {{ .Values.azure.managedIdentity.clientId | quote }}
keyvaultName: {{ .Values.azure.keyVault.name | quote }}
tenantId: {{ .Values.azure.keyVault.tenantId | quote }}
objects: |
array:
{{- range .Values.azure.keyVault.secrets }}
- |
objectName: {{ .keyVaultKey }}
objectType: secret
{{- end }}
secretObjects:
- secretName: {{ include "myapp.fullname" . }}-kv-secrets
type: Opaque
data:
{{- range .Values.azure.keyVault.secrets }}
- objectName: {{ .keyVaultKey }}
key: {{ .secretName }}
{{- end }}
{{- end }}
hpa.yaml
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "myapp.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}
Deployment Commands
# Add Helm repo for dependencies
helm dependency update ./myapp
# Install to development
helm install myapp ./myapp \
--namespace dev \
--create-namespace \
-f ./myapp/values-dev.yaml \
--set image.tag=1.0.0
# Upgrade production
helm upgrade myapp ./myapp \
--namespace prod \
-f ./myapp/values-prod.yaml \
--set image.tag=1.0.1 \
--atomic \
--timeout 10m
# Rollback if needed
helm rollback myapp 1 --namespace prod
# Template for debugging
helm template myapp ./myapp -f ./myapp/values-prod.yaml > output.yaml
Azure DevOps Pipeline
trigger:
- main
variables:
chartPath: './charts/myapp'
acrName: 'myacr'
aksClusterName: 'aks-prod'
aksResourceGroup: 'rg-aks-prod'
stages:
- stage: Build
jobs:
- job: BuildAndPush
steps:
- task: Docker@2
inputs:
containerRegistry: 'ACR'
repository: 'myapp'
command: 'buildAndPush'
Dockerfile: '**/Dockerfile'
tags: '$(Build.BuildId)'
- stage: Deploy
jobs:
- deployment: DeployToAKS
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: HelmDeploy@0
inputs:
connectionType: 'Azure Resource Manager'
azureSubscription: 'Azure-Prod'
azureResourceGroup: '$(aksResourceGroup)'
kubernetesCluster: '$(aksClusterName)'
namespace: 'prod'
command: 'upgrade'
chartType: 'FilePath'
chartPath: '$(chartPath)'
releaseName: 'myapp'
valueFile: '$(chartPath)/values-prod.yaml'
overrideValues: 'image.tag=$(Build.BuildId)'
arguments: '--atomic --timeout 10m'
Conclusion
Helm charts provide a powerful way to package and deploy applications to AKS with Azure-specific integrations. By leveraging values files for environment-specific configurations, Azure Key Vault for secrets, and managed identities for authentication, you create secure, maintainable deployments. Combined with CI/CD pipelines, Helm enables reliable, repeatable deployments across environments.