Azure Container Apps: Serverless Containers Made Simple

Introduction

Containers revolutionized application deployment, but managing Kubernetes clusters adds operational complexity. Azure Container Apps delivers the best of both worlds: deploy containers with Kubernetes power, but with serverless simplicityβ€”no cluster management, automatic scaling, and pay-per-use pricing.

In this comprehensive guide, you'll master Azure Container Apps from basics through advanced production scenarios. You'll deploy multi-container applications, configure custom domains with TLS, implement blue-green deployments, integrate Dapr for microservices, and build event-driven architectures.

What You'll Learn:

  • Azure Container Apps architecture and concepts
  • Creating container apps with Azure CLI and Bicep
  • Container registry integration (ACR)
  • Environment configuration and secrets management
  • Auto-scaling rules (HTTP, CPU, memory, custom metrics)
  • Ingress configuration and custom domains
  • Multi-container applications with sidecars
  • Dapr integration for service-to-service communication
  • Event-driven scaling with KEDA
  • Blue-green and canary deployments
  • Monitoring with Application Insights
  • Cost optimization strategies

Time to Complete: 90-120 minutes
Skill Level: Intermediate to Advanced

Azure Container Apps Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Azure Container Apps Architecture                         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                              β”‚
β”‚                           Internet / VNet                                    β”‚
β”‚                                  β”‚                                           β”‚
β”‚                                  β–Ό                                           β”‚
β”‚                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                             β”‚
β”‚                    β”‚   Azure Load Balancer    β”‚                             β”‚
β”‚                    β”‚   (Automatic Ingress)    β”‚                             β”‚
β”‚                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                             β”‚
β”‚                                 β”‚                                            β”‚
β”‚                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                             β”‚
β”‚                    β”‚  Container Apps          β”‚                             β”‚
β”‚                    β”‚  Environment             β”‚                             β”‚
β”‚                    β”‚  (Kubernetes Cluster)    β”‚                             β”‚
β”‚                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                             β”‚
β”‚                                 β”‚                                            β”‚
β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚         β”‚                       β”‚                       β”‚                   β”‚
β”‚         β–Ό                       β–Ό                       β–Ό                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚  β”‚ Container   β”‚         β”‚ Container   β”‚        β”‚ Container   β”‚           β”‚
β”‚  β”‚ App 1       β”‚         β”‚ App 2       β”‚        β”‚ App 3       β”‚           β”‚
β”‚  β”‚ (Frontend)  β”‚         β”‚ (API)       β”‚        β”‚ (Worker)    β”‚           β”‚
β”‚  β”‚             β”‚         β”‚             β”‚        β”‚             β”‚           β”‚
β”‚  β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚         β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚        β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚           β”‚
β”‚  β”‚ β”‚Revision β”‚ β”‚         β”‚ β”‚Revision β”‚ β”‚        β”‚ β”‚Revision β”‚ β”‚           β”‚
β”‚  β”‚ β”‚  v1.0   β”‚ β”‚         β”‚ β”‚  v2.1   β”‚ β”‚        β”‚ β”‚  v1.3   β”‚ β”‚           β”‚
β”‚  β”‚ β”‚ (100%)  β”‚ β”‚         β”‚ β”‚  (80%)  β”‚ β”‚        β”‚ β”‚ (100%)  β”‚ β”‚           β”‚
β”‚  β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚         β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚        β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚           β”‚
β”‚  β”‚             β”‚         β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚        β”‚             β”‚           β”‚
β”‚  β”‚             β”‚         β”‚ β”‚Revision β”‚ β”‚        β”‚             β”‚           β”‚
β”‚  β”‚             β”‚         β”‚ β”‚  v2.0   β”‚ β”‚        β”‚             β”‚           β”‚
β”‚  β”‚             β”‚         β”‚ β”‚  (20%)  β”‚ β”‚        β”‚             β”‚           β”‚
β”‚  β”‚             β”‚         β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚        β”‚             β”‚           β”‚
β”‚  β”‚             β”‚         β”‚             β”‚        β”‚             β”‚           β”‚
β”‚  β”‚ Replicas:   β”‚         β”‚ Replicas:   β”‚        β”‚ Replicas:   β”‚           β”‚
β”‚  β”‚ Min: 1      β”‚         β”‚ Min: 2      β”‚        β”‚ Min: 0      β”‚           β”‚
β”‚  β”‚ Max: 10     β”‚         β”‚ Max: 30     β”‚        β”‚ Max: 50     β”‚           β”‚
β”‚  β”‚ Current: 3  β”‚         β”‚ Current: 8  β”‚        β”‚ Current: 5  β”‚           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚         β”‚                       β”‚                       β”‚                   β”‚
β”‚         β”‚                       β”‚                       β”‚                   β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                     β”‚                       β”‚                               β”‚
β”‚                     β–Ό                       β–Ό                               β”‚
β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”‚
β”‚         β”‚   Dapr Sidecar     β”‚   β”‚  Managed Services    β”‚                  β”‚
β”‚         β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€   β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€                  β”‚
β”‚         β”‚ β€’ Service Invoc.   β”‚   β”‚ β€’ Azure SQL          β”‚                  β”‚
β”‚         β”‚ β€’ Pub/Sub          β”‚   β”‚ β€’ Cosmos DB          β”‚                  β”‚
β”‚         β”‚ β€’ State Store      β”‚   β”‚ β€’ Redis Cache        β”‚                  β”‚
β”‚         β”‚ β€’ Bindings         β”‚   β”‚ β€’ Service Bus        β”‚                  β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚ β€’ Storage Account    β”‚                  β”‚
β”‚                                   β”‚ β€’ Key Vault          β”‚                  β”‚
β”‚                                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚
β”‚                                                                              β”‚
β”‚  Scaling Triggers:                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”‚
β”‚  β”‚ β€’ HTTP Requests (concurrent requests per replica)              β”‚        β”‚
β”‚  β”‚ β€’ CPU Utilization (percentage)                                 β”‚        β”‚
β”‚  β”‚ β€’ Memory Usage (bytes or percentage)                           β”‚        β”‚
β”‚  β”‚ β€’ Azure Queue Length (Service Bus, Storage Queue)             β”‚        β”‚
β”‚  β”‚ β€’ Custom Metrics (App Insights, Prometheus)                   β”‚        β”‚
β”‚  β”‚ β€’ Event Hub Messages                                           β”‚        β”‚
β”‚  β”‚ β€’ Kafka Topics                                                 β”‚        β”‚
β”‚  β”‚ β€’ CRON Schedule                                                β”‚        β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β”‚
β”‚                                                                              β”‚
β”‚  Networking:                                                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”‚
β”‚  β”‚ External Ingress:                                              β”‚        β”‚
β”‚  β”‚   β€’ Public endpoint with auto-generated domain                β”‚        β”‚
β”‚  β”‚   β€’ Custom domain with managed certificates                   β”‚        β”‚
β”‚  β”‚   β€’ CORS configuration                                         β”‚        β”‚
β”‚  β”‚                                                                 β”‚        β”‚
β”‚  β”‚ Internal Ingress:                                              β”‚        β”‚
β”‚  β”‚   β€’ VNet integration (private endpoints)                      β”‚        β”‚
β”‚  β”‚   β€’ Service-to-service within environment (Dapr)              β”‚        β”‚
β”‚  β”‚   β€’ No public internet exposure                               β”‚        β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β”‚
β”‚                                                                              β”‚
β”‚  Security:                                                                  β”‚
β”‚  β€’ Managed Identity (Azure AD authentication)                              β”‚
β”‚  β€’ Secrets stored in environment (encrypted at rest)                       β”‚
β”‚  β€’ TLS/SSL automatic certificates (Let's Encrypt)                          β”‚
β”‚  β€’ Network policies (NSG integration)                                      β”‚
β”‚  β€’ Container image scanning (ACR integration)                              β”‚
β”‚                                                                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Prerequisites

Required Azure Resources

  • Azure Subscription with Owner or Contributor role
  • Azure Container Registry (or Docker Hub account)
  • Resource Group for Container Apps resources

Required Tools

  • Azure CLI 2.40+ with containerapp extension
  • Docker Desktop (for local container building)
  • Visual Studio Code with Docker extension
  • Git (for sample application)

Verify Prerequisites

# Check Azure CLI version
az version

# Install Container Apps extension
az extension add --name containerapp --upgrade

# Check Docker installation
docker --version

# Login to Azure
az login

# Set default subscription
az account set --subscription "<Your-Subscription-ID>"

# Verify subscription
az account show --output table

Step 1: Create Container Apps Environment

Create Resource Group

# Set variables
RESOURCE_GROUP="rg-containerapps-prod"
LOCATION="eastus"
ENVIRONMENT_NAME="cae-prod-env"
LOG_ANALYTICS_WORKSPACE="law-containerapps"

echo "Creating resource group..."

az group create \
  --name $RESOURCE_GROUP \
  --location $LOCATION

echo "βœ“ Resource group created: $RESOURCE_GROUP"

Create Log Analytics Workspace

echo "Creating Log Analytics workspace..."

az monitor log-analytics workspace create \
  --resource-group $RESOURCE_GROUP \
  --workspace-name $LOG_ANALYTICS_WORKSPACE \
  --location $LOCATION

# Get workspace credentials
LOG_ANALYTICS_WORKSPACE_ID=$(az monitor log-analytics workspace show \
  --resource-group $RESOURCE_GROUP \
  --workspace-name $LOG_ANALYTICS_WORKSPACE \
  --query customerId \
  --output tsv)

LOG_ANALYTICS_WORKSPACE_KEY=$(az monitor log-analytics workspace get-shared-keys \
  --resource-group $RESOURCE_GROUP \
  --workspace-name $LOG_ANALYTICS_WORKSPACE \
  --query primarySharedKey \
  --output tsv)

echo "βœ“ Log Analytics workspace created"
echo "  Workspace ID: $LOG_ANALYTICS_WORKSPACE_ID"

Create Container Apps Environment

echo "Creating Container Apps environment..."

az containerapp env create \
  --name $ENVIRONMENT_NAME \
  --resource-group $RESOURCE_GROUP \
  --location $LOCATION \
  --logs-workspace-id $LOG_ANALYTICS_WORKSPACE_ID \
  --logs-workspace-key $LOG_ANALYTICS_WORKSPACE_KEY

echo "βœ“ Container Apps environment created: $ENVIRONMENT_NAME"

# Get environment details
az containerapp env show \
  --name $ENVIRONMENT_NAME \
  --resource-group $RESOURCE_GROUP \
  --output table

Step 2: Create Azure Container Registry

Deploy ACR

ACR_NAME="acrcontosoprod$RANDOM"

echo "Creating Azure Container Registry..."

az acr create \
  --resource-group $RESOURCE_GROUP \
  --name $ACR_NAME \
  --sku Standard \
  --admin-enabled true \
  --location $LOCATION

echo "βœ“ Container Registry created: $ACR_NAME"

# Get ACR credentials
ACR_LOGIN_SERVER=$(az acr show \
  --name $ACR_NAME \
  --resource-group $RESOURCE_GROUP \
  --query loginServer \
  --output tsv)

ACR_USERNAME=$(az acr credential show \
  --name $ACR_NAME \
  --resource-group $RESOURCE_GROUP \
  --query username \
  --output tsv)

ACR_PASSWORD=$(az acr credential show \
  --name $ACR_NAME \
  --resource-group $RESOURCE_GROUP \
  --query passwords[0].value \
  --output tsv)

echo "  Login Server: $ACR_LOGIN_SERVER"
echo "  Username: $ACR_USERNAME"

Build and Push Sample Application

# Create sample Node.js application
mkdir -p ~/containerapp-demo
cd ~/containerapp-demo

# Create package.json
cat <<EOF > package.json
{
  "name": "containerapp-demo",
  "version": "1.0.0",
  "description": "Sample API for Azure Container Apps",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.18.0"
  }
}
EOF

