Post

(How-To) Deploying Cloud-Init Template with Ubuntu on Proxmox with Ansible Playbook - Part 2

Note… This is a Two Part Post

This is a continuation of the post “(How-To) Deploying Cloud-Init Template with Ubuntu on Proxmox with Ansible Playbook - Part 1”. Check it out if you want to learn more about creating templates manually versus using the Ansible script below.

Difference Between User Data and the Built-in Templating within Proxmox

Before I share the Ansible script below, I want to touch upon the difference between using commands like this:

1
qm set 1234 --sshkeys "/var/lib/vz/snippets/sshkey.pub" --ciuser serveradmin --cipassword "$6$JRsY9F47HwweJbaT$GA619Vj37xThZSm/7NrLh8DkRkISoei05kxSqF3mzH2I/GhZIB1FdmVE6soqgmt5fZqVPwf8Jls.Hjx14Xits1"

versus using cloud-init user data files. To start off, the command above is leveraging the built-in features within Proxmox, which can also be updated via the GUI: Datacenter -> Host -> Template -> Cloud-Init, as seen in the screenshot below.

Cloud-Init - GUI circled in red

For more information on this, take a look at the Proxmox documentation: Cloud-Init FAQ and Cloud-Init Support. My suggestion is to use this built-in feature for quick templates you may want to use for demos or proofs of concept (POCs). However, as mentioned in the previous post, I recommend creating reusable scripts that can be customized and used in source control.

But for more complex use cases that leverage the built-in features of Ubuntu and cloud-init, I highly recommend you bypass the more basic part of the Proxmox integration and refer to this documentation: Cloud-init documentation. Using this allows for the creation of multiple users, custom software installations, and even writing custom configuration files as the machine is being set up.

Focusing on Cloud-Init

Using cloud-init requires at least one extra file, typically based in YAML, and generally called user-data. If you take a peek at the documentation I shared above Cloud-init documentation, you will also see sections for files such as network and meta-data. While I’m not going to cover those here today, they may come in handy if you need to set things like customized DNS servers, namespaces, or even static IPs.

The user-data below will demonstrate how to create a user account, embed the ssh keys for key-based login, allowing for sudo without a password, installing the hwe kernel, and finally the qemu-guest-agent or the virtual machine guest tools.

The user-data.yaml.j2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#cloud-config

# Creation of the user account and associated SSH keys
user: {{ ciuser }}
password: {{ encrypted_password }}
ssh_authorized_keys:
  - {{ sshkey }}
chpasswd:
  expire: False
# This section enable's SUDO without a password
users:
  - name: {{ ciuser }}
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: users, admin
    shell: /bin/bash
# Place distro specific packages here
packages:
  - qemu-guest-agent
  - linux-generic-hwe-22.04  
package_update: true
package_upgrade: true
package_reboot_if_required: true
# Ensures qemu-guest-agent is enabled and started. Note that if you have 
# "package_reboot_if_required" set to "true" and you are installing
# something like linux-generic-hwe-22.04, this will cause the system 
# to reboot. Consequently, the command "systemctl start --no-block
# qemu-guest-agent.service" is not strictly necessary.
runcmd:
  - [ systemctl, daemon-reload ]
  - [ systemctl, enable, qemu-guest-agent.service ]
  - [ systemctl, start, --no-block, qemu-guest-agent.service ]

The above is a Jinja2 template that is being passed variables based on the Ansible variables, both in the secrets.yaml file that I will discuss in a moment, as well as the task to convert a plain text password stored in the Ansible vault, to a hashed password for use in the userdata file. An example of a variable being passed in is signified by the ex: {{ sshkey }}; it’s the {{ and }} that tells Ansible to replace a value with a variable that can be pulled from the playbook.

Ansible Vault

The secrets.yaml file is the next file you will need to customize for your use. If you don’t already have such a file, create one with:

1
ansible-vault create secrets.yaml

This will prompt you to type a password of your choice twice, then you will be brought into Vim (unless you have changed the default), where you will need to create two variables: sshkey and cipassword. I won’t go over Vim commands here, but once you have added your SSH key, which will look something like this: ssh-ed25519 AAAB3NzaC1lZDI1NTE5AAAAI5PCrJ1U83BsqkI1MmQ1RSTUvg7j23eSJ8KQXzZmr0 youremail@yourdomain.com and a password, you can check the contents by running:

1
ansible-vault view secrets.yaml

Your output should look something like this:

