Version: 1.0.0

Series Introduction

Whether you’re new to ARM/Bicep or a pro, I’m sure you’ve run into the issue of needing conditional properties. Unfortunately, ARM and Bicep do not officially have functions built in to handle this natively. At least not as of yet. I was surprised when writing this, to not see a whole lot of articles showing how work around this.

In this series, I’m going to show you a few ways you can work a round this. All of them are convoluted, but they do work. Each one will have pros and cons, and it will be up to you determine which one you like best. Of course, you don’t have to stick to one or other. Like most things in technology, use what works best in your situation.

Series Index

Series Demo Setup

In this series demo, I’ll show you how I solved the issue with conditional properties in the virtual network “subnet” properties. This was a particularly challenging one as it’s an array of objects. I chose this for our series example to show you how to handle conditional properties in more complex scenarios.

Throughout this series, we’ll use a common set of files to demonstrate both working and non-working examples. The contents of the files will change throughout each post, so I would suggest creating a folder structure that looks something like this.

  • Root Directory
    • 1
    • 2
    • 3
    • etc.

You can store a copy of the files used in the demo in each folder. This will allow you to walk through each option, and go back to previous posts if you need to review.

Article Introduction

In part 3 we established that Bicep is not a programming language, but that doesn’t mean we can’t work a round some of it’s limitations. In this article, I want to show you how to deal with conditional properties in nested object. This builds on what we already learned in part 3 and extends your abilities to build some powerful templates with a few simple helper modules.

Article Demo Setup

For this article, you will need the following files.

  • parameters.ps1: This file is used to define / generate our deployment parameters.
  • template.bicep: This file is the main bicep template that consumes the deployment parameters and ultimately executes the deployment.
  • templateSubnetHelper.bicep: This file is used by the main template to conditionally create a subnet object.
  • templateArrayHelper.bicep: This is used by the main template to take the results of a module, and convert it into a generic array that’s easier to consume.
  • deploy.ps1: This file is used to execute the deployment.

You can find the snippets of each file below. Keep in mind, these might change from article to article, so I suggest overwriting your local copies if you’re following along.

parameters.ps1

#############################
#START: Parameters
[CmdletBinding()]
param (
    [Parameter(Mandatory = $true)]
    [string]$TeamName = 'team1'
    )

#END: Parameters
#############################

#############################
#START: Variables
$networkPrefix = "10.0"

$googleDNSServer = @{
    dhcpOptions = @{
        dnsServers = @(
            '8.8.8.8'
            )
        }
    }

#END: Variables
#############################

#########################################
#START: allSubnets
$allSubnets = @(
    #########################################
    #START: GatewaySubnet
    @{
    name = "GatewaySubnet"
    addressPrefix = "$($networkPrefix).0.0/24"
    applicationGatewayIpConfigurations = @()
    delegations = @()
    natGateway = @()
    networkSecurityGroup = @{}
    privateEndpointNetworkPolicies = "Disabled"
    privateLinkServiceNetworkPolicies = "Disabled"
    routeTable= @{}
    serviceEndpointPolicies = @()
    serviceEndpoints = @()
    }
    #END: GatewaySubnet
    #########################################

    #########################################
    #START: webFarmSubnet
    @{
    name = "webFarm1"
    addressPrefix = "$($networkPrefix).10.0/24"
    applicationGatewayIpConfigurations = @()
    delegations = @(
        @{
        name = "Microsoft.Web/serverFarms"
        properties = @{
            serviceName = "Microsoft.Web/serverFarms"
            }
        }
        )
    natGateway = @{}
    networkSecurityGroup = @{}
    privateEndpointNetworkPolicies = "Disabled"
    privateLinkServiceNetworkPolicies = "Disabled"
    routeTable = @{}
    serviceEndpointPolicies = @()
    serviceEndpoints = @()
    }
    #END: webFarmSubnet
    #########################################
    )

#############################
#START: Base Template
$template = @{
    name = "vnet$($TeamName)" #notice we now have a dynamic name vnet based on the team name
    #location = 'eastUS'
    tags = @{environment = 'sandbox'}
    addressPrefixes = @(
        "$($networkPrefix).0.0/16"
        )
    #Notice we have no dhcpOption property specified
    enableDdosProtection = $false
    enableVmProtection = $false
    subnets = $allSubnets
    virtualNetworkPeerings = @()
    }
