vSphere Prerequisites #
PowerShell PowerCLI #
Use PowerShell PowerCLI to list the vSphere details that are necessary to connect with Terraform.
Connect to vSphere #
# Connect to vCenter node: Define PW
Connect-VIServer -Server 192.168.70.70 -User Administrator@vsphere.local -Password my-secure-password
# Shell output:
Name Port User
---- ---- ----
192.168.70.70 443 VSPHERE.LOCAL\Administrator
List vSphere Datacenters #
# List vSphere Datacenters
Get-Datacenter
# Shell output:
Name
----
Datacenter
# List vSphere Datacenters: More details
Get-Datacenter | Select-Object Name, Id, Status
# Shell output:
Name Id Status
---- -- ------
Datacenter Datacenter-datacenter-1001
List vSphere Clusters #
# List vSphere Clusters: All available clusters
Get-Cluster
# Shell output:
Name HAEnabled HAFailover DrsEnabled DrsAutomationLevel
Level
---- --------- ---------- ---------- ------------------
jkw-cluster-1 False 1 True FullyAutomated
jkw-cluster-2 False 1 True FullyAutomated
# List vSphere Clusters: In specific Datacenter
Get-Cluster -Location "Datacenter"
# Shell output:
Name HAEnabled HAFailover DrsEnabled DrsAutomationLevel
Level
---- --------- ---------- ---------- ------------------
jkw-cluster-1 False 1 True FullyAutomated
jkw-cluster-2 False 1 True FullyAutomated
# List vSphere Clusters: In specific Datacenter / specify Id output
Get-Cluster -Location "Datacenter" | Select-Object Name, Id | Format-List
# Shell output:
Name : jkw-cluster-1
Id : ClusterComputeResource-domain-c1012
Name : jkw-cluster-2
Id : ClusterComputeResource-domain-c1017
List vSphere Datastores #
# List vSphere Datastores: All available Datastores
Get-Datastore
# Shell output:
Name FreeSpaceGB CapacityGB
---- ----------- ----------
Datastore1 273,754 349,750
# List vSphere Datastores: In a specific Datacenter
Get-Datastore -Datacenter "Datacenter" | Select-Object Name, Id, CapacityGB, FreeSpaceGB
# Shell output:
Name Id CapacityGB FreeSpaceGB
---- -- ---------- -----------
Datastore1 Datastore-datastore-1009 349,75 273,75390625
List vSphere Networks #
# List vSphere Networks
Get-VirtualNetwork
# Shell output:
Name NetworkType
---- -----------
VM Network Network
# List vSphere Networks: More details
Get-VirtualNetwork | Select-Object Name, Id | Format-List
# Shell output:
Name : VM Network
Id : Network-network-1010
Write Down vSphere Resource Names #
Write down the vSphere resource names and define them for the Terraform provider:
# vSphere Datacenter
Datacenter
# vSphere Cluster
jkw-cluster-1
# vSphere Datastore
Datastore1
# vSphere VM Network
VM Network
Install vSphere root CA Certificate #
Make sure to Install the vSphere root CA certificate on the host where Terraform is run.
Test TLS Encryption #
# Test the TLS encryption with curl
curl https://vcsa.vsphere.local/
# Shell output: Without vSphere root CA certificate
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
Download vSphere Certificate #
# Download the vSphere server certificate
wget https://vcsa.vsphere.local/certs/download.zip --no-check-certificate
# Unzip archive
unzip download.zip
# The unpacked archive looks as follows:
├── certs
│ ├── lin
│ │ ├── 94dfc8ac.0
│ │ └── 94dfc8ac.r0
│ ├── mac
│ │ ├── 94dfc8ac.0
│ │ └── 94dfc8ac.r0
│ └── win
│ ├── 94dfc8ac.0.crt
│ └── 94dfc8ac.r0.crl
└── download.zip
# List certificate details
openssl x509 -in certs/lin/94dfc8ac.0 -text -noout
Install vSphere Certificate #
# Create folder for certificate
sudo mkdir /usr/share/ca-certificates/vsphere
# Copy certificate in ca-certificates directory
sudo cp certs/lin/94dfc8ac.0 /usr/share/ca-certificates/vsphere
# Rename certificate file
sudo mv /usr/share/ca-certificates/vsphere/94dfc8ac.0 /usr/share/ca-certificates/vsphere/94dfc8ac.crt
# Install / uninstall certificate: Start wizard
sudo dpkg-reconfigure ca-certificates
# Verify the certificate is installed
ls -la /etc/ssl/certs | grep vsphere
# Shell output:
lrwxrwxrwx 1 root root 47 Jul 11 20:23 94dfc8ac.pem -> /usr/share/ca-certificates/vsphere/94dfc8ac.crt
Test TLS Encryption #
# Test the TLS encryption with curl
curl https://vcsa.vsphere.local/
# Shell output: With vSphere root CA certificate
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head> ...
Terraform & Packer Prerequisites #
vSphere Hosts Entry #
# Hosts entry
192.168.70.70 vcsa.vsphere.local
Installation #
Install Terraform #
Use the following script to install Terraform ob Debian based Linux distributions:
# Install the HashiCorp GPG key
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null
# Verify the GPG key fingerprint
gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint
# Add the official HashiCorp repository
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
# Install Terraform
sudo apt update && sudo apt-get install terraform
Verify the installation:
# Verify the installation / check version
terraform version
# Shell output:
Terraform v1.9.1
on linux_amd64
Install Packer #
# Install Packer dependencies
sudo apt update &&
sudo apt install xorriso genisoimage -y
# Install packer (From official HashiCorp repository)
sudo apt update &&
sudo apt install packer
Verify the installation:
# Verify the installation / check version
packer --version
# Shell output:
Packer v1.11.1
Example: Create Simple VM #
The following example creates a new virtual machine that boots from an ISO file. The installation is done manually, this setup is just for testing and troubleshooting.
-
In this example, the vSphere resources like “Datacenter”, “Datastore”, “Cluster” and “VM Network” are defined directly in the “provider.tf” configuration.
-
The vSphere credentials are defined as variables in “vars.tf” configuration and the credential values are define in “terraform.tfvars”.
-
The actual virtual machine is defined in the “main.tf” configuration.
The file structure looks as follows:
├── main.tf
├── provider.tf
├── terraform.tfstate
├── terraform.tfstate.backup
├── terraform.tfvars
└── vars.tf
provider.tf #
# vSphere Provider
provider "vsphere" {
user = var.vsphere_user
password = var.vsphere_password
vsphere_server = var.vsphere_server
allow_unverified_ssl = true # Disable if vSphere root CA certificate is installed
api_timeout = 10
}
# vSphere Datacenter
data "vsphere_datacenter" "datacenter" {
name = "Datacenter"
}
# vSphere Datastore
data "vsphere_datastore" "datastore" {
name = "Datastore1"
datacenter_id = data.vsphere_datacenter.datacenter.id
}
# vSphere Cluster
data "vsphere_compute_cluster" "cluster" {
name = "jkw-cluster-1"
datacenter_id = data.vsphere_datacenter.datacenter.id
}
# vSphere VM Network
data "vsphere_network" "network" {
name = "VM Network"
datacenter_id = data.vsphere_datacenter.datacenter.id
}
vars.tf #
# vars.tf
variable "vsphere_user" {
type = string
description = "User name for vSphere API operations."
}
variable "vsphere_password" {
type = string
description = "Password for vSphere API operations."
sensitive = true // This will prevent the password from being displayed in logs or CLI output
}
variable "vsphere_server" {
type = string
description = "vSphere server for API operations."
}
terraform.tfvars #
# terraform.tfvars
vsphere_user = "Administrator@vsphere.local"
vsphere_password = "my-secure-pw"
vsphere_server = "vcsa.vsphere.local"
vSphere VM #
# main.tf
resource "vsphere_virtual_machine" "vm" {
name = "vm1"
resource_pool_id = data.vsphere_compute_cluster.cluster.resource_pool_id
datastore_id = data.vsphere_datastore.datastore.id
num_cpus = 1
memory = 2048
guest_id = "otherLinux64Guest"
network_interface {
network_id = data.vsphere_network.network.id
}
disk {
label = "disk0"
size = 20
}
cdrom {
datastore_id = data.vsphere_datastore.datastore.id
path = "ISO/ubuntu-24.04-live-server-amd64.iso"
}
}
Terraform Apply #
# Check for any syntax errors or inconsistencies in the Terraform configuration files
terraform validate
# List actions Terraform will take when run "terraform apply"
terraform plan
# Deploy the resources
terraform apply
# Deploy the resources: Skip approval
terraform apply -auto-approve
Example: Deploy VM from Template #
Packer Template #
The following Packer configuration creates an Ubuntu-based VMware vSphere template tf-ubuntu22.04
designed for cloning with Terraform.
I’m using an Ubuntu 22.04 image located in vSphere: Datestore1/ISO/ubuntu-22.04.3-live-server-amd64.iso
GitHub repository: https://github.com/jueklu/packer-vsphere
Packer File Structure #
# Packer file structure
├── meta-data
├── user-data
├── variables.pkr.hcl
├── vars.auto.pkrvars.hcl
└── vsphere-iso_basic_ubuntu.pkr.hcl
Cloud-Init: “user-data” #
#cloud-config
autoinstall:
version: 1
identity:
hostname: ubuntu
password: "$6$exDY1mhS4KUYCE/2$zmn9ToZwTKLhCw.b4/b.ZRTIZM30JZ4QrOQ2aOXJ8yk96xpcCof0kxKwuX1kqLG/ygbJ1f8wxED22bTL4F46P0"
username: ubuntu
locale: "de_DE.UTF-8"
keyboard:
layout: "de"
ssh:
install-server: true
allow-pw: true
packages:
- open-vm-tools
Variables “variables.pkr.hcl” #
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
variable "vsphere_server" {
type = string
default = ""
}
variable "vsphere_user" {
type = string
default = ""
}
variable "vsphere_password" {
type = string
default = ""
}
variable "datacenter" {
type = string
default = ""
}
variable "cluster" {
type = string
default = ""
}
variable "datastore" {
type = string
default = ""
}
variable "network_name" {
type = string
default = ""
}
Variables Values: “vars.auto.pkrvars.hcl” #
# vars.auto.pkrvars.hcl
vsphere_server = "vcsa.vsphere.local"
vsphere_user = "Administrator@vsphere.local"
vsphere_password = "my-secure-pw"
datacenter = "Datacenter"
cluster = "jkw-cluster-1"
datastore = "Datastore1"
network_name = "VM Network"
vars.auto.pkrvars.hcl #
# vsphere-iso_basic_ubuntu.pkr.hcl
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
packer {
required_plugins {
vsphere = {
version = ">= 1.2.3"
source = "github.com/hashicorp/vsphere"
}
}
}
source "vsphere-iso" "this" {
vcenter_server = var.vsphere_server
username = var.vsphere_user
password = var.vsphere_password
datacenter = var.datacenter
cluster = var.cluster
insecure_connection = true
vm_name = "tf-ubuntu22.04" # Define VM template name
guest_os_type = "ubuntu64Guest"
CPUs = 2
RAM = 2048
RAM_reserve_all = true
ssh_username = "ubuntu"
ssh_password = "ubuntu"
ssh_timeout = "30m"
/* Uncomment when running on vcsim
ssh_host = "127.0.0.1"
ssh_port = 2222
configuration_parameters = {
"RUN.container" : "lscr.io/linuxserver/openssh-server:latest"
"RUN.mountdmi" : "false"
"RUN.port.2222" : "2222"
"RUN.env.USER_NAME" : "ubuntu"
"RUN.env.USER_PASSWORD" : "ubuntu"
"RUN.env.PASSWORD_ACCESS" : "true"
}
*/
disk_controller_type = ["pvscsi"]
datastore = var.datastore
storage {
disk_size = 16384
disk_thin_provisioned = true
}
# Define ISO path
iso_paths = ["[Datastore1] ISO/ubuntu-22.04-live-server-amd64.iso"]
network_adapters {
network = var.network_name
}
cd_files = ["./meta-data", "./user-data"]
cd_label = "cidata"
boot_command = ["<wait>e<down><down><down><end> autoinstall ds=nocloud;<F10>"]
}
build {
sources = [
"source.vsphere-iso.this"
]
provisioner "shell-local" {
inline = ["echo the address is: $PACKER_HTTP_ADDR and build name is: $PACKER_BUILD_NAME"]
}
}
Create ISO File #
Initialize Packer Configuration #
# Initialize the Packer configuration
packer init .
# Shell output:
Installed plugin github.com/hashicorp/vsphere v1.3.0 in "/home/ubuntu/.config/packer/plugins/github.com/hashicorp/vsphere/packer-plugin-vsphere_v1.3.0_x5.0_linux_amd64
Build the Ubuntu Template #
# Build image
packer build .
# Shell output:
vsphere-iso.this: output will be in this color.
==> vsphere-iso.this: Creating CD disk...
vsphere-iso.this: xorriso 1.5.6 : RockRidge filesystem manipulator, libburnia project.
vsphere-iso.this: Drive current: -outdev 'stdio:/tmp/packer273668188.iso'
vsphere-iso.this: Media current: stdio file, overwriteable
vsphere-iso.this: Media status : is blank
vsphere-iso.this: Media summary: 0 sessions, 0 data blocks, 0 data, 23.9g free
vsphere-iso.this: xorriso : WARNING : -volid text does not comply to ISO 9660 / ECMA 119 rules
vsphere-iso.this: Added to ISO image: directory '/'='/tmp/packer_to_cdrom1249690970'
vsphere-iso.this: xorriso : UPDATE : 2 files added in 1 seconds
vsphere-iso.this: xorriso : UPDATE : 2 files added in 1 seconds
vsphere-iso.this: ISO image produced: 185 sectors
vsphere-iso.this: Written to medium : 185 sectors at LBA 0
vsphere-iso.this: Writing to 'stdio:/tmp/packer273668188.iso' completed successfully.
vsphere-iso.this: Done copying paths from CD_dirs
==> vsphere-iso.this: Uploading packer273668188.iso to [Datastore1] packer_cache...
==> vsphere-iso.this: Creating virtual machine...
==> vsphere-iso.this: Customizing hardware...
==> vsphere-iso.this: Mounting ISO images...
==> vsphere-iso.this: Adding configuration parameters...
==> vsphere-iso.this: Set boot order temporary...
==> vsphere-iso.this: Power on VM...
==> vsphere-iso.this: Waiting 10s for boot...
==> vsphere-iso.this: Typing boot command...
==> vsphere-iso.this: Waiting for IP...
==> vsphere-iso.this: IP address: 192.168.70.254
==> vsphere-iso.this: Using SSH communicator to connect: 192.168.70.254
==> vsphere-iso.this: Waiting for SSH to become available...
==> vsphere-iso.this: Connected to SSH!
==> vsphere-iso.this: Running local shell script: /tmp/packer-shell3240343788
vsphere-iso.this: the address is: 192.168.30.14:0 and build name is: this
==> vsphere-iso.this: Shutting down VM...
==> vsphere-iso.this: Deleting Floppy drives...
==> vsphere-iso.this: Ejecting CD-ROM media...
==> vsphere-iso.this: Clear boot order...
vsphere-iso.this: Closing sessions ....
Build 'vsphere-iso.this' finished after 7 minutes 21 seconds.
==> Wait completed after 7 minutes 21 seconds
==> Builds finished. The artifacts of successful builds are:
--> vsphere-iso.this: tf-ubuntu22.04
Terraform Configuration #
Overview #
This Terraform project demonstrates how to deploy a new VM from the VM template previously created with Packer, and assigns a specified IP address to the VM.
The project file structure looks like this:
├── main.tf
├── provider.tf
├── terraform.tfvars
└── vars.tf
GitHub repository: https://github.com/jueklu/terraform-vsphere-example
Provider: provider.tf #
# vSphere Provider
provider "vsphere" {
user = var.vsphere_user
password = var.vsphere_password
vsphere_server = var.vsphere_server
allow_unverified_ssl = true
api_timeout = 10
}
# vSphere Datacenter
data "vsphere_datacenter" "datacenter" {
name = var.datacenter_name
}
# vSphere Datastore
data "vsphere_datastore" "datastore" {
name = var.datastore_name
datacenter_id = data.vsphere_datacenter.datacenter.id
}
# vSphere Cluster
data "vsphere_compute_cluster" "cluster" {
name = var.compute_cluster_name
datacenter_id = data.vsphere_datacenter.datacenter.id
}
# vSphere VM Network
data "vsphere_network" "network" {
name = var.vm_network_name
datacenter_id = data.vsphere_datacenter.datacenter.id
}
Variables: vars.tf #
# vSphere Authentication
variable "vsphere_user" {
type = string
description = "User name for vSphere API operations."
}
variable "vsphere_password" {
type = string
description = "Password for vSphere API operations."
sensitive = true // This will prevent the password from being displayed in logs or CLI output
}
variable "vsphere_server" {
type = string
description = "vSphere server for API operations."
}
# vSphere Resources
variable "datacenter_name" {
description = "Name of the vSphere datacenter"
type = string
}
variable "datastore_name" {
description = "Name of the vSphere datastore"
type = string
}
variable "compute_cluster_name" {
description = "Name of the vSphere compute cluster"
type = string
}
variable "vm_network_name" {
description = "Name of the VM network"
type = string
}
# VM Template
variable "datacenter" {
description = "vSphere data center"
type = string
}
variable "ubuntu_name" {
description = "Ubuntu name (ie: image_path)"
type = string
}
Variable Values: terraform.tfvars #
# vSphere Authentication
vsphere_user = "Administrator@vsphere.local"
vsphere_password = "my-secure-pw"
vsphere_server = "vcsa.vsphere.local"
# vSphere Resources
datacenter_name = "Datacenter"
datastore_name = "Datastore1"
compute_cluster_name = "jkw-cluster-1"
vm_network_name = "VM Network"
# VM Template
datacenter = "Datacenter"
ubuntu_name = "tf-ubuntu24.04"
Deploy VMs: main.tf #
Deploy Single VM
# Define template
data "vsphere_virtual_machine" "ubuntu" {
name = "/${var.datacenter}/vm/${var.ubuntu_name}"
datacenter_id = data.vsphere_datacenter.datacenter.id
}
# virtual machine configuration
resource "vsphere_virtual_machine" "vm" {
name = "vm1" # Define VM name in vSphere
resource_pool_id = data.vsphere_compute_cluster.cluster.resource_pool_id
datastore_id = data.vsphere_datastore.datastore.id
num_cpus = 2
memory = 2048
network_interface {
network_id = data.vsphere_network.network.id
}
wait_for_guest_net_timeout = -1
wait_for_guest_ip_timeout = -1
disk {
label = "disk0"
thin_provisioned = true
size = 16
}
guest_id = "ubuntu64Guest" # Define vSphere VM type
# Clone VM from template
clone {
template_uuid = data.vsphere_virtual_machine.ubuntu.id
# Customize the new VM
customize {
linux_options {
host_name = "ubuntu-2"
domain = "local"
}
network_interface {
ipv4_address = "192.168.70.111"
ipv4_netmask = 24
}
ipv4_gateway = "192.168.70.1"
}
}
}
Deploy Multiple VMs: Ascending names
The following configuration below creates VMs with ascending names / hostnames and IP addresses:
# name hostname IP
vm1 ubuntu1 192.168.70.120
vm2 ubuntu2 192.168.70.121
vm3 ubuntu3 192.168.70.122
# Define template
data "vsphere_virtual_machine" "ubuntu" {
name = "/${var.datacenter}/vm/${var.ubuntu_name}"
datacenter_id = data.vsphere_datacenter.datacenter.id
}
# virtual machine configuration
resource "vsphere_virtual_machine" "vm" {
count = 3 # Number of VMs to create
name = "vm${count.index + 1}" # Dynamically create VM name
resource_pool_id = data.vsphere_compute_cluster.cluster.resource_pool_id
datastore_id = data.vsphere_datastore.datastore.id
num_cpus = 2
memory = 2048
network_interface {
network_id = data.vsphere_network.network.id
}
wait_for_guest_net_timeout = -1
wait_for_guest_ip_timeout = -1
disk {
label = "disk0"
thin_provisioned = true
size = 16
}
guest_id = "ubuntu64Guest" # Define vSphere VM type
# Clone VM from template
clone {
template_uuid = data.vsphere_virtual_machine.ubuntu.id
# Customize the new VM
customize {
linux_options {
host_name = "ubuntu-${count.index + 1}" # Dynamically create hostname
domain = "local"
}
network_interface {
ipv4_address = "192.168.70.12${count.index}" # Assign unique IP for each VM
ipv4_netmask = 24
}
ipv4_gateway = "192.168.70.1"
}
}
}
Deploy Multiple VMs: Custom names, create vSphere folder
The following configuration below creates VMs with custom names / hostnames and ascending IP addresses. The VMs are deployed in a folder named “Terraform-Cluster-1”:
# name IP
controller1 192.168.70.120
controller2 192.168.70.121
controller3 192.168.70.122
worker1 192.168.70.123
worker2 192.168.70.124
# Define template
data "vsphere_virtual_machine" "ubuntu" {
name = "/${var.datacenter}/vm/${var.ubuntu_name}"
datacenter_id = data.vsphere_datacenter.datacenter.id
}
# Create vSphere folder for the VMs
resource "vsphere_folder" "vm_folder" {
path = "Terraform-Cluster-1" # Define the folder name
type = "vm" # Folder type (vm for virtual machines)
datacenter_id = data.vsphere_datacenter.datacenter.id
}
# Define VM names
locals {
vm_names = ["controller1", "controller2", "controller3", "worker1", "worker2"]
}
# virtual machine configuration
resource "vsphere_virtual_machine" "vm" {
count = 5 # Number of VMs to create
name = local.vm_names[count.index] # Use custom names from local variables
folder = vsphere_folder.vm_folder.path # Place VMs in the folder
resource_pool_id = data.vsphere_compute_cluster.cluster.resource_pool_id
datastore_id = data.vsphere_datastore.datastore.id
num_cpus = 2
memory = 2048
network_interface {
network_id = data.vsphere_network.network.id
}
wait_for_guest_net_timeout = -1
wait_for_guest_ip_timeout = -1
disk {
label = "disk0"
thin_provisioned = true
size = 16
}
guest_id = "ubuntu64Guest" # Define vSphere VM type
# Clone VM from template
clone {
template_uuid = data.vsphere_virtual_machine.ubuntu.id
# Customize the new VM
customize {
linux_options {
host_name = local.vm_names[count.index] # Use custom hostnames from local variables
domain = "local"
}
network_interface {
ipv4_address = "192.168.70.12${count.index}" # Assign unique IP for each VM
ipv4_netmask = 24
}
ipv4_gateway = "192.168.70.1"
}
}
}
Deploy Resources #
# Initialize repository / add the vSphere provider
terraform init
# Check for any syntax errors or inconsistencies in the Terraform configuration files
terraform validate
# List actions Terraform will take when run "terraform apply"
terraform plan
# Deploy the resources
terraform apply
# Deploy the resources: Skip approval
terraform apply -auto-approve
Verify the Deployment in vSphere #
Deploy Multiple VMs: Ascending names
Deploy Multiple VMs: Custom names, create vSphere folder:
Delete the Deployment #
# Destroy VMs
terraform destroy
Links #
# Install Packer
https://developer.hashicorp.com/packer/tutorials/docker-get-started/get-started-install-cli
# Packer Tutorial
https://developer.hashicorp.com/terraform/tutorials/virtual-machine/vsphere-provider
# Terraform vSphere Provider
https://registry.terraform.io/providers/hashicorp/vsphere/latest/docs
# Ubuntu Cloud Images
https://cloud-images.ubuntu.com/noble/current/