Vagrant Advanced Examples

15 minute read

Overview

When I started to work with Vagrant, I had a few requirements.

  • Boot and set any number of virtual machines without a requirement to duplicate code, but be able to change the configuration for each machine.
  • Integrate Ansible playbooks (my common used configuration tool) into the Vagrant flow.
  • If required, define a custom ansible inventory file to be generated based on my configuration.
  • When needed, use an external configuration file.

Below, you will find a few advanced examples of the scenarios I used.

The GitHub repository with the examples could be found here.

The repository contains 4 examples of the Vagrantfile when each example is based on the previous and adds extra functionality.


Example1

Contains the following configuration:

* Multiple servers creation

* Insert custom ssh public key to the vm

Full Vagrantfile:

# -*- mode: ruby -*-
# vi: set ft=ruby :

ssh_key                     = "~/.ssh/id_rsa"
box                         = "centos/7"

servers = [
  { :hostname => "server1", :ip => "10.10.10.10", :ram => 1024, :cpu => 2 },
  { :hostname => "client1", :ip => "10.10.10.11", :box => "ubuntu/xenial64" },
  { :hostname => "client2", :ip => "10.10.10.12", :port_guest => 80, :port_host => 8080 },
  { :hostname => "client3", :ip => "dhcp", :folder_guest => "/srv/website", :folder_host => "src/" }
]

Vagrant.configure(2) do |config|
  if Vagrant.has_plugin?("vagrant-hostmanager")
    config.hostmanager.enabled = true
    config.hostmanager.manage_host = true
    config.hostmanager.manage_guest = true
    config.hostmanager.ignore_private_ip = false
    config.hostmanager.include_offline = false

    # Resolve dynamic ip address (dhcp) of the host and guests for the /etc/hosts file.
    # https://github.com/devopsgroup-io/vagrant-hostmanager/issues/86
    servers.each do |server|
      if server[:ip] == "dhcp"
        cached_addresses = {}
        config.hostmanager.ip_resolver = proc do |vm, resolving_vm|
          if cached_addresses[vm.name].nil?
            if hostname = (vm.ssh_info && vm.ssh_info[:host])
              vm.communicate.execute("hostname -I | cut -d ' ' -f 2") do |type, contents|
                cached_addresses[vm.name] = contents.split("\n").first[/(\d+\.\d+\.\d+\.\d+)/, 1]
              end
            end
          end
          cached_addresses[vm.name]
        end
      end
    end
  end

  servers.each do |server|
    box_image = server[:box] ? server[:box] : box;
    config.vm.define server[:hostname] do |conf|
      conf.vm.box = box_image.to_s
      conf.vm.hostname = server[:hostname]

      net_config = {}
      if server[:ip] != "dhcp"
        net_config[:ip] = server[:ip]
        net_config[:netmask] = server[:netmask] || "255.255.255.0"
      else
        net_config[:type] = "dhcp"
      end
      conf.vm.network "private_network", net_config

      if !server[:port_guest].nil? && !server[:port_host].nil?
        conf.vm.network "forwarded_port", guest: server[:port_guest], host: server[:port_host]
      end

      if !server[:folder_guest].nil? && !server[:folder_host].nil?
        conf.vm.synced_folder server[:folder_host], server[:folder_guest]
      end

      cpu = server[:cpu] ? server[:cpu] : 1;
      memory = server[:ram] ? server[:ram] : 512;
      conf.vm.provider "virtualbox" do |vb|
        vb.customize ["modifyvm", :id, "--cpus", cpu.to_s]
        vb.customize ["modifyvm", :id, "--memory", memory.to_s]
      end

      conf.ssh.private_key_path = ["~/.vagrant.d/insecure_private_key", ssh_key]
      conf.ssh.insert_key = false
      conf.vm.provision "file", source: ssh_key + ".pub", destination: "~/.ssh/authorized_keys"
    end
  end
end

Let’s go over the parts of the file:

Set global variables
SSH key and a box (vm image) that should be used for the servers.
The box variable could be overridden for each VM.

ssh_key                     = "~/.ssh/id_rsa"
box                         = "centos/7"

Define servers details
Unlimited number of servers could be defined.
Each server could have its own configuration options.

Mandatory variables:

  • hostname
  • ip (possible arguments - static ip or dhcp - “10.10.10.10”/”dhcp”)

