Chapter 2 - Local Infrastructure Development: Ansible and Vagrant
Prototyping and testing with local virtual machines
Ansible works well with any server to which you can connect—remote or local. For speedier testing and development of Ansible playbooks, and for testing in general, it’s a very good idea to work locally. Local development and testing of infrastructure is both safer and faster than doing it on remote/live machines—especially in production environments!
The past decade has seen the growth of many virtualization tools that allow for flexible and very powerful infrastructure emulation, all from your local workstation! It’s empowering to be able to play around with a config file, or to tweak the order of a server update to perfection, over and over again, with no fear of breaking an important server. If you use a local virtual machine, there’s no downtime for a server rebuild; just re-run the provisioning on a new VM, and you’re back up and running in minutes—with no one the wiser.
Vagrant, a server provisioning tool, and VirtualBox, a local virtualization environment, make a potent combination for testing infrastructure and individual server configurations locally. Both applications are free and open source, and work well on Mac, Linux, or Windows hosts.
We’re going to set up Vagrant and VirtualBox for easy testing with Ansible to provision a new server.
Your first local server: Setting up Vagrant
To get started with your first local virtual server, you need to download and install Vagrant and VirtualBox, and set up a simple Vagrantfile, which will describe the virtual server.
- Download and install Vagrant and VirtualBox (whichever version is appropriate for your OS): - Download Vagrant - Download VirtualBox (when installing, make sure the command line tools are installed, so Vagrant works with it)
- Create a new folder somewhere on your hard drive where you will keep your Vagrantfile and provisioning instructions.
- Open a Terminal or PowerShell window, then navigate to the folder you just created.
- Add a Rocky Linux 8.x 64-bit ‘box’ using the
vagrant box addcommand:vagrant box add geerlingguy/rockylinux8(note: HashiCorp’s Vagrant Cloud has a comprehensive list of different pre-made Linux boxes. Also, check out the ‘official’ Vagrant Ubuntu boxes in Vagrant’s Boxes documentation. - Create a default virtual server configuration using the box you just downloaded:
vagrant init geerlingguy/rockylinux8 - Boot your Rocky Linux server:
vagrant up
Vagrant downloaded a pre-built 64-bit Rocky Linux 8 virtual machine image (you can build your own virtual machine ‘boxes’, if you so desire), loaded the image into VirtualBox with the configuration defined in the default Vagrantfile (which is now in the folder you created earlier), and booted the virtual machine.
Managing this virtual server is extremely easy: vagrant halt will shut down the VM, vagrant up will bring it back up, and vagrant destroy will completely delete the machine from VirtualBox. A simple vagrant up again will re-create it from the base box you originally downloaded.
Now that you have a running server, you can use it just like you would any other server, and you can connect via SSH. To connect, enter vagrant ssh from the folder where the Vagrantfile is located. If you want to connect manually, or connect from another application, enter vagrant ssh-config to get the required SSH details.
Using Ansible with Vagrant
Vagrant’s ability to bring up preconfigured boxes is convenient on its own, but you could do similar things with the same efficiency using VirtualBox’s (or VMWare’s, or Parallels’) GUI. Vagrant has some other tricks up its sleeve:
- Network interface management: You can forward ports to a VM, share the public network connection, or use private networking for inter-VM and host-only communication.
- Shared folder management: Vagrant sets up shares between your host machine and VMs using NFS or (much slower) native folder sharing in VirtualBox.
- Multi-machine management: Vagrant is able to configure and control multiple VMs within one Vagrantfile. This is important because, as stated in the documentation, “Historically, running complex environments was done by flattening them onto a single machine. The problem with that is that it is an inaccurate model of the production setup, which behaves far differently.”
-
Provisioning: When running
vagrant upthe first time, Vagrant automatically provisions the newly-minted VM using whatever provisioner you have configured in the Vagrantfile. You can also runvagrant provisionafter the VM has been created to explicitly run the provisioner again.
It’s this last feature that is most important for us. Ansible is one of many provisioners integrated with Vagrant (others include basic shell scripts, Chef, Docker, Puppet, and Salt). When you call vagrant provision (or vagrant up the first time), Vagrant passes off the VM to Ansible, and tells Ansible to run a defined Ansible playbook. We’ll get into the details of Ansible playbooks later, but for now, we’re going to edit our Vagrantfile to use Ansible to provision our virtual machine.
Open the Vagrantfile that was created when we used the vagrant init command earlier. Add the following lines just before the final ‘end’ (Vagrantfiles use Ruby syntax, in case you’re wondering):
1 # Provisioning configuration for Ansible.
2 config.vm.provision "ansible" do |ansible|
3 ansible.playbook = "playbook.yml"
4 end
This is a very basic configuration to get you started using Ansible with Vagrant. There are many other Ansible options you can use once we get deeper into using Ansible. For now, we just want to set up a very basic playbook—a simple file you create to tell Ansible how to configure your VM.
Your first Ansible playbook
Let’s create the Ansible playbook.yml file now. Create an empty text file in the same folder as your Vagrantfile, and put in the following contents:
1 ---
2 - hosts: all
3 become: yes
4
5 tasks:
6 - name: Ensure chrony (for time synchronization) is installed.
7 dnf:
8 name: chrony
9 state: present
10
11 - name: Ensure chrony is running.
12 service:
13 name: chronyd
14 state: started
15 enabled: yes
I’ll get into what this playbook is doing in a minute. For now, let’s run the playbook on our VM. Make sure you’re in the same directory as the Vagrantfile and new playbook.yml file, and enter vagrant provision. You should see status messages for each of the ‘tasks’ you defined, and then a recap showing what Ansible did on your VM—something like the following:
PLAY RECAP **********************************************************
default : ok=3 changed=0 unreachable=0 failed=0
Ansible just took the simple playbook you defined, parsed the YAML syntax, and ran a bunch of commands via SSH to configure the server as you specified. Let’s go through the playbook, step by step:
1 ---
This first line is a marker showing that the rest of the document will be formatted in YAML (read an introduction to YAML).
2 - hosts: all
This line tells Ansible to which hosts this playbook applies. all works here, since Vagrant is invisibly using its own Ansible inventory file (instead of using a manually-created hosts.ini file), which just defines the Vagrant VM.
3 become: yes
Since we need privileged access to install chrony and modify system configuration, this line tells Ansible to use sudo for all the tasks in the playbook (you’re telling Ansible to ‘become’ the root user with sudo, or an equivalent).
5 tasks:
All the tasks after this line will be run on all hosts (or, in our case, our one VM).
6 - name: Ensure chrony (for time synchronization) is installed.
7 dnf:
8 name: chrony
9 state: present
This command is the equivalent of running dnf install chrony, but is much more intelligent; it will check if chrony is installed, and, if not, install it. This is the equivalent of the following shell script:
if ! rpm -qa | grep -qw chrony; then
dnf install -y chrony
fi
However, the above script is still not quite as robust as Ansible’s dnf command. What if some other package with chrony in its name is installed, but not chrony? This script would require extra tweaking and complexity to match the simple Ansible dnf command, especially after we explore the dnf module more intimately (or the apt module for Debian-flavored Linux, or package for OS-agnostic package installation).
11 - name: Ensure chrony is running.
12 service:
13 name: chronyd
14 state: started
15 enabled: yes
This final task both checks and ensures that the chronyd service is started and running, and sets it to start at system boot. A shell script with the same effect would be:
# Start chronyd if it's not already running.
if ps aux | grep -q "[c]hronyd"
then
echo "chronyd is running." > /dev/null
else
systemctl start chronyd.service > /dev/null
echo "Started chronyd."
fi
# Make sure chronyd is enabled on system startup.
systemctl enable chronyd.service
You can see how things start getting complex in the land of shell scripts! And this shell script is still not as robust as what you get with Ansible. To maintain idempotency and handle error conditions, you’ll have to do even more work with basic shell scripts than you do with Ansible.
We could be more terse (and demonstrate Ansible’s powerful simplicity) ignoring Ansible’s self-documenting name parameter and shorthand key=value syntax, resulting in the following playbook:
1 ---
2 - hosts: all
3 become: yes
4 tasks:
5 - dnf: name=chrony state=present
6 - service: name=chronyd state=started enabled=yes
Cleaning Up
Once you’re finished experimenting with the Rocky Linux Vagrant VM, you can remove it from your system by running vagrant destroy. If you want to rebuild the VM again, run vagrant up. If you’re like me, you’ll soon be building and rebuilding hundreds of VMs and containers per week using Vagrant and Ansible!
Summary
Your workstation is on the path to becoming an “infrastructure-in-a-box,” and you can now ensure your infrastructure is as well-tested as the code that runs on top of it. With one small example, you’ve got a glimpse at the simple-yet-powerful Ansible playbook. We’ll dive deeper into Ansible playbooks later, and we’ll also explore Vagrant a little more as we go.
______________________________________
/ I have not failed, I've just found \
| 10,000 ways that won't work. (Thomas |
\ Edison) /
--------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||