Skip to main content

Create VMs with Ansible and KVM on multiple Linux hosts

This is a quick presentation of a simple way to automate creation of VMs on multiple Linux hosts. At the end of this article there is a link to GitHub containing this Ansible project.

Prerequisites

- few host machines running Linux. 
- KVM installed on each host.
- an SSH key provisioned on each host.
- Ansible installed on the machine that performs the installation, which can be one of the hosts.
- The Ansible inventory file containing the hosts to be used in installation. Currently all the hosts under vmhosts will be used.

How to create a VM with KVM

Creating a VM with KVM is done using a cloud image because it is relatively easy to configure it. At the very minimum, we need to be able to set the VM name, specify some default packages to install, then configure at least one user so we can ssh into that VM.
Many distros provide cloud images. Here are some:

The above sites host images for multiple cloud providers and in various different formats.
For the purpose of this exercise, we will be using Ubuntu 20.04 LTS.
Here are the steps needed to create a VM:
1. Download the cloud image from the official site
2. Create a copy of the cloud image which will become the VM boot image. Let's call it the VM image going forward. 
3. Resize the VM image to the size needed by the VM.
4. Prepare a cloud init configuration that contains the VM name, the user name and the ssh public key to be provisioned inside the VM, and some other additional operations that may be required. For reference, please consult the documentation.
5. Create a disk file with the cloud init configuration.
6. Run virt-install command that creates the VM:

virt-install \
      --name "{{ vm_name }}" \
      --memory  "{{ vm_ram_mb }}" \
      --vcpus "{{ vm_vcpus }}" \
      --disk "{{ pool_home }}/image_{{ vm_name }}.img",device=disk,bus=virtio \
      --disk "{{ pool_home }}/image_{{ vm_name }}_clcnf.img",device=cdrom \
      --boot hd \
      --os-type linux \
      --virt-type kvm \
      --graphics none \
      --noautoconsole \
      --network bridge=br0

The Ansible project

The goal is to automate the installation of "n" VMs on "m" hosts.
To keep it simple, I defined the configuration values in the defaults area of the playbook: 

base_image_name: focal-server-cloudimg-amd64.img
base_image_url: https://cloud-images.ubuntu.com/focal/20220527/{{ base_image_name }}
base_image_sha: f8ecf168d056f5b2dcc8d89581f075d17ec6b7866223ffaba5d61265b16036a4
pool_dir: "/tmp/kvmlab"
vcpus: 2
ram_mb: 16384
disk_size_gb: 100G
user_name: [user-name-goes-here]
ssh_pub_key: [ssh-key]

As a side note, Ubuntu cloud images site only keeps the most recent 5-6 images, so the above config needs to be updated when running or else the url will definitely return a 404. The image SHA can be found inside the file SHA256SUMS under the corresponding image directory, matching the image file name.

This is a simple playbook that drives the creation of the VMs:
- name: Deploys VM based on cloud image
  hosts: vmhosts 
  gather_facts: yes
  become: yes
  vars:
    vmnames:
      - devnode1
      - devnode2
  tasks:
    - name: provision kvm nodes
      include_role:
        name: vmspawn
      vars:
        pool_home: "{{ pool_dir }}"
        vm_names: "{{ vmnames }}"
        vm_vcpus: "{{ vcpus }}"
        vm_ram_mb: "{{ ram_mb }}"
        vm_user: "{{ user_name }}"
        vm_ssh_key: "{{ ssh_pub_key }}"
        vm_image_size: "{{ disk_size_gb }}" 

The number of VMs is defined by the variable vmnames above, and the number of hosts is defined by the group vmhosts inside the Ansible inventory file (/etc/ansible/hosts)
This is how my inventory file looks like for the above group, defining 4 hosts:
[vmhosts]
hostmachine1 ansible_connection=ssh ansible_ssh_private_key_file=/home/[user]/.ssh/[key] ansible_user=[user]
hostmachine2 ansible_connection=ssh ansible_ssh_private_key_file=/home/[user]/.ssh/[key] ansible_user=[user]
hostmachine3 ansible_connection=ssh ansible_ssh_private_key_file=/home/[user]/.ssh/[key] ansible_user=[user]
hostmachine4 ansible_connection=ssh ansible_ssh_private_key_file=/home/[user]/.ssh/[key] ansible_user=[user]

