Sitemap

Automating VM creation in VMware ESXi with PowerCLI

9 min readAug 13, 2026

--

Press enter or click to view image in full size

TL;DR howto automate creation and initial configuration of a VM in VMware ESXi.

Welcome to Part XIII of our cheatsheet series compiled from various courses, labs, stuff we did at work, Google, etc.

Part I: Mimikatz cheatsheet

Part II: Set-Acl cheatsheet

Part III: Get-Acl cheatsheet

Part IV: Enumerating AD cheatsheet

Part V: Windows reverse shells cheatsheet

Part VI: MS Graph PowerShell cheatsheet

Part VII: Hash cracking cheatsheet

Part VIII: The Credential Theft Shuffle

Part IX: SACLs & Querying Collected Logs

Part X: Setting up a simple AD lab in Azure

Part XI: Hyper-V cheatsheet

Part XII: Migrating to openSUSE LEAP

Part XIII: Automating VM creation in ESXi with PowerCLI

Part XIV: PowerCLI cheatsheet

Part XV: Automating VM creation in ESXi with Ansible

Background

I automated initial VM creation and configuration awhile back in Hyper-V and then cleaned it up and made it almost professional around January of this year. I wrote it so one simply runs

New-TestVM -NewVMName "TestFallback" -NewVMIP "106"

For example and the function takes care of everything.

  • It captures the IP scheme from the Hypervisor itself and then configures the VM to use all the same settings. It sets the last octet according to what’s specified in the command line option ‘-NewVMIP’.
  • It captures the username of whoever runs it, prompts for their password, and saves that password in a SecureString to use later to join the VM to the home lab domain. Simply run the function as a Domain Admin, supply the command line options, and go grab a coffee. The VM is up & running, domain joined, and pulling Group Policy when you get back.

Doing it in Hyper-V was easy and intuitive thanks to PowerShell Direct.

However work still uses VMware, for now anyway. License renewal hasn’t come up yet. Therefore I wanted to screw around with managing ESXi with PowerShell and ideally automate initial VM creation and configuration. Once a VM is up & running with the IP scheme set and domain joined I can then simply manage it via WinRM regardless of the hypervisor.

The trick is automating that initial creation & configuration.

PowerCLI

PowerCLI is basically VMware’s version of Hyper-V’s PowerShell Direct. ESXi and PowerCLI themselves are free from Broadcom, however one can only query ESXi without a license. If you try to change any settings, create anything, or even start a VM it doesn’t work.

Press enter or click to view image in full size

Someone let me know a little tip; just Google “insite:github.com esxi key”. I was rather ashamed that I hadn’t thought of that.

Generally you don’t want to install PowerCLI on a system that also manages Hyper-V as many of the commands overlap. I simply did everything from my laptop that’s not domain joined. I’m in the habit of RDPing into my domain joined “jumpbox” to then home lab so no issue there. I have only used this laptop to manage Azure VMs and Entra ID.

You can install PowerCLI via:

Install-Module -Name VMware.PowerCLI -Scope CurrentUser -Repository PSGallery -Force

If you’re just screwing around in the home lab then you can also do:

Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false
Set-PowerCLIConfiguration -Scope User -ParticipateInCEIP $false -Confirm:$false

Creating the HD Template

The tactic I went with is to create the HD from an answer file and the OS’s ISO, mostly because I’m used to doing it that way in Hyper-V. I wrote the Cyber Range using that method as my code downloads the ISO. This kept the range setup to mere MBs.

Get Rich’s stories in your inbox

Join Medium for free to get updates from this writer.

I poked around on Google and realized I could simply use my existing function that creates a VHDX from an answer file and an ISO.