Optional variables, could be set to override the defaults for specific box:

  • ram (default: 512)
  • cpu (default: 1)
  • box (default: defined above)
  • port_guest and port_host (port forwarding - not defined by default)
  • folder_guest and folder_host (synced folders - not defined by default)
servers = [
  { :hostname => "server1", :ip => "10.10.10.10", :ram => 1024, :cpu => 2 },
  { :hostname => "client1", :ip => "10.10.10.11", :box => "ubuntu/xenial64" },
  { :hostname => "client2", :ip => "10.10.10.12", :port_guest => 80, :port_host => 8080 },
  { :hostname => "client3", :ip => "dhcp", :folder_guest => "/srv/website", :folder_host => "src/" }
]

The vagrant-hostmanager plugin
I’m using a vagrant-hostmanager plugin for the configuration of the /etc/hosts file on my local machine and within the guests. The following section checks for the plugin existence and if exists, apply the configuration on the host and guests.
Refer to the plugin configuration for the sudo less configuration on the host.

Vagrant.configure(2) do |config|
  if Vagrant.has_plugin?("vagrant-hostmanager")
    config.hostmanager.enabled = true
    config.hostmanager.manage_host = true
    config.hostmanager.manage_guest = true
    config.hostmanager.ignore_private_ip = false
    config.hostmanager.include_offline = false

    # Resolve dynamic ip address (dhcp) of the host and guests for the /etc/hosts file.
    # https://github.com/devopsgroup-io/vagrant-hostmanager/issues/86
    servers.each do |server|
      if server[:ip] == "dhcp"
        cached_addresses = {}
        config.hostmanager.ip_resolver = proc do |vm, resolving_vm|
          if cached_addresses[vm.name].nil?
            if hostname = (vm.ssh_info && vm.ssh_info[:host])
              vm.communicate.execute("hostname -I | cut -d ' ' -f 2") do |type, contents|
                cached_addresses[vm.name] = contents.split("\n").first[/(\d+\.\d+\.\d+\.\d+)/, 1]
              end
            end
          end
          cached_addresses[vm.name]
        end
      end
    end
end

Servers loop
Loop through the list of the servers mentioned above and perform all the configuration mentioned in the next blocks.

servers.each do |server|

Set a box and hostname for the vm
Check if a custom box is defined for the specific server within the servers array and apply it.
Otherwise, apply the global box variable.

box_image = server[:box] ? server[:box] : box;
config.vm.define server[:hostname] do |conf|
  conf.vm.box = box_image.to_s
  conf.vm.hostname = server[:hostname]

VM network configuration
Set network options (static/dhcp) according to the options provided by the user.

net_config = {}
if server[:ip] != "dhcp"
  net_config[:ip] = server[:ip]
  net_config[:netmask] = server[:netmask] || "255.255.255.0"
else
  net_config[:type] = "dhcp"
end
conf.vm.network "private_network", net_config

Set port forwarding and synced folders
Set port forwarding and/or synced folders if defined.

if !server[:port_guest].nil? && !server[:port_host].nil?
  conf.vm.network "forwarded_port", guest: server[:port_guest], host: server[:port_host]
end

if !server[:folder_guest].nil? && !server[:folder_host].nil?
  conf.vm.synced_folder server[:folder_host], server[:folder_guest]
end

Set CPU and Memory
Checks for the custom cpu and/or ram defined for a specific server. Note, that if options not provided, default values defined below, applied (cpu=1, ram=512).

cpu = server[:cpu] ? server[:cpu] : 1;
memory = server[:ram] ? server[:ram] : 512;
conf.vm.provider "virtualbox" do |vb|
  vb.customize ["modifyvm", :id, "--cpus", cpu.to_s]
  vb.customize ["modifyvm", :id, "--memory", memory.to_s]
end

Insert custom ssh public key to the vm
Takes the ssh key provided within the global variables and copying the public key to the server.

conf.ssh.private_key_path = ["~/.vagrant.d/insecure_private_key", ssh_key]
conf.ssh.insert_key = false
conf.vm.provision "file", source: ssh_key + ".pub", destination: "~/.ssh/authorized_keys"

Example 2

Contains the following configuration:

* Multiple servers creation

* Insert custom ssh public key to the vm

* Ansible provisioner

* Custom groups definition for Ansible

* Inventory generated by Vagrant. Custom groups are added to the generated inventory

