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:
- Ubuntu
- Debian
- Fedora
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
Post a Comment