Function Create-VHDX
{
Param
(
[Parameter(Mandatory=$true, Position=0)]
[string] $VMName,
[Parameter(Mandatory=$false, Position=1)]
[string] $IP
)
Write-Host "FYSA: this function must be run as Administrator."
#Creates the VM from a provided ISO & answer file, names it provided VMName
Set-Location "C:\Users\fdhsr\Google Drive\Documents\CompTIA studying\Lab Domain Projects\00 Ideas\Automating ESXi VM deployment"
$isoFilePath = ".\Windows Server 2022 (20348.169.210806–2348.fe_release_svc_refresh_SERVER_EVAL_x64FRE_en-us).iso"
$answerFilePath = ".\ESXi_unattend.xml"
New-Item -ItemType Directory -Path ".\$VMName"
$convertParams = @{
SourcePath = $isoFilePath
SizeBytes = 100GB
Edition = 'Windows Server 2022 Datacenter Evaluation (Desktop Experience)'
VHDFormat = 'VHDX'
VHDPath = ".\$VMName\$VMName.vhdx"
DiskLayout = 'UEFI'
UnattendPath = $answerFilePath
}
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope CurrentUser
. ".\Convert-WindowsImage (from PS Gallery)\Convert-WindowsImage.ps1"
Convert-WindowsImage @convertParams
}
Press enter or click to view image in full size

I then used qemu to convert the VHDX to VMDK so VMware can use it. You can download qemu from here.

Function Convert-VHDX_to_VMDK
{
Param
(
[Parameter(Mandatory=$true, Position=0)]
[string] $VHDX,
[Parameter(Mandatory=$true, Position=1)]
[string] $VMDK_Name,
[Parameter(Mandatory=$true, Position=2)]
[string] $Working_Path
)
Set-Location "C:\Program Files\qemu\"
#Don't use this 'subformat=monolithicFlat' unless you want qemu to use up the full 100 GB
.\qemu-img.exe convert -f vhdx -O vmdk -o subformat=streamOptimized "$Working_Path\$VHDX" "$Working_Path\$VMDK_Name"
}

I then uploaded the Template VMDK to ESXi via:

Connect-VIServer -Server "192.168.0.98" -User "root" -Password '<password>'
New-Item "vmstore:\ha-datacenter\datastore1\TestTemplate" -ItemType Directory
Get-ChildItem -Path "$WorkingPath\*.vmdk" | Copy-DatastoreItem -Destination "vmstore:\ha-datacenter\datastore1\TestTemplate\" -Force

I used ‘subformat=streamOptimzed’ so that qemu would create a thin provisioned VMDK. This part is crucial and not just to save space on physical HDs. The thin provisioned VHDX is only around 10 GB and the resulting VMDK is 4.77 GB.

Creating the new VM’s HD from the template using different command line options results in a 100 GB template VMDK, which of course takes a lot longer to upload to ESXi as well as wasting space on anything it’s stored on.

Using the template

Everything up to this point simply

  • Created a sysprepped VHDX using an answer file and an ISO.
  • Converted it to VMDK.
  • Uploaded the VMDK to ESXi.

At this point I needed to be able to take that template VMDK and create new VMs from it.

Function New-VMDK
{
Param
(
[Parameter(Mandatory=$true, Position=0)]
[string] $ESXi,
[Parameter(Mandatory=$false, Position=1)]
[string] $VMName
)
$CommandString = "cd /vmfs/volumes/datastore1/TestTemplate && vmkfstools -i TestTemplate.vmdk -d thin ${VMName}.vmdk && mkdir -p ../${VMName} && mv ${VMName}* ../${VMName}/"
# Pass command directly via argument rather than piped stdin
ssh -tt $ESXi $CommandString
}

This creates the new VM’s HD, creates a folder on datastore1 named for the VM, and moves the VM’s HD into that folder. It’s run via

New-VMDK -ESXi root@192.168.0.98 -VMName TestServerII

Creating the VM

I’ll clean this part up more and make it into a function later. I just wanted to get it working and this is a cheatsheet of how I did that after all.

This part creates the VM without a HD.

#VMware won't let us create the VM without a HD, so we'll make a dummy drive and then immediately remove it
$Network = Get-VirtualPortGroup -Name "VM Network" -Standard
$NewVM = New-VM -Name "TestServerII" -MemoryGB 4 -NumCPU 2 -Datastore "datastore1" -GuestId "windows9Server64Guest" -DiskGB 1 -NetworkName $Network
Get-HardDisk -VM $NewVM | Remove-HardDisk -DeletePermanently:$true -Confirm:$false

This part attaches our VMDK to the new VM.

By default ESXi attaches the VMDK to the SCCI Controller … which then causes the VM to crash on boot. This took much Googling and trial & error to diagnose, troubleshoot, and fix. This part worked in Hyper-V immediately.

Press enter or click to view image in full size
This part worked flawlessly in Hyper-V immediately but was a huge PITA in ESXi
#Gemini wrote this part, but it works. Maps the vmdk to the VM's IDE 0
# 1. Grab the VM object
$VM = Get-VM -Name "TestServerII"
# 2. Build the VirtualDisk configuration spec
$DiskSpec = New-Object VMware.Vim.VirtualMachineConfigSpec
$DiskChange = New-Object VMware.Vim.VirtualDeviceConfigSpec
$DiskChange.Operation = [VMware.Vim.VirtualDeviceConfigSpecOperation]::add
# Define the Virtual Disk
$Disk = New-Object VMware.Vim.VirtualDisk
$Disk.ControllerKey = 200 # Target IDE Controller 0 (Key 200)
$Disk.UnitNumber = 0 # First drive position on IDE 0
# Set the backing to your existing datastore file
$DiskBacking = New-Object VMware.Vim.VirtualDiskFlatVer2BackingInfo
$DiskBacking.FileName = "[datastore1] TestServerII/TestServerII.vmdk"
$DiskBacking.DiskMode = "persistent"
$Disk.Backing = $DiskBacking
$DiskChange.Device = $Disk
$DiskSpec.DeviceChange += $DiskChange
# 3. Apply changes to the VM
$VM.ExtensionData.ReconfigVM($DiskSpec)

This part mounts the VMware Tools installer.

#Gemini wrote this part too. Mounts the VM Tools installer.
#PowerCLI and VMware in general really kinda sucked here. Hyper-V does this a lot easier.
#This part must be run between creating the VM and starting it.
$NewVM = Get-VM -Name "TestServerII"
# 1. Initialize configuration spec
$Spec = New-Object VMware.Vim.VirtualMachineConfigSpec
$CdSpec = New-Object VMware.Vim.VirtualDeviceConfigSpec
$CdSpec.Operation = [VMware.Vim.VirtualDeviceConfigSpecOperation]::add
# 2. Instantiate a new Virtual CD-ROM Device
$CdDrive = New-Object VMware.Vim.VirtualCdrom
$CdDrive.ControllerKey = 200 # Attach to IDE Controller 0
$CdDrive.UnitNumber = 1 # Place on Unit 1 (Hard disk is on Unit 0)
# 3. Define ISO Backing
$CdBacking = New-Object VMware.Vim.VirtualCdromIsoBackingInfo
$CdBacking.FileName = "[] /usr/lib/vmware/isoimages/windows.iso"
$CdDrive.Backing = $CdBacking
# 4. Set connection flags so Windows mounts it automatically
$CdDrive.Connectable = New-Object VMware.Vim.VirtualDeviceConnectInfo
$CdDrive.Connectable.Connected = $true
$CdDrive.Connectable.StartConnected = $true
# 5. Assign device to spec and reconfigure VM
$CdSpec.Device = $CdDrive
$Spec.DeviceChange += $CdSpec
$NewVM.ExtensionData.ReconfigVM($Spec)

Initial Configuration

At this point we can start the VM. The answer file tells it to install VMware Tools on first boot hence why we mapped them before starting the VM.