#END: Base Template
#############################

#############################
#START: Conditional update

If ($TeamName -eq "One")
    {
    $template.add('dhcpOptions', $googleDNSServer.dhcpOptions)
    }

If ($TeamName -eq "Two")
    {
    $template.remove('subnets')
    }

#END: Conditional update
#############################

#############################
#START: Output Object

$template

#END: Output Object
#############################

template.bicep

////////////////////////////////////////
//START: res_networkVirtualNetwork
param deploymentObject object
//END: res_networkVirtualNetwork
////////////////////////////////////////

////////////////////////////////////////
//START: variables

var defaultLocation = 'EastUs'

//END: variables
////////////////////////////////////////

////////////////////////////////////////
//START: conditionally build subnets
var subnetsPropertyDefined = contains(deploymentObject, 'subnets') == true ? true : false
var subnetsExistToBeDeploy = subnetsPropertyDefined == true ? empty(deploymentObject.subnets) == false : false
var subnetsToDeploy = subnetsExistToBeDeploy == true ? deploymentObject.subnets : []

module mod_allSubnets 'templateSubnetHelper.bicep' = [for subnet in subnetsToDeploy: if(subnetsExistToBeDeploy == true) {
  name: '${uniqueString(deploymentObject.name)}_${subnet.name}'
  params:{
    subnetObject: subnet
  }
}]

module mod_arrayHelper 'templateArrayHelper.bicep' = if(subnetsExistToBeDeploy == true) {
  name: 'arrayHelper_${deploymentObject.name}'
  params:{
    array0: [for (subnetName, index) in subnetsToDeploy: mod_allSubnets[index].outputs.subnet ]
  }
}

//END: conditionally build subnets
////////////////////////////////////////

////////////////////////////////////////
//START: conditionally properties

// Create a base property
var baseProperties = {
  addressSpace: {
    addressPrefixes: deploymentObject.addressPrefixes
    }
  }

// Dynamically check the paramters for each property to see if it exists.  If so, we define it, if not, we create a blank object
var dhcpOptions = contains(deploymentObject, 'dhcpOptions') ? {dhcpOptions: deploymentObject.dhcpOptions} : {}
var subnets = subnetsExistToBeDeploy == true ? {subnets: mod_arrayHelper.outputs.arrayunion } : {}
var enableDdosProtection = contains(deploymentObject, 'enableDdosProtection') ? {enableDdosProtection: deploymentObject.enableDdosProtection} : {}
var enableVmProtection = contains(deploymentObject, 'enableVmProtection') ? {enableVmProtection: deploymentObject.enableVmProtection} : {}
var virtualNetworkPeerings = contains(deploymentObject, 'virtualNetworkPeerings') ? {virtualNetworkPeerings: deploymentObject.virtualNetworkPeerings} : {}

// Merge all of the objects together into the complete object
var properties = union(baseProperties,dhcpOptions,subnets,enableDdosProtection,enableVmProtection,virtualNetworkPeerings)

//END: conditionally build properties
////////////////////////////////////////

////////////////////////////////////////
//START: res_networkVirtualNetwork
resource res_networkVirtualNetwork 'Microsoft.Network/virtualNetworks@2021-03-01' = {
  name: deploymentObject.name
  //Dynamic property value for location
  location: contains(deploymentObject, 'location') ? deploymentObject.location : defaultLocation
  tags: deploymentObject.tags
  properties: properties
  }
//END: res_networkVirtualNetwork
////////////////////////////////////////

///////////////////////////////////////
//START: res_networkVirtualNetwork
output outputProperties object = properties
//END: res_networkVirtualNetwork
////////////////////////////////////////

templateSubnetHelper.bicep

////////////////////////////////////////
//START: targetScope
targetScope = 'resourceGroup'
//END: targetScope
////////////////////////////////////////

////////////////////////////////////////
//START: parameters

@description('''

''')
param subnetObject object
//END: parameters
////////////////////////////////////////

////////////////////////////////////////
//START: variables


