Post

Automating Virtual Machine Provisioning with Terraform and Libvirt

Step-by-step guide to automating virtual machine provisioning with Terraform, Libvirt, and Cloud-init on Linux. Includes project structure, setup instructions, configuration, commands, and usage.

Automating Virtual Machine Provisioning with Terraform and Libvirt

In today’s fast-paced IT world, efficiently setting up and managing virtual machines (VMs) is essential for development, testing, and deployment. Manual VM setup can be time-consuming and prone to errors, leading to inconsistencies and delays. This is where Infrastructure as Code (IaC) tools like Terraform come in handy, transforming how we define, deploy, and manage infrastructure.

This guide will walk you through a practical Terraform project that automates VM provisioning using Libvirt, a robust virtualization management library, and Cloud-init, a popular tool for initial VM configuration.

By the end of this guide, you’ll know how to use these technologies to create a secure, flexible, and automated VM deployment workflow, keeping sensitive information out of version control. You’ll learn about the core components, project structure, environment setup, configuration, usage, and essential virsh commands for managing your VMs.

Core Components

To effectively automateing VM provisioning, first you need to understand the key technologies involved: Terraform, Libvirt, and Cloud-init.

Terraform: Infrastructure as Code (IaC)

Terraform is an open-source IaC tool developed by HashiCorp. It allows you to define and provision infrastructure using a declarative configuration language. Instead of manually setting up resources, you describe the desired state of your infrastructure in configuration files, and Terraform handles the creation, modification, and deletion of resources to match that state.

This brings several benefits, including:

  • Automation: Eliminates manual processes, reducing human error and speeding up deployments.
  • Consistency: Ensures that your infrastructure is provisioned identically every time.
  • Version Control: Configurations can be stored in version control systems, allowing for tracking changes, collaboration, and rollbacks.
  • Reusability: Modules and configurations can be reused across different projects and environments.

Libvirt: Virtualization Management

Libvirt is an open-source API, daemon, and management tool for managing platform virtualization. It supports various virtualization technologies, including KVM, QEMU, Xen, VMware ESX, and more. In this project, we use Libvirt to interact with KVM (Kernel-based Virtual Machine) on a Linux host, enabling us to create, configure, and manage virtual machines programmatically. Libvirt provides a stable and consistent interface for controlling virtualization capabilities.

Cloud-init: Automating Initial VM Configuration

Cloud-init is the industry standard multi-distribution package for early initialization of cloud instances. It is used to perform initial setup tasks on a VM when it first boots, such as:

  • Setting hostname
  • Configuring network interfaces
  • Adding users and SSH keys
  • Installing packages
  • Running custom scripts

By using Cloud-init, we can ensure that our newly provisioned VMs are immediately configured with the necessary users, SSH access, and network settings without manual intervention after the VM is created.

Why This Combination is Powerful

Terraform, Libvirt, and Cloud-init creates a robust and efficient solution for VM provisioning:

  • Terraform defines the VM’s infrastructure (disk, network, domain) and orchestrates the deployment.
  • Libvirt provides the underlying virtualization capabilities to host the VM.
  • Cloud-init handles the guest operating system’s initial setup, making the VM ready for use immediately after deployment.

This combination allows for fully automated, repeatable, and scalable VM deployments, making it an ideal solution for development environments, testing labs, or even small-scale production setups.

Project Structure and Key Files

A well-organized project structure is key to managing Infrastructure as Code effectively. This Terraform project consists of several .tf files, along with configuration files for Cloud-init, each serving a specific purpose.

main.tf: Orchestrating VM Resources

This is the core of our Terraform configuration, where the primary resources for the virtual machine are defined.

libvirt_volume: Image Management

This resource defines the virtual disk volume for our VM. It specifies the name, the storage pool it belongs to (e.g., default), the source image path, and the disk format (qcow2).

1
2
3
4
5
6
resource "libvirt_volume" "ubuntu-qcow2" {
  name   = "ubuntu-qcow2"
  pool   = "default"
  source = "file://${pathexpand(var.image_source_path)}"  # ubuntu cloud image
  format = "qcow2"
}

template_file (user_data & network_config): Dynamic Configuration

Terraform’s template_file data source allows us to dynamically generate configuration files based on variables. Here, it’s used to process cloud_init.yaml and network_config.cfg, injecting values like the SSH public key.

1
2
3
4
5
6
7
8
9
10
data "template_file" "user_data" {
  template = file("${path.module}/cloud_init.yaml")
  vars = {
    ssh_public_key = var.ssh_public_key
  }
}

data "template_file" "network_config" {
  template = file("${path.module}/network_config.cfg")
}

libvirt_cloudinit_disk: Attaching Cloud-init Data

This resource creates a virtual CD-ROM disk containing the rendered Cloud-init configuration. This disk is then attached to the VM, allowing Cloud-init to execute its setup tasks on first boot.

