Chapter 3 of 12

Azure for .NET Developers: Production-Grade Cloud Architecture and Operations

Containerizing .NET Workloads with AKS and Container Apps

The shift from virtual machines to containers has fundamentally changed how .NET applications are packaged, deployed, and operated at scale. This chapter examines AKS and Azure Container Apps alongside ACR, Helm, and Dapr—the complete container supply chain from image build to auto-scaled, identity-secured deployment.

Container Strategy for .NET Teams

The .NET Container Story in 2026

Microsoft's investment in container-first .NET has compounded with each runtime release. Distroless base images (mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy-chiseled) reduce a typical ASP.NET Core API image from 220 MB to under 80 MB, shrinking attack surface and cold-start latency. .NET 8 AOT compilation eliminates runtime overhead for compute-bound workloads.

The architectural consequence of smaller, faster images is that container granularity can now match business domain boundaries. Teams can follow DDD boundaries without apology—each bounded context in its own container, scaled and updated independently.

Choosing Between AKS and Azure Container Apps

AKS gives you a fully managed Kubernetes control plane with direct access to every Kubernetes primitive: Deployment, StatefulSet, DaemonSet, NetworkPolicy, HPA, VPA, and custom CRDs. This control is essential for GPU workloads, stateful data services, and latency-sensitive workloads with complex scheduling constraints. The tradeoff is operational surface area: you own node pool patching and cluster upgrade orchestration.

Container Apps is a serverless abstraction built on Kubernetes and KEDA that hides the cluster entirely. It integrates KEDA natively, supports Dapr sidecars as first-class config, and bills on consumption reaching true zero. For greenfield microservices without a dedicated Kubernetes ops capability, Container Apps delivers 70–80% of AKS flexibility at 20% of the operational cost.

Decision DimensionAKSAzure Container Apps
Kubernetes primitivesFull access (all resource types)Abstracted — no direct kubectl access
Node pool customizationFull (GPU, spot, system/user pools)Managed by platform
Scaling modelHPA, VPA, KEDA, cluster autoscalerKEDA-native, HTTP scaling, consumption
Stateful workloadsSupported (StatefulSet + PV)Limited (volume mounts, no StatefulSet)
Dapr integrationManual sidecar injection or HelmFirst-class config option
Cost modelPer-node (VM SKU billing)Consumption (vCPU-seconds + memory-GiB-seconds)
Operations maturity requiredHigh (Kubernetes expertise)Low to medium
Ideal workloadComplex, stateful, GPU, multi-tenantEvent-driven APIs, background workers, Dapr apps

Note

AKS and Container Apps are not mutually exclusive. A common enterprise pattern uses AKS for stateful/GPU workloads alongside Container Apps for HTTP-facing APIs. ACR and Workload Identity work identically across both, making the hybrid pattern operationally coherent.

Architecture diagram showing a CI/CD pipeline pushing images to Azure Container Registry with geo-replication, then deploying ASP.NET Core microservices via Helm charts to AKS node pools with workload identity and Dapr sidecar integration, alongside Azure Container Apps with KEDA autoscaling triggered by Azure Service Bus queue depth, all backed by shared Azure AD, Key Vault, Monitor, and Log Analytics services.
Figure 3.1 — End-to-end .NET container architecture spanning ACR, AKS, and Azure Container Apps

Azure Container Registry: Image Lifecycle and Geo-Replication

Registry Architecture and Naming Strategy

ACR is the foundation of the container supply chain. Premium is the only production SKU: it adds geo-replication, private endpoints, customer-managed keys, dedicated data endpoints, and a 99.95% SLA. Basic (10 GB) and Standard (100 GB) lack private endpoints and geo-replication—do not use them for production workloads.

bash
# Create Premium ACR with zone redundancy and geo-replication
az acr create \
  --resource-group rg-containerizing-dotnet-workloads-prod-001 \
  --name containerizingdotnetprodeastus2 \
  --sku Premium --zone-redundancy Enabled \
  --public-network-enabled false --admin-enabled false