//END: variables
////////////////////////////////////////

////////////////////////////////////////
//START: Build subnet object


var addressPrefix = {
  addressPrefix: subnetObject.addressPrefix
}

var delegations = empty(subnetObject.delegations) ? {
 }:{
  delegations: subnetObject.delegations
 }

 var natGateway = empty(subnetObject.natGateway) ? {
}:{
  natGateway: subnetObject.natGateway
}

var networkSecurityGroup = empty(subnetObject.networkSecurityGroup) ? {
}:{
  networkSecurityGroup: subnetObject.networkSecurityGroup
}

var privateEndpointNetworkPolicies = empty(subnetObject.privateEndpointNetworkPolicies) ? {
}:{
  privateEndpointNetworkPolicies: subnetObject.privateEndpointNetworkPolicies
}

var privateLinkServiceNetworkPolicies = empty(subnetObject.privateLinkServiceNetworkPolicies) ? {
}:{
  privateLinkServiceNetworkPolicies: subnetObject.privateLinkServiceNetworkPolicies
}

var routeTable = empty(subnetObject.routeTable) ? {
}:{
  routeTable: subnetObject.routeTable
}

var serviceEndpointPolicies = empty(subnetObject.serviceEndpointPolicies) ? {
}:{
  serviceEndpointPolicies: subnetObject.serviceEndpointPolicies
}

var serviceEndpoints = empty(subnetObject.serviceEndpoints) ? {
}:{
  serviceEndpoints: subnetObject.serviceEndpoints
}

var newSubnetObject = {
  name: subnetObject.name
  properties: union(addressPrefix,delegations,natGateway,networkSecurityGroup,privateEndpointNetworkPolicies,privateLinkServiceNetworkPolicies,routeTable,serviceEndpointPolicies,serviceEndpoints)
}

//END: Build subnet object
////////////////////////////////////////

////////////////////////////////////////
//START: output

output subnet object = newSubnetObject

//END: output
////////////////////////////////////////

templateArrayHelper.bicep

////////////////////////////////////////
//START: parameters

param array1 array = []
param array2 array = []
param array3 array = []
param array4 array = []
param array5 array = []
param array6 array = []
param array7 array = []
param array8 array = []
param array9 array = []
param array0 array = []

//END: parameters
////////////////////////////////////////


////////////////////////////////////////
//START: variable

var arrayunion = union(array0,array1,array2,array3,array4,array5,array6,array7,array8,array9)

//END: variable
////////////////////////////////////////

////////////////////////////////////////
//START: Output

output arrayunion array = arrayunion

//END: Output
////////////////////////////////////////

deploy.ps1

try 
    {
    $ErrorActionPreference = "Stop"

    ###################################################
    #START: Team One
    $deploymentObject = & .\parameters.ps1 -TeamName 'One'

    $resourceGroupDeploymentSplat = @{
        Name = "ericsDemoDeployment"
        ResourceGroupName = "eric-sandbox"
        deploymentObject = $deploymentObject
        TemplateFile = '.\template.bicep'
        }
        
    New-AzResourceGroupDeployment @resourceGroupDeploymentSplat
    #END Team One
    ###################################################

    ###################################################
    #START: Team Two
    $deploymentObject = & .\parameters.ps1 -TeamName 'Two'

    $resourceGroupDeploymentSplat = @{
        Name = "ericsDemoDeployment2"
        ResourceGroupName = "eric-sandbox"
        deploymentObject = $deploymentObject
        TemplateFile = '.\template.bicep'
        }
        
    New-AzResourceGroupDeployment @resourceGroupDeploymentSplat
    #END Team Two
    ###################################################
    }
catch 
    {
    $ExceptionObject = @(
        "############################################"
        "ExceptionMessage: "
        "$($_.Exception.Message)"
        "############################################"
        "Line: "
        "$($_.InvocationInfo.ScriptLineNumber)"
        "############################################"
        "ScriptName: "
        "$($_.InvocationInfo.ScriptName)"
        "############################################"
        "Command: "
        "$($_.InvocationInfo.MyCommand)"
        "############################################"
        "#Parameters (JSON): "
        "$($_.InvocationInfo.BoundParameters | ConvertTo-Json -Depth 10)"
        )

    Write-Host $($ExceptionObject | Out-String ) 
    Throw $($ExceptionObject | Where-Object {$PSItem -notlike "#*"} | Out-String)
    }