Full Vagrantfile:

# -*- mode: ruby -*-
# vi: set ft=ruby :

ssh_key                     = "~/.ssh/id_rsa"
box                         = "centos/7"

servers = [
  { :hostname => "server1", :ip => "10.10.10.10", :ram => 1024, :cpu => 2, :group => "servers" },
  { :hostname => "client1", :ip => "10.10.10.11", :box => "ubuntu/xenial64", :group => "clients" },
  { :hostname => "client2", :ip => "10.10.10.12", :group => "clients", :port_guest => 80, :port_host => 8080 },
  { :hostname => "client3", :ip => "dhcp", :group => "clients", :folder_guest => "/srv/website", :folder_host => "src/" }
]

ansible_playbook = "playbook.yml"

Vagrant.configure(2) do |config|
  if Vagrant.has_plugin?("vagrant-hostmanager")
    config.hostmanager.enabled = true
    config.hostmanager.manage_host = true
    config.hostmanager.manage_guest = true
    config.hostmanager.ignore_private_ip = false
    config.hostmanager.include_offline = false

    # Resolve dynamic ip address (dhcp) of the host and guests for the /etc/hosts file.
    # https://github.com/devopsgroup-io/vagrant-hostmanager/issues/86
    servers.each do |server|
      if server[:ip] == "dhcp"
        cached_addresses = {}
        config.hostmanager.ip_resolver = proc do |vm, resolving_vm|
          if cached_addresses[vm.name].nil?
            if hostname = (vm.ssh_info && vm.ssh_info[:host])
              vm.communicate.execute("hostname -I | cut -d ' ' -f 2") do |type, contents|
                cached_addresses[vm.name] = contents.split("\n").first[/(\d+\.\d+\.\d+\.\d+)/, 1]
              end
            end
          end
          cached_addresses[vm.name]
        end
      end
    end
  end

  groups = {"all" => []}
  servers.each do |cfg|
    if ! groups.has_key?(cfg[:group])
      groups[cfg[:group]] = [cfg[:hostname]]
    else
      groups[cfg[:group]].push(cfg[:hostname])
    end
    groups["all"].push(cfg[:hostname])
  end

  servers.each_with_index do |server, index|
    box_image = server[:box] ? server[:box] : box;
    config.vm.define server[:hostname] do |conf|
      conf.vm.box = box_image.to_s
      conf.vm.hostname = server[:hostname]

      net_config = {}
      if server[:ip] != "dhcp"
        net_config[:ip] = server[:ip]
        net_config[:netmask] = server[:netmask] || "255.255.255.0"
      else
        net_config[:type] = "dhcp"
      end
      conf.vm.network "private_network", net_config

      if !server[:port_guest].nil? && !server[:port_host].nil?
        conf.vm.network "forwarded_port", guest: server[:port_guest], host: server[:port_host]
      end

      if !server[:folder_guest].nil? && !server[:folder_host].nil?
        conf.vm.synced_folder server[:folder_host], server[:folder_guest]
      end

      cpu = server[:cpu] ? server[:cpu] : 1;
      memory = server[:ram] ? server[:ram] : 512;
      conf.vm.provider "virtualbox" do |vb|
        vb.customize ["modifyvm", :id, "--cpus", cpu.to_s]
        vb.customize ["modifyvm", :id, "--memory", memory.to_s]
      end

      conf.ssh.private_key_path = ["~/.vagrant.d/insecure_private_key", ssh_key]
      conf.ssh.insert_key = false
      conf.vm.provision "file", source: ssh_key + ".pub", destination: "~/.ssh/authorized_keys"

      # The ubuntu/xenial64 box is missing python. Install it for ansible provision.
      if box_image == "ubuntu/xenial64"
        conf.vm.provision "shell" do |s|
          s.inline = "test -e /usr/bin/python || (apt-get -qqy update && apt-get install -qqy python-minimal)"
        end
      end

      if index == servers.size - 1
        if ansible_playbook != ""
          conf.vm.provision :ansible do |ansible|
            ansible.verbose = "v"
            ansible.limit = "all"
            ansible.groups = groups
            ansible.playbook = ansible_playbook
          end
        end
      end
    end
  end
end

As most of the file is identical to the example above, I will mention and explain just the differences.

