bicep template to inject an azure app into vnet

60 Views Asked by At

I'm trying to manage my Azure resources with bicep. The resource group that I currently deploy in has also some externally managed resources including a vnet (managed externally, so marking it es existing in my bicep template).

I need my azure Function app that I deploy using bicep to have that existing vnet (managed externally) to be injected for the outbound traffic. I know how to do it in the Azure UI when you create a Function app: you need to select "Enable network injection" to "On" and then select your subnet from the dropdown. Then it would look like this in the UI

enter image description here

This is the expected result. But I can't do it in bicep. Tried the following:

Option 1:

resource azFunction 'Microsoft.Web/sites@2023-01-01' = {
  name: name
  .....
  properties: {
    enabled: true
    serverFarmId: appServicePlan.id
    reserved: true
    publicNetworkAccess: 'Disabled'
    virtualNetworkSubnetId: vNet.id

Option 2:

resource azFunction 'Microsoft.Web/sites@2023-01-01' = {
  name: name
  .....
  properties: {
    enabled: true
    serverFarmId: appServicePlan.id
    reserved: true
    publicNetworkAccess: 'Disabled'
    virtualNetworkSubnetId: vNet.properties.subnets[0].id

(so trying both vnet itself and the only subnet of this vnet)

Option 3:

resource azFunctionVnetConnection 'Microsoft.Web/sites/virtualNetworkConnections@2023-01-01' = {
  parent: azFunction
  name: 'vnetconnection'
  properties: {
    vnetResourceId: vNet.id
    isSwift: false
  }
}

Option 4:

resource azFunctionVnetConnection 'Microsoft.Web/sites/virtualNetworkConnections@2023-01-01' = {
  parent: azFunction
  name: 'vnetconnection'
  properties: {
    vnetResourceId: vNet.properties.subnets[0].id
    isSwift: false
  }
}

also both vnet and its only subnet. Still seeing something like this:

enter image description here

So the question is: how do I do in bicep the Virtual Network integration as it's done from the UI?

UPDATE: I already have the delegations in my subnet to serverFarms, I forgot to specify this in my initial question. That's in regards to @wenbo's response. The whole vnet/subnet resource looks like:

resource vNet 'Microsoft.Network/virtualNetworks@2023-06-01' = {
  name: vnetName
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [
        '172.22.0.0/24'
      ]
    }
    subnets: [
      {
        name: 'default'
        properties: {
          addressPrefix: '172.22.0.0/24'
          networkSecurityGroup: {
            id: nsg.id
          }
          serviceEndpoints: [
            {
              service: 'Microsoft.EventHub'
              locations: ['*']
            }
            {
              service: 'Microsoft.AzureCosmosDB'
              locations: ['*']
            }
            {
              service: 'Microsoft.Storage'
              locations: ['westus', 'eastus']
            }
            {
              service: 'Microsoft.Web'
              locations: ['*']
            }
            {
              service: 'Microsoft.Sql'
              locations: ['westus']
            }
            {
              service: 'Microsoft.KeyVault'
              locations: ['*']
            }
            {
              service: 'Microsoft.ContainerRegistry'
              locations: ['*']
            }
            {
              service: 'Microsoft.AzureActiveDirectory'
              locations: ['*']
            }
            {
              service: 'Microsoft.ServiceBus'
              locations: ['*']
            }
          ]
          delegations: [
            {
              name: 'funcwebapp'
              properties: {
                serviceName: 'Microsoft.Web/serverFarms'
              }
              type: 'Microsoft.Network/virtualNetworks/subnets/delegations'
            }
          ]
          privateEndpointNetworkPolicies: 'Enabled'
          privateLinkServiceNetworkPolicies: 'Enabled'
        }
      }
3

There are 3 best solutions below

2
viktorh On

afaik, this should do the trick:

param functionVNetID string = '/subscriptions/subXX/resourceGroups/rgXX/providers/Microsoft.Network/virtualNetworks/vnetXX/subnets/subnetXX'

resource functionAppName_virtualNetwork 
    'Microsoft.Web/sites/networkConfig@2022-09-01' = {
    parent: functionApp
    name: 'virtualNetwork'
    properties: {
    subnetResourceId: functionVNetID
    }
    }
2
wenbo On

There is a trap here. When you operate manually on Azure UI, a certain step is omitted. Microsoft.Web/serverFarms add to subnet delegation is prerequisites here

You should update your subnet itself in UI or bicep template.

option 1: If you do it in UI, something make like below:

enter image description here

option 2: If you want to update the subnet in bicep. blow is my sample code:

@description('existing VNET subnet')
param vnetName string = 'wbvnet1'
param subnetName string = 'wbsubnet1'
param subnetAddressPrefix string = '10.0.1.0/24'   // your sub net existing address

@description('The name of the function app that you wish to create.')
param appName string = 'fn1app${uniqueString(resourceGroup().id)}'

@description('Storage Account type')
@allowed([
  'Standard_LRS'
  'Standard_GRS'
  'Standard_RAGRS'
])
param storageAccountType string = 'Standard_LRS'

@description('Location for all resources.')
param location string = resourceGroup().location

@description('The language worker runtime to load in the function app.')
@allowed([
  'node'
  'dotnet'
  'java'
])
param runtime string = 'node'

var functionAppName = appName
var hostingPlanName = appName
var storageAccountName = '${uniqueString(resourceGroup().id)}aa'
var functionWorkerRuntime = runtime

resource storageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: storageAccountType
  }
  kind: 'Storage'
  properties: {
    supportsHttpsTrafficOnly: true
    defaultToOAuthAuthentication: true
  }
}

resource hostingPlan 'Microsoft.Web/serverfarms@2021-03-01' = {
  name: hostingPlanName
  location: location
  sku: {
    name: 'B1'
    tier: 'Basic'
  }
  properties: {}
  dependsOn: [
    subnet
  ]
}

resource functionApp 'Microsoft.Web/sites@2021-03-01' = {
  name: functionAppName
  location: location
  kind: 'functionapp'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: hostingPlan.id
    siteConfig: {
      appSettings: [
        {
          name: 'AzureWebJobsStorage'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
        }
        {
          name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
        }
        {
          name: 'WEBSITE_CONTENTSHARE'
          value: toLower(functionAppName)
        }
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~4'
        }
        {
          name: 'WEBSITE_NODE_DEFAULT_VERSION'
          value: '~14'
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: functionWorkerRuntime
        }
      ]
      ftpsState: 'FtpsOnly'
      minTlsVersion: '1.2'
    }
    httpsOnly: true
    virtualNetworkSubnetId: subnet.id
  }
}

resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-09-01' = {
  name: '${vnetName}/${subnetName}'
  properties:{
    addressPrefix: subnetAddressPrefix
    delegations: [
      {
        name: 'Microsoft.Web.hostingEnvironments'
        properties: {
          serviceName: 'Microsoft.Web/serverFarms'
        }
      }
    ]
  }
}

