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¶
-
Create the Premium IaC folder.
-
Create
infra/premium/main.bicepwith 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' } } -
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' ] } } ] } } -
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 matchesinfra/premium/main.bicep.Premium uses classic
siteConfig.appSettingsand does not usefunctionAppConfig. -
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.bicepuses abaseNameparameter and shared modules (infra/modules/). Use the deployment command in this section to deploy the repository template directly. -
Verify Premium resources and SKU.
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¶
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
B-9: Private DNS zones and VNet links (storage)¶
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
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¶
- Tutorial Overview & Plan Chooser
- Python Language Guide
- Platform: Hosting Plans
- Operations: Deployment
- Recipes Index