Define servers details
The “:group” variable has been added to the servers details.
By using these groups, custom groups are created and could be used later for the ansible playbook execution.

servers = [
  { :hostname => "server1", :ip => "10.10.10.10", :ram => 1024, :cpu => 2, :group => "servers" },
  { :hostname => "client1", :ip => "10.10.10.11", :box => "ubuntu/xenial64", :group => "clients" },
  { :hostname => "client2", :ip => "10.10.10.12", :group => "clients", :port_guest => 80, :port_host => 8080 },
  { :hostname => "client3", :ip => "dhcp", :group => "clients", :folder_guest => "/srv/website", :folder_host => "src/" }
]

Create ansible groups to be used during the play execution

groups = {"all" => []}
servers.each do |cfg|
  if ! groups.has_key?(cfg[:group])
    groups[cfg[:group]] = [cfg[:hostname]]
  else
    groups[cfg[:group]].push(cfg[:hostname])
  end
  groups["all"].push(cfg[:hostname])
end

Install python if “ubuntu/xenial64” box is used
The “ubuntu/xenial64” box is missing python. Install it for ansible provision.

if box_image == "ubuntu/xenial64"
  conf.vm.provision "shell" do |s|
    s.inline = "test -e /usr/bin/python || (apt-get -qqy update && apt-get install -qqy python-minimal)"
  end
end

Provision nodes with ansible

  • Define ansible playbook that should be executed
    The path to the playbook could be relative or absolute.
  • Note, that the servers loop changed to gather index.
    The index calculation is used by the ansible provision.
    The play execution should start after all the servers are up and running.
    The reason behind this is to execute the play once for all the machines and not for each machine separately.
  • The last section prepare the ansible provision.
    The index condition is checked at the first line.
    • Set the verbosity of the play.
    • Set the play limit.
    • Set the groups that have been created for that play, according to the servers details.
      Note, that the inventory for the ansible play, by default created by the vagrant and placed in the following location: .vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory
    • Set the play to execute.
(output omitted...)
ansible_playbook = "playbook.yml"
(output omitted...)

(output omitted...)
servers.each_with_index do |server, index|
(output omitted...)

(output omitted...)
if index == servers.size - 1
  if ansible_playbook != ""
    conf.vm.provision :ansible do |ansible|
      ansible.verbose = "v"
      ansible.limit = "all"
      ansible.groups = groups
      ansible.playbook = ansible_playbook
    end
  end
end

Example 3

Contains the following configuration:

* Multiple servers creation

* Insert custom ssh public key to the vm

* Ansible provisioner

* Custom groups definition for Ansible

* Inventory generated by Vagrant.

* Custom Ansible inventory creation (including custom groups)

Full Vagrantfile:

# -*- mode: ruby -*-
# vi: set ft=ruby :

ssh_key                     = "~/.ssh/id_rsa"
box                         = "centos/7"

servers = [
  { :hostname => "server1", :ip => "10.10.10.10", :ram => 1024, :cpu => 2, :group => "servers" },
  { :hostname => "client1", :ip => "10.10.10.11", :box => "ubuntu/xenial64", :group => "clients" },
  { :hostname => "client2", :ip => "10.10.10.12", :group => "clients", :port_guest => 80, :port_host => 8080 },
  { :hostname => "client3", :ip => "dhcp", :group => "clients", :folder_guest => "/srv/website", :folder_host => "src/" }
]

ansible_playbook = "playbook.yml"
ansible_inventory_path = "inventory/hosts"
ansible_user = "vagrant"