1
2
sshkey: "ssh-ed25519 AAAB3NzaC1lZDI1NTE5AAAAI5PCrJ1U83BsqkI1MmQ1RSTUvg7j23eSJ8KQXzZmr0 youremail@yourdomain.com"
cipassword: "a_really_complex_password"

The Files You Need for the Ansible Script

You should now have:

  • secrets.yaml
  • user-data.yaml.j2

Place these in the directory where you plan to execute the script. Next up, we will discuss the playbook.yml.

Before we proceed any further, if you haven’t ever run an Ansible playbook before, I highly recommend checking out Techno Tim’s video going over the basics of Ansible and running some simple update commands. The video can be found here: Automate EVERYTHING with Ansible! (Ansible for Beginners)

The VARS

Below is the section of the script where you will want to make updates. This is where all the customization should take place. There is no need to change anything below this section unless you have specific needs that aren’t addressed by the customization variables. As this post does not cover such changes, I will not delve into them here.

The variables are broken out into key areas: Proxmox node information, Cloud image settings, VM settings, Template Name, Cloud-Init User Data settings, and Cloud-Init login settings. I have left comments inline to aid in your own changes, but if you have a question, please drop me a line in the comment section below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
---
- name: Prepare Proxmox template with Cloud-Init
  hosts: your-proxmox-node
  become: false
  vars:
    proxmox_node: "your-proxmox-node"
    proxmox_storage: "truenas"  # The storage ID where the images and VMs will be stored; default is "local-lvm".

    # Cloud image settings
    cloud_image_url: "https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-amd64.img"
    cloud_image_storage: "/var/lib/vz/template/iso"
    cloud_image_name: "ubuntu-22.04-server-cloudimg-amd64.img"

    # VM settings
    vm_id: 5001  # Make sure this VM ID is not in use on your Proxmox.
    cores: 2
    memory: 2048
    resize_disk: 10  # In GB, use this to increase the disk size of the VM.
    QEMU_AGENT: true # Set this to false if you prefer not to use the QEMU agent.
    UEFI: true # Set this to false if you would rather use a non-UEFI image.

    # Template Name
    template_name: "ubuntu-22.04-cloudinit-template" # Name that will show in the Proxmox console.

    # Cloud-Init User Data settings
    user_data_file: "user-data.yaml.j2"  # Make sure this file exists and contains your cloud-init user data.
    user_data_file_storage: "snippets"  # The storage ID where the snippets will be stored; default is "local".
    user_data_file_storage_path: "/mnt/pve/snippets/" # Default is "/var/lib/vz/snippets".

    # Cloud-Init login settings
    ciuser: "serveradmin"
    # cipassword - Stored in a vault file.
    # sshkey - Stored in a vault file.

The Tasks

The Ansible playbook begins by importing necessary secrets for configuration, then moves on to creating a secure, hashed password. A cloud image is retrieved and stored with the proper permissions, setting the stage for VM creation in Proxmox. After ensuring the intended VM ID is available, the playbook proceeds to create a new VM with predefined specifications.

Subsequently, the cloud image is imported as the VM’s disk, and a series of configurations are applied, including hardware settings, initialization with a Cloud-Init drive, and network setup. An SSH key is temporarily stored for secure access, then removed to maintain a clean setup.

