Multi Machine Setup With Vagrant And Saltstack
Vagrant is a great way for developing and testing your configurations before deploying to real hosts. In combination with the Saltstack provisioner, you can run your salt states on virtual machines and achieve really fast iteration cycles.
While the Vagrant docs give a great example for masterless quickstart with Vagrant and Saltstack, they do not elaborate on multi-machine configurations, like you are likely to encounter in bigger setups.
In this post I will present you my two-machine Vagrantfile, which can be used as a starting point for your individual setup. My example configures a master host running salt-master and one minion, which will be provisioned for running Docker.
tl;dr
You can find the repository for this example on Github.
First, clone the repository:
$ git clone https://github.com/mbrgm/vagrant-saltstack-example.git
You need the vagrant-hostmanager and vagrant-cachier plugins, so install them:
$ vagrant plugin install vagrant-hostmanager vagrant-cachier
Then, bring the vagrant hosts up from inside your working copy:
$ cd vagrant-saltstack-example
$ vagrant up
You can now ssh into the master and run the salt highstate:
$ vagrant ssh master1 -c "sudo salt '*' state.highstate"
For simplicity, I also added a target running that command in a Makefile:
$ make update
Detailed description
This section explains the Vagrantfile in greater detail. First, I’ll describe the individual sections. You can find the complete file at the end of the post.
#===================
# Package cache
#===================
if Vagrant.has_plugin?("vagrant-cachier")
config.cache.scope = :box
end
I strongly recommend you use the vagrant-cachier plugin for your Vagrantfiles. It shares a common package cache among similar VM instances and has support for multiple package managers and Linux distros. We set the scope to the ‘box’ level, which means that machines running from the same base box share a common cache.
#================
# Networking
#================
config.vm.network :private_network, type: "dhcp"
config.hostmanager.enabled = true
config.hostmanager.ignore_private_ip = false
config.hostmanager.ip_resolver = proc do |vm, resolving_vm|
if vm.id
`VBoxManage guestproperty get #{vm.id} "/VirtualBox/GuestInfo/Net/1/V4/IP"`.split()[1]
end
end
config.vm.provision :hostmanager
We configure our machines to use an additional private network for communication. Getting IPs via DHCP saves us the trouble of configuring them manually for each machine. Then we enable the hostmanager plugin. The hostmanager manages the /etc/hosts file of each machine, so that machines can lookup each other’s IP using the Vagrant hostnames and optional aliases. This enables us to specify a ‘salt’ alias for the salt-master host, so the minions can connect to the master without any additional configuration. The custom IP resolver is a workaround to make hostmanager work with DHCP addresses. Once smdahlen/vagrant-hostmanager#86 is resolved, this should be obsolete. In the last line, we tell vagrant to use the hostmanager provisioner.
#==============
# Machines
#==============
# Set array of hostnames
minions = [ 'minion1' ]
#-----------------
# Salt master
#-----------------
config.vm.define "master1" do |node|
node.vm.box = "ubuntu-14.10-server-64"
node.vm.hostname = "master1"
node.hostmanager.aliases = %w(salt)
node.vm.synced_folder "./salt/roots/salt", "/srv/salt"
node.vm.provision :salt do |salt|
salt.install_master = true
master_keys_hash = { "master1" => "./salt/keys/master1/minion.pub" }
minion_keys_hash = Hash[minions.map{
|hostname| [hostname, "./salt/keys/#{hostname}/minion.pub"]
}]
salt.seed_master = master_keys_hash.merge(minion_keys_hash)
salt.minion_key = "./salt/keys/master1/minion.pem"
salt.minion_pub = "./salt/keys/master1/minion.pub"
end
end
In order to easily support multiple minions, we define a list of hostnames,
which will later be mapped to according minion configurations. Next, we define
our salt master. As I wrote before, the hostmanager plugin can add additional
aliases for a host. That way minions will be able to reach the salt-master
under the ‘salt’ hostname. The salt root directory is shared using a synced
folder. If you want an additional pillar root, you must add another synced
folder. salt.install_master = true
tells the salt provisioner that this
machine is going to be a a salt-master (the salt-minion is installed by
default).
The Vagrant Saltstack provisioner offers a way for preseeding the master keys,
so you don’t have to ssh into the master and accept them manually. The hostname
=> pubkey map is created dynamically using the list of minion hostnames. I
included keys for one master and one minion in the example repo. If you
want to create your own keys or increase the number of minions, you can run
make genkeys
from the repository root (provided you have openssl
installed). This task takes the configured machines’ hostnames and creates
a public/private key pair for each one.
In order to make the preseeded keys work, we also have to configure the minions
to use the generated keys via the salt.minion_key
and salt.minion_pub
options. As the master host also runs a salt-minion daemon, we have to tell it
the location of its keypair as well.
#------------------
# Salt minions
#------------------
# Create a machine for each hostname
minions.each do |hostname|
config.vm.define hostname do |node|
node.vm.box = "ubuntu-14.10-server-64"
node.vm.hostname = hostname
node.vm.provision :salt do |salt|
salt.minion_key = "./salt/keys/#{hostname}/minion.pem"
salt.minion_pub = "./salt/keys/#{hostname}/minion.pub"
end
end
end
The last section of the Vagrantfile creates a machine running salt-minion for
every hostname in the minions
list. There is nothing special about the minion
configuration. As with the master, the minions must use the generated keys for
the preseeding to work.
You can now bring the configured machines up and start your salt development:
$ vagrant up
# ... wait for machines to be provisioned
$ vagrant ssh master1 -c "sudo salt '*' test.ping"
master1:
True
minion1:
True
Connection to 127.0.0.1 closed.
The salt state tree in the example repo configures minion1 to be a docker host.
I also included a make task for quickly running state.highstate
during
development, so a simple
$ make update
should get your docker host up and running. Note that no states have been specified for the master1 minion, so salt will give you a warning about that.
And, at last, the complete Vagrantfile. Have fun!
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
#===================
# Package cache
#===================
if Vagrant.has_plugin?("vagrant-cachier")
config.cache.scope = :box
end
#================
# Networking
#================
config.vm.network :private_network, type: "dhcp"
config.hostmanager.enabled = true
config.hostmanager.ignore_private_ip = false
config.hostmanager.ip_resolver = proc do |vm, resolving_vm|
if vm.id
`VBoxManage guestproperty get #{vm.id} "/VirtualBox/GuestInfo/Net/1/V4/IP"`.split()[1]
end
end
config.vm.provision :hostmanager
#==============
# Machines
#==============
# Set array of hostnames
minions = [ 'minion1' ]
#-----------------
# Salt master
#-----------------
config.vm.define "master1" do |node|
node.vm.box = "ubuntu-14.10-server-64"
node.vm.hostname = "master1"
node.hostmanager.aliases = %w(salt)
node.vm.synced_folder "./salt/roots/salt", "/srv/salt"
node.vm.provision :salt do |salt|
salt.install_master = true
master_keys_hash = { "master1" => "./salt/keys/master1/minion.pub" }
minion_keys_hash = Hash[minions.map{
|hostname| [hostname, "./salt/keys/#{hostname}/minion.pub"]
}]
salt.seed_master = master_keys_hash.merge(minion_keys_hash)
salt.minion_key = "./salt/keys/master1/minion.pem"
salt.minion_pub = "./salt/keys/master1/minion.pub"
end
end
#------------------
# Salt minions
#------------------
# Create a machine for each hostname
minions.each do |hostname|
config.vm.define hostname do |node|
node.vm.box = "ubuntu-14.10-server-64"
node.vm.hostname = hostname
node.vm.provision :salt do |salt|
salt.minion_key = "./salt/keys/#{hostname}/minion.pem"
salt.minion_pub = "./salt/keys/#{hostname}/minion.pub"
end
end
end
end