Vagrant.configure(2) do |config|
  if Vagrant.has_plugin?("vagrant-hostmanager")
    config.hostmanager.enabled = true
    config.hostmanager.manage_host = true
    config.hostmanager.manage_guest = true
    config.hostmanager.ignore_private_ip = false
    config.hostmanager.include_offline = false

    # Resolve dynamic ip address (dhcp) of the host and guests for the /etc/hosts file.
    # https://github.com/devopsgroup-io/vagrant-hostmanager/issues/86
    servers.each do |server|
      if server[:ip] == "dhcp"
        cached_addresses = {}
        config.hostmanager.ip_resolver = proc do |vm, resolving_vm|
          if cached_addresses[vm.name].nil?
            if hostname = (vm.ssh_info && vm.ssh_info[:host])
              vm.communicate.execute("hostname -I | cut -d ' ' -f 2") do |type, contents|
                cached_addresses[vm.name] = contents.split("\n").first[/(\d+\.\d+\.\d+\.\d+)/, 1]
              end
            end
          end
          cached_addresses[vm.name]
        end
      end
    end
  end

  if File.dirname(ansible_inventory_path) != "."
    Dir.mkdir(File.dirname(ansible_inventory_path)) unless Dir.exist?(File.dirname(ansible_inventory_path))
  end
  File.open(ansible_inventory_path, 'w') do |f|
    servers.each do |cfg|
      if cfg[:ip] != "dhcp"
        f.write "#{cfg[:hostname]} ansible_host=#{cfg[:ip]} "
        f.write "ansible_user=#{ansible_user} ansible_ssh_private_key_file=#{ssh_key}\n"
      else
        f.write "#{cfg[:hostname]} ansible_user=#{ansible_user} ansible_ssh_private_key_file=#{ssh_key}\n"
      end
    end
    f.write "\n"
    f.write "[all]\n"
    servers.each do |cfg|
      f.write "#{cfg[:hostname]}\n"
    end
    f.write "\n"
    servers.each do |cfg|
      f.write "[#{cfg[:group]}]\n"
      f.write "#{cfg[:hostname]}\n"
      f.write "\n"
    end
  end

  servers.each_with_index do |server, index|
    box_image = server[:box] ? server[:box] : box;
    config.vm.define server[:hostname] do |conf|
      conf.vm.box = box_image.to_s
      conf.vm.hostname = server[:hostname]

      net_config = {}
      if server[:ip] != "dhcp"
        net_config[:ip] = server[:ip]
        net_config[:netmask] = server[:netmask] || "255.255.255.0"
      else
        net_config[:type] = "dhcp"
      end
      conf.vm.network "private_network", net_config

      if !server[:port_guest].nil? && !server[:port_host].nil?
        conf.vm.network "forwarded_port", guest: server[:port_guest], host: server[:port_host]
      end

      if !server[:folder_guest].nil? && !server[:folder_host].nil?
        conf.vm.synced_folder server[:folder_host], server[:folder_guest]
      end

      cpu = server[:cpu] ? server[:cpu] : 1;
      memory = server[:ram] ? server[:ram] : 512;
      conf.vm.provider "virtualbox" do |vb|
        vb.customize ["modifyvm", :id, "--cpus", cpu.to_s]
        vb.customize ["modifyvm", :id, "--memory", memory.to_s]
      end

      conf.ssh.private_key_path = ["~/.vagrant.d/insecure_private_key", ssh_key]
      conf.ssh.insert_key = false
      conf.vm.provision "file", source: ssh_key + ".pub", destination: "~/.ssh/authorized_keys"

      if box_image == "ubuntu/xenial64"
        conf.vm.provision "shell" do |s|
          s.inline = "test -e /usr/bin/python || (apt-get -qqy update && apt-get install -qqy python-minimal)"
        end
      end

      if index == servers.size - 1
        if ansible_playbook != ""
          conf.vm.provision :ansible do |ansible|
            ansible.inventory_path = ansible_inventory_path
            ansible.verbose = "v"
            ansible.limit = "all"
            ansible.playbook = ansible_playbook
          end
        end
      end
    end
  end
end

As most of the file is identical to the example above, I will mention and explain just the differences.

Define ansible inventory path
Ansible inventory. The path supports nested directories or a single file

ansible_inventory_path = "inventory/hosts"
ansible_user = "vagrant"

Create custom ansible inventory
The inventory will hold servers details and groups per each server.
My main reason for the creation of custom inventory was the ability to hold an IP address that I provided within the servers list.
By default, vagrant set the “127.0.0.1” as the ansible_host variable.

The path of the inventory file checked. If the path does not exist, it will be created.
Created inventory will contain the following host details:

  • ansible_host
  • ansible_user (specified above)
  • ansible_ssh_private_key_file
  • group - “all”
  • a group that has been mentioned for the server

When using the dhcp allocation for the vm, vagrant-hostmanager plugin is required to provide the resolving of the vm hostname.

if File.dirname(ansible_inventory_path) != "."
  Dir.mkdir(File.dirname(ansible_inventory_path)) unless Dir.exist?(File.dirname(ansible_inventory_path))