The playbook includes tasks for enabling the QEMU Agent, resizing the VM’s disk, and injecting user-defined configurations through a ‘user-data’ file. Optionally, UEFI firmware settings are configured if required. The culmination of the playbook’s activities is the conversion of this newly configured VM into a template, which streamlines the future deployment of additional VMs with identical settings.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
  tasks:
    - name: Include vault secrets
      ansible.builtin.include_vars:
        file: secrets.yaml
      no_log: true

    - name: Generate SHA-512 hashed password
      ansible.builtin.set_fact:
        encrypted_password: "{{ cipassword | password_hash('sha512') }}"

    - name: Download the cloud image
      ansible.builtin.get_url:
        url: "{{ cloud_image_url }}"
        dest: "{{ cloud_image_storage }}/{{ cloud_image_name }}"
        owner: root
        group: root
        mode: '0644'
        timeout: 60

    - name: Check if VM ID is in use
      ansible.builtin.shell: "qm status {{ vm_id }}"
      register: vm_status
      ignore_errors: true
      args:
        executable: /bin/bash
      changed_when: vm_status.rc == 0 or vm_status.rc == 1

    - name: Create VM for template
      ansible.builtin.shell: "qm create {{ vm_id }} --name {{ template_name }} --core {{ cores }} --memory {{ memory }} --net0 virtio,bridge=vmbr0"
      args:
        executable: /bin/bash
      when: vm_status.rc != 0
      register: create_vm
      changed_when: create_vm.rc == 0
      failed_when: create_vm.rc not in [0]

    - name: Import the disk to Proxmox
      ansible.builtin.shell: |
        qm importdisk {{ vm_id }} {{ cloud_image_storage }}/{{ cloud_image_name }} {{ proxmox_storage }}
      args:
        executable: /bin/bash
      when: vm_status.rc != 0
      register: import_disk
      changed_when: import_disk.rc == 0
      failed_when: import_disk.rc not in [0]

    - name: Attach the disk to the VM
      ansible.builtin.shell: |
        qm set {{ vm_id }} --scsihw virtio-scsi-pci --scsi0 {{ proxmox_storage }}:{{ vm_id }}/vm-{{ vm_id }}-disk-0.raw
      args:
        executable: /bin/bash
      when: vm_status.rc != 0
      register: attach_disk
      changed_when: attach_disk.rc == 0
      failed_when: attach_disk.rc not in [0]

    - name: Configure add Cloud-Init drive, set serial console, boot order, and network to dhcp
      ansible.builtin.shell: |
        qm set {{ vm_id }} --ide2 {{ proxmox_storage }}:cloudinit --boot c --bootdisk scsi0 --serial0 socket --vga serial0 --citype nocloud --ipconfig0 ip=dhcp
      args:
        executable: /bin/bash
      when: vm_status.rc != 0
      register: cloud_init
      changed_when: cloud_init.rc == 0
      failed_when: cloud_init.rc not in [0]

    - name: Copy ssh key to Proxmox node temporarily
      ansible.builtin.copy:
        dest: "{{ user_data_file_storage_path }}/snippets/{{ vm_id }}.pub"  # Specify the destination path here
        content: "{{ sshkey }}"
        remote_src: false
        owner: root
        group: root
        mode: '0644'
      when: vm_status.rc != 0  

    - name: Delete temporarily copied ssh key
      ansible.builtin.file:
        path: "{{ user_data_file_storage_path }}/snippets/{{ vm_id }}.pub"
        state: absent
      when: vm_status.rc != 0

    - name: Enable QEMU Agent
      ansible.builtin.shell: |
        qm set {{ vm_id }} --agent 1
      args:
        executable: /bin/bash
      when: vm_status.rc != 0 and QEMU_AGENT
      register: enable_qemu_agent
      changed_when: enable_qemu_agent.rc == 0
      failed_when: enable_qemu_agent.rc not in [0]

    - name: Resize disk
      ansible.builtin.shell: |
        qm resize {{ vm_id }} scsi0 +{{ resize_disk }}G
      args:
        executable: /bin/bash
      when: vm_status.rc != 0
      register: resize_disk
      changed_when: resize_disk.rc == 0
      failed_when: resize_disk.rc not in [0]

    - name: Copy user-data to Proxmox node
      ansible.builtin.template:
        src: "./{{ user_data_file }}"
        remote_src: false
        dest: "{{ user_data_file_storage_path }}/snippets/user-data.yaml"
        owner: root
        group: root
        mode: '0644'
      when: vm_status.rc != 0

    - name: Attach user-data to VM
      ansible.builtin.shell: |
        qm set {{ vm_id }} --cicustom "user={{ user_data_file_storage }}:snippets/user-data.yaml"
      args:
        executable: /bin/bash
      when: vm_status.rc != 0
      register: attach_user_data
      changed_when: attach_user_data.rc == 0
      failed_when: attach_user_data.rc not in [0]

    - name: Enable UEFI
      ansible.builtin.shell: |
        qm set {{ vm_id }} --bios ovmf --machine q35 --efidisk0 {{ proxmox_storage }}:0
      args:
        executable: /bin/bash
      when: vm_status.rc != 0 and UEFI
      register: enable_uefi
      changed_when: enable_uefi.rc == 0
      failed_when: enable_uefi.rc not in [0]

    - name: Convert VM to template
      ansible.builtin.shell: |
        qm template {{ vm_id }}
      args:
        executable: /bin/bash
      when: vm_status.rc != 0
      register: convert_template
      changed_when: convert_template.rc == 0
      failed_when: convert_template.rc not in [0]

Find the complete scripts and user-data file in the repo: Ansible-Proxmox-Ubuntu-Cloud-Init-Template.

Let me know what you think of this script in the comments below. Help me out by liking the repo or giving it a star.

This post is licensed under CC BY 4.0 by the author.