The second part of the series about Azure Container Apps. In the first part we established when migration is worthwhile. Here we move to practice: environment, deployment, and scaling.
Container Apps environment
An environment is the boundary in which one or more container apps run. Apps in the same environment share the same virtual network and can communicate with each other through internal service discovery. It is also the boundary for observability (Log Analytics) and for Dapr.
# Install the extension (only once)
az extension add --name containerapp --upgrade
# Register the necessary providers
az provider register --namespace Microsoft.App
az provider register --namespace Microsoft.OperationalInsights
# Create the environment
az containerapp env create \
--name my-aca-env \
--resource-group my-rg \
--location westeurope
Application deployment
We assume you already have an image in Azure Container Registry (building the image is covered in the CI/CD part). Basic deployment:
az containerapp create \
--name my-api \
--resource-group my-rg \
--environment my-aca-env \
--image myregistry.azurecr.io/my-api:latest \
--target-port 8080 \
--ingress external \
--registry-server myregistry.azurecr.io \
--min-replicas 1 \
--max-replicas 10 \
--cpu 0.5 \
--memory 1.0Gi
Some important details for a .NET application:
- target-port — the port Kestrel listens on inside the container. Explicitly set
ASPNETCORE_URLS=http://+:8080in the image or as an environment variable. - ingress external — exposes the application publicly. Use
internalfor services that communicate only inside the environment. - min-replicas — set to 0 for scale-to-zero (we'll return to this shortly).
Managed Identity for pull from ACR
Instead of registry credentials, use Managed Identity — consistent with everything we've built in the series:
# Enable system-assigned identity on the container app
az containerapp identity assign \
--name my-api \
--resource-group my-rg \
--system-assigned
PRINCIPAL_ID=$(az containerapp identity show \
--name my-api --resource-group my-rg \
--query principalId -o tsv)
ACR_ID=$(az acr show --name myregistry --query id -o tsv)
# AcrPull role for the identity
az role assignment create \
--assignee $PRINCIPAL_ID \
--role "AcrPull" \
--scope $ACR_ID
# Configure the registry to use Managed Identity
az containerapp registry set \
--name my-api --resource-group my-rg \
--server myregistry.azurecr.io \
--identity system
Health probes
ACA uses exactly the health check endpoints you have already built. Configuring liveness and readiness probes:
# containerapp.yaml (fragment from template)
properties:
template:
containers:
- name: my-api
image: myregistry.azurecr.io/my-api:latest
probes:
- type: Liveness
httpGet:
path: /health/live
port: 8080
periodSeconds: 10
failureThreshold: 3
- type: Readiness
httpGet:
path: /health/ready
port: 8080
periodSeconds: 5
failureThreshold: 3
- type: Startup
httpGet:
path: /health/live
port: 8080
periodSeconds: 5
failureThreshold: 30
The Startup probe is useful for .NET applications with longer warm-up (JIT, Key Vault loading, initial connections): ACA waits until startup passes before starting liveness checks, avoiding premature restarts.
Scaling rules with KEDA
This is the major difference compared to App Service. ACA uses KEDA for event-based scaling, not just CPU/RAM.
Scale-to-zero
# min-replicas 0 = scale-to-zero when there is no traffic
az containerapp update \
--name my-api --resource-group my-rg \
--min-replicas 0 \
--max-replicas 10
With min-replicas 0, the application stops completely when there is no traffic and starts on the first request. The trade-off is cold start (a few seconds for a .NET application). For APIs with sporadic traffic or occasional jobs, the cost savings are significant.
Scaling on HTTP concurrency
# Scale when exceeding 50 concurrent requests per replica
az containerapp update \
--name my-api --resource-group my-rg \
--scale-rule-name http-rule \
--scale-rule-type http \
--scale-rule-http-concurrency 50
Scaling on Service Bus queue
The classic scenario for which ACA is better than App Service: a worker that processes messages and scales based on queue length.
# Scale rule for Service Bus queue
scale:
minReplicas: 0
maxReplicas: 20
rules:
- name: servicebus-rule
custom:
type: azure-servicebus
metadata:
queueName: orders
namespace: my-servicebus-namespace
messageCount: "10" # 1 replica per 10 messages in the queue
identity: system # uses Managed Identity
With this rule: 0 messages in queue → 0 replicas (zero cost). 100 messages → 10 replicas processing them in parallel. The queue empties → scales back to 0. Exactly the kind of elasticity that App Service does not offer natively.
Note identity: system: KEDA authenticates to Service Bus via Managed Identity, without a connection string — consistent with the security principles in the series.
What’s next
In the third part we will see how to enable Dapr in Container Apps for communication between services: service invocation, pub/sub, and state management — without writing infrastructure code.
Questions? Write to me at contact@ludoprogramming.com.