Learnixo
Back to blog
Backend Systemsintermediate

Azure Container Apps for .NET: Serverless Containers in Production

Deploy .NET microservices to Azure Container Apps. Covers environments, revisions, scaling rules, Dapr integration, secrets, managed identity, KEDA, blue-green deployments, and cost optimisation.

LearnixoJune 4, 20266 min read
.NETC#AzureContainer AppsKubernetesServerlessDocker
Share:š•

What is Azure Container Apps?

Azure Container Apps (ACA) is a serverless container platform built on Kubernetes — but you never touch Kubernetes directly. It manages scaling, networking, certificates, and Dapr integration for you.

Azure Container Apps
ā”œā”€ā”€ Environment (shared VNet, Log Analytics)
│   ā”œā”€ā”€ Container App: orders-api
│   │   ā”œā”€ā”€ Revision 1 (previous)    ← 20% traffic
│   │   └── Revision 2 (current)     ← 80% traffic
│   ā”œā”€ā”€ Container App: product-api
│   └── Container App: notifications

ACA vs AKS vs App Service:

| | App Service | ACA | AKS | |---|---|---|---| | Control | Low | Medium | Full | | K8s expertise needed | None | None | Yes | | Scale to zero | No | Yes | Configurable | | Dapr built-in | No | Yes | Manual | | Cost | Always-on | Pay per use | Node VMs | | Use when | Simple web apps | Microservices | Full K8s control |


Core Concepts

Environment — shared boundary for all your apps. Shared VNet, Log Analytics workspace, Dapr configuration.

Revision — an immutable snapshot of a container app. Every deployment creates a new revision. Traffic splits between revisions for blue-green.

Replica — a running instance of a revision. ACA scales replicas based on rules.

Ingress — HTTP/HTTPS routing. Can be external (public) or internal (VNet only).


Deploy with Azure CLI

Bash
# Create resource group and environment
az group create --name orderflow-rg --location uksouth

az containerapp env create \
  --name orderflow-env \
  --resource-group orderflow-rg \
  --location uksouth

# Deploy a container app
az containerapp create \
  --name orders-api \
  --resource-group orderflow-rg \
  --environment orderflow-env \
  --image myregistry.azurecr.io/orders-api:latest \
  --target-port 8080 \
  --ingress external \
  --min-replicas 1 \
  --max-replicas 10 \
  --cpu 0.5 \
  --memory 1.0Gi \
  --registry-server myregistry.azurecr.io \
  --registry-identity system

# Update to a new image (creates new revision)
az containerapp update \
  --name orders-api \
  --resource-group orderflow-rg \
  --image myregistry.azurecr.io/orders-api:v2

Bicep / Infrastructure as Code

BICEP
// main.bicep
resource environment 'Microsoft.App/managedEnvironments@2024-03-01' = {
  name: 'orderflow-env'
  location: location
  properties: {
    appLogsConfiguration: {
      destination: 'log-analytics'
      logAnalyticsConfiguration: {
        customerId: logAnalytics.properties.customerId
        sharedKey: logAnalytics.listKeys().primarySharedKey
      }
    }
  }
}

resource ordersApi 'Microsoft.App/containerApps@2024-03-01' = {
  name: 'orders-api'
  location: location
  identity: { type: 'SystemAssigned' }
  properties: {
    environmentId: environment.id
    configuration: {
      ingress: {
        external: true
        targetPort: 8080
        transport: 'http2'
      }
      secrets: [
        { name: 'db-connection', value: sqlConnectionString }
      ]
      registries: [
        { server: acrLoginServer, identity: 'system' }
      ]
    }
    template: {
      containers: [
        {
          name: 'orders-api'
          image: '${acrLoginServer}/orders-api:${imageTag}'
          resources: { cpu: '0.5', memory: '1Gi' }
          env: [
            { name: 'ASPNETCORE_ENVIRONMENT', value: 'Production' }
            { name: 'ConnectionStrings__Default', secretRef: 'db-connection' }
          ]
        }
      ]
      scale: {
        minReplicas: 1
        maxReplicas: 20
        rules: [
          {
            name: 'http-scale'
            http: { metadata: { concurrentRequests: '100' } }
          }
        ]
      }
    }
  }
}

Scaling Rules

ACA uses KEDA under the hood. Scale based on HTTP traffic, queue depth, CPU, memory, or custom metrics.

Bash
# HTTP-based scaling
az containerapp update \
  --name orders-api \
  --scale-rule-name http-rule \
  --scale-rule-type http \
  --scale-rule-metadata concurrentRequests=50

