My first Bicep template

Last Saturday, May 28 2022, I visited the DotNed Saturday organized by the DotNed user group. It is always a great way to hear about interesting subjects, and for getting motivation to look into new developments. This time I was mostly interested in the subjects on Azure, so I join talks about KeyVault, Network Security in Azure and Bicep, and some other interesting talks as well. Unfortunately you can't join all sessions, as there where lots of other interesting sessions too. I greatly appreciate the speakers, the organizers, and not to forget, the sponsors of these kind of events.

To see what I had learned, I came up with the idea to combine the subject of these 3 talks into the following assignment: Use Bicep for setting up function app infrastructure that uses a KeyVault that is available through a private network.

I had thought it should not be that hard, as the Bicep language seemed quit clean and simple. Well, it was a bit more of a challenge than I anticipated, still I got there in the end, and doing it in my spare time, while also being full time on the job, it didn't take that much hours. I really like Bicep, and think that it is really good for developers to be more cloud infrastructure aware. I also really liked the networking talk, as I already was thinking about getting more insights on this subject. It is really important for setting up extra layer(s) of security in Azure.

Setting up things, I used Visual Studio Code with the Bicep extension installed, I also used the Azure cli and a bit of PowerShell. To get started:

az login --tenant [tenant name]
az account set --subscription "[guid of subscription]"
az group create --name [resource group name] --location "northeurope"

You can use Bicep for creating a resource group, but because by default Bicep is targeted to a resource group, and I didn't want to complicate things, I used the Azure cli to create the resource group. Then I started creating a Bicep template, and I did quite a few tests with rolling out the template, until I got stuck on getting the KeyVault references to work in the function app configuration. This part was the hardest thing to solve.

I had taken an example, and used that in my Bicep file, the example was for the privateDnsZones that had the name 'privatelink${environment().suffixes.keyvaultDns}', so it took me quite some time to figure out the issue with this, as I was reading some documentation, I noticed that the the link was different, then I also found this question, and I updated the template to use keyvaultDns as a parameter with the default value set to the required '.vaultcore.azure.net'. Well it might not need to be a parameter, and probably I should just use a fixed 'privatelink.vaultcore.azure.net', as this must always be the name for this resource. I left it in, just so you know my template isn't perfect. So the problem was that environment().suffixes.keyvaultDns although useful, points to the public endpoint prefix .vault.azure.net, after changing this, the template worked, so I was really happy.

Well, I did also add some extra's, as I noticed that I couldn't reach the KeyVault secrets etc. from my own network in the Azure portal anymore. I then configured an exception for the IP range of my office location (an /32 IP range 😜). I also added my own account as a 'key master' in KeyVault, and created parameters for these. Then I added some PowerShell to get my Azure AD Object ID to pass as a parameter, so I am calling my template as follows:

$user = az ad signed-in-user show | ConvertFrom-Json
$userId = $user.id
az deployment group create --resource-group [resource group name] --template-file [mytemplatename].bicep --parameters prefix="[my prefix]" myOfficeIpRange="[my IP range]" defaultOwner=$userId

With this I called the following Bicep template:

param prefix string
param defaultOwner string
param myOfficeIpRange string
param location string = resourceGroup().location
param keyvaultDns string = '.vaultcore.azure.net'


resource appServicePlan 'Microsoft.Web/serverfarms@2021-03-01' = {
  name: '${prefix}AppServicePlan'
  location: location
  sku: {
    name: 'B1'
  }
  kind: 'windows'
}

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-09-01' = {
  name: toLower('${prefix}StorageAccount')
  location: location
  kind: 'StorageV2'
  sku: {
    name: 'Standard_LRS'
  }
}

resource functionApp 'Microsoft.Web/sites@2021-03-01' = {
  name: '${prefix}FunctionApp'
  location: location
  kind: 'functionapp'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      appSettings: [
        {
          name: 'AzureWebJobsStorage'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
        }
        {
          name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
        }
        {
          name: 'WEBSITE_CONTENTSHARE'
          value: toLower('${prefix}FunctionApp')
        }
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~4'
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: 'PowerShell'
        }
        {
          name: 'clientId'
          value: '@Microsoft.KeyVault(VaultName=${prefix}KeyVault;SecretName=clientId)'
        }
        {
          name: 'clientSecret'
          value: '@Microsoft.KeyVault(VaultName=${prefix}KeyVault;SecretName=clientSecret)'
        }
      ]
      powerShellVersion: '7.2'
      use32BitWorkerProcess: false
      ftpsState: 'FtpsOnly'
      minTlsVersion: '1.2'
    }
    httpsOnly: true
  }
  dependsOn: [
    privateKeyVaultDnsZone
    privateKeyVaultDnsZoneLink
  ]
}