end
File.open(ansible_inventory_path, 'w') do |f|
  servers.each do |cfg|
    if cfg[:ip] != "dhcp"
      f.write "#{cfg[:hostname]} ansible_host=#{cfg[:ip]} "
      f.write "ansible_user=#{ansible_user} ansible_ssh_private_key_file=#{ssh_key}\n"
    else
      f.write "#{cfg[:hostname]} ansible_user=#{ansible_user} ansible_ssh_private_key_file=#{ssh_key}\n"
    end
  end
  f.write "\n"
  f.write "[all]\n"
  servers.each do |cfg|
    f.write "#{cfg[:hostname]}\n"
  end
  f.write "\n"
  servers.each do |cfg|
    f.write "[#{cfg[:group]}]\n"
    f.write "#{cfg[:hostname]}\n"
    f.write "\n"
  end
end

Define custom created inventory path
The ansible.inventory_path sets the path to the created inventory file.

if index == servers.size - 1
  if ansible_playbook != ""
    conf.vm.provision :ansible do |ansible|
      ansible.inventory_path = ansible_inventory_path
      ansible.verbose = "v"
      ansible.limit = "all"
      ansible.playbook = ansible_playbook
    end
  end
end

Example 4

Contains the following configuration:

* Multiple servers creation

* Insert custom ssh public key to the vm

* Ansible provisioner

* Custom groups definition for Ansible

* Inventory generated by Vagrant.

* Custom Ansible inventory creation (including custom groups)

* Config yml based file shared between Vagrantfile and Ansible playbook. Holds variables used by both

Full Vagrantfile:

# -*- mode: ruby -*-
# vi: set ft=ruby :

ansible_playbook = "playbook.yml"
ansible_inventory_path = "inventory/hosts"
config_file = "vagrant.yml"

require 'yaml'
# https://blog.scottlowe.org/2016/01/14/improved-way-yaml-vagrant/
if !File.exists?(File.join(File.dirname(__FILE__), config_file))
  puts "The vagrant.yml file config is missing"
  abort
end
vars = YAML.load_file(File.join(File.dirname(__FILE__), config_file))

servers = vars['servers']
if !servers || servers.empty?
  puts "No servers defined in " + config_file
  abort
end

Vagrant.configure(2) do |config|
  if Vagrant.has_plugin?("vagrant-hostmanager")
    config.hostmanager.enabled = true
    config.hostmanager.manage_host = true
    config.hostmanager.manage_guest = true
    config.hostmanager.ignore_private_ip = false
    config.hostmanager.include_offline = false

    # Resolve dynamic ip address (dhcp) of the host and guests for the /etc/hosts file.
    # https://github.com/devopsgroup-io/vagrant-hostmanager/issues/86
    servers.each do |server|
      if server["ip"] == "dhcp"
        cached_addresses = {}
        config.hostmanager.ip_resolver = proc do |vm, resolving_vm|
          if cached_addresses[vm.name].nil?
            if hostname = (vm.ssh_info && vm.ssh_info[:host])
              vm.communicate.execute("hostname -I | cut -d ' ' -f 2") do |type, contents|
                cached_addresses[vm.name] = contents.split("\n").first[/(\d+\.\d+\.\d+\.\d+)/, 1]
              end
            end
          end
          cached_addresses[vm.name]
        end
      end
    end
  end

  if File.dirname(ansible_inventory_path) != "."
    Dir.mkdir(File.dirname(ansible_inventory_path)) unless Dir.exist?(File.dirname(ansible_inventory_path))
  end
  File.open(ansible_inventory_path, 'w') do |f|
    servers.each do |cfg|
      if cfg["ip"] != "dhcp"
        f.write "#{cfg["hostname"]} ansible_host=#{cfg["ip"]} "
        f.write "ansible_user=#{vars['ansible_user']} ansible_ssh_private_key_file=#{vars['ssh_key']}\n"
      else
        f.write "#{cfg["hostname"]} ansible_user=#{vars['ansible_user']} ansible_ssh_private_key_file=#{vars['ssh_key']}\n"
      end
    end
    f.write "\n"
    f.write "[all]\n"
    servers.each do |cfg|
      f.write "#{cfg["hostname"]}\n"
    end
    f.write "\n"
    servers.each do |cfg|
      f.write "[#{cfg["group"]}]\n"
      f.write "#{cfg["hostname"]}\n"
      f.write "\n"
    end
  end

  servers.each_with_index do |server, index|
    box_image = server["box"] ? server["box"] : vars['box'];
    config.vm.define server["hostname"] do |conf|
      conf.vm.box = box_image.to_s
      conf.vm.hostname = server["hostname"]

      net_config = {}
      if server["ip"] != "dhcp"
        net_config[:ip] = server["ip"]
        net_config[:netmask] = server["netmask"] || "255.255.255.0"
      else
        net_config[:type] = "dhcp"
      end
      conf.vm.network "private_network", net_config

      if !server["port_guest"].nil? && !server["port_host"].nil?
        conf.vm.network "forwarded_port", guest: server["port_guest"], host: server["port_host"]
      end

      if !server["folder_guest"].nil? && !server["folder_host"].nil?
        conf.vm.synced_folder server["folder_host"], server["folder_guest"]
      end

      cpu = server["cpu"] ? server["cpu"] : 1;
      memory = server["ram"] ? server["ram"] : 512;
      conf.vm.provider "virtualbox" do |vb|
        vb.customize ["modifyvm", :id, "--cpus", cpu.to_s]
        vb.customize ["modifyvm", :id, "--memory", memory.to_s]
      end

      conf.ssh.private_key_path = ["~/.vagrant.d/insecure_private_key", vars['ssh_key']]
      conf.ssh.insert_key = false
      conf.vm.provision "file", source: vars['ssh_key'] + ".pub", destination: "~/.ssh/authorized_keys"

      if box_image == "ubuntu/xenial64"
        conf.vm.provision "shell" do |s|
          s.inline = "test -e /usr/bin/python || (apt-get -qqy update && apt-get install -qqy python-minimal)"
        end
      end

      if index == servers.size - 1
        if ansible_playbook != ""
          conf.vm.provision :ansible do |ansible|
            ansible.inventory_path = ansible_inventory_path
            ansible.verbose = "v"
            ansible.limit = "all"
            ansible.playbook = ansible_playbook
          end
        end
      end
    end
  end
