Vagrant Provisioning with SaltStack

Provisioning Virtual System using Masterless Salt Stack

This tutorial covers provisioning systems with a Vagrant provisioner for Salt Stack. This will demonstrate how to create a local development environment for crafting your own Salt Stack solutions.

About Salt Stack

Salt Stack is a distributed remote execution platform with change configuration (desired state). Salt Stack scripts are called states and can be grouped into a component structure called a formula. A formula can contain several states, local variables, files, and templates. In addition to states and formulas, Salt Stack uses pillars to store variables and arbitrary data and secrets.

Activities within the Salt Stack system are coordinated with Salt Master server(s) and a message queuing control bus, with minions installed onto the clients.

This organizational structure allows for not only high performance scenarios, but for managing cloud based infrastructure as well as services that exist as clusters, like Elastic Search, Apache Kafka, Apache Spark, Apache Storm, or Kubernetes.



This tutorial requires these components:

Previous Articles on Virtualbox

I have published some previous guides on installing Vagrant and Virtualbox on macOS, Windows, and Linux operating systems.

Windows 8.1

Using Chocolatey to install the requirements:

macOS High Sierra 10.3

Using Homebrew to install the requirements:

Fedora 28

Using native package manager Dandified YUM, or in short DNF, to install requirements:

Part I: Salt Formula with Intelligent Defaults

We’ll start this tutorial with a Salt Stack Formula called hello_web that will install an Apache web server and copy over HTML content on Ubuntu system.

Vagrant will call Salt Stack to run this formula using masterless mode.

Staging Area Structure

We need to lay down some roots…

Using Bash shell, we can create our overall staging area and structure for this tutorial.

mkdir -p ~/vagrant-salt/roots/{pillar,salt/hello_web/files}
cd ~/vagrant-salt
touch Vagrantfile roots/salt/top.sls \
roots/pillar/{top.sls,hello_web.sls} \
cat <<-'HTML' > roots/salt/hello_web/files/index.html
<h1>Hello World!</h1>

The resulting structure will look like this, with our formula under the salt directory.

├── Vagrantfile
└── roots
├── pillar
│ ├── hello_web.sls
│ └── top.sls
└── salt
├── hello_web
│ ├── defaults.yaml
│ ├── files
│ │ └── index.html
│ ├── init.sls
│ └── map.jinja
└── top.sls

Vagrant Configuraiton

Update the Vagrantfile to have this content, using the Ubuntu box from the Bento project:

Vagrant.configure('2') do |config| = 'bento/ubuntu-16.04' 'forwarded_port', guest: 80, host: 8085
####### File Share #######
config.vm.synced_folder './roots/salt/', '/srv/salt'
config.vm.synced_folder './roots/pillar', '/srv/pillar'
####### Provision #######
config.vm.provision :salt do |salt|
salt.masterless = true
salt.run_highstate = true
salt.verbose = true

We can see provisioner doesn’t do much of anything, except use the default behavior, which are looking for the top.sls in /srv/salt and /srv/pillar. These are mounted from out local roots directory.

Create Top Salt State

In the top file, we can organize and group several systems, and then specify what to do on them, such as formulas to run. For our one machine scenario, we’re going to run the formula hello_web on all systems.

Update the state/top.sls to mach the following:

- hello_web

Create Top Pillar

Similar to the top salt state, we want to configure variables and data for our systems, which is all systems again, all systems being our one guest Ubuntu system.

Update the pillar/top.sls to match the following:

- hello_web

Formula Defaults

Now we can dive into developing our formula. First let’s create intelligent default variables that make sense for an Ubuntu system.

In the hello_web formula, update the defaults.yaml with the following:

docroot: /var/www/html
package: apache2
service: apache2

Formula Map

Now we can create a map that glues in all of our variables that we would like to use for this formula. The map is essentially a Jinja template.

In this map, we want to we will import our defaults, optionally specify our own local variables, and then merge in variables from pillar.

In the hello_web formula, update the map.jinja with the following:

