Managing a VMware Template Lifecycle with Ansible

Managing a VMware Template Lifecycle with Ansible

When we manage a large number of virtual machines (VMs), we want to reduce the differences between them and create a standard template. By using a standard template, it becomes easier to manage and propagate the same operation on the different nodes. When using VMware vSphere, it is a common practice to share a standardized VM template and use it to create new VMs. This template is often called a golden image. Its creation involves a series of steps that can be automated with Ansible. In this blog, we will see how one can create and use a new golden image.

Prepare the golden image

screenshot

We use image builder to prepare a new image. The tool provides a user interface that allows users to define custom images. In this example, we include the SSH server and tmux. The result image is a file in the VMDK4 format that is not totally supported by VMware vSphere 7, so this is the reason why we use a .vmdk-4 suffix.

create image ui screenshot

We upload the image using the uri module. Uploading large files using this method is rather slow. If you can,  you may want to drop the file on the datastore directly (e.g: NFS/CIFS). The following example considers that the datastore is called rw_datastore and the datacenter name is my_dc. The following excerpt from the playbook shows how we upload the image.

- name: Upload from contents of remote file
  ansible.builtin.uri:
    force_basic_auth: true
    url: 'https://vcenter.test/folder/my-vmdk/my-golden-image.vmdk-4?dcPath=my_dc&dsName=rw_datastore'
    url_username: '{{ lookup("ansible.builtin.env", "VMWARE_USER") }}'
    url_password: '{{ lookup("ansible.builtin.env", "VMWARE_PASSWORD") }}'
    method: PUT
    status_code: 201
    src: my-golden-image.vmdk-4
    validate_certs: false
    follow_redirects: yes

Now that we've uploaded our file, we will convert it as an up to date VMDK file. For this purpose, we use vmkfstools. The tool is available on the ESXi7 hosts by default. In the task below, we use delegate_to: esxi1.test to run the command on one of our hosts.

- name: Convert the image in an up to date VMDK format
  ansible.builtin.command: "vmkfstools -i /vmfs/volumes/rw_datastore/my-vmdk/my-golden-image.vmdk-4.vmdk-4   /vmfs/volumes/rw_datastore/my-vmdk/my-golden-image.vmdk -d thin"
  delegate_to: esxi1.test
  vars:
      ansible_user: root
      ansible_python_interpreter: /bin/python3

Preparing the golden VM

At this stage, the disk is ready and we can connect it to a VM.

- name: Create a VM
  vmware.vmware_rest.vcenter_vm:
    placement:
      cluster: ""
      datastore: ""
      folder: ""
      resource_pool: ""
    name: my-golden-vm
    guest_OS: RHEL_7_64
    hardware_version: VMX_11
    memory:
      hot_add_enabled: true
      size_MiB: 1024
    disks:
      - type: SATA
        backing:
          type: VMDK_FILE
          vmdk_file: "[rw_datastore] my-vmdk/my-golden-image.vmdk"
    cdroms:
      - type: SATA
        sata:
          bus: 0
          unit: 2
    nics:
      - backing:
          type: STANDARD_PORTGROUP
          network: ""
  register: my_vm

There are four different ways to clone a VM with Ansible's vmware.vmware_rest collection. This article explains the difference between them and how to use them with Ansible.

Cloning the VM

When we clone a VM, we create a copy of the original. However, the original can still evolve and a second clone from the same VM is likely to be different. There is no guarantee that two clones will be based on exactly the same image.

The vmware.vmware_rest.vcenter_vm module allows us to prepare either an instant clone or a regular clone.

Instant clone

According to the VMware official documentation, an instant clone is a lightweight copy of a live VM. It shares memory blocks with the original VM. This is the reason why the original VM must be running before we can clone it.

- name: Turn the power on the VM on, since it's are pre-condition for Instant Clone
  vmware.vmware_rest.vcenter_vm_power:
    state: start
    vm: '{{ my_vm.id }}'

- name: Wait until my VM is ready
  vmware.vmware_rest.vcenter_vm_tools_info:
    vm: '{{ my_vm.id }}'
  register: vm_tools_info
  until:
  - vm_tools_info is not failed
  - vm_tools_info.value.run_state == "RUNNING"
  retries: 10
  delay: 5

Now we've got the VM up and running, we can instant clone it:

- name: Create an instant clone of a VM
  vmware.vmware_rest.vcenter_vm:
    placement:
      datastore: "{{ lookup('vmware.vmware_rest.datastore_moid', '/my_dc/datastore/local') }}"
      folder: "{{ lookup('vmware.vmware_rest.folder_moid', '/my_dc/vm') }}"
      resource_pool: "{{ lookup('vmware.vmware_rest.resource_pool_moid', '/my_dc/host/my_cluster/Resources') }}"
    state: instant_clone
    source: "{{ my_vm.id }}"
    name: instant_clone_1

Regular clone

We can also clone an existing VM. The operation takes more time, up to several hours, if the VM comes with large disks. The operation creates a full clone of the original VM. This time, the original VM doesn't need to be running.