Start-VM -VM $NewVM

# VM's initial local admin credentials:
[string]$userName = ".\Administrator"
[string]$userPassword = 'SuperSecureLocalPassword123!@#'
# Convert to SecureString and build PSCredential object
$secStringPassword = ConvertTo-SecureString $userPassword -AsPlainText -Force
$InitialCredObject = New-Object System.Management.Automation.PSCredential ($userName, $secStringPassword)

#Set the IP scheme and name the VM
Invoke-VMScript -VM "TestServerII" -GuestCredential $InitialCredObject -ScriptType PowerShell -ScriptText {
$Adapter = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | Select-Object -First 1
New-NetIPAddress -InterfaceAlias $Adapter.Name -IPAddress "192.168.0.112" -PrefixLength 24 -DefaultGateway "192.168.0.1"
Set-DnsClientServerAddress -InterfaceAlias $Adapter.Name -ServerAddresses "192.168.0.101","192.168.0.102","192.168.0.103","192.168.0.104","8.8.8.8"
#Disable IPv6
$NIC = (Get-NetAdapter).InterfaceAlias
Disable-NetAdapterBinding -InterfaceAlias $NIC -ComponentID ms_tcpip6
Install-WindowsFeature -Name "RSAT" -IncludeAllSubFeature
Rename-Computer -NewName "TestServerII" -Force
Restart-Computer -Force
}
Start-Sleep -Seconds 120

We then join the VM to the home lab domain.

#This almost had to be a function as PowerCLI doesn't support $using:variable
#The ` character escapes variables in the string being passed to the VM so Windows ignores them locally
Function Add-VMToDomain
{
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true)]
[string]$VMName,
[Parameter(Mandatory=$true)]
[string]$DomainName,
[Parameter(Mandatory=$true)]
[string]$DomainUser,
[Parameter(Mandatory=$true)]
[string]$DomainPassword
)
# Expand local string variables into script text before passing to ESXi
$ScriptPayload = "
netsh advfirewall firewall set rule group='Network Discovery' new enable=Yes
`$SecPass = ConvertTo-SecureString '$DomainPassword' -AsPlainText -Force
`$GuestDomainCred = New-Object System.Management.Automation.PSCredential ('$DomainUser', `$SecPass)
Add-Computer -DomainName '$DomainName' -Credential `$GuestDomainCred -Restart -Force
"
Invoke-VMScript -VM $VMName -GuestCredential $InitialCredObject -ScriptType PowerShell -ScriptText $ScriptPayload
}

Confirm via

Invoke-VMScript -VM "TestServerII" -GuestCredential $InitialCredObject -ScriptType PowerShell -ScriptText {ipconfig /all}
Press enter or click to view image in full size

We can also see our VM’s information in the ESXi web UI, if you’re into GUIs.

Press enter or click to view image in full size

Summary

I’ll wrap the process to create and configure a new VM from the template up in a function with command line options for VM name, last IP octet, and credentials to join the domain. I already captured the relevant information in variables from ESXi itself.

#Capture IP scheme from ESXi
$GW = (Get-VMHostNetwork | Select-Object *).VMKernelGateway
$Subnet = (Get-VMHostNetworkAdapter -Name vmk0 | Select-Object *).SubnetMask
$DNS = (Get-VMHostNetwork | Select-Object *).DnsAddress
$DomainName = (Get-VMHost "192.168.0.98" | Get-VMHostAuthentication).Domain

A future project is to write a Config-NIC version for ESXi. Another future project is automating VM creation & configuration in Ansible.

Well those are future projects for future howtos.

References

PowerCLI howto: https://www.techrepublic.com/article/how-to-manage-esxi-hosts-remotely-with-powercli/

Qemu: https://qemu.weilnetz.de/w64/

--

--

Rich
Rich

Written by Rich

I work various IT jobs & like Windows domain security as a hobby. Most of what’s here is my notes from auditing or the lab.