Back to Blog
5 min read

Azure Container Apps Updates: Serverless Containers at Scale

Azure Container Apps has received significant updates at Build 2023, making it even more powerful for running containerized applications. Today, I will cover the new features and best practices.

Azure Container Apps Overview

Container Apps provides a serverless container platform built on Kubernetes but without the complexity:

┌─────────────────────────────────────────────────────┐
│            Azure Container Apps                      │
├─────────────────────────────────────────────────────┤
│                                                      │
│  ┌─────────────────────────────────────────────────┐│
│  │           Container Apps Environment            ││
│  │  ┌─────────────┐  ┌─────────────┐              ││
│  │  │  App 1      │  │  App 2      │              ││
│  │  │  (HTTP)     │  │  (Event)    │              ││
│  │  └─────────────┘  └─────────────┘              ││
│  │  ┌─────────────┐  ┌─────────────┐              ││
│  │  │  Job 1      │  │  Job 2      │              ││
│  │  │  (Cron)     │  │  (Manual)   │              ││
│  │  └─────────────┘  └─────────────┘              ││
│  └─────────────────────────────────────────────────┘│
│                        │                            │
│                        ▼                            │
│  ┌─────────────────────────────────────────────────┐│
│  │     Managed Kubernetes (KEDA, Envoy, Dapr)     ││
│  └─────────────────────────────────────────────────┘│
│                                                      │
└─────────────────────────────────────────────────────┘

Creating Container Apps

Basic HTTP App

# Create environment
az containerapp env create \
    --name my-environment \
    --resource-group container-rg \
    --location eastus

# Create app from container image
az containerapp create \
    --name my-api \
    --resource-group container-rg \
    --environment my-environment \
    --image myregistry.azurecr.io/my-api:v1 \
    --target-port 8080 \
    --ingress external \
    --min-replicas 1 \
    --max-replicas 10 \
    --cpu 0.5 \
    --memory 1.0Gi

Using Bicep

// container-app.bicep
param location string = resourceGroup().location
param environmentName string
param appName string
param containerImage string
param targetPort int = 8080

resource environment 'Microsoft.App/managedEnvironments@2023-05-01' = {
  name: environmentName
  location: location
  properties: {
    zoneRedundant: true
    workloadProfiles: [
      {
        name: 'Consumption'
        workloadProfileType: 'Consumption'
      }
    ]
  }
}

resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
  name: appName
  location: location
  properties: {
    managedEnvironmentId: environment.id
    configuration: {
      ingress: {
        external: true
        targetPort: targetPort
        transport: 'http'
        corsPolicy: {
          allowedOrigins: ['*']
          allowedMethods: ['GET', 'POST', 'PUT', 'DELETE']
        }
      }
      registries: [
        {
          server: 'myregistry.azurecr.io'
          identity: 'system'
        }
      ]
      secrets: [
        {
          name: 'db-connection'
          value: 'Server=...;Database=...'
        }
      ]
    }
    template: {
      containers: [
        {
          name: 'main'
          image: containerImage
          resources: {
            cpu: json('0.5')
            memory: '1Gi'
          }
          env: [
            {
              name: 'DB_CONNECTION'
              secretRef: 'db-connection'
            }
            {
              name: 'ASPNETCORE_ENVIRONMENT'
              value: 'Production'
            }
          ]
          probes: [
            {
              type: 'Liveness'
              httpGet: {
                path: '/health/live'
                port: targetPort
              }
              initialDelaySeconds: 10
              periodSeconds: 10
            }
            {
              type: 'Readiness'
              httpGet: {
                path: '/health/ready'
                port: targetPort
              }
              initialDelaySeconds: 5
              periodSeconds: 5
            }
          ]
        }
      ]
      scale: {
        minReplicas: 1
        maxReplicas: 10
        rules: [
          {
            name: 'http-rule'
            http: {
              metadata: {
                concurrentRequests: '100'
              }
            }
          }
        ]
      }
    }
  }
  identity: {
    type: 'SystemAssigned'
  }
}

output fqdn string = containerApp.properties.configuration.ingress.fqdn

Container Apps Jobs (New!)

Jobs are perfect for batch processing and scheduled tasks:

// container-app-job.bicep
resource job 'Microsoft.App/jobs@2023-05-01' = {
  name: 'data-processing-job'
  location: location
  properties: {
    environmentId: environment.id
    configuration: {
      triggerType: 'Schedule'
      scheduleTriggerConfig: {
        cronExpression: '0 0 * * *'  // Daily at midnight
        parallelism: 1
        replicaCompletionCount: 1
      }
      replicaTimeout: 1800  // 30 minutes
      replicaRetryLimit: 3
      registries: [
        {
          server: 'myregistry.azurecr.io'
          identity: 'system'
        }
      ]
      secrets: [
        {
          name: 'storage-connection'
          value: storageConnectionString
        }
      ]
    }
    template: {
      containers: [
        {
          name: 'processor'
          image: 'myregistry.azurecr.io/data-processor:v1'
          resources: {
            cpu: json('1.0')
            memory: '2Gi'
          }
          env: [
            {
              name: 'STORAGE_CONNECTION'
              secretRef: 'storage-connection'
            }
          ]
        }
      ]
    }
  }
}

