How to Deploy VMware VMs with Azure DSC

Save to My DOJO

How to Deploy VMware VMs with Azure DSC

Desired State Configuration (DSC) can be a powerful tool for defining your VMware infrastructure by code. Let’s walk through deploying VMs through DSC the Azure DSC service acting as the pull server.

Previously we walked through the steps on how to set up an Azure Automation account and how add a node for LCM (Local Configuration Manager). So, if you need to set up a DSC pull server yet, be sure to check out the mentioned article. The LCM node will act as our proxy by receiving the DSC configuration from Azure and then running the PowerCLI commands to perform said configuration in our VMware environment. VMware has created some DSC resources for vSphere that they are actively developing on GitHub, however, there currently isn’t a DSC resource for deploying VMs, so we will just make our own custom DSC resource.

Template and VM Customization

We’re going to use the typical VMware VM deployment method by using a VM Template and a Custom OS configuration so we’ll need to set these two items up before we create our custom DSC Resource. I have a template created called VMTemp. If you don’t know the steps on how to create a template, be sure to check out Jason Fenech’s walkthrough.:

VMTemp

Now we need to create a VM Customization in order to customize the desired settings for the OS. I’m naming mine WebServer since I’m going to be creating 2 VM’s for hosting a web application:

VM Customization Specifications

Now that we have our VM Customization and Template created, we can now create our DSC Resource module and upload it into Azure Automation.

Creating the DSC Custom Resource

Our DSC Custom Resource for deploying VM’s will consist of a module file (.psm1) and a manifest file (.psd1). To create the module file, copy the code below and save it as a .psm1. I saved mine as  “DeployVM.psm1”:

[DscResource()]
class DeployVM {

        [DscProperty(key)]
        [String]$VMname
        [DscProperty(Mandatory)]
        [String]$VCenter
        [DscProperty(Mandatory)]
        [PSCredential]$Credentials
        [DSCProperty()]
        [String]$Template
        [DscProperty(Mandatory)]
        [String]$Customization
        [DscProperty()]
        [int]$CPU
        [DscProperty()]
        [int]$MemoryGB
        [DscProperty(Mandatory)]
        [String]$VMhost
        [DscProperty(Mandatory)]
        [String]$datastore
        [DscProperty(Mandatory)]
        [String]$cluster
        hidden [PSObject] $Connection

        #Create VM or update with settings if its already created
        [void] Set(){
            Try{

                $this.ConnectVIServer()
                $vm = $this.getvm()

                if ($null -eq $vm){
                        #if VM doesnt exist, create it, if it does check Mem and CPU
                    Write-Verbose "Creating $($this.VMname)"
                    $result = $this.CreateVM()
                    if ($result -eq $true){
                        Write-Verbose "$($this.VMname) has been created "
                    } else {
                        throw "There was an issue creating the VM"
                    }
                }else{

                        #Set Memory
                    if ($vm.MemoryGB -ne $this.MemoryGB) {
                        
                        #verify if VM is powered off or on if so check for hot add
                        if ($vm.PowerState -eq 'Poweredon'){
                            Write-Verbose "$($this.VMname) is powered on, checking for Hot Add"

                            #if hot add is enabled and memory is less than what is declared
                            if($vm.ExtensionData.Config.MemoryHotAddEnabled -eq $true -and $vm.MemoryGB -lt $this.MemoryGB){

                                Write-Verbose "Hot add is enabled, adding memory"
                                $this.UpdateMemory()
                            } else {

                                Write-Error "Cannot set Memory while VM is powered on"
                            }

                        } Else{
                            $this.UpdateMemory()

                        }else {Write-error "Unable to set memory while VM is powered on"}

                    }
                        #Set CPU
                    if ($vm.NumCpu -ne $this.CPU) {
                        
                        #verify if VM is powered off or on if so check for hot add
                        if ($vm.PowerState -eq 'Poweredon'){
                            Write-Verbose "$($this.VMname) is powered on, checking for Hot Add"
                            
                            #if hot add is enabled and CPU is less than what is declared
                            if($vm.ExtensionData.Config.CpuHotAddEnabled -eq $true -and $vm.NumCpu -lt $this.CPU){

                                Write-Verbose "Hot add is enabled, adding CPU"
                                $this.UpdateCPU()
                            } else {

                                Write-Error "Cannot increase CPU while VM is powered on"
                            }

                        } Else {
                            $this.UpdateCPU()

                        }

                    }

                }

            } Catch{
                Write-Verbose "There was an issue with setting the resource: $($_.Exception.Message)"
            }
            
        }


        #Check if current settings of VM equal settings of the DSC config
        [bool] Test() {
            $this.ConnectVIServer()

            Write-Verbose "Looking for VM: $($this.VMname)"
            $VMConfig = $this.getvm()

            return $this.Equals($VMConfig)

           
        
        }

