- Published on
A Tour with Vagrant and Virtualbox on Mac
- Authors
- Name
- Ruan Bekker
- @ruanbekker
Vagrant, yet another amazing product from Hashicorp.
Vagrant makes it really easy to provision virtual servers for local development (not limited to), which they refer as "boxes", that enables developers to run their jobs/tasks/applications in a really easy and fast way. Vagrant utilizes a declarative configuration model, so you can describe which OS you want, bootstrap them with installation instructions as soon as it boots, etc.
What are we doing today?
When completing this tutorial, you will have Vagrant and Virtualbox installed on your Mac and should be able to launch a Ubuntu Virtual Server locally with Vagrant and using the Virtualbox provider which will be responsible for running our VM's.
We will also look at different configuration options to configure the VM, bootstrapping software, using the shell, docker and ansible provisioner.
For this demonstration, I am using a Mac OSX, but you can run this on Mac, Windows or Linux. First we will use Homebrew to install Virtualbox, then Vagrant, then we will provision a Ubuntu box and I will also show how to inject shell commands into your Vagrantfile so that you can provision software to your VM, and also forward traffic to a web server from the host to the guest.
If you are looking for a Linux version instead of mac, you can look at this post:
Pre-Requisites
I will be installing Vagrant and Virtualbox with Homebrew, if you do not have homebrew installed, you can install homebrew with:
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
Once homebrew is installed, it's a good thing to update the indexes:
$ brew update
Virtualbox
Install VirtualBox using homebrew:
$ brew install --cask virtualbox
Vagrant
Install Vagrant using homebrew:
$ brew install --cask vagrant
Install the virtualbox guest additions plugin for vagrant:
$ vagrant plugin install vagrant-vbguest
If you would like a vagrant manager utility to help you manage your vagrant boxes, you can install vagrant-manager using homebrew:
$ brew install --cask vagrant-manager
Create your first Vagrant Box
From app.vagrantup.com/boxes/search you can search for any box, such as ubuntu, centos, alpine etc and for this demonstration I am going with ubuntu/focal64.
I am creating a new directory for my devbox:
$ mkdir devbox
$ cd devbox
Then initialize the Vagrantfile by running:
$ vagrant init ubuntu/focal64
A Vagrantfile
has been created in the current working directory:
$ cat Vagrantfile | grep -v "#"
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
end
Boot the VM:
$ vagrant up
The box should now be in a started state, and we can verify that by running:
$ vagrant status
Current machine states:
default running (virtualbox)
We can now SSH to our VM by running:
$ vagrant ssh
vagrant@ubuntu-focal:~$
Installing Software with Vagrant
First let's destroy the VM that we created:
$ vagrant destroy --force
Then edit the Vagrantfile
and add the commands that we want to be executed when the VM boots, in our case, installing Nginx:
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
config.vm.network "forwarded_port", guest: 80, host: 8080
config.vm.provision "shell", inline: <<-SHELL
apt update
apt install nginx -y
SHELL
end
You will also notice that we are forwarding port 8080 from our host, to port 80 on the VM so that we can access the webserver on port 8080 from our laptop. Then boot the VM:
$ vagrant up
Once the VM has booted and installed our software, we should be able to access the index document served by Nginx on our VM:
$ curl -I http://localhost:8080/
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 14 Aug 2021 18:11:59 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Sat, 14 Aug 2021 18:11:10 GMT
Connection: keep-alive
ETag: "6118073e-264"
Accept-Ranges: bytes
Shared Folders
Let's say you want to map your local directory to your VM, in a scenario where you want to store your index.html
on your laptop and map it to the VM, we can use config.vm.synced_folder
.
On our laptop, create a html
directory where we will store our index.hml
:
$ mkdir html
Now create the content in the index.html
under the html
directory:
$ echo "Hello, World" > html/index.html
Now we need to make vagrant aware of the folder that we are mapping to the VM, so we need to edit the Vagrantfile
and it will now look like this:
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
config.vm.network "forwarded_port", guest: 80, host: 8080
config.vm.provision "shell", inline: <<-SHELL
apt update
apt install nginx -y
SHELL
config.vm.synced_folder "html", "/var/www/html"
end
To reload the VM with our changes, we use vagrant provision
to update our VM when changes to provisioners are made, and vagrant reload
when we have config changes such as config.vm.network
, but to restart the VM and forcing provisioners to run, we can use the following:
Thanks @joshva_jebaraj
$ vagrant reload --provision
Once the VM is up, we can verify the changes:
$ curl http://localhost:8080/
Hello, World
Now we can edit our content locally which is synced to our VM.
Setting Hostname and Configure Memory
We can also configure the hostname of our VM and configure the amount of memory that we want to allocate to our VM using:
config.vm.hostname
vb.memory
An example of that will look like the following:
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
config.vm.hostname = "mydevbox"
config.vm.network "forwarded_port", guest: 80, host: 8080
config.vm.provision "shell", inline: <<-SHELL
apt update
apt install nginx -y
SHELL
config.vm.synced_folder "html", "/var/www/html"
config.vm.provider "virtualbox" do |vb|
vb.memory = "1024"
end
end
In this example our VM's hostname is mydevbox
and we assigned 1024MB of memory to our VM.
Provisioners: Shell
We can also run scripts from our local directory on our laptop on our VM using the shell provisioner.
First we need to create the script on our local directory:
$ cat bootstrap.sh
#!/usr/bin/env bash
set -x
echo "my hostname is $(hostname)"
Then in our Vagrantfile
we inform vagrant to execute the shell script:
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
config.vm.hostname = "mydevbox"
config.vm.provision :shell, :path => "bootstrap.sh"
end
Since my VM is already running, I will be doing a reload
:
$ vagrant reload --provision
...
==> default: Running provisioner: shell...
default: Running: /var/folders/04/r10yvb8d5dgfvd167jz5z23w0000gn/T/vagrant-shell20210814-70233-1p9dump.sh
default: ++ hostname
default: my hostname is mydevbox
default: + echo 'my hostname is mydevbox'
As you can see the shell script from our local directory was executed on our VM, you can use this method to automate installations as well, etc.
Provisioners: Docker
Vagrant offers a docker provisioner, and for this example we will be hosting a mysql server using docker container in our VM.
Our Vagrantfile
:
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
config.vm.hostname = "mydevbox"
config.vm.network "forwarded_port", guest: 3306, host: 3306
config.vm.provision "docker" do |d|
d.run "mysql", image: "mysql:8.0",
args: "-p 3306:3306 -e MYSQL_ROOT_PASSWORD=password"
end
end
Since I don't have port 3306
listening locally, I have mapped port 3306
from my laptop to port 3306
on my VM and I am using the mysql:8.0
container image from docker hub and passing the arguments which is specific to the container.
The convenient thing about the docker provisioner, is that it will install docker onto the VM for you.
Once the config has been set in your Vagrantfile
do a reload:
$ vagrant reload --provision
...
default: /vagrant => /Users/ruanbekker/workspace/vagrant/devbox
==> default: Running provisioner: docker...
default: Installing Docker onto machine...
==> default: Starting Docker containers...
==> default: -- Container: mysql
From our laptop we should be able to communicate with our mysql server:
$ nc -vz localhost 3306
found 0 associations
found 1 connections:
1: flags=82<CONNECTED,PREFERRED>
outif lo0
src 127.0.0.1 port 58745
dst 127.0.0.1 port 3306
rank info not available
TCP aux info available
Connection to localhost port 3306 [tcp/mysql] succeeded!
We can also SSH to our VM and verify if the container is running:
$ vagrant ssh
And then list the containers:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
30a843a486ae mysql:8.0 "docker-entrypoint.sh 2 minutes ago Up 2 minutes 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp mysql
Provisioners: Ansible
We can also execute Ansible playbooks on our VM using the Ansible Provisioner.
Something to note is that we use ansible
to execute the playbook on the host, and ansible_local
to execute the playbook on the VM.
First we will create our project structure for ansible, so that we have the following in place:
.
Vagrantfile
provisioning/playbook.yml
provisioning/group_vars/all
Create the provisioning
directory:
$ mkdir provisioning
Then the content for our provisioning/playbook.yml
playbook:
---
- hosts: all
become: yes
tasks:
- name: ensure ntpd is at the latest version
apt:
pkg: ntp
state: "{{ desired_state }}"
notify:
- restart ntpd
handlers:
- name: restart ntpd
service:
name: ntp
state: restarted
Our provisioning/group_vars/all
file that will contain the variables for the all group:
desired_state: "latest"
In our Vagrantfile
:
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
config.vm.hostname = "mydevbox"
config.vm.provision :ansible do |ansible|
ansible.playbook = "provisioning/playbook.yml"
end
end
When using ansible with vagrant the inventory is auto-generated when then inventory is not specified. Vagrant will store the inventory on the host at .vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory
.
To execute playbooks with ansible, we need ansible installed on our host machine, for this demonstration I will be using virtualenv and then install ansible using pip:
$ python3 -m pip install virtualenv
$ virtualenv -p $(which python3) .venv
$ source .venv/bin/activate
$ pip install ansible
Now that we have ansible installed, reload the VM to execute the playbook on our VM:
$ vagrant reload --provision
...
==> default: Running provisioner: ansible...
default: Running ansible-playbook...
PLAY [all] *********************************************************************
TASK [Gathering Facts] *********************************************************
ok: [default]
TASK [ensure ntpd is at the latest version] ************************************
ok: [default]
PLAY RECAP *********************************************************************
default : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Pretty neat right?
Tear Down
To destroy the VM:
$ vagrant destroy --force
Resources
For more information on vagrant, check out their documentation:
On provisioning documentation:
- https://www.vagrantup.com/docs/provisioning/shell
- https://www.vagrantup.com/docs/provisioning/docker
- https://www.vagrantup.com/docs/provisioning/ansible_intro
I have a couple of example Vagrantfile
s available on my github repository:
Thank You
Thanks for reading, feel free to check out my website, and subscribe to my newsletter or follow me at @ruanbekker on Twitter.
- Linktree: https://go.ruan.dev/links
- Patreon: https://go.ruan.dev/patreon