Azure Container Apps gives you containers without managing Kubernetes. Great for APIs, background workers, and event-driven apps. Here's how to deploy a Python application.
When to Use Container Apps
Good fit:
- HTTP APIs and web apps
- Background processing jobs
- Event-driven microservices
- Apps that need to scale to zero
Consider AKS instead for:
- Complex multi-service deployments
- Need for Kubernetes-specific features
- Existing Kubernetes expertise
Project Structure
my-app/
├── Dockerfile
├── requirements.txt
├── app/
│ ├── __init__.py
│ └── main.py
└── .dockerignore
Dockerfile for Python
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app/ ./app/
# Create non-root user
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
# Expose port
EXPOSE 8000
# Run with Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "app.main:app"]
requirements.txt
fastapi==0.104.1
gunicorn==21.2.0
uvicorn==0.24.0
Simple FastAPI App
# app/main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"status": "healthy"}
@app.get("/api/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
Terraform Deployment
# Container Apps Environment
resource "azurerm_container_app_environment" "this" {
name = "cae-production"
location = azurerm_resource_group.this.location
resource_group_name = azurerm_resource_group.this.name
log_analytics_workspace_id = azurerm_log_analytics_workspace.this.id
}
# Container Registry
resource "azurerm_container_registry" "this" {
name = "crproduction"
resource_group_name = azurerm_resource_group.this.name
location = azurerm_resource_group.this.location
sku = "Basic"
admin_enabled = true
}
# Container App
resource "azurerm_container_app" "api" {
name = "ca-api"
container_app_environment_id = azurerm_container_app_environment.this.id
resource_group_name = azurerm_resource_group.this.name
revision_mode = "Single"
template {
container {
name = "api"
image = "${azurerm_container_registry.this.login_server}/api:latest"
cpu = 0.5
memory = "1Gi"
env {
name = "ENVIRONMENT"
value = "production"
}
env {
name = "DATABASE_URL"
secret_name = "database-url"
}
}
min_replicas = 0
max_replicas = 10
}
secret {
name = "database-url"
value = var.database_connection_string
}
secret {
name = "registry-password"
value = azurerm_container_registry.this.admin_password
}
registry {
server = azurerm_container_registry.this.login_server
username = azurerm_container_registry.this.admin_username
password_secret_name = "registry-password"
}
ingress {
external_enabled = true
target_port = 8000
transport = "auto"
traffic_weight {
percentage = 100
latest_revision = true
}
}
}
Build and Push with Azure CLI
# Build and push to ACR
az acr build --registry crproduction \
--image api:latest \
--file Dockerfile .
DevOps Pipeline
trigger:
branches:
include:
- main
paths:
include:
- 'src/**'
- 'Dockerfile'
pool:
vmImage: 'ubuntu-latest'
variables:
acrName: 'crproduction'
imageName: 'api'
stages:
- stage: Build
jobs:
- job: BuildAndPush
steps:
- task: AzureCLI@2
displayName: 'Build and Push Image'
inputs:
azureSubscription: 'your-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az acr build --registry $(acrName) \
--image $(imageName):$(Build.BuildId) \
--image $(imageName):latest \
--file Dockerfile .
- stage: Deploy
dependsOn: Build
jobs:
- job: DeployToContainerApps
steps:
- task: AzureCLI@2
displayName: 'Update Container App'
inputs:
azureSubscription: 'your-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az containerapp update \
--name ca-api \
--resource-group rg-production \
--image $(acrName).azurecr.io/$(imageName):$(Build.BuildId)
Persistent Storage with Azure Files
For apps needing persistent storage:
resource "azurerm_container_app_environment_storage" "data" {
name = "data-storage"
container_app_environment_id = azurerm_container_app_environment.this.id
account_name = azurerm_storage_account.this.name
share_name = azurerm_storage_share.data.name
access_key = azurerm_storage_account.this.primary_access_key
access_mode = "ReadWrite"
}
resource "azurerm_container_app" "worker" {
# ... other config ...
template {
container {
name = "worker"
image = "${azurerm_container_registry.this.login_server}/worker:latest"
cpu = 0.5
memory = "1Gi"
volume_mounts {
name = "data"
path = "/data"
}
}
volume {
name = "data"
storage_name = azurerm_container_app_environment_storage.data.name
storage_type = "AzureFile"
}
}
}
Scaling Configuration
template {
min_replicas = 1
max_replicas = 10
# Scale on HTTP requests
http_scale_rule {
name = "http-scaling"
concurrent_requests = 100
}
# Or scale on queue messages
azure_queue_scale_rule {
name = "queue-scaling"
queue_name = "work-items"
queue_length = 20
authentication {
secret_name = "queue-connection"
trigger_parameter = "connection"
}
}
}
Health Probes
template {
container {
# ... other config ...
liveness_probe {
transport = "HTTP"
path = "/health"
port = 8000
}
readiness_probe {
transport = "HTTP"
path = "/ready"
port = 8000
}
}
}
Need help containerising your applications? Get in touch - we help organisations modernise their application deployments.