Introduction

Vagrant creates reproducible virtual machine environments. Combined with Ansible as the provisioner, you get local infrastructure that mirrors production — test playbooks, develop roles, and onboard developers with a single vagrant up. This guide covers the Ansible provisioner, multi-machine setups, and using Vagrant as your Ansible testing ground.

Basic Setup

Vagrantfile

# Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/noble64"
  config.vm.hostname = "webserver"
  config.vm.network "private_network", ip: "192.168.56.10"

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

  config.vm.provision "ansible" do |ansible|
    ansible.playbook = "site.yml"
    ansible.inventory_path = "inventory/vagrant"
  end
end

Playbook

# site.yml
---
- name: Configure web server
  hosts: all
  become: true
  roles:
    - base
    - nginx
    - app_deploy

Inventory

# inventory/vagrant
[webservers]
192.168.56.10

[all:vars]
ansible_user=vagrant
ansible_ssh_private_key_file=.vagrant/machines/webserver/virtualbox/private_key

Ansible Local Provisioner

Run Ansible inside the VM (no Ansible needed on host):

config.vm.provision "ansible_local" do |ansible|
  ansible.playbook = "site.yml"
  ansible.install_mode = "pip"
  ansible.pip_install_cmd = "pip3 install"
end

Multi-Machine Environment

Vagrant.configure("2") do |config|
  # Web servers
  (1..2).each do |i|
    config.vm.define "web#{i}" do |web|
      web.vm.box = "ubuntu/noble64"
      web.vm.hostname = "web#{i}"
      web.vm.network "private_network", ip: "192.168.56.#{10 + i}"
      web.vm.provider "virtualbox" do |vb|
        vb.memory = 1024
      end
    end
  end

  # Database server
  config.vm.define "db" do |db|
    db.vm.box = "ubuntu/noble64"
    db.vm.hostname = "db"
    db.vm.network "private_network", ip: "192.168.56.20"
    db.vm.provider "virtualbox" do |vb|
      vb.memory = 2048
    end
  end

  # Load balancer — runs Ansible after all machines are up
  config.vm.define "lb" do |lb|
    lb.vm.box = "ubuntu/noble64"
    lb.vm.hostname = "lb"
    lb.vm.network "private_network", ip: "192.168.56.5"

    # Provision ALL machines from the last one
    lb.vm.provision "ansible" do |ansible|
      ansible.playbook = "site.yml"
      ansible.limit = "all"
      ansible.groups = {
        "webservers" => ["web1", "web2"],
        "databases" => ["db"],
        "loadbalancers" => ["lb"],
        "all:vars" => {
          "ansible_user" => "vagrant",
          "env" => "development"
        }
      }
    end
  end
end

Ansible Groups from Vagrant

config.vm.provision "ansible" do |ansible|
  ansible.playbook = "site.yml"
  ansible.groups = {
    "webservers" => ["web1", "web2"],
    "databases" => ["db"],
    "loadbalancers" => ["lb"],
    "production:children" => ["webservers", "databases", "loadbalancers"]
  }
  ansible.extra_vars = {
    env: "vagrant",
    debug_mode: true
  }
  ansible.tags = ENV['ANSIBLE_TAGS'] || ""
  ansible.verbose = ENV['ANSIBLE_VERBOSE'] || false
end

Testing Playbooks with Vagrant

Development Workflow

# Create VMs and run playbooks
vagrant up

# Re-run Ansible after changes
vagrant provision

# Run specific tags
ANSIBLE_TAGS=nginx vagrant provision

# SSH into a machine
vagrant ssh web1

# Destroy and rebuild
vagrant destroy -f && vagrant up

Makefile

# Makefile
.PHONY: up provision test destroy

up:
	vagrant up

provision:
	vagrant provision

test:
	vagrant provision
	ansible-playbook -i inventory/vagrant tests/integration.yml

destroy:
	vagrant destroy -f

reset: destroy up

Vagrant + Molecule

# molecule/vagrant/molecule.yml
---
dependency:
  name: galaxy
driver:
  name: vagrant
  provider:
    name: virtualbox
platforms:
  - name: ubuntu
    box: ubuntu/noble64
    memory: 1024
    cpus: 1
  - name: rocky
    box: rockylinux/9
    memory: 1024
    cpus: 1
provisioner:
  name: ansible
verifier:
  name: ansible

Synced Folders for Development

config.vm.synced_folder "./app", "/opt/app", type: "rsync",
  rsync__exclude: [".git/", "node_modules/"]

# Or NFS for better performance
config.vm.synced_folder "./app", "/opt/app", type: "nfs"

Port Forwarding

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

Libvirt Provider (Linux)

config.vm.provider "libvirt" do |lv|
  lv.memory = 2048
  lv.cpus = 2
  lv.driver = "kvm"
end

Environment-Specific Variables

# group_vars/all/vagrant.yml (only loaded in vagrant inventory)
app_debug: true
db_host: 192.168.56.20
lb_backend_servers:
  - 192.168.56.11
  - 192.168.56.12
ssl_enabled: false
monitoring_enabled: false

Troubleshooting

SSH Connection Issues

# Check SSH config Vagrant generates
vagrant ssh-config

# Test Ansible connection
ansible all -i inventory/vagrant -m ping

Provisioning Failed

# Re-run with verbose output
ANSIBLE_VERBOSE=vvv vagrant provision

# Run specific machine only
vagrant provision web1

Slow Synced Folders

Use rsync or nfs instead of default VirtualBox shared folders:

config.vm.synced_folder ".", "/vagrant", type: "rsync"

Conclusion

Vagrant + Ansible gives you production-like infrastructure on your laptop. Use the ansible provisioner for multi-machine setups with proper inventory groups, ansible_local when Ansible isn't installed on the host, and vagrant provision for iterative development. Combine with Molecule for structured role testing. Every team member gets identical environments with vagrant up.