How To: Deploy Multiple Virtual Machines Using Bicep, PowerShell and a little XML.

Continuing my journey with Bicep, this time by deploying two virtual machines.

Following on from my previous post I thought it would be interesting to look at how I can use Bicep to deploy multiple Virtual Machines.

Now obviously there are several ways that this can be done, for example you could extend the parameters if the bicep file and call this file multiple times (which is sort of what I have done), you could also list separate VM blocks within the same file or alternatively you could use the looping feature provided by Bicep, however this would require the management of arrays or other constructs within Bicep, which to me looked a little complicated for what I wanted to do as a first step.

I suspect that adding multiple VM blocks (or using loops) within one Bicep file would be the most effective method in terms of processing & deployments. However, It did not seem to give me the control I wanted over the deployment process and repeating the bicep template for a VM x number of times seemed contrary to how I would normally develop code (re-use etc.).

What I wanted was to have one Bicep template which could be called multiple times, this then added the question of how I wanted to deal with components which may only exist once in the deployment such as a VNet and related Subnet. There does not seem to be a way of checking if a resource exists prior to the deployment within Bicep, so this led me to two decisions one to split out the VNet creation and secondly to add some extra logic to the PowerShell component of the solution.

As a result I ended up with some differences to the code in the first example. Firstly I created a Bicep module which was there to manage the deployment of the VNet. To do this I created a new folder (modules) and added the following template/code.

param VNetName string
param Location string
param SubNetName string

resource VNet_Default 'Microsoft.Network/virtualNetworks@2020-08-01' = {
  name: VNetName
  location: Location
  properties: {
    addressSpace: {
      addressPrefixes: [
        '10.0.0.0/24'
      ]
    }
    subnets: [
      {
        name: SubNetName
        properties: {
          addressPrefix: '10.0.0.0/24'
          delegations: []
          privateEndpointNetworkPolicies: 'Enabled'
          privateLinkServiceNetworkPolicies: 'Enabled'
        }
      }
    ]
    virtualNetworkPeerings: []
    enableDdosProtection: false
  }
}

output VNetId string = VNet_Default.id

This will create a VNet and a Subnet with the simple parameters of Name, SubnetName and Location. To call this from the main Bicep file just add following to a separate Bicep file which in this instance I have called VNet-Deployment.Bicep (Please note I am assuming at this point that a resource group already exists).

param VNetName string
param Location string
param SubNetName string

module VNet 'modules/VNet.bicep' = {
  name: 'appService'
  params: {
    Location: Location
    VNetName: VNetName
    SubNetName: SubNetName
  }
}

This provides me with a discrete VNet template and module file for managing this one off part of my deployment. To run this I just need to call this from a PowerShell command, similar to the below.

New-AzResourceGroupDeployment -ResourceGroupName  VirtualMachines -TemplateFile $VNetBicepFile -VNetName $VNetName -SubNetName $SubNetName -Location $Location

Now that I have the core elements deployed I can start to work on the deployment of the VMs. To do this I firstly wanted a way that I can manage the parameters that I would need for this. For example if I am deploying two VMs I would need a collection of some kind containing the VM Names, NIC Names etc I would also need to modify the main bicep file to allow for the new parameters. This Bicep file now looks like the below.

param VMName string
param VNetId string
param Location string
param SubNetName string
param OSDiskName string
param NICName string
param PublicIPAddressName string

resource VM_PublicIP 'Microsoft.Network/publicIPAddresses@2021-02-01' ={
  name:PublicIPAddressName
  location: Location
  sku: {
    name: 'Basic'
    tier: 'Regional'
  }
  properties: {
    publicIPAddressVersion: 'IPv4'
    publicIPAllocationMethod: 'Dynamic'
  }
}

resource NIC_VM 'Microsoft.Network/networkInterfaces@2020-08-01' = {
  name: NICName
  location: Location
  properties: {
    ipConfigurations: [
      {
        name: 'ipconfig1'
        properties: {
          publicIPAddress: {
            id: VM_PublicIP.id
          }
          privateIPAllocationMethod: 'Dynamic'
          subnet: {
            id: '${VNetId}/subnets/${SubNetName}'
          }
          primary: true
          privateIPAddressVersion: 'IPv4'
        }
      }
    ]
    dnsSettings: {
      dnsServers: []
    }
    enableAcceleratedNetworking: false
    enableIPForwarding: false
  }
}