# Azure Service Bus queue depth
az containerapp update \
  --name notification-worker \
  --scale-rule-name queue-rule \
  --scale-rule-type azure-servicebus \
  --scale-rule-metadata queueName=notifications messageCount=10 \
  --scale-rule-auth triggerAuthentication=sb-auth

Scale to zero (pay nothing when idle):

Bash
az containerapp update \
  --name batch-processor \
  --min-replicas 0 \
  --max-replicas 5

Managed Identity (No Credentials)

C#
// Use DefaultAzureCredential — works locally (dev identity) and in ACA (managed identity)
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(new SqlConnection
    {
        ConnectionString = configuration.GetConnectionString("Default"),
        AccessToken = new DefaultAzureCredential()
            .GetToken(new TokenRequestContext(["https://database.windows.net/.default"]))
            .Token
    }));

// Or for Key Vault
builder.Configuration.AddAzureKeyVault(
    new Uri($"https://{kvName}.vault.azure.net/"),
    new DefaultAzureCredential());

Grant access in ACA:

Bash
# Assign Key Vault Secrets User role to the container app's managed identity
az role assignment create \
  --role "Key Vault Secrets User" \
  --assignee $(az containerapp show --name orders-api --query identity.principalId -o tsv) \
  --scope /subscriptions/.../resourceGroups/orderflow-rg/providers/Microsoft.KeyVault/vaults/my-kv

Blue-Green Deployments

Bash
# Deploy new version — traffic still goes to old revision
az containerapp update \
  --name orders-api \
  --image myregistry.azurecr.io/orders-api:v2 \
  --revision-suffix v2 \
  --traffic-weight latest=0  # new revision gets 0% traffic

# Gradually shift traffic
az containerapp ingress traffic set \
  --name orders-api \
  --revision-weight orders-api--v2=20 orders-api--v1=80

# Full cutover
az containerapp ingress traffic set \
  --name orders-api \
  --revision-weight orders-api--v2=100

# Rollback instantly — just point traffic back
az containerapp ingress traffic set \
  --name orders-api \
  --revision-weight orders-api--v1=100

Built-in Dapr Integration

No separate Dapr installation needed — ACA manages the sidecar:

Bash
az containerapp dapr enable \
  --name orders-api \
  --dapr-app-id orders-api \
  --dapr-app-port 8080 \
  --dapr-app-protocol http
BICEP
// In Bicep
dapr: {
  enabled: true
  appId: 'orders-api'
  appPort: 8080
  appProtocol: 'http'
}

GitHub Actions Deployment

YAML
- name: Deploy to Azure Container Apps
  uses: azure/container-apps-deploy-action@v1
  with:
    acrName: ${{ vars.ACR_NAME }}
    containerAppName: orders-api
    resourceGroup: orderflow-rg
    imageToDeploy: ${{ vars.ACR_LOGIN_SERVER }}/orders-api:${{ github.sha }}
    environmentVariables: |
      ASPNETCORE_ENVIRONMENT=Production

Health Probes

C#
// Startup, liveness, and readiness probes
builder.Services.AddHealthChecks()
    .AddSqlServer(connectionString, tags: ["ready"])
    .AddRedis(redisConnection, tags: ["ready"]);

app.MapHealthChecks("/health/live");   // liveness — is the process alive?
app.MapHealthChecks("/health/ready",   // readiness — is it ready to serve traffic?
    new HealthCheckOptions { Predicate = c => c.Tags.Contains("ready") });

ACA uses these automatically if you configure them in the revision.


Interview Questions

Q: What is the difference between Azure Container Apps and Azure Kubernetes Service? ACA is a managed serverless container platform — you don't manage nodes, networking, or Kubernetes control plane. AKS gives you full Kubernetes control but requires operational expertise. Use ACA when you want containers without K8s complexity; use AKS when you need advanced networking, custom admission controllers, or specific node configurations.

Q: What is a revision in Azure Container Apps? An immutable snapshot created on every deployment. Revisions can receive a share of traffic — enabling blue-green and canary deployments without additional tooling. Rolling back means redirecting traffic to a previous revision — instant with no redeployment.

Q: How does ACA scale to zero and why is that useful? When min replicas is set to 0, ACA terminates all instances when there's no traffic. You pay nothing for idle time. When traffic arrives, ACA starts a replica (cold start ~1-2s for .NET). Useful for batch processors, scheduled jobs, and low-traffic services in dev/staging.

Q: How do you avoid hardcoded credentials in Azure Container Apps? Assign a system-managed identity to the container app, then grant it RBAC roles on Key Vault, SQL Server, Service Bus, etc. Use DefaultAzureCredential in code — it automatically uses the managed identity in ACA and your developer identity locally. No secrets, no rotation.

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.