Salt DevKit: Developing Formulas

Salt Stack offers modularity where you can package up your Salt Stack code into a single component called a Salt formula. You store on the file system along with other Salt code, or publish these in a git repository.

In previous article, I documented how to use external modules, such as the nginx-formula, and now in this article, I show how to craft your own formulas. We’ll demonstrate how to install Docker and run a nginx container.

We will facilitate this by using a local Salt DevKit by using Vagrant with the Vagrant Salt Provisioner.

Previous Article

How to use external formulas:

Tool Prerequisites

These are the tools used in this guide:

  • Vagrant is virtual machine automation solution setups up local development environments.
  • VirtualBox (recommended) is a cross-platform (Windows, Linux, mac OS) that allows you to run virtual machines. Another virtual machine system can be used, such as Hyper-V for Windows 10 Pro or KVM (libvrt) on Linux. This guide uses VirtualBox as it is open source and cross platform.
  • Bash Shell (recommended) is not strictly required, but the commands in this tutorial are tested and oriented toward bash. [Note: this is default on mac OS and popular Linux distros, or msys2 with Windows].

The Guide

In this guide, we start by creating the docker formula, then states and pillars that use the docker formula, and finally the Vagrantfile configuration that will show how to provision the system using all of this.

Step 1: Create Project Area

Create the directory and file structure that looks like following:

├── Vagrantfile
└── roots
├── pillar
│ ├── docker.sls
│ └── top.sls
└── salt
├── docker
│ ├── default.yaml
│ ├── init.sls
│ └── map.jinja
├── docker_nginx.sls
└── top.sls

In Bash, you can quickly create this by running the following:

# create directory structure
-p ~/salt-devkit-dev-formula/roots/{pillar,salt/docker}
cd ~/salt-devkit-dev-formula
# create files
roots/salt/{top.sls,docker_nginx.sls} \
roots/salt/docker/{default.yaml,init.sls,map.jinja} \
roots/pillar/{top.sls,docker.sls} \

Step 2: Create Docker Formula

A typical Salt formula has an main entry point (init.sls), default values (default.yaml), and a map.jinja to merge variables between pillars and local defaults, should values in the pillar not be set.

We can start by adding the following to salt/docker/init.sls:

docker/init.sls (Part 1)

This is a very generic way to install package dependencies, add a remote Debian repository, and then install the desired package from the remote repository.

To reference these values, we need to set them somewhere locally. Edit the docker/default.yaml, and add the following:


You noticed that the top of the salt/docker/init.sls, we had this line:

{% from "docker/map.jinja" import docker with context %}

This references our local map that will merge pillar and default data together into a single structure. Edit the salt/docker/map.jinja with the following content:


Note: In the above map, we create a dict referenced by the key docker. This merges both default.yaml and pillar, and should the user neglect to setup docker: in a pillar somewhere, this map will then use the defaults.yaml only.

So far, we showed how to install the docker environment on the target guest system. For convenience, we may want to add developers to the docker group, so that they can access docker. For demonstration purposes, we’ll add the vagrant user, so that it can access the docker on the guest system. Append the following code to salt/docker/init.sls.

docker/init.sls (Part 2)

Now that Docker is installed, we need to install the docker python module, so that Salt can actually manage docker wherever docker is installed. We can do that by appending the following to salt/docker/init.sls:

docker/init.sls (Part 3)

Note: Both python and python3 versions of pip are installed, so that either will be available regardless of the Python version Salt commands use.

Step 3: Create Salt State

Let’s create a state that will use docker. Edit docker_nginx.sls file and add the following content:


This will run a container using some settings up we set locally. This is another way to set values locally.

Note: This state will only work if docker engine along with python module docker are installed on the target system previously.

We can create a top state that will use both of these:


If we run this in highstate, it will fail with a Traceback on its first run, because despite installing docker engine along with the docker python module, the python module will not be in memory, so will need to run again.

Normally, you could reload the python modules with reload_modules: true when installing a python module that you need, but this doesn’t seem to be working anymore:

Step 4: Create Salt Pillars

Now comes time to setup the values we want to use for our project. First edit the top pillar pillar/top.sls that references pillars for use with our docker formula:


Now we should define our values for docker. Edit pillar/docker.sls pillar:


Normally, in pure function mindset, a modular component like a formula should not be dependent global states outside its function. In a previous step, when we created a map.jinja that should not depend setting any values external to the formula itself.

However, that being said, many formulas out there will fall flat and give feedback with a Traceback when pillar values are not set.

So for that above reason, when using a formula, it is good practice in Salt, to, at minimum, setup required key(s) without any value fields. This will create an empty dict structure when the YAML is converted into Python.

Step 5: Create Vagrant Configuration

Add the following content to the Vagrantfile configuration:


In this configuration, we want to do provisioning in two phases:

  1. Install Docker CE and Python docker module.
  2. Run nginx docker container

I posed some questions and answers for this approach:

Why use two steps instead of just one step?

Earlier, we noted that a newly installed docker python module is not available in Salt’s memory space, and so we need to run it again for all states that require this module. This is the reason for two steps.

Why must we use Shell provisioner?

Simply, the salt provisioner can only run everything in highstate (roots/salt/top.sls) when using masterless.

Therefore, we have to use the shell provisioner to run a single state in two separate commands, one for the docker formula and one for the docker_nginx state.

Why then use Salt provisioner at all?

The salt provisioner is used only for downloading and installing the Salt minion on this system. This needs to happen before running salt commands with the shell provisioner.

Step 6: Provision and Test the Docker Formula

The whole Ubuntu 18.04 guest environment (using VirtualBox) can be brought up by typing the following:

vagrant up --no-provision

If you are doing this the first time, it may take some time to download the Ubuntu 18.04 box image, and then create and run the virtual machine guest.

Once completed, you can provision the system using docker formula and docker_nginx state to demonstrate docker is working.

vagrant provision

After, you can test by pointing your web browser to http://localhost:8081 or if you have curl, you can run:

curl localhost:8081

Step 7: Clean up

You can halt and destroy the virtual guest by the following command:

vagrant destroy

I recommend doing this if the virtual machine guest instance is no longer needed, as this will free a few gigabytes of disk space.

Testing Solution with Python 3

Salt Stack installation process (salt bootstrap) defaults to Python2. Unfortunately, Python2 is officially deprecated as of 2020.

On a clean system (one that hasn’t been provisioned already), you can create a fresh Ubuntu guest system, bootstrap it with Salt using Python3, and then provision with Salt using Python3, by typing the following:

PYTHON=python3 vagrant up

If the system is already up, but not provisioned yet (and no Salt is yet installed), then you can just do:

PYTHON=python3 vagrant provision

Note: In the Vagrantfile configuration (see Step 6 above), I added capability to use either Python3 or Python2, by specifying the environment variable PYTHON.


Here are some useful articles I have come across that might be useful in your journey and related to the topics covered above.


Here’s information about installing Docker Engine as well as the nginx image (and source).

Vagrant Ubuntu 18.04 box

Vagrant community calls system images boxes (or boxen). This guide uses a bento box, which is a kit for building vagrant boxes in a variety of distros for the purpose of testing change configuration tools.


Information about provisioners in case you need options not demonstrated in this guide:

Salt States

General information about Salt States.

Salt Pillars

General information about Salt Pillars.

Salt Formulas

General information about Salt Formulas.

Salt References

Some Salt states configured in this guide.


This concludes the guide on how to quickly develop formulas for use with your Salt Stack environment. Vagrant as a tool from Hashicorp is useful to configuring, developing, and testing Salt formulas before they are integrated into production.



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store