Terraform for dummies part 5: Terraform deployment On-premises (KVM)

This image has an empty alt attribute; its file name is image.pngIntro

For a long time, Terraform was associated with deploying resources in the cloud. But what many people donโ€™t know is that terraform already had private and community based providers that worked perfectly on non cloud environments. Today, we will discover how to deploy a compute vm in a KVM host. Not only that, but we will also do it on top of VirtualBox in a nested virtualization environment. As always, I will provide the vagrant build to allow you to launch the lab for a front-row experience. It is indeed the cheapest way to use terraform on-prem on your laptop.

Terraform on-prem


Libvirtd provider is a community based project built by Duncan Mac-Vicar. There is no difference between using terraform on cloud platforms and doing it with Libvirtd provider. In this lab I had to enable nested virtualization in my VBox to make it easier to run the demo.The resulting hypervision is qemu-kvm, a non bare-metal KVM environment also known as type 2 hypervisor (virtual hardware emulation).     

How to get started


No need to subscribe to a cloud Free-tier using credit cards to play with terraform. You can start this lab right now on your laptop with my vagrant build. The environment comes with all necessary modules & packages to deploy vms using terraform.

Lab Content:
– KVM
– KCLI (wrapper tool for managing vms)
– Terraform 1.0
– Libvirt terraform provider
– Terraform configuration samples to get started (ubuntu.tf , kvm-compute.tf)

GitHub repo
: https://github.com/brokedba/KVM-on-virtualbox 

  • Clone the repo
  • C:Usersbrokedba> git clone https://github.com/brokedba/KVM-on-virtualbox.git C:Usersbrokedba> cd KVM-on-virtualbox

  • Start the vm (make sure you have 2Cores and 4GB RAM to spare before the launch)
  • C:Users*KVM-on-virtualbox> vagrant up C:Users*KVM-on-virtualbox> vagrant ssh ---- access to KVM host

    Now you have a new virtual machine shipped with kvm and terraform which will help us complete the lab.
    Note: Terraform files will be located under   /root/projects/terraform/

What you should know 

  • Libvirt provider in Terraform registry 


    Up until terraform version 0.12, Hashicorp didnโ€™t officially recognize this libvirt provider, you could still run config files if the plugin was in a local plugin folder (i.e. /root/.terraform.d/plugins/)
    But after version 0.13,  terraform enforced Explicit Provider Source Locations. As result, youโ€™ll need few tweaks to make it run in terraform. Everything is documented in GitHub issue1 & 2 but Iโ€™ll summarize it below.   

    The steps to run libvirt provider in terraform v1.0 (Already done in my build)

    – Download the Binary (current vers: 0.6.12). For my part I used an older version for fedora (0.6.2)

    [root@localhost]# wget URL [root@localhost]# tar xvf terraform-provider-libvirt-**.tar.gz

    – Add the plugin in a local registry

    [root@localhost]# mkdir โ€“p ~/.local/share/terraform/plugins/registry.terraform.io/dmacvicar/libvirt/0.6.2/linux_amd64 [root@localhost]# mv terraform-provider-libvirt ~/.local/share/terraform/plugins/registry.terraform.io/dmacvicar/libvirt/0.6.2/linux_amd64

    – Add the below code block to the main.tf file to map libvirt references with the actual provider

    [root@localhost]# vi libvirt.tf ... terraform { required_version = ">= 0.13" required_providers { libvirt = { source = "dmacvicar/libvirt" version = "0.6.2" } } } ... REST of the Config

    – Initialize and validate by running terraform init which will detect and add libvirt plugin in the local registry  

    [root@localhost]# terraform init

     Initializing the backend...  Initializing provider plugins... - Finding dmacvicar/libvirt versions matching "0.6.2"... - Installing dmacvicar/libvirt v0.6.2... - Installed dmacvicar/libvirt v0.6.2 (unauthenticated)

   

Terraform deployment  

  • Deploy basic ubuntu vm 


    Letโ€™s first provision a simple ubuntu vm on our KVM environment . Again in a nested virtualization mode we are using hardware emulated hypervision โ€œQemuโ€, and this will require a small hack by setting a special variable. Will
    explain why further down.  Just bear with me for now.