{% import_yaml 'hello_web/defaults.yaml' as defaults %}
{% set hello_web = salt['pillar.get'](
merge=True) or defaults.get('hello_web')

Update: I added or defaults.get('hello_web') for the off chance you add an empty hash of hello_web to roots/pillar/hello_web.sls. This would cause Salt Stack to ERROR because is cannot merge None with default hash.

Formula Salt State

The main course has arrived with our Salt State file. In the hello_web formula, update init.sls with the following:

{% from "hello_web/map.jinja" import hello_web with context %}hello_web:
- name: {{ hello_web.package }}
- name: {{ hello_web.service }}
- enable: True
- reload: True
- name: {{ hello_web.docroot }}/index.html
- source: salt://hello_web/files/index.html

Salt State Code Explanation

This needs some explanation…

The first thing here to to make a bold declaration!!!

Er, um, rather, I mean an identity declaration that I like to use to group all of the states together under hello_web, but serves a functional purpose to declare a high state component within Salt Stack.

Under this identity declaration, each of the following members will have will be a state module (pkg, service, file) followed by a corresponding state function (installed, running, managed). Under these state module and state function keys, we supply a list of parameters that will be passed to the function, such as a name parameter that makes sense to the state, like a package name, a service name, and a file name.

Salt States can use Jinja, reference variables, whether they are local default variables of the formula, or variables set in a pillar.

Testing the Results

Now let’s bring up our virtual guest, provision the system, and then use lovable curl command on the host:

vagrant up
curl -i

This should result in the following:

HTTP/1.1 200 OK
Date: Sat, 11 Aug 2018 10:48:33 GMT
Server: Apache/2.4.18 (Ubuntu)
Last-Modified: Sat, 11 Aug 2018 10:47:41 GMT
ETag: "3c-5732696eef3bc"
Accept-Ranges: bytes
Content-Length: 60
Content-Type: text/html
<h1>Hello World!</h1>

Part II: Salt Formula and a New Pillar

Let’s shift gears and use the delicious Linux flavor of CentOS from the Bento folks.

As the default variables in the hello_web formula will not work well for with CentOS, we’ll need to specify different variables using pillar. The hello_world formula will scoop these pillar variables up and use them instead of the default variables.

Update Vagrantfile

Update the Vagrantfile with the following:

Vagrant.configure("2") do |config| = "bento/centos-7.5" "forwarded_port", guest: 80, host: 8083
####### File Share #######
config.vm.synced_folder "./roots/salt/", "/srv/salt"
config.vm.synced_folder "./roots/pillar", "/srv/pillar"
####### Provision #######
config.vm.provision :salt do |salt|
salt.masterless = true
salt.run_highstate = true
salt.verbose = true
salt.pillar "hello_web" => {
"package" => "httpd",
"service" => "httpd",
"docroot" => "/var/www/html"

We can add new pillar values with the salt provisioner by calling the pillar method and passing in a single ruby hash. Vagrant converts this ruby hash to a Python dict format and passes it on the command line to salt-call state.highstate in our case. For more information, see Vagrant docs on Pillar Data.

Side note: For the those uninitiated to Ruby, when passing anonymous hashes, you can remove the outer curly braces {}, and when using a parameter list, you can omit the outer parenthesis (). Thus:

salt.pillar({"hello_web" => { … }}) # pass anonymous hash
salt.pillar( "hello_web" => { … } ) # omit outer hash braces
salt.pillar "hello_web" => { … } # omit parameter parenthesis

Trying Out New Results

First destroy our existing virtual guest running the previous distro and bring this new one up with CentOS, and then test again with curl:

vagrant destroy --force
vagrant up
curl -i

This will give us the following:

HTTP/1.1 200 OK
Date: Sat, 11 Aug 2018 11:01:25 GMT
Server: Apache/2.4.6 (CentOS)
Last-Modified: Sat, 11 Aug 2018 11:01:08 GMT
ETag: "3c-57326c70410e0"
Accept-Ranges: bytes
Content-Length: 60
Content-Type: text/html; charset=UTF-8
<h1>Hello World!</h1>

Final Thoughts

Well, not stating the obvious, but this with Vagrant you can achieve a new pillar of success or high state of achievement by sprinkling a little salt across your systems.

You can quickly develop, test, and learn Salt Stack environment in a controlled and repeatable way across a variety of distributions. I hope this tutorial has helped begin that journey.

Linux NinjaPants Automation Engineering Mutant — exploring DevOps, o11y, k8s, progressive deployment (ci/cd), cloud native infra, infra as code