Event-Driven Jobs

resource eventJob 'Microsoft.App/jobs@2023-05-01' = {
  name: 'queue-processor-job'
  location: location
  properties: {
    environmentId: environment.id
    configuration: {
      triggerType: 'Event'
      eventTriggerConfig: {
        parallelism: 5
        replicaCompletionCount: 1
        scale: {
          minExecutions: 0
          maxExecutions: 100
          pollingInterval: 30
          rules: [
            {
              name: 'azure-queue'
              type: 'azure-queue'
              metadata: {
                queueName: 'processing-queue'
                queueLength: '10'
              }
              auth: [
                {
                  secretRef: 'storage-connection'
                  triggerParameter: 'connection'
                }
              ]
            }
          ]
        }
      }
      replicaTimeout: 300
      replicaRetryLimit: 2
    }
    template: {
      containers: [
        {
          name: 'worker'
          image: 'myregistry.azurecr.io/queue-worker:v1'
          resources: {
            cpu: json('0.5')
            memory: '1Gi'
          }
        }
      ]
    }
  }
}

Manual Jobs

from azure.mgmt.appcontainers import ContainerAppsAPIClient
from azure.identity import DefaultAzureCredential

client = ContainerAppsAPIClient(
    credential=DefaultAzureCredential(),
    subscription_id=subscription_id
)

# Start a manual job execution
execution = client.jobs.begin_start(
    resource_group_name="container-rg",
    job_name="manual-job",
    body={
        "template": {
            "containers": [
                {
                    "name": "processor",
                    "image": "myregistry.azurecr.io/processor:v1",
                    "env": [
                        {"name": "INPUT_FILE", "value": "data/input.csv"},
                        {"name": "OUTPUT_FILE", "value": "data/output.csv"}
                    ]
                }
            ]
        }
    }
).result()

print(f"Job execution started: {execution.name}")

Advanced Scaling

// Custom scaling rules
scale: {
  minReplicas: 0
  maxReplicas: 30
  rules: [
    {
      name: 'http-scaling'
      http: {
        metadata: {
          concurrentRequests: '50'
        }
      }
    }
    {
      name: 'cpu-scaling'
      custom: {
        type: 'cpu'
        metadata: {
          type: 'Utilization'
          value: '70'
        }
      }
    }
    {
      name: 'memory-scaling'
      custom: {
        type: 'memory'
        metadata: {
          type: 'Utilization'
          value: '80'
        }
      }
    }
    {
      name: 'queue-scaling'
      custom: {
        type: 'azure-servicebus'
        metadata: {
          queueName: 'orders'
          messageCount: '100'
        }
        auth: [
          {
            secretRef: 'sb-connection'
            triggerParameter: 'connection'
          }
        ]
      }
    }
  ]
}

Dapr Integration

// Enable Dapr sidecar
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
  // ... other properties
  properties: {
    configuration: {
      dapr: {
        enabled: true
        appId: 'my-service'
        appPort: 8080
        appProtocol: 'http'
      }
    }
  }
}

// Dapr components
resource stateStore 'Microsoft.App/managedEnvironments/daprComponents@2023-05-01' = {
  parent: environment
  name: 'statestore'
  properties: {
    componentType: 'state.azure.cosmosdb'
    version: 'v1'
    secrets: [
      {
        name: 'cosmos-key'
        value: cosmosKey
      }
    ]
    metadata: [
      {
        name: 'url'
        value: cosmosEndpoint
      }
      {
        name: 'masterKey'
        secretRef: 'cosmos-key'
      }
      {
        name: 'database'
        value: 'mydb'
      }
      {
        name: 'collection'
        value: 'state'
      }
    ]
    scopes: ['my-service']
  }
}

Traffic Splitting

# Deploy new revision
az containerapp update \
    --name my-api \
    --resource-group container-rg \
    --image myregistry.azurecr.io/my-api:v2

# Split traffic 80/20
az containerapp ingress traffic set \
    --name my-api \
    --resource-group container-rg \
    --revision-weight my-api--v1=80 my-api--v2=20

# Full cutover
az containerapp ingress traffic set \
    --name my-api \
    --resource-group container-rg \
    --revision-weight my-api--v2=100

Container Apps provides a powerful, serverless container platform. Tomorrow, I will cover Jobs in Container Apps in more detail.

Resources

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.