resource VirtualMachine 'Microsoft.Compute/virtualMachines@2021-03-01' = {
  name: VMName
  location: Location
  properties:{
    hardwareProfile: {
      vmSize:'Standard_D2s_v3'
      }
      storageProfile: {
        osDisk: {
          name: OSDiskName
          createOption: 'FromImage'
          osType: 'Windows'
          managedDisk: {
            storageAccountType: 'StandardSSD_LRS'
          }
        }
        imageReference: {
          publisher: 'MicrosoftWindowsDesktop'
          offer: 'Windows-10'
          sku: '19H1-ent'
          version: '18362.1198.2011031735'
        }
      }
      osProfile: {
        computerName: VMName
        adminUsername: 'vmAdministrator'
        adminPassword: 'Adm1nP@55w0rd'
      }
      networkProfile: {
        networkInterfaces: [
          {
            id: NIC_VM.id
          }
        ]
      }
  }
}

In order to manage the collection of parameters I would need for deploying my VMs, I used an old technique that has served me well over the years, which is to use an XML based configuration file, which would look something like this.

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Deployment Name="VM Deployment" ResourceGroupName="VirtualMachines" VNetName="VMVNet01" Region="uksouth" SubNetName="VMSubNet01" />
    <VirtualMachines>
        <VM VMName="VM01" NetworkInterface="Vm01Nic01" PublicIPName="Vm01Pip01" OSDiskName="Vm01OSDisk01" />
        <VM VMName="VM02" NetworkInterface="Vm01Nic02" PublicIPName="Vm01Pip02" OSDiskName="Vm01OSDisk02" />
    </VirtualMachines>    
</Configuration>

This provides me with all the details I currently need for deploying the environment and can be easily extended to include additional components. You will notice that I have included configuration values for the VNet here as well.

To reference these values in PowerShell I just need to reference the config file and then the various elements as and when required. To do this you need to first add the reference, which I have done like this.

$Path = "Z:\Development\themicrosoftcloudblog\Samples\BICEP\Deploy Virtual Machines\Deploy Multiple VMs\"
$ConfigFile = $Path + "Config.xml"

Then you can start to reference the configuration values within your PowerShell script. I have done this two ways in this instance, firstly for the one of values such as the VNet parameters which can simply be done by referencing the correct element in the XML file, like this.

$VNetName = $Config.Configuration.Deployment.VNetName

and for other components such as the multiple VMs you can loop through the desired element and leverage the individual parameters like so.

#Deploy Virtual Machines
foreach ($VM in $Config.Configuration.VirtualMachines.VM)
{
    New-AzResourceGroupDeployment -ResourceGroupName $ResourceGroupName 
        -TemplateFile $VMBicepFile -VMName $VM.VMName -SubNetName $SubNetName 
        -Location $Location -OSDiskName $VM.OSDiskName -VNetID $VNetID.id 
        -NICName $VM.NetworkInterface -PublicIPAddressName $VM.PublicIPName | Out-Null
    }

The above script would loop x number of times, creating a deployment for each loop. This may not be the most efficient way of doing this, but it seems to me to provide a level of control which may not be available using just Bicep.

I have uploaded the sample files to Git for reference and they can be found here https://bit.ly/3sFJPuG.

One thought on “How To: Deploy Multiple Virtual Machines Using Bicep, PowerShell and a little XML.

Add yours

  1. Thanks for your write-up. I am attempting to do a similar deployment via AZ CLI & BICEP templates. I’m able to deploy my VM’s ok, however, the problem I’m having is when I loop through my VM’s and deploy via “az deployment group create”, the only resource in the deployment group is the last resource created. It seems the only way to add multiple resources in a deployment group is to have 1 BICEP template for all of the resources which negates the use of a single template and for loop. Still trying to figure out a way around this.

    Like

Leave a comment

Website Built with WordPress.com.

Up ↑