The tasks

There are two tasks contributing to the creation of VMs: the default task main that downloads the cloud image and acts as the driver for creating the actual VMs by calling the subtask called create_vm.
In a nutshell, the main task looks like this:
- name: Download base image
  become: yes
  get_url:
    url: "{{ base_image_url }}"
    dest: "{{ pool_dir }}/{{ base_image_name }}"
    checksum: "sha256:{{ base_image_sha }}"
  register: base_image
- name: create the vms
  include_tasks: create_vm.yml
  vars:
    vm_name: "{{ item }}{{ inventory_hostname }}"
  loop: "{{ vm_names }}"

And the create_vm task:
# Create a VM 
- name: Create VM if not exists
  block:
  - name: Create VM image from base
    copy:
      dest: "{{ pool_home }}/image_{{ vm_name }}.img"
      src: "{{ pool_home }}/{{ base_image_name }}"
    register: copy_results
  - name: Create user data from template
    template:
      src: user-data-template.yaml.j2
      dest: "{{ pool_home }}/{{ vm_name }}-user-data.yaml"
      mode: 0644
  - name: Resize the image
    command: |
      qemu-img resize "{{ pool_home }}/image_{{ vm_name }}.img" "{{ vm_image_size }}"
  - name: Create cloud init disk
    command: |
      cloud-localds -v "{{ pool_home }}/image_{{ vm_name }}_clcnf.img" "{{ pool_home }}/{{ vm_name }}-user-data.yaml"
  - name: Create the VM 
    command: |
      virt-install \
      --name "{{ vm_name }}" \
      --memory  "{{ vm_ram_mb }}" \
      --vcpus "{{ vm_vcpus }}" \
      --disk "{{ pool_home }}/image_{{ vm_name }}.img",device=disk,bus=virtio \
      --disk "{{ pool_home }}/image_{{ vm_name }}_clcnf.img",device=cdrom \
      --boot hd \
      --os-type linux \
      --virt-type kvm \
      --graphics none \
      --noautoconsole \
      --network bridge=br0

The template

A careful inspection of the create_vm task above reveals that the vm user-data.yaml file is actually generated from a template j2 file. This is because the user-data yaml file has to contain VM specific user data such as the user info to be created in the VM and also the vm name.

Run it

Just run ansible-playbook with the playbook yaml file as the argument:

ansible-playbook -K vmspawn.yaml

Further reading and experiments

The full source code can be found on Github here.

Comments

Popular posts from this blog

Installing Ubuntu 20 on a VM with KVM and cloud init

Prerequisites First thing to do when preparing to install KVM on a new machine is to make sure that the machine supports virtualization. This is typically controlled from BIOS. For instance with my Intel NUC, the following settings need to be turned on or maximized: Performance >> Processor >> Hyper-Threading: enabled Performance >> Processor >> Intel Turbo Boost Technology: enabled Performance >> Processor >> Active Processor Cores: all Security >> Intel Virtualization Technology: enabled Security >> Intel VT for Directed I/O (VT-d): enabled Installation Fist install QEmu+kvm, which is the emulation for the KVM supervisor. Run the following bash command to install the command line KVM: sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virtinst Next, quickly try it by running the command  virsh. It should look like this: $virsh Welcome to virsh, the virtualization interacti...

Intel NUC dev machine for home

Here is my newest Linux dev machine I put together recently. It is an  Intel NUC BXNUC10I7FNK1 ,  2 x Samsung 32GB DDR4 2666MHz    and a fast Samsung 970 EVO Plus 500GB NVMe M.2 Internal SSD.  I do not need a monitor or a keyboard, since this machine will run headless and I will only SSH into it from my Mac. Took very little time to put it together. Just opened the NUC case, installed the SSD and the memory sticks. I installed Ubuntu server on it. Downloaded the image from ubuntu.com , then created a boot image on a memory stick and used it to install the OS. This new machine is powerful enough to run few VMs, many Docker containers and Kubernetes. And all was about $900 which is quite affordable.