11 KiB
LibVirt + OpenTofu VM Provisioning Plan
Overview
This plan outlines how to use LibVirt (virtualization management toolkit) and OpenTofu (Infrastructure as Code tool) to provision and manage virtual machines declaratively.
LibVirt provides a unified API for managing different hypervisors (KVM, QEMU, Xen, etc.), while OpenTofu allows you to define infrastructure using declarative configuration files.
Prerequisites
System Requirements
- Linux host system (Ubuntu/Debian/RHEL/CentOS)
- KVM/QEMU support (check with
lscpu | grep Virtualization) - Sufficient RAM and storage for VMs
- Root/sudo access
Software Installation
1. Install LibVirt and KVM
# Ubuntu/Debian
sudo apt update
sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virt-manager
# RHEL/CentOS/Fedora
sudo dnf install qemu-kvm libvirt libvirt-daemon-system libvirt-clients bridge-utils virt-manager
# Start and enable libvirt service
sudo systemctl start libvirtd
sudo systemctl enable libvirtd
# Add user to libvirt group
sudo usermod -aG libvirt $USER
2. Install OpenTofu
# Download and install OpenTofu
curl -fsSL https://get.opentofu.org/install-opentofu.sh | sh
# Or using package manager (Ubuntu/Debian)
sudo snap install opentofu --classic
# Verify installation
tofu version
3. Verify LibVirt Connection
# Test libvirt connection
virsh list --all
# Check available storage pools
virsh pool-list --all
# Check available networks
virsh net-list --all
Project Structure
vm-infrastructure/
├── main.tf # Main OpenTofu configuration
├── variables.tf # Input variables
├── outputs.tf # Output values
├── terraform.tfvars # Variable values
├── cloud-init/ # Cloud-init configuration files
│ ├── user-data.yaml
│ └── meta-data.yaml
├── images/ # VM base images
└── README.md # Project documentation
Configuration Files
1. Main Configuration (main.tf)
terraform {
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.7.0"
}
}
}
# Configure the LibVirt Provider
provider "libvirt" {
uri = "qemu:///system"
}
# Create a storage pool
resource "libvirt_pool" "vm_pool" {
name = var.pool_name
type = "dir"
path = var.pool_path
}
# Download Ubuntu cloud image
resource "libvirt_volume" "base_image" {
name = "ubuntu-22.04-base.qcow2"
pool = libvirt_pool.vm_pool.name
source = "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img"
format = "qcow2"
}
# Create cloud-init disk
resource "libvirt_cloudinit_disk" "cloudinit" {
name = "cloudinit.iso"
pool = libvirt_pool.vm_pool.name
user_data = file("${path.module}/cloud-init/user-data.yaml")
meta_data = file("${path.module}/cloud-init/meta-data.yaml")
}
# Create VM volumes from base image
resource "libvirt_volume" "vm_disk" {
count = var.vm_count
name = "vm-${count.index + 1}-disk.qcow2"
pool = libvirt_pool.vm_pool.name
base_volume_id = libvirt_volume.base_image.id
size = var.disk_size
}
# Create VMs
resource "libvirt_domain" "vm" {
count = var.vm_count
name = "vm-${count.index + 1}"
memory = var.memory
vcpu = var.vcpu
cloudinit = libvirt_cloudinit_disk.cloudinit.id
network_interface {
network_name = "default"
wait_for_lease = true
}
disk {
volume_id = libvirt_volume.vm_disk[count.index].id
}
console {
type = "pty"
target_port = "0"
target_type = "serial"
}
graphics {
type = "spice"
listen_type = "address"
autoport = true
}
}
2. Variables (variables.tf)
variable "pool_name" {
description = "Name of the storage pool"
type = string
default = "vm-pool"
}
variable "pool_path" {
description = "Path to the storage pool"
type = string
default = "/var/lib/libvirt/images/vm-pool"
}
variable "vm_count" {
description = "Number of VMs to create"
type = number
default = 3
}
variable "memory" {
description = "Memory allocation for each VM (MB)"
type = number
default = 2048
}
variable "vcpu" {
description = "Number of vCPUs for each VM"
type = number
default = 2
}
variable "disk_size" {
description = "Disk size for each VM (bytes)"
type = number
default = 21474836480 # 20GB
}
3. Outputs (outputs.tf)
output "vm_ips" {
description = "IP addresses of the created VMs"
value = {
for i, vm in libvirt_domain.vm : vm.name => vm.network_interface[0].addresses[0]
}
}
output "vm_names" {
description = "Names of the created VMs"
value = libvirt_domain.vm[*].name
}
4. Variable Values (terraform.tfvars)
pool_name = "my-vm-pool"
pool_path = "/var/lib/libvirt/images/my-vm-pool"
vm_count = 3
memory = 4096 # 4GB
vcpu = 2
disk_size = 32212254720 # 30GB
5. Cloud-Init Configuration
User Data (cloud-init/user-data.yaml)
#cloud-config
users:
- name: ubuntu
groups: sudo
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAA... # Your SSH public key
packages:
- curl
- wget
- git
- htop
package_update: true
package_upgrade: true
# Set timezone
timezone: UTC
# Enable SSH
ssh_pwauth: false
# Run commands on first boot
runcmd:
- systemctl enable ssh
- systemctl start ssh
- echo "VM provisioned successfully" > /tmp/provision-complete
Meta Data (cloud-init/meta-data.yaml)
instance-id: vm-instance
local-hostname: vm-host
Step-by-Step Workflow
1. Initial Setup
# Create project directory
mkdir vm-infrastructure
cd vm-infrastructure
# Create subdirectories
mkdir cloud-init images
# Generate SSH key if needed
ssh-keygen -t rsa -b 4096 -C "your-email@example.com"
2. Create Configuration Files
Create all the configuration files listed above in their respective locations.
3. Initialize OpenTofu
# Initialize the project
tofu init
# Validate configuration
tofu validate
# Plan the deployment
tofu plan
4. Deploy VMs
# Apply the configuration
tofu apply
# Confirm with 'yes' when prompted
5. Verify Deployment
# Check VM status
virsh list --all
# Get VM IP addresses
tofu output vm_ips
# SSH into a VM
ssh ubuntu@<vm-ip-address>
6. Management Commands
# Show current state
tofu show
# Update configuration (modify .tf files then)
tofu plan
tofu apply
# Destroy infrastructure
tofu destroy
Advanced Features
Custom Networks
# Create custom network
resource "libvirt_network" "vm_network" {
name = "vm-network"
mode = "nat"
domain = "vm.local"
addresses = ["192.168.100.0/24"]
dhcp {
enabled = true
}
dns {
enabled = true
}
}
# Use custom network in VM
resource "libvirt_domain" "vm" {
# ... other configuration ...
network_interface {
network_id = libvirt_network.vm_network.id
wait_for_lease = true
}
}
Multiple VM Types
# Web servers
resource "libvirt_domain" "web_servers" {
count = 2
name = "web-${count.index + 1}"
memory = 2048
vcpu = 2
# ... other configuration ...
}
# Database server
resource "libvirt_domain" "db_server" {
name = "database"
memory = 4096
vcpu = 4
# ... other configuration ...
}
Storage Volumes
# Additional data volume
resource "libvirt_volume" "data_volume" {
count = var.vm_count
name = "vm-${count.index + 1}-data.qcow2"
pool = libvirt_pool.vm_pool.name
size = 53687091200 # 50GB
}
# Attach to VM
resource "libvirt_domain" "vm" {
# ... other configuration ...
disk {
volume_id = libvirt_volume.data_volume[count.index].id
}
}
Best Practices
1. Resource Organization
- Use consistent naming conventions
- Group related resources in modules
- Use variables for configurable values
- Document your infrastructure
2. State Management
- Store state files securely (consider remote backends)
- Use state locking to prevent concurrent modifications
- Backup state files regularly
3. Security
- Use SSH keys instead of passwords
- Configure proper firewall rules
- Keep base images updated
- Use cloud-init for secure initial configuration
4. Performance
- Size VMs appropriately for workload
- Use thin provisioning for storage
- Monitor resource usage
- Consider CPU topology for multi-vCPU VMs
Troubleshooting
Common Issues
1. Permission Denied
# Ensure user is in libvirt group
sudo usermod -aG libvirt $USER
# Logout and login again
# Check libvirt service status
sudo systemctl status libvirtd
2. Network Issues
# Check default network
virsh net-list --all
virsh net-info default
# Start default network if stopped
virsh net-start default
virsh net-autostart default
3. Storage Pool Issues
# Check storage pools
virsh pool-list --all
# Create default pool if missing
virsh pool-define-as default dir - - - - /var/lib/libvirt/images
virsh pool-build default
virsh pool-start default
virsh pool-autostart default
4. VM Won't Start
# Check VM configuration
virsh dumpxml vm-name
# Check libvirt logs
sudo journalctl -u libvirtd -f
# Validate domain XML
virsh validate vm-name
Debug Commands
# OpenTofu debugging
export TF_LOG=DEBUG
tofu plan
# LibVirt debugging
export LIBVIRT_DEBUG=1
virsh list
# Check VM console
virsh console vm-name
Monitoring and Maintenance
VM Monitoring
# Check VM stats
virsh domstats vm-name
# Monitor VM performance
virsh dominfo vm-name
virsh vcpuinfo vm-name
Backup Strategy
# Backup VM configuration
virsh dumpxml vm-name > vm-name.xml
# Backup VM disk
cp /var/lib/libvirt/images/vm-disk.qcow2 /backup/location/
Updates
# Update base images regularly
tofu apply -replace="libvirt_volume.base_image"
# Update OpenTofu provider
tofu init -upgrade
Conclusion
This plan provides a comprehensive approach to using LibVirt and OpenTofu for VM provisioning. The Infrastructure as Code approach ensures reproducible, version-controlled virtual infrastructure that can be easily managed and scaled.
Key benefits:
- Reproducible: Infrastructure defined in code
- Scalable: Easy to add/remove VMs
- Version Controlled: Track infrastructure changes
- Automated: Minimal manual intervention required
- Consistent: Standardized VM configurations
Start with the basic configuration and gradually add advanced features as needed. Always test changes in a development environment before applying to production.