The multipass obsession continues… In this article, I’m going to show you how multipass makes it very easy to make an Ubuntu VM that you can use as an ansible test machine. We could do this in the cloud, but it can be a real pain getting a VM created quickly, it costs money, you can’t make a snapshot, you might need to delete it and start again etc etc… You could also create a VM in proxmox and snapshot it, restart it, revert it etc etc… but that is a lot of steps to a GUI and clicks. Or, you could use multipass to get a brand new VM every 40 seconds. So, here goes, the simplest possible implementation, and then some automation of the steps at the end.
SSH Keys
So, we need to make sure we have an SSH private and public key first. Multipass instances are passwordless by default, so we use an SSH key to access them. Even though Multipass comes with it’s own SSH key, it is in a directory that requires sudo rights to get to it on a Mac… So, just use your own. Generate one like so. Don’t password protect it, just hit enter. Also, i’m presuming Linux or Mac here, Windows users, you are on your own!
ssh-keygen -t ed25519 -f ~/.ssh/id_multipass
We will get two files, ~/.ssh/id_multipass and ~/.ssh/id_multipass.pub.
Instance Creation
Start by creating an instance called ansible-test
multipass launch --name ansible-test
And then copy the public key to the authorized_users file on the instance so it will let us login via SSH.
multipass exec ansible-test -- bash -c "cat >> ~/.ssh/authorized_keys" < ~/.ssh/id_multipass.pub
Then, do an info command and note the IP of it (this example presumes it is 192.168.2.13, yours will be different!).
multipass info ansible-test
Ansible Playbook
We need to create a small inventory file with the required connection info for ansible to connect to the VM.
Create a file called inventory.yml with the IP from before.
all:
hosts:
ansible-test:
ansible_connection: ssh
ansible_host: "192.168.2.13"
ansible_user: ubuntu
ansible_ssh_private_key_file: ~/.ssh/id_multipass
Then create a file called playbook.yml which is what we will run. It is a very simple test playbook, make it as complicated as you like though.
---
- name: Write a file on the Multipass VM
hosts: all
tasks:
- name: Create a test file in /tmp
copy:
content: "Hello from Ansible!"
dest: /tmp/ansible_test.txt
mode: '0644'
Then run it
ansible-playbook -i inventory.yml playbook.yml
It should succeed!
% ansible-playbook -i inventory.yml playbook.yml
PLAY [Write a file on the Multipass VM] *********************************************************************************************************************
TASK [Gathering Facts] **************************************************************************************************************************************
[WARNING]: Platform linux on host ansible-test is using the discovered Python interpreter at /usr/bin/python3.12, but future installation of another Python
interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.18/reference_appendices/interpreter_discovery.html for more
information.
ok: [ansible-test]
TASK [Create a test file in /tmp] ***************************************************************************************************************************
changed: [ansible-test]
PLAY RECAP **************************************************************************************************************************************************
ansible-test : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Then, if you want to start again just do this. Not too shabby.
multipass delete ansible-test
multipass purge
multipass launch --name ansible-test
multipass exec ansible-test -- bash -c "cat >> ~/.ssh/authorized_keys" < ~/.ssh/id_multipass.pub
# Update inventory.yml IP address
Taskfile
But…… we could make this even easier. So, here is a taskfile to smooth it out Tweak the SSH key to your liking in the variable at the top. Just create it as taskfile.yml and then type task rebuild and then run task ansible-playbook. Awesome! You can recreate an instance in 40 seconds (Apple M1 Max)
version: '3'
vars:
SSH_KEY: ~/.ssh/id_multipass
tasks:
launch-instance:
desc: Launch a Multipass VM named ansible-test
cmds:
- multipass launch --name ansible-test
delete-instance:
desc: Delete the ansible-test Multipass VM
cmds:
- multipass delete ansible-test || true
- multipass purge
add-ssh-key:
desc: Append your public SSH key to the VM's authorized_keys
cmds:
- multipass exec ansible-test -- bash -c "cat >> ~/.ssh/authorized_keys" < {{.SSH_KEY}}.pub
get-vm-ip-unix:
desc: Get the IP address of the Multipass VM on macOS/Linux and save to .vm_ip
platforms: [darwin, linux]
cmds:
- multipass info ansible-test | grep 'IPv4:' | awk '{print $2}' > .vm_ip
get-vm-ip-windows:
desc: Get the IP address of the Multipass VM on Windows and save to .vm_ip
platforms: [windows]
cmds:
- multipass info ansible-test | powershell -Command "Select-String 'IPv4:' | ForEach-Object { \$_.Line.Split(':')[1].Trim() }" > .vm_ip
get-vm-ip:
desc: Get the IP address of the Multipass VM and save to .vm_ip (cross-platform)
cmds:
- task: get-vm-ip-unix
- task: get-vm-ip-windows
generate-inventory:
desc: Generate Ansible inventory.yml with the VM IP read from .vm_ip
cmds:
- |
VM_IP=$(cat .vm_ip)
cat <<EOF > inventory.yml
all:
hosts:
ansible-test:
ansible_connection: ssh
ansible_host: "$VM_IP"
ansible_user: ubuntu
ansible_ssh_private_key_file: {{.SSH_KEY}}
EOF
ansible-ping:
desc: Test Ansible connectivity to the VM (not supported on Windows hosts)
platforms: [darwin, linux]
cmds:
- ansible all -i inventory.yml -m ping
ansible-playbook:
desc: Run the Ansible playbook against the VM
cmds:
- ansible-playbook -i inventory.yml playbook.yml
rebuild:
desc: Delete the ansible-test instance, create a new one, and generate inventory.yml
cmds:
- task delete-instance
- task launch-instance
- task get-vm-ip
- task generate-inventory
- task add-ssh-key
- task ansible-ping
If we run it, success!
% task rebuild
task: [rebuild] task delete-instance
task: [delete-instance] multipass delete ansible-test || true
task: [delete-instance] multipass purge
task: [rebuild] task launch-instance
task: [launch-instance] multipass launch --name ansible-test
Launched: ansible-test
task: [rebuild] task get-vm-ip
task: [get-vm-ip-unix] multipass info ansible-test | grep 'IPv4:' | awk '{print $2}' > .vm_ip
task: [rebuild] task generate-inventory
task: [generate-inventory] VM_IP=$(cat .vm_ip)
cat <<EOF > inventory.yml
all:
hosts:
ansible-test:
ansible_connection: ssh
ansible_host: "$VM_IP"
ansible_user: ubuntu
ansible_ssh_private_key_file: ~/.ssh/id_multipass
EOF
task: [rebuild] task add-ssh-key
task: [add-ssh-key] multipass exec ansible-test -- bash -c "cat >> ~/.ssh/authorized_keys" < ~/.ssh/id_multipass.pub
task: [rebuild] task ansible-ping
task: [ansible-ping] ansible all -i inventory.yml -m ping
[WARNING]: Platform linux on host ansible-test is using the discovered Python interpreter at
/usr/bin/python3.12, but future installation of another Python interpreter could change the meaning of
that path. See https://docs.ansible.com/ansible-core/2.18/reference_appendices/interpreter_discovery.html
for more information.
ansible-test | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.12"
},
"changed": false,
"ping": "pong"
}
If you want to override the SSH key in a run, you can do this as well
task rebuild SSH_KEY=/path/to/your/key
Location of Multipass SSH Key
This could be useful if you want to just use the native key. Copy to a more central location if you want to avoid using a dedicated Private Key. Just tweak the things above (no need to update the authorized keys!).
| Platform | SSH Key Location |
|---|---|
| macOS | /var/root/Library/Application Support/multipassd/ssh-keys/id_rsa |
| Linux (snap) | /var/snap/multipass/common/data/multipassd/ssh-keys/id_rsa |
| Linux (deb) | ~/.local/share/multipassd/ssh-keys/id_rsa |
| Windows | %USERPROFILE%\\AppData\\Local\\Multipass\\data\\multipassd\\ssh-keys\\id_rsa |