Azure Container Apps em produção: serverless containers com escala automática, Dapr e observabilidade - Parte 1
Introdução
Se você acompanha esta série sobre infraestrutura Azure, já vimos como operar AKS em cenários multi-cluster para IA, como implementar observabilidade avançada para GPU e como orquestrar networking avançado entre clusters. Mas nem todo workload precisa da complexidade do Kubernetes. Muitas equipes querem rodar containers em produção — com escala automática, integração com filas e eventos, e observabilidade nativa — sem gerenciar clusters, node pools ou upgrades de control plane.
É exatamente aqui que o Azure Container Apps (ACA) se posiciona: uma plataforma serverless para containers que abstrai toda a infraestrutura subjacente (que, por baixo dos panos, roda sobre Kubernetes e KEDA), oferecendo escala automática de zero a milhares de réplicas, integração nativa com Dapr para microserviços, Jobs para processamento batch, e um modelo de billing por segundo de consumo.
Neste artigo, vamos explorar Container Apps em profundidade: desde os conceitos fundamentais até arquiteturas avançadas de microserviços em produção, passando por implementação prática com CLI e Bicep, escala com KEDA, integração com Dapr, Jobs para batch processing, networking com VNet, observabilidade com Azure Monitor e os erros mais comuns em ambientes reais.
Quando usar Container Apps (e quando não usar)
Antes de mergulhar na implementação, é fundamental entender o posicionamento do Azure Container Apps no ecossistema de compute do Azure. A escolha errada de plataforma gera atrito operacional que acompanha o projeto por toda sua vida útil.
Tabela comparativa: Container Apps vs AKS vs App Service vs Functions
| Critério | Container Apps | AKS | App Service | Azure Functions |
|---|---|---|---|---|
| Gerenciamento de infra | Zero (serverless) | Você gerencia nodes/clusters | Gerenciado (com limitações) | Zero (serverless) |
| Escala para zero | ✅ Sim | ❌ Mínimo 1 node | ❌ Mínimo 1 instância (exceto Linux) | ✅ Sim (Consumption) |
| Containers customizados | ✅ Qualquer imagem | ✅ Qualquer imagem | ⚠️ Limitado | ⚠️ Custom handlers |
| Orquestração de microserviços | ✅ Dapr nativo | ✅ Total controle | ❌ Não é o foco | ❌ Não é o foco |
| Jobs/Batch | ✅ ACA Jobs | ✅ CronJobs/Jobs | ✅ WebJobs | ✅ Timer triggers |
| Networking avançado | ✅ VNet + Private Endpoints | ✅ Total controle | ✅ VNet integration | ✅ VNet integration |
| GPU | ✅ Serverless e dedicado | ✅ GPU node pools | ❌ | ❌ |
| Custo em idle | $0 (escala a zero) | $$ (nodes sempre rodando) | $$ (plano always-on) | $0 (Consumption) |
| Complexidade operacional | Baixa | Alta | Baixa | Muito baixa |
Use Container Apps quando:
- Você tem microserviços em containers e quer evitar a complexidade do Kubernetes
- Precisa de escala automática baseada em eventos (filas, tópicos, HTTP)
- Quer escala para zero com billing por segundo
- Usa ou quer usar Dapr para comunicação entre serviços, pub/sub e state management
- Tem workloads de batch/ETL que rodam periodicamente ou sob demanda
- Precisa de GPU serverless para inferência de IA sem gerenciar VMs
Não use Container Apps quando:
- Precisa de controle total sobre o cluster Kubernetes (custom operators, CRDs complexos, service mesh customizado)
- Tem workloads que exigem acesso direto ao kernel ou configurações de rede de baixo nível
- Precisa de volumes persistentes complexos (StatefulSets com storage dinâmico)
- Já tem um time maduro de platform engineering com AKS bem operado
Conceitos fundamentais
Para trabalhar com Container Apps de forma eficiente, você precisa dominar quatro conceitos-chave que formam a hierarquia da plataforma.
Environment (Ambiente)
O Container Apps Environment é o boundary de isolamento lógico. Todos os apps dentro de um environment compartilham a mesma rede virtual, configuração de logging e região. Pense nele como o equivalente a um namespace com rede compartilhada.