end

As most of the file is identical to the example above, I will mention and explain just the differences.

Full vagrant.yml

servers:
  - hostname: server1
    ip: 10.10.10.10
    ram: 1024
    cpu: 2
    group: servers
  - hostname: client1
    ip: 10.10.10.11
    box: "ubuntu/xenial64"
    group: clients
  - hostname: client2
    ip: 10.10.10.12
    group: clients
    port_guest: 80
    port_host: 8080
  - hostname: client3
    ip: dhcp
    group: clients
    folder_guest: "/srv/website"
    folder_host: "src/"

box: "centos/7"
ssh_key: "~/.ssh/id_rsa"

ansible_user: vagrant
new_file_content: "This is the new content of the file"
test_file: "/tmp/test_file"

This example is pretty different from the example above.
The variables provided to vagrant, including servers list, global and ansible playbook variables, provided in a separate yml file.

External config file
Configuration file shared between Vagrantfile and Ansible playbook defined as “config_file” variable. Yaml module imported and yml based config file is loaded. The list of servers details loaded in a “servers” variable. The “servers” variable tested as non-empty.

config_file = "vagrant.yml"

require 'yaml'
# https://blog.scottlowe.org/2016/01/14/improved-way-yaml-vagrant/
if !File.exists?(File.join(File.dirname(__FILE__), config_file))
  puts "The vagrant.yml file config is missing"
  abort
end
vars = YAML.load_file(File.join(File.dirname(__FILE__), config_file))

servers = vars['servers']
# Validate servers config in config_file
if !servers || servers.empty?
  puts "No servers defined in " + config_file
  abort
end

Refer to the servers details
When the details of the servers taken from the external file, the reference to the details are different.
Below is a small part of the differences between the previous and current references.

# Previous reference
if !server[:port_guest].nil? && !server[:port_host].nil?
  conf.vm.network "forwarded_port", guest: server[:port_guest], host: server[:port_host]
end

# Current reference
if !server["port_guest"].nil? && !server["port_host"].nil?
  conf.vm.network "forwarded_port", guest: server["port_guest"], host: server["port_host"]
end

In previous reference, the “port_guest” mentioned in the following type: server[:port_guest].
Current reference, mention the value in another way: server["port_guest"].

The vagrant.yml file provided above has a simple yml file structure.

Tags:

Updated: