TDD for Infrastructure Automation Code
With the advent of Infrastructure As Code and provisioning automation tools, cloud or virtual infrastructure has started to go from an immovable asset tomato a disposable asset. Teams are expected to bring up and tear down the environment at will, and all of this is without any errors in configuration or overall environment setup. While existing Infrastructure As Code scripts do a great job at this, the way they are developed has been worrying for me to say the least. Most teams resort to a write-it, run it on a test machine and validate manually to validate their automation code. This blog post is about how we can use Test Driven Development to write better Infrastructure automation code.
The Toolchain
In this example:
We will use Vagrant for its ability to create fully automated flows for creating and provisioning different types of virtualized environments.
Vagrant requires a backing virtualization provider; we will use Virtualbox in this case.
We will use a simple Shell provisioner. However, be aware that tools like Puppet perform the legwork of provisioning DevOps tools in the real world.
Finally, for infrastructure validation, we will use ServerSpec.
Install a Vagrant Plugin
First, install the vagrant-serverspec
plugin — it allows integrating ServerSpec tests into Vagrantfiles. Execute:
vagrant plugin install vagrant-serverspec
Now create a Vagrantfile and choose a base box. You may choose a box of your own, or you may use a ready-to-go Ubuntu 14.04 base box from Agility Roots which is prepackaged with essential configuration for Vagrant.
Create a Basic Vagrantfile
Add the following code to any directory of your choice
Vagrant.configure(2) do |config|
config.vm.box = "packer-ubuntu1404-base-2014-12-15-10-10-26_virtualbox.box"
config.vm.box_url = "https://github.com/agilityroots/vagrant-toolchains/releases/download/2017-12-15-ubuntu1404-base/packer-ubuntu1404-base-2014-12-15-10-10-26_virtualbox.box"
# validate vagrant box against spec
config.vm.provision "test", type: "serverspec" do |spec|
# specs are stored here
spec.pattern = '*_spec.rb'
end
end
This code:
Chooses the Ubuntu 14.04 base box from Agility Roots.
Inserts some “glue” code to provision with ServerSpec, i.e. call ServerSpec tests.
- The test filenames are expected to be of format
*_spec.rb
in the same directory as the Vagrantfile.
- The test filenames are expected to be of format
With all this boilerplate, let’s create our first spec!
Create a Spec
A “spec” or specification, is simply a set of acceptance criteria for our Vagrant box to be considered “Pass”. Let’s make up some acceptance criteria. Say we are creating infrastructure where SSH and Ansible are supposed to be present:
The
openssh-server
package should be installedAn SSH server must be listening on port 22
Ansible must be installed and
Ansible version must be x.y
We’ll see how a spec can define these requirements into failing tests so that we are sure we have created the right set of expectations from our “to be” written Infra automation code.
Write Failing Tests
Let’s write the spec file for this — name it base_spec.rb. (You can really name it anything, just ensure it matches the *_spec.rb pattern). The ServerSpec specification reads almost like English — but it does follow a certain syntax. Keep the ServerSpec tutorial handy and go through the tutorials if the code below does not make sense:
describe package('openssh-server') do
it {should be_installed}
end
describe port(22) do
it {should be_listening}
end
describe package('ansible') do
it {should be_installed}
end
describe command('/usr/bin/ansible --version') do
its(:stdout) {is_expected.to match("1.8.1.0")}
end
Now execute the command:
vagrant up --provision
This command will
Download the base box
Import it and start a basic Virtualbox VM
Set it up and try connecting to it.
Run the spec tests.
If all goes well, you should see that two specs failed:
Build the logic
Now let’s build the logic within our Vagrantfile to make our failing tests pass. In other words, let’s install Ansible, set up openssh
server and run it on port 22, In this simple example, we are using the Shell provisioner for Vagrant. A more in-depth example is available here and uses Puppet. From this guide, the method to install Ansible is as follows (insert this script into the Vagrantfile):
Vagrant.configure(2) do |config|
# ...
# ...
$script = <<SCRIPT
sudo apt-add-repository ppa:ansible/ansible
sudo apt-get update
sudo apt-get install -y ansible
SCRIPT
config.vm.provision "shell", inline: $script
config.vm.provision "test", type: "serverspec" do |spec|
# ...
# ...
end
end
Now that you have written the code to install Ansible, execute:
vagrant up --provision
Your tests have passed — in other words, you have just used Test-Driven Development to build a Vagrant box with openssh
and Ansible installed.
Closing Notes
If you notice this cycle other than documenting the required configuration into specs which becomes a stringent guideline to writing the infra automation code; we are able to assert our automation indeed works without manually installing the code in an environment or verifying the expected changes; this leads to tremendous efficiency in creating such automation. Also, note that with the application of TDD, you can have infrastructure that can be further developed and refactored as needed. You have to get into the habit of writing a failing test first. For instance, I can replace my “shell” provisioner with a Puppet provisioner, developing against the same spec each time, and be confident that my refactored code will meet the specifications.
Originally published at anadimisra.com on February 13, 2015.