Demo Execution

To run the demo, all we need to do is execute the deploy.ps1 file. This can be accomplished by the following command.\

. .\deploy.ps1

Demo Result

Upon completion of the script, here is what you’ll see that is different.

  1. You will now have two virtual networks.
  2. vnetOne is the same as it always was. That is, it contains a custom DNS server and a set of subnets.
  3. vnetTwo is slightly different than vnetOne. vnetTwo does not have a custom DNS server, nor any subnets.
  4. The template now has an output, this is so we can see the results of our conditional properties.
  5. You’ll notice that we were able to conditionally set subnets properties. The output for vNetOne subnet, only contain properties defined with values in the parameters.ps1.

Demo Result Explanation

To understand how I accomplished this, let’s look at a few changes I made.

parameters.ps1 changes

Like in part 3, we continued to simplify the parameters.ps1.

parameters.ps1 subnet changes

Now the subnets themselves, only need property names defined, and do not need to mirror the official property schema. Previously, we needed the subnets to match the schema of subnets bicep structure as detailed here. Now you can craft your parameters however you like. The complexity of a well formed schema is moved to the helper template as you’ll see further on.

#########################################
  #START: GatewaySubnet
  @{
  name = "GatewaySubnet"
  addressPrefix = "$($networkPrefix).0.0/24"
  applicationGatewayIpConfigurations = @()
  delegations = @()
  natGateway = @()
  networkSecurityGroup = @{}
  privateEndpointNetworkPolicies = "Disabled"
  privateLinkServiceNetworkPolicies = "Disabled"
  routeTable= @{}
  serviceEndpointPolicies = @()
  serviceEndpoints = @()
  }
  #END: GatewaySubnet
  #########################################

Generate JSON artifact to validate this

Here is how we can validate that what is mentioned above, is in fact true. Take a look specifically at TeamOne. Notice how we have a number of defined properties which are empty. This is something our helper modules is going to take care of.

Team One
 . .\parameters.ps1 -TeamName "one" | ConvertTo-Json -Depth 100
{
  "addressPrefixes": [
    "10.0.0.0/16"
  ],
  "subnets": [
    {
      "serviceEndpoints": [],
      "networkSecurityGroup": {},
      "privateLinkServiceNetworkPolicies": "Disabled",
      "routeTable": {},
      "addressPrefix": "10.0.0.0/24",
      "serviceEndpointPolicies": [],
      "applicationGatewayIpConfigurations": [],
      "privateEndpointNetworkPolicies": "Disabled",
      "natGateway": [],
      "name": "GatewaySubnet",
      "delegations": []
    },
    {
      "serviceEndpoints": [],
      "networkSecurityGroup": {},
      "privateLinkServiceNetworkPolicies": "Disabled",
      "routeTable": {},
      "addressPrefix": "10.0.10.0/24",
      "serviceEndpointPolicies": [],
      "applicationGatewayIpConfigurations": [],
      "privateEndpointNetworkPolicies": "Disabled",
      "natGateway": {},
      "name": "webFarm1",
      "delegations": [
        {
          "name": "Microsoft.Web/serverFarms",
          "properties": {
            "serviceName": "Microsoft.Web/serverFarms"
          }
        }
      ]
    }
  ],
  "enableVmProtection": false,
  "enableDdosProtection": false,
  "dhcpOptions": {
    "dnsServers": [
      "8.8.8.8"
    ]
  },
  "name": "vnetone",
  "virtualNetworkPeerings": [],
  "tags": {
    "environment": "sandbox"
  }
}
Team Two
 . .\parameters.ps1 -TeamName "two" | ConvertTo-Json -Depth 100
{
  "addressPrefixes": [
    "10.0.0.0/16"
  ],
  "name": "vnettwo",
  "enableVmProtection": false,
  "virtualNetworkPeerings": [],
  "tags": {
    "environment": "sandbox"
  },
  "enableDdosProtection": false
}

template.bicep changes

