Skip to content

05 - Infrastructure as Code (Premium)

Deploy Azure Functions Premium infrastructure with Bicep from infra/premium/main.bicep, including Elastic Premium plan settings, VNet integration, private endpoints, identity-based host storage, and Azure Files content share.

Prerequisites

  • You completed 04 - Logging and Monitoring.
  • You exported $RG, $APP_NAME, $PLAN_NAME, $STORAGE_NAME, $LOCATION.
  • Azure CLI is authenticated to your target subscription.

What You'll Build

  • A Premium (EP1) Function App infrastructure model defined with Bicep.
  • VNet integration, private endpoints, and private DNS links for core dependencies.
  • Identity-based host storage settings plus Azure Files content-share configuration.

Infrastructure Context

Plan: Premium (EP1) | Network: VNet + Private Endpoints | Always warm: ✅

Premium deploys with VNet integration (delegated subnet), a private endpoint for inbound access, private DNS zone, and pre-warmed instances. Storage uses connection string or identity-based authentication.

flowchart TD
    INET[Internet] -->|HTTPS| FA[Function App\nPremium EP1\nLinux Python 3.11]

    subgraph VNET["VNet 10.0.0.0/16"]
        subgraph INT_SUB["Integration Subnet 10.0.1.0/24\nDelegation: Microsoft.Web/serverFarms"]
            FA
        end
        subgraph PE_SUB["Private Endpoint Subnet 10.0.2.0/24"]
            PE_BLOB[PE: blob]
            PE_QUEUE[PE: queue]
            PE_TABLE[PE: table]
            PE_FILE[PE: file]
        end
    end

    PE_BLOB --> ST["Storage Account\nallowPublicAccess: false\nallowSharedKeyAccess: true"]
    PE_QUEUE --> ST
    PE_TABLE --> ST
    PE_FILE --> ST

    subgraph DNS[Private DNS Zones]
        DNS_BLOB[privatelink.blob.core.windows.net]
        DNS_QUEUE[privatelink.queue.core.windows.net]
        DNS_TABLE[privatelink.table.core.windows.net]
        DNS_FILE[privatelink.file.core.windows.net]
    end

    PE_BLOB -.-> DNS_BLOB
    PE_QUEUE -.-> DNS_QUEUE
    PE_TABLE -.-> DNS_TABLE
    PE_FILE -.-> DNS_FILE

    FA -.->|System-Assigned MI| ENTRA[Microsoft Entra ID]
    FA --> AI[Application Insights]

    subgraph STORAGE[Content Backend]
        SHARE[Azure Files\ncontent share]
    end
    ST --- SHARE

    WARM["🔥 Pre-warmed instances\nMin: 1, Max: 20-100"] -.- FA

    style FA fill:#ff8c00,color:#fff
    style VNET fill:#E8F5E9,stroke:#4CAF50
    style ST fill:#FFF3E0
    style DNS fill:#E3F2FD
    style WARM fill:#FFF3E0,stroke:#FF9800
flowchart LR
    A[Bicep deployment] --> B[Premium plan and Function App]
    B --> C[Managed identity host storage]
    B --> D[Azure Files content share]
    B --> E[VNet integration and private endpoints]

