Theoretical Foundation: Provision a container by using Azure Container Apps
1. Initial Intuitionβ
In previous modules, we saw two extremes of the container spectrum in Azure: ACI (simple execution, no management, ideal for one-off tasks) and AKS (complete orchestration with Kubernetes, ideal for complex production architectures). Azure Container Apps (ACA) occupies the space between them.
Think of it this way: ACI is like renting a hotel room for one night. AKS is like managing a complete condominium, you have full control but need to take care of everything. Azure Container Apps is like living in a co-living service: you have your independent space, don't need to worry about the building infrastructure, can automatically receive more guests when needed, and only pay for what you use.
ACA is built on top of Kubernetes and KEDA (Kubernetes Event-Driven Autoscaling) internally, but you never need to interact with these systems directly. Azure manages the entire underlying Kubernetes cluster. You simply define: "my application is this container, it should scale from 0 to 10 replicas based on the number of HTTP requests per second".
2. Contextβ
ACA was launched by Microsoft as a response to the growth of microservices and event-driven architectures that need sophisticated automatic scaling (including scale-to-zero) without the operational complexity of Kubernetes.
ACA is built on open source technologies: Kubernetes for orchestration, KEDA for event-based autoscaling, Dapr for microservice communication, and Envoy as a service proxy. You benefit from all of this without needing to configure anything manually.
3. Concept Constructionβ
3.1 Resource Hierarchy: Environment, App, Revisionβ
Container Apps Environment: the logical container that groups multiple Container Apps. All apps within an environment share the same VNet, the same Log Analytics workspace, and can communicate with each other via internal DNS. An environment corresponds to a Kubernetes cluster managed by Azure.
Container App: the application itself. Defines the container image, environment variables, scaling, ingress and other configurations. A Container App can have multiple Revisions active simultaneously.
Revision: an immutable version of a Container App. Each time you update a configuration that affects the container (image, environment variables, CPU/memory), a new revision is created. You can distribute traffic between revisions for canary deployments or blue/green.
Replica: a running instance of a revision. The autoscaler increases or decreases the number of replicas based on configured rules.
3.2 Revision Modesβ
Revision behavior depends on the Revision Mode:
| Mode | Behavior | Use Case |
|---|---|---|
| Single (default) | Only one active revision at a time. New revision automatically replaces the old one. | Simple applications without A/B testing needs |
| Multiple | Multiple active revisions simultaneously. Traffic can be split between them. | Canary deployments, A/B testing, blue/green |
3.3 Ingress: Container App Accessβ
Ingress controls how the Container App is accessible:
| Type | Description | Generated URL |
|---|---|---|
| External (enabled) | Accessible from internet via HTTPS | https://my-api.<hash>.brazilsouth.azurecontainerapps.io |
| Internal (enabled) | Accessible only within the environment | https://my-api (short name) |
| Disabled | No network access (jobs, processors) | No URL |
ACA automatically manages:
- TLS certificate (HTTPS enabled by default for external ingress)
- Load balancing between replicas
- Custom domains (with your own certificate or free managed certificate)
3.4 Autoscaling with KEDAβ
ACA autoscaling is based on scalers (scaling triggers). The most important for AZ-104:
| Scaler Type | What it monitors | Example |
|---|---|---|
| HTTP | Concurrent HTTP requests | Scale when > 10 concurrent req per replica |
| CPU | CPU usage | Scale when CPU > 70% |
| Memory | Memory usage | Scale when memory > 80% |
| Azure Service Bus | Messages in queue | 1 replica per message in queue |
| Azure Storage Queue | Messages in queue | 1 replica per 5 messages |
| Custom | Any metric via KEDA | Any source supported by KEDA |
Scale-to-zero: when there's no traffic or messages in queue, the number of replicas can reach zero. The container doesn't consume CPU or memory, and you don't pay for compute. When a new request arrives, ACA automatically starts a replica (with cold start latency, usually seconds).
The scale range is defined by minReplicas and maxReplicas:
scale:
minReplicas: 0 # scale-to-zero active
maxReplicas: 10 # maximum of 10 replicas
rules:
- name: http-rule
http:
metadata:
concurrentRequests: "10" # 1 replica for every 10 concurrent req
3.5 Jobs vs. Appsβ
Besides Container Apps (long-running services), ACA supports Container Apps Jobs: executions that terminate after completing a task, similar to ACI with restartPolicy: Never.
| Type | Duration | Trigger | Billing |
|---|---|---|---|
| Container App | Continuous | Traffic / messages | Per vCPU/s and GB/s used |
| Container App Job | Finite (terminates) | Manual, Scheduled (cron), Event-driven | Per execution |
4. Structural Viewβ
5. Practical Operationβ
Deployment Lifecycleβ
Important and Non-Obvious Behaviorsβ
Revision names are auto-generated: each new revision receives a name in the format <app-name>--<random-suffix>. You can specify a custom suffix: --revision-suffix v2-release.
Scale-to-zero has cold start: when there are 0 replicas and a request arrives, ACA needs to start a new replica before responding. This takes seconds. For latency-sensitive applications, configure minReplicas: 1 to always have at least one replica ready, at the cost of continuous billing.
Dapr is optional but integrated: ACA has native integration with Dapr (Distributed Application Runtime) for microservice patterns like pub/sub, service invocation and state management. You enable Dapr per app with a flag, without manual installation.
Environments have network isolation: each environment can be created in a VNet. Apps within the same environment communicate via internal DNS without going to the internet. Apps in different environments need communication via public URL or VNet peering.
6. Implementation Methodsβ
6.1 Azure Portalβ
When to use: initial exploratory creation, traffic management between revisions, metrics visualization.
Path: Create a resource > Containers > Container App
The wizard automatically creates a Container Apps Environment if one doesn't exist, or allows selecting an existing one.
6.2 Azure CLIβ
Create Container Apps Environment:
# Create Log Analytics workspace (required for environment)
az monitor log-analytics workspace create \
--resource-group rg-containers \
--workspace-name law-aca \
--location brazilsouth
LAW_ID=$(az monitor log-analytics workspace show \
--resource-group rg-containers \
--workspace-name law-aca \
--query customerId -o tsv)
LAW_KEY=$(az monitor log-analytics workspace get-shared-keys \
--resource-group rg-containers \
--workspace-name law-aca \
--query primarySharedKey -o tsv)
# Create the environment
az containerapp env create \
--name env-production \
--resource-group rg-containers \
--location brazilsouth \
--logs-workspace-id $LAW_ID \
--logs-workspace-key $LAW_KEY
Create basic Container App:
az containerapp create \
--name my-api \
--resource-group rg-containers \
--environment env-production \
--image myacr.azurecr.io/api:v1.0 \
--cpu 0.5 \
--memory 1.0Gi \
--min-replicas 1 \
--max-replicas 5 \
--target-port 8080 \
--ingress external \
--registry-server myacr.azurecr.io \
--registry-identity system \
--env-vars ENVIRONMENT=production DB_HOST=my-db.postgres.database.azure.com
Update to new image version:
az containerapp update \
--name my-api \
--resource-group rg-containers \
--image myacr.azurecr.io/api:v2.0 \
--revision-suffix v2-release
Manage traffic between revisions (Multiple mode):
# First, enable Multiple mode
az containerapp revision set-mode \
--name my-api \
--resource-group rg-containers \
--mode multiple
# List available revisions
az containerapp revision list \
--name my-api \
--resource-group rg-containers \
--output table
# Split traffic: 90% v1, 10% v2
az containerapp ingress traffic set \
--name my-api \
--resource-group rg-containers \
--revision-weight \
my-api--v1-release=90 \
my-api--v2-release=10
# After validation: 100% to v2
az containerapp ingress traffic set \
--name my-api \
--resource-group rg-containers \
--revision-weight my-api--v2-release=100
Configure HTTP-based autoscaling:
az containerapp update \
--name my-api \
--resource-group rg-containers \
--min-replicas 0 \
--max-replicas 10 \
--scale-rule-name http-scaler \
--scale-rule-type http \
--scale-rule-http-concurrency 20
Configure Azure Service Bus-based autoscaling:
az containerapp update \
--name processor \
--resource-group rg-containers \
--min-replicas 0 \
--max-replicas 15 \
--scale-rule-name sb-scaler \
--scale-rule-type azure-servicebus \
--scale-rule-metadata \
queueName=processing-queue \
namespace=my-servicebus \
messageCount=5 \
--scale-rule-auth \
connectionString=secretRef:sb-connection-string
Use secrets for credentials:
# Create secret
az containerapp secret set \
--name my-api \
--resource-group rg-containers \
--secrets db-password=MySecurePassword123
# Reference secret as environment variable
az containerapp update \
--name my-api \
--resource-group rg-containers \
--set-env-vars "DB_PASSWORD=secretref:db-password"
Create Container App Job (scheduled single execution):
az containerapp job create \
--name job-daily-report \
--resource-group rg-containers \
--environment env-production \
--trigger-type Schedule \
--cron-expression "0 8 * * *" \
--image myacr.azurecr.io/report:latest \
--cpu 1 \
--memory 2.0Gi
6.3 Bicepβ
resource environment 'Microsoft.App/managedEnvironments@2023-05-01' = {
name: 'env-production'
location: location
properties: {
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalyticsWorkspaceId
sharedKey: logAnalyticsSharedKey
}
}
}
}
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
name: 'my-api'
location: location
identity: {
type: 'SystemAssigned'
}
properties: {
managedEnvironmentId: environment.id
configuration: {
ingress: {
external: true
targetPort: 8080
transport: 'http'
}
registries: [
{
server: 'myacr.azurecr.io'
identity: 'system'
}
]
secrets: [
{
name: 'db-password'
value: dbPassword
}
]
}
template: {
containers: [
{
name: 'api-container'
image: 'myacr.azurecr.io/api:v1.0'
resources: {
cpu: json('0.5')
memory: '1.0Gi'
}
env: [
{
name: 'ENVIRONMENT'
value: 'production'
}
{
name: 'DB_PASSWORD'
secretRef: 'db-password'
}
]
}
]
scale: {
minReplicas: 1
maxReplicas: 10
rules: [
{
name: 'http-scaler'
http: {
metadata: {
concurrentRequests: '20'
}
}
}
]
}
}
}
}
7. Control and Securityβ
ACR Authentication via Managed Identityβ
ACA supports System-assigned and User-assigned Managed Identities. For pulling images from ACR, use the system identity:
# Create app with system-assigned identity
az containerapp create \
--name my-api \
--registry-server myacr.azurecr.io \
--registry-identity system \
...
# Assign AcrPull to the identity (needed after creating the app)
APP_IDENTITY=$(az containerapp show \
--name my-api \
--resource-group rg-containers \
--query identity.principalId -o tsv)
ACR_ID=$(az acr show --name myacr --query id -o tsv)
az role assignment create \
--assignee $APP_IDENTITY \
--role AcrPull \
--scope $ACR_ID
Built-in Authentication (Easy Auth)β
ACA has integration with Easy Auth (codeless authentication): you can protect a Container App by requiring users to authenticate with Entra ID, GitHub, Google or other providers before accessing the application, without any changes to the container code.
az containerapp auth microsoft update \
--name my-api \
--resource-group rg-containers \
--client-id <entra-app-client-id> \
--client-secret <client-secret> \
--issuer https://login.microsoftonline.com/<tenant-id>/v2.0 \
--yes
Network Isolationβ
Environments can be created in a VNet for network isolation. Two types:
| Type | External access | Use cases |
|---|---|---|
| External Environment | Public IP for external ingress | Applications accessible from internet |
| Internal Environment | Only via VNet (no public IP) | Internal applications, internal service communication |
8. Decision Makingβ
ACA vs. other container servicesβ
| Situation | Best choice | Reason |
|---|---|---|
| REST API with variable traffic, scale-to-zero | ACA | Native scale-to-zero, pay-per-use billing |
| Microservices with event-driven communication | ACA with KEDA | Queue/topic-based autoscaling |
| Easy canary deployment or A/B testing | ACA (Revision traffic) | Native traffic splitting between revisions |
| Scheduled batch job (cron) | ACA Job or ACI | ACA Job better integrated; ACI simpler |
| Granular Kubernetes control (custom controllers, CRDs) | AKS | ACA doesn't expose Kubernetes directly |
| Web application with staging slots | App Service | More mature deployment slots |
| Simple and fast container without scaling | ACI | Lower overhead, simpler |
When to configure minReplicas: 0 vs. 1?β
| Situation | minReplicas | Reason |
|---|---|---|
| API with critical latency | 1 | Avoids cold start; always has replica ready |
| Job/processor with eventual execution | 0 | Zero cost when idle |
| Development environment | 0 | Minimal cost, acceptable cold start |
| Production with latency SLA | 1 or more | Guarantee of immediate availability |
9. Best Practicesβ
Use Revisions for safe deployments: instead of updating directly to 100% traffic, enable Multiple mode and perform canary deployments: 5% traffic to the new version, monitor for a few minutes, expand to 50%, then 100%. If something goes wrong, redirect 100% back to the previous version immediately.
Centralize secrets and never put them in common environment variables: use secretref for all sensitive information. Even better, use Managed Identity + Key Vault to fetch secrets at runtime, completely eliminating the need to pass secrets to the Container App.
Configure health probes to accelerate autoscaling: define startupProbe, readinessProbe and livenessProbe so that ACA knows exactly when a new replica is ready to receive traffic during scale-out and when a replica has problems during normal operation.
Choose the appropriate environment size: environments have workload profiles. The Consumption profile is pay-per-use and ideal for most cases. Dedicated profiles (D4, D8, D16, etc.) offer reserved capacity for workloads with high and predictable CPU/memory requirements.
10. Common Errorsβ
Forgetting to assign AcrPull after creating the app with system identity
The container app is created with --registry-identity system, but image pull fails. The cause is that the Managed Identity needs time to be provisioned and the AcrPull assignment needs to be done explicitly after creation. The app stays in an error state with an image not found message.
Using Single mode and losing rollback revisions
In Single mode (default), when a new revision is created, the old one is automatically deactivated. If the new version has a bug, there's no active old revision to redirect traffic to immediately. Switch to Multiple mode in production to keep the previous revision active until confirming the new one works.
Configuring scale-to-zero without understanding cold start impact
A production API with a 200ms latency SLA is configured with minReplicas: 0. When there's no traffic for a few minutes and a new request arrives, the cold start can take 5-15 seconds, violating the SLA. For latency-sensitive APIs, use minReplicas: 1.
Confusing revision with deployment
A new revision is created whenever the container configuration changes (image, variables, CPU/memory). Changes to ingress, scaling, or secrets don't create a new revision. Many administrators expect to create a new revision when changing a scaling rule, which doesn't happen.
Storing state in container filesystem expecting persistence
The container filesystem is ephemeral in ACA (as in any container). Data written to /tmp or any local directory is lost when the replica is restarted or replaced by another. Use Azure Storage, Azure SQL, or other external services for persistent state.
11. Operation and Maintenanceβ
Check Status and Logsβ
# Container App and revisions status
az containerapp show \
--name minha-api \
--resource-group rg-containers \
--query "{Estado:provisioningState, URL:properties.configuration.ingress.fqdn, Replicas:properties.template.scale}" \
--output table
# Real-time logs (streaming)
az containerapp logs show \
--name minha-api \
--resource-group rg-containers \
--follow
# Logs from a specific revision
az containerapp logs show \
--name minha-api \
--resource-group rg-containers \
--revision minha-api--v2-release
# List revisions and traffic distribution
az containerapp revision list \
--name minha-api \
--resource-group rg-containers \
--query "[].{Nome:name, Ativa:properties.active, Trafego:properties.trafficWeight, Replicas:properties.replicas}" \
--output table
Monitoring with Log Analyticsβ
With the environment integrated to Log Analytics, KQL queries are the main monitoring tool:
// Container logs
ContainerAppConsoleLogs_CL
| where ContainerAppName_s == "minha-api"
| where TimeGenerated > ago(1h)
| project TimeGenerated, Log_s, RevisionName_s
| order by TimeGenerated desc
// Request count by revision
ContainerAppSystemLogs_CL
| where Reason_s == "Scaling"
| where TimeGenerated > ago(24h)
| summarize count() by RevisionName_s, bin(TimeGenerated, 5m)
| render timechart
Important Limitsβ
| Item | Limit |
|---|---|
| Container Apps per environment | 20 (default, adjustable) |
| Revisions per Container App | 100 (older ones are automatically cleaned) |
| CPU per replica | 2 vCPU (Consumption profile) |
| Memory per replica | 4 Gi (Consumption profile) |
| Replicas per revision | 300 |
| Ingress: maximum request size | 128 KB (headers) |
12. Integration and Automationβ
Event-Driven Pattern with Service Busβ
The most powerful use case of ACA is processing queue messages with automatic scaling:
GitHub Actions for Continuous Deployβ
# .github/workflows/deploy.yml
name: Deploy to Azure Container Apps
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Build and push to ACR
run: |
az acr build \
--registry minhacr \
--image api:${{ github.sha }} \
.
- name: Deploy to Container Apps
run: |
az containerapp update \
--name minha-api \
--resource-group rg-containers \
--image minhacr.azurecr.io/api:${{ github.sha }} \
--revision-suffix ${{ github.run_number }}
13. Final Summaryβ
Essential points:
- ACA is a PaaS for containers with automatic management of underlying Kubernetes, KEDA, and Envoy. You don't manage the cluster.
- The hierarchy is: Environment (shared cluster) > Container App (application) > Revision (immutable version) > Replica (running instance).
- Scale-to-zero is native: with
minReplicas: 0, replicas are removed when there's no traffic and created automatically when demand arrives. - Revisions allow native canary deployments and A/B testing with percentage traffic splitting.
Critical differences:
- ACA vs. ACI: ACA has automatic scaling, revisions, traffic splitting and lifecycle management; ACI is simple execution without these functionalities.
- ACA vs. AKS: ACA completely abstracts Kubernetes; AKS gives direct access to the cluster. ACA is simpler; AKS is more flexible and controllable.
- Revision Mode Single vs. Multiple: Single replaces revisions automatically (simpler); Multiple keeps revisions active in parallel (necessary for canary/blue-green).
- minReplicas 0 vs. 1: zero has cold start but no cost when idle; 1 eliminates cold start with cost of at least one replica always active.
What needs to be remembered:
- A new revision is created when the container configuration changes (image, env vars, resources), not when scaling or ingress rules change.
- For ACR image pulls: use
--registry-identity systemand assignAcrPullto the Managed Identity created after deployment. - Environments are regional and have associated VNet. Apps within the same environment communicate via internal DNS.
- For event-driven workloads with Service Bus or Storage Queue, ACA with KEDA is the most efficient approach in the Azure ecosystem.
- The
az containerapp logs show --followcommand is the equivalent ofkubectl logs -fto follow logs in real time.