Deployment¶
Deploy the ECommerce system to production using Azure Kubernetes Service (AKS), implement CI/CD pipelines, configure monitoring, and enable auto-scaling.
:::note This is Part 9 (Final) of the ECommerce Tutorial. Complete Testing Strategy first. :::
Deployment Architecture¶
flowchart TD
subgraph AKS["Azure Kubernetes Service (AKS)"]
Ingress["Ingress Controller (nginx)<br/>- SSL/TLS Termination<br/>- Load Balancing"]
OrderSvc["Order Service<br/>(3 replicas)"]
CustomerSvc["Customer Service<br/>(2 replicas)"]
InventoryWorker["Inventory Worker<br/>(2 replicas)"]
PaymentWorker["Payment Worker<br/>(2 replicas)"]
NotificationWorker["Notification Worker<br/>(2 replicas)"]
ShippingWorker["Shipping Worker<br/>(2 replicas)"]
AnalyticsWorker["Analytics Worker (1 replica)"]
Ingress --> OrderSvc
OrderSvc ~~~ CustomerSvc
InventoryWorker ~~~ PaymentWorker
NotificationWorker ~~~ ShippingWorker
OrderSvc ~~~ InventoryWorker
InventoryWorker ~~~ NotificationWorker
NotificationWorker ~~~ AnalyticsWorker
end
subgraph Managed["Azure Managed Services"]
direction LR
ASB["Azure Service Bus (Premium)"] ~~~ PG["Azure Database for PostgreSQL"] ~~~ Monitor["Azure Monitor"]
end
AKS ~~~ Managed
class Ingress layer-command
class OrderSvc,CustomerSvc,InventoryWorker,PaymentWorker,NotificationWorker,ShippingWorker,AnalyticsWorker layer-core
class ASB,PG,Monitor layer-infrastructure
Step 1: Dockerfiles¶
Order Service Dockerfile¶
ECommerce.OrderService.API/Dockerfile:
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Copy solution and project files
COPY ECommerce.sln .
COPY ECommerce.OrderService.API/ECommerce.OrderService.API.csproj ECommerce.OrderService.API/
COPY ECommerce.Contracts/ECommerce.Contracts.csproj ECommerce.Contracts/
# Restore dependencies
RUN dotnet restore ECommerce.OrderService.API/ECommerce.OrderService.API.csproj
# Copy source code
COPY . .
# Build and publish
WORKDIR /src/ECommerce.OrderService.API
RUN dotnet publish -c Release -o /app/publish \
--no-restore \
/p:UseAppHost=false
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
# Copy published files
COPY --from=build /app/publish .
# Create non-root user
RUN adduser --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Expose port
EXPOSE 8080
ENTRYPOINT ["dotnet", "ECommerce.OrderService.API.dll"]
Worker Service Dockerfile¶
ECommerce.InventoryWorker/Dockerfile:
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ECommerce.sln .
COPY ECommerce.InventoryWorker/ECommerce.InventoryWorker.csproj ECommerce.InventoryWorker/
COPY ECommerce.Contracts/ECommerce.Contracts.csproj ECommerce.Contracts/
RUN dotnet restore ECommerce.InventoryWorker/ECommerce.InventoryWorker.csproj
COPY . .
WORKDIR /src/ECommerce.InventoryWorker
RUN dotnet publish -c Release -o /app/publish \
--no-restore \
/p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/runtime:10.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
RUN adduser --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser
ENTRYPOINT ["dotnet", "ECommerce.InventoryWorker.dll"]
Step 2: Kubernetes Manifests¶
Order Service Deployment¶
k8s/order-service/deployment.yaml:
Order Service Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: ecommerce
labels:
app: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: ecommerceacr.azurecr.io/order-service:latest
ports:
- containerPort: 8080
name: http
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
- name: ConnectionStrings__OrdersDb
valueFrom:
secretKeyRef:
name: database-secrets
key: orders-db-connection-string
# Services resolve the transport via GetConnectionString("servicebus")
- name: ConnectionStrings__servicebus
valueFrom:
secretKeyRef:
name: servicebus-secrets
key: connection-string
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
# Paths match ServiceDefaults' MapDefaultEndpoints():
# /alive = liveness-tagged checks only, /health = all checks
livenessProbe:
httpGet:
path: /alive
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: order-service
namespace: ecommerce
spec:
selector:
app: order-service
ports:
- port: 80
targetPort: 8080
name: http
type: ClusterIP
Horizontal Pod Autoscaler¶
k8s/order-service/hpa.yaml:
Horizontal Pod Autoscaler
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service-hpa
namespace: ecommerce
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
Ingress¶
k8s/ingress.yaml:
Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ecommerce-ingress
namespace: ecommerce
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.ecommerce.example.com
secretName: ecommerce-tls
rules:
- host: api.ecommerce.example.com
http:
paths:
- path: /api/orders
pathType: Prefix
backend:
service:
name: order-service
port:
number: 80
- path: /api/customers
pathType: Prefix
backend:
service:
name: customer-service
port:
number: 80
- path: /api/analytics
pathType: Prefix
backend:
service:
name: analytics-service
port:
number: 80
Step 3: Azure Infrastructure (Bicep)¶
infra/main.bicep:
param location string = 'eastus'
param environment string = 'production'
// Azure Kubernetes Service
resource aks 'Microsoft.ContainerService/managedClusters@2024-01-01' = {
name: 'ecommerce-aks-${environment}'
location: location
identity: {
type: 'SystemAssigned'
}
properties: {
kubernetesVersion: '1.28'
dnsPrefix: 'ecommerce-${environment}'
agentPoolProfiles: [
{
name: 'nodepool1'
count: 3
vmSize: 'Standard_D4s_v3'
mode: 'System'
enableAutoScaling: true
minCount: 3
maxCount: 10
osDiskSizeGB: 128
osType: 'Linux'
}
]
networkProfile: {
networkPlugin: 'azure'
loadBalancerSku: 'standard'
serviceCidr: '10.0.0.0/16'
dnsServiceIP: '10.0.0.10'
}
addonProfiles: {
azurePolicy: {
enabled: true
}
omsagent: {
enabled: true
config: {
logAnalyticsWorkspaceResourceID: logAnalytics.id
}
}
}
}
}
// Azure Database for PostgreSQL
resource postgres 'Microsoft.DBforPostgreSQL/flexibleServers@2023-03-01-preview' = {
name: 'ecommerce-postgres-${environment}'
location: location
sku: {
name: 'Standard_D4s_v3'
tier: 'GeneralPurpose'
}
properties: {
version: '16'
administratorLogin: 'pgadmin'
administratorLoginPassword: '<secure-password>'
storage: {
storageSizeGB: 128
}
backup: {
backupRetentionDays: 7
geoRedundantBackup: 'Enabled'
}
highAvailability: {
mode: 'ZoneRedundant'
}
}
}
// Azure Service Bus
resource serviceBus 'Microsoft.ServiceBus/namespaces@2023-01-01-preview' = {
name: 'ecommerce-servicebus-${environment}'
location: location
sku: {
name: 'Premium'
tier: 'Premium'
capacity: 1
}
properties: {
zoneRedundant: true
}
}
// Azure Container Registry
resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' = {
name: 'ecommerceacr${environment}'
location: location
sku: {
name: 'Premium'
}
properties: {
adminUserEnabled: false
publicNetworkAccess: 'Enabled'
}
}
// Log Analytics Workspace
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
name: 'ecommerce-logs-${environment}'
location: location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 30
}
}
// Application Insights
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
name: 'ecommerce-appinsights-${environment}'
location: location
kind: 'web'
properties: {
Application_Type: 'web'
WorkspaceResourceId: logAnalytics.id
}
}
output aksName string = aks.name
output acrLoginServer string = acr.properties.loginServer
output postgresHost string = postgres.properties.fullyQualifiedDomainName
output serviceBusNamespace string = serviceBus.name
Deploy infrastructure:
Step 3: Azure Infrastructure (Bicep)
az deployment group create \
--resource-group ecommerce-rg \
--template-file infra/main.bicep \
--parameters environment=production
Step 4: CI/CD Pipeline (GitHub Actions)¶
.github/workflows/deploy.yaml:
Step 4: CI/CD Pipeline (GitHub Actions)
name: Build and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
AZURE_RESOURCE_GROUP: ecommerce-rg
AKS_CLUSTER_NAME: ecommerce-aks-production
ACR_NAME: ecommerceacrproduction
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Run unit tests
run: dotnet test --no-build --verbosity normal --logger trx
- name: Run integration tests
run: |
docker-compose -f docker-compose.test.yml up -d
dotnet test tests/ECommerce.IntegrationTests --no-build
docker-compose -f docker-compose.test.yml down
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
strategy:
matrix:
service:
- order-service
- inventory-worker
- payment-worker
- notification-worker
- shipping-worker
- customer-service
- analytics-worker
steps:
- uses: actions/checkout@v4
- name: Login to Azure Container Registry
uses: azure/docker-login@v1
with:
login-server: ${{ env.ACR_NAME }}.azurecr.io
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- name: Build and push Docker image
run: |
docker build -t ${{ env.ACR_NAME }}.azurecr.io/${{ matrix.service }}:${{ github.sha }} \
-f ECommerce.${{ matrix.service }}/Dockerfile .
docker push ${{ env.ACR_NAME }}.azurecr.io/${{ matrix.service }}:${{ github.sha }}
deploy:
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Get AKS credentials
run: |
az aks get-credentials \
--resource-group ${{ env.AZURE_RESOURCE_GROUP }} \
--name ${{ env.AKS_CLUSTER_NAME }}
- name: Update Kubernetes manifests
run: |
sed -i "s|:latest|:${{ github.sha }}|g" k8s/**/*.yaml
- name: Deploy to AKS
run: |
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/secrets/ --namespace ecommerce
kubectl apply -f k8s/ --recursive --namespace ecommerce
- name: Wait for rollout
run: |
kubectl rollout status deployment/order-service --namespace ecommerce --timeout=10m
kubectl rollout status deployment/inventory-worker --namespace ecommerce --timeout=10m
Step 5: Monitoring and Observability¶
Application Insights Integration¶
Program.cs:
The sample's ECommerce.ServiceDefaults project (wired by builder.AddServiceDefaults()) already configures OpenTelemetry with the modern AddOpenTelemetry() builder:
Application Insights Integration
// From ECommerce.ServiceDefaults/Extensions.cs (applied via builder.AddServiceDefaults())
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => {
metrics.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
})
.WithTracing(tracing => {
tracing.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation();
});
// The OTLP exporter is enabled when OTEL_EXPORTER_OTLP_ENDPOINT is configured
// (Aspire sets this automatically; in AKS point it at your collector).
Prometheus Metrics¶
k8s/monitoring/prometheus.yaml:
Prometheus Metrics
apiVersion: v1
kind: ServiceMonitor
metadata:
name: ecommerce-services
namespace: ecommerce
spec:
selector:
matchLabels:
app: order-service
endpoints:
- port: http
path: /metrics
interval: 30s
Custom Metrics¶
Receptors/CreateOrderReceptor.cs:
Custom Metrics
private static readonly Meter _meter = new("ECommerce.OrderService");
private static readonly Counter<long> _ordersCreated = _meter.CreateCounter<long>(
"orders_created_total",
description: "Total number of orders created"
);
public async ValueTask<OrderCreatedEvent> HandleAsync(
CreateOrderCommand message,
CancellationToken cancellationToken = default) {
// ... process order (validate, build OrderCreatedEvent, PublishAsync)
_ordersCreated.Add(1, new TagList {
{ "item_count", message.LineItems.Count }
});
return orderCreated;
}
Step 6: Database Migrations¶
Migration Job¶
k8s/jobs/migrate-orders-db.yaml:
Migration Job
apiVersion: batch/v1
kind: Job
metadata:
name: migrate-orders-db
namespace: ecommerce
spec:
template:
spec:
containers:
- name: migrate
image: ecommerceacr.azurecr.io/order-service:latest
command: ["dotnet", "ECommerce.OrderService.API.dll", "migrate"]
env:
- name: ConnectionStrings__OrdersDb
valueFrom:
secretKeyRef:
name: database-secrets
key: orders-db-connection-string
restartPolicy: OnFailure
backoffLimit: 3
Run migration before deployment:
Migration Job (2)
kubectl apply -f k8s/jobs/migrate-orders-db.yaml
kubectl wait --for=condition=complete job/migrate-orders-db --timeout=5m
:::note
Whizbang's own schema (inbox, outbox, event store, and PerspectiveRow<T> tables plus their PostgreSQL functions) is created at service startup by the generated EnsureWhizbangDatabaseInitializedAsync() extension — no separate job needed for it. This migration job covers the service's custom tables (e.g. customer_activity, the analytics time-series tables); the migrate argument is a CLI entry point you implement in the service.
:::
Step 7: Blue-Green Deployment¶
k8s/order-service/deployment-blue.yaml:
Step 7: Blue-Green Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service-blue
namespace: ecommerce
labels:
app: order-service
version: blue
spec:
replicas: 3
selector:
matchLabels:
app: order-service
version: blue
template:
metadata:
labels:
app: order-service
version: blue
spec:
containers:
- name: order-service
image: ecommerceacr.azurecr.io/order-service:v1.0.0
# ...
k8s/order-service/service-switch.yaml:
Step 7: Blue-Green Deployment (2)
apiVersion: v1
kind: Service
metadata:
name: order-service
namespace: ecommerce
spec:
selector:
app: order-service
version: blue # Switch to "green" for rollover
ports:
- port: 80
targetPort: 8080
Deployment process:
Step 7: Blue-Green Deployment (3)
# Deploy green version
kubectl apply -f k8s/order-service/deployment-green.yaml
# Wait for readiness
kubectl wait --for=condition=available deployment/order-service-green --timeout=5m
# Switch traffic to green
kubectl patch service order-service -p '{"spec":{"selector":{"version":"green"}}}'
# Monitor for errors (5 minutes)
# If successful, delete blue
kubectl delete deployment order-service-blue
Key Takeaways¶
✅ Kubernetes Deployment - AKS with auto-scaling and health checks ✅ CI/CD Pipeline - GitHub Actions for automated testing and deployment ✅ Infrastructure as Code - Bicep for Azure resources ✅ Monitoring - Application Insights and Prometheus metrics ✅ Blue-Green Deployment - Zero-downtime deployments ✅ Database Migrations - Automated with Kubernetes jobs
Production Checklist¶
Before going live:
- [ ] SSL/TLS certificates configured (cert-manager + Let's Encrypt)
- [ ] Secrets stored in Azure Key Vault (not ConfigMaps)
- [ ] Database backups configured (7-day retention)
- [ ] Log aggregation configured (Azure Monitor)
- [ ] Alerts configured for critical errors
- [ ] Auto-scaling tested under load
- [ ] Disaster recovery plan documented
- [ ] Security scanning in CI/CD pipeline
- [ ] Rate limiting configured on Ingress
- [ ] DDoS protection enabled
Congratulations!¶
You've completed the ECommerce Tutorial and built a production-ready, event-driven microservices system with Whizbang! 🎉
What you've learned: - Event-driven architecture with CQRS - Distributed transactions with sagas - Read models with perspectives - Testing strategies (unit, integration, e2e) - Production deployment on Kubernetes
Next steps: - Explore Advanced Topics for performance tuning and scaling - Check out Customization Examples for real-world patterns - Join the community and share your Whizbang projects!
Version 1.0.0 - Foundation Release | Last Updated: 2024-12-12