As with part 3, we continue to move more of the complexity into the Bicep templates.

Adding a new section to deal with subnets

We now have a section which offloads the subnet evaluation work to a whole other Bicep module.

////////////////////////////////////////
//START: conditionally build subnets
var subnetsPropertyDefined = contains(deploymentObject, 'subnets') == true ? true : false //make sure there is even a subnet property defined
var subnetsExistToBeDeploy = subnetsPropertyDefined == true ? empty(deploymentObject.subnets) == false : false //make sure we have subnets defined and that it's not just an empty array
var subnetsToDeploy = subnetsExistToBeDeploy == true ? deploymentObject.subnets : [] // use the defined subnets, or create an empty array

//If the array has subnets, we'll loop through them and have a helper module process them.  Otherwise, if the array is empty, this will ultimately not run.  (not items to loop through)
module mod_allSubnets 'templateSubnetHelper.bicep' = [for subnet in subnetsToDeploy: if(subnetsExistToBeDeploy == true) {
  name: '${uniqueString(deploymentObject.name)}_${subnet.name}'
  params:{
    subnetObject: subnet
  }
}]

//We have an array or arrays, and it's a pain to parse looped outputs in Bicep.  Rather than trying to do something to complex, we send it to another helper module to "union" the arrays.  
module mod_arrayHelper 'templateArrayHelper.bicep' = if(subnetsExistToBeDeploy == true) {
  name: 'arrayHelper_${deploymentObject.name}'
  params:{
    array0: [for (subnetName, index) in subnetsToDeploy: mod_allSubnets[index].outputs.subnet ]
  }
}

//END: conditionally build subnets
////////////////////////////////////////

Now to explain what is going on.

To avoid errors in the deployment, we first need to see if any subnet property has been defined at all. We’ll do this with the contains object. If the property exits, it will return true, other wise false. We’ll use this value in the next evaluation.

var subnetsPropertyDefined = contains(deploymentObject, 'subnets') == true ? true : false

We now know whether there is a subnet properties defined or not. Thanks to the “subnetPropertiesDefined” var. Now we need to determine if there is any values. If we have values, again, we set a boolean value of true otherwise false.

var subnetsExistToBeDeploy = subnetsPropertyDefined == true ? empty(deploymentObject.subnets) == false : false

If subnetsExistToDeploy is true, we now know that we have a subnet object to deploy. We can now use this value to setup either a var with an empty array or a var with the subnets you want to deploy. The reason we do this is because Bicep doesn’t handle conditions + loops very well. At least there are a lot of limitations. With an empty array, Bicep will effectively skip the module because there is nothing to loop through. Therefor, it’s essentially making it a conditional deployment. You can use this tactic with any property that is based on an array.

var subnetsToDeploy = subnetsExistToBeDeploy == true ? deploymentObject.subnets : []

Now, we’ll loop through all the subnets and determine if anything needs to be conditionally deployed. If a subnet exists, we’ll send it our help module (more on that later). We’re using a module because this is the only way that we can do loops with conditions in Bicep. Looking at this code now, I suspect we don’t even need the condition statement since an empty array will do nothing.

module mod_allSubnets 'templateSubnetHelper.bicep' = [for subnet in subnetsToDeploy: if(subnetsExistToBeDeploy == true) {
  name: '${uniqueString(deploymentObject.name)}_${subnet.name}'
  params:{
    subnetObject: subnet
  }
}]

The results of the subnet helper module will be an array of objects. This next part is to simply make things easier for us. How? Well Bicep has a lot of loop limits, and one of those with consuming the output of a Bicep module that was the result of a loop its self. One way to get around this, is to use yet another module, but this time, not doing any loops. Instead, we put the data in to a generic module that does nothing more than union an array and spit an array back out. Since the module in this case is not a loop, we can directly reference the output.

module mod_arrayHelper 'templateArrayHelper.bicep' = if(subnetsExistToBeDeploy == true) {
  name: 'arrayHelper_${deploymentObject.name}'
  params:{
    array0: [for (subnetName, index) in subnetsToDeploy: mod_allSubnets[index].outputs.subnet ]
  }
}

As an aside, here is what kind of error might you get if you didn’t use the array module. For example, if your variable looks like this.

