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.