az acr replication create \
  --registry containerizingdotnetprodeastus2 \
  --location westus3 --zone-redundancy Enabled

Important

Disable the ACR admin account (--admin-enabled false) in all production registries. The admin account uses username/password authentication that cannot be scoped, rotated automatically, or audited per-caller. All access should flow through managed identities with AcrPull or AcrPush roles.

Image Lifecycle Policies and Vulnerability Management

Container images accumulate rapidly in active CI/CD pipelines. ACR Tasks and lifecycle policies automate purge of untagged manifests, tags older than a configurable window, and retain a minimum number of recent images. The acr purge command executed as a cron-scheduled ACR Task is the primary tool.

bash
# Purge task: retain 30 days, keep min 5 per repo, daily at 01:00 UTC
az acr task create \
  --registry containerizingdotnetprodeastus2 \
  --name purge-old-images \
  --cmd "acr purge --filter '.*:.*' --ago 30d --keep 5 --untagged" \
  --schedule "0 1 * * *" \
  --context /dev/null

Tip

Pair ACR vulnerability scanning (Microsoft Defender for Containers) with Azure Policy to block deployments of images with critical CVEs. The built-in policy Container images in Azure Container Registry should have vulnerability findings resolved can be assigned at subscription scope without modifying individual CI/CD pipelines.

Geo-Replication Design

Geo-replication in ACR is an active-active model: pull requests are served from the nearest replica via anycast routing, keeping image pulls local during pod scheduling. Enable --zone-redundancy Enabled per replica to survive a single-zone failure. Replication latency for images under 500 MB is typically under 60 seconds.

Warning

Geo-replication does not replicate ACR Tasks, webhooks, or lifecycle policies. Design CI/CD pipelines to push to the home registry and let replication propagate—do not push directly to individual replicas.

AKS Node Pool Design and Workload Identity for .NET

Cluster Architecture and Node Pool Topology

The foundational principle is separation of concerns through dedicated node pools. Every AKS cluster requires a system pool (tainted CriticalAddonsOnly=true:NoSchedule), a general-purpose user pool for ASP.NET Core APIs, and optionally a spot pool for interrupt-tolerant batch workloads. The --network-dataplane cilium flag enables eBPF-based dataplane with native NetworkPolicy, removing kube-proxy iptables overhead—measurable for microservices with high inter-service call volumes.

bash
# Create AKS cluster with Cilium CNI and Workload Identity
az aks create \
  --name containerizing-dotnet-prod-eastus2-001 \
  --resource-group rg-containerizing-dotnet-workloads-prod-001 \
  --node-vm-size Standard_D4ds_v5 --node-count 3 \
  --network-plugin azure --network-dataplane cilium \
  --enable-oidc-issuer --enable-workload-identity \
  --enable-cluster-autoscaler --min-count 3 --max-count 5 \
  --attach-acr containerizingdotnetprodeastus2 \
  --zones 1 2 3 --tier Standard

Workload Identity for .NET Applications

The legacy approach of storing service principal secrets in Kubernetes Secrets has three critical flaws: manual rotation, base64 storage in etcd, and no per-pod audit trail. Azure Workload Identity solves all three by issuing short-lived OIDC tokens via a federated credential that maps a Kubernetes ServiceAccount to an Azure Managed Identity. The .NET application uses DefaultAzureCredential from Azure.Identity—no code changes beyond the package.

bash
# Create managed identity and federated credential
OIDC_ISSUER=$(az aks show -g rg-containerizing-dotnet-workloads-prod-001 \
  -n containerizing-dotnet-prod-eastus2-001 \
  --query "oidcIssuerProfile.issuerUrl" -o tsv)

az identity create -g rg-containerizing-dotnet-workloads-prod-001 \
  --name mi-catalogapi-prod-001

