Implementing Cloud-Native Microservices with ASP.NET Core and Kubernetes
Executive Summary
In modern .NET core enterprise applications, monolithic architectures struggle with scaling, deployment speed, and team velocity. This guide solves that by showing you how to build, containerize, and deploy independent ASP.NET Core microservices to Kubernetes. You’ll create a production-ready catalog service that scales horizontally, handles health checks, and communicates reliably—essential for cloud-native apps that must run 24/7 with zero-downtime updates and automatic scaling.
Prerequisites
- .NET 10 SDK (latest stable release)
- Docker Desktop with Kubernetes enabled (for local cluster)
- kubectl CLI (install via
winget install Kubernetes.kubectlon Windows or brew on macOS) - Visual Studio 2022 or VS Code with C# Dev Kit extension
- Minikube (optional fallback:
minikube start) - Basic folders: Create a solution root with
services/catalogsubfolder
Step-by-Step Implementation
Step 1: Create the Catalog Microservice with Minimal APIs
Let’s build our first microservice—a catalog API exposing products. Start in services/catalog.
dotnet new webapi -n CatalogService --no-https -f net10.0
cd CatalogService
dotnet add package Microsoft.AspNetCore.OpenApi
Replace Program.cs with this modern minimal API using primary constructors and records:
using CatalogService.Models;
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHealthChecks();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
var products = new[]
{
new Product(1, "Laptop", 999.99m),
new Product(2, "Mouse", 29.99m)
};
app.MapGet("/products", () => products)
.WithTags("Products")
.WithOpenApi();
app.MapHealthChecks("/health");
app.MapHealthChecks("/ready", HealthCheckOptions);
app.Run();
static void HealthCheckOptions(HealthCheckOptions options)
{
options.AddCheck("self", () => HealthCheckResult.Healthy());
}
Create Models/Product.cs:
namespace CatalogService.Models;
public record Product(int Id, string Name, decimal Price);
Step 2: Add Docker Multi-Stage Build
Create Dockerfile in services/catalog for optimized, production-ready images:
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["CatalogService.csproj", "."]
RUN dotnet restore "CatalogService.csproj"
COPY . .
RUN dotnet publish "CatalogService.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl --fail http://localhost:8080/health || exit 1
ENTRYPOINT ["dotnet", "CatalogService.dll"]
Build and test locally:
docker build -t catalog-service:dev .
docker run -p 8080:8080 catalog-service:dev
Hit http://localhost:8080/swagger—your API is live!
Step 3: Deploy to Kubernetes with Manifests
Enable Kubernetes in Docker Desktop. Create k8s/ folder with these YAML files.
deployment.yaml (with probes and resource limits):
apiVersion: apps/v1
kind: Deployment
metadata:
name: catalog-deployment
spec:
replicas: 2
selector:
matchLabels:
app: catalog
template:
metadata:
labels:
app: catalog
spec:
containers:
- name: catalog
image: catalog-service:dev
ports:
- containerPort: 8080
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: catalog-service
spec:
selector:
app: catalog
ports:
- port: 80
targetPort: 8080
type: ClusterIP
Deploy:
kubectl apply -f k8s/deployment.yaml
kubectl get pods
kubectl port-forward service/catalog-service 8080:80
Access at http://localhost:8080/swagger. Scale with kubectl scale deployment catalog-deployment --replicas=3.
Step 4: Add ConfigMaps and Secrets
For environment-specific config, create configmap.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: catalog-config
data:
Logging__LogLevel__Default: "Information"
Products__MinPrice: "10.0"
---
apiVersion: v1
kind: Secret
metadata:
name: catalog-secret
type: Opaque
data:
ConnectionStrings__Db: c29tZS1iYXNlNjQtZGF0YQo= # "some-base64-data"
Mount in deployment under envFrom and volumeMounts.
Production-Ready C# Examples
Enhance with gRPC for inter-service calls and async events. Add to Program.cs:
// gRPC example for Product service
builder.Services.AddGrpc();
app.MapGrpcService<ProductService>();
// Event publishing with IMessageBroker (inject MassTransit or custom)
app.MapPost("/products", async (Product product, IMessageBroker broker) =>
{
await broker.PublishAsync(new ProductCreated(product.Id, product.Name));
return Results.Created($"/products/{product.Id}", product);
});
Use primary constructors for lean services:
public class ProductService(IMessageBroker broker) : ProductServiceBase
{
public override async Task<GetProductsResponse> GetProducts(GetProductsRequest request, ServerCallContext context)
{
// Fetch from DB or cache
await broker.PublishAsync(new ProductsQueried());
return new() { Products = { /* products */ } };
}
}
Common Pitfalls & Troubleshooting
- Pod stuck in CrashLoopBackOff: Check logs with
kubectl logs <pod-name>. Fix health probe paths or port mismatches. - Image pull errors: Tag images correctly; use
docker pushto registry like Docker Hub. - Service not reachable: Verify selector labels match deployment. Use
kubectl describe service catalog-service. - High memory usage: Set resource limits; profile with
dotnet-countersinside pod. - Config not loading: Use
envFrom: configMapRefinstead of individual env vars.
Performance & Scalability Considerations
-
- Enable Horizontal Pod Autoscaler (HPA):
kubectl autoscale deployment catalog-deployment --cpu-percent=50 --min=2 --max=10. - Use ASP.NET Core Kestrel tuning: Set
Kestrel__Limits__MaxConcurrentConnections=1000in ConfigMap. - Distributed caching with Redis: Add
services.AddStackExchangeRedisCache(). - Readiness gates for database migrations before traffic routing.
- Monitor with Prometheus + Grafana; scrape
/metricsendpoint.
- Enable Horizontal Pod Autoscaler (HPA):
Practical Best Practices
-
-
- Always use multi-stage Dockerfiles to keep images under 100MB.
- Implement OpenTelemetry for tracing:
builder.Services.AddOpenTelemetryTracing(). - Test locally with Docker Compose for multi-service setups.
- Use Helm charts for complex deployments:
helm create catalog-chart. - Write integration tests against Kubernetes-in-Docker (kind or minikube).
- Prefer gRPC over REST for internal service calls—faster and typed.
-
Conclusion
You now have a fully functional, cloud-native catalog microservice running on Kubernetes. Next, add more services (basket, ordering), wire them with an API Gateway like Ocelot, and deploy to AKS or EKS. Experiment with Istio for service mesh and CI/CD with GitHub Actions.
FAQs
1. How do I expose my service externally in production?
Use an Ingress controller like NGINX Ingress. Create an Ingress resource pointing to your service port 80, with TLS for HTTPS.
2. What’s the difference between liveness and readiness probes?
Liveness restarts unhealthy pods; readiness stops routing traffic until the app is fully initialized (e.g., DB connected).
3. How do microservices communicate reliably?
Synchronous: gRPC or HTTP. Asynchronous: MassTransit with RabbitMQ/Kafka for events. Avoid direct DB coupling.
4. Can I use Entity Framework in microservices?
Yes, but per-service DBs only. Use dotnet ef migrations add in init containers for schema changes.
5. How to handle secrets in Kubernetes?
Store in Kubernetes Secrets or external vaults like Azure Key Vault. Mount as volumes or env vars—never hardcode.
6. Why multi-stage Dockerfiles?
They exclude build tools (SDK=500MB+), resulting in tiny runtime images (~100MB) that deploy faster and scale better.
7. How to debug pods interactively?
kubectl exec -it <pod> -- bash, then dotnet-counters collect or attach VS Code debugger.
8. Should I use StatefulSets or Deployments?
Deployments for stateless APIs like catalog. StatefulSets for databases needing stable identities.
9. How to roll out zero-downtime updates?
Kubernetes rolling updates replace pods gradually. Use strategy: type: RollingUpdate, maxUnavailable: 0.
10. What’s next after this single service?
Build a full eShopOnContainers clone: add ordering/basket services, API Gateway, and observability with Jaeger.
Headless Architecture in .NET Microservices with gRPC
