6. Integrations
No Azure service is an island, and AKS is the least island-like of all of them — a cluster is mostly a machine for wiring other Azure services together. These are the pairings you will meet on essentially every real cluster.
The two glue mechanisms, and the AKS-specific third
Two mechanisms answer most "how do these talk to each other" questions across Azure, and AKS adds one of its own:
- Managed identity + role assignment — the keyless way one Azure resource authenticates to another. On AKS this splits three ways (cluster identity, kubelet identity, workload identity) and choosing the wrong one is a real security bug, not a style preference.
- Private endpoint + Private DNS zone — the way one Azure resource reaches another without
the public internet. From a cluster, the pod resolves the target's public FQDN, Private DNS
returns a private IP in your VNet, and traffic never leaves it. Name the sub-resource the endpoint
targets:
blob,vault,registry,management(for a private cluster's API server). - CSI drivers — the AKS-specific glue: a Kubernetes volume request becomes an Azure resource. Disks, Files, Blob, and Key Vault secrets all reach pods this way.
The pairings
| Pairs with | Why | The glue |
|---|---|---|
| Azure Container Registry | Somewhere to store the images the cluster runs | AcrPull on the kubelet identity — az aks update --attach-acr does exactly this. Private endpoint on the registry for private clusters |
| Key Vault | Hold the secrets a Kubernetes Secret only base64-encodes |
Secrets Store CSI driver add-on: the secret mounts as a file in the pod, authenticated by workload identity, with rotation on |
| Microsoft Entra ID | Who may use the cluster, and who a pod is | Entra-integrated Kubernetes RBAC (or Azure RBAC for Kubernetes Authorization) for humans; Workload ID federated to the cluster's OIDC issuer for pods |
| Azure Monitor | Logs, metrics, and traces from a system that emits enormous volumes of all three | Container Insights for logs, managed Prometheus for metrics, Managed Grafana for dashboards. All add-ons; all off by default |
| Application Gateway / App Routing | Get HTTPS traffic into the cluster with a WAF in front | Application Gateway for Containers (via the ALB controller) or the App Routing add-on (managed NGINX + optional Entra-integrated certificate management) |
| Virtual Network + NAT Gateway | Where the nodes and pods live, and how they get out | Node subnet passed at create time; outbound_type = managedNATGateway for SNAT that doesn't exhaust |
| Azure Storage (Disk / Files / Blob) | Persistent state for stateful workloads | The corresponding CSI driver and a StorageClass; a PVC becomes a real Azure resource |
| Service Bus / Event Hubs / Storage Queues | The event sources that should drive scaling | KEDA add-on scalers, authenticated with workload identity — scale on queue depth, not CPU |
| Azure Front Door | Global entry, WAF, and the traffic switch for a blue/green cluster migration | Origin pointing at the cluster's ingress IP, or Private Link origin to an internal load balancer |
| Azure Policy | Enforce rules the cluster itself should refuse to break | Azure Policy add-on (Gatekeeper) — admission-time Deny on privileged containers, untrusted registries, missing limits |
| Azure DevOps / GitHub Actions | Build images and deploy manifests | OIDC federation for the infrastructure pipeline; Flux or Argo CD for in-cluster delivery |
[Image Prompt: 2D minimalistic hub-and-spoke diagram with an AKS cluster at the centre connected to Azure Container Registry, Key Vault, Microsoft Entra ID, Azure Monitor, Application Gateway, and Azure Storage, with labelled edges naming the glue mechanism for each, flat design, clean vector art style, white background]
Azure Container Registry — the first thing that breaks
Every cluster needs an image source, and ImagePullBackOff is the most common first failure on a
new AKS cluster. The cause is almost always the same: the kubelet identity has no AcrPull role
assignment on the registry.
# The one-liner that creates the role assignment for you
az aks update -g rg-payments-aks-prod -n aks-payments-prod --attach-acr acrpayments
Do it in Terraform instead, so it survives a rebuild and appears in a plan:
resource "azurerm_role_assignment" "acr_pull" {
scope = azurerm_container_registry.this.id
role_definition_name = "AcrPull"
principal_id = azurerm_kubernetes_cluster.this.kubelet_identity[0].object_id
}
Three follow-ons worth knowing:
- Role assignments propagate asynchronously. A pull that fails immediately after an apply may succeed a couple of minutes later. Don't debug a race as if it were a misconfiguration.
- On a private cluster, the registry needs a private endpoint (
registrysub-resource) and its Private DNS zone linked to the node VNet, or pulls fail on DNS rather than on authorization. imagePullSecretsare the wrong answer on AKS. They're a stored credential with a rotation problem, and the identity-based path exists specifically to replace them.
Key Vault — where secrets should actually live
A Kubernetes Secret is base64, not encryption. Anyone with API read access on the namespace can
read it, and it sits in etcd. The Azure answer is the Key Vault provider for the Secrets Store CSI
driver, enabled as an add-on:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: payments-kv
namespace: payments
spec:
provider: azure
parameters:
usePodIdentity: "false"
useVMManagedIdentity: "false"
clientID: "<workload-identity-client-id>" # the pod's identity, not the node's
keyvaultName: "kv-payments-prod"
tenantId: "<tenant-id>"
objects: |
array:
- |
objectName: db-connection-string
objectType: secret
The pod mounts it as a volume; the secret arrives as a file. Two things to get right:
- Authenticate with workload identity, not the kubelet identity. Granting the kubelet identity
Key Vault Secrets Usergives every pod on every node access to those secrets — a lateral movement path handed over for convenience. - Turn on secret rotation (
secret_rotation_enabled = true) and make sure the application re-reads the file. Rotation updates the mounted file; it does not restart your process.
Microsoft Entra Workload ID — how a pod becomes an identity
This is the mechanism that removes credentials from AKS entirely, and it's worth understanding end-to-end because it's the answer to at least three interview questions.
- The cluster has an OIDC issuer enabled, publishing a public discovery document.
- A Kubernetes ServiceAccount is annotated with a user-assigned managed identity's client ID.
- A federated identity credential on that managed identity trusts tokens from the cluster's issuer, for that specific namespace and service account name.
- A pod using that service account gets a projected token; the Azure SDK exchanges it with Entra ID for a real access token; the token is used against Storage, Key Vault, Cosmos DB, anything.
az identity federated-credential create \
--name "payments-api" \
--identity-name "id-payments-api" \
--resource-group "rg-payments-aks-prod" \
--issuer "$(az aks show -g rg-payments-aks-prod -n aks-payments-prod --query oidcIssuerProfile.issuerURL -o tsv)" \
--subject "system:serviceaccount:payments:payments-api" \
--audience "api://AzureADTokenExchange"
apiVersion: v1
kind: ServiceAccount
metadata:
name: payments-api
namespace: payments
annotations:
azure.workload.identity/client-id: "<user-assigned-identity-client-id>"
---
apiVersion: apps/v1
kind: Deployment
# ...
spec:
template:
metadata:
labels:
azure.workload.identity/use: "true" # without this label, nothing happens
spec:
serviceAccountName: payments-api
Note the subject string: system:serviceaccount:<namespace>:<name>. The trust is scoped to one
service account in one namespace, which is exactly the granularity you want — and exactly the
granularity the retired AAD Pod Identity struggled to provide. If a tutorial tells you to install
AzureIdentity and AzureIdentityBinding CRDs, it predates this and should not be followed.
If you're coming from AWS: this is IRSA, nearly feature-for-feature.
Observability — Azure Monitor, and the volume problem
Nothing is collected by default. Three add-ons cover the ground:
- Container Insights — container stdout/stderr and inventory into a Log Analytics workspace.
Powerful and the single largest source of surprise AKS bills. Use data collection rules to
exclude
kube-systemnoise and namespaces you don't need, and set the workspace's daily cap. - Azure Monitor managed Prometheus — metrics, scraped in the Prometheus ecosystem's own idiom, stored in an Azure Monitor workspace. This is what you alert on.
- Azure Managed Grafana — dashboards over both, with AKS dashboards preinstalled.
The KQL query you'll write most often — what is restarting, and how much:
KubePodInventory
| where TimeGenerated > ago(1h)
| where ClusterName == "aks-payments-prod"
| summarize Restarts = max(PodRestartCount) by Namespace, Name = Name, ContainerName = ContainerName
| where Restarts > 0
| order by Restarts desc
And the one that finds the reason:
KubeEvents
| where TimeGenerated > ago(1h)
| where KubeEventType == "Warning"
| summarize Count = count(), Latest = max(TimeGenerated) by Reason, Name, Namespace
| order by Count desc
Route the control plane's own logs too, via a diagnostic setting on the cluster resource:
kube-apiserver, kube-audit (very high volume — use kube-audit-admin unless you need read
events), kube-controller-manager, cluster-autoscaler, and guard (the Entra authorization
component). cluster-autoscaler logs are the only way to answer "why didn't it scale up".
Ingress — three options and how to choose
| Option | What it is | Choose when |
|---|---|---|
| App Routing add-on | Managed NGINX ingress controller, with optional Entra-integrated DNS and certificate management from Key Vault | You want a supported ingress controller without owning its lifecycle. The sensible default |
| Application Gateway for Containers | Azure's L7 load balancer, driven by the ALB controller via Gateway API | You want WAF, mutual TLS, and Azure-native L7 outside the cluster's data plane. The successor to AGIC — check migration guidance if you're on AGIC today |
| Your own controller (NGINX, Traefik, Envoy Gateway, Istio gateway) | Whatever you install | You need behaviour the managed options don't have, and accept owning upgrades and CVEs |
Whichever you choose, the ingress terminates on an Azure Load Balancer IP — public, or internal
(service.beta.kubernetes.io/azure-load-balancer-internal: "true") with Front Door or Application
Gateway in front. Internal-only ingress is the right default for anything that shouldn't be on the
public internet.
Storage — a PVC is an Azure resource
| Driver | Backing resource | Access mode | Use for |
|---|---|---|---|
| Azure Disk CSI | Managed disk | ReadWriteOnce — one node at a time |
Databases, anything wanting block storage and low latency. The default StorageClass |
| Azure File CSI | Azure Files share (SMB or NFS) | ReadWriteMany |
Shared state across pods, legacy apps expecting a file share |
| Blob CSI | Blob container (via blobfuse or NFS) | ReadWriteMany |
Large sequential data, ML datasets, model artifacts |
Two traps:
- Zone affinity. A managed disk lives in one availability zone; a pod using it can only be
scheduled on a node in that zone. A zone outage takes that pod down and no amount of replicas
elsewhere helps.
WaitForFirstConsumervolume binding avoids provisioning the disk in the wrong zone in the first place. - Reclaim policy. The default
Deleteremoves the Azure disk when the PVC goes — convenient, and catastrophic if the PVC was deleted by accident.Retainkeeps your data and quietly keeps billing you for orphaned disks. Pick per storage class, deliberately, and audit for unattached disks monthly.
Event-driven scaling with KEDA
The pairing that makes AKS genuinely good at asynchronous work: KEDA (an add-on) turns an external metric into an HPA, and can scale a Deployment to zero.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: payments-worker
namespace: payments
spec:
scaleTargetRef:
name: payments-worker
minReplicaCount: 0
maxReplicaCount: 50
triggers:
- type: azure-servicebus
metadata:
queueName: payments-inbound
namespace: sb-payments-prod
messageCount: "20" # target messages per replica
authenticationRef:
name: keda-workload-identity # no connection string anywhere
Note what this doesn't do: scaling pods to zero doesn't scale nodes to zero. The node savings come from the cluster autoscaler or node autoprovisioning noticing the empty nodes afterwards — with minutes of lag in both directions.
Next: Production →
← Back to the Azure Kubernetes Service overview · ← Previous: Deployment