# Create server.js
cat <<EOF > server.js
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

// Health check endpoint
app.get('/health', (req, res) => {
  res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});

// API endpoint
app.get('/api/hello', (req, res) => {
  const name = req.query.name || 'World';
  res.json({ 
    message: \`Hello, \${name}!\`,
    version: '1.0.0',
    hostname: require('os').hostname()
  });
});

// List all routes
app.get('/', (req, res) => {
  res.json({
    endpoints: [
      { path: '/', method: 'GET', description: 'List all routes' },
      { path: '/health', method: 'GET', description: 'Health check' },
      { path: '/api/hello', method: 'GET', description: 'Hello API' }
    ]
  });
});

app.listen(port, () => {
  console.log(\`Server running on port \${port}\`);
});
EOF

# Create Dockerfile
cat <<EOF > Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install --production

COPY . .

EXPOSE 3000

CMD ["npm", "start"]
EOF

# Create .dockerignore
cat <<EOF > .dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
EOF

echo "βœ“ Sample application created"

# Build Docker image
docker build -t $ACR_LOGIN_SERVER/demo-api:v1 .

echo "βœ“ Docker image built"

# Login to ACR
az acr login --name $ACR_NAME

# Push image to ACR
docker push $ACR_LOGIN_SERVER/demo-api:v1

echo "βœ“ Image pushed to ACR: $ACR_LOGIN_SERVER/demo-api:v1"

# List images in ACR
az acr repository list --name $ACR_NAME --output table

Step 3: Deploy Container App

Create Container App with CLI

CONTAINER_APP_NAME="api-demo"

echo "Deploying container app..."

az containerapp create \
  --name $CONTAINER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --environment $ENVIRONMENT_NAME \
  --image "$ACR_LOGIN_SERVER/demo-api:v1" \
  --registry-server $ACR_LOGIN_SERVER \
  --registry-username $ACR_USERNAME \
  --registry-password $ACR_PASSWORD \
  --target-port 3000 \
  --ingress external \
  --min-replicas 1 \
  --max-replicas 10 \
  --cpu 0.5 \
  --memory 1.0Gi \
  --env-vars "PORT=3000" "ENVIRONMENT=production"

echo "βœ“ Container app deployed: $CONTAINER_APP_NAME"

# Get app URL
APP_URL=$(az containerapp show \
  --name $CONTAINER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --query properties.configuration.ingress.fqdn \
  --output tsv)

echo "βœ“ Application URL: https://$APP_URL"

# Test the application
echo ""
echo "Testing application..."
curl -s "https://$APP_URL/health" | jq
curl -s "https://$APP_URL/api/hello?name=Azure" | jq

Deploy with Bicep

// main.bicep
param location string = resourceGroup().location
param environmentName string = 'cae-prod-env'
param containerAppName string = 'api-demo'
param containerImage string
param containerPort int = 3000
param minReplicas int = 1
param maxReplicas int = 10

// Container Apps Environment
resource environment 'Microsoft.App/managedEnvironments@2023-05-01' existing = {
  name: environmentName
}

// Container App
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
  name: containerAppName
  location: location
  properties: {
    managedEnvironmentId: environment.id
    configuration: {
      ingress: {
        external: true
        targetPort: containerPort
        transport: 'auto'
        allowInsecure: false
      }
      registries: [
        {
          server: split(containerImage, '/')[0]
          username: acrUsername
          passwordSecretRef: 'acr-password'
        }
      ]
      secrets: [
        {
          name: 'acr-password'
          value: acrPassword
        }
      ]
    }
    template: {
      containers: [
        {
          name: 'api'
          image: containerImage
          resources: {
            cpu: json('0.5')
            memory: '1.0Gi'
          }
          env: [
            {
              name: 'PORT'
              value: string(containerPort)
            }
            {
              name: 'ENVIRONMENT'
              value: 'production'
            }
          ]
        }
      ]
      scale: {
        minReplicas: minReplicas
        maxReplicas: maxReplicas
        rules: [
          {
            name: 'http-rule'
            http: {
              metadata: {
                concurrentRequests: '50'
              }
            }
          }
        ]
      }
    }
  }
}

output fqdn string = containerApp.properties.configuration.ingress.fqdn
output latestRevisionName string = containerApp.properties.latestRevisionName
# Deploy with Bicep
az deployment group create \
  --resource-group $RESOURCE_GROUP \
  --template-file main.bicep \
  --parameters \
    environmentName=$ENVIRONMENT_NAME \
    containerAppName=$CONTAINER_APP_NAME \
    containerImage="$ACR_LOGIN_SERVER/demo-api:v1" \
    minReplicas=1 \
    maxReplicas=10

echo "βœ“ Bicep deployment completed"

Step 4: Configure Auto-Scaling Rules

HTTP Scaling Rule

echo "Configuring HTTP-based scaling..."

az containerapp update \
  --name $CONTAINER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --min-replicas 2 \
  --max-replicas 20 \
  --scale-rule-name http-scale \
  --scale-rule-type http \
  --scale-rule-http-concurrency 100

echo "βœ“ HTTP scaling configured (100 concurrent requests per replica)"

CPU and Memory Scaling Rules

echo "Adding CPU scaling rule..."

az containerapp update \
  --name $CONTAINER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --scale-rule-name cpu-scale \
  --scale-rule-type cpu \
  --scale-rule-metadata "type=Utilization" "value=70"

echo "βœ“ CPU scaling configured (70% threshold)"

# Add memory scaling
az containerapp update \
  --name $CONTAINER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --scale-rule-name memory-scale \
  --scale-rule-type memory \
  --scale-rule-metadata "type=Utilization" "value=80"

echo "βœ“ Memory scaling configured (80% threshold)"

Azure Queue Scaling (KEDA)

# Create Storage Account for queue
STORAGE_ACCOUNT="stcontosoqueueprod$RANDOM"

az storage account create \
  --name $STORAGE_ACCOUNT \
  --resource-group $RESOURCE_GROUP \
  --location $LOCATION \
  --sku Standard_LRS

# Get connection string
STORAGE_CONNECTION_STRING=$(az storage account show-connection-string \
  --name $STORAGE_ACCOUNT \
  --resource-group $RESOURCE_GROUP \
  --query connectionString \
  --output tsv)

# Create queue
az storage queue create \
  --name tasks \
  --connection-string "$STORAGE_CONNECTION_STRING"

echo "βœ“ Storage queue created"

# Add queue scaling rule to worker container app
WORKER_APP_NAME="worker-demo"

az containerapp create \
  --name $WORKER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --environment $ENVIRONMENT_NAME \
  --image "$ACR_LOGIN_SERVER/demo-api:v1" \
  --registry-server $ACR_LOGIN_SERVER \
  --registry-username $ACR_USERNAME \
  --registry-password $ACR_PASSWORD \
  --min-replicas 0 \
  --max-replicas 30 \
  --cpu 0.25 \
  --memory 0.5Gi \
  --secrets "storage-connection=$STORAGE_CONNECTION_STRING" \
  --scale-rule-name queue-scale \
  --scale-rule-type azure-queue \
  --scale-rule-metadata "queueName=tasks" "queueLength=10" \
  --scale-rule-auth "connection=storage-connection"

echo "βœ“ Worker app created with queue-based scaling"
echo "  Scales to 0 when queue is empty"
echo "  Scales up when queue length > 10 messages"

Step 5: Implement Blue-Green Deployments

Create New Revision (v2)

# Update application code (v2)
cat <<EOF > server.js
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

app.get('/health', (req, res) => {
  res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});

app.get('/api/hello', (req, res) => {
  const name = req.query.name || 'World';
  res.json({ 
    message: \`Hello, \${name}! Welcome to v2!\`,
    version: '2.0.0',  // Updated version
    hostname: require('os').hostname(),
    features: ['New UI', 'Better performance', 'Bug fixes']  // New field
  });
});

app.get('/', (req, res) => {
  res.json({
    version: '2.0.0',
    endpoints: [
      { path: '/', method: 'GET', description: 'List all routes' },
      { path: '/health', method: 'GET', description: 'Health check' },
      { path: '/api/hello', method: 'GET', description: 'Hello API (v2)' }
    ]
  });
});

app.listen(port, () => {
  console.log(\`Server v2 running on port \${port}\`);
});
EOF

# Build and push v2
docker build -t $ACR_LOGIN_SERVER/demo-api:v2 .
docker push $ACR_LOGIN_SERVER/demo-api:v2

echo "βœ“ Version 2 image pushed"

Deploy v2 with Traffic Splitting

echo "Deploying v2 with 20% traffic..."

# Update container app with new revision
az containerapp update \
  --name $CONTAINER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --image "$ACR_LOGIN_SERVER/demo-api:v2"

# Get revision names
REVISION_V1=$(az containerapp revision list \
  --name $CONTAINER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --query "[?contains(name, 'v1')].name" \
  --output tsv | head -n 1)

REVISION_V2=$(az containerapp revision list \
  --name $CONTAINER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --query "[0].name" \
  --output tsv)

echo "  Revision v1: $REVISION_V1"
echo "  Revision v2: $REVISION_V2"

# Split traffic: 80% to v1, 20% to v2
az containerapp ingress traffic set \
  --name $CONTAINER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --revision-weight "$REVISION_V1=80" "$REVISION_V2=20"

echo "βœ“ Traffic split: 80% v1, 20% v2"

# Test both versions
echo ""
echo "Testing traffic distribution..."
for i in {1..10}; do
  curl -s "https://$APP_URL/api/hello" | jq -r '.version'
done

Promote v2 to 100%

echo "Promoting v2 to 100% traffic..."

az containerapp ingress traffic set \
  --name $CONTAINER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --revision-weight "$REVISION_V2=100"

echo "βœ“ v2 is now serving 100% of traffic"

# Deactivate old revision
az containerapp revision deactivate \
  --name $CONTAINER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --revision $REVISION_V1

echo "βœ“ v1 revision deactivated"

# List active revisions
az containerapp revision list \
  --name $CONTAINER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --query "[?properties.active].{Name:name,Active:properties.active,Traffic:properties.trafficWeight,Created:properties.createdTime}" \
  --output table

Step 6: Integrate Dapr for Microservices

Enable Dapr on Container Apps Environment

echo "Enabling Dapr..."

az containerapp env dapr-component set \
  --name $ENVIRONMENT_NAME \
  --resource-group $RESOURCE_GROUP \
  --dapr-component-name statestore \
  --yaml dapr-statestore.yaml

# Create Dapr state store configuration
cat <<EOF > dapr-statestore.yaml
componentType: state.azure.blobstorage
version: v1
metadata:
- name: accountName
  value: "$STORAGE_ACCOUNT"
- name: accountKey
  secretRef: storage-key
- name: containerName
  value: "dapr-state"
secrets:
- name: storage-key
  value: "$STORAGE_CONNECTION_STRING"
scopes:
- $CONTAINER_APP_NAME
EOF

echo "βœ“ Dapr state store configured"

Deploy Dapr-Enabled Container App

# Create Dapr-enabled app
DAPR_APP_NAME="api-dapr-demo"

az containerapp create \
  --name $DAPR_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --environment $ENVIRONMENT_NAME \
  --image "$ACR_LOGIN_SERVER/demo-api:v2" \
  --registry-server $ACR_LOGIN_SERVER \
  --registry-username $ACR_USERNAME \
  --registry-password $ACR_PASSWORD \
  --target-port 3000 \
  --ingress external \
  --min-replicas 1 \
  --max-replicas 5 \
  --enable-dapr \
  --dapr-app-id demo-api \
  --dapr-app-port 3000 \
  --dapr-app-protocol http

echo "βœ“ Dapr-enabled container app deployed"

# Get Dapr components
az containerapp env dapr-component list \
  --name $ENVIRONMENT_NAME \
  --resource-group $RESOURCE_GROUP \
  --output table

Service-to-Service Communication with Dapr

// Example: Calling another service via Dapr
// Add to server.js

app.get('/api/call-service', async (req, res) => {
  const daprPort = process.env.DAPR_HTTP_PORT || 3500;
  const serviceAppId = 'other-service';
  const method = 'api/data';
  
  try {
    const response = await fetch(
      `http://localhost:${daprPort}/v1.0/invoke/${serviceAppId}/method/${method}`
    );
    const data = await response.json();
    res.json({ 
      success: true, 
      data: data,
      calledVia: 'Dapr service invocation'
    });
  } catch (error) {
    res.status(500).json({ 
      success: false, 
      error: error.message 
    });
  }
});

