Skip to content

02 - First Deploy (Premium)

Deploy a Python Function App to an Elastic Premium plan (EP1) with VNet integration and private endpoint support, then publish code and verify the app is live.

Prerequisites

  • You completed 01 - Run Locally.
  • You are signed in to Azure CLI and have Contributor access.
  • You already exported: $RG, $APP_NAME, $PLAN_NAME, $STORAGE_NAME, $LOCATION (use koreacentral for this guide).

What You'll Build

  • A Linux Python Function App on Elastic Premium (EP1) with runtime settings.
  • VNet integration and a site private endpoint for private inbound access.
  • A first deployment pipeline (func azure functionapp publish) and endpoint verification.

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

Steps

  1. Authenticate and set subscription context.

    az login
    az account set --subscription "<subscription-id>"
    

    Expected output (abridged):

    [
      {
        "name": "<account-name>",
        "tenantId": "<tenant-id>",
        "id": "<subscription-id>",
        "isDefault": true
      }
    ]
    
    {
      "id": "<subscription-id>",
      "name": "<subscription-name>",
      "state": "Enabled"
    }
    
  2. Create 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
    

    Expected output (abridged):

    {
      "name": "<resource-group-name>",
      "location": "koreacentral",
      "properties": {
        "provisioningState": "Succeeded"
      }
    }
    
    {
      "name": "<storage-account-name>",
      "location": "koreacentral",
      "kind": "StorageV2",
      "sku": {
        "name": "Standard_LRS"
      },
      "allowBlobPublicAccess": false,
      "provisioningState": "Succeeded"
    }
    
  3. Create the Premium plan and Function App (Linux example).

    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"
    

Globally unique names required

Both $APP_NAME and $STORAGE_NAME must be globally unique across all Azure subscriptions. If you get a naming conflict, append a random suffix (e.g., func-prem-04091234).

Expected output (abridged):

{
  "name": "<plan-name>",
  "location": "koreacentral",
  "sku": {
    "name": "EP1",
    "tier": "ElasticPremium"
  },
  "provisioningState": "Succeeded"
}
{
  "name": "<function-app-name>",
  "state": "Running",
  "kind": "functionapp,linux",
  "defaultHostName": "<function-app-name>.azurewebsites.net"
}

Enterprise policy: Shared key access