Steps

  1. Create the Premium IaC folder.

    mkdir --parents "infra/premium"
    
  2. Create infra/premium/main.bicep with Premium plan and storage resources.

    targetScope = 'resourceGroup'
    
    param location string = resourceGroup().location
    param planName string
    param appName string
    param storageName string
    
    resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
      name: planName
      location: location
      sku: {
        name: 'EP1'
        tier: 'ElasticPremium'
      }
      kind: 'elastic'
      properties: {
        reserved: true
      }
    }
    
    resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
      name: storageName
      location: location
      sku: {
        name: 'Standard_LRS'
      }
      kind: 'StorageV2'
      properties: {
        supportsHttpsTrafficOnly: true
      }
    }
    
    resource fileService 'Microsoft.Storage/storageAccounts/fileServices@2023-01-01' = {
      name: '${storage.name}/default'
    }
    
    resource contentShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2023-01-01' = {
      name: '${storage.name}/default/${toLower(appName)}'
      properties: {
        accessTier: 'TransactionOptimized'
      }
    }
    
  3. Add VNet, delegated subnet, and private endpoint resources.

    resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
      name: 'vnet-premium-demo'
      location: location
      properties: {
        addressSpace: {
          addressPrefixes: [
            '10.20.0.0/16'
          ]
        }
        subnets: [
          {
            name: 'snet-integration'
            properties: {
              addressPrefix: '10.20.1.0/24'
              delegations: [
                {
                  name: 'web-farms'
                  properties: {
                    serviceName: 'Microsoft.Web/serverFarms'
                  }
                }
              ]
            }
          }
          {
            name: 'snet-private-endpoints'
            properties: {
              addressPrefix: '10.20.2.0/24'
              privateEndpointNetworkPolicies: 'Disabled'
            }
          }
        ]
      }
    }
    
    resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-09-01' = {
      name: 'pe-${appName}'
      location: location
      properties: {
        subnet: {
          id: resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, 'snet-private-endpoints')
        }
        privateLinkServiceConnections: [
          {
            name: 'conn-${appName}'
            properties: {
              privateLinkServiceId: functionApp.id
              groupIds: [
                'sites'
              ]
            }
          }
        ]
      }
    }
    
  4. Add Function App configuration with identity-based host storage.

    resource functionApp 'Microsoft.Web/sites@2024-04-01' = {
      name: appName
      location: location
      kind: 'functionapp,linux'
      properties: {
        serverFarmId: plan.id
        httpsOnly: true
        virtualNetworkSubnetId: resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, 'snet-integration')
        siteConfig: {
          linuxFxVersion: 'Python|3.11'
          minTlsVersion: '1.2'
          ftpsState: 'Disabled'
          appSettings: [
            {
              name: 'FUNCTIONS_EXTENSION_VERSION'
              value: '~4'
            }
            {
              name: 'FUNCTIONS_WORKER_RUNTIME'
              value: 'python'
            }
            {
              name: 'WEBSITE_CONTENTSHARE'
              value: toLower(appName)
            }
            {
              name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
              value: 'DefaultEndpointsProtocol=https;AccountName=${storageName};AccountKey=<resolved-at-deploy>;EndpointSuffix=core.windows.net'
            }
            {
              name: 'AzureWebJobsStorage__accountName'
              value: storageName
            }
            {
              name: 'AzureWebJobsStorage__credential'
              value: 'managedidentity'
            }
            {
              name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
              value: '<resolved-at-deploy>'
            }
          ]
        }
      }
    }
    

    Connection strings vs identity-based storage

    The repository Premium template uses identity-based host storage (AzureWebJobsStorage__accountName + AzureWebJobsStorage__credential=managedidentity) and Azure Files for content share deployment (WEBSITE_CONTENTSHARE + WEBSITE_CONTENTAZUREFILECONNECTIONSTRING). This matches infra/premium/main.bicep.

    Premium uses classic siteConfig.appSettings and does not use functionAppConfig.

  5. Deploy the template.

    az group create \
      --name "$RG" \
      --location "$LOCATION"
    
    az deployment group create \
      --resource-group "$RG" \
      --name "premium-main" \
      --template-file "infra/premium/main.bicep" \
      --parameters \
        baseName="premdemo" \
        location="$LOCATION"
    

    Tutorial snippet vs actual infra/

    The Bicep snippets above are simplified for learning. The actual infra/premium/main.bicep uses a baseName parameter and shared modules (infra/modules/). Use the deployment command in this section to deploy the repository template directly.

  6. Verify Premium resources and SKU.

    az resource list \
      --resource-group "$RG" \
      --output table
    
    az functionapp plan show \
      --name "$PLAN_NAME" \
      --resource-group "$RG" \
      --query "{sku:sku.name,tier:sku.tier,maxWorkers:maximumElasticWorkerCount}" \
      --output json
    

Option B: Deploy with Azure CLI (No Bicep)

Use this sequence when you want to provision the same Premium architecture directly with Azure CLI commands.

export VNET_NAME="vnet-premium-demo"
export CONTENT_SHARE_NAME="${APP_NAME}-content"
export APPINSIGHTS_NAME="appi-${APP_NAME}"

B-1: Resource group and storage account

az group create \
  --name "$RG" \
  --location "$LOCATION"

az storage account create \
  --name "$STORAGE_NAME" \
  --resource-group "$RG" \
  --location "$LOCATION" \
  --sku Standard_LRS \
  --kind StorageV2 \
  --allow-blob-public-access false

B-2: Azure Files content share

