Deploy VMware VMs using Azure DSC

Desired State Configuration (DSC) resources for VMware allows you to apply standard configuration management processes through PowerShell DSC and PowerCLI. DSC can manage and monitor a system’s configuration based on configuration files.

The image below from VMware’s website provides a good overview of the layout :

DSC Resources for VMware is a little different than a standard DSC configuration, using a proxy because the LCM cannot run on VCSA or ESXi host.

Setting Up Azure Automation Configuration Management as a pull server

First of all we need to create an Automation Account.

  • Log into Azure, do a search for Automation Accounts.
  • Then select Add to create an Automation account.
  • Next, fill out the blade with your Name, Subscription, Resource group and Location.

Note: Keep the option for Azure Run As Account on Yes and select Create:

The Run As account expires one year from the date of creation. At some point before your Run As account expires, you must renew the certificate. You can renew it any time before it expires.

Now we need two modules (AzureRM.Automation & xPSDesiredStateConfiguration) which can be installed on the LCM node itself or on the management workstation.

Install-Module AzureRM.Automation -Force
Install-Module -name xPSDesiredStateConfiguration -Force

We will need to fill out the parameters to match our newly created automation account.

# Define the parameters for Get-AzureRmAutomationDscOnboardingMetaconfig using PowerShell Splatting
$Params = @{
    ResourceGroupName = 'BogdanLabDSC'; # The name of the Resource Group that contains your Azure Automation Account
    AutomationAccountName = 'BogdanLabDSC'; # The name of the Azure Automation Account where you want a node on-boarded to
    ComputerName = @('computername'); # The names of the computers that the meta configuration will be generated for
    OutputFolder = "C:\DSCConfigs";
}
# Use PowerShell splatting to pass parameters to the Azure Automation cmdlet being invoked
# For more info about splatting, run: Get-Help -Name about_Splatting
Get-AzureRmAutomationDscOnboardingMetaconfig @Params

Note: Make sure the computername field matches the name of the LCM node.

Connect to Azure by typing in: connect-azurermaccount

A prompt window to log in to Azure will appear asking for your credentials.

Once logged in, we’ll run the code mentioned above generating our meta.mof file, the file that configures the LCM engine. In other words, this tell our vSphere DSC node to report into Azure for its config. file.

Copy the .meta.mof file to the LCM node itself and run Set-DSLOcalConfigurationManager locally (also you can use PSRemoting to push the configuration)

Set-DSCLocalConfigurationManager -path "C:\DscConfigs\DscMetaConfigs" -Computername computername -credential $creds -verbose

Note: Make sure the computername field matches the name of the LCM node.

Now, when we take a peek at our Automation Account in Azure, and select State Configuration (DSC), we can see our added node and a pretty graph for managing all of our nodes:

Note that the LCM node cannot be a part of a domain.

For VMware VMs deployment I will use an existing VM templates from my LAB, called TestVM and a Custom OS configuration.

Now we need to create DSC Resource module and upload it into created Automation Account. DSC Custom Resource for deploying VM’s will consist of a module file (.psm1) and a manifest file (.psd1)

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"
            }
        
        }
}

DeployVM.psd1

#
# Module manifest for module 'DeployVM'
#
# Generated by: Bogdan
#
# Generated on: 21/09/2021
#
@{
# Script module or binary module file associated with this manifest.
RootModule = 'DeployVM.psm1'
# Version number of this module.
ModuleVersion = '2.0'
# Supported PSEditions
# CompatiblePSEditions = @()
# ID used to uniquely identify this module
GUID = '184099d3-bb59-49f8-a4dc-f0f0gfg8b5cb'
# Author of this module
Author = 'Bogdan'
# Company or vendor of this module
CompanyName = 'Lab'
# Copyright statement for this module
Copyright = '(c) 2021 Bogdan. 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'

Zip the two files together and upload it into Azure. Navigate to your Azure Automation account and select Modules and choose the Add a module button. Upload the .zip file created and choose Ok.

DeployVM module has “available” status now.

We’ll create a configuration to deploy two VM’s “TestVM1” and “TestVM2”.

Configuration DeployVM {
    Import-DscResource -ModuleName DeployVM
    Node LCMNODE {
       
        #Credentials from Azure
        $Cred = Get-AutomationPSCredential 'VMware'
        $vccreds = New-Object System.Management.Automation.PSCredential ("administrator@vsphere.local", $cred.password)
        #Apply Config to each host
        foreach ($VMname in @("TestVM1","TestVM2")) {
         
            DeployVM "VMConfig_$($VMName)" {
                VMName = $VMName
                VCenter = "192.168.0.111"
                Credentials = $vccreds
                Template = "TestVM"
                Customization = "TestVM"
                CPU = 2
                MemoryGB = 4
                VMhost = "192.168.0.103"
                Datastore = "datastore1"
                Cluster = "Cluster"
            }
            
        }
    }
}

Make sure you specify the name of the credential you are storing, for example, mine is “VMware”.

Save the config file to a .ps1 and 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:

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 “DeployVM.LCMNODE” configuration

Remote into the node and run the following command update the configuration:

Update-DscConfiguration -wait -verbose

We can see our VMs 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.

Unique Visitors

web counter

Author: bogdan.buruiana

Leave a Reply

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