This article introduces the todoroff/multipass Terraform provider: a modern, plugin-framework-based provider that exposes Multipass features as declarative infrastructure. We’ll walk through:
- Basic instance management
- Multiple NICs and static IPs
- Inline and templated cloud-init
- File upload/download without provisioners
- Patterns for Ansible and dev/CI workflows
The examples target Terraform ≥ 1.6 and Multipass ≥ 1.13.
Why another Multipass provider?
There is an existing Multipass provider out in the wild (larstobi/terraform-provider-multipass), but users have requested features like:
- Network interfaces & predictable IP handling
- Better cloud-init ergonomics
- More expressive networking and file transfer support
This provider focuses on:
- Rich instance schema: CPU, memory, disk, multiple networks, mounts, cloud-init (file or inline), auto-recovery.
-
Networking primitives:
networksblock that maps directly tomultipass networksand supports multiple NICs with explicit MACs. -
First-class cloud-init:
cloud_init_fileorcloud_initinline, with clear mutual exclusion and templating support. -
File workflows:
multipass_file_uploadandmultipass_file_downloadas alternatives to brittle provisioners. - Clean diagnostics: backed by a dedicated CLI parsing layer for better error messages.
Getting started
Add the provider to your Terraform configuration:
terraform {
required_providers {
multipass = {
source = "todoroff/multipass"
version = ">= 1.4.0"
}
}
}
provider "multipass" {
# Optional overrides
multipass_path = "/usr/bin/multipass"
command_timeout = 180 # seconds
default_image = "lts" # fallback image alias
}
A basic VM with networking and an alias
First, let’s create a simple dev box, attach it to a host NIC, and expose a convenient alias:
resource "multipass_instance" "dev" {
name = "dev-box"
image = "lts"
cpus = 2
memory = "4G"
disk = "15G"
# Attach to a specific host NIC from `multipass networks`
networks {
name = "en0"
}
# Optional host mount
mounts {
host_path = "/home/USERNAME/projects"
instance_path = "/workspace"
read_only = false
}
primary = true
auto_recover = true
}
resource "multipass_alias" "shell" {
name = "dev-shell"
instance = multipass_instance.dev.name
command = "bash"
}
output "dev_ip" {
value = multipass_instance.dev.ipv4
}
-
networksblock: Binds the VM to a host network (e.g.en0on macOS) discovered viamultipass networks. -
ipv4output: Exposes the VM’s IP(s) for use in other Terraform resources or inventories.
Multiple NICs and deterministic MACs
Multipass supports multiple NICs per instance; this provider lets you express that with repeated networks blocks. Each entry becomes a --network flag under the hood.
resource "multipass_instance" "router" {
name = "lab-router"
image = "22.04"
cpus = 2
memory = "2G"
disk = "8G"
# NIC 1: Wi-Fi
networks {
name = "en0"
mode = "manual"
mac = "52:54:00:4b:ab:bd"
}
# NIC 2: Wired
networks {
name = "en5"
mode = "manual"
mac = "52:54:00:4b:ab:cd"
}
}
This pattern is useful when you care about:
- Stable MACs for DHCP reservations.
- Traffic separation across multiple host interfaces.
- Lab setups that mimic routers or multi-homed hosts.
Static IPs with cloud-init + Netplan
If you want static IPs inside the guest, the recommended approach is:
- Use a fixed MAC in the
networksblock. - Use cloud-init to write a Netplan config that assigns a static IP to that MAC.
- Apply Netplan on boot.
Here’s a complete example using inline cloud-init:
resource "multipass_instance" "static" {
name = "lab-node-1"
image = "22.04"
cpus = 1
memory = "2G"
disk = "5G"
# Stable MAC on a specific host NIC
networks {
name = "en0"
mode = "manual"
mac = "52:54:00:4b:ab:bd"
}
# Inline cloud-init configuring a static IPv4 via Netplan
cloud_init = <<-EOT
#cloud-config
write_files:
- path: /etc/netplan/10-custom.yaml
permissions: "0644"
content: |
network:
version: 2
ethernets:
extra0:
dhcp4: no
match:
macaddress: "52:54:00:4b:ab:bd"
addresses: ["192.168.64.97/24"]
runcmd:
- netplan apply
EOT
}
output "static_ip" {
value = multipass_instance.static.ipv4
}
-
cloud_initis a normal Terraform string; you can also feed it fromfile()ortemplatefile(). - The instance’s
ipv4attribute will include the static address once cloud-init finishes.
For more complex cloud-init content, the provider also supports:
-
cloud_init_file: path to a YAML file on disk. -
cloud_init: inline YAML (mutually exclusive withcloud_init_file).
Template-driven cloud-init (CI runner example)
The provider ships with a cloud-init-lab example that shows how to render cloud-init from a template and assign it to instances.
A simplified pattern:
locals {
rendered_cloud_init = templatefile("${path.module}/cloud-init.tpl", {
username = "ci-runner"
motd = "Runner ready!"
})
}
resource "multipass_instance" "runner" {
name = "ci-runner"
image = "lts"
cpus = 2
memory = "3G"
disk = "12G"
cloud_init = local.rendered_cloud_init
}
Template (cloud-init.tpl):
#cloud-config
users:
- name: ${username}
groups: sudo
shell: /bin/bash
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
runcmd:
- printf "%s\n" "${motd}" > /etc/motd
This works well for:
- Per-environment cloud-init (dev vs staging vs CI).
- Injecting SSH keys, user accounts, packages, and systemd unit files.
- Staying purely declarative (no provisioners required).
File upload and download without provisioners
Instead of relying on local-exec + multipass transfer, the provider exposes dedicated resources:
-
multipass_file_upload: host → instance. -
multipass_file_download: instance → host.
Example: upload a rendered config and fetch logs back:
resource "multipass_file_upload" "nginx_conf" {
instance = multipass_instance.dev.name
direction = "upload"
source_path = "${path.module}/nginx.conf"
destination_path = "/etc/nginx/nginx.conf"
}
resource "multipass_file_download" "logs" {
instance = multipass_instance.dev.name
direction = "download"
source_path = "/var/log/nginx"
destination_path = "${path.module}/nginx-logs"
}
These resources:
- Integrate with Terraform’s dependency graph.
- Are idempotent (only rerun when inputs change).
- Remove the need for ad-hoc shell provisioners.
Ansible-friendly: stable IPs and inventories
If you’re driving configuration with Ansible (similar to the workflow described in Ryan Schachte’s blog post on Multipass, Terraform & Ansible), you can:
- Use
networks { mac = "…" }+ cloud-init Netplan to get stable IPs. - Expose those IPs via
outputs or data sources. - Generate inventory files using
local_fileor other Terraform tooling.
Example of an inventory-like output:
output "ansible_hosts" {
value = {
master = multipass_instance.lab_master.ipv4[0]
worker1 = multipass_instance.lab_worker1.ipv4[0]
worker2 = multipass_instance.lab_worker2.ipv4[0]
}
}
From there, you can either:
- Point Ansible directly at those outputs (e.g. via
terraform output -json), or - Render an INI inventory file with
local_fileandtemplatefile.
Where to go next
-
Examples: explore the
examplesdirectory (basic,dev-lab,bridged-workstation,cloud-init-lab) for ready-made scenarios. -
Docs: check the
multipass_instanceresource docs for full argument and attribute references, includingcloud_init,networks, andmounts. - CI / CD: plug this provider into your existing Terraform flows to bootstrap ephemeral CI runners, dev boxes, or disposable labs on top of Multipass.
With multiple NICs, static IPs via cloud-init, file transfer resources, and inline cloud-init all first-class, this provider is designed to make Multipass feel like a natural part of your Terraform-based infrastructure story.
Top comments (0)