Some enterprise subscriptions enforce Azure Policy that sets allowSharedKeyAccess: false on all storage accounts. Premium (EP1) requires WEBSITE_CONTENTAZUREFILECONNECTIONSTRING with a connection string that uses shared key access to create the content file share during provisioning. If your subscription has this policy, the Function App creation will fail with a 403 error. Solutions:

  • Request a policy exemption from your Azure administrator
  • Use Flex Consumption (FC1) which supports identity-based blob storage without shared keys
  • Use Dedicated (B1) which uses WEBSITE_RUN_FROM_PACKAGE without a content file share
  1. Configure app settings using classic siteConfig.appSettings model values.

    az functionapp config appsettings set \
      --name "$APP_NAME" \
      --resource-group "$RG" \
      --settings \
        "FUNCTIONS_WORKER_RUNTIME=python" \
        "AzureWebJobsStorage__accountName=$STORAGE_NAME" \
        "AzureWebJobsStorage__credential=managedidentity"
    

    Expected output (abridged):

    [
      {
        "name": "FUNCTIONS_WORKER_RUNTIME",
        "value": "python"
      },
      {
        "name": "AzureWebJobsStorage__accountName",
        "value": "<storage-account-name>"
      },
      {
        "name": "AzureWebJobsStorage__credential",
        "value": "managedidentity"
      }
    ]
    

    For Premium, both host-storage models are valid: - Connection string: AzureWebJobsStorage=<connection-string> - Identity-based: AzureWebJobsStorage__accountName=<storage-account-name> plus AzureWebJobsStorage__credential=managedidentity

  2. Enable a system-assigned managed identity for the Function App.

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

    Expected output (abridged):

    {
      "type": "SystemAssigned",
      "principalId": "<object-id>",
      "tenantId": "<tenant-id>"
    }
    
    <object-id>
    
  3. Assign storage RBAC roles to the managed identity.

    export STORAGE_ID=$(az storage account show \
      --name "$STORAGE_NAME" \
      --resource-group "$RG" \
      --query "id" \
      --output tsv)
    
    az role assignment create \
      --assignee "$MI_PRINCIPAL_ID" \
      --role "Storage Blob Data Owner" \
      --scope "$STORAGE_ID"
    
    az role assignment create \
      --assignee "$MI_PRINCIPAL_ID" \
      --role "Storage Account Contributor" \
      --scope "$STORAGE_ID"
    
    az role assignment create \
      --assignee "$MI_PRINCIPAL_ID" \
      --role "Storage Queue Data Contributor" \
      --scope "$STORAGE_ID"
    
    az role assignment create \
      --assignee "$MI_PRINCIPAL_ID" \
      --role "Storage File Data Privileged Contributor" \
      --scope "$STORAGE_ID"
    

    Expected output (abridged):

    /subscriptions/<subscription-id>/resourceGroups/<resource-group-name>/providers/Microsoft.Storage/storageAccounts/<storage-account-name>
    
    {
      "principalId": "<object-id>",
      "roleDefinitionName": "Storage Blob Data Owner",
      "scope": "/subscriptions/<subscription-id>/resourceGroups/<resource-group-name>/providers/Microsoft.Storage/storageAccounts/<storage-account-name>"
    }
    
    {
      "principalId": "<object-id>",
      "roleDefinitionName": "Storage Account Contributor",
      "scope": "/subscriptions/<subscription-id>/resourceGroups/<resource-group-name>/providers/Microsoft.Storage/storageAccounts/<storage-account-name>"
    }
    
    {
      "principalId": "<object-id>",
      "roleDefinitionName": "Storage Queue Data Contributor",
      "scope": "/subscriptions/<subscription-id>/resourceGroups/<resource-group-name>/providers/Microsoft.Storage/storageAccounts/<storage-account-name>"
    }
    
    {
      "principalId": "<object-id>",
      "roleDefinitionName": "Storage File Data Privileged Contributor",
      "scope": "/subscriptions/<subscription-id>/resourceGroups/<resource-group-name>/providers/Microsoft.Storage/storageAccounts/<storage-account-name>"
    }
    

    Why these four roles are required on Premium

    Premium Function Apps need host storage and Azure Files content share access during provisioning and runtime:

    • Storage Blob Data Owner for host blobs, leases, and trigger state.
    • Storage Account Contributor for storage account-level management operations used by the platform.
    • Storage Queue Data Contributor for queue-backed host coordination and trigger operations.
    • Storage File Data Privileged Contributor for the Azure Files content share (WEBSITE_CONTENTAZUREFILECONNECTIONSTRING) used by Premium.
  4. Create a VNet with separate subnets for integration and private endpoints.

    az network vnet create \
      --name "vnet-premium-demo" \
      --resource-group "$RG" \
      --location "$LOCATION" \
      --address-prefixes "10.20.0.0/16" \
      --subnet-name "snet-integration" \
      --subnet-prefixes "10.20.1.0/24"
    
    az network vnet subnet create \
      --name "snet-private-endpoints" \
      --resource-group "$RG" \
      --vnet-name "vnet-premium-demo" \
      --address-prefixes "10.20.2.0/24"
    
    az network vnet subnet update \
      --name "snet-integration" \
      --resource-group "$RG" \
      --vnet-name "vnet-premium-demo" \
      --delegations "Microsoft.Web/serverFarms"
    
    az functionapp vnet-integration add \
      --name "$APP_NAME" \
      --resource-group "$RG" \
      --vnet "vnet-premium-demo" \
      --subnet "snet-integration"
    
    for SVC in blob queue table file; do
      az network private-endpoint create \
        --name "pe-st-$SVC" \
        --resource-group "$RG" \
        --location "$LOCATION" \
        --vnet-name "vnet-premium-demo" \
        --subnet "snet-private-endpoints" \
        --private-connection-resource-id "$STORAGE_ID" \
        --group-ids "$SVC" \
        --connection-name "conn-st-$SVC"
    done
    

    Expected output (abridged):

    {
      "newVNetName": "vnet-premium-demo",
      "newSubnetName": "snet-integration",
      "newRouteAllEnabled": false
    }
    
    {
      "name": "pe-st-blob",
      "provisioningState": "Succeeded"
    }
    {
      "name": "pe-st-queue",
      "provisioningState": "Succeeded"
    }
    {
      "name": "pe-st-table",
      "provisioningState": "Succeeded"
    }
    {
      "name": "pe-st-file",
      "provisioningState": "Succeeded"
    }
    
  5. Create a private endpoint for inbound private access.

    APP_ID=$(az functionapp show \
      --name "$APP_NAME" \
      --resource-group "$RG" \
      --query "id" \
      --output tsv)
    
    az network private-endpoint create \
      --name "pe-$APP_NAME" \
      --resource-group "$RG" \
      --location "$LOCATION" \
      --vnet-name "vnet-premium-demo" \
      --subnet "snet-private-endpoints" \
      --private-connection-resource-id "$APP_ID" \
      --group-ids "sites" \
      --connection-name "conn-$APP_NAME"
    

    Expected output (abridged):

    /subscriptions/<subscription-id>/resourceGroups/<resource-group-name>/providers/Microsoft.Web/sites/<function-app-name>
    
    {
      "name": "pe-<function-app-name>",
      "provisioningState": "Succeeded",
      "privateLinkServiceConnections": [
        {
          "groupIds": [
            "sites"
          ]
        }
      ]
    }
    

    Private endpoint name resolution requires private DNS configuration. At minimum, create and link privatelink.azurewebsites.net to the VNet, then attach the zone to the private endpoint:

    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-premium-demo" \
      --virtual-network "vnet-premium-demo" \
      --registration-enabled false
    
    az network private-endpoint dns-zone-group create \
      --resource-group "$RG" \
      --endpoint-name "pe-$APP_NAME" \
      --name "web-dns-zone-group" \
      --private-dns-zone "privatelink.azurewebsites.net" \
      --zone-name "web-config"
    
    for SVC in blob queue table file; do
      az network private-dns zone create \
        --resource-group "$RG" \
        --name "privatelink.$SVC.core.windows.net"
    
      az network private-dns link vnet create \
        --resource-group "$RG" \
        --zone-name "privatelink.$SVC.core.windows.net" \
        --name "link-$SVC" \
        --virtual-network "vnet-premium-demo" \
        --registration-enabled false
    
      az network private-endpoint dns-zone-group create \
        --resource-group "$RG" \
        --endpoint-name "pe-st-$SVC" \
        --name "$SVC-dns-zone-group" \
        --private-dns-zone "privatelink.$SVC.core.windows.net" \
        --zone-name "$SVC"
    done
    

    Expected output (abridged):

    {
      "name": "privatelink.azurewebsites.net",
      "numberOfRecordSets": 0
    }
    
    {
      "name": "web-dns-zone-group",
      "provisioningState": "Succeeded"
    }
    
    {
      "name": "privatelink.blob.core.windows.net",
      "provisioningState": "Succeeded"
    }
    {
      "name": "privatelink.queue.core.windows.net",
      "provisioningState": "Succeeded"
    }
    {
      "name": "privatelink.table.core.windows.net",
      "provisioningState": "Succeeded"
    }
    {
      "name": "privatelink.file.core.windows.net",
      "provisioningState": "Succeeded"
    }
    
  6. Publish function code (Premium supports file share-based deployment and SCM/Kudu).

    cd apps/python
    func azure functionapp publish "$APP_NAME" --python
    

    Expected output (abridged):

    Getting site publishing info...
    Creating archive for current directory...
    Upload completed successfully.
    Deployment completed successfully.
    Syncing triggers...
    
  7. Verify app status and endpoint.

    az functionapp show \
      --name "$APP_NAME" \
      --resource-group "$RG" \
      --output table
    
    curl --request GET "https://$APP_NAME.azurewebsites.net/api/health"
    

    Expected output (abridged):

    Name               State    DefaultHostName
    -----------------  -------  ----------------------------------------
    <function-app-name> Running  <function-app-name>.azurewebsites.net
    
    {"status":"healthy","timestamp":"2026-01-01T00:00:00Z","version":"1.0.0"}
    

