Conclusion
By marrying .NET 8’s performance, Dapr’s portable pub/sub abstraction, and Azure Container Apps’ serverless scaling, you can build event‑driven workers that handle millions of messages a day with minimal operational overhead. The stack lets developers focus on business logic while the platform automatically provisions compute, balances load, and retries failures.
Start with a local Dapr run, push a container to ACR, and let Azure’s built‑in KEDA scaler do the heavy lifting. The result is a resilient, cost‑effective pipeline ready for any spike in traffic.
Author: Mahmut Sarıkaya — sarikayadev.com
Sources
- Microsoft Docs – Azure Container Apps overview
- Dapr Documentation – Pub/Sub building blocks
- .NET 8 release notes – Performance improvements
Observability: Metrics, Traces, and Logs
When you move from a local Dapr sandbox to Azure Container Apps, visibility into the worker’s health becomes a critical factor. Azure Monitor integrates natively with Container Apps, exposing both platform metrics (CPU, memory, replica count) and custom application metrics emitted through Dapr sidecars. By instrumenting your .NET 8 worker with OpenTelemetry, you can push traces and metrics directly to Azure Monitor or to a dedicated Azure Log Analytics workspace.
Start by adding the OpenTelemetry SDK to your project:
<ItemGroup>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.6.0" />
<PackageReference Include="OpenTelemetry.Exporter.AzureMonitor" Version="1.2.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.5.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.5.0" />
</ItemGroup>
In Program.cs, register the exporter and enable activity propagation across the Dapr sidecar:
builder.Services.AddOpenTelemetry()
.WithTracing(traceBuilder =>
{
traceBuilder
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddDaprInstrumentation() // provided by Dapr SDK
.AddAzureMonitorTraceExporter(o =>
{
o.ConnectionString = Environment.GetEnvironmentVariable("AZURE_MONITOR_CONNECTION_STRING");
});
})
.WithMetrics(metricBuilder =>
{
metricBuilder
.AddRuntimeInstrumentation()
.AddHttpClientInstrumentation()
.AddDaprInstrumentation()
.AddAzureMonitorMetricExporter(o =>
{
o.ConnectionString = Environment.GetEnvironmentVariable("AZURE_MONITOR_CONNECTION_STRING");
});
});
Once deployed, you can create Azure Dashboard tiles that display the number of processed messages per second, latency distribution, and error rates. The Dapr sidecar also emits its own health metrics (e.g., dapr_runtime_up), which you can query to detect mis‑configurations before they affect the business flow. Coupled with Azure Application Insights alerts, you get a full‑stack observability solution that works across scaling events, whether KEDA adds or removes replicas.
CI/CD Automation with GitHub Actions and Azure Container Apps
Manual container pushes are fine for experiments, but production workloads benefit from a repeatable pipeline. GitHub Actions provides a seamless way to build, test, push the image to Azure Container Registry (ACR), and finally update the Container Apps revision. The workflow below demonstrates a three‑stage pipeline: build, security scan, and deploy.
name: CI/CD for .NET 8 Worker
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Restore and build
run: |
dotnet restore
dotnet test --no-build --verbosity normal
dotnet publish -c Release -o out
- name: Build Docker image
run: |
docker build -t ${{ secrets.ACR_NAME }}.azurecr.io/worker:${{ github.sha }} .
- name: Log in to ACR
uses: azure/docker-login@v1
with:
login-server: ${{ secrets.ACR_NAME }}.azurecr.io
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- name: Push image
run: |
docker push ${{ secrets.ACR_NAME }}.azurecr.io/worker:${{ github.sha }}
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Azure login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to Azure Container Apps
run: |
az containerapp update \
--name my-worker \
--resource-group ${{ secrets.AZURE_RG }} \
--image ${{ secrets.ACR_NAME }}.azurecr.io/worker:${{ github.sha }} \
--cpu 0.5 --memory 1Gi
This pipeline guarantees that every commit that passes unit tests ends up as a new revision of the worker. Because Azure Container Apps supports zero‑downtime rollouts, the new replica set is warmed up while the previous one continues to process messages. If health probes (exposed by Dapr on /healthz) fail, the platform automatically rolls back, preserving message integrity.
Beyond the basic flow, you can extend the workflow with Trivy for container vulnerability scanning, or add a step that runs dapr run locally to perform integration tests against a real pub/sub component before pushing. By codifying these practices, your team reduces the risk of configuration drift and ensures that scaling, observability, and security are baked into every deployment.