Efficient Testing Environments with Vagrant and Ansible: Simplifying Automation


Bicycle

Setting up consistent testing environments is essential for development, and Vagrant combined with Ansible is a powerful, flexible solution for automating this process on local virtual machines. Vagrant, an open-source tool from HashiCorp, simplifies the creation and management of virtualized environments, while Ansible automates the configuration within those environments using playbooks. Here’s a look at how this combination can streamline testing infrastructure setup.

Why Use Vagrant and Ansible Together?

Vagrant is designed to work with local virtualization tools like VirtualBox, providing developers with an easy way to spin up and tear down VM environments. Ansible, on the other hand, is a configuration management tool that uses simple YAML playbooks to define and enforce server configurations.

By using Vagrant to create the base virtual machine and Ansible to handle provisioning, you can:

  1. Ensure Consistency: Every environment can be set up identically, reducing “it works on my machine” issues.
  2. Increase Speed: Quickly spin up testing environments without manual setup.
  3. Enhance Scalability: Provision multiple VMs with specific roles or configurations as needed.

Setting Up Vagrant and Ansible for Local Testing

Define an ansible inventory with some extras

Using the yaml inventory style to define a inventory with virtual machines that we need for our testing setup. Ansible inventory is also able to define custom variables for certain hosts and groups. So, along with the predefined ansible configuration values within the inventory, we can add variables within this configuration file to define for example:

  • forwarded ports
  • synced folders
  • init commands
 1---
 2all:
 3  hosts:
 4    testserver01:
 5      ansible_host: 192.168.199.9
 6      ansible_user: vagrant
 7      ansible_ssh_private_key_file: .vagrant/machines/testserver01/vmware_fusion/private_key
 8      forwarded_ports:
 9        - guest: 8200
10          host: 8200
11
12    testserver02:
13      ansible_host: 192.168.199.10
14      ansible_user: vagrant
15      ansible_ssh_private_key_file: .vagrant/machines/testserver02/vmware_fusion/private_key
16      cpu: 4
17      memory: 4096
18      forwarded_ports:
19        - guest: 80
20          host: 8080
21        - guest: 8080
22          host: 8088
23
24    testclient:
25      ansible_host: 192.168.199.100
26      ansible_user: vagrant
27      ansible_ssh_private_key_file: .vagrant/machines/testclient/vmware_fusion/private_key
28      synced_folders:
29      - src: ./client
30        dest: /usr/local/client/
31      shell_always:
32        cmd: "echo VAULT_ADDR=https://192.168.199.9:8200 >> ~/.bashrc"
33
34  children:
35    vault_nodes:
36      hosts:
37        testserver01:
38
39    docker_nodes:
40      vars:
41        docker_users:
42          - vagrant
43
44      hosts:
45        testserver02:

Ansible Inventory for define Vagrant virtualization

Combining these technologies is available out of the box, because vagrant can run ansible provisioning tasks. So the logical next step is to use the ansible inventory configuration to have a single definition of actions and machines for our use case(s). Vagrantfiles are written in ruby, that allows us to write further ruby code to enhance the functionality. And this leads to following example on how to combine a ansible inventory with a vagrantfile:

 1require 'rbconfig'
 2require 'yaml'
 3
 4ENV["LC_ALL"] = "en_US.UTF-8"
 5DEFAULT_BASE_BOX = 'bento/ubuntu-24.04'
 6FORCE_LOCAL_RUN = false
 7VAGRANTFILE_API_VERSION = '2'
 8PROJECT_NAME = '/' + File.basename(Dir.getwd)
 9
10inventory = YAML.load_file(File.join(__dir__,  'hosts.yml'))
11hosts = inventory['all']['hosts']
12
13def network_options(host)
14  options = {}
15
16  if host.key?('ansible_host')
17    options[:ip] = host['ansible_host']
18    options[:netmask] = host['netmask'] ||= '255.255.255.0'
19  else
20    options[:type] = 'dhcp'
21  end
22
23  options[:mac] = host['mac'].gsub(/[-:]/, '') if host.key?('mac')
24  options[:auto_config] = host['auto_config'] if host.key?('auto_config')
25  options[:virtualbox__intnet] = true if host.key?('intnet') && host['intnet']
26  options
27end
28
29def custom_synced_folders(vm, host)
30  return unless host.key?('synced_folders')
31  folders = host['synced_folders']
32
33  folders.each do |folder|
34    vm.synced_folder folder['src'], folder['dest'], folder['options']
35  end
36end
37
38def shell_provisioners_always(vm, host)
39  if host.has_key?('shell_always')
40    scripts = host['shell_always']
41
42    scripts.each do |script|
43      vm.provision "shell", inline: script['cmd'], run: "always"
44    end
45  end
46end
47
48def forwarded_ports(vm, host)
49  if host.has_key?('forwarded_ports')
50    ports = host['forwarded_ports']
51
52    ports.each do |port|
53      vm.network "forwarded_port", guest: port['guest'], host: port['host']
54    end
55  end
56end
57
58Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
59  hosts.each do |idx, host|
60    config.vm.define idx do |node|
61
62      node.vm.box = host['box'] ||= DEFAULT_BASE_BOX
63      node.vm.hostname = idx
64      node.ssh.insert_key = true
65      node.ssh.forward_agent = true
66
67      node.vm.provider :virtualbox do |vb|
68        vb.memory = host['memory'] ||= 2048
69        vb.cpus = host['cpus'] ||= 2
70      end
71      node.vm.provider :vmware_fusion do |v|
72        v.gui = true
73        v.vmx["memsize"] = host['memory'] ||  "2048"
74        v.vmx["numvcpus"] = host['cpu'] || 2
75        v.vmx["cpuid.coresPerSocket"] = "1"
76        node.vm.network "private_network", ip: host['ansible_host']
77      end
78
79      node.vm.network "private_network", network_options(host)
80
81      node.vm.synced_folder ".", "/vagrant", disabled: true
82      custom_synced_folders(node.vm, host)
83      shell_provisioners_always(node.vm, host)
84      forwarded_ports(node.vm, host)
85
86    end
87  end
88end

Benefits and Use Cases

Using Vagrant with Ansible is ideal for:

  • Testing complex multi-VM environments that simulate production.
  • Developing and testing Ansible playbooks before deploying them to production.
  • Creating disposable environments that can be destroyed and recreated with ease.

This combination of tools allows for flexible, efficient local testing infrastructure that closely mirrors production setups, making it a valuable solution for any developer or DevOps engineer working on configuration management and automation.

Go Back explore our courses

We are here for you

You are interested in our courses or you simply have a question that needs answering? You can contact us at anytime! We will do our best to answer all your questions.

Contact us