// State management with Dapr
app.post('/api/state/:key', async (req, res) => {
  const daprPort = process.env.DAPR_HTTP_PORT || 3500;
  const stateStore = 'statestore';
  const key = req.params.key;
  const value = req.body;
  
  try {
    await fetch(
      `http://localhost:${daprPort}/v1.0/state/${stateStore}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify([{ key: key, value: value }])
      }
    );
    res.json({ success: true, message: 'State saved' });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

Step 7: Configure Custom Domain and TLS

Add Custom Domain

CUSTOM_DOMAIN="api.contoso.com"

echo "Adding custom domain: $CUSTOM_DOMAIN"

# Add domain (requires DNS verification)
az containerapp hostname add \
  --name $CONTAINER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --hostname $CUSTOM_DOMAIN

echo "βœ“ Custom domain added"
echo "⚠ Add CNAME record in DNS:"
echo "  CNAME: api"
echo "  Target: $APP_URL"

# Bind managed certificate (after DNS verification)
az containerapp hostname bind \
  --name $CONTAINER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --hostname $CUSTOM_DOMAIN \
  --environment $ENVIRONMENT_NAME \
  --validation-method CNAME

echo "βœ“ TLS certificate bound automatically"

Step 8: Monitoring and Diagnostics

Configure Application Insights

# Create Application Insights
APP_INSIGHTS_NAME="ai-containerapps"

az monitor app-insights component create \
  --app $APP_INSIGHTS_NAME \
  --location $LOCATION \
  --resource-group $RESOURCE_GROUP \
  --workspace $LOG_ANALYTICS_WORKSPACE

# Get instrumentation key
INSTRUMENTATION_KEY=$(az monitor app-insights component show \
  --app $APP_INSIGHTS_NAME \
  --resource-group $RESOURCE_GROUP \
  --query instrumentationKey \
  --output tsv)

echo "βœ“ Application Insights created"
echo "  Instrumentation Key: $INSTRUMENTATION_KEY"

# Update container app with App Insights
az containerapp update \
  --name $CONTAINER_APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --set-env-vars "APPINSIGHTS_INSTRUMENTATIONKEY=$INSTRUMENTATION_KEY"

echo "βœ“ App Insights integrated"

Query Logs with Kusto

-- Container app logs
ContainerAppConsoleLogs_CL
| where ContainerAppName_s == "api-demo"
| where TimeGenerated > ago(1h)
| project TimeGenerated, Log_s, Stream_s
| order by TimeGenerated desc
| take 100

-- HTTP requests
AppRequests
| where AppRoleName == "api-demo"
| where TimeGenerated > ago(1h)
| summarize 
    RequestCount = count(),
    AvgDuration = avg(DurationMs),
    P95Duration = percentile(DurationMs, 95),
    FailureCount = countif(Success == false)
  by bin(TimeGenerated, 5m)
| order by TimeGenerated desc

-- Scaling events
ContainerAppSystemLogs_CL
| where ContainerAppName_s == "api-demo"
| where Log_s contains "Scaling"
| project TimeGenerated, Log_s, Reason_s
| order by TimeGenerated desc

-- Replica count over time
ContainerAppSystemLogs_CL
| where ContainerAppName_s == "api-demo"
| summarize ReplicaCount = dcount(ReplicaName_s) by bin(TimeGenerated, 1m)
| render timechart

Best Practices Summary

DO:

  1. βœ… Use managed identities for Azure service authentication
  2. βœ… Store secrets in environment secrets (not in code)
  3. βœ… Configure health probes for production workloads
  4. βœ… Use traffic splitting for gradual rollouts
  5. βœ… Enable Dapr for microservices patterns
  6. βœ… Set appropriate CPU/memory limits
  7. βœ… Configure auto-scaling based on workload patterns
  8. βœ… Use ACR for private container images
  9. βœ… Enable Application Insights for monitoring
  10. βœ… Tag revisions for easy rollback

DON'T:

  1. ❌ Deploy to production without testing revisions
  2. ❌ Skip setting min replicas for critical apps (use β‰₯ 2)
  3. ❌ Expose internal services publicly (use internal ingress)
  4. ❌ Hard-code connection strings or credentials
  5. ❌ Ignore scaling limits (test max load scenarios)
  6. ❌ Deploy without health check endpoints
  7. ❌ Use "latest" tag in production (use version tags)
  8. ❌ Skip monitoring and alerting configuration
  9. ❌ Deploy large container images (optimize Dockerfile)
  10. ❌ Forget to clean up old revisions

Key Takeaways

  1. Serverless simplicity - No cluster management, automatic scaling, pay-per-use
  2. Container flexibility - Run any containerized app (Linux, Windows)
  3. Built-in Dapr - Microservices patterns without complexity
  4. Traffic splitting - Blue-green and canary deployments made easy
  5. KEDA scaling - Scale to zero for cost optimization
  6. Managed certificates - Automatic TLS with Let's Encrypt
  7. Environment isolation - Share infrastructure across apps securely
  8. Revision management - Instant rollback to previous versions
  9. VNet integration - Private networking for secure workloads
  10. Azure ecosystem - Seamless integration with Azure services

Additional Resources

Next Steps

  1. Deploy multi-container apps: Add sidecars for logging, monitoring
  2. Implement Dapr pub/sub: Event-driven architectures
  3. Configure VNet integration: Private endpoints for security
  4. Set up CI/CD: GitHub Actions or Azure DevOps pipelines
  5. Add authentication: Azure AD or custom auth providers
  6. Implement rate limiting: Protect APIs from abuse
  7. Create dashboards: Power BI or Grafana for metrics
  8. Test disaster recovery: Multi-region deployments

Ready to deploy containers without Kubernetes complexity? Azure Container Apps delivers enterprise-grade container hosting with serverless simplicity!