DEV Community

Cover image for Multipass + Terraform: Modern VM Automation Guide
todoroff
todoroff

Posted on

Multipass + Terraform: Modern VM Automation Guide

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: networks block that maps directly to multipass networks and supports multiple NICs with explicit MACs.
  • First-class cloud-init: cloud_init_file or cloud_init inline, with clear mutual exclusion and templating support.
  • File workflows: multipass_file_upload and multipass_file_download as 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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
  • networks block: Binds the VM to a host network (e.g. en0 on macOS) discovered via multipass networks.
  • ipv4 output: 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Use a fixed MAC in the networks block.
  2. Use cloud-init to write a Netplan config that assigns a static IP to that MAC.
  3. 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
}
Enter fullscreen mode Exit fullscreen mode
  • cloud_init is a normal Terraform string; you can also feed it from file() or templatefile().
  • The instance’s ipv4 attribute 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 with cloud_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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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_file or 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]
  }
}
Enter fullscreen mode Exit fullscreen mode

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_file and templatefile.

Where to go next

  • Examples: explore the examples directory (basic, dev-lab, bridged-workstation, cloud-init-lab) for ready-made scenarios.
  • Docs: check the multipass_instance resource docs for full argument and attribute references, including cloud_init, networks, and mounts.
  • 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)