Azure Container Apps: Serverless Containers Made Simple

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?