- name: Create a full clone of a VM
  vmware.vmware_rest.vcenter_vm:
    placement:
      datastore: "{{ lookup('vmware.vmware_rest.datastore_moid', '/my_dc/datastore/local') }}"
      folder: "{{ lookup('vmware.vmware_rest.folder_moid', '/my_dc/vm') }}"
      resource_pool: "{{ lookup('vmware.vmware_rest.resource_pool_moid', '/my_dc/host/my_cluster/Resources') }}"
    state: clone
    source: "{{ my_vm.id }}"
    name: full_clone_1

Build a template

Unlike clone, a template gives us the guarantee that the VM inherits from a static VM image. The vmware.vmware_rest collection gives us the ability to prepare OVF packages, and since the 2.2.0 release, we can also prepare a template VM.

Export a VM as an OVF package on a content library

We can export a VM as an OVF package and upload the package into a content library. This is handy if you want to prepare a standard template that you will reuse on your vSphere infrastructure. Also, an OVF package can be downloaded from one vSphere and be reuploaded in a different one.

In this example, we use Ansible to prepare the OVF package. In this example, my_vm is not running and the content library is called my_library_on_nfs.

- name: List all Local Content Library
  vmware.vmware_rest.content_locallibrary_info:
  register: all_content_libraries

- name: Use the name to identify the right Content Library
  set_fact:
    nfs_lib: "{{ all_content_libraries.value | selectattr('name', 'equalto', 'my_library_on_nfs')|first }}"

- name: Export the VM as an OVF on the library
  vmware.vmware_rest.vcenter_ovf_libraryitem:
    session_timeout: 2900
    source:
      type: VirtualMachine
      id: "{{ my_vm.id }}"
    target:
      library_id: "{{ nfs_lib.id }}"
    create_spec:
      name: golden_image
      description: an OVF example
      flags: []
    state: present

vsphere client screenshot

To spawn a new VM based on this OVF package, you need first to identify its item entry on the content library.

- name: Get the list of items of the NFS library
  vmware.vmware_rest.content_library_item_info:
    library_id: '{{ nfs_lib.id }}'
    register: lib_items
- name: Define a new fact with the golden image ID
  ansible.builtin.set_fact:
    golden_image_id: '{{ (lib_items.value|selectattr("name", "equalto", "golden_image")|first).id }}'

Once we've got the item ID, we can call vcenter_ovf_libraryitem to spawn a new VM. Since the ID is immutable, we can save for a future use.

- name: Create a new VM based on the OVF
  vmware.vmware_rest.vcenter_ovf_libraryitem:
    ovf_library_item_id: '{{ golden_image_id }}'
    state: deploy
    target: resource_pool_id: "{{ lookup('vmware.vmware_rest.resource_pool_moid', '/my_dc/host/my_cluster/Resources') }}"
    deployment_spec:
      name: my_vm_from_ovf
      accept_all_EULA: true
      storage_provisioning: thin

Export a VM as a VM template on a content library

The template creation is done with one call of the vmware.vmware_rest.vcenter_vmtemplate_libraryitems module. This module was introduced in the vmware_rest collection 2.2.0.

Here, nfs_lib is your content library and your VM details are registered in the my_vm variable.

- name: Create a VM template on the library
  vmware.vmware_rest.vcenter_vmtemplate_libraryitems:
    name: golden-template
    library: "{{ nfs_lib.id }}"
    source_vm: "{{ my_vm.id }}"
    placement:
      cluster: "{{ lookup('vmware.vmware_rest.cluster_moid', '/my_dc/host/my_cluster') }}"
      folder: "{{ lookup('vmware.vmware_rest.folder_moid', '/my_dc/vm') }}"
      resource_pool: "{{ lookup('vmware.vmware_rest.resource_pool_moid', '/my_dc/host/my_cluster/Resources') }}"

vsphere client screenshot

To spawn a new VM based on this template, we once again need to identify the item on the content library.

- name: Get the list of items of the NFS library
  vmware.vmware_rest.content_library_item_info:
    library_id: '{{ nfs_lib.id }}'
  register: lib_items

- name: Use the name to identify the item
  set_fact:
    my_template_item: "{{ lib_items.value | selectattr('name', 'equalto', 'golden-template')|first }}"

We use the same module for the deployment:

- name: Deploy a new VM based on the template
  vmware.vmware_rest.vcenter_vmtemplate_libraryitems:
    name: vm-from-template
    library: "{{ nfs_lib.id }}"
    template_library_item: "{{ my_template_item.id }}"
    placement:
      cluster: "{{ lookup('vmware.vmware_rest.cluster_moid', '/my_dc/host/my_cluster') }}"
      folder: "{{ lookup('vmware.vmware_rest.folder_moid', '/my_dc/vm') }}"
      resource_pool: "{{ lookup('vmware.vmware_rest.resource_pool_moid', '/my_dc/host/my_cluster/Resources') }}"
    state: deploy

In conclusion, in just a couple of Ansible tasks, we can promote an existing VM as a Template. This allows us to rely on Ansible to automate the maintenance of our VM Templates and ensure the reproducibility of the process.