05 - Infrastructure as Code with Bicep¶
Use Bicep to define your Azure Container Apps platform consistently across environments. This step focuses on repeatable provisioning and safe updates.
Infrastructure Context
Service: Container Apps (Consumption) | Network: VNet integrated | VNet: ✅
This tutorial assumes a production-ready Container Apps deployment with a custom VNet, ACR with managed identity pull, and private endpoints for backend services.
flowchart TD
INET[Internet] -->|HTTPS| CA["Container App\nConsumption\nLinux Python 3.11"]
subgraph VNET["VNet 10.0.0.0/16"]
subgraph ENV_SUB["Environment Subnet 10.0.0.0/23\nDelegation: Microsoft.App/environments"]
CAE[Container Apps Environment]
CA
end
subgraph PE_SUB["Private Endpoint Subnet 10.0.2.0/24"]
PE_ACR[PE: ACR]
PE_KV[PE: Key Vault]
PE_ST[PE: Storage]
end
end
PE_ACR --> ACR[Azure Container Registry]
PE_KV --> KV[Key Vault]
PE_ST --> ST[Storage Account]
subgraph DNS[Private DNS Zones]
DNS_ACR[privatelink.azurecr.io]
DNS_KV[privatelink.vaultcore.azure.net]
DNS_ST[privatelink.blob.core.windows.net]
end
PE_ACR -.-> DNS_ACR
PE_KV -.-> DNS_KV
PE_ST -.-> DNS_ST
CA -.->|System-Assigned MI| ENTRA[Microsoft Entra ID]
CAE --> LOG[Log Analytics]
CA --> AI[Application Insights]
style CA fill:#107c10,color:#fff
style VNET fill:#E8F5E9,stroke:#4CAF50
style DNS fill:#E3F2FD Infrastructure Lifecycle¶
graph LR
WRITE[Write Bicep] --> VAL[Validate]
VAL --> WHAT[What-If]
WHAT --> DEPLOY[Deploy]
DEPLOY --> VERIFY[Verify Outputs] Prerequisites¶
- Completed 04 - Logging, Monitoring, and Observability
- Bicep files under
infra/
Run validate and what-if before every apply
Treat az deployment group validate and az deployment group what-if as required safety checks to prevent accidental production-impacting infrastructure changes.
Step-by-step¶
- Set standard variables
- Validate the Bicep template
az deployment group validate \
--resource-group "$RG" \
--template-file infra/main.bicep \
--parameters baseName="$BASE_NAME" location="$LOCATION"
???+ example "Expected output"
- Preview changes with what-if
az deployment group what-if \
--resource-group "$RG" \
--template-file infra/main.bicep \
--parameters baseName="$BASE_NAME" location="$LOCATION"
???+ example "Expected output"
Resource and property changes are indicated with these symbols:
+ Create
~ Modify
The deployment will update the following scope:
Scope: /subscriptions/<subscription-id>/resourceGroups/rg-aca-python-demo
~ Microsoft.App/containerApps/<your-app-name> [2024-03-01]
~ properties.template.containers[0].image: "<acr-name>.azurecr.io/myapp:v1.0.0"
- Deploy infrastructure
az deployment group create \
--name "$DEPLOYMENT_NAME" \
--resource-group "$RG" \
--template-file infra/main.bicep \
--parameters baseName="$BASE_NAME" location="$LOCATION"
???+ example "Expected output"
{
"id": "/subscriptions/<subscription-id>/resourceGroups/rg-aca-python-demo/providers/Microsoft.Resources/deployments/main",
"name": "main",
"properties": {
"provisioningState": "Succeeded",
"outputs": {
"containerAppName": { "type": "String", "value": "<your-app-name>" },
"containerAppEnvName": { "type": "String", "value": "<your-env-name>" },
"containerRegistryName": { "type": "String", "value": "<acr-name>" },
"location": { "type": "String", "value": "koreacentral" }
}
}
}
- Verify outputs and key resources
az deployment group show \
--resource-group "$RG" \
--name "$DEPLOYMENT_NAME" \
--query properties.outputs
???+ example "Expected output"
{
"containerAppName": {
"type": "String",
"value": "<your-app-name>"
},
"containerAppEnvName": {
"type": "String",
"value": "cae-myapp"
},
"containerRegistryName": {
"type": "String",
"value": "<acr-name>"
},
"containerRegistryLoginServer": {
"type": "String",
"value": "<acr-name>.azurecr.io"
}
}
Example Bicep snippet (environment + logs)¶
param baseName string
var uniqueSuffix = uniqueString(resourceGroup().id)
var containerAppEnvName = 'cae-${baseName}-${uniqueSuffix}'
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
name: 'log-${baseName}-${uniqueSuffix}'
location: resourceGroup().location
properties: {
sku: {
name: 'PerGB2018'
}
}
}
resource environment 'Microsoft.App/managedEnvironments@2024-03-01' = {
name: containerAppEnvName
location: resourceGroup().location
properties: {
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalytics.properties.customerId
sharedKey: logAnalytics.listKeys().primarySharedKey
}
}
}
}
Advanced Topics¶
- Split templates into modules (network, observability, apps, identity).
- Use parameter files per environment (dev, test, prod).
- Provision Dapr components declaratively with managed identities.
Avoid out-of-band portal edits
Manual portal changes can create drift from your Bicep templates. Prefer template updates and redeployment so environments remain reproducible and auditable.
CLI Alternative (No Bicep)¶
Use these commands when you need an imperative deployment path without Bicep.
Step 1: Set variables¶
RG="rg-flask-containerapp"
APP_NAME="ca-flask-demo"
BASE_NAME="flask-app"
ENVIRONMENT_NAME="cae-flask-demo"
ACR_NAME="crflaskdemo"
LOG_NAME="log-flask-demo"
LOCATION="koreacentral"
Step 2: Create resource group and Log Analytics workspace¶
az group create --name "$RG" --location "$LOCATION"
az monitor log-analytics workspace create --resource-group "$RG" --workspace-name "$LOG_NAME" --location "$LOCATION"
LOG_ID=$(az monitor log-analytics workspace show --resource-group "$RG" --workspace-name "$LOG_NAME" --query customerId --output tsv)
Expected output
Step 3: Create ACR and Container Apps environment¶
az acr create --resource-group "$RG" --name "$ACR_NAME" --sku Basic
az containerapp env create --resource-group "$RG" --name "$ENVIRONMENT_NAME" --location "$LOCATION" --logs-workspace-id "$LOG_ID"
az acr build --registry "$ACR_NAME" --image "$BASE_NAME:v1" ./apps/python
Expected output
Step 4: Create Container App with environment variables¶
az containerapp create --resource-group "$RG" --name "$APP_NAME" --environment "$ENVIRONMENT_NAME" --image "$ACR_NAME.azurecr.io/$BASE_NAME:v1" --target-port 8000 --ingress external --env-vars FLASK_ENV=production --query "properties.configuration.ingress.fqdn"
Step 5: Validate configuration¶
az containerapp show --resource-group "$RG" --name "$APP_NAME" --query "{state:properties.provisioningState,fqdn:properties.configuration.ingress.fqdn,env:properties.template.containers[0].env}"