KVM: Terraform and cloud-init to create local KVM resources

Terraform is a popular tool for provisioning infrastructure on cloud provider such as EC2 and Azure, but there is also a provider written for local KVM libvirt resources.

Using the libvirt provider, we can use standard Terraform constructs to create local VMs, networks, and disks.

Prerequisite KVM

As a prerequisite for this article, you must install KVM and libvirt as described here.

Install Terraform

Find the latest version of the binaries at the Terraform download page, download.

sudo apt-get install jq unzip -y

# explicitly choose version OR pull latest using github api
export TERRA_VERSION=0.12.20
export TERRA_VERSION=$(curl -sL https://api.github.com/repos/hashicorp/terraform/releases/latest | jq -r ".tag_name" | cut -c2-)

# download
wget https://releases.hashicorp.com/terraform/${TERRA_VERSION}/terraform_${TERRA_VERSION}_linux_amd64.zip

# unzip
unzip terraform_${TERRA_VERSION}_linux_amd64.zip

# set permissions and move into path
chmod +x terraform
sudo mv terraform /usr/local/bin/.

# validate
terraform version

Install libvirt provider plugin

Use prebuilt

If you are using Ubunti bionic 18.04, then go to the latest “releases” page for the terraform-provider-libvirt project, and download the latest binary plugin.

Copy the plugin into the “~/.terraform.d” plugins directory

# make plugins directory
mkdir -p ~/.terraform.d/plugins && cd $_

# download
wget https://github.com/dmacvicar/terraform-provider-libvirt/releases/download/v0.6.1/terraform-provider-libvirt-0.6.1+git.1578064534.db13b678.Ubuntu_18.04.amd64.tar.gz

# extract
tar xvfz terraform*.tar.gz

Build from source

However, if you are not lucky enough to have a release already built, you can build from source.  I have provided a Docker image that can build the plugin for Ubuntu xenial/Debian stretch.

cd ~

# download project
git clone https://github.com/fabianlee/terraform-libvirt-provider-builder.git
cd terraform-libvirt-provider-builder

# build plugin using docker image
sudo apt-get install -y make git libvirt-dev
make -f Makefile.stretch

# check required dynamically loaded libraries
ldd terraform-provider-libvirt

# check plugin output (should say 'This binary is a plugin')
./terraform-provider-libvirt 

This binary is a plugin. These are not meant to be executed directly.
Please execute the program that consumes these plugins, which will
load any plugins automatically

# make plugins directory, copy plugin
mkdir -p ~/.terraform.d/plugins && cd $_
cp terraform-provider-libvirt ~/.terraform.d/plugins/.

Create simple VM using Terraform

Let’s start with a simple test, I have a github project named terraform-libvirt-ubuntu-examples that contains various examples of terraform usage with KVM.

sudo apt-get install git make

git clone https://github.com/fabianlee/terraform-libvirt-ubuntu-examples.git

cd terraform-libvirt-ubuntu-examples/simple

First, create an ssh keypair that can be used when we later want to login to this new VM.

ssh-keygen -t rsa -b 4096 -f id_rsa -C simple -N "" -q

Then use terraform to initialize the configuration defined in “simple.tf”

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "template" (hashicorp/template) 2.1.2...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.template: version = "~> 2.1"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

Then have terraform show you what it plans on building given the “simple.tf” definition, then have it apply and start constructing.

# show plan
terraform plan

# apply plan
terraform apply

# underlying libvirt list of domains
virsh list

While terraform is used to setup the virtual hardware resources, cloud-init is used to initialize the OS level configuration including: user accounts, ssh login keys, packages, and networking.

You have to give it a couple of minutes to establish DHCP networking, but after the creation of the VM you can do a refresh, then get a listing of its assigned IP address using:

# have terraform show IP address
terraform refresh && terraform output ips

# have libvirt show all IP address leases
virsh net-dhcp-leases default

If this doesn’t work the first time you try, give the guest OS a couple of minutes to gain network connectivity.  Then you can ssh into the VM using the private key:

ssh ubuntu@192.168.122.<lastOctet> -i id_rsa

Other Examples

For further exploration, I have multiple examples in my github repository:

  • simple – simple host with DHCP address
  • staticip – host with static IP address
  • datadisks – host with 2 additional data disks, one xfs and the other ext4
  • nestedkvm – passes through cpu features, installed docker and kvm with data drive
  • lclone-cinit – linked clone using backing file with cloud-init for customization
  • net-install – Ubuntu net installer
  • iso-install – Ubuntu fat ISO installer
  • linkedclone – linked clone using backing file from ISO installer

 

REFERENCES

github dmacvicar, Terraform provider libvirt

Terraform, configuration language

CloudInit reference, modules

ubuntu.com, cloud-init documentation

Ubuntu, UEC/Images/KVMKernelOptions

computingforgeeks.com, How to provision VMs on KVM with terraform

titosoft, Terraform and KVM

github, terraform-provider-libvirt pull 242 for kernel/initrd/cmdline

emilwypch.com, terraform with additional vsphere disk

github sghuyennet, terraform-vsphere-standalone

blah.cloud, cloud-init and vm templating vmware, cloud-init with kubernetes and docker packages

itzg github, gist on using ubuntu cloud-init image with libvirt

niteshvganesh, full tutorial on terraform on libvirt and azure

cloudinit, debugging by starting with kvm/qemu and cloudinit image

stafwag, centos and cloud-init

kubernetes.io, Installing kubeadm

 

NOTES

Remove ssh entry, required when host is recreated

ssh-keygen -f ~/.ssh/known_hosts -R <host>

cloudinit logs

/var/log/{cloud-init,cloud-init-output}.log

check DNS settings of systemd-resolv

sudo systemd-resolve --status

View kernel parameters

$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-4.15.0-74-generic root=UUID=6c7...c7 ro quiet cgroup_enable=memory swapaccount=1

True size of sparse file

qemu-img info /data/kvm/pool/simple1-qcow2

login to VM with private key

ssh ubuntu@192.168.122.30 -i id_rsa

Make sure any old ssh keys are cleared, then re-add, then ssh (helps when  rebuilding VM a lot)

ssh-keygen -f ~/.ssh/known_hosts -R "192.168.122.30" && ssh-keyscan -H 192.168.122.30 >> ~/.ssh/known_hosts && ssh ubuntu@192.168.122.30 -i id_rsa

teardown and then rebuild terraform plan

terraform destroy -auto-approve && terraform apply -auto-approve

If network interface is not immediately populated

terraform refresh && terraform output

If issues with permissions on disk creation and permissions on Ubuntu, disable AppArmor in /etc/libvirt/qemu.conf [1,2,3]

security_driver = "none"
user = "<yourid>"
group = "libvirt"

sudo systemctl restart qemu-kvm
sudo systemctl restart libvirtd