Tipos de planos:
| Plano | Características | Ideal para |
|---|---|---|
| Consumption | Serverless puro, escala a zero, billing por segundo | Maioria dos workloads, dev/test, APIs com tráfego variável |
| Dedicated | Compute reservado, GPUs, workloads que não toleram cold start | Inferência de IA, workloads latency-sensitive, compliance |
Container App
A Container App é a unidade de deploy. Cada app pode ter um ou mais containers (sidecar pattern), configurações de ingress (HTTP ou TCP), secrets, variáveis de ambiente e regras de escala.
Revision
Uma Revision é um snapshot imutável do app em um ponto no tempo. Cada vez que você atualiza a configuração ou a imagem do container, uma nova revision é criada. Você pode fazer traffic splitting entre revisions para blue-green deployments ou canary releases.
Replica
Uma Replica é uma instância individual do container rodando. A escala automática controla quantas réplicas existem (de 0 a N) baseado nas regras configuradas.
Implementação prática
Vamos construir uma aplicação completa com uma API REST e um worker que processa mensagens de uma fila — o cenário clássico de microserviços em produção.
Pré-requisitos
# Instalar/atualizar a extensão de Container Apps
az extension add --name containerapp --upgrade
# Registrar os providers necessários
az provider register --namespace Microsoft.App
az provider register --namespace Microsoft.OperationalInsights
Criando o Environment
# Variáveis
RESOURCE_GROUP="rg-containerapps-prod"
LOCATION="brazilsouth"
ENVIRONMENT="env-producao"
# Criar resource group
az group create \
--name $RESOURCE_GROUP \
--location $LOCATION
# Criar o environment com Log Analytics
az containerapp env create \
--name $ENVIRONMENT \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--logs-destination log-analytics
Deploy de uma API REST
# Deploy da API com ingress externo
az containerapp create \
--name api-pedidos \
--resource-group $RESOURCE_GROUP \
--environment $ENVIRONMENT \
--image mcr.microsoft.com/k8se/quickstart:latest \
--target-port 80 \
--ingress external \
--min-replicas 1 \
--max-replicas 10 \
--cpu 0.5 \
--memory 1.0Gi \
--env-vars "APP_ENV=production" "LOG_LEVEL=info"
O parâmetro --ingress external expõe o app com um FQDN público. Use --ingress internal para apps que só devem ser acessíveis dentro do environment.
Deploy com imagem de um Azure Container Registry (ACR) privado
Na prática, você vai usar imagens do seu próprio registry. A abordagem recomendada para produção é user-assigned managed identity com role AcrPull:
ACR_NAME="acrprodcontainers"
# 1. Criar user-assigned managed identity
az identity create \
--name id-containerapps-acr \
--resource-group $RESOURCE_GROUP
IDENTITY_ID=$(az identity show \
--name id-containerapps-acr \
--resource-group $RESOURCE_GROUP \
--query id -o tsv)
IDENTITY_CLIENT_ID=$(az identity show \
--name id-containerapps-acr \
--resource-group $RESOURCE_GROUP \
--query clientId -o tsv)
# 2. Atribuir role AcrPull no ACR
ACR_ID=$(az acr show --name $ACR_NAME --query id -o tsv)
az role assignment create \
--assignee $IDENTITY_CLIENT_ID \
--role AcrPull \
--scope $ACR_ID
# 3. Deploy usando user-assigned identity (recomendado para produção)
az containerapp create \
--name api-pedidos \
--resource-group $RESOURCE_GROUP \
--environment $ENVIRONMENT \
--image "$ACR_NAME.azurecr.io/api-pedidos:v1.2.0" \
--registry-server "$ACR_NAME.azurecr.io" \
--registry-identity "$IDENTITY_ID" \
--user-assigned "$IDENTITY_ID" \
--target-port 8080 \
--ingress external \
--min-replicas 1 \
--max-replicas 20 \
--cpu 1.0 \
--memory 2.0Gi
Nota: system-assigned identity também funciona, mas exige um fluxo em duas etapas (criar app → atribuir role → atualizar imagem). User-assigned é o caminho mais limpo para produção.
Configurando secrets e variáveis de ambiente
# Criar secrets (valores sensíveis)
az containerapp secret set \
--name api-pedidos \
--resource-group $RESOURCE_GROUP \
--secrets "db-connection=Server=tcp:sql-prod.database.windows.net;Database=pedidos;Authentication=Active Directory Managed Identity;" \
"storage-key=<sua-key>"
# Referenciar secrets como variáveis de ambiente
az containerapp update \
--name api-pedidos \
--resource-group $RESOURCE_GROUP \
--set-env-vars "DATABASE_URL=secretref:db-connection" \
"STORAGE_KEY=secretref:storage-key" \
"APP_VERSION=v1.2.0"
Infrastructure as Code com Bicep
Para produção, defina tudo via IaC. Aqui um template Bicep completo:
@description('Nome do environment')
param environmentName string = 'env-producao'
@description('Localização')
param location string = resourceGroup().location
@description('Nome do Log Analytics workspace')
param logAnalyticsName string = 'law-containerapps'
// Log Analytics Workspace
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
name: logAnalyticsName
location: location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 30
}
}
// Container Apps Environment
resource environment 'Microsoft.App/managedEnvironments@2024-03-01' = {
name: environmentName
location: location
properties: {
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalytics.properties.customerId
sharedKey: logAnalytics.listKeys().primarySharedKey
}
}
zoneRedundant: true
}
}
// Container App - API de Pedidos
resource apiPedidos 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api-pedidos'
location: location
properties: {
environmentId: environment.id
configuration: {
ingress: {
external: true
targetPort: 8080
transport: 'http'
traffic: [
{
latestRevision: true
weight: 100
}
]
}
secrets: [
{
name: 'db-connection'
value: 'Server=tcp:sql-prod.database.windows.net;...'
}
]
}
template: {
containers: [
{
name: 'api'
image: 'acrprod.azurecr.io/api-pedidos:v1.2.0'
resources: {
cpu: json('1.0')
memory: '2Gi'
}
env: [
{
name: 'DATABASE_URL'
secretRef: 'db-connection'
}
{
name: 'APP_ENV'
value: 'production'
}
]
probes: [
{
type: 'Startup'
httpGet: {
path: '/healthz'
port: 8080
}
failureThreshold: 30
periodSeconds: 2
}
{
type: 'Liveness'
httpGet: {
path: '/healthz'
port: 8080
}
periodSeconds: 10
}
{
type: 'Readiness'
httpGet: {
path: '/ready'
port: 8080
}
initialDelaySeconds: 5
}
]
}
]
scale: {
minReplicas: 1
maxReplicas: 20
rules: [
{
name: 'http-scaling'
http: {
metadata: {
concurrentRequests: '50'
}
}
}
]
}
}
}
}
output apiUrl string = 'https://${apiPedidos.properties.configuration.ingress.fqdn}'
Escala automática: HTTP, KEDA e eventos
A escala automática é o coração do Container Apps. A plataforma usa KEDA (Kubernetes Event Driven Autoscaling) internamente, mas você não precisa instalar nem configurar nada — basta declarar as regras.
Escala por HTTP (padrão)
Para apps com ingress HTTP, a escala é baseada em requisições concorrentes:
az containerapp update \
--name api-pedidos \
--resource-group $RESOURCE_GROUP \
--min-replicas 1 \
--max-replicas 30 \
--scale-rule-name http-rule \
--scale-rule-type http \
--scale-rule-http-concurrency 50
Isso significa: cada réplica aguenta 50 requisições concorrentes. Se houver 200 requisições simultâneas, o ACA escala para 4 réplicas. Se o tráfego cair para zero e min-replicas for 0, escala até zero réplicas.
Escala por Azure Service Bus Queue
Este é o cenário mais poderoso para arquiteturas event-driven:
az containerapp create \
--name worker-processamento \
--resource-group $RESOURCE_GROUP \
--environment $ENVIRONMENT \
--image acrprod.azurecr.io/worker:v1.0.0 \
--min-replicas 0 \
--max-replicas 50 \
--scale-rule-name queue-rule \
--scale-rule-type azure-servicebus \
--scale-rule-metadata \
"queueName=pedidos-processar" \
"namespace=sb-prod-pedidos" \
"messageCount=5" \
--scale-rule-auth \
"connection=sb-connection-string" \
--secrets "sb-connection-string=Endpoint=sb://sb-prod-pedidos.servicebus.windows.net/;SharedAccessKeyName=worker;SharedAccessKey=..."
Com messageCount=5, cada 5 mensagens na fila dispara uma nova réplica. Se a fila está vazia, o worker escala a zero — custo zero quando não há trabalho.
Escala por Azure Storage Queue
az containerapp update \
--name worker-imagens \
--resource-group $RESOURCE_GROUP \
--scale-rule-name queue-scale \
--scale-rule-type azure-queue \
--scale-rule-metadata \
"queueName=imagens-processar" \
"queueLength=10" \
"accountName=stprodimagens" \
--scale-rule-auth \
"connection=storage-connection" \
--secrets "storage-connection=DefaultEndpointsProtocol=https;AccountName=stprodimagens;..."
Tabela de scalers KEDA disponíveis
| Scaler | Trigger | Caso de uso |
|---|---|---|
| HTTP | Requisições concorrentes | APIs, web apps |
| Azure Service Bus | Mensagens na fila/tópico | Event-driven processing |
| Azure Storage Queue | Mensagens na fila | Processamento assíncrono |
| Azure Event Hubs | Eventos no hub | Streaming, IoT |
| Azure Cosmos DB | Change feed | Reação a mudanças de dados |
| PostgreSQL / MySQL | Queries customizadas | Batch baseado em dados |
| Cron | Horário agendado | Scale preventivo |
| Custom | Métricas customizadas | Qualquer fonte via KEDA |
Integração com Dapr: microserviços simplificados
O Dapr (Distributed Application Runtime) é integrado nativamente ao Container Apps. Isso significa que você ganha service-to-service invocation, pub/sub, state management e bindings sem instalar nada — basta habilitar o sidecar.
Habilitando Dapr
az containerapp dapr enable \
--name api-pedidos \
--resource-group $RESOURCE_GROUP \
--dapr-app-id api-pedidos \
--dapr-app-port 8080 \
--dapr-app-protocol http
Service Invocation (chamadas entre serviços)
Com Dapr, um serviço chama outro usando o app-id, sem precisar saber o endereço ou gerenciar service discovery:
# api-pedidos chamando o serviço "servico-estoque"
import requests
def verificar_estoque(produto_id: str) -> dict:
# Dapr resolve o endereço automaticamente via sidecar
dapr_url = f"http://localhost:3500/v1.0/invoke/servico-estoque/method/estoque/{produto_id}"
response = requests.get(dapr_url)
return response.json()
// Em C# usando o SDK do Dapr
using Dapr.Client;
var client = new DaprClientBuilder().Build();
var estoque = await client.InvokeMethodAsync<EstoqueResponse>(
HttpMethod.Get,
"servico-estoque",
$"estoque/{produtoId}");
Pub/Sub (publicação e assinatura de eventos)
Configure um componente de pub/sub usando Azure Service Bus:
# pubsub-servicebus.yaml (aplicado via CLI ou Bicep)
componentType: pubsub.azure.servicebus.topics
version: v1
metadata:
- name: connectionString
secretRef: sb-connection
- name: maxConcurrentHandlers
value: "10"
scopes:
- api-pedidos
- worker-notificacoes
Publicando eventos:
import requests
import json
def publicar_pedido_criado(pedido: dict):
dapr_url = "http://localhost:3500/v1.0/publish/pubsub-servicebus/pedido-criado"
requests.post(dapr_url, json=pedido)
Assinando eventos:
from flask import Flask, request
app = Flask(__name__)
# Dapr chama essa rota quando chega uma mensagem no tópico
@app.route('/pedido-criado', methods=['POST'])
def processar_pedido():
evento = request.json
pedido = evento['data']
# Processar pedido...
enviar_email_confirmacao(pedido)
return '', 200
# Declarar as subscriptions para o Dapr
@app.route('/dapr/subscribe', methods=['GET'])
def subscribe():
return [
{
'pubsubname': 'pubsub-servicebus',
'topic': 'pedido-criado',
'route': '/pedido-criado'
}
], 200
State Management
Dapr oferece state store plugável. Com Container Apps, você pode usar Azure Cosmos DB, Azure Table Storage ou Redis:
import requests
DAPR_URL = "http://localhost:3500/v1.0/state/statestore"
# Salvar estado
def salvar_carrinho(usuario_id: str, itens: list):
state = [{"key": f"carrinho-{usuario_id}", "value": {"itens": itens}}]
requests.post(DAPR_URL, json=state)
# Ler estado
def obter_carrinho(usuario_id: str) -> dict:
resp = requests.get(f"{DAPR_URL}/carrinho-{usuario_id}")
return resp.json()
Conclusão da Parte 1
Neste artigo, exploramos os fundamentos do Azure Container Apps — desde o posicionamento no ecossistema Azure até a implementação prática com CLI e Bicep, passando pela escala automática com KEDA e a integração nativa com Dapr para comunicação entre microserviços, pub/sub e state management. Esses pilares formam a base para rodar containers em produção com custo otimizado e complexidade operacional mínima.
Na Parte 2, exploraremos Container Apps Jobs, networking avançado com VNet, observabilidade com KQL e OpenTelemetry, blue-green deployments e troubleshooting.