az identity federated-credential create \
  --name fc-catalogapi-prod \
  --identity-name mi-catalogapi-prod-001 \
  --resource-group rg-containerizing-dotnet-workloads-prod-001 \
  --issuer "$OIDC_ISSUER" \
  --subject "system:serviceaccount:catalog:catalogapi" \
  --audience "api://AzureADTokenExchange"

Important

The OIDC discovery endpoint ({oidc-issuer}/.well-known/openid-configuration) must remain publicly accessible even in private-API-server clusters. AKS manages this automatically, but firewall policies on the node resource group can inadvertently block it and break token exchange.

Resource Quotas and Pod Disruption Budgets

Standard ASP.NET Core applications with EF Core and AutoMapper typically consume 150–300 MB RSS at startup. Set memory requests at 120% of idle RSS and limits at 200%; set CPU requests at the 95th-percentile under expected load. Pod Disruption Budgets with minAvailable: 2 on 3-replica deployments are mandatory—without them, a cluster upgrade that drains two nodes simultaneously can take a deployment to zero replicas.

Deployment topology diagram showing Azure Container Registry with geo-replication feeding images to an AKS cluster containing a system node pool and a .NET user node pool with Dapr-sidecar pods, KEDA operator, Workload Identity via Microsoft Entra ID federated credentials, Azure Key Vault via CSI driver, Azure Container Apps with KEDA autoscaling, and a legend panel describing key design decisions for each component.
Figure 3.2 — AKS node pool topology with Dapr sidecars, KEDA autoscaling, and Workload Identity for .NET microservices

Azure Container Apps with KEDA-Based Autoscaling

Container Apps Environment and Dapr Component Architecture

A Container Apps environment is the billing and networking boundary—it maps to a single VNet subnet and shares an internal load balancer and Log Analytics workspace. Two environments (production + staging, separate subnets) is the minimum viable configuration. Apps within the same environment communicate over private DNS without traversing the public internet.

bash
# Create Container Apps Environment with workload profiles
az containerapp env create \
  --name cae-containerizing-dotnet-prod-eastus2-001 \
  --resource-group rg-containerizing-dotnet-workloads-prod-001 \
  --location eastus2 \
  --internal-only true \
  --enable-workload-profiles \
  --tags environment=production workload=containerizing-dotnet

Tip

Use the Consumption + Dedicated workload profiles plan to keep warm containers for latency-sensitive .NET APIs (billing on reserved compute) while bursty background workers use the Consumption profile and bill at zero when idle. This hybrid model typically reduces cost by 40–60% versus fully dedicated hosting.

KEDA Scalers for .NET Microservices

KEDA's power for .NET microservices lies in event-source scalers: Azure Service Bus, Storage Queues, Event Hubs, RabbitMQ, and Kafka all have native scalers. When queue depth exceeds the configured threshold per replica, KEDA adds replicas; when it falls below, replicas are removed—potentially to zero. Tune the polling interval and cooldown based on downstream system capacity to avoid connection pool exhaustion.

bash
# Deploy KEDA-scaled .NET worker (0-20 replicas via Service Bus queue depth)
az containerapp create \
  --name ca-orderprocessor-prod-001 \
  --environment cae-containerizing-dotnet-prod-eastus2-001 \
  --image containerizingdotnetprodeastus2.azurecr.io/order-processor:1.4.2 \
  --min-replicas 0 --max-replicas 20 \
  --scale-rule-type azure-servicebus \
  --scale-rule-metadata "queueName=orders-prod" "messageCount=5"

Warning

Scale-to-zero introduces a cold-start latency of 15–45 seconds for .NET 8 applications. For Service Bus queues with strict SLAs (e.g., 30-second end-to-end), set --min-replicas 1 to keep one warm replica and accept the baseline compute cost.

HTTP Scaling and Traffic Splitting for Canary Deployments

Container Apps supports HTTP-based scaling via a built-in ingress controller with automatic TLS provisioning, triggering on concurrent requests per replica (default: 10). Traffic splitting enables canary deployments natively—shift 10% to a new revision, monitor Application Insights, then complete cutover without Flagger or Argo Rollouts. Revision labels (--v142 suffix) provide stable named endpoints for deterministic integration test routing.

