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

Bicep isn’t a scripting language. At least not a full featured one. However, that doesn’t mean there aren’t creative ways we can’t solve certain programmatic needs. Utilizing our series example, I’ll show you how we can utilized a few Bicep object functions to build a the ultimate object we need for deployment.

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.
  • 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"
    properties = @{
        addressPrefix = "$($networkPrefix).0.0/24"
        applicationGatewayIpConfigurations = @()
        delegations = @()
        natGateway = @()
        networkSecurityGroup = @{}
        privateEndpointNetworkPolicies = "Disabled"
        privateLinkServiceNetworkPolicies = "Disabled"
        routeTable= @{}
        serviceEndpointPolicies = @()
        serviceEndpoints = @()
        }
    }
    #END: GatewaySubnet
    #########################################

    #########################################
    #START: webFarmSubnet
    @{
    name = "webFarm1"
    properties = @{
        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: Remove empty properties

$allSubnetPropertiesToRemove = Foreach ($subnet in $allSubnets)
    {
    Foreach ($subnetPropertyKey in $subnet.properties.Keys)
        {
        If ([string]::IsNullOrEmpty($subnet.properties.$($subnetPropertyKey) ) -eq $true -or $subnet.properties.$($subnetPropertyKey).count -eq 0)
            {
            Write-Host "We found an empty property of $($subnetPropertyKey) in subnet $($subnet.name), marking property for removal" -ForegroundColor Yellow
            [PSCustomObject]@{
                subnetName = $($subnet.name) 
                propertyName = $($subnetPropertyKey)
                }
            }
        else 
            {
            Write-Host "The property of $($subnetPropertyKey) in subnet $($subnet.name) is NOT empty" -ForegroundColor Green
            }
        }
    }

Foreach ($subnetPropertyToRemove in $allSubnetPropertiesToRemove)
    {
    ($allSubnets | Where-Object {$_.Name -eq $($subnetPropertyToRemove.subnetName)}).properties.remove($subnetPropertyToRemove.propertyName)
    }

#END: Remove empty properties
#############################

#############################
#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'

// 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 = contains(deploymentObject, 'subnets') ? {subnets: deploymentObject.subnets} : {}
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: variables
////////////////////////////////////////

////////////////////////////////////////
//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
////////////////////////////////////////

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.

Demo Result Explanation

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

parameters.ps1 changes

You’ll notice overall, the parameters file got a little simpler.

Template variables changes

Here is a list of changes specific to the templates variable.

  • The location property is now commented out. This was to show that we can either choose to have a location handled by the parameters file, or create a default one in the template. This is more of a conditional value then property. However I wanted to show it regardless.
  • There is no longer a full Bicep property structure. Instead we have a mostly flat object key value pairs. This is because we moved the complexity to the template instead of the parameters file. The only exception to this is the subnets and addressprefixes. We’ll address the subnets in an upcoming article.
#############################
#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
#############################
Team two variables changes

Here is a list of changes specific to the dynamic properties based on the team parameter

  • To demonstrate not having a subnets property specified, we remove the subnets property when team two is specified. This was to enforce the ability of the template to cope with the variations.
If ($TeamName -eq "Two")
    {
    $template.remove('subnets')
    }

Generate JSON artifact to validate this

Here is how we can validate that what is mentioned above, is in fact true.

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

Notice how much emptier team two’s deployment artifact is?

template.bicep changes

In exchange for having a simpler parameters file, we’ve now moved the complexity into the Bicep template.

Adding dynamic variables

We now have a variables section inside the template. See below for the snippet of what we added.

var defaultLocation = 'EastUs'

// 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 = contains(deploymentObject, 'subnets') ? {subnets: deploymentObject.subnets} : {}
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)

Now to explain what is going on.

The defaultLocation variable is a value we’re setting to use later on. This will be in the event that no location is mentioned in the template.

var defaultLocation = 'EastUs'

The baseProperties object is use defining the start of what will ultimately be a more full properties object. I like to start by defining an object with all known properties that will not be dynamic.

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

The next section, we are evaluating the parameters object. Essentially we’re looking for all the potential properties that exist in the parameters. Utilizing the contains() template function, we can check for the existence of the property. This is where the term conditional properties is coming from.

We have two actions we take for each variable.

  1. If the parameters object contains the property, we use it’s value.
  2. If the parameters object does not contain the property, we create a blank object.

In terms of PowerShell or other scripting languages, this is the way the contains() function works.

$dhcpOptions = If ($deploymentObject.keys -contains 'dhcpOptions')
    {
    #dhcp options object
    @{dhcpOptions = $deploymentObject.dhcpOptions}
    }
  else
    {
    #Blank object
    @{}
    }
// 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 = contains(deploymentObject, 'subnets') ? {subnets: deploymentObject.subnets} : {}
var enableDdosProtection = contains(deploymentObject, 'enableDdosProtection') ? {enableDdosProtection: deploymentObject.enableDdosProtection} : {}
var enableVmProtection = contains(deploymentObject, 'enableVmProtection') ? {enableVmProtection: deploymentObject.enableVmProtection} : {}
var virtualNetworkPeerings = contains(deploymentObject, 'virtualNetworkPeerings') ? {virtualNetworkPeerings: deploymentObject.virtualNetworkPeerings} : {}

After all these evaluations are complete, we will have set of independent variables, which either contain an object, or are simply a blank object. It’s important when utilizing this strategy, that you understand the value type. Meaning, whether the ultimate property should be an object, int, array, string, etc.

Now that we have all these independent values / evaluations complete, we can merge them into one complete object. We accomplish this with the union() function. It’s important to keep in mind you need at least two parameters to utilize the union object.

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

The union function will discard empty object (or ignore them) and we’ll end up with a complete properties object.

How do you know it’s working? Well besides the results, we can look at the output to take a look.

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

Huge difference right?

Article Conclusion

In this article, we went over ways you can dynamically or conditionally set properties. This technique is something I use a lot to simplify the parameters file. Enabling the person doing the deployment to spend less time trying to figure out how to craft the deployment parameters, and focus more on simply deploying.