You need to pay attention to the way this subnet is written, it is an update operation


Update:

integrated with you vnet code, Just copy and paste, test it, I use your option 2.

@description('existing VNET subnet')
param vnetName string = 'wbvnet1'

@description('Location for all resources.')
param location string = resourceGroup().location

@description('The name of the function app that you wish to create.')
param appName string = 'fn1app${uniqueString(resourceGroup().id)}'

@description('Storage Account type')
@allowed([
  'Standard_LRS'
  'Standard_GRS'
  'Standard_RAGRS'
])
param storageAccountType string = 'Standard_LRS'

@description('The language worker runtime to load in the function app.')
@allowed([
  'node'
  'dotnet'
  'java'
])
param runtime string = 'node'

var functionAppName = appName
var hostingPlanName = appName
var storageAccountName = '${uniqueString(resourceGroup().id)}aa'
var functionWorkerRuntime = runtime

resource storageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: storageAccountType
  }
  kind: 'Storage'
  properties: {
    supportsHttpsTrafficOnly: true
    defaultToOAuthAuthentication: true
  }
}

resource hostingPlan 'Microsoft.Web/serverfarms@2021-03-01' = {
  name: hostingPlanName
  location: location
  sku: {
    name: 'B1'
    tier: 'Basic'
  }
  properties: {}
}

resource functionApp 'Microsoft.Web/sites@2021-03-01' = {
  name: functionAppName
  location: location
  kind: 'functionapp'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: hostingPlan.id
    siteConfig: {
      appSettings: [
        {
          name: 'AzureWebJobsStorage'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
        }
        {
          name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
        }
        {
          name: 'WEBSITE_CONTENTSHARE'
          value: toLower(functionAppName)
        }
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~4'
        }
        {
          name: 'WEBSITE_NODE_DEFAULT_VERSION'
          value: '~14'
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: functionWorkerRuntime
        }
      ]
      ftpsState: 'FtpsOnly'
      minTlsVersion: '1.2'
    }
    httpsOnly: true
    virtualNetworkSubnetId: vNet.properties.subnets[0].id
  }
}

// resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-09-01' = {
//   name: '${vnetName}/${subnetName}'
//   properties:{
//     addressPrefix: subnetAddressPrefix
//     delegations: [
//       {
//         name: 'Microsoft.Web.hostingEnvironments'
//         properties: {
//           serviceName: 'Microsoft.Web/serverFarms'
//         }
//       }
//     ]
//   }
// }
resource vNet 'Microsoft.Network/virtualNetworks@2023-06-01' = {
  name: vnetName
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [
        '172.22.0.0/24'
      ]
    }
    subnets: [
      {
        name: 'default'
        properties: {
          addressPrefix: '172.22.0.0/24'

          serviceEndpoints: [
            {
              service: 'Microsoft.EventHub'
              locations: ['*']
            }
            {
              service: 'Microsoft.AzureCosmosDB'
              locations: ['*']
            }
            {
              service: 'Microsoft.Storage'
              locations: ['westus', 'eastus']
            }
            {
              service: 'Microsoft.Web'
              locations: ['*']
            }
            {
              service: 'Microsoft.Sql'
              locations: ['westus']
            }
            {
              service: 'Microsoft.KeyVault'
              locations: ['*']
            }
            {
              service: 'Microsoft.ContainerRegistry'
              locations: ['*']
            }
            {
              service: 'Microsoft.AzureActiveDirectory'
              locations: ['*']
            }
            {
              service: 'Microsoft.ServiceBus'
              locations: ['*']
            }
          ]
          delegations: [
            {
              name: 'funcwebapp'
              properties: {
                serviceName: 'Microsoft.Web/serverFarms'
              }
              type: 'Microsoft.Network/virtualNetworks/subnets/delegations'
            }
          ]
          privateEndpointNetworkPolicies: 'Enabled'
          privateLinkServiceNetworkPolicies: 'Enabled'
        }
      }
    ]
  }
}

output o1 string = vNet.properties.subnets[0].id

enter image description here

0
Alexey On

The solution was to add vnetRouteAllEnabled: true to the function (Microsoft.Web/sites@2022-09-01) to route the egress traffic as well of the function over the attached subnet