resource keyVault 'Microsoft.KeyVault/vaults@2021-10-01' = {
  name: '${prefix}KeyVault'
  location: location
  properties: {
    sku: {
      name: 'standard'
      family: 'A'
    }
    tenantId: subscription().tenantId
    accessPolicies: [
      {
        objectId: defaultOwner
        tenantId: subscription().tenantId
        permissions: {
          secrets: [
            'all'
          ]
          certificates: [
            'all'
          ]
          keys: [
            'all'
          ]
        }
      }
      {
        objectId: functionApp.identity.principalId
        tenantId: functionApp.identity.tenantId
        permissions: {
          secrets: [
            'get'
          ]
        }
      }
    ]
    networkAcls: {
      bypass: 'None'
      defaultAction: 'Deny'
      ipRules: [
        {
          value: myOfficeIpRange
        }
      ]
    }
  }
}

resource clientId 'Microsoft.KeyVault/vaults/secrets@2021-10-01' = {
  name: 'clientId'
  parent: keyVault
  properties: {
    value: 'please set this value, but not in Bicep'
  }
}

resource clientSecret 'Microsoft.KeyVault/vaults/secrets@2021-10-01' = {
  name: 'clientSecret'
  parent: keyVault
  properties: {
    value: 'please set this value, but not in Bicep'
  }
}

resource vNet 'Microsoft.Network/virtualNetworks@2021-08-01' = {
  name: '${prefix}vNet'
  location: location
  properties: {
    addressSpace: {
      addressPrefixes:[
        '10.20.0.0/24'
      ]
    }
  }
}

resource defaultSubnet 'Microsoft.Network/virtualNetworks/subnets@2021-08-01' = {
  parent: vNet
  name: 'default'
  properties: {
    addressPrefix: '10.20.0.0/25'
    delegations: [
      {
        name: 'Microsoft.Web.serverFarms'
        properties: {
            serviceName: 'Microsoft.Web/serverFarms'
        }
      } 
    ]
    privateEndpointNetworkPolicies: 'Enabled'
    privateLinkServiceNetworkPolicies: 'Enabled'
  }
}

resource servicesSubnet 'Microsoft.Network/virtualNetworks/subnets@2021-08-01' = {
  parent: vNet
  name: 'services'
  properties: {
    addressPrefix: '10.20.0.128/25'
    privateEndpointNetworkPolicies: 'Disabled'
    privateLinkServiceNetworkPolicies: 'Enabled'
  }
}

resource privateEndpoint 'Microsoft.Network/privateEndpoints@2021-08-01' = {
  name: '${prefix}KeyVaultPrivateEndpoint'
  location: location
  properties: {
    subnet: {
      id: servicesSubnet.id
    }
    privateLinkServiceConnections: [
      {
        name: '${prefix}KeyVaultPrivateEndpoint'
        properties: {
          privateLinkServiceId: keyVault.id
          groupIds: [
            'vault'
          ]
        }
      }
    ]
  }
}

resource privateKeyVaultDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
  name: 'privatelink${keyvaultDns}'
  location: 'global'
  properties: {}
  dependsOn: [
    vNet
  ]
}

resource privateKeyVaultDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
  parent: privateKeyVaultDnsZone
  name: '${privateKeyVaultDnsZone.name}-link'
  location: 'global'
  properties: {
    registrationEnabled: false
    virtualNetwork: {
      id: vNet.id
    }
  }
}

resource privateKeyVaultEndpointDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2021-05-01' = {
  name: 'vault-${prefix}PrivateEndpointDnsGroupName'
  parent: privateEndpoint
  properties: {
    privateDnsZoneConfigs: [
      {
        name: privateKeyVaultDnsZone.name
        properties: {
          privateDnsZoneId: privateKeyVaultDnsZone.id
        }
      }
    ]
  }
}

resource virtualNetwork 'Microsoft.Web/sites/networkConfig@2021-01-01' = {
  parent: functionApp
  name: 'virtualNetwork'
  properties: {
    subnetResourceId: resourceId('Microsoft.Network/virtualNetworks/subnets', vNet.name, defaultSubnet.name)
    swiftSupported: true
  }
}

So there are still some open ends with this template, for one, the storage account should also be only available through private endpoints, and what about application insights? For an example I have looked at azure-quickstart-templates, would be nice for another time. Also, I might need to think more about a naming convention, and there are other things to improve. Like I said, this is not a perfect Bicep template (this is my first one you know), still it might be an interesting starter, and help people out with the issue of the private endpoint for KeyVault. One thing I know, I will be doing more Bicep templates in the future.

One last thing, I added some KeyVault references to secrets that I added through the Bicep template, don't do this at home (or at the office)! At least not with fixed values in your template, these will then probably end up in source control, keep secrets secret.