
Learn how to deploy .NET microservices to Azure Kubernetes Service (AKS) step by step with Docker, YAML manifests, and best practices. Start deploying today.
If you are building .NET microservices and want to run them in the cloud with automatic scaling, self-healing, and zero-downtime deployments, Azure Kubernetes Service (AKS) is the most popular choice for .NET teams. AKS is Microsoft's managed Kubernetes offering: Azure runs the control plane for you (for free), and you only pay for the worker nodes your containers run on. In this Azure Kubernetes Service tutorial, you will containerize an ASP.NET Core microservice, push it to Azure Container Registry, create an AKS cluster, and deploy it with Kubernetes YAML manifests — plus the best practices and pitfalls that separate a demo from a production-ready deployment.
What Is Azure Kubernetes Service and Why Use It for .NET Microservices?
Kubernetes is an open-source container orchestrator. It schedules containers onto machines, restarts them when they crash, load-balances traffic, and rolls out new versions gradually. Running Kubernetes yourself is hard: you have to manage etcd, the API server, certificates, and upgrades. AKS removes that burden.
Why does this matter for .NET microservices specifically?
- Independent scaling: Your order service can scale to 20 pods during a sale while the reporting service stays at 2. A monolith cannot do that.
- Fast, safe releases: Rolling updates replace pods one at a time and roll back automatically if health checks fail.
- First-class .NET support: Microsoft publishes official, hardened Linux images for ASP.NET Core, and .NET 8/9/10 are tuned for containers (cgroup-aware GC, smaller chiseled images).
- Azure integration: Managed identities, Azure Key Vault, Azure Monitor, and Azure Container Registry (ACR) plug in with a single CLI flag.
Prerequisites
- An Azure subscription (a free account works for this tutorial)
- Azure CLI 2.60 or later
- Docker Desktop (or any Docker engine)
- .NET 8 SDK or later
kubectl— install it withaz aks install-cli
Step 1: Create a .NET Microservice
We will build a small "Products" API using minimal APIs. The important additions for Kubernetes are health check endpoints, which Kubernetes uses to decide whether a pod is alive and ready to receive traffic.
dotnet new webapi -n ProductService --no-openapi
cd ProductService
Replace Program.cs with the following:
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
var builder = WebApplication.CreateBuilder(args);
// Health checks: "live" means the process is running; "ready" means it can serve traffic.
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" })
.AddCheck("startup-warmup", () => HealthCheckResult.Healthy(), tags: new[] { "ready" });
var app = builder.Build();
var products = new List<Product>
{
new(1, "Keyboard", 49.99m),
new(2, "Monitor", 229.00m),
new(3, "Headset", 89.50m)
};
app.MapGet("/api/products", () => Results.Ok(products));
app.MapGet("/api/products/{id:int}", (int id) =>
products.FirstOrDefault(p => p.Id == id) is { } product
? Results.Ok(product)
: Results.NotFound());
// Expose the pod name so you can watch load balancing across replicas.
app.MapGet("/api/whoami", () =>
Results.Ok(new { Host = Environment.MachineName, Version = "1.0.0" }));
app.MapHealthChecks("/healthz/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live")
});
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
app.Run();
public record Product(int Id, string Name, decimal Price);
Why two health endpoints? A liveness failure tells Kubernetes to restart the container. A readiness failure tells it to stop sending traffic but leave the container alone. If you point liveness at a check that depends on your database, a database outage will cause Kubernetes to restart-loop every pod — making the outage worse. Keep liveness trivial, and put dependency checks under readiness.
Step 2: Write a Production-Ready Dockerfile
Use a multi-stage build so the final image contains only the runtime and your compiled output, not the SDK. Also run as a non-root user — a requirement in most enterprise clusters.
# Stage 1: build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy the csproj first so package restore is cached between builds
COPY ProductService.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish --no-restore
# Stage 2: runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
# Listen on 8080 (non-privileged port) and run as the built-in 'app' user
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
USER app
ENTRYPOINT ["dotnet", "ProductService.dll"]
Build and test it locally before touching the cloud:
docker build -t productservice:1.0.0 .
docker run --rm -p 8080:8080 productservice:1.0.0
curl http://localhost:8080/api/products
Step 3: Push the Image to Azure Container Registry
AKS pulls images from a registry. Azure Container Registry (ACR) is the natural choice because AKS can authenticate to it with a managed identity — no image-pull secrets to rotate.
az login
az group create --name rg-dotnet-aks --location eastus
# Registry names must be globally unique and alphanumeric
az acr create --resource-group rg-dotnet-aks --name acrdotnetdemo123 --sku Basic
# Build in the cloud (no local Docker needed) and push in one step
az acr build --registry acrdotnetdemo123 --image productservice:1.0.0 .
Step 4: Create the AKS Cluster
The --attach-acr flag grants the cluster's identity AcrPull permission on your registry. This is the single most common thing beginners forget, and it causes the dreaded ImagePullBackOff error.
az aks create \
--resource-group rg-dotnet-aks \
--name aks-dotnet-demo \
--node-count 2 \
--node-vm-size Standard_B2s \
--enable-managed-identity \
--attach-acr acrdotnetdemo123 \
--generate-ssh-keys
# Download credentials so kubectl talks to your new cluster
az aks get-credentials --resource-group rg-dotnet-aks --name aks-dotnet-demo
kubectl get nodes
Cluster creation takes 3–5 minutes. When kubectl get nodes shows two nodes in Ready state, you are set.
Step 5: Deploy .NET Microservices to Kubernetes with YAML
A Kubernetes deployment for a .NET microservice needs two objects: a Deployment (how many pods, which image, health probes, resource limits) and a Service (a stable network endpoint that load-balances across pods). Save this as k8s/productservice.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: productservice
labels:
app: productservice
spec:
replicas: 3
selector:
matchLabels:
app: productservice
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
metadata:
labels:
app: productservice
spec:
containers:
- name: productservice
image: acrdotnetdemo123.azurecr.io/productservice:1.0.0
ports:
- containerPort: 8080
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
- name: DOTNET_gcServer
value: "0"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
livenessProbe:
httpGet:
path: /healthz/live
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: productservice
spec:
type: LoadBalancer
selector:
app: productservice
ports:
- port: 80
targetPort: 8080
Apply it and watch the rollout:
kubectl apply -f k8s/productservice.yaml
kubectl rollout status deployment/productservice
kubectl get service productservice --watch
Once the EXTERNAL-IP column changes from <pending> to a real address, call it a few times. You will see different pod names come back, proving traffic is spread across replicas:
for i in 1 2 3 4 5; do curl -s http://<EXTERNAL-IP>/api/whoami; echo; done
Why the resource limits matter for .NET
The .NET garbage collector reads the container's memory limit and sizes its heap accordingly. Without a limit, a single pod can consume the whole node and get evicted. Set DOTNET_gcServer=0 (workstation GC) for small pods with under 1 CPU — Server GC creates one heap per core and wastes memory when CPU is throttled.
Step 6: Roll Out a New Version with Zero Downtime
Change the version string in /api/whoami to "1.1.0", build a new tag, and update the deployment:
az acr build --registry acrdotnetdemo123 --image productservice:1.1.0 .
kubectl set image deployment/productservice productservice=acrdotnetdemo123.azurecr.io/productservice:1.1.0
kubectl rollout status deployment/productservice
# Something wrong? Roll back instantly.
kubectl rollout undo deployment/productservice
Because we set maxUnavailable: 0, Kubernetes starts a new pod, waits for its readiness probe to pass, and only then terminates an old one. Users never see an error.
Step 7: Configuration and Secrets the Right Way
Never bake connection strings into images. Use a ConfigMap for non-sensitive settings and a Secret for credentials. ASP.NET Core reads environment variables automatically, using __ (double underscore) as the section separator:
apiVersion: v1
kind: Secret
metadata:
name: productservice-secrets
type: Opaque
stringData:
ConnectionStrings__Products: "Server=tcp:mysql.database.windows.net;Database=Products;..."
Then reference it in the container spec:
envFrom:
- secretRef:
name: productservice-secrets
In your code, builder.Configuration.GetConnectionString("Products") just works. For production, go one step further and use the Azure Key Vault Secrets Store CSI driver, so secrets never live in the cluster at all.
Step 8: Autoscale Your .NET Microservice
A Horizontal Pod Autoscaler adds pods when CPU crosses a threshold. Because we declared CPU requests, Kubernetes can compute utilization percentages:
kubectl autoscale deployment productservice --cpu-percent=70 --min=2 --max=10
kubectl get hpa
Pair this with the AKS cluster autoscaler (az aks update --enable-cluster-autoscaler --min-count 2 --max-count 5) so new nodes appear when pods have nowhere to run.
Azure Kubernetes Service Best Practices for .NET Teams
- Use an Ingress controller instead of one LoadBalancer per service. Each
type: LoadBalancerservice allocates a public IP. With ten microservices that gets expensive and messy. Enable the AKS Application Routing add-on (managed NGINX) and route by path or host. - Pin image tags, never use
latest.latestmakes rollbacks impossible and hides which version is actually running. - Handle SIGTERM gracefully. ASP.NET Core's generic host already listens for shutdown signals and drains in-flight requests. Don't override
ShutdownTimeoutto zero, and setterminationGracePeriodSecondsto match. - Log to stdout as JSON. Azure Monitor Container Insights collects console output automatically. Use Serilog with a compact JSON formatter so logs are queryable in Log Analytics.
- Add OpenTelemetry. Distributed tracing is the only sane way to debug a request that crosses five microservices. .NET's OpenTelemetry packages export to Azure Monitor in a few lines.
- Use Helm or Kustomize once you have more than two services. Copy-pasting YAML across services drifts fast.
- Automate with GitHub Actions or Azure DevOps. A pipeline that builds, pushes to ACR, and runs
kubectl apply(or a GitOps tool like Argo CD / Flux) keeps deployments repeatable.
Common Pitfalls When Deploying .NET to AKS
ImagePullBackOff: The cluster cannot authenticate to ACR. Runaz aks update --attach-acr <name>and check the image name is spelled exactly as it appears in ACR.CrashLoopBackOff: The app is dying at startup. Runkubectl logs <pod> --previousto see the exception. Common causes: missing configuration, or binding to port 80 as a non-root user (use 8080).- Pods stuck in
Pending: Resource requests exceed what nodes offer. Runkubectl describe pod <pod>and look at the Events section. - OOMKilled: The memory limit is too low for the .NET heap. Raise it or reduce allocations; check
kubectl top podsfor real usage. - Liveness probe too aggressive: If the app takes 20 seconds to warm up and
initialDelaySecondsis 5, Kubernetes kills it before it ever starts. Use astartupProbefor slow-starting services. - Data Protection keys not shared: With multiple replicas, ASP.NET Core antiforgery tokens and auth cookies will randomly fail unless you persist Data Protection keys to Azure Blob Storage or Key Vault.
Clean Up
AKS nodes bill by the hour. When you are done experimenting, delete the whole resource group:
az group delete --name rg-dotnet-aks --yes --no-wait
Conclusion: Key Takeaways
Deploying .NET microservices to Azure Kubernetes Service is far less intimidating once you understand the handful of building blocks involved. Here is what to remember:
- Azure Kubernetes Service gives you a managed control plane for free; you only pay for worker nodes.
- Containerize with a multi-stage Dockerfile, run as non-root, and listen on port 8080.
- Push images to Azure Container Registry and attach it to the cluster with
--attach-acr. - Every Deployment needs separate liveness and readiness probes plus CPU/memory requests and limits — the .NET GC depends on them.
- Use rolling updates with
maxUnavailable: 0for zero-downtime releases, andkubectl rollout undowhen things go wrong. - Keep configuration in ConfigMaps and Secrets (or Key Vault), autoscale with HPA, and centralize traffic through an Ingress controller.
With this foundation in place, you can add more microservices, wire in a message broker like Azure Service Bus, and build out a full CI/CD pipeline — all on the same AKS cluster you created today.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment