Skip to content

05 - Infrastructure as Code with Bicep

Use Bicep to define your .NET application infrastructure consistently across environments. This step focuses on repeatable provisioning and safe updates of Azure Container Apps resources.

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 .NET 8"]

    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

Naming Convention

The shared infra/main.bicep template generates unique resource names using uniqueString(resourceGroup().id) (e.g., ca-dotnet-guide-abc123def). Earlier tutorials in this guide use simplified names like ca-dotnet-guide for readability. When deploying via Bicep, always capture actual names from deployment outputs using az deployment group show.

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

  1. Set standard variables
RG="rg-dotnet-guide"
BASE_NAME="dotnet-guide"
LOCATION="koreacentral"
DEPLOYMENT_NAME="main"
  1. 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"

{
  "status": "Succeeded",
  "error": null
}

  1. 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-dotnet-guide

  ~ Microsoft.App/containerApps/<your-app-name> [2024-03-01]
    ~ properties.template.containers[0].image: "<acr-name>.azurecr.io/dotnet-guide:latest"

  1. 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-dotnet-guide/providers/Microsoft.Resources/deployments/main",
  "name": "main",
  "properties": {
    "provisioningState": "Succeeded",
    "outputs": {
      "containerAppName": { "type": "String", "value": "ca-dotnet-guide-<unique-suffix>" },
      "containerAppUrl": { "type": "String", "value": "https://ca-dotnet-guide-<unique-suffix>.<env-suffix>.koreacentral.azurecontainerapps.io" }
    }
  }
}

   !!! note "Unique suffix"
       The `<unique-suffix>` is generated by `uniqueString(resourceGroup().id)` in Bicep to ensure globally unique resource names.
  1. 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": "ca-dotnet-guide-<unique-suffix>"
  },
  "containerAppEnvName": {
    "type": "String",
    "value": "cae-dotnet-guide-<unique-suffix>"
  },
  "containerRegistryName": {
    "type": "String",
    "value": "crdotnetguide<unique-suffix>"
  },
  "containerAppUrl": {
    "type": "String",
    "value": "https://ca-dotnet-guide-<unique-suffix>.<env-suffix>.koreacentral.azurecontainerapps.io"
  }
}

Example Bicep snippet (.NET App with Health Probes)

resource containerApp 'Microsoft.App/containerApps@2024-03-01' = {
  name: 'ca-${baseName}'
  location: location
  properties: {
    managedEnvironmentId: environment.id
    configuration: {
      ingress: {
        external: true
        targetPort: 8000
      }
    }
    template: {
      containers: [
        {
          name: 'app'
          image: '${acr.properties.loginServer}/${imageName}'
          probes: [
            {
              type: 'Liveness'
              httpGet: {
                path: '/health'
                port: 8000
              }
              initialDelaySeconds: 5
              periodSeconds: 10
            }
            {
              type: 'Readiness'
              httpGet: {
                path: '/health'
                port: 8000
              }
              initialDelaySeconds: 5
              periodSeconds: 10
            }
          ]
        }
      ]
    }
  }
}

Advanced Topics

  • Modular Bicep: Split your templates into reusable modules for networking, storage, and identity.
  • Deployment Scripts: Use Microsoft.Resources/deploymentScripts to perform post-deployment tasks like database migrations.
  • Resource Locking: Apply Microsoft.Authorization/locks to prevent accidental deletion of critical infrastructure.

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-dotnet-containerapp"
LOCATION="koreacentral"
APP_NAME="ca-dotnet-demo"
BASE_NAME="dotnet-app"
ENVIRONMENT_NAME="cae-dotnet-demo"
ACR_NAME="crdotnetdemo"
LOG_NAME="log-dotnet-demo"
Expected output
Variables exported for resource group, workspace, registry, environment, and app.

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
Expected output
{
  "name": "rg-dotnet-containerapp",
  "properties": {
    "provisioningState": "Succeeded"
  }
}
{
  "name": "log-dotnet-demo",
  "customerId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
  "id": "/subscriptions/<subscription-id>/resourceGroups/rg-dotnet-containerapp/providers/Microsoft.OperationalInsights/workspaces/log-dotnet-demo"
}

Step 3: Create ACR and Container Apps environment

az acr create --resource-group $RG --name $ACR_NAME --sku Basic
LOG_ID=$(az monitor log-analytics workspace show --resource-group $RG --workspace-name $LOG_NAME --query customerId --output tsv)
LOG_KEY=$(az monitor log-analytics workspace get-shared-keys --resource-group $RG --workspace-name $LOG_NAME --query primarySharedKey --output tsv)
az containerapp env create --resource-group $RG --name $ENVIRONMENT_NAME --location $LOCATION --logs-workspace-id $LOG_ID --logs-workspace-key $LOG_KEY
Expected output
{
  "name": "crdotnetdemo",
  "loginServer": "crdotnetdemo.azurecr.io",
  "provisioningState": "Succeeded"
}
{
  "name": "cae-dotnet-demo",
  "id": "/subscriptions/<subscription-id>/resourceGroups/rg-dotnet-containerapp/providers/Microsoft.App/managedEnvironments/cae-dotnet-demo",
  "provisioningState": "Succeeded"
}

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 ASPNETCORE_ENVIRONMENT=Production --query "properties.configuration.ingress.fqdn"
Expected output
"ca-dotnet-demo.mistyfield-1a2b3c4d.koreacentral.azurecontainerapps.io"

Step 5: Validate configuration

az containerapp show --resource-group $RG --name $APP_NAME --query "{fqdn:properties.configuration.ingress.fqdn,targetPort:properties.configuration.ingress.targetPort,environmentVariables:properties.template.containers[0].env}"
Expected output
{
  "environmentVariables": [
    {
      "name": "ASPNETCORE_ENVIRONMENT",
      "value": "Production"
    }
  ],
  "fqdn": "ca-dotnet-demo.mistyfield-1a2b3c4d.koreacentral.azurecontainerapps.io",
  "targetPort": 8000
}

See Also

Sources