Personal Customizations
To this point, everything we have learned was directed at creating a Vagrant box just the way we like it. If you are creating a box that is intended to be used by an entire team or organization, the box will have to remain free of personal touches, such as Git configuration and Bash aliases. What we need is a way to apply personal customizations to the shared box. Thankfully, Vagrant has a mechanism for customizing the box at creation time.
Vagrant Provisioning
Provisioners in Vagrant allow you to automatically install software, alter configurations, and more on the machine as part of the vagrant up process.
This is useful since boxes typically are not built perfectly for your use case. Of course, if you want to just use vagrant ssh and install the software by hand, that works. But by using the provisioning systems built-in to Vagrant, it automates the process so that it is repeatable. Most importantly, it requires no human interaction, so you can vagrant destroy and vagrant up and have a fully ready-to-go work environment with a single command. Powerful.
Vagrant gives you multiple options for provisioning the machine, from simple shell scripts to more complex, industry-standard configuration management systems.
Vagrant Provisioning provides us with many options but we will restrict our exploration to the 3 I find most useful.
Vagrant Provisioning via Bash
The simplest way to customize your box is to run a shell script. Remember, we need to write the Vagrant file in such a way that it can be seamlessly adjusted by each user. To do that, we rely on environment variables which will tell us the location of the scripts to run. The scripts can be either local on the host machine or an remote web server, such as a GitHub Gist.
Snippet from the vagrantfile
script_file_location = ENV['CUSTOM_SHELL_SCRIPT']
puts 'Running custom script: ' + script_file_location
config.vm.provision "shell" do |custom|
custom.name = "Personal touches via Bash"
custom.path = script_file_location
custom.privileged = false
end
What we've done in the above snippet of a our vagrantfile
is to add a little bit of Ruby code that checks the environment for the location of a Bash script to run.
Custom Bash Script
#!/usr/bin/env bash
echo "Custom script invoked using the $(whoami) account."
touch ~/custom-script-was-here.txt
When the box is created, the custom provisioning script is run and a file is left in the home directory. After that, the only way to get the provisioners to run again is to use vagrant provision
, which is not normally needed.
Vagrant Provisioning via Docker
If your team is using Docker, and who isn't these days, you can have Vagrant install containers at box creation time. Perhaps you love to use Vault to store secrets, but the rest of your team isn't quite there yet, you can have Vagrant automatically spin up the Docker version of Vault for you. Although very powerful and convenient, it is not amenable to templating like the shell script was. You can't know ahead of time how many containers a developer wants to install and what arguments the container might need. This technique requires editing of the stock vagrantfile
.
Snippet from the vagrantfile
puts 'Installing Docker container'
config.vm.provision "docker" do |custom|
custom.run "vault", args: "--cap-add=IPC_LOCK", auto_assign_name: true, daemonize: true, restart: "always"
end
Vagrant Provisioning via Ansible
The final provisioner we'll discuss is the Ansible Local Provisioner. As we've already seen, Ansible is very powerful and can easily customize the box in any way we see fit. Just like when we constructed the box itself, we'll upload our Ansible files into the guest and let Vagrant run ansible -playbook
.
There are several options to the Ansible provisioner, giving us many ways to use it. For this example, we'll use the File Provisioner to upload our playbook into the guest. The File Provisioner can also upload entire directories which is useful for more complicated Ansible configurations involving template files and roles.
Snippet from the vagrantfile
playbook_location = ENV['CUSTOM_PLAYBOOK']
puts 'Uploading custom playbook: ' + playbook_location
config.vm.provision "file", source: playbook_location, destination: "/tmp/ansible/playbook.yml"
puts 'Running custom playbook: /tmp/ansible/playbook.yml'
config.vm.provision "ansible_local" do |custom|
custom.playbook = "/tmp/ansible/playbook.yml"
custom.install = false
custom.provisioning_path = "/tmp/ansible"
custom.tmp_path = "/tmp/vagrant-ansible"
custom.become = false
custom.become_user = "root"
custom.compatibility_mode = "auto"
custom.verbose = true
custom.limit = "localhost"
custom.raw_arguments = ["--connection=local","--inventory=localhost,"]
end
Ansible Playbook
---
- name: Install Tools
hosts: localhost
become: true
tasks:
- name: Favorite Tools
apt:
name : "{{ item }}"
state : present
update_cache: yes
with_items:
- "htop"
Conclusion
Today we learned how use three different Vagrant provisioners to customize a box to our liking. This allows for the creation of a more general box for the entire organization while still allowing individuals to make automated changes. The use of environment variables allows the Vagrant file to remain static and still apply personal customizations. I encourage you to experiment with the different provisioners and find the combination that works for you and your organization.
Full Vagrant File
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.require_version ">= 2.0.4"
Vagrant.configure(2) do |config|
config.ssh.username = "vagrant"
config.ssh.password = "vagrant"
config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.provider "virtualbox" do |v|
v.gui = true
end
# customizations via Bash
script_file_location = ENV['CUSTOM_SHELL_SCRIPT']
puts 'Running custom script: ' + script_file_location
config.vm.provision "shell" do |custom|
custom.name = "Personal touches via Bash"
custom.path = script_file_location
custom.privileged = false
end
# customizations via Docker
puts 'Installing Docker container'
config.vm.provision "docker" do |custom|
custom.run "vault", args: "--cap-add=IPC_LOCK", auto_assign_name: true, daemonize: true, restart: "always"
end
# customizations via Ansible
playbook_location = ENV['CUSTOM_PLAYBOOK']
puts 'Uploading custom playbook: ' + playbook_location
config.vm.provision "file", source: playbook_location, destination: "/tmp/ansible/playbook.yml"
puts 'Running custom playbook: /tmp/ansible/playbook.yml'
config.vm.provision "ansible_local" do |custom|
custom.playbook = "/tmp/ansible/playbook.yml"
custom.install = false
custom.provisioning_path = "/tmp/ansible"
custom.tmp_path = "/tmp/vagrant-ansible"
custom.become = false
custom.become_user = "root"
custom.compatibility_mode = "auto"
custom.verbose = true
custom.limit = "localhost"
custom.raw_arguments = ["--connection=local","--inventory=localhost,"]
end
config.vm.define "bionic-xubuntu" do |server|
server.vm.provider "virtualbox" do |v|
v.gui = true
v.name = "packer.bionic-xubuntu"
end
server.vm.box = "bionic-xubuntu"
end
end