Many times you’ll run into situation that you would want your installation package to install the app automatically when you deploy your VM to Azure. DSC is the right tool for that, and there’s plenty of samples with ARM templates how you can do that. When you turn to Bicep, you won’t find any complete samples, but you need to try to collect from crumbles of information the necessary pieces, and it will take some time to figure out how you can do that. As I went through that path, I thought it would be nice to put all that in one place for the next one who need to do the same thing.
Create the DSC configuration
First thing you need, is a valid DSC configuration. Let’s create one using 7zip installation package. Create a new file called MyDSC1.ps1 with the following content:
Configuration InstallApp { Import-DscResource -ModuleName PsDesiredStateConfiguration Node 'localhost' { Package InstallApp { Ensure = 'Present' Name = '7-Zip 19.00 (x64 edition)' Path = 'https://www.7-zip.org/a/7z1900-x64.msi' ProductId = '23170F69-40C1-2702-1900-000001000000' } } }
Things worth mentioning here are the name of the configuration, InstallApp which we need later, and ProductId. ProductId is something you can get with PowerShell from installed apps on your computer, and the Name parameter needs to match also the name which is visible in Apps & Features -list of installed apps, so you can’t put anything random there. If you are not running MSI package but EXE file, you can leave the ProductId field empty.
Azure doesn’t like ps1 files, but wants to have a zip archive instead. The recommended way to create it is to use PowerShell command Publish-AzVMDscConfiguration which is part of the Az.Compute -PowerShell module. If you don’t have that installed, run (and select A as your option when it asks):
Install-Module -Name Az.Compute -AllowClobber
Now that the module is installed (in case it wasn’t already), you can then create the zip archive with:
Publish-AzVMDscConfiguration .\MyDSC1.ps1 -OutputArchivePath '.\MyDSC1.zip'
Now that you have a zip archive including a ps1-file, you need to head to Azure portal, and create a Storage Account. After you have created one, open the Storage Account, under ‘Data Storage’, select ‘Containers’ and create new Container called ‘installers’ and upload the MyDSC1.zip to that container. Next open the file, and create SAS token for it, and copy the https link to notepad for later use.
Create the Bicep for the VM
Next we dig down to Bicep, and see how you insert VM extension to VM script. Lets see how the script would look and then dissect it a bit. Here we have a VM and just behind it we define the extension:
resource vm 'Microsoft.Compute/virtualMachines@2021-03-01' = { name: vmName ... } resource extDSC 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = { parent: vm name: 'Microsoft.Powershell.DSC' location: 'westeurope' properties: { autoUpgradeMinorVersion: true publisher: 'Microsoft.Powershell' type: 'DSC' typeHandlerVersion: '2.83' settings: { ModulesUrl: '<YOUR SAS TOKEN HERE, eg- https://mydsctest2021.blob.${environment().suffixes.storage}/installers/MyDSC1.zip?sp...' ConfigurationFunction: 'MyDSC1.ps1\\InstallApp' WmfVersion: 'latest' Privacy: { DataCollection: 'Enable' } } protectedSettings: {} } }
You notice that we tie the extension to the machine by setting the parent to point to the VM resource. This also handles the DependsOn behind the scenes. typeHandlerVersion is just the version of the Poweshell.DSC module we are using. ModulesUrl is where we set the SAS token you created, but do notice the ${environment().suffixes.storage} which replaces the blob storage url. This is one of the Bicep rules, you will notice a warning if you don’t replace that part of the SAS, saying you are not allowed to have that URL in Bicep. The ConfigurationFunction defines that we have a configuration file called MyDSC1.ps1 which includes Configuration called InstallApp inside it, and that is what should be executed.
Now lets take a look of a complete Bicep file which you can use to test deploy VM and necessary other resources, with DSC which installs 7Zip:
// Parameters param location string = 'westeurope' param adminUsername string = 'azureAdmin' @secure() param adminPassword string // Variables var vnetName = 'myVnet' var nsgName = 'myVM-nsg' var nsgId = resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', nsgName) var vnetId = resourceId(resourceGroup().name, 'Microsoft.Network/virtualNetworks', vnetName) var subnetName = 'default' var subnetRef = '${vnetId}/subnets/${subnetName}' var subnetPrefix = '10.0.0.0/26' var publicIpAddressName = 'myVM-ip' var vmName = 'myVM' var addressPrefix = [ '10.0.0.0/25' ] // Resources resource nic 'Microsoft.Network/networkInterfaces@2018-10-01' = { name: 'myNIC' location: location properties: { ipConfigurations: [ { name: 'ipconfig1' properties: { subnet: { id: subnetRef } privateIPAllocationMethod: 'Dynamic' publicIPAddress: { id: resourceId(resourceGroup().name, 'Microsoft.Network/publicIpAddresses', publicIpAddressName) } } } ] enableAcceleratedNetworking: true networkSecurityGroup: { id: nsgId } } dependsOn: [ nsg vnet publicIP ] } resource nsg 'Microsoft.Network/networkSecurityGroups@2019-02-01' = { name: nsgName location: location properties: { securityRules: [ { name: 'RDP' properties: { protocol: 'Tcp' sourcePortRange: '*' destinationPortRange: '3389' sourceAddressPrefix: '*' destinationAddressPrefix: '*' access: 'Allow' priority: 300 direction: 'Inbound' sourcePortRanges: [] destinationPortRanges: [] sourceAddressPrefixes: [] destinationAddressPrefixes: [] } } ] } } resource vnet 'Microsoft.Network/virtualNetworks@2020-11-01' = { name: vnetName location: location properties: { addressSpace: { addressPrefixes: addressPrefix } subnets: [ { name: subnetName properties: { addressPrefix: subnetPrefix networkSecurityGroup: { id: nsg.id } } } ] } } resource publicIP 'Microsoft.Network/publicIpAddresses@2019-02-01' = { name: publicIpAddressName location: location properties: { publicIPAllocationMethod: 'Dynamic' } sku: { name: 'Basic' } } resource vm 'Microsoft.Compute/virtualMachines@2021-03-01' = { name: vmName location: location properties: { hardwareProfile: { vmSize: 'Standard_DS1_v2' } storageProfile: { osDisk: { createOption: 'FromImage' managedDisk: { storageAccountType: 'StandardSSD_LRS' } } imageReference: { publisher: 'MicrosoftWindowsServer' offer: 'WindowsServer' sku: '2019-Datacenter' version: 'latest' } } networkProfile: { networkInterfaces: [ { id: nic.id } ] } osProfile: { computerName: vmName adminUsername: adminUsername adminPassword: adminPassword windowsConfiguration: { enableAutomaticUpdates: false provisionVMAgent: true patchSettings: { enableHotpatching: false patchMode: 'Manual' } } } diagnosticsProfile: { bootDiagnostics: { enabled: true } } } } resource extDSC 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = { parent: vm name: 'Microsoft.Powershell.DSC' location: 'westeurope' properties: { autoUpgradeMinorVersion: true publisher: 'Microsoft.Powershell' type: 'DSC' typeHandlerVersion: '2.83' settings: { ModulesUrl: '<PUT YOUR SAS TOKEN HERE AND USE ${environment().suffixes.storage}>' ConfigurationFunction: 'MyDSC1.ps1\\InstallApp' WmfVersion: 'latest' Privacy: { DataCollection: 'Enable' } } protectedSettings: {} } }
The above script is not production code, and leaves doors open for hackers, so do not use that in production, it is here just for testing purposes and keeping things as simple as possible. Save the code above as vm.bicep and run the following commands to deploy it:
az login az group create --name myRG --location "West Europe" az deployment group create --resource-group myRG --template-file vm.bicep
Now you can go to Azure portal, connect to the machine with RDP, and see the 7Zip installed on the Start Menu. Not too complicated, when you just find all the pieces needed to make it work.
Scale Set extensions
If you are wondering how to do the same with Virtual Machine Scale Set, wonder no more! You can use the resource definition for the extension from the script above, you just need to move it to ExtensionProfile under the scale set virtualMachineProfile. You would define the extension as extension profile to the scale set, like this:
extensionProfile: { extensions: [ { name: 'Microsoft.Powershell.DSC' properties: { autoUpgradeMinorVersion: true publisher: 'Microsoft.Powershell' type: 'DSC' typeHandlerVersion: '2.83' settings: { ModulesUrl: '<PUT YOUR SAS TOKEN HERE AND USE ${environment().suffixes.storage}>' ConfigurationFunction: 'MyDSC1.ps1\\InstallApp' Properties: '' WmfVersion: 'latest' Privacy: { DataCollection: 'Enable' } } } } ] }
And if you are confused, how it would actually sit in the whole template, fear not, I have a full deployable scale set MVP (ports open to internet, so you can RDP to it, so not production ready!) here for you to try (do the az group create & az deployment using this file as parameter):
param adminUsername string = 'azureAdmin' @secure() param adminPassword string param vmssName string = 'MyScaleSet' param location string = resourceGroup().location param windowsOSVersion string = '2019-Datacenter' var osType = { publisher: 'MicrosoftWindowsServer' offer: 'WindowsServer' sku: windowsOSVersion version: 'latest' } var addressPrefix = '10.0.0.0/25' var subnetPrefix = '10.0.0.0/26' var virtualNetworkName = 'myvnet' var publicIPAddressName = 'mypip' var subnetName = 'mysubnet' var loadBalancerName = 'mylb' var natPoolName = 'mynatpool' var bePoolName = 'mybepool' var natStartPort = 50000 var natEndPort = 50119 var natBackendPort = 3389 var nicname = 'mynic' var ipConfigName = 'myipconfig' var nsgName='mynsg' resource vnet 'Microsoft.Network/virtualNetworks@2020-11-01' = { name: virtualNetworkName location: location properties: { addressSpace: { addressPrefixes: [ addressPrefix ] } subnets: [ { name: subnetName properties: { addressPrefix: subnetPrefix delegations: [] privateEndpointNetworkPolicies: 'Enabled' privateLinkServiceNetworkPolicies: 'Enabled' } } ] virtualNetworkPeerings: [] enableDdosProtection: false } dependsOn: [ loadBalancer nsg publicIP ] } resource nsg 'Microsoft.Network/networkSecurityGroups@2020-11-01' = { name: nsgName location: 'westeurope' properties: { securityRules: [ { name: 'RDP' properties: { protocol: 'Tcp' sourcePortRange: '*' destinationPortRange: '3389' sourceAddressPrefix: '*' destinationAddressPrefix: '*' access: 'Allow' priority: 300 direction: 'Inbound' sourcePortRanges: [] destinationPortRanges: [] sourceAddressPrefixes: [] destinationAddressPrefixes: [] } } ] } } resource nsgRule 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = { parent: nsg name: 'RDP' properties: { protocol: 'Tcp' sourcePortRange: '*' destinationPortRange: '3389' sourceAddressPrefix: '*' destinationAddressPrefix: '*' access: 'Allow' priority: 300 direction: 'Inbound' sourcePortRanges: [] destinationPortRanges: [] sourceAddressPrefixes: [] destinationAddressPrefixes: [] } } resource publicIP 'Microsoft.Network/publicIPAddresses@2020-06-01' = { name: publicIPAddressName location: location sku: { name: 'Standard' } properties: { publicIPAddressVersion: 'IPv4' publicIPAllocationMethod: 'Static' } } resource loadBalancer 'Microsoft.Network/loadBalancers@2020-06-01' = { name: loadBalancerName location: location sku: { name: 'Standard' } properties: { frontendIPConfigurations: [ { name: 'LoadBalancerFrontEnd' properties: { privateIPAllocationMethod: 'Dynamic' publicIPAddress: { id: publicIP.id } } } ] backendAddressPools: [ { name: bePoolName } ] inboundNatPools: [ { name: natPoolName properties: { frontendIPConfiguration: { id: resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', loadBalancerName, 'LoadBalancerFrontEnd') } protocol: 'Tcp' enableFloatingIP: false enableTcpReset: false frontendPortRangeStart: natStartPort frontendPortRangeEnd: natEndPort backendPort: natBackendPort } } ] probes: [ { name: 'tcpProbe' properties: { protocol: 'Tcp' port: 80 intervalInSeconds: 5 numberOfProbes: 2 } } ] loadBalancingRules: [ { name: 'LBRule' properties: { frontendIPConfiguration: { id: resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', loadBalancerName, 'LoadBalancerFrontEnd') } backendAddressPool: { id: resourceId('Microsoft.Network/loadBalancers/backendAddressPools', loadBalancerName, '${bePoolName}') } protocol: 'Tcp' frontendPort: 80 backendPort: 80 enableFloatingIP: false idleTimeoutInMinutes: 5 enableTcpReset: false disableOutboundSnat: false loadDistribution: 'Default' probe: { id: resourceId('Microsoft.Network/loadBalancers/probes', loadBalancerName, 'tcpProbe') } } } ] } } // VM Scale set resource vmss 'Microsoft.Compute/virtualMachineScaleSets@2020-06-01' = { name: vmssName location: location sku: { name: 'Standard_DS1_v2' tier: 'Standard' capacity: 2 } properties: { overprovision: true upgradePolicy: { mode: 'Manual' automaticOSUpgradePolicy: { enableAutomaticOSUpgrade: false } } virtualMachineProfile: { storageProfile: { osDisk: { createOption: 'FromImage' caching: 'ReadWrite' } imageReference: osType } osProfile: { computerNamePrefix: 'my' adminUsername: adminUsername adminPassword: adminPassword } networkProfile: { networkInterfaceConfigurations: [ { name: nicname properties: { primary: true enableAcceleratedNetworking: true networkSecurityGroup: { id: nsg.id } dnsSettings: { dnsServers: [] } enableIPForwarding: false ipConfigurations: [ { name: ipConfigName properties: { publicIPAddressConfiguration: { name: 'publicIp-my-vnet-nic01' properties: { idleTimeoutInMinutes: 15 ipTags: [] publicIPAddressVersion: 'IPv4' } } primary: true subnet: { id: '${vnet.id}/subnets/${subnetName}' } loadBalancerBackendAddressPools: [ { id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Network/loadBalancers/${loadBalancerName}/backendAddressPools/${bePoolName}' } ] loadBalancerInboundNatPools: [ { id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Network/loadBalancers/${loadBalancerName}/inboundNatPools/${natPoolName}' } ] } } ] } } ] } extensionProfile: { extensions: [ { name: 'Microsoft.Powershell.DSC' properties: { autoUpgradeMinorVersion: true publisher: 'Microsoft.Powershell' type: 'DSC' typeHandlerVersion: '2.83' settings: { ModulesUrl: '<YOUR SAS TOKEN HERE>' ConfigurationFunction: 'MyDSC1.ps1\\InstallApp' Properties: '' WmfVersion: 'latest' Privacy: { DataCollection: 'Enable' } } } } ] } } } }