Auto-created Application Insights

az functionapp create automatically creates an Application Insights resource and links it via APPINSIGHTS_INSTRUMENTATIONKEY and APPLICATIONINSIGHTS_CONNECTION_STRING. Tutorial 04 creates a second, explicit Application Insights resource. You may see duplicate resources — this is expected.

Verification

Expected output when policy allows shared key access

{
  "id": "/subscriptions/<subscription-id>/resourceGroups/rg-func-premium-demo/providers/Microsoft.Web/sites/func-premium-demo",
  "location": "koreacentral",
  "name": "func-premium-demo",
  "state": "Running",
  "defaultHostName": "func-premium-demo.azurewebsites.net"
}
Getting site publishing info...
Creating archive for current directory...
Uploading 14.8 MB [########################################]
Upload completed successfully.
Deployment completed successfully.
Syncing triggers...
Functions in func-premium-demo:
    health - [httpTrigger]
    info - [httpTrigger]
{"status":"healthy","timestamp":"2026-01-01T00:00:00Z","version":"1.0.0"}

Deployment Verification Results

Blocked by enterprise policy

In our Korea Central deployment, EP1 Premium was blocked during provisioning by the same allowSharedKeyAccess: false policy as Consumption. Premium plans also require WEBSITE_CONTENTAZUREFILECONNECTIONSTRING with shared key access for the content file share.

Observed error:

ERROR: Creation of storage file share failed with: 'The remote server returned an error: (403) Forbidden.'.
Please check if the storage account is accessible.

Workarounds:

  • Request a policy exemption from your Azure administrator
  • Use Flex Consumption (FC1) which supports identity-based blob storage
  • Use Dedicated (B1) which uses WEBSITE_RUN_FROM_PACKAGE without a content file share
flowchart LR
    A[Internet] --> B[Private Endpoint]
    B --> C[Function App Premium EP1]
    C --> D[VNet Integration Subnet]
    D --> E[Private Backend]
    C --> F[Pre-warmed Instances]

Next Steps

Next: 03 - Configuration

See Also

Sources