Sitemap
Joaquín Menchaca (智裕)

Joaquín Menchaca (智裕)

DevOps/SRE/PlatformEng — k8s, o11y, vault, terraform, ansible

Press enter or click to view image in full size

Vagrant: Linux VMs on macOS

Provision Linux VMs on Apple Silicon (ARM64) with Ansible

8 min read3 days ago

--

Previously, we explored container-based development with Colima, then moved closer to the underlying infrastructure by managing virtual machines directly with Lima and Ansible. Now, we return to Vagrant, using VirtualBox to manage virtual machines on MacBooks powered by Apple Silicon (ARM64) processors.

We’ll follow a familiar approach: use Vagrant to manage the virtual machine lifecycle and Ansible to provision the guest operating system. The difference is that this time, VirtualBox provides the virtualization backend, while Vagrant coordinates the overall workflow.

What Made Vagrant so Popular?

Vagrant’s strength lies in its modular, plugin-based architecture. Each component handles a different layer of complexity, bringing the pieces together through a consistent interface for building and managing development environments.

Virtual machine automation. Provider plugins handle creating machines from reusable boxes, allocating CPU and memory, attaching storage, and configuring networking and port forwarding. Developers can follow a familiar workflow across different virtualization backends without having to manage each one independently.

Ready-to-use access and file sharing. Vagrant manages SSH credentials and connection details, making it straightforward to access guest machines. It also provides mechanisms for sharing project files between the host and guest through technologies such as NFS, SMB, rsync, and VirtualBox shared folders (vboxsf), depending on the host operating system and provider.

Configuration management integrated into the lifecycle. Provisioner plugins coordinate configuration management tools such as Ansible, Chef, CFEngine, Puppet, and Salt. Depending on the provisioner, Vagrant can install guest-side tooling where necessary and invoke the appropriate configuration process to bring the machine into its desired state. Developers can modify their configuration code, provision the machine again, inspect the results, or recreate the entire environment from scratch.

A cross-platform workflow. Vagrant’s availability across Linux, Windows, and macOS made it accessible to developers working on different platforms. VirtualBox played an important role in this ecosystem by providing a familiar virtualization backend across these operating systems.

Together, these capabilities made Vagrant a practical tool for creating reproducible development environments and testing infrastructure changes locally before applying them in a data center.

Requirements

  • Vagrant [vagrant]: creates and manages virtual machines.
  • Virtualbox [vboxmanage]: runs the virtual machine.
  • Ansible [ansible]: configures the guest operating system
  • Git [git]: downloads the project files.

Install the Tools

On macOS, install the tools using Homebrew:

brew install --cask vagrant virtualbox
brew install ansible

Project Files

Clone the repository with a sparse checkout to retrieve only the files for this example:

cd $HOME

git clone --depth 1 --filter=blob:none \
--sparse https://github.com/darkn3rd/blog_tutorials.git

cd blog_tutorials
git sparse-checkout set devx/vm/vagrant/virtualbox/ansible
cd devx/vm/vagrant/virtualbox/ansible

The project directory contains:

.
├── ansible.cfg # Ansible configuration
├── inventory.yaml # Ansible inventory
├── Makefile # Workflow shortcuts
├── mise.toml # Optional mise configuration
├── playbook.yaml # Ansible Playbook
├── README.md # Project documentation
├── site
│ ├── index.html # Custom web page
│ └── nginx.conf # Nginx site configuration
└── Vagrantfile # Vagrant configuration

Running the Project

The project uses Vagrant and Virtualbox to run an Ubuntu 26.04 virtual machine, then Ansible to configure it.

Step 1: Launch the Virtual Machine

The Vagrantfile defines the virtual machine and its Ansible provisioner:

Vagrant.configure("2") do |config|
config.vm.box = "bento/ubuntu-26.04"

config.vm.define "dev"

config.vm.network "forwarded_port", guest: 80, host: 8080

config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.synced_folder ".", "/workspace"

config.vm.provider "virtualbox" do |vb|
vb.cpus = 2
vb.memory = 4096
end

config.vm.provision "ansible" do |ansible|
ansible.compatibility_mode = "2.0"
ansible.playbook = "playbook.yaml"
ansible.groups = { "web" => ["dev"] }
end
end

This configuration:

  • Names the virtual machine dev and assigns it two CPUs and 4 GB of memory.
  • Forwards port 8080 on your computer’s loopback address to port 80 in the guest.
  • Shares the project directory at /workspace, with the default /vagrant share disabled.
  • Configures Ansible to run playbook.yaml, placing dev in the web inventory group.

From the project directory, start the virtual machine:

vagrant up --no-provision

The --no-provision option skips Ansible for now, allowing you to launch the virtual machine and configure it in separate steps.

After Vagrant finishes launching the system, you can view its presence in VirtualBox Manager application:

Press enter or click to view image in full size

Step 2: Provision VM with Ansible

The Ansible playbook, playbook.yaml, describes the desired configuration of the web server:

  • Nginx is installed.
  • The site’s document root and configuration exist.
  • The custom web page is deployed.
  • The default Nginx site is disabled.
  • The custom site is enabled.
  • Nginx is running and starts automatically at boot.

The following excerpt shows the package installation and service tasks:

#!/usr/bin/env ansible-playbook
---
- name: Configure the development web server
hosts: web
become: true

tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
cache_valid_time: 3600
lock_timeout: 120

# Additional tasks deploy the page and configure the site.

- name: Start nginx on boot
ansible.builtin.service:
name: nginx
state: started
enabled: true