Side-by-side comparison diagram showing .NET containerized workloads deploying from Azure Container Registry with geo-replication into either AKS with node pool design, workload identity, and Helm chart patterns, or Azure Container Apps with KEDA autoscaling triggers and Dapr sidecar integration, including a decision guide and shared infrastructure notes at the bottom.
Figure 3.3 — AKS vs Container Apps: choosing the right .NET container runtime on Azure

Helm Chart Patterns for ASP.NET Core Microservices

Helm Chart Structure and Values Hierarchy

The values hierarchy should express the minimum viable configuration at chart level and override only the delta between environments. Three layers: values.yaml (templating defaults), values-dev.yaml (fewer replicas, relaxed limits, disabled PDBs), and values-prod.yaml (replica counts, resource limits, PDB enabled, autoscaling on). The install command for any environment is then predictable: helm upgrade --install {release} {chart} -f values-{env}.yaml.

yaml
# values-prod.yaml — production layer of the three-tier hierarchy
replicaCount: 3
image:
  repository: containerizingdotnetprodeastus2.azurecr.io/catalog-api
  tag: "1.4.2"
serviceAccount:
  annotations:
    azure.workload.identity/client-id: "b3c4d5e6-..."
autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 20
podDisruptionBudget:
  enabled: true
  minAvailable: 2

Health Checks and ConfigMap Templates

The ASP.NET Core health check framework exposes /healthz/live (liveness—application only) and /healthz/ready (readiness—including downstream deps). Map liveness to the live endpoint and readiness to the ready endpoint to prevent cascading restarts during transient downstream outages. A Helm-managed ConfigMap mounted at /app/appsettings.Override.json injects non-secret config without cluttering the pod spec with environment variable entries.

Helm OCI Registry Integration with ACR

Helm 3.8+ supports OCI artifacts in container registries. Co-locating Helm charts in ACR with application images eliminates a separate chart repository. The AKS cluster's ACR attachment already grants the kubelet's managed identity permissions to pull OCI Helm charts—no additional auth configuration needed. ArgoCD and Flux both support OCI Helm sources for GitOps workflows.

bash
# Package, push, and install Helm chart via ACR OCI registry
az acr login --name containerizingdotnetprodeastus2
helm package ./charts/catalog-api --version 1.4.2
helm push catalog-api-1.4.2.tgz \
  oci://containerizingdotnetprodeastus2.azurecr.io/helm
helm upgrade --install catalog-api \
  oci://containerizingdotnetprodeastus2.azurecr.io/helm/catalog-api \
  --version 1.4.2 --namespace catalog \
  -f values-prod.yaml --atomic --timeout 5m

Dapr Sidecar Integration with .NET Applications

Dapr Building Blocks for .NET Microservices

Dapr addresses cross-cutting microservices concerns—service invocation, pub/sub, state management, secret retrieval, and distributed tracing—via a localhost sidecar (daprd). The .NET application has no direct dependency on Azure Service Bus or Redis; only on Dapr's abstract API. The Dapr .NET SDK integrates with ASP.NET Core: topic subscriptions use the [Topic] attribute on minimal API endpoints, and DaprClient is injected via DI. On AKS, install via the GA AKS Dapr extension to receive automatic updates under Microsoft support.

bash
# Enable Dapr AKS extension with HA mode
az k8s-extension create \
  --cluster-type managedClusters \
  --cluster-name containerizing-dotnet-prod-eastus2-001 \
  --resource-group rg-containerizing-dotnet-workloads-prod-001 \
  --name dapr --extension-type Microsoft.Dapr \
  --auto-upgrade-minor-version true \
  --configuration-settings "global.ha.enabled=true"

Service Invocation and Resiliency Policies