        #Get the current settings of the VM
        [DeployVM] Get() {
            $result = [DeployVM]::new()
            $this.ConnectVIServer()

            Write-Verbose "Looking for VM: $($this.VMname)"
            $vm = $this.getvm()
            
            
            $result.VMname = $vm.name
            $result.VCenter = $this.VCenter
            $result.Credentials = $this.Credentials
            $result.template = $this.Template
            $result.Customization = $this.Customization
            $result.CPU = $vm.NumCpu
            $result.MemoryGB = $vm.MemoryGB
            $result.VMhost = $vm.VMHost
            $result.datastore = (get-datastore | where-object {$_.id -eq $vm.DatastoreIdList}).Name
            $result.cluster = (get-cluster -vm $vm).name
                
            return $result
                
                
            }

        #Helpers

        
        #Import modules and connect to VC
        [void] ConnectVIServer() {
            $savedVerbosePreference = $global:VerbosePreference
            $global:VerbosePreference = 'SilentlyContinue'
            Import-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue
             $global:VerbosePreference = $savedVerbosePreference
            if ($null -eq $this.Connection) {
                try {
                    $this.Connection = Connect-VIServer -Server $this.vcenter -Credential $this.Credentials -ErrorAction Stop
                }
                catch {
                    throw "Cannot establish connection to server $($this.vcenter). For more information: $($_.Exception.Message)"
                }
            }
        }


        #create VM if doesnt esxist, if it does set the CPU and Memory
        [bool] CreateVM() {
                
              

                $VMcluster = Get-Cluster -Name $this.cluster

                $props = @{

                    Name = $this.VMname
                    template = $this.Template
                    OSCustomizationSpec = $this.Customization
                    VMhost = (Get-VMHost -name $this.vmhost)
                    Datastore = $this.datastore
                    Server = $this.VCenter
                    resourcepool = $VMcluster

                }
                $VM = New-VM @props

                if($vm.NumCpu -ne $this.CPU){ set-vm $vm -NumCpu $this.cpu -Confirm:$false }
                if($vm.MemoryGB -ne $this.MemoryGB){ set-vm $vm -MemoryGB $this.MemoryGB -confirm:$false}

                Start-vm $vm

                if ($null -ne $vm){

                    return $true
                } else {
                    return $false
                }
        
            }


        [PSObject] GetVM(){
             
             try{
                $VM = Get-VM -Name $this.VMName -verbose:$false -ErrorAction SilentlyContinue | select -First 1
                return $vm
            }
            catch{ 
            write-verbose "VM is not there" 
            return $null
            
            }
        }

        [void] UpdateMemory(){
            Try{
                
                 set-vm $this.VMname -MemoryGB $this.MemoryGB -confirm:$false

            }
            Catch{
                Throw "there is an issue setting the Memory"
            }
        }

        [bool] Equals($VMConfig) {
                $vm = $this.getvm()

                #Check if VM exists
                if ($null -eq $vm){ 
                    Write-Verbose "$($this.VMname) does not exist"
                    return $false
                }
                #Check CPU
                if ($VMConfig.NumCpu -ne $this.CPU){
                    Write-Verbose "$($this.VMname) has $($vmconfig.NumCpu) vCPUs and should have $($this.CPU)"
                    return $false
                }
                #check Memory
                if ($VMConfig.MemoryGB -ne $this.MemoryGB){
                    Write-Verbose "$($this.VMname) has $($vmconfig.MemoryGB) Memory and should have $($this.MemoryGB)"
                    return $false
                }
            
                return $true
        }

        [void] UpdateCPU(){
            Try{       
                 set-vm $this.VMname -NumCpu $this.CPU -confirm:$false
            }
            Catch{
                Throw "there is an issue setting the CPU"
            }
        
        }

}

Now we need to create the manifest file which is the file that provides the information about our module. Copy the code below and save it as a “.psd1” file. I saved mine as “DeployVM.psd1”:

#
# Module manifest for module 'DeployVM'
#
# Generated by: Luke
#
# Generated on: 5/10/2019
#

