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

You may or may not have noticed I’m utilizing a PowerShell script as a parameters file. This is something I do for just about any type of configuration that I’ll be consuming / executing with PowerShell. While I could probably extol all the great virtues of why a scripting language can make an excellent configuration file. A great blog post by Sedat Kapanoglu here already handles that. Instead, I will preface this by saying, you don’t have to limit yourself to PowerShell. If you prefer Python, Perl, BaSH, etc., it doesn’t matter. The point here is to have something that can dynamically generate a hashtable object, or a raw JSON file which can be consumed by Azure’s deployment command.

What we’re going to show here, is how we can get rid of those properties we’re not using. That’s the ultimate solution being proposed in this article. That is, have a perfectly crafted set of parameters which can be executed without any Bicep tricks. Above, I explained I’m utilizing a scripting language to handle this. However, if you’re more of a purest. You could solve this with some find / replace functionality as well.

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'}
    properties = @{
        addressSpace = @{
            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.properties.add('dhcpOptions', $googleDNSServer.dhcpOptions)
    }

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

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

$template

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

template.bicep

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

////////////////////////////////////////
//START: res_networkVirtualNetwork
resource res_networkVirtualNetwork 'Microsoft.Network/virtualNetworks@2021-03-01' = {
  name: deploymentObject.name
  location: deploymentObject.location
  tags: deploymentObject.tags
  properties: deploymentObject.properties
  }
//END: res_networkVirtualNetwork
////////////////////////////////////////

deploy.ps1

$deploymentObject = & .\parameters.ps1 -TeamName 'One'

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

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, you should now see your virtual network deployed, and your subnets. And best of all, no errors.

Demo Result Explanation

To understand how I accomplished this, let’s look at the new section I added to our parameters.ps1 file.

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

What’s going on here? Well, once the subnets are defined in the top parameters section. We loop through each subnet object, and prune out any properties that are not defined. This is a prime example of why using a scripting language to generate our parameters file is so powerful. We’re able to keep a consistent schema for an admin to update (or not) at the top, and let the script handle getting rid of what doesn’t work.

How else can we validate this? The same way we validated the non-working example.

Generate JSON artifact

$deploymentObject | ConvertTo-Json -Depth 100
{
  "properties": {
    "addressSpace": {
      "addressPrefixes": [
        "10.0.0.0/16"
      ]
    },
    "subnets": [
      {
        "name": "GatewaySubnet",
        "properties": {
          "privateLinkServiceNetworkPolicies": "Disabled",
          "privateEndpointNetworkPolicies": "Disabled",
          "addressPrefix": "10.0.0.0/24"
        }
      },
      {
        "name": "webFarm1",
        "properties": {
          "privateLinkServiceNetworkPolicies": "Disabled",
          "privateEndpointNetworkPolicies": "Disabled",
          "delegations": [
            {
              "name": "Microsoft.Web/serverFarms",
              "properties": {
                "serviceName": "Microsoft.Web/serverFarms"
              }
            }
          ],
          "addressPrefix": "10.0.10.0/24"
        }
      }
    ],
    "enableVmProtection": false,
    "virtualNetworkPeerings": [],
    "enableDdosProtection": false,
    "dhcpOptions": {
      "dnsServers": [
        "8.8.8.8"
      ]
    }
  },
  "location": "eastUS",
  "name": "vnetOne",
  "tags": {
    "environment": "sandbox"
  }
}

Notice now that all empty properties are cleared. The JSON artifact shows that we’re only sending properties that have values.

Article Conclusion

In this article, I showed a working example of utilizing conditional properties up stream before your template attempts to deploy it. While it’s technically not conditional properties inside Bicep, it is ultimately achieving the goal of having conditional properties. Of course, because the conditional property is a script, you can utilize any number of programable techniques to accomplish your goal.