Dapr service invocation provides mTLS-encrypted, load-balanced HTTP/gRPC calls between services. Outbound requests go to http://localhost:3500/v1.0/invoke/{app-id}/method/{path}—the distributed systems complexity is entirely in the sidecar. Dapr resiliency policies (GA in 1.11) declare retry, timeout, and circuit breaker behavior in Resiliency CRDs, eliminating the need for per-service Polly pipelines for infrastructure-level calls.

Important

Dapr resiliency policies operate at the sidecar layer and apply to the Dapr API surface. For EF Core database retry and direct third-party HTTP calls that bypass Dapr, Polly remains the appropriate tool. Do not duplicate the same policy at both layers.

Dapr in Azure Container Apps

Container Apps provides first-class Dapr support—set dapr.enabled: true, dapr.appId, and dapr.appPort in the manifest; the platform injects daprd automatically. Dapr components are defined at the environment level and scoped per app via the scopes array. The combination of KEDA scaling (driven by raw queue depth) and Dapr pub/sub (reliable delivery + retries) creates a particularly elegant event-driven pattern with no code duplication between infrastructure scaling and message processing.

Lab

1

CE-05: Build and Push a Containerized ASP.NET Core API to ACR

Create a chiseled multi-stage ASP.NET Core 8 image, push it to ACR, and validate Defender vulnerability scan results. Requires az login, Docker Desktop, and .NET 8 SDK.

bash
REGISTRY="containerizingdotnetprodeastus2"
dotnet new webapi -n CatalogApi --use-controllers --no-https
# Write multi-stage Dockerfile with 8.0-jammy-chiseled final stage
az acr login --name "$REGISTRY"
docker build -t "${REGISTRY}.azurecr.io/catalog-api:1.4.2" .
docker push "${REGISTRY}.azurecr.io/catalog-api:1.4.2"
az aks check-acr -g rg-containerizing-dotnet-workloads-prod-001 \
  -n containerizing-dotnet-prod-eastus2-001 \
  --acr "${REGISTRY}.azurecr.io"
2

CE-06: Deploy KEDA-Scaled .NET Worker to AKS with Workload Identity and Helm

Deploy a Service Bus queue consumer .NET worker to AKS using Helm with KEDA scaling (0–20 replicas) and Workload Identity. Creates the managed identity, federated credential, Service Bus queue, and Helm release in one flow.

bash
RG="rg-containerizing-dotnet-workloads-prod-001"
# Create identity, assign Service Bus Data Receiver, create federated credential
az identity create -g "$RG" --name mi-orderprocessor-prod-001
# Enable KEDA add-on and deploy via Helm OCI chart from ACR
az aks enable-addons -g "$RG" -n containerizing-dotnet-prod-eastus2-001 \
  --addons keda
helm upgrade --install order-processor \
  oci://containerizingdotnetprodeastus2.azurecr.io/helm/order-processor \
  --version 1.0.0 --namespace orders --atomic \
  -f /tmp/lab-values.yaml --timeout 5m

Summary

ConceptKey Point
AKS vs. Container AppsAKS for complex, stateful, GPU workloads; Container Apps for event-driven APIs—use both in the same solution when workload types differ.
ACR SKU SelectionPremium is mandatory for production: geo-replication, private endpoints, and zone redundancy are unavailable in Basic and Standard.
Workload IdentityReplace service principal secrets with federated credentials tied to Kubernetes ServiceAccounts; DefaultAzureCredential resolves automatically.
KEDA ScalingScale-to-zero eliminates idle cost but adds cold-start latency; set minReplicas: 1 for latency-sensitive workloads.
Helm Values HierarchyThree-layer hierarchy (chart defaults → dev overrides → prod overrides) separates structural from environmental config; store charts as OCI in ACR.
Dapr Building BlocksUse Dapr resiliency policies for sidecar-layer retry/circuit-breaker and Polly for application-layer resiliency—do not duplicate policies at both layers.
Image LifecycleAutomate ACR purge tasks; combine Defender for Containers scanning with Azure Policy to block critical CVE images before they reach the cluster.

Chapter: 3 of 12  |  Status: v0.1 Draft  |