var subnets = subnetsExistToBeDeploy == true ? [for (subnetName, index) in subnetsToDeploy: mod_allSubnets[index].outputs.subnet ] : {}

Then you can expect to see an error message like the one below.

owner: "_generated_diagnostic_collection_name_#1",
code: "BCP138",
message: "For-expressions are not supported in this context. For-expressions may be used as values of resource, module, variable, and output declarations, or values of resource and module properties.",
source: "bicep"

The final difference in the main bicep file is just a tweak of the original conditional property for the subnet. You can see here that we again check if there is a subnet to deploy and if so, we are able to directly reference the output of the helper array module.

var subnets = subnetsExistToBeDeploy == true ? {subnets: mod_arrayHelper.outputs.arrayunion } : {}

templateSubnetHelper.bicep explication

The subnet helper bicep template actually doesn’t need a a lot of explanation. It works very much like the way our evaluation variables worked in part 3 (and still work in part 4). But let’s break it down into a few steps.

  1. We are only sending a subnet object that we know has data. Those evaluations were done ahead of time, thanks to conditional variables we explained starting here
  2. The module is used to send a single subnet object into the template. This allows us to process a nested object and implement conditional properties. You can mostly follow this method as deep as you need, within the constraints of Bicep. Meaning, if you have multiple levels of nesting, you’ll likely need multiple helper modules to conditionally join things together. To restate, this is an issue when we’re dealing with nested objects that are in arrays. This is not an issue with nested standalone object and can be accomplished without a helper module.
  3. For each subnet submitted, we evaluate if it contains any of the available properties. if it does, we create a variable which contains the property name and it’s value. Otherwise we create a blank object. See below as an example.
var privateLinkServiceNetworkPolicies = empty(subnetObject.privateLinkServiceNetworkPolicies) ? {
}:{
  privateLinkServiceNetworkPolicies: subnetObject.privateLinkServiceNetworkPolicies
}
  1. Once all the property evaluations are complete, we utilize the union function to join them together as a single object again.
  2. Finally we output them back to the module so they can be consumed by the main template.

templateArrayHelper.bicep explication

Again, very similar to the above point, we’ve create a generic array helper that will allow us to join together what is already an array. The problem as mentioned above, is Bicep has some real weird limitation. Since we can’t conditionally loop in a variable, we want to first create a variable that contains all the values we want. That is the point of the array module. We use it to conditionally create a single array of data that we can directly reference.

The results

Once you’ve deployed the two virtual networks, you should see the following outputs available.

vNetOne Output

Remember when I mentioned to keep an eye on team ones vnet? Remember how it had a bunch of empty properties specified? Below, you can see that other than peerings properties, all empty properties have been filtered out.

{
  "addressSpace": {
    "addressPrefixes": [
      "10.0.0.0/16"
    ]
  },
  "dhcpOptions": {
    "dnsServers": [
      "8.8.8.8"
    ]
  },
  "subnets": [
    {
      "name": "GatewaySubnet",
      "properties": {
        "addressPrefix": "10.0.0.0/24",
        "privateEndpointNetworkPolicies": "Disabled",
        "privateLinkServiceNetworkPolicies": "Disabled"
      }
    },
    {
      "name": "webFarm1",
      "properties": {
        "addressPrefix": "10.0.10.0/24",
        "delegations": [
          {
            "name": "Microsoft.Web/serverFarms",
            "properties": {
              "serviceName": "Microsoft.Web/serverFarms"
            }
          }
        ],
        "privateEndpointNetworkPolicies": "Disabled",
        "privateLinkServiceNetworkPolicies": "Disabled"
      }
    }
  ],
  "enableDdosProtection": false,
  "enableVmProtection": false,
  "virtualNetworkPeerings": []
}
vNetTwo Output
{
  "addressSpace": {
    "addressPrefixes": [
      "10.0.0.0/16"
    ]
  },
  "enableDdosProtection": false,
  "enableVmProtection": false,
  "virtualNetworkPeerings": []
}

Article Conclusion

In this article we touched on dealing with nested conditional properties. It’s a tad more convoluted for sure, but 100% doable if you want to keep the complexity in the template instead of your parameters.