The hosts: web setting targets the inventory group defined in the Vagrantfile. The become: true setting runs tasks with elevated privileges, which are required to install packages and manage system services.

Apply the complete playbook:

vagrant provision

Vagrant runs Ansible on your host computer and connects to the virtual machine over SSH to configure the web server.

Step 4: Verify Service Works

With Nginx configured, test the web service from your host computer on port 8080:

curl --fail --include http://localhost:8080

You should see something that looks like this:

Press enter or click to view image in full size

Open http://localhost:8080 in a browser to view the page.

Press enter or click to view image in full size

Step 5: Inspecting the System

From the project directory on your host, open a shell inside the virtual machine:

vagrant ssh

Inside the guest, inspect the service and its configuration:

# Display the guest's fully qualified hostname
hostname -f

# Check the Nginx service
systemctl status nginx --no-pager

# Test the web service from inside the guest
curl --fail http://localhost:80

# View the deployed web page
cat /var/www/example/index.html

# Inspect the available and enabled sites
ls -l /etc/nginx/sites-available/
ls -l /etc/nginx/sites-enabled/

# View the enabled site configuration
cat /etc/nginx/sites-enabled/example

For the systemctl status command, you should see something that looks something similar to this.

Press enter or click to view image in full size

Inside the guest, Nginx listens on port 80. From the host, you access it through port 8080, which VirtualBox forwards to the guest.

Get Joaquín Menchaca (智裕)’s stories in your inbox

Join Medium for free to get updates from this writer.

When finished, return to your host shell:

exit

Step 6: Make a Change and Provision Again

From the project directory on your host, replace the contents of site/index.html:

cat <<'EOF' >| site/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vagrant example</title>
</head>
<body>
<h1>The classic workflow, running on Virtualbox.</h1>
<p>Served by nginx on Ubuntu LTS, provisioned by Ansible.</p>
</body>
</html>
EOF

Apply the playbook again:

vagrant provision

Verify the updated page:

curl --include --fail http://localhost:8080

You should see something that looks like the following:

Press enter or click to view image in full size

Refresh your browser to see the new heading.

Press enter or click to view image in full size

Ansible copies the updated page into the site’s document root. Changing a static HTML file does not require restarting Nginx.

You can repeat this process without rebuilding the VM. The running machine provides the development environment, while the playbook records its desired configuration.

Run provisioning once more without changing any files:

vagrant provision

The page deployment task should report ok instead of changed, because the deployed file already matches the source. This illustrates Ansible’s idempotence: applying the same desired configuration again should leave an already configured resource unchanged.

This edit → provision → verify cycle is the core development workflow for this example.

Step 7: Halting the System

When you want to stop the environment and keep it for later, shut down the virtual machine:

vagrant halt

This preserves the guest’s disk and configuration. You san start it up again with vagrant up.

Step 8: Destroying the System

When you no longer need the environment, delete the virtual machine:

vagrant destroy

Vagrant asks for confirmation before removing the VM and its disk. Files stored only on the guest’s disk are deleted; the project files shared from your Mac remain on the host.

To recreate the environment and test provisioning from a fresh VM, run vagrant up. Vagrant creates the virtual machine and runs the Ansible provisioner automatically.

Addendum: Run Ansible Directly

You can also run ansible-playbook directly instead of invoking it through vagrant provision.

With the virtual machine running, execute these commands from the project directory:

# Write the virtual machine's SSH connection settings
vagrant ssh-config > .vagrant/ssh.config

# Run playbook
ansible-playbook playbook.yaml

Ansible uses the settings in the project’s ansible.cfg:

[defaults]
inventory = inventory.yaml
interpreter_python = auto_silent

[ssh_connection]
pipelining = true

The inventory setting points to inventory.yaml, which places the virtual machine in the web group:

web:
hosts:
dev:
ansible_ssh_common_args: >-
'-F "{{ inventory_dir }}/.vagrant/ssh.config"'

The -F option tells SSH to use the configuration generated by Vagrant. That file supplies the connection details for dev, including its hostname, port, user, and private key. The inventory_dir variable resolves to the directory containing the inventory file.

Regenerate .vagrant/ssh.config after recreating the VM so that its connection details stay current.

When you run vagrant provision, Vagrant supplies the inventory and SSH connection settings automatically. This local configuration lets you run the same playbook independently.

Links

Virtualization Tools

Vagrant Box Images Templates

  • Bento: Packer templates for building minimal Vagrant baseboxes for multiple platforms.
  • Robox: The tools needed to robotically create/configure/provision a large number of operating systems, for a variety of hypervisors, using packer.

Configuration Managment Tools

  • Ansible configuration management tool

Container Runtime Tools

  • Colima (Containers on Lima) container runtime

Previous Article

Final Thoughts

VirtualBox 7.1 brought an official macOS release for Apple Silicon, adding ARM virtualization for Linux and BSD guests. This extended the familiar Virtualbox workflow beyond Intel and AMD x86 systems to ARM64 Macs. (VirtualBox 7.1 release notes)

The Bento project provides VirtualBox boxes for both x86–64 and ARM64, making it a useful source of base images for this workflow. Choose a box that supports your host’s architecture and the Virtualbox provider.

With Vagrant defining the virtual machine and Ansible configuring its services, the development cycle stays straightforward: edit → provision → verify. You can keep the environment between sessions or recreate it from the project files when you need a fresh start.

--

--

Joaquín Menchaca (智裕)
Joaquín Menchaca (智裕)

Written by Joaquín Menchaca (智裕)

DevOps/SRE/PlatformEng — k8s, o11y, vault, terraform, ansible