@{

# Script module or binary module file associated with this manifest.
RootModule = 'DeployVM.psm1'

# Version number of this module.
ModuleVersion = '1.0'

# Supported PSEditions
# CompatiblePSEditions = @()

# ID used to uniquely identify this module
GUID = '389099d3-aa57-49f8-a4dc-f0f0bfc8b5cb'

# Author of this module
Author = 'Luke'

# Company or vendor of this module
CompanyName = 'Unknown'

# Copyright statement for this module
Copyright = '(c) 2019 Luke. All rights reserved.'

# Description of the functionality provided by this module
Description = 'Deploy VMwawre VM Module'

# Minimum version of the Windows PowerShell engine required by this module
# PowerShellVersion = ''

# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''

# Minimum version of the Windows PowerShell host required by this module
# PowerShellHostVersion = ''

# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# DotNetFrameworkVersion = ''

# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# CLRVersion = ''

# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''

# Modules that must be imported into the global environment prior to importing this module
# RequiredModules = @()

# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()

# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()

# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()

# Format files (.ps1xml) to be loaded when importing this module
# FormatsToProcess = @()

# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
# NestedModules = @()

# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = '*'

# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = '*'

# Variables to export from this module
VariablesToExport = '*'

# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = '*'

# DSC resources to export from this module
DscResourcesToExport = 'DeployVM'

}

Note: at the bottom of the Manifest file, you need to make sure the “DSCResourcesToExport” matches the name of your module file. Also, when we zip the name of the folder, the zip file needs to match the name. So in my example, I have the name of my module as DeployVM, since my .psm1 is named the same:

Zip the two files together, now we are ready to upload our custom resource into Azure.

deploy vm

Log into your Azure Subscription. Navigate to your Azure Automation account and select Modules and choose the Add a module button. Upload the .zip file we created and choose Ok. Wait a few minutes for the module to import, when complete you should see it in the list of your modules with the “available” status:

Now we are ready to create a configuration and assign it to our LCM node.

Creating and Compiling the Configuration

I want to create a configuration to deploy two VM’s “web1” and “web2”. So I will create my config file like below.  I specify the CPU and MemoryGB, the datastore and host to deploy to, and template/customization to use. This is how we are defining our VM build with code:

Configuration DeployVMS {


    Import-DscResource -ModuleName DeployVM

    Node localhost {
       
        #Credentials from Azure
        $Cred = Get-AutomationPSCredential 'ESXi Password'
        $vccreds = New-Object System.Management.Automation.PSCredential ("[email protected]", $cred.password)

        #Apply Config to each host
        foreach ($VMname in @("web1","web2")) {
         
            DeployVM "VMConfig_$($VMName)" {
                VMName = $VMName
                VCenter = "192.168.0.7"
                Credentials = $vccreds
                Template = "VMTemp"
                Customization = "WebServer"
                CPU = 2
                MemoryGB = 6
                VMhost = "ESXi1.lukelab.lcl"
                Datastore = "ESXi1-Internal"
                Cluster = "Luke-HA-DRS"
            }

            
        }
    }
}

Also, note that I am using the Get-AutomationPSCredential cmdlet. This is because I am storing the VCenter credentials in the Azure Automation password vault and retrieving it safely when compiling the MOF files in Azure. This is one of the benefits to using a pull server in Azure DSC, not only do you get an already set up pull server, but you also can store your credentials safely and call them in your scripts. Just make sure you specify the name of the credential you are storing, for example, mine is “ESXi Password” so, in my config file, I reference the same credential:

credentials

Save the config file to a .ps1. Now we can upload it as a configuration into Azure DSC. Under the Automation Account, select State Configuration (DSC) and select the configurations tab. Then click the Add button and upload the configuration file (.ps1). Click refresh and it will appear as a list of configurations. Select Compose Configuration to create our Mof file for our node:

DSC State configuration

Select Compile to begin the compiling process:

DeployVMS

This process will take 3 to 5 minutes. Once done it will show as “complete”. Now we are ready to assign the new compiled configuration to our LCM node. Select the LCM node under the Nodes tab:

Select Assign Node Configuration and we’ll choose our “DeployVMS.localhost” configuration:

We can either wait for the next pull interval on our VsphereDSC node, or we can remote into the node and run the following command to have it update the configuration:

Update-DSConfiguration -wait -verbose

We can see that it’s looking for web1 and web2 and since they are not there, they will automatically be created:

After a few minutes we can see our VMs we declared in the config file now exist:

If we delete a VM, change the CPU, or modify the Memory it will automatically get recreated/reconfigured again in 15 minutes during the next poll. This can be extremely powerful and we can get as granular with the configuration file and custom resources as we want. With a stateless web server, we could essentially configure the IIS configuration and VM build entirely with just code. Azure DSC is still in it’s infancy stage but it is a great platform for managing and maintaining DSC configurations. It allows for IT Pros to quickly jump into DSC without the hassle of setting up and securing their own pull server. Microsoft will be investing a lot into this platform in the future and it will be interesting to see the evolution in just a year.

Have you done much with Desired State Configuration? Has it worked well if you have? If you haven’t do you have questions or barriers to getting started?

Let us know in the comments section below!

Altaro VM Backup
Share this post

Not a DOJO Member yet?

Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!

1 thoughts on "How to Deploy VMware VMs with Azure DSC"

Leave a comment

Your email address will not be published. Required fields are marked *