Azure Container Apps: Serverless Containers Made Simple
Introduction
Azure Container Apps provides fully managed serverless containers without the complexity of Kubernetes. Built on Kubernetes and open-source technologies (KEDA, Dapr, Envoy), it offers automatic scaling, traffic splitting, and integrated observability for microservices and background jobs.
Prerequisites
- Azure subscription
- Docker Desktop
- Azure CLI 2.45+
- Container registry (Azure Container Registry or Docker Hub)
Key Concepts
| Concept | Description |
|---|---|
| Environment | Secure boundary for Container Apps (shared vnet, logs) |
| Revision | Immutable snapshot of container app version |
| KEDA | Kubernetes Event-Driven Autoscaling for triggers |
| Dapr | Distributed application runtime for microservices patterns |
| Ingress | HTTP/HTTPS traffic routing with TLS termination |
Step-by-Step Guide
Step 1: Create Environment
az extension add --name containerapp --upgrade
az group create --name rg-containerapp --location eastus
az containerapp env create \
--name env-production \
--resource-group rg-containerapp \
--location eastus
Step 2: Build and Push Container Image
# Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyApi.csproj", "."]
RUN dotnet restore
COPY . .
RUN dotnet build -c Release -o /app/build
FROM build AS publish
RUN dotnet publish -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyApi.dll"]
Build and Push:
az acr create --resource-group rg-containerapp --name contosoregistry --sku Basic
az acr build --registry contosoregistry --image myapi:v1 .
Step 3: Deploy Container App
az containerapp create \
--name api-orders \
--resource-group rg-containerapp \
--environment env-production \
--image contosoregistry.azurecr.io/myapi:v1 \
--registry-server contosoregistry.azurecr.io \
--registry-identity system \
--target-port 80 \
--ingress external \
--min-replicas 1 \
--max-replicas 10 \
--cpu 0.5 --memory 1Gi \
--env-vars "ASPNETCORE_ENVIRONMENT=Production" "ConnectionStrings__Database=secretref:db-connection"
Step 4: Configure Autoscaling with KEDA
HTTP Scaling Rule:
az containerapp update \
--name api-orders \
--resource-group rg-containerapp \
--scale-rule-name http-rule \
--scale-rule-type http \
--scale-rule-http-concurrency 50
Azure Queue Scaling Rule:
az containerapp update \
--name processor-jobs \
--resource-group rg-containerapp \
--scale-rule-name queue-rule \
--scale-rule-type azure-queue \
--scale-rule-metadata queueName=orders accountName=contosostore queueLength=10 \
--scale-rule-auth triggerParameter=connection secretRef=storage-connection
Custom KEDA Scaler (CPU):
# containerapp.yaml
properties:
template:
scale:
minReplicas: 0
maxReplicas: 30
rules:
- name: cpu-scaling
custom:
type: cpu
metadata:
type: Utilization
value: "70"
Step 5: Traffic Splitting (Blue-Green/Canary)
Deploy New Revision:
az containerapp update \
--name api-orders \
--resource-group rg-containerapp \
--image contosoregistry.azurecr.io/myapi:v2 \
--revision-suffix v2
Split Traffic:
az containerapp ingress traffic set \
--name api-orders \
--resource-group rg-containerapp \
--revision-weight api-orders--v1=80 api-orders--v2=20
Gradually Shift to 100% v2:
az containerapp ingress traffic set \
--name api-orders \
--resource-group rg-containerapp \
--revision-weight api-orders--v2=100
Step 6: Dapr Integration for Microservices
Enable Dapr:
az containerapp dapr enable \
--name api-orders \
--resource-group rg-containerapp \
--dapr-app-id orders \
--dapr-app-port 80
Service-to-Service Invocation:
// C# - Dapr SDK
var daprClient = new DaprClientBuilder().Build();
// Call another container app
var inventory = await daprClient.InvokeMethodAsync<InventoryResponse>(
HttpMethod.Get,
"inventory-service",
"api/items/123"
);
Pub/Sub Pattern:
# Create Dapr component (Azure Service Bus)
az containerapp env dapr-component set \
--name env-production \
--resource-group rg-containerapp \
--dapr-component-name pubsub \
--yaml "
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.azure.servicebus
metadata:
- name: connectionString
secretRef: servicebus-connection
"
// Publish event
await daprClient.PublishEventAsync("pubsub", "orders", new OrderCreatedEvent
{
OrderId = "12345",
Total = 99.99m
});
// Subscribe to event
[Topic("pubsub", "orders")]
[HttpPost("orders")]
public async Task<IActionResult> HandleOrder(OrderCreatedEvent order)
{
// Process order
return Ok();
}
Step 7: Secrets Management
Add Secrets:
az containerapp secret set \
--name api-orders \
--resource-group rg-containerapp \
--secrets "db-connection=Server=...;Database=..." "api-key=secret123"
Reference in Environment Variables:
az containerapp update \
--name api-orders \
--resource-group rg-containerapp \
--set-env-vars "DatabaseConnection=secretref:db-connection" "ApiKey=secretref:api-key"
Azure Key Vault Integration:
az containerapp env dapr-component set \
--name env-production \
--resource-group rg-containerapp \
--dapr-component-name secretstore \
--yaml "
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: secretstore
spec:
type: secretstores.azure.keyvault
metadata:
- name: vaultName
value: contoso-keyvault
- name: azureClientId
value: <managed-identity-client-id>
"
Step 8: Observability
Application Insights:
az containerapp update \
--name api-orders \
--resource-group rg-containerapp \
--set-env-vars "APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=..."
Log Analytics Queries:
ContainerAppConsoleLogs_CL
| where ContainerAppName_s == "api-orders"
| where Level_s == "Error"
| project TimeGenerated, Message_s
| order by TimeGenerated desc
Metrics Monitoring:
ContainerAppSystemLogs_CL
| where ContainerAppName_s == "api-orders"
| summarize AvgReplicas = avg(ReplicaCount_d) by bin(TimeGenerated, 5m)
| render timechart
Advanced Patterns
Pattern 1: Background Jobs with Queue Trigger
az containerapp create \
--name job-processor \
--resource-group rg-containerapp \
--environment env-production \
--image contosoregistry.azurecr.io/processor:v1 \
--min-replicas 0 \
--max-replicas 50 \
--scale-rule-name queue-scaling \
--scale-rule-type azure-queue \
--scale-rule-metadata queueName=tasks accountName=contosostore queueLength=5
Pattern 2: Scheduled Jobs (Cron)
az containerapp job create \
--name nightly-report \
--resource-group rg-containerapp \
--environment env-production \
--image contosoregistry.azurecr.io/reports:v1 \
--trigger-type Schedule \
--cron-expression "0 2 * * *" \
--parallelism 1 \
--replica-completion-count 1
Pattern 3: Internal Communication (No Public Ingress)
az containerapp create \
--name internal-api \
--resource-group rg-containerapp \
--environment env-production \
--image contosoregistry.azurecr.io/internal:v1 \
--target-port 80 \
--ingress internal \
--min-replicas 2
Pattern 4: Custom Domain with TLS
az containerapp hostname add \
--name api-orders \
--resource-group rg-containerapp \
--hostname api.contoso.com
az containerapp hostname bind \
--name api-orders \
--resource-group rg-containerapp \
--hostname api.contoso.com \
--environment env-production \
--validation-method CNAME
Cost Optimization
| Strategy | Savings | Implementation |
|---|---|---|
| Scale to zero | 100% idle cost | Set minReplicas=0 for infrequent workloads |
| Right-size CPU/memory | 30-50% | Use 0.25 vCPU / 0.5 Gi for lightweight APIs |
| Spot instances | N/A | Not available (already consumption-based) |
| Commitment discounts | Up to 15% | Reserved capacity for predictable workloads |
Pricing Model:
- vCPU: $0.000012/second ($0.043/hour)
- Memory: $0.0000013/GB/second ($0.0047/GB/hour)
- Requests: First 2M free, then $0.40/million
Comparison: Container Apps vs AKS vs App Service
| Feature | Container Apps | AKS | App Service |
|---|---|---|---|
| Complexity | Low | High | Low |
| Control | Medium | Full | Limited |
| Scaling | KEDA (event-driven) | HPA/KEDA | Built-in |
| Pricing | Consumption | Node-based | Plan-based |
| Best For | Microservices, APIs, jobs | Complex orchestration | Web apps |
Troubleshooting
Issue: Container fails to start
Solution: Check logs via az containerapp logs show; verify container health probe; ensure image exists in registry
Issue: Ingress not reachable
Solution: Confirm ingress type (external/internal); check NSG rules; verify DNS propagation for custom domains
Issue: Scaling not triggering
Solution: Validate KEDA scaler metadata; check authentication for queue/event sources; review replica metrics
Best Practices
- Use managed identities for Azure resource authentication
- Enable Dapr for cross-service communication patterns
- Set appropriate min/max replicas based on load patterns
- Implement health probes (readiness/liveness)
- Use secrets for sensitive configuration
- Monitor with Application Insights + Log Analytics
- Version container images with immutable tags
Key Takeaways
- Container Apps = Serverless + Containers without Kubernetes complexity.
- KEDA enables event-driven autoscaling (queues, HTTP, custom).
- Dapr simplifies microservices patterns (pub/sub, state, secrets).
- Traffic splitting supports zero-downtime deployments.
Next Steps
- Implement circuit breaker with Dapr resiliency policies
- Add Azure Front Door for global load balancing
- Explore Azure Container Apps jobs for batch processing
Additional Resources
Ready to simplify your container deployments?