az storage share-rm create \
  --storage-account "$STORAGE_NAME" \
  --resource-group "$RG" \
  --name "$CONTENT_SHARE_NAME" \
  --access-tier TransactionOptimized

B-3: Premium plan (EP1) and Function App

az functionapp plan create \
  --name "$PLAN_NAME" \
  --resource-group "$RG" \
  --location "$LOCATION" \
  --sku EP1 \
  --is-linux

az functionapp create \
  --name "$APP_NAME" \
  --resource-group "$RG" \
  --plan "$PLAN_NAME" \
  --storage-account "$STORAGE_NAME" \
  --runtime python \
  --runtime-version 3.11 \
  --functions-version 4 \
  --os-type Linux

B-4: Enable system-assigned managed identity

az functionapp identity assign \
  --name "$APP_NAME" \
  --resource-group "$RG"

B-5: RBAC role assignments

export PRINCIPAL_ID=$(az functionapp identity show \
  --name "$APP_NAME" \
  --resource-group "$RG" \
  --query principalId \
  --output tsv)

export STORAGE_ID=$(az storage account show \
  --name "$STORAGE_NAME" \
  --resource-group "$RG" \
  --query id \
  --output tsv)

for ROLE in \
  "Storage Blob Data Owner" \
  "Storage Account Contributor" \
  "Storage Queue Data Contributor" \
  "Storage File Data Privileged Contributor"
do
  az role assignment create \
    --assignee-object-id "$PRINCIPAL_ID" \
    --assignee-principal-type ServicePrincipal \
    --role "$ROLE" \
    --scope "$STORAGE_ID"
done

B-6: VNet and subnets

az network vnet create \
  --resource-group "$RG" \
  --name "$VNET_NAME" \
  --location "$LOCATION" \
  --address-prefixes "10.20.0.0/16" \
  --subnet-name "snet-integration" \
  --subnet-prefixes "10.20.1.0/24"

az network vnet subnet update \
  --resource-group "$RG" \
  --vnet-name "$VNET_NAME" \
  --name "snet-integration" \
  --delegations Microsoft.Web/serverFarms

az network vnet subnet create \
  --resource-group "$RG" \
  --vnet-name "$VNET_NAME" \
  --name "snet-private-endpoints" \
  --address-prefixes "10.20.2.0/24" \
  --disable-private-endpoint-network-policies true

B-7: VNet integration

az functionapp vnet-integration add \
  --name "$APP_NAME" \
  --resource-group "$RG" \
  --vnet "$VNET_NAME" \
  --subnet "snet-integration"

B-8: Storage private endpoints (blob, queue, table, file)

for SUBRESOURCE in blob queue table file
do
  az network private-endpoint create \
    --name "pe-${STORAGE_NAME}-${SUBRESOURCE}" \
    --resource-group "$RG" \
    --location "$LOCATION" \
    --vnet-name "$VNET_NAME" \
    --subnet "snet-private-endpoints" \
    --private-connection-resource-id "$STORAGE_ID" \
    --group-ids "$SUBRESOURCE" \
    --connection-name "conn-${STORAGE_NAME}-${SUBRESOURCE}"
done
export VNET_ID=$(az network vnet show \
  --resource-group "$RG" \
  --name "$VNET_NAME" \
  --query id \
  --output tsv)

for ZONE in \
  "privatelink.blob.core.windows.net" \
  "privatelink.queue.core.windows.net" \
  "privatelink.table.core.windows.net" \
  "privatelink.file.core.windows.net"
do
  az network private-dns zone create \
    --resource-group "$RG" \
    --name "$ZONE"

  az network private-dns link vnet create \
    --resource-group "$RG" \
    --zone-name "$ZONE" \
    --name "link-${VNET_NAME}-${ZONE%%.*}" \
    --virtual-network "$VNET_ID" \
    --registration-enabled false
done

for SUBRESOURCE in blob queue table file
do
  ZONE="privatelink.${SUBRESOURCE}.core.windows.net"
  ZONE_ID=$(az network private-dns zone show \
    --resource-group "$RG" \
    --name "$ZONE" \
    --query id \
    --output tsv)

  az network private-endpoint dns-zone-group create \
    --resource-group "$RG" \
    --endpoint-name "pe-${STORAGE_NAME}-${SUBRESOURCE}" \
    --name "zg-${SUBRESOURCE}" \
    --private-dns-zone "$ZONE_ID" \
    --zone-name "$ZONE"
