If you’ve been through my Introduction to Vagrant article, then you have a basic understanding of what Vagrant is. By using a Vagrantfile, you can launch a virtual machine of any operating system on your local system. This way you can have a completely isolated environment for your needs. Share this Vagrantfile with your team and they can have a Vagrant box configured like yours.

Depending on what apps you need, you can always find a ready-to-go box on the HashiCorp Vagrant catalogue. Failing that, you can SSH into the box and install the apps by hand. But what if we want a more repeatable, automated process? In this article, we’ll cover how to create a pre-provisioned Vagrant box.

I’m going to show you two types of provisioning: shell and Ansible provisioning.

Shell Provisioning

You can add shell commands which Vagrant will execute after launching a new box. These commands can be in the Vagrantfile itself or provided via a path to an external shell file. With inline provisioning, your Vagrantfile will look like this:

Vagrant.configure("2") do |config|
  
  config.vm.box = "ubuntu/hirsute64"
  config.vm.box_version = "20210820.0.0"
  
  config.vm.provision "shell", inline: <<-SHELL
     sudo apt update; sudo apt -y upgrade;
     sudo apt-get install -y nginx
     sudo service nginx status
  SHELL

end

Run vagrant up and you’ll see the following output:

Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'ubuntu/hirsute64'...
==> default: Matching MAC address for NAT networking...
==> default: Checking if box 'ubuntu/hirsute64' version '20210820.0.0' is up to date...
==> default: Setting the name of the VM: a_default_1630206695334_8633
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
    default:
    default: Vagrant insecure key detected. Vagrant will automatically replace
    default: this with a newly generated keypair for better security.
    default:
    default: Inserting generated public key within guest...
    default: Removing insecure key from the guest if it's present...
    default: Key inserted! Disconnecting and reconnecting using new SSH key...
==> default: Machine booted and ready!
==> default: Attempting graceful shutdown of VM...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
==> default: Mounting shared folders...
    default: /vagrant => /private/tmp/a
==> default: Running provisioner: shell...
    default: Running: inline script

---

 default: No user sessions are running outdated binaries.
    default: ● nginx.service - A high performance web server and a reverse proxy server
    default:      Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
    default:      Active: active (running) since Sun 2021-08-29 03:13:22 UTC; 2s ago
    default:        Docs: man:nginx(8)
    default:     Process: 1779 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
    default:     Process: 1780 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
    default:    Main PID: 1863 (nginx)
    default:       Tasks: 3 (limit: 1121)
    default:      Memory: 7.0M
    default:      CGroup: /system.slice/nginx.service
    default:              ├─1863 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
    default:              ├─1866 nginx: worker process
    default:              └─1867 nginx: worker process
    default:
    default: Aug 29 03:13:22 ubuntu-hirsute systemd[1]: Starting A high performance web server and a reverse proxy server...
    default: Aug 29 03:13:22 ubuntu-hirsute systemd[1]: Started A high performance web server and a reverse proxy server.

I’ve truncated some output for brevity, but notice in the last few lines that Nginx is running as required. Once you commit this file in source control, the same commands will run for your team.

Ansible Provisioning

Ansible provisioning works by providing a playbook to Vagrant in the Vagrantfile.

Before we try this out, use vagrant destroy to delete the old box. Then launch a new one for our Ansible test. Create a file called ansible.yml and place the following content in it:

---
- hosts: all
 become: true
 tasks:
  - name: Install things
 apt: name= state=present
 with_items:
    - nginx

Now pass this playbook in the Vagrantfile:

Vagrant.configure(2) do |config|
  
  config.vm.box = "ubuntu/trusty64"

  config.vm.provision "ansible" do |ansible|
    ansible.playbook = "ansible.yml"
  end
  
end

Run vagrant up and you’ll see Vagrant provision our new box with Ansible:

Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'ubuntu/hirsute64'...
==> default: Matching MAC address for NAT networking...
==> default: Checking if box 'ubuntu/hirsute64' version '20210820.0.0' is up to date...
==> default: Setting the name of the VM: a_default_1630207319348_72757
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
    default:
    default: Vagrant insecure key detected. Vagrant will automatically replace
    default: this with a newly generated keypair for better security.
    default:
    default: Inserting generated public key within guest...
    default: Removing insecure key from the guest if it's present...
    default: Key inserted! Disconnecting and reconnecting using new SSH key...
==> default: Machine booted and ready!
==> default: Attempting graceful shutdown of VM...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
==> default: Mounting shared folders...
    default: /vagrant => /private/tmp/a
==> default: Running provisioner: ansible...
    default: Running ansible-playbook...

PLAY [all] *********************************************************************

TASK [Gathering Facts] *********************************************************
ok: [default]

TASK [Install things] **********************************************************
changed: [default] => (item=nginx)

PLAY RECAP *********************************************************************
default                    : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Check the logs and you’ll find that our Ansible playbook ran as expected.

Vagrant + Shell + Ansible is a great way to package dev boxes and pass them around as Vagrantfiles. Not everyone will have the same operating system and config as you, and with Vagrant, they don’t have to :)