Production Readiness
You've deployed a LangGraph agent to Azure Container Apps. It works. Now you need it to work reliably under load, survive failures, stay secure, and cost something you can explain to finance.
This guide codifies our team's production standards for Container Apps. Every recommendation includes a concrete configuration, a rationale, and a tradeoff. Treat this as the authoritative reference when moving agents from "it runs" to "it runs in production."
What "Production-Ready" Meansβ
A production-ready Container Apps deployment satisfies five properties:
| Property | What It Looks Like |
|---|---|
| Resilient | Health probes catch failures before users do. Revisions enable instant rollback. |
| Secure | No hardcoded secrets. Managed identity for every Azure service. Key Vault for everything else. |
| Observable | Structured logs, distributed traces, and dashboards that answer "why is it slow?" in under 60 seconds. |
| Scalable | Auto-scaling matches demand without over-provisioning. Costs are predictable. |
| Recoverable | Cross-region redundancy, tested failover, and documented runbooks. |
Scaling Strategiesβ
Container Apps uses KEDA (Kubernetes Event-driven Autoscaling) for horizontal scaling. Getting scaling right is the single biggest lever for both performance and cost.
KEDA Auto-Scaling Configurationβ
Every revision has a scale definition with limits and rules. Limits bound the replica count; rules determine when replicas are added or removed.
- Azure CLI
- Bicep
az containerapp create \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--environment "cae-ai-agents" \
--image "aicaeng1234.azurecr.io/research-agent:1.0.0" \
--min-replicas 1 \
--max-replicas 10 \
--scale-rule-name "http-scale" \
--scale-rule-type http \
--scale-rule-http-concurrency 25 \
--cpu 2 \
--memory 4Gi
resource containerApp 'Microsoft.App/containerApps@2025-02-02-preview' = {
name: 'research-agent'
location: 'swedencentral'
properties: {
managedEnvironmentId: caeEnv.id
template: {
containers: [
{
name: 'agent'
image: 'aicaeng1234.azurecr.io/research-agent:1.0.0'
resources: {
cpu: 2
memory: '4Gi'
}
}
]
scale: {
minReplicas: 1
maxReplicas: 10
rules: [
{
name: 'http-scale'
http: {
metadata: {
concurrentRequests: '25'
}
}
}
]
}
}
}
}
Key Parametersβ
| Parameter | Recommended Default | Min | Max | Notes |
|---|---|---|---|---|
minReplicas | 1 | 0 | 1000 | Set to 1 for always-on agents to avoid cold starts. |
maxReplicas | 10 | 1 | 1000 | Cap conservatively. LangGraph agents are expensive per replica. |
concurrentRequests | 25 | 1 | β | KEDA evaluates concurrent requests every 15 seconds. |
concurrentRequestsKEDA calculates concurrent requests as (requests in the last 15 seconds) / 15. A value of 25 means a new replica is spawned when the rolling 15-second average exceeds 25 requests. For LangGraph agents that take 5-15 seconds per invocation, this means each replica handles roughly 2-5 concurrent requests before scaling out.
CPU and Memory Limits for LangGraph Agentsβ
LangGraph agents are memory-intensive due to LLM context windows, checkpointing, and tool execution. Right-sizing prevents OOM kills and wasted spend.
| Agent Type | CPU | Memory | Rationale |
|---|---|---|---|
| Simple Q&A (single tool, small context) | 1 vCPU | 2Gi | Light LLM calls, minimal tool use. |
| Standard RAG (search + synthesis) | 2 vCPU | 4Gi | Hybrid search, vector embeddings, multi-step reasoning. |
| Complex orchestration (multi-agent, long context) | 4 vCPU | 8Gi | Multiple LLM calls, large checkpoints, tool chains. |
| Batch processing (queue-driven) | 2 vCPU | 4Gi | Processed sequentially; memory-bound, not CPU-bound. |
When a replica exceeds its memory limit, the container is killed with exit code 137. This is the most common cause of agent failures in production. Monitor for OOMKilled events in Log Analytics and increase memory before increasing replicas.
Concurrency Settingsβ
The concurrentRequests parameter controls how many in-flight requests a single replica handles before KEDA creates another. For LangGraph agents:
- Default KEDA behavior: Every 15 seconds, KEDA computes
concurrent_requests = requests_last_15s / 15. - Agent invocation time matters: If your agent takes 10 seconds to respond, a single replica can only handle ~1.5 requests in 15 seconds. Set
concurrentRequestslow (e.g.,5-10) to scale aggressively. - Fast agents: If your agent responds in < 2 seconds, you can set
concurrentRequestshigher (e.g.,50-100).
Tuning Formulaβ
concurrentRequests = (target_replicas * 15) / avg_agent_response_time
For a target of 3 replicas and an average response time of 8 seconds:
concurrentRequests = (3 * 15) / 8 β 6
Scale-to-Zero Considerationsβ
Container Apps can scale to zero replicas when minReplicas is 0. This saves money but introduces cold start latency.
| Scenario | minReplicas | Why |
|---|---|---|
| User-facing agent (low latency required) | 1 | Cold starts add 10-30 seconds. Users notice. |
| Internal tool (used intermittently) | 0 | Cost savings outweigh occasional cold starts. |
| Batch/nightly jobs | 0 | Only runs on schedule. Zero idle cost. |
| Always-on monitoring agent | 1 | Must be available for health checks. |
A cold start for a Python/LangGraph agent typically takes 10-30 seconds (dependency loading, model initialization, database connection pooling). If you set minReplicas to 0, the first request after scale-to-zero will experience this full latency.
Custom Scalers for Queue-Based Workloadsβ
For agents that process messages from Azure Service Bus, Azure Queue Storage, or Kafka, use custom KEDA scalers instead of HTTP scaling.
az containerapp create \
--name "batch-agent" \
--resource-group "rg-ai-engineering" \
--environment "cae-ai-agents" \
--image "aicaeng1234.azurecr.io/batch-agent:1.0.0" \
--min-replicas 0 \
--max-replicas 20 \
--scale-rule-name "servicebus-scale" \
--scale-rule-type custom \
--scale-rule-metadata "type=azure-servicebus" \
--scale-rule-metadata "queueName=agent-tasks" \
--scale-rule-metadata "messageCount=5" \
--scale-rule-auth "identity=system"
This configuration scales the agent based on queue depth. When more than 5 messages are pending, KEDA adds replicas up to the maximum. When the queue drains, replicas scale back to zero.
Cost Implicationsβ
| Strategy | Monthly Cost (est.) | Latency | Best For |
|---|---|---|---|
minReplicas: 0, maxReplicas: 5, HTTP scale | $15-30 | High (cold starts) | Intermittent internal tools |
minReplicas: 1, maxReplicas: 5, HTTP scale | $40-80 | Low | User-facing agents |
minReplicas: 2, maxReplicas: 10, HTTP scale | $80-200 | Very low | High-availability agents |
Queue-based, minReplicas: 0 | $10-25 | N/A (async) | Batch processing |
Set maxReplicas conservatively and monitor actual peak usage for one week before increasing. LangGraph agents are expensive β a single replica at 4 vCPU / 8Gi costs roughly $100-150/month when running 24/7.
Secrets Managementβ
Every credential, API key, and connection string must be managed through proper secrets infrastructure. Never hardcode secrets in Docker images, environment variables, or source code.
Using Azure Key Vault for Secretsβ
Azure Key Vault is our team's single source of truth for all secrets. Container Apps integrates natively with Key Vault through managed identity.
- Azure CLI
- Bicep
# 1. Create the secret in Key Vault
az keyvault secret set \
--vault-name "kv-ai-engineering" \
--name "langfuse-secret-key" \
--value "sk-lf-xxxxxxxxxxxx"
# 2. Reference the Key Vault secret in Container Apps
# (use the versionless URI for automatic rotation)
az containerapp secret set \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--secrets "langfuse-key=keyvaultref:https://kv-ai-engineering.vault.azure.net/secrets/langfuse-secret-key,identityref:system"
# 3. Expose as environment variable via secret reference
az containerapp appsettings set \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--settings "LANGFUSE_SECRET_KEY=secretref:langfuse-key"
param keyVaultSecretUri string
resource containerApp 'Microsoft.App/containerApps@2025-02-02-preview' = {
name: 'research-agent'
identity: {
type: 'SystemAssigned'
}
properties: {
configuration: {
secrets: [
{
name: 'langfuse-key'
keyVaultUrl: keyVaultSecretUri
identity: 'system'
}
]
}
template: {
containers: [
{
name: 'agent'
image: 'aicaeng1234.azurecr.io/research-agent:1.0.0'
env: [
{
name: 'LANGFUSE_SECRET_KEY'
secretRef: 'langfuse-key'
}
]
}
]
}
}
}
Managed Identity Authenticationβ
Managed identity is our team's mandatory standard for authenticating to Azure services. It eliminates credential management entirely.
# Enable system-assigned managed identity
az containerapp identity assign \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--system-assigned
# Get the principal ID for role assignments
PRINCIPAL_ID=$(az containerapp identity show \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--query principalId \
--output tsv)
# Grant Key Vault access
az role assignment create \
--assignee "$PRINCIPAL_ID" \
--role "Key Vault Secrets User" \
--scope "/subscriptions/SUB_ID/resourceGroups/rg-ai-engineering/providers/Microsoft.KeyVault/vaults/kv-ai-engineering"
Application Code Patternβ
# src/config.py
import os
from azure.identity import DefaultAzureCredential
from langchain_openai import AzureChatOpenAI
class Settings:
"""Application settings β managed identity for all Azure services."""
credential = DefaultAzureCredential()
llm = AzureChatOpenAI(
model="gpt-4o",
azure_deployment="gpt-4o",
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
api_version="2024-10-01-preview",
azure_ad_token_provider=lambda: credential.get_token(
"https://cognitiveservices.azure.com/.default"
).token,
)
# Key Vault access for non-Azure secrets
key_vault_credential = DefaultAzureCredential()
The managed identity backend caches tokens for approximately 24 hours per resource URI. If you update RBAC permissions on a target resource, the old cached token may still be used until it expires. Plan permission changes accordingly.
Rotating Secrets Without Downtimeβ
When a Key Vault secret URI omits the version suffix, Container Apps automatically picks up the latest version within 30 minutes and restarts affected revisions.
| URI Format | Behavior |
|---|---|
https://kv.vault.azure.net/secrets/my-secret | Versionless β auto-rotates within 30 min. |
https://kv.vault.azure.net/secrets/my-secret/ec96f02... | Versioned β pins to specific version. No auto-rotation. |
Our standard: Always use versionless URIs in production. This enables zero-downtime secret rotation.
# Rotate a secret in Key Vault β Container Apps picks it up automatically
az keyvault secret set \
--vault-name "kv-ai-engineering" \
--name "langfuse-secret-key" \
--value "sk-lf-NEWVALUE"
Environment-Specific Secretsβ
Use separate Key Vaults per environment and parameterize the secret URIs in your IaC:
- Bicep Parameters
param environment string = 'production'
var keyVaultName = {
development: 'kv-ai-eng-dev'
staging: 'kv-ai-eng-staging'
production: 'kv-ai-eng-prod'
}[environment]
var keyVaultSecretUri = 'https://${keyVaultName}.vault.azure.net/secrets/langfuse-secret-key'
The Golden Ruleβ
Never hardcode API keys. If a secret exists, it belongs in Key Vault. If it's in Key Vault, it's accessed via managed identity. If it's accessed via managed identity, it's never in your code, your Docker image, or your CI/CD logs.
Health Probesβ
Health probes are how Container Apps determines whether your agent is alive, ready to receive traffic, or stuck. Misconfigured probes are the #1 cause of unnecessary container restarts and missed failures.
Probe Typesβ
| Probe | Purpose | Failure Action |
|---|---|---|
| Startup | Checks if the app started successfully. Runs only during initial boot. | Restarts the container after failureThreshold attempts. |
| Liveness | Checks if the app is still running and responsive. | Restarts the container. |
| Readiness | Checks if the replica should receive traffic. | Removes the replica from the load balancer pool. |
Default Configurationβ
When ingress is enabled, Container Apps adds default TCP probes automatically:
| Probe | Protocol | Timeout | Period | Initial Delay | Failure Threshold |
|---|---|---|---|---|---|
| Startup | TCP | 3s | 1s | 1s | 240 |
| Liveness | TCP | β | β | β | β |
| Readiness | TCP | 5s | 5s | 3s | 48 |
Default probes use TCP (connection check). For LangGraph agents, always override with HTTP probes that validate application-level health β database connectivity, LLM availability, and agent initialization.
Configuring Probe Endpoints in FastAPIβ
# src/main.py
import os
import time
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from psycopg_pool import ConnectionPool
startup_time = time.time()
db_pool: ConnectionPool | None = None
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifecycle β initialize resources at startup."""
global db_pool
db_pool = ConnectionPool(
conninfo=os.environ["LANGGRAPH_DB_URI"],
min_size=3,
max_size=10,
timeout=5,
open=True,
)
yield
if db_pool:
await db_pool.close()
app = FastAPI(
title="Research Agent API",
version=os.environ.get("AGENT_VERSION", "1.0.0"),
lifespan=lifespan,
)
@app.get("/health")
async def health_check():
"""
Liveness probe endpoint.
Returns 200 if the application core is functional.
Returns 503 if critical dependencies are down.
"""
try:
# Check database connectivity
if db_pool:
async with db_pool.connection() as conn:
await conn.execute("SELECT 1")
return {"status": "healthy", "uptime": int(time.time() - startup_time)}
except Exception:
raise HTTPException(status_code=503, detail="Database unreachable")
@app.get("/ready")
async def readiness_check():
"""
Readiness probe endpoint.
Returns 200 only when the agent is fully initialized and ready
to process requests β including LLM connectivity.
"""
try:
# Check database
if db_pool:
async with db_pool.connection() as conn:
await conn.execute("SELECT 1")
# Check that the agent is initialized
from src.agent import agent
if agent is None:
raise HTTPException(status_code=503, detail="Agent not initialized")
return {"status": "ready"}
except Exception as exc:
raise HTTPException(status_code=503, detail=str(exc))
@app.get("/startup")
async def startup_check():
"""
Startup probe endpoint.
Lightweight check that returns 200 once the FastAPI server
has bound to its port. Used to prevent liveness probe from
killing the container during slow initialization.
"""
return {"status": "started"}
Probe Timing Configurationβ
- Bicep
resource containerApp 'Microsoft.App/containerApps@2025-02-02-preview' = {
name: 'research-agent'
properties: {
template: {
containers: [
{
name: 'agent'
image: 'aicaeng1234.azurecr.io/research-agent:1.0.0'
probes: [
{
type: 'Startup'
httpGet: {
path: '/startup'
port: 8000
}
initialDelaySeconds: 3
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30
successThreshold: 1
}
{
type: 'Liveness'
httpGet: {
path: '/health'
port: 8000
}
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
}
{
type: 'Readiness'
httpGet: {
path: '/ready'
port: 8000
}
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
}
]
}
]
}
}
}
Recommended Timing Valuesβ
| Parameter | Startup Probe | Liveness Probe | Readiness Probe |
|---|---|---|---|
initialDelaySeconds | 3 | 15 | 10 |
periodSeconds | 5 | 10 | 5 |
timeoutSeconds | 3 | 5 | 5 |
failureThreshold | 30 | 3 | 3 |
successThreshold | 1 | 1 | 1 |
LangGraph agents can take 15-30 seconds to initialize (loading models, establishing database pools, warming caches). The startup probe's high failureThreshold (30) with periodSeconds of 5 gives the container up to 150 seconds before a restart is triggered. This prevents the liveness probe from killing the container during normal initialization.
Probe Failure Troubleshootingβ
When probes fail, Container Apps marks the revision as unhealthy. Common causes:
- Liveness probe fails repeatedly β The app is stuck or crashing. Check logs for exceptions.
- Readiness probe fails but liveness passes β The app is running but dependencies are down. Check database and LLM connectivity.
- Startup probe times out β The container image is broken or the app crashes during initialization.
// Log Analytics query: find probe failures
ContainerAppConsoleLogs_CL
| where Log_s contains "probe" or Log_s contains "unhealthy"
| project TimeGenerated, ContainerName_s, Log_s
| order by TimeGenerated desc
| limit 100
Revision Managementβ
Revisions are the deployment unit in Container Apps. Every revision is an immutable snapshot of your app's configuration. Understanding revisions is essential for safe deployments.
What Are Revisionsβ
A revision is created automatically when you deploy your app for the first time. New revisions are created when you make revision-scope changes:
| Change Type | Creates New Revision? | Examples |
|---|---|---|
| Revision-scope | Yes | Container image, environment variables, scale rules, resource limits, probes |
| Application-scope | No | Secret values, ingress configuration, traffic splitting rules, labels |
Rollout Strategiesβ
Single Revision Mode (Default)β
# Single revision mode β automatic zero-downtime rollout
az containerapp revision set-mode \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--mode single
In single revision mode, Container Apps:
- Provisions the new revision alongside the old one.
- Waits for all replicas to pass startup and readiness probes.
- Shifts 100% of traffic to the new revision.
- Deprovisions the old revision.
Use this for: Standard deployments where you want automatic, safe rollouts.
Multiple Revision Modeβ
# Multiple revision mode β manual traffic control
az containerapp revision set-mode \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--mode multiple
In multiple revision mode, you manually:
- Activate the new revision.
- Configure traffic splitting.
- Deactivate old revisions when ready.
Use this for: Canary deployments, A/B testing, and blue-green deployments.
Rollback Proceduresβ
Rollback is fast because revisions are immutable. To roll back:
- Single Revision Mode
- Multiple Revision Mode
# List all revisions to find the stable one
az containerapp revision list \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
-o table
# Copy the stable revision to create a new one with the same config
az containerapp revision copy \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--source-revision "research-agent-abc123"
# Deactivate the broken revision
az containerapp revision deactivate \
--revision "research-agent-bad456" \
--resource-group "rg-ai-engineering"
# Activate the known-good revision
az containerapp revision activate \
--revision "research-agent-good789" \
--resource-group "rg-ai-engineering"
When rolling back, also check:
- Were there application-scope changes (new secrets, ingress changes)? These persist across revisions.
- Did the rollback revision use a different Key Vault secret version? Versionless URIs auto-rotate.
- Are there pending database migrations that the old revision doesn't support?
Revision Suffixes for Custom DNSβ
Customize revision names for easier identification:
az containerapp update \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--revision-suffix "v1-gpt4o-rag"
The revision name becomes research-agent-v1-gpt4o-rag. Use descriptive suffixes that encode the version and key changes.
Traffic Splitting for Canary Deploymentsβ
In multiple revision mode, split traffic between revisions:
# Deploy new revision
az containerapp update \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--image "aicaeng1234.azurecr.io/research-agent:2.0.0"
# Split traffic: 90% old, 10% new
az containerapp traffic set \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--revision "research-agent-old-rev" --weight 90 \
--revision "research-agent-new-rev" --weight 10
Canary rollout process:
- Deploy new revision (10% traffic).
- Monitor error rates and latency for 30 minutes.
- If stable, increase to 50/50.
- Monitor another 30 minutes.
- If stable, shift to 100% new revision.
- Deactivate old revision.
Observabilityβ
Without observability, you're flying blind. Container Apps integrates with Azure Monitor and Application Insights, but you need to configure it properly.
Application Insights Integrationβ
Application Insights is automatically provisioned with Container Apps environments. Verify it's capturing data:
# Check the connected Log Analytics workspace
az containerapp env show \
--name "cae-ai-agents" \
--resource-group "rg-ai-engineering" \
--query "properties.appInsightsDataCollectionEndpoint"
Distributed Tracing with Langfuseβ
Langfuse provides end-to-end tracing for agent runs. Configure it at the application level:
# Store Langfuse credentials as Key Vault-backed secrets
az containerapp secret set \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--secrets \
"langfuse-pk=keyvaultref:https://kv-ai-engineering.vault.azure.net/secrets/langfuse-public-key,identityref:system" \
"langfuse-sk=keyvaultref:https://kv-ai-engineering.vault.azure.net/secrets/langfuse-secret-key,identityref:system"
az containerapp appsettings set \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--settings \
"LANGFUSE_PUBLIC_KEY=secretref:langfuse-pk" \
"LANGFUSE_SECRET_KEY=secretref:langfuse-sk" \
"LANGFUSE_HOST=https://cloud.langfuse.com"
Custom Metrics and Dashboardsβ
Create an Application Insights dashboard with these key metrics:
| Metric | Source | Alert Threshold |
|---|---|---|
| HTTP 5xx rate | Application Insights | > 1% over 5 min |
| Agent response latency (p95) | Custom metric | > 30s |
| Replica count | Container Apps metrics | > maxReplicas for 10 min |
| OOM kill count | KubeEvents | > 0 in 1 hour |
| Probe failure count | ContainerAppReplicas | > 3 in 5 min |
| LLM token cost | Custom metric (Langfuse) | Budget-dependent |
Custom Metric Emissionβ
# src/metrics.py
import time
from opencensus.ext.azure import log_metrics
metrics_client = log_metrics.get_metrics_client()
def track_agent_invocation(duration_ms: int, step_count: int, status: str) -> None:
"""Emit custom metrics for agent invocations."""
metrics_client.track_metric(name="agent.invocation.duration_ms", value=duration_ms)
metrics_client.track_metric(name="agent.invocation.step_count", value=step_count)
metrics_client.track_metric(name="agent.invocation.status", value=1, tags={"status": status})
Alert Rules for Common Failure Modesβ
- Log Analytics Query
// Alert: High error rate
app()
| where ipv4_is_private(clientIp) == false
| where result_code >= 500
| summarize errorCount = count() by bin(timestamp, 5m)
| where errorCount > 10
// Alert: OOM kills
ContainerAppInstances
| where runningState has "OOMKilled" or runningState has "CrashLoopBackOff"
| summarize count() by ContainerAppName, bin(TimeGenerated, 1h)
// Alert: Replica count at maximum
ContainerAppReplicas
| summarize maxReplicas = max(replicasCount) by ContainerAppName, bin(TimeGenerated, 10m)
| where maxReplicas >= 10 // matches maxReplicas config
Log Retention and Cost Managementβ
Application Insights data retention defaults to 30 days. For cost management:
| Setting | Recommendation |
|---|---|
| Log retention | 30 days for development, 90 days for production. |
| Sampling | Enable adaptive sampling at 50% for non-production. |
| Custom Logs | Filter out DEBUG-level logs in production. |
| Dependency tracking | Enable only for critical external calls (LLM, database). |
Application Insights costs scale with data volume. A typical LangGraph agent emits 50-200 MB/day of logs. At default pricing, this is roughly $5-15/month per agent. Use data caps and sampling to control costs.
Networkingβ
Secure networking is non-negotiable for production agents that handle sensitive data and connect to multiple Azure services.
VNet Integration for Private Accessβ
VNet integration isolates your Container Apps environment from the public internet and enables private connectivity to Azure services.
# Create VNet with dedicated subnets
az network vnet create \
--name "vnet-ai-agents" \
--resource-group "rg-ai-engineering" \
--location "swedencentral" \
--address-prefixes "10.0.0.0/16" \
--subnet-name "cae-subnet" \
--subnet-prefixes "10.0.1.0/24"
# Get subnet ID
SUBNET_ID=$(az network vnet subnet show \
--vnet-name "vnet-ai-agents" \
--name "cae-subnet" \
--resource-group "rg-ai-engineering" \
--query id \
--output tsv)
# Create Container Apps environment with VNet integration
az containerapp env create \
--name "cae-ai-agents" \
--resource-group "rg-ai-engineering" \
--location "swedencentral" \
--infrastructure-subnet-id "$SUBNET_ID"
Custom Domains and TLS Certificatesβ
Production agents should use custom domains with TLS:
# Add a custom domain
az containerapp domain bind \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--domain-name "agent.yourcompany.com" \
--manual \
--cert-name "agent-tls-cert" \
--cert-file "/path/to/cert.pfx" \
--cert-key "your-certificate-password"
When binding a custom domain, Container Apps provides a CNAME record you must add to your DNS zone. The domain won't work until DNS propagation completes (typically 5-15 minutes).
Ingress Configurationβ
| Setting | Internal | External |
|---|---|---|
| Reachable from internet | No | Yes |
| Reachable from same environment | Yes | Yes |
| Custom domain support | Limited | Full |
| TLS termination | No | Yes |
# Internal ingress β only accessible within the VNet
az containerapp create \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--environment "cae-ai-agents" \
--ingress internal \
--target-port 8000 \
--transport auto
# External ingress β publicly accessible
az containerapp create \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--environment "cae-ai-agents" \
--ingress external \
--target-port 8000 \
--transport auto
Our standard: Use internal ingress for all agent apps. Place an API Management instance or Azure Front Door in front for public-facing endpoints.
Rate Limitingβ
Protect your agents from abuse and cost overruns:
# Configure IP restrictions on the Container App
az containerapp restriction set \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--action Allow \
--ip-address "10.0.0.0/8" \
--rule-name "internal-network"
For application-level rate limiting, use FastAPI middleware:
# src/middleware.py
from slowapi import Limiter
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from fastapi.responses import JSONResponse
limiter = Limiter(key_func=get_remote_address)
@limiter.errorhandler
def handle_rate_limit(error: RateLimitExceeded):
return JSONResponse(
status_code=429,
content={"detail": "Too many requests. Try again later."},
)
# Apply to the app
app.state.limiter = limiter
@app.post("/chat")
@limiter.limit("10/minute")
async def chat(request: ChatRequest):
# ... agent invocation
pass
Disaster Recoveryβ
Production agents must survive regional outages. This section covers our team's disaster recovery standards.
Cross-Region Deployment Strategiesβ
Deploy agent replicas to at least two Azure regions. Use Azure Traffic Manager or Front Door for failover routing.
| Strategy | RTO | RPO | Complexity | Cost |
|---|---|---|---|---|
| Cold standby (secondary region, no active replicas) | 15-30 min | Last successful backup | Low | ~20% of primary |
| Warm standby (secondary region, minReplicas: 1) | 1-5 min | Near-zero | Medium | ~50% of primary |
| Active-active (both regions serving traffic) | < 1 min | Near-zero | High | ~100% of primary |
Our standard: Warm standby for user-facing agents. Cold standby for internal tools.
Warm Standby Configurationβ
# Primary region
PRIMARY_RG="rg-ai-engineering-se"
PRIMARY_REGION="swedencentral"
# Secondary region
SECONDARY_RG="rg-ai-engineering-nee"
SECONDARY_REGION="northeurope"
# Deploy to secondary with minReplicas: 1 (warm standby)
az containerapp create \
--name "research-agent" \
--resource-group "$SECONDARY_RG" \
--environment "cae-ai-agents-secondary" \
--image "aicaeng1234.azurecr.io/research-agent:1.0.0" \
--min-replicas 1 \
--max-replicas 5 \
--target-port 8000 \
--ingress external
Backup and Restore Proceduresβ
Container Apps itself doesn't provide built-in backup, but your data stores (PostgreSQL, Azure AI Search, Key Vault) do.
| Resource | Backup Method | Frequency | Retention |
|---|---|---|---|
| PostgreSQL | Azure automated backup | Continuous (point-in-time restore) | 7-35 days (depends on tier) |
| Azure AI Search | Index snapshots via Azure Backup | Daily | 30 days |
| Key Vault | Built-in soft delete + purge protection | Continuous | 90 days |
| Container App config | Bicep/ARM templates in Git | On every change | Infinite (Git history) |
PostgreSQL Point-in-Time Restoreβ
# Restore PostgreSQL to a specific point in time
az postgres flexible-server restore \
--name "research-agent-db-restore" \
--resource-group "rg-ai-engineering" \
--source-server "research-agent-db" \
--restore-point-time "2025-01-15T10:30:00Z"
Testing Failover Regularlyβ
Disaster recovery is only as good as your last test. Schedule quarterly failover drills:
- Simulate primary region failure β deactivate the primary Container App revision.
- Verify secondary region handles traffic β check health endpoints and agent responses.
- Measure actual RTO β time from failure detection to full recovery.
- Document findings β update runbooks with actual timings and any issues found.
Before each failover drill:
- Notify the team (scheduled maintenance window).
- Verify secondary region has current Key Vault secrets.
- Confirm database replication lag is within acceptable bounds.
- Have a rollback plan ready (re-activate primary).
- Assign an observer to time each step.
Summary Checklistβ
Use this checklist when promoting an agent to production:
- Scaling:
minReplicas: 1,maxReplicasset,concurrentRequeststuned to agent response time. - Resources: CPU and memory right-sized for agent complexity. OOM monitoring in place.
- Secrets: All credentials in Key Vault. Managed identity for Azure services. No hardcoded secrets.
- Probes: HTTP startup, liveness, and readiness probes configured with appropriate timing.
- Revisions: Revision mode set. Rollback procedure documented.
- Observability: Application Insights active. Langfuse tracing enabled. Alert rules configured.
- Networking: VNet integration enabled. Internal ingress with API Management/Front Door. IP restrictions applied.
- Disaster Recovery: Secondary region deployed. Backup procedures verified. Failover runbook written.
Next Stepsβ
- Getting Started with Container Apps β deploy your first LangGraph agent.
- LangGraph Production Patterns β agent design patterns for reliability and performance.
- PostgreSQL Checkpointing β persist agent state with LangGraph checkpointers.
- Azure Container Apps Documentation β official Microsoft documentation.