Terraform and KVM (x86)
Terraform is what they call “Infrastructure as a code”. It has a different approach from other automation tools like Puppet, Chef or Ansible because it is focused on Cloud Infrastructure.
It supports a bunch of providers like AWS, Azure, Softlayer… but, as you can see, there is no official support for KVM. I don’t want to create a AWS account just to try terraform, so in this article I am going to write step by step how to create a KVM virtual environment using Terraform libvirt provider.
Update:
05/22/2019 - This article has been updated to support Terraform 0.11.14 and libvirt provider 0.5.1.
Pre-requisites
- x86 server
- Ubuntu 16.04 or 18.04
- KVM installed and configured
- Some disk space for your guests
APT pre-requisites
# apt install unzip git libvirt-dev
Installing Terraform
First find the appropriate package for Linux on Terraform Download page.
I am using https://releases.hashicorp.com/terraform/0.11.14/terraform_0.11.14_linux_amd64.zip that is the latest version available to me.
wget https://releases.hashicorp.com/terraform/0.11.14/terraform_0.11.14_linux_amd64.zip
Unzip it, it is just a binary that we will move to /usr/local/bin :
root@ubuntu-host:~# unzip terraform_0.11.14_linux_amd64.zip
Archive: terraform_0.11.14_linux_amd64.zip
inflating: terraform
root@ubuntu-host:~# chmod +x terraform
root@ubuntu-host:~# mv terraform /usr/local/bin/
Verifying the Installation
root@ubuntu-host:~$ terraform
Usage: terraform [-version] [-help] <command> [args]
The available commands for execution are listed below.
The most common, useful commands are shown first, followed by
less common or more advanced commands. If you're just getting
started with Terraform, stick with the common commands. For the
other commands, please read the help and docs before usage.
Common commands:
apply Builds or changes infrastructure
console Interactive console for Terraform interpolations
destroy Destroy Terraform-managed infrastructure
env Workspace management
fmt Rewrites config files to canonical format
get Download and install modules for the configuration
graph Create a visual graph of Terraform resources
import Import existing infrastructure into Terraform
init Initialize a Terraform working directory
output Read an output from a state file
plan Generate and show an execution plan
providers Prints a tree of the providers used in the configuration
push Upload this Terraform module to Atlas to run
refresh Update local state file against real resources
show Inspect Terraform state or plan
taint Manually mark a resource for recreation
untaint Manually unmark a resource as tainted
validate Validates the Terraform files
version Prints the Terraform version
workspace Workspace management
All other commands:
0.12checklist Checks whether the configuration is ready for Terraform v0.12
debug Debug output management (experimental)
force-unlock Manually unlock the terraform state
state Advanced state management
Next step, install libvirt provider!
Installing Terraform libvirt Provider
If you want to build the latest version the libvirt provide will require:
- libvirt 1.2.14 or newer
- latest golang version
- mkisofs is required to use the CloudInit feature.
Installing golang 1.9
To get the latest version of golang we are going to use a ppa:
sudo add-apt-repository ppa:gophers/archive
Update the repositories:
apt-get update
Remove an old version of golang:
apt remove golang
apt autoremove
apt-get install golang-1.9-go
Add golang 1.9 to your PATH, create a file on /etc/profile.d:
vim /etc/profile.d/golang19.sh
Copy this content:
#!/bin/bash
if [ -d "/usr/lib/go-1.9/bin" ] ; then
export PATH="$PATH:/usr/lib/go-1.9/bin"
fi
Set your GOPATH, add the content above to your .bashrc:
export GOPATH=$HOME/go
Logon with your user again (I am using root) and test the installation:
root@ubuntu-host:~# go version
go version go1.9.4 linux/amd64
Building libvirt provider
Use “go get” to download the source from github:
root@ubuntu-host:~# go get github.com/dmacvicar/terraform-provider-libvirt
root@ubuntu-host:~# go install github.com/dmacvicar/terraform-provider-libvirt
You will now find the binary at $GOPATH/bin/terraform-provider-libvirt
Moving the libvirt provider to terraform.d
There is a directory called “terraform.d” in your $HOME. In my example it is located in /root/.terraform.d
If .terraform.d is not present execute a command and terraform will create it for you, example:
root@ubuntu-host:~# terraform init
Terraform initialized in an empty directory!
The directory has no Terraform configuration files. You may begin working
with Terraform immediately by creating Terraform configuration files.
root@ubuntu-host:~# cd .terraform.d/
root@ubuntu-host:~/.terraform.d# ls
checkpoint_signature
We are going to create a folder called “plugins” there:
root@ubuntu-host:~/.terraform.d# mkdir plugins
root@ubuntu-host:~/.terraform.d# cd plugins/
root@ubuntu-host:~/.terraform.d/plugins# pwd
/root/.terraform.d/plugins
Copy our plugin binary to this new directory:
root@ubuntu-host:~/.terraform.d/plugins# cp ~/go/bin/terraform-provider-libvirt .
root@ubuntu-host:~/.terraform.d/plugins# ls -alh
total 31M
drwxr-xr-x 2 root root 4.0K Feb 26 13:09 .
drwxr-xr-x 3 root root 4.0K Feb 26 13:09 ..
-rwxr-xr-x 1 root root 31M Feb 26 13:09 terraform-provider-libvirt
We should now be able to create a environment on KVM using Terraform, check the next session.
Creating a Terraform configuration file for KVM
With Terraform installed, let’s dive right into it and start creating some infrastructure. Create a new directory called “terraform”, we will use this directory to store some configurations files of our project.
root@ubuntu-host:~/terraform# mkdir terraform
root@ubuntu-host:~/terraform# pwd
/root/terraform
We’ll build infrastructure on KVM. Our configuration file will create a NAT network, a new volume and install Ubuntu 16.04 using a cloud image from Canonical servers.
Create a new file called “libvirt.tf” and copy the content below. The format of the configuration files is documented here.
# instance the provider
provider "libvirt" {
uri = "qemu:///system"
}
# We fetch the latest ubuntu release image from their mirrors
resource "libvirt_volume" "ubuntu-qcow2" {
name = "ubuntu-qcow2"
pool = "images" #CHANGE_ME
source = "https://cloud-images.ubuntu.com/releases/xenial/release/ubuntu-16.04-server-cloudimg-amd64-disk1.img"
format = "qcow2"
}
# Create a network for our VMs
resource "libvirt_network" "vm_network" {
name = "vm_network"
addresses = ["10.0.1.0/24"]
dhcp {
enabled = true
}
}
# Use CloudInit to add our ssh-key to the instance
resource "libvirt_cloudinit_disk" "commoninit" {
name = "commoninit.iso"
pool = "images" #CHANGEME
user_data = "${data.template_file.user_data.rendered}"
network_config = "${data.template_file.network_config.rendered}"
}
data "template_file" "user_data" {
template = "${file("${path.module}/cloud_init.cfg")}"
}
data "template_file" "network_config" {
template = "${file("${path.module}/network_config.cfg")}"
}
# Create the machine
resource "libvirt_domain" "domain-ubuntu" {
name = "ubuntu-terraform"
memory = "512"
vcpu = 1
cloudinit = "${libvirt_cloudinit_disk.commoninit.id}"
network_interface {
network_id = "${libvirt_network.vm_network.id}"
network_name = "vm_network"
}
# IMPORTANT
# Ubuntu can hang is a isa-serial is not present at boot time.
# If you find your CPU 100% and never is available this is why
console {
type = "pty"
target_port = "0"
target_type = "serial"
}
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"
}
}
The provider block is used to configure the named provider, in our case “libvirt”. If you want to connect to a remote KVM host you can change the uri to something like:
provider "libvirt" {
uri = "virsh -c qemu+ssh://ubuntu@yourhostname.com/system?socket=/var/run/libvirt/libvirt-sock"
}
Make sure that the user ubuntu, for example, has the proper permission to execute virsh commands.
The resource block defines a resource that exists within the infrastructure. We have defined:
- libvirt_volume that is a qcow2 disk that will be created inside our storage pool called “images” (Note: KVM creates a storage pool called “default” during the installation, this example uses “images” as a storage pool, change to your storage pool accordingly.)
- libvirt_network will create a NAT network called “vm_network” using network “10.0.1.0/24” for DHCP.
- libvirt_domain defines our guest “ubuntu-terraform” with 512MB of RAM, 1 vcpu, with a network interface and our qcow disk created on “libvirt_volume” resource.
- There are 2 templates files that we will need to create for cloudinit. They will define our user data and network interface information.
For the user data we will create a file called cloud_init.cfg
and paste the content below:
#cloud-config
users:
- name: ubuntu
sudo: ALL=(ALL) NOPASSWD:ALL
groups: users, admin
home: /home/ubuntu
shell: /bin/bash
ssh-authorized-keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDYnZmg #CHANGE_ME
ssh_pwauth: True
disable_root: false
chpasswd:
list: |
ubuntu:linux
expire: False
package_update: true
packages:
- qemu-guest-agent
growpart:
mode: auto
devices: ['/']
(All available parameters for cloudinit can be found here)
-
The configuration above creates an user called ubuntu that will have SUDO access without password, an authorized key for passwordless access (Note: change it to your id_rsa.pub), it will also allow password access and the default password is linux.
-
The package section will install qemu-guest-agent package to provide us some facilities managing our VM.
-
The growpart statement resizes partitions to fill the available disk space.
ubuntu@ubuntu-host:~/terraform/blogtest$ ls
cloud_init.cfg libvirt.tf
Now we will create our last configuration file that will setup our network card, it will be called network_config.cfg
. Paste the content below:
version: 2
ethernets:
ens3:
dhcp4: true
(All available parameters for cloudinit network file can be found here)
- It is a simple file that will create a interface called ens3 and setup as DHCP client.
Now we have all the 3 files that we need in our terraform folder:
ubuntu@ubuntu-host:~/terraform/blogtest$ ls
cloud_init.cfg libvirt.tf network_config.cfg
Initialization
The first command to run for a new configuration – or after checking out an existing configuration from version control – is terraform init, which initializes various local settings and data that will be used by subsequent commands.
root@ubuntu-host:~/terraform# terraform init
Initializing provider plugins...
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.
Apply Changes
In the same directory as the libvirt.tf file you created, run terraform apply. You should see output similar to below, though we’ve truncated some of the output to save space:
root@ubuntu-host:~/terraform# terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
+ libvirt_cloudinit.commoninit
id: <computed>
name: "commoninit.iso"
pool: "images"
ssh_authorized_key: "ssh-rsa AAAAB3NzaC1yc2[...]"
+ libvirt_domain.domain-ubuntu
id: <computed>
arch: <computed>
cloudinit: "${libvirt_cloudinit.commoninit.id}"
console.#: "2"
console.0.target_port: "0"
console.0.target_type: "serial"
console.0.type: "pty"
console.1.target_port: "1"
console.1.target_type: "virtio"
console.1.type: "pty"
disk.#: "1"
disk.0.scsi: "false"
disk.0.volume_id: "${libvirt_volume.ubuntu-qcow2.id}"
emulator: <computed>
graphics.#: "1"
graphics.0.autoport: "true"
graphics.0.listen_type: "address"
graphics.0.type: "spice"
machine: <computed>
memory: "512"
name: "ubuntu-terraform"
network_interface.#: "1"
network_interface.0.addresses.#: <computed>
network_interface.0.hostname: "master"
network_interface.0.mac: <computed>
network_interface.0.network_id: <computed>
network_interface.0.network_name: "vm_network"
vcpu: "1"
+ libvirt_network.vm_network
id: <computed>
addresses.#: "1"
addresses.0: "10.0.1.0/24"
bridge: <computed>
mode: "nat"
name: "vm_network"
+ libvirt_volume.ubuntu-qcow2
id: <computed>
format: "qcow2"
name: "ubuntu-qcow2"
pool: "images"
size: <computed>
source: "https://cloud-images.ubuntu.com/releases/xenial/release/ubuntu-16.04-server-cloudimg-amd64-disk1.img"
Plan: 4 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value:
This output shows the execution plan, describing which actions Terraform will take in order to change real infrastructure to match the configuration.
Now confirm these actions typing “yes” and wait for your new server.
At the end you are going to see an output like this one:
libvirt_domain.domain-ubuntu: Creation complete after 33s (ID: f6905a4e-993f-488e-a933-f74ce982f2a4)
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
ip = 10.0.1.166
Check your new infrastructure
You can inspect the current state using terraform show:
root@ubuntu-host:~/terraform# terraform show
libvirt_cloudinit.commoninit:
id = /var/lib/libvirt/images/commoninit.iso;5a9439b5-51c2-de45-f646-9a4c0dcd640f
local_hostname =
name = commoninit.iso
pool = images
ssh_authorized_key = ssh-rsa AAAAB3NzaC1[...]
user_data = #cloud-config
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1[...]
libvirt_domain.domain-ubuntu:
id = f6905a4e-993f-488e-a933-f74ce982f2a4
arch = x86_64
autostart = false
cloudinit = /var/lib/libvirt/images/commoninit.iso;5a9439b5-51c2-de45-f646-9a4c0dcd640f
cmdline.# = 0
console.# = 2
console.0.source_path =
console.0.target_port = 0
console.0.target_type = serial
console.0.type = pty
console.1.source_path =
console.1.target_port = 1
console.1.target_type = virtio
console.1.type = pty
disk.# = 1
disk.0.file =
disk.0.scsi = false
disk.0.url =
disk.0.volume_id = /var/lib/libvirt/images/ubuntu-qcow2
disk.0.wwn =
emulator = /usr/bin/kvm-spice
firmware =
graphics.# = 1
graphics.0.autoport = true
graphics.0.listen_type = address
graphics.0.type = spice
initrd =
kernel =
machine = ubuntu
memory = 512
name = ubuntu-terraform
network_interface.# = 1
network_interface.0.addresses.# = 1
network_interface.0.addresses.0 = 10.0.1.166
network_interface.0.bridge =
network_interface.0.hostname =
network_interface.0.mac = DE:E8:A7:F1:D0:77
network_interface.0.macvtap =
network_interface.0.network_id = 6ec048ad-3e33-4080-bf19-f109fe1c5f27
network_interface.0.network_name = vm_network
network_interface.0.passthrough =
network_interface.0.vepa =
network_interface.0.wait_for_lease = false
nvram.# = 0
vcpu = 1
libvirt_network.vm_network:
id = 6ec048ad-3e33-4080-bf19-f109fe1c5f27
addresses.# = 1
addresses.0 = 10.0.1.0/24
autostart = false
bridge = virbr1
mode = nat
name = vm_network
libvirt_volume.ubuntu-qcow2:
id = /var/lib/libvirt/images/ubuntu-qcow2
format = qcow2
name = ubuntu-qcow2
pool = images
size = 2361393152
source = https://cloud-images.ubuntu.com/releases/xenial/release/ubuntu-16.04-server-cloudimg-amd64-disk1.img
Outputs:
ip = 10.0.1.166
You can see that by creating our resource, we’ve also gathered a lot of information about it. As you can see my server got the NAT IP 10.0.1.166.
Check your KVM host to see what happened:
root@ubuntu-host:~/terraform# virsh list
Id Name State
----------------------------------------------------
13 ubuntu-terraform running
root@ubuntu-host:~/terraform# virsh net-list
Name State Autostart Persistent
----------------------------------------------------------
br0 active yes yes
default active yes yes
vm_network active no yes
Access your new server:
ssh ubuntu@10.0.1.166
It shouldn’t ask a password because we have setup ssh authorized keys.
And that is it! Enjoy your new server, play with Terraform configuration file and try to increase the number of guests, networks and disks!
On a next article I will try to build a Kubernetes environment on KVM using Terraform!
See ya!
Leave a comment