Proxmox - Automate VM provisioning using Terraform
Welcome back! In this post, I’ll be building on my previous articles by providing a step-by-step guide on how to create and manage virtual machines using Terraform. With this guide, you'll be able to manage your infrastructure as code, bringing automation and consistency to your VM provisioning process.
Previous Post:
Prerequisites
- Terraform - Using Terraform v1.8.5 in this post
- Proxmox Server
- A VM image built using Cloud-Init
- Implemented the Proxmox Image Automation - See Previous Post
Introduction
What is Cloud-Init?
Cloud-Init in Proxmox automates the initial configuration of virtual machines (VMs) during their first boot, making it ideal for large-scale deployments. You can modify settings such as hostnames, create user accounts, configure networking, and install packages without manual intervention.
Let's start!
Create API Credentials in Proxmox
To deploy virtual machines in Proxmox, you will need to create API credentials via Proxmox. See the Previous Post on how to set this up.
You will also need to modify the credentials.auto.tfvars files with the URL of your poroxmox server and API token id/secret
proxmox_api_url = "your-proxmox-url" # Your Proxmox IP Address
proxmox_api_token_id = "your-api-token-id" # API Token ID
proxmox_api_token_secret = "your-api-token-secret" # API Token Secret
ssh_user = "your-ssh-user"
ssh_password = "your-ssh-password"
ssh_host = "your-ssh-host"
Create Terraform files
Clone this repo: https://github.com/phipcode/phiptechblog.git
Navigate to terraform\proxmox\vm_build
Edit environment tf files
Next, you should now define your VM parameters in the environments folder. There should be a file called vms.tfvars. Edit as required per your environment. Ensure you change the below values:
VM to be the same e.g.
"prod-test-01" = {
vm_name = "prod-test-01"
name = "prod-test-01"name = "prod-test-01"
}
vm_id - needs to be unique
You can define as many vms as you want, e.g. example below
vms = {
"prod-test-01" = {
clone = "ubuntu-server-focal-docker-micro"
vm_name = "prod-test-01"
name = "prod-test-01"
vm_cores = 2
vm_memory = 4096
disk_size = "20G"
vm_id = 201
target_node = "proxmicro"
storage = "vm-storage" #storage pool
},
"prod-test-02" = {
clone = "ubuntu-server-focal-docker-micro"
vm_name = "prod-test-02"
name = "prod-test-02"
vm_cores = 2
vm_memory = 4096
disk_size = "20G"
vm_id = 201
target_node = "proxmicro"
storage = "vm-storage" #storage pool
},
}
Change your password - Optional
Next, you might want to create additional users in the VM. Navigate to the cloud-inits folder/cloud-init.cloud_config.tpl and change as required. Use mkpasswd to generate the hash. The password for the user to login should already be set up if you have followed my previous article.
Terraform Init
Now you're finally ready to start deploying the VM. Start by typing terraform init in the console. This will initialise the files, providers plugins, backend for your terraform config
Terraform Plan
Great, now that the files have been initialised, enter the terraform plan command. This will preview changes in your terraform config using variables from the vms.tfvars file
terraform plan -var-file vms.tfvars
Example Terraform Plan Output
Note - I am also creating an ansible account user who is granted sudo privileges and added to the docker group. You can comment this out in the code as this is optional
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# module.vm_build.local_file.cloud_init_user_data_file["prod-test-01"] will be created
+ resource "local_file" "cloud_init_user_data_file" {
+ content = <<-EOT
#cloud-config
hostname: prod-test-01
timezone: Australia/Melbourne
users:
- name: ansible
groups: [adm, sudo]
lock-passwd: false
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
passwd: password
# - or -
# ssh_authorized_keys:
# - your-ssh-key
runcmd:
- echo 'ansible ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ansible
- chmod 440 /etc/sudoers.d/ansible
- usermod -aG docker ansible
final_message: "The system is finally up, after $UPTIME seconds"
EOT
+ content_base64sha256 = (known after apply)
+ content_base64sha512 = (known after apply)
+ content_md5 = (known after apply)
+ content_sha1 = (known after apply)
+ content_sha256 = (known after apply)
+ content_sha512 = (known after apply)
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "../../vm_module/files/user_data_prod-test-01.cfg"
+ id = (known after apply)
}
# module.vm_build.null_resource.cloud_init_config_files["prod-test-01"] will be created
+ resource "null_resource" "cloud_init_config_files" {
+ id = (known after apply)
}
# module.vm_build.proxmox_vm_qemu.ubuntu-server["prod-test-01"] will be created
+ resource "proxmox_vm_qemu" "ubuntu-server" {
+ additional_wait = 5
+ agent = 1
+ automatic_reboot = false
+ balloon = 0
+ bios = "seabios"
+ boot = "order=virtio0"
+ bootdisk = (known after apply)
+ cicustom = "user=local:snippets/user_data_prod-test-01.yml"
+ ciupgrade = false
+ clone = "ubuntu-server-focal-docker-micro"
+ clone_wait = 10
+ cores = 2
+ cpu = "x86-64-v2-AES"
+ default_ipv4_address = (known after apply)
+ default_ipv6_address = (known after apply)
+ define_connection_info = true
+ desc = "Ubuntu Server"
+ force_create = false
+ full_clone = true
+ hotplug = "network,disk,usb"
+ id = (known after apply)
+ ipconfig0 = "ip=dhcp"
+ kvm = true
+ linked_vmid = (known after apply)
+ memory = 4096
+ name = "prod-test-01"
+ onboot = true
+ os_type = "cloud-init"
+ protection = false
+ reboot_required = (known after apply)
+ scsihw = "virtio-scsi-pci"
+ skip_ipv4 = false
+ skip_ipv6 = false
+ sockets = 1
+ ssh_host = (known after apply)
+ ssh_port = (known after apply)
+ tablet = true
+ tags = (known after apply)
+ target_node = "proxmicro"
+ unused_disk = (known after apply)
+ vcpus = 0
+ vm_state = "running"
+ vmid = 201
+ disks {
+ ide {
+ ide3 {
+ cloudinit {
+ storage = "local-lvm"
}
}
}
+ virtio {
+ virtio0 {
+ disk {
+ backup = true
+ format = "raw"
+ id = (known after apply)
+ iops_r_burst = 0
+ iops_r_burst_length = 0
+ iops_r_concurrent = 0
+ iops_wr_burst = 0
+ iops_wr_burst_length = 0
+ iops_wr_concurrent = 0
+ linked_disk_id = (known after apply)
+ mbps_r_burst = 0
+ mbps_r_concurrent = 0
+ mbps_wr_burst = 0
+ mbps_wr_concurrent = 0
+ size = "20G"
+ storage = "vm-storage"
}
}
}
}
+ network {
+ bridge = "vmbr0"
+ firewall = false
+ link_down = false
+ macaddr = (known after apply)
}
}
}
}
+ network {
+ bridge = "vmbr0"
+ firewall = false
+ link_down = false
+ macaddr = (known after apply)
+ model = "virtio"
+ queues = (known after apply)
+ rate = (known after apply)
+ tag = -1
}
}
Plan: 3 to add, 0 to change, 0 to destroy.
Terraform Apply
Now, you're ready to deploy the VM. Type the below command to deploy.
terraform apply -var-file .\vms.tfvars
What Happens Next?
- Review the Plan: Terraform will show you a summary of the changes it will make. Check the details to ensure everything looks correct.
- Confirm: Type
yes
when prompted to start the deployment. - Deployment: Terraform will create your VM and any other defined resources.
When finished, it should look like this in the console
module.vm_build.local_file.cloud_init_user_data_file["smart-home-vm-01"]: Creating...
module.vm_build.local_file.cloud_init_user_data_file["smart-home-vm-01"]: Creation complete after 0s [id=58e04fdec62faab49aa08cdb7a32648a1f7c40d2]
module.vm_build.null_resource.cloud_init_config_files["smart-home-vm-01"]: Creating...
module.vm_build.null_resource.cloud_init_config_files["smart-home-vm-01"]: Provisioning with 'file'...
module.vm_build.proxmox_vm_qemu.ubuntu-server["smart-home-vm-01"]: Creating...
module.vm_build.null_resource.cloud_init_config_files["smart-home-vm-01"]: Creation complete after 0s [id=670910192147341994]
module.vm_build.proxmox_vm_qemu.ubuntu-server["smart-home-vm-01"]: Still creating... [10s elapsed]
module.vm_build.proxmox_vm_qemu.ubuntu-server["smart-home-vm-01"]: Still creating... [20s elapsed]
module.vm_build.proxmox_vm_qemu.ubuntu-server["smart-home-vm-01"]: Still creating... [30s elapsed]
module.vm_build.proxmox_vm_qemu.ubuntu-server["smart-home-vm-01"]: Still creating... [40s elapsed]
module.vm_build.proxmox_vm_qemu.ubuntu-server["smart-home-vm-01"]: Still creating... [50s elapsed]
module.vm_build.proxmox_vm_qemu.ubuntu-server["smart-home-vm-01"]: Creation complete after 57s [id=boss/qemu/300]
Validate Your Deployment
Congratulations! Your virtual machine has been successfully deployed using Terraform. To ensure everything is set up correctly, open the Proxmox GUI and verify the VM's status and configuration.
Optional - Terraform destroy
If you no longer need the resources you created, you can remove them with the terraform destroy
command. This is useful for cleaning up after testing or when you’re done with a project.
To destroy your resources, run:
terraform destroy
What Happens Next?
- Removal Plan: Terraform will show what resources it will delete.
- Confirm: Type
yes
to confirm and proceed with the removal. - Destruction: Terraform will delete the resources.
Use terraform destroy
when you want to clean up resources and avoid unnecessary costs.
Example 1 - Destroy all resources within vms.tfvars.
terraform destroy -var-file C:\repo\phipcode\homelab\InfrastructureAutomation\terraform\vm_build\environments\boss\boss.tfvars in pwsh at 23:45:52
module.vm_build.local_file.cloud_init_user_data_file["smart-home-vm-01"]: Refreshing state... [id=58e04fdec62faab49aa08cdb7a32648a1f7c40d2]
module.vm_build.null_resource.cloud_init_config_files["smart-home-vm-01"]: Refreshing state... [id=8543126241945102410]
module.vm_build.proxmox_vm_qemu.ubuntu-server["smart-home-vm-01"]: Refreshing state... [id=boss/qemu/300]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
- destroy
Terraform will perform the following actions:
# module.vm_build.local_file.cloud_init_user_data_file["smart-home-vm-01"] will be destroyed
- resource "local_file" "cloud_init_user_data_file" {
- content = <<-EOT
#cloud-config
hostname: smart-home-vm-01
timezone: Australia/Melbourne
users:
- name: ansible
groups: [adm, sudo]
lock-passwd: false
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
passwd:
# - or -
# ssh_authorized_keys:
# - your-ssh-key
runcmd:
- echo 'ansible ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ansible
- chmod 440 /etc/sudoers.d/ansible
- usermod -aG docker ansible
final_message: "The system is finally up, after $UPTIME seconds"
EOT -> null
- content_base64sha256 = "8H8MMJa3jQ0E3J5g7+T+1S1fj/dZHlyXfMXKbR7Xi+s=" -> null
- content_base64sha512 = "cjFIUDd0je93YFf7NjurBU02hOVh5amobW+3+YXFR8XL+KCOvPKI2C98xs/JMkxC1yWq4mQh4YSxfcl/IgLLzQ==" -> null
- content_md5 = "163bf1cc8b4e06f2cee4de27f6bffceb" -> null
- content_sha1 = "58e04fdec62faab49aa08cdb7a32648a1f7c40d2" -> null
- content_sha256 = "f07f0c3096b78d0d04dc9e60efe4fed52d5f8ff7591e5c977cc5ca6d1ed78beb" -> null
- content_sha512 = "7231485037748def776057fb363bab054d3684e561e5a9a86d6fb7f985c547c5cbf8a08ebcf288d82f7cc6cfc9324c42d725aae26421e184b17dc97f2202cbcd" -> null
- directory_permission = "0777" -> null
- file_permission = "0777" -> null
- filename = "../../vm_module/files/user_data_smart-home-vm-01.cfg" -> null
- id = "58e04fdec62faab49aa08cdb7a32648a1f7c40d2" -> null
}
# module.vm_build.null_resource.cloud_init_config_files["smart-home-vm-01"] will be destroyed
- resource "null_resource" "cloud_init_config_files" {
- id = "8543126241945102410" -> null
}
# module.vm_build.proxmox_vm_qemu.ubuntu-server["smart-home-vm-01"] will be destroyed
- resource "proxmox_vm_qemu" "ubuntu-server" {
- additional_wait = 5 -> null
- agent = 1 -> null
- automatic_reboot = false -> null
- balloon = 0 -> null
- bios = "seabios" -> null
- boot = "order=virtio0" -> null
- cicustom = "user=local:snippets/user_data_smart-home-vm-01.yml" -> null
- clone = "ubuntu-server-focal-docker-boss" -> null
- clone_wait = 10 -> null
- cores = 4 -> null
- cpu = "x86-64-v2-AES" -> null
- default_ipv4_address = "192.168.0.122" -> null
- default_ipv6_address = "fdfd:b3ca:ab0:36f2:9499:3ff:fe1c:608f" -> null
- define_connection_info = true -> null
- desc = "Ubuntu Server" -> null
- force_create = false -> null
- full_clone = true -> null
- hotplug = "network,disk,usb" -> null
- id = "boss/qemu/300" -> null
- ipconfig0 = "ip=dhcp" -> null
- kvm = true -> null
- linked_vmid = 0 -> null
- memory = 4096 -> null
- name = "smart-home-vm-01" -> null
- numa = false -> null
- onboot = true -> null
- os_type = "cloud-init" -> null
- protection = false -> null
- qemu_os = "other" -> null
- reboot_required = false -> null
- scsihw = "virtio-scsi-pci" -> null
- skip_ipv4 = false -> null
- skip_ipv6 = false -> null
- sockets = 1 -> null
- ssh_host = "192.168.0.122" -> null
- ssh_port = "22" -> null
- tablet = true -> null
tags = null
- target_node = "boss" -> null
- unused_disk = [] -> null
- vcpus = 0 -> null
- vm_state = "running" -> null
- vmid = 300 -> null
# (27 unchanged attributes hidden)
- disks {
- ide {
- ide3 {
- cloudinit {
- storage = "local-lvm" -> null
}
}
}
- virtio {
- virtio0 {
- disk {
- backup = true -> null
- discard = false -> null
- format = "raw" -> null
- id = 0 -> null
- iops_r_burst = 0 -> null
- iops_r_burst_length = 0 -> null
- iops_r_concurrent = 0 -> null
- iops_wr_burst = 0 -> null
- iops_wr_burst_length = 0 -> null
- iops_wr_concurrent = 0 -> null
- iothread = false -> null
- linked_disk_id = -1 -> null
- mbps_r_burst = 0 -> null
- mbps_r_concurrent = 0 -> null
- mbps_wr_burst = 0 -> null
- mbps_wr_concurrent = 0 -> null
- readonly = false -> null
- replicate = false -> null
- size = "20G" -> null
- storage = "local-lvm" -> null
# (4 unchanged attributes hidden)
}
}
}
}
- network {
- bridge = "vmbr0" -> null
- firewall = false -> null
- link_down = false -> null
- macaddr = "96:99:03:1C:60:8F" -> null
- model = "virtio" -> null
- mtu = 0 -> null
- queues = 0 -> null
- rate = 0 -> null
- tag = -1 -> null
}
- smbios {
- uuid = "64049ee6-6da5-478b-80a2-19ae44e3d99c" -> null
# (6 unchanged attributes hidden)
}
}
Plan: 0 to add, 0 to change, 3 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
module.vm_build.null_resource.cloud_init_config_files["smart-home-vm-01"]: Destroying... [id=8543126241945102410]
module.vm_build.null_resource.cloud_init_config_files["smart-home-vm-01"]: Destruction complete after 0s
module.vm_build.local_file.cloud_init_user_data_file["smart-home-vm-01"]: Destroying... [id=58e04fdec62faab49aa08cdb7a32648a1f7c40d2]
module.vm_build.local_file.cloud_init_user_data_file["smart-home-vm-01"]: Destruction complete after 0s
module.vm_build.proxmox_vm_qemu.ubuntu-server["smart-home-vm-01"]: Destroying... [id=boss/qemu/300]
module.vm_build.proxmox_vm_qemu.ubuntu-server["smart-home-vm-01"]: Destruction complete after 4s
Destroy complete! Resources: 3 destroyed.
Change Virtual Machine Config - Optional
If you need to update or remove a virtual machine, you can easily do so by editing the vms.tfvars
file. This allows you to modify specific parameters or entirely remove a VM from the configuration.
1. Modifying VM Configuration
To change a setting, such as increasing the number of CPU cores or memory, simply update the relevant fields in the vms.tfvars
file. Here’s an example configuration:
vms = {
"prod-test-01" = {
clone = "ubuntu-server-focal-docker-micro"
vm_name = "prod-test-01"
name = "prod-test-01"
vm_cores = 4
vm_memory = 4096
disk_size = "20G"
vm_id = 500
target_node = "proxmicro"
storage = "vm-storage" # storage pool
}
}
To change a value (e.g., to increase the CPU cores from 4 to 6), simply edit the relevant field:
vm_cores = 4
2. Removing a VM
If you want to completely remove a virtual machine from your configuration, delete its entry from the vms.tfvars
file. For example, to remove the prod-test-01
VM, simply delete or comment out the entire block corresponding to it:
# "prod-test-01" = {
# clone = "ubuntu-server-focal-docker-micro"
# vm_name = "prod-test-01"
# name = "prod-test-01"
# vm_cores = 4
# vm_memory = 4096
# disk_size = "20G"
# vm_id = 500
# target_node = "proxmicro"
# storage = "vm-storage"
# }
Alternatively, you can remove it entirely, and Terraform will handle the cleanup during the next apply.
3. Applying the Changes
After making your changes, whether modifying or removing a VM, save the file and apply the updates by running:
terraform apply -var-file ./vms.tfvars
This will update your infrastructure by applying the changes made in the vms.tfvars
file, including removing any VMs that were deleted from the list.
Example - Changing the cores for a VM
Found this article useful? Why not buy Phi a coffee to show your appreciation?