Automate Ubuntu Server 20.04 Installation with Ansible

And now something completely different. Recently, while working on a project, I had to come up with a way to automate Ubuntu Server 20.04 VM installations on vSphere. Utilizing Ubuntu’s new autoinstall method together with some Ansible code I managed to get something up and running. Decent enough to share it with you.

Overview

The Playbook I’m showcasing in this article will carry out the following operations:

  1. Download the Ubuntu Server 20.04 ISO
  2. Modify ISO contents to enable unattended installation
  3. Upload the modified ISO to a vSphere datastore
  4. Create a VM and install Ubuntu Server 20.04 using the modified ISO
  5. Configure static IP on the Ubuntu Server

Step 5 is required because for some reason Ubuntu’s autoinstall reverts the network configuration to DHCP after rebooting the server. I hope this will be fixed in a future release.

Requirements

To run this Playbook you will need the following:

  • An Ubuntu 20.04 VM with the following:
    • sudo apt install python3 python3-pip xorriso
    • pip3 install ansible pyvim pyvmomi
    • An Internet connection
    • Access to your vSphere environment
  • vSphere 6.7 or higher

Playbook

Below the contents of the Ansible Playbook for reference. This together with the supporting files is actually better viewed and cloned on Github.

---
- hosts: localhost
  name: util_DeployUbuntu.yml
  gather_facts: false
  vars:
    LOCAL_TempDir: "/tmp"
    LOCAL_WorkingDir: "/tmp/ubuntu20"
    LOCAL_UbuntuISO: "ubuntu-20.04-live-server-amd64.iso"
    LOCAL_UbuntuISO_URL: "https://releases.ubuntu.com/20.04/ubuntu-20.04-live-server-amd64.iso"
    LOCAL_New_UbuntuISO: "ubuntu2004.iso"
    LOCAL_ESXiHost: "esxi03.demo.local"
    LOCAL_ESXiUser: "root"
    LOCAL_ESXiPassword: "VMware1!"
    LOCAL_DataCenter: "ha-datacenter"
    LOCAL_vSphereCluster: ""
    LOCAL_VMFolder: "ha-datacenter/vm"
    LOCAL_DataStore: "nested_nfs01"
    LOCAL_DataStoreDir: "/Lab-ISO-Folder"
    LOCAL_UbuntuVMName: "ubuntu-server"
    LOCAL_UbuntuVMDiskSize: "50"           # gigabytes
    LOCAL_UbuntuVMMemorySize: "2048"       # megabytes
    LOCAL_UbuntuVMCPUs: "1"
    LOCAL_UbuntuVMCPUCores: "1"
    LOCAL_UbuntuVMPortGroup: "Lab-Routers"
    LOCAL_UbuntuOSLocale: "en_US"
    LOCAL_UbuntuOSKeyboardLayout: "en"
    LOCAL_UbuntuOSKeyboardVariant: "us"
    LOCAL_UbuntuOSIPv4Address: "10.203.0.50/24"
    LOCAL_UbuntuOSIPv4Gateway: "10.203.0.1"
    LOCAL_UbuntuOSIPv4DNS: "10.203.0.5"
    LOCAL_UbuntuOSSearchDomain: "sddc.lab"
    LOCAL_UbuntuOSHostname: "ubuntu-server"
    LOCAL_UbuntuOSUser: "ubuntu"
  tasks:
    - name: Create working directory on Ansible Controller
      file:
        path: "{{ LOCAL_WorkingDir }}"
        state: directory

    - name: Check if Ubuntu ISO exists locally on Ansible Controller
      stat:
        path: "{{ LOCAL_TempDir }}/{{ LOCAL_UbuntuISO }}"
      register: InstallerFileCheck

    - name: Download Ubuntu ISO (if ISO file doesn't exist locally)
      get_url:
        url:  "{{ LOCAL_UbuntuISO_URL }}"
        dest: "{{ LOCAL_TempDir }}/{{ LOCAL_UbuntuISO }}"
      when:
        - InstallerFileCheck.stat.exists != true
        
    - name: Mount Ubuntu ISO
      action: mount name="{{ LOCAL_WorkingDir }}/iso" src="{{ LOCAL_TempDir }}/{{ LOCAL_UbuntuISO }}" opts=loop fstype=iso9660 state=mounted

    - name: Copy Ubuntu ISO contents to working directory
      copy: 
        src: "{{ LOCAL_WorkingDir }}/iso/"
        dest: "{{ LOCAL_WorkingDir }}/isocopy/"

    - name: Unmount Ubuntu ISO
      action: mount name="{{ LOCAL_WorkingDir }}/iso" src="{{ LOCAL_TempDir }}/{{ LOCAL_UbuntuISO }}" fstype=iso9660 state=absent

    - name: Edit txt.cfg to modify append line 
      replace:
        dest: "{{ LOCAL_WorkingDir }}/isocopy/isolinux/txt.cfg"
        regexp: 'append   initrd=/casper/initrd quiet  ---'
        replace: 'append   initrd=/casper/initrd quiet --- autoinstall ds=nocloud;s=/cdrom/SDDC.Lab/'

    - name: Create directory to store user-data and meta-data
      file:
        path: "{{ LOCAL_WorkingDir }}/isocopy/SDDC.Lab"
        state: directory

    - name: Copy user-data file to directory
      template: 
        src: "./user-data.j2"
        dest: "{{ LOCAL_WorkingDir }}/isocopy/SDDC.Lab/user-data"

    - name: Create empty meta-data file in directory
      file:
        path: "{{ LOCAL_WorkingDir }}/isocopy/SDDC.Lab/meta-data"
        state: touch

    - name: Create custom Ubuntu ISO
      command: "xorrisofs -relaxed-filenames -J -R -o {{ LOCAL_TempDir }}/{{ LOCAL_New_UbuntuISO }} -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table {{ LOCAL_WorkingDir }}/isocopy/"
      args:
        chdir: "{{ LOCAL_WorkingDir }}/isocopy/"

    - name: Upload the custom Ubuntu ISO to the datastore
      vsphere_copy: 
        hostname: "{{ LOCAL_ESXiHost }}"
        username: "{{ LOCAL_ESXiUser }}"
        password: "{{ LOCAL_ESXiPassword }}"
        validate_certs: no
        datacenter: "{{ LOCAL_DataCenter }}"
        src: "{{ LOCAL_TempDir }}/{{ LOCAL_New_UbuntuISO }}" 
        datastore: "{{ LOCAL_DataStore }}"
        path: "{{ LOCAL_DataStoreDir }}/{{ LOCAL_New_UbuntuISO }}"

    - name: Deploy Ubuntu VM
      vmware_guest:
        hostname: "{{ LOCAL_ESXiHost }}"
        username: "{{ LOCAL_ESXiUser }}"
        password: "{{ LOCAL_ESXiPassword }}"
        validate_certs: no
        name: "{{ LOCAL_UbuntuVMName }}"
        state: poweredon
        guest_id: ubuntu64Guest
        cluster: "{{ LOCAL_vSphereCluster }}"
        datacenter: "{{ LOCAL_DataCenter }}"
        folder: "{{ LOCAL_VMFolder }}"
        disk:
        - size_gb: "{{ LOCAL_UbuntuVMDiskSize }}"
          type: "thin"
          datastore: "{{ LOCAL_DataStore }}"
        hardware:
          memory_mb: "{{ LOCAL_UbuntuVMMemorySize }}"
          num_cpus: "{{ LOCAL_UbuntuVMCPUs }}"
          num_cpu_cores_per_socket: "{{ LOCAL_UbuntuVMCPUCores }}"
          scsi: paravirtual
        networks:
          - name: "{{ LOCAL_UbuntuVMPortGroup }}"
            device_type: vmxnet3
        cdrom:
          type: "iso"
          iso_path: "[{{ LOCAL_DataStore }}] {{ LOCAL_DataStoreDir }}/{{ LOCAL_New_UbuntuISO }}"
        annotation: "Username: {{ LOCAL_UbuntuOSUser }} Password: VMware1!"

    - name: Wait 10 minutes for the Ubuntu installation to complete
      pause:
        seconds: 600

    - name: Copy network configuration file to working directory
      template: 
        src: "./00-installer-config.j2"
        dest: "{{ LOCAL_WorkingDir }}/00-installer-config.yaml"

    - name: Copy network configuration file to Ubuntu VM
      vmware_guest_file_operation:
        hostname: "{{ LOCAL_ESXiHost }}"
        username: "{{ LOCAL_ESXiUser }}"
        password: "{{ LOCAL_ESXiPassword }}"
        validate_certs: no
        vm_id: "{{ LOCAL_UbuntuVMName }}"
        vm_username: "{{ LOCAL_UbuntuOSUser }}"
        vm_password: "VMware1!"
        copy:
            src: "{{ LOCAL_WorkingDir }}/00-installer-config.yaml"
            dest: "/home/{{ LOCAL_UbuntuOSUser }}/00-installer-config.yaml"

    - name: Move network configuration file to right location on Ubuntu VM
      vmware_vm_shell:
        hostname: "{{ LOCAL_ESXiHost }}"
        username: "{{ LOCAL_ESXiUser }}"
        password: "{{ LOCAL_ESXiPassword }}"
        validate_certs: no
        vm_id: "{{ LOCAL_UbuntuVMName }}"
        vm_username: "{{ LOCAL_UbuntuOSUser }}"
        vm_password: "VMware1!"
        vm_shell: /usr/bin/sudo
        vm_shell_args: "mv /home/{{ LOCAL_UbuntuOSUser }}/00-installer-config.yaml /etc/netplan/00-installer-config.yaml"

    - name: Apply the network configuration on Ubuntu VM
      vmware_vm_shell:
        hostname: "{{ LOCAL_ESXiHost }}"
        username: "{{ LOCAL_ESXiUser }}"
        password: "{{ LOCAL_ESXiPassword }}"
        validate_certs: no
        vm_id: "{{ LOCAL_UbuntuVMName }}"
        vm_username: "{{ LOCAL_UbuntuOSUser }}"
        vm_password: "VMware1!"
        vm_shell: /usr/bin/sudo
        vm_shell_args: "netplan apply"

    - name: Delete working directory on Ansible controller
      file:
        path: "{{ LOCAL_WorkingDir }}"
        state: absent

Most of the tasks here are pretty self-explanatory. I’ve also tried to use descriptive names for each task to help you understand what is happening.

You will change some of the values of the variables defined under vars: so that they match your environment. Besides that it’s pretty much good to go.

Usage

So, to deploy an Ubuntu Server 20.04 VM using this Playbook you’ll first clone the repository:

git clone https://github.com/rutgerblom/ubuntu-autoinstall.git

Then modify util_DeployUbuntu.yml so that the values of the variables match your environment:

Now run the Playbook with:

sudo ansible-playbook util_DeployUbuntu.yml

The deployment kicks off and will take about 15 minutes depending on your environment:

And the result is a shiny new Ubuntu Server 20.04 VM:

Not too hard. If Ubuntu’s autoinstall would leave the networking configuration in place after reboot, I would be able to shave off 5 tasks and more than 10 minutes from this deployment. Perhaps somebody out there already knows of a way to handle this? Let me know in the comments.

Summary

In this article I showcased a very basic single-purpose Ansible Playbook for unattended deployment of an Ubuntu Server 20.04 VM on VMware vSphere. This can of course be expanded upon and become part of a larger (automation) process. Hopefully you’ll find this useful and can serve as some inspiration for your projects.

Thanks for reading.

References and resources:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.