[root@localhost]# export TERRAFORM_LIBVIRT_TEST_DOMAIN_TYPE="qemu"

  • Next  we need to find our configuration file, letโ€™s check declared resource behind that  ubuntu.tf
  • [root@/*/ubuntu/]# ls /root/projects/terraform/ubuntu/ .. ubuntu.tf --- you can click to download or read content

    [root@/*/ubuntu/]# vi ubuntu.tf provider "libvirt" { uri = "qemu:///system"} terraform {   required_providers {     libvirt = {       source  = "dmacvicar/libvirt"       version = "0.6.2"     }   } } ## 1. --------> Section that declares the provider in Terraform registry

    # 2. ----> We fetch the smallest ubuntu image from the cloud image repo resource "libvirt_volume" "ubuntu-disk" { name   = "ubuntu-qcow2" pool   = "default" ## ---> This should be same as your disk pool name source = https://cloud-images.ubuntu.com/releases/xenial/release/ubuntu-16.04-server-cloudimg-amd64-disk1.img format = "qcow2" }

    # 3. -----> Create the compute vm resource "libvirt_domain" "ubuntu-vm" { name   = "ubuntu-vm" memory = "512" vcpu   = 1

    network_interface {    network_name = "default" ## ---> This should be the same as your network name   }

    console { # ----> define a console for the domain.    type        = "pty"    target_port = "0"    target_type = "serial" }

    disk {   volume_id = libvirt_volume.ubuntu-disk.id } # ----> map/attach the disk graphics { ## ---> graphics settings    type        = "spice"    listen_type = "address"    autoport    = "true"} }

  • Run terraform init to initialize the setup and fetch the called providers in the tf file, like we did earlier.
  • [root@localhost]# terraform init

  • Run terraform plan
  • [root@localhost]# terraform plan Terraform will perform the following actions:

      # libvirt_domain.ubuntu-vm will be created   + resource "libvirt_domain" "ubuntu-vm" {       + arch        = (known after apply)       + disk        = [           + {               + block_device = null               + file         = null               + scsi         = null               + url          = null               + volume_id    = (known after apply)               + wwn          = null             },         ]       + emulator    = (known after apply)       + fw_cfg_name = "opt/com.coreos/config"       + id          = (known after apply)       + machine     = (known after apply)       + memory      = 512       + name        = "ubuntu-vm"       + qemu_agent  = false       + running     = true       + vcpu        = 1

          + console {           + source_host    = "127.0.0.1"           + source_service = "0"           + target_port    = "0"           + target_type    = "serial"           + type           = "pty"         }

          + graphics {           + autoport       = true           + listen_address = "127.0.0.1"           + listen_type    = "address"           + type           = "spice"         }

          + network_interface {           + addresses    = (known after apply)           + hostname     = (known after apply)           + mac          = (known after apply)           + network_id   = (known after apply)           + network_name = "default"         }     }

      # libvirt_volume.ubuntu-disk will be created   + resource "libvirt_volume" "ubuntu-disk" {       + format = "qcow2"       + id     = (known after apply)       + name   = "ubuntu-qcow2"       + pool   = "default"       + size   = (known after apply)       + source = https://cloud-images.ubuntu.com/releases/xenial/release/ubuntu-16.04-server-cloudimg-amd64-disk1.img     }

    Plan: 2 to add, 0 to change, 0 to destroy.

  • Run terraform apply to deploy the vm which was declared in the plan command output 
  • [root@localhost]# terraform apply -auto-approve Plan: 2 to add, 0 to change, 0 to destroy. libvirt_volume.ubuntu-disk: Creating... libvirt_volume.ubuntu-disk: Creation complete after 17s [id=/u01/guest_images/ubuntu-qcow2] libvirt_domain.ubuntu-vm: Creating... libvirt_domain.ubuntu-vm: Creation complete after 0s [id=29735a37-ef91-4c26-b194-05887b1fb264]

    Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

  • Wait a little a bit and run kcli command or virsh list
  • [root@localhost ubuntu]# kcli list vm +-----------+--------+----------------+--------+------+---------+ |    Name   | Status |      Ips       | Source | Plan | Profile | +-----------+--------+----------------+--------+------+---------+ | ubuntu-vm |   up   | 192.168.122.74 |        |      |         | +-----------+--------+----------------+--------+------+---------+

  • Cool, we have a vm with an IP address but you still need to login to it. Cloud images just donโ€™t come with root passwords so letโ€™s destroy it now and jump into our second example.
  • [root@localhost]# terraform destroy -auto-approve Destroy complete! Resources: 2 destroyed.

     

  • Deploy a vm with CloudInit 

Same way as with Cloud vms we can also call startup scripts to do anything we want during the bootstrap.
I chose Centos in this example where CloudInit bootstrap actions were:

– Set a new password to root user

– Add an SSH key to root user

– Change the hostname 

  • Create a CloudInit config file: please careful with the indentation. It can also be downloaded here cloud_init.cfg.

# cd ~/projects/terraform [root@~/projects/terraform]# cat cloud_init.cfg #cloud-config disable_root: 0 users:   - name: root     ssh-authorized-keys: ### โ€“> add a public SSH key       - ${file("~/.ssh/id_rsa.pub")} ssh_pwauth: True chpasswd: ### โ€“> change the password   list: |      root:unix1234   expire: False

runcmd:   - hostnamectl set-hostname terracentos

  • I will only display the part where CloudInit is involved but you can read the full content here kvm-compute.tf

# cd ~/projects/terraform [root@~/projects/terraform]# cat kvm_compute.tf provider "libvirt" { โ€ฆ resource "libvirt_volume" "centos7-qcow2" { โ€ฆ ## 1. ----> Instantiate cloudinit as a media drive to add our startup tasks resource "libvirt_cloudinit_disk" "commoninit" { name           = "commoninit.iso" pool           = "default" ## ---> This should be same as your disk pool name user_data      = data.template_file.user_data.rendered } ## 2. ----> Data source converting the cloudinit file into a userdata format data "template_file" "user_data" { template = file("${path.module}/cloud_init.cfg")}

resource "libvirt_domain" "centovm" {   name   = "centovm"   memory = "1024"   vcpu   = 1

cloudinit = libvirt_cloudinit_disk.commoninit.id ## 3. ----> map CloudInit

...---> Rest of the usual domain declaration

  • We can now run a terraform INIT then PLAN (donโ€™t forget to set TERRAFORM_LIBVIRT_TEST_DOMAIN_TYPE variable)

    [root@~/projects/terraform]# terraform init

    [root@~/projects/terraform]# terraform plan ... Other resources declaration # libvirt_cloudinit_disk.commoninit will be created + resource "libvirt_cloudinit_disk" "commoninit" {    + id        = (known after apply)     + name      = "commoninit.iso"     + pool      = "default"     + user_data = <<-EOT           #cloud-config           disable_root: 0           users:             - name: root               ssh-authorized-keys:                 - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ** root@localhost.localdomain

              ssh_pwauth: True           chpasswd:             list: |                root:unix1234             expire: False

              runcmd:             - hostnamectl set-hostname terracentos       EOT   } ... Remaining declaration

  • Run the Apply

    [root@~/projects/terraform]# terraform apply -auto-approve Plan: 3 to add, 0 to change, 0 to destroy. libvirt_cloudinit_disk.commoninit: Creation complete after 1m22s [id=/u01/guest_images/commoninit.iso;61c50cfc-**] ...

    Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

  • Wait a little a bit after completion and run kcli command to confirm an IP is allocated.

    [root@~/projects/terraform]# kcli list vm +-----------+--------+----------------+--------+------+---------+ |    Name   | Status |      Ips       | Source | Plan | Profile | +-----------+--------+----------------+--------+------+---------+ | centovm |   up   | 192.168.122.68 |        |      |         | +-----------+--------+----------------+--------+------+---------+

  • Login into the vm using ssh and password authentication

    -- 1. SSH [root@~/projects/terraform]# ssh -i ~/.ssh/id_rsa root@192.168.122.68 Warning: Permanently added '192.168.122.68' (RSA) to the list of known hosts. [root@terracentos ~]# cat /etc/centos-release CentOS Linux release 7.8.2003 (Core) -- 2. Password [root@~/projects/terraform]# virsh console centovm Connected to domain centovm Escape character is ^] CentOS Linux 7 (Core) Kernel 3.10.0-1127.el7.x86_64 on an x86_64

    terracentos login: root Password: [root@terracentos ~]#

And here you go, your local terraform vm was changed during startup using a simple config file just like the ones on AWS ๐Ÿ˜‰ .

Undocumented QEMU tip on Terraform

    • I can now explain why we needed to set the environment variable to โ€œqemuโ€ in order to have your deployment working. In fact, the vm will never start-up without this trick. Letโ€™s find why

      • Nested virtualization doesn’t seem to support kvm domain type.
      • This issue is similar to what openstak and miniduke encounter when they use kvm within vbox “could not find capabilities for domaintype=kvm”
      • I needed to make libvirt provider chose qemu instead of kvm during the provisioning
      • With the help of @titogarrido we found out that the logic inside its code โ€œdomain_def.goโ€
        implied kvm was the only supported virtualization but checked a mysterious variable first
      • Finally found the workaround by setting the variable to qemu which is an old hack from days when authors were testing travis 
    I asked them to replace that variable by an attribute inside terraform code but the bug is still there, see more in my issue

    --- Workaround for non BareMetal hosts (nested) export TERRAFORM_LIBVIRT_TEST_DOMAIN_TYPE="qemu"

    --- Below Go check happens where qemu is selected (domain_def.go)

    if v := os.Getenv("TERRAFORM_LIBVIRT_TEST_DOMAIN_TYPE"); v != "" {
    		domainDef.Type = v
    	} else {domainDef.Type = "kvm"}
    
     
    

Conclusion

  • We have seen in this lab just how easy it was to deploy resources using terraform on-promises
         (no more credit card needed ๐Ÿ™‚ 
  • I am very happy to make this little contribution via my vagrant build shipped with KVM on VirtualBox
  • I hope youโ€™ll give this lab a try as itโ€™s super easy and fun !! ๐Ÿ™‚
  • I would like to thank @titogarrido, who has accepted to dig deeper with me so we can find the bug and
  • I love pair programming tools like tmate that allowed us to live collaborate while he was in Brazil and me in Canada.  
  • If you want to know about my vagrant build check my previous blog post 
  • Learn more about libvirt provider usage in the official terraform registry for libvirt provider dmacvicar/libvirt
  • Thank you for reading !