1
2
3
4
5
resource "libvirt_cloudinit_disk" "commoninit" {
  name           = "commoninit.iso"
  user_data      = data.template_file.user_data.rendered
  network_config = data.template_file.network_config.rendered
}

libvirt_domain: Defining the VM

This is the central resource that defines the virtual machine itself. It specifies the VM’s name, memory, vCPUs, and links to the Cloud-init disk and the primary disk volume. It also configures the network interface and console access.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
resource "libvirt_domain" "domain-ubuntu" {
  name   = "ubuntu-vm"
  memory = var.vm_memory
  vcpu   = var.vm_vcpu

  cloudinit = libvirt_cloudinit_disk.commoninit.id

  network_interface {
    network_name   = var.network_name
    wait_for_lease = true
  }

  console {
    type        = "pty"
    target_type = "virtio"
    target_port = "1"
  }

  disk {
    volume_id = libvirt_volume.ubuntu-qcow2.id
  }

  graphics {
    type        = "spice"
    listen_type = "address"
    autoport    = true
  }
}

resource "null_resource" "wait_for_ip" {
  depends_on = [libvirt_domain.domain-ubuntu]
}

cloud_init.yaml: User and SSH Key Setup

This YAML file is a standard Cloud-init configuration. It defines initial system settings, including enabling SSH password authentication, setting a root password (for initial access, though SSH is preferred), and creating a ubuntu user with sudo privileges and your provided SSH public key for secure access.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#cloud-config
# vim: syntax=yaml
# examples:
# https://cloudinit.readthedocs.io/en/latest/topics/examples.html
---
ssh_pwauth: true
disable_root: false
chpasswd:
  list: |
    root:password
  expire: false
users:
  - name: ubuntu
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: users, admin
    home: /home/ubuntu
    shell: /bin/bash
    lock_passwd: false
    ssh-authorized-keys:
      - ${ssh_public_key}

network_config.cfg: Network Interface Configuration (DHCP)

This file, also processed by Cloud-init, configures the network interface of the VM. In this case, it sets up the ens3 interface to obtain an IP address via DHCP.

1
2
3
4
version: 2
ethernets:
  ens3:
    dhcp4: true

variables.tf: Customizing the Deployment

This file defines the input variables for your Terraform project. These variables allow you to customize aspects of your VM, such as the SSH public key, the path to the cloud image, VM memory, vCPU count, and the network name, without modifying the main configuration files.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
variable "ssh_public_key" {
  description = "The SSH public key to be added to the authorized_keys"
  type        = string
}

variable "image_source_path" {
  description = "Path to the cloud image"
  type        = string
  default     = "~/Downloads/noble-server-cloudimg-amd64.img"
}

variable "vm_memory" {
  description = "The amount of memory for the VM in MB"
  type        = number
  default     = 512
}

variable "vm_vcpu" {
  description = "The number of vCPUs for the VM"
  type        = number
  default     = 1
}

variable "network_name" {
  description = "The name of the network to attach the VM to"
  type        = string
  default     = "default"
}

outputs.tf: Retrieving VM Information (IP Address)

The outputs.tf file defines output values that Terraform will display after a successful apply. In this project, it’s used to easily retrieve the IP address assigned to your newly provisioned VM.

1
2
3
output "ip_address" {
  value = libvirt_domain.domain-ubuntu.network_interface[0].addresses[0]
}

Setting Up Your Environment (Prerequisites)

Before you can deploy your virtual machines with Terraform and Libvirt, you need to ensure your system has the necessary tools installed and configured.

Installing Terraform

Terraform is available for various operating systems. You can download the appropriate package from the official Terraform website and follow their installation instructions. Alternatively, you can use a package manager if available for your system.

Installing KVM/Libvirt on Debian/Ubuntu

For Debian/Ubuntu-based systems, you can install the required KVM and Libvirt packages using apt:

1
2
sudo apt update
sudo apt install -y qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virt-manager cloud-image-utils

After installation, it is often necessary to adjust the Libvirt configuration to prevent common permission issues. Edit the /etc/libvirt/qemu.conf file and set security_driver = "none". This change allows the libvirtd daemon to manage VMs without strict security contexts that might interfere with Terraform’s operations.

1
2
sudo sed -i 's/#security_driver = "tap"/security_driver = "none"/' /etc/libvirt/qemu.conf
sudo systemctl restart libvirtd

Remember to restart the libvirtd service after making changes to its configuration for them to take effect.

Configuration: Making it Your Own

This project is designed to be flexible and secure, allowing you to customize the VM deployment without hardcoding sensitive information. This is achieved through the use of Terraform variables and a .tfvars file.

Creating terraform.tfvars File

The terraform.tfvars file is where you’ll store your environment-specific or sensitive variables. It’s crucial that this file is ignored by your version control system (e.g., via .gitignore) to prevent accidental exposure of sensitive data like SSH private keys. The project includes a terraform.tfvars.example file to guide you.

