Azure 36: Key Vault Integration at Scale — Pipeline, CSI, App Service
easy⏱ 5 mincourseazure
Pattern 1 — Pipeline task (build-time)
AzureKeyVault@2 pulls specified secrets at pipeline time and exposes them as pipeline variables (masked in logs). You then pass them to helm --set or write them into a .env on the agent. Trade-off: simple and needs no cluster changes, but secrets are snapshotted — if you rotate the Key Vault value, nothing updates until the next deploy.
- task: AzureKeyVault@2
inputs:
azureSubscription: 'prod-service-conn'
KeyVaultName: 'prod-k8s-kv-westus2'
SecretsFilter: 'DB-PASSWORD,API-KEY'
RunAsPreJob: true
- script: |
helm upgrade --install orders ./chart \
--set secrets.dbPassword=$(DB-PASSWORD) \
--set secrets.apiKey=$(API-KEY)
Pattern 2 — CSI Secrets Store (AKS runtime)
The Secrets Store CSI driver + Azure Key Vault provider mount Key Vault secrets into pods as files or sync them into native Kubernetes Secrets. Bind the AKS workload identity to the Key Vault with RBAC, then reference the driver via a SecretProviderClass manifest. Trade-off: auto-rotates via a sync interval and keeps secrets out of the pipeline entirely, but requires a one-time cluster setup (driver + identity binding).
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: orders-kv
spec:
provider: azure
parameters:
usePodIdentity: "false"
useVMManagedIdentity: "false"
clientID: <workload-identity-client-id>
keyvaultName: prod-k8s-kv-westus2
tenantId: <tenant-id>
objects: |
array:
- |
objectName: DB-PASSWORD
objectType: secret
Pattern 3 — App Service reference
For Azure App Service / Functions, skip the driver and the pipeline task entirely: set an app setting value to @Microsoft.KeyVault(SecretUri=https://kv.vault.azure.net/secrets/DB-PASSWORD/). The runtime resolves the reference via the site's managed identity and injects the current value as an environment variable. Trade-off: zero code, zero pipeline wiring, but only works for App Service and Functions.
# application settings
DB_PASSWORD = @Microsoft.KeyVault(SecretUri=https://prod-k8s-kv-westus2.vault.azure.net/secrets/DB-PASSWORD/)
Terraform at scale
Provision the vault, its RBAC role assignments, and the secrets themselves as Terraform resources. Use naming conventions (${purpose}-k8s-kv-${env}) so every environment has a matching vault. Grant Key Vault Secrets User to the pipeline's service principal and to the AKS workload identity — never to individual users.
resource "azurerm_key_vault" "this" {
name = "${var.purpose}-k8s-kv-${var.env}"
location = var.location
resource_group_name = var.rg
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
enable_rbac_authorization = true
purge_protection_enabled = true
}
resource "azurerm_role_assignment" "aks_read" {
scope = azurerm_key_vault.this.id
role_definition_name = "Key Vault Secrets User"
principal_id = var.aks_workload_identity_principal_id
}
Build an integration recommender
Create a KeyVaultAdvisor that takes a Workload { type, rotationRequired, env } (type = 'pipeline-deploy' | 'aks-pod' | 'app-service' | 'function') and returns the recommended pattern with a short rationale. Log recommendations for 4 different workloads and warn if rotationRequired is true but the chosen pattern is snapshot-based.
Always enable purge protection in prod
purge_protection_enabled = true makes soft-deleted secrets recoverable for 90 days — but more importantly it prevents an attacker (or a misfiring Terraform destroy) from permanently nuking the vault. You cannot turn this flag off once enabled, which is exactly the point.
Quiz: Rotation vs snapshot
Your AKS workload reads a DB password. Ops rotates it in Key Vault. Which pattern picks up the new value without redeploying? CSI Secrets Store — its sync loop fetches the latest secret on the configured interval. The pipeline task pattern captured the old value at deploy time; nothing updates until the next deployment.