done

B-10: Site private endpoint (inbound private access)

export APP_ID=$(az functionapp show \
  --name "$APP_NAME" \
  --resource-group "$RG" \
  --query id \
  --output tsv)

az network private-endpoint create \
  --name "pe-${APP_NAME}-sites" \
  --resource-group "$RG" \
  --location "$LOCATION" \
  --vnet-name "$VNET_NAME" \
  --subnet "snet-private-endpoints" \
  --private-connection-resource-id "$APP_ID" \
  --group-ids sites \
  --connection-name "conn-${APP_NAME}-sites"

B-11: Site private DNS zone (privatelink.azurewebsites.net)

az network private-dns zone create \
  --resource-group "$RG" \
  --name "privatelink.azurewebsites.net"

az network private-dns link vnet create \
  --resource-group "$RG" \
  --zone-name "privatelink.azurewebsites.net" \
  --name "link-${VNET_NAME}-azurewebsites" \
  --virtual-network "$VNET_ID" \
  --registration-enabled false

export SITE_ZONE_ID=$(az network private-dns zone show \
  --resource-group "$RG" \
  --name "privatelink.azurewebsites.net" \
  --query id \
  --output tsv)

az network private-endpoint dns-zone-group create \
  --resource-group "$RG" \
  --endpoint-name "pe-${APP_NAME}-sites" \
  --name "zg-sites" \
  --private-dns-zone "$SITE_ZONE_ID" \
  --zone-name "privatelink.azurewebsites.net"

B-12: App settings (identity-based storage + content share connection string)

export STORAGE_CONN=$(az storage account show-connection-string \
  --name "$STORAGE_NAME" \
  --resource-group "$RG" \
  --output tsv)

az functionapp config appsettings set \
  --name "$APP_NAME" \
  --resource-group "$RG" \
  --settings \
    "FUNCTIONS_WORKER_RUNTIME=python" \
    "AzureWebJobsStorage__accountName=$STORAGE_NAME" \
    "AzureWebJobsStorage__credential=managedidentity" \
    "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING=$STORAGE_CONN" \
    "WEBSITE_CONTENTSHARE=$CONTENT_SHARE_NAME" \
    "APPLICATIONINSIGHTS_CONNECTION_STRING=$APPINSIGHTS_CONN"

Premium host storage + content share requirements

Premium requires both identity-based storage (for AzureWebJobsStorage) AND a connection string (for WEBSITE_CONTENTAZUREFILECONNECTIONSTRING). The content share connection string uses shared key access.

B-13: Application Insights

az monitor app-insights component create \
  --app "$APPINSIGHTS_NAME" \
  --location "$LOCATION" \
  --resource-group "$RG" \
  --application-type web

export APPINSIGHTS_CONN=$(az monitor app-insights component show \
  --app "$APPINSIGHTS_NAME" \
  --resource-group "$RG" \
  --query connectionString \
  --output tsv)

az functionapp config appsettings set \
  --name "$APP_NAME" \
  --resource-group "$RG" \
  --settings "APPLICATIONINSIGHTS_CONNECTION_STRING=$APPINSIGHTS_CONN"

B-14: Validate resources

az resource list \
  --resource-group "$RG" \
  --output table

az functionapp plan show \
  --name "$PLAN_NAME" \
  --resource-group "$RG" \
  --query "{sku:sku.name,tier:sku.tier,maxWorkers:maximumElasticWorkerCount}" \
  --output json

az network private-endpoint list \
  --resource-group "$RG" \
  --query "[].name" \
  --output table

Verification

Name                              ResourceType
--------------------------------  --------------------------------------
plan-premium-demo                 Microsoft.Web/serverfarms
func-premium-demo                 Microsoft.Web/sites
stpremdemo123                     Microsoft.Storage/storageAccounts
vnet-premium-demo                 Microsoft.Network/virtualNetworks
pe-func-premium-demo              Microsoft.Network/privateEndpoints
{
  "sku": "EP1",
  "tier": "ElasticPremium",
  "maxWorkers": 20
}

maximumElasticWorkerCount default

Without the --max-burst flag, az functionapp plan create sets maximumElasticWorkerCount to 1, not 100. To set a higher value, use the Bicep template or update manually via az functionapp plan update --max-burst 20.

Next Steps

Next: 06 - CI/CD

See Also

Sources