To get started, simply copy the example file:

1
cp terraform.tfvars.example terraform.tfvars

Providing ssh_public_key and image_source_path

Open the newly created terraform.tfvars file and provide the required values:

  • ssh_public_key: Your SSH public key. This key will be added to the ubuntu user’s authorized_keys file on the VM, allowing you to securely connect via SSH.
  • image_source_path: The local path to the cloud image you wish to use for your VM. The default value is ~/Downloads/noble-server-cloudimg-amd64.img, but you can change it to any compatible QCOW2 image.

Example terraform.tfvars content:

1
2
ssh_public_key    = "ssh-rsa AAAAB3NzaC..."
image_source_path = "/path/to/your/noble-server-cloudimg-amd64.img"

Usage: Deploying and Managing Your VM

Once your environment is set up and your terraform.tfvars file is configured, you can use the standard Terraform workflow to deploy and manage your virtual machine.

terraform init: Initializing the Project

This command initializes the working directory containing your Terraform configuration files. It downloads the necessary provider plugins (in this case, the libvirt provider) and sets up the backend for state management. You should run this command once when you start a new Terraform project or when you add new providers.

1
terraform init

terraform plan: Previewing Changes

The terraform plan command generates an execution plan, showing you exactly what actions Terraform will take to achieve the desired state defined in your configuration. It’s a crucial step for reviewing changes before applying them, helping to prevent unintended modifications to your infrastructure.

1
terraform plan

terraform apply: Creating the VM

This command applies the changes required to reach the desired state of the configuration. Terraform will prompt you for confirmation before proceeding. Upon confirmation, it will provision the virtual machine and any other defined resources.

1
terraform apply

terraform destroy: Tearing Down the Infrastructure

When you no longer need the virtual machine, you can use the terraform destroy command to remove all resources managed by your Terraform configuration. This command also requires confirmation before execution.

1
terraform destroy

Essential Libvirt (virsh) Commands

While Terraform manages the lifecycle of your VMs, virsh is the command-line utility for directly interacting with Libvirt. It’s invaluable for monitoring, troubleshooting, and performing quick management tasks on your virtual machines.

Here are some of the most essential virsh commands:

CommandDescription
virsh list --allList all virtual machines (running and stopped).
virsh start <vm-name>Start a virtual machine.
virsh shutdown <vm-name>Gracefully shut down a virtual machine.
virsh destroy <vm-name>Forcefully stop a virtual machine (like pulling the power plug).
virsh reboot <vm-name>Reboot a virtual machine.
virsh console <vm-name>Connect to the virtual machine’s console for direct interaction.
virsh dominfo <vm-name>Show detailed information about a virtual machine.
virsh domifaddr <vm-name>Show the IP address(es) of a virtual machine.
virsh net-list --allList all virtual networks configured in Libvirt.
virsh pool-list --allList all storage pools.
virsh vol-list <pool-name>List all volumes (disks) in a specified storage pool.

Connecting to the System Libvirt

By default, virsh often connects to the session instance, which manages VMs created by your user. To manage system-wide VMs (which Terraform typically creates), you need to connect to the system instance. You can do this in two ways:

  1. Use the -c flag:

Specify the system URI with the -c flag for a single command:

1
virsh -c qemu:///system list --all
  1. Set the LIBVIRT_DEFAULT_URI environment variable:

For persistent access to the system instance, add the following line to your shell’s configuration file (e.g., ~/.bashrc or ~/.zshrc):

1
export LIBVIRT_DEFAULT_URI="qemu:///system"

After adding the line, reload your shell configuration:

1
source ~/.bashrc  # or source ~/.zshrc

Conclusion

Automating virtual machine provisioning with Terraform and Libvirt offers a powerful and efficient approach to infrastructure management. By embracing Infrastructure as Code, you gain significant benefits, including:

  • Automation: Streamlining the entire VM setup process, from disk creation to initial configuration.
  • Consistency: Ensuring that every VM is provisioned identically, reducing configuration drift and errors.
  • Security: Keeping sensitive information like SSH keys out of version control through .tfvars files.
  • Scalability: Easily deploying multiple VMs with minimal effort.
  • Repeatability: Recreating your environment reliably whenever needed.

This project provides a solid foundation for managing your local virtualization infrastructure. As next steps, you might consider:

  • More Complex Networking: Implementing custom Libvirt networks with specific IP ranges or VLANs.
  • Multiple VMs: Extending the configuration to deploy a cluster of interconnected VMs for a multi-tier application.
  • Different Operating Systems: Experimenting with other cloud images (e.g., CentOS, Fedora) and adapting Cloud-init configurations.
  • Provisioning Tools: Integrating configuration management tools like Ansible or Puppet for post-deployment software installation and configuration.

I encourage you to explore this project, adapt it to your needs, and experience the benefits of automated VM provisioning.

This post is licensed under CC BY 4.0 by the author.