Getting Started with Saltstack – Part 2

In the first part of this tutorial, we set up Salt on a simple setup and played around with some basic commands from the command line. In this part of the tutorial we will go ahead and write a complete module for installing and configure a web server. This will also introduce us to some of Salt’s terminology and working in detail. By end of this tutorial we will have installed apache2 displaying a custom page with info specific to that node. We will also setup apache such that it restarts by itself if it goes down at all. From salt’s perspective, we will write a short formula, utilize values from Pillar and Grain and use the beacon to watch service and send an event that will be caught by reactor to restart the service. But don’t get scared – we have a working setup and you can always play around.  Do check out the project from link so that you can follow along.
Before we build out the code for formula (Which is equivalent to playbook in Ansible, Cookbook in Chef or Module in Puppet) – let’s understand how salt finds out what it needs to execute.
The only difference in diagram and our case is: to enable any changes we make locally to be visible in Salt vagrant machine immediately – I have added /vagrant/salt/srv/salt and /vagrant/salt/srv/pillar  directories to the roots. Since /vagrant is the directory from host – any changes we make in editor will be made directly to Vagrant machine. Going forward we will refer as salt_root & pillar_root to refer to formula & pillar base directories respectively.
In salt_root create a directory named apache and inside it init.sls looks like:
    - name: apache2

run apache:
    - name: apache2
The above is super simple! There are two blocks – one is installing apache and other is making sure it is running. Let’s look at first block: “install-apache” is just a title and can be anything but has to be unique in a formula. Second line calls a module named “pkg” and expects the state to be “installed”.  The name attribute provided to state tells what needs to be installed. There are lot more attributes that we can pass to installed state and lot more states available in pkg module as documented here. As a Salt engineer you will need to refer to documentation quite often to write appropriate states. Now add in salt_root directory a file top.sls with following contents.
    - apache
 We are telling salt to include apache formula for all nodes which have a grain roles with value as apache in base environment. Similarly you could have MySQL installed only on certain roles/sites etc. For things which need to be installed on all nodes, we could use ‘*’ to target all nodes. But for above top.sls file to work – we will have to add role apache to our nodes where we want apache installed. We can go and edit /etc/salt/grains on all nodes and restart minions or we could again use command line to achieve the same. Let’s use command line:
sudo salt '*' grains.append role apache
 Let’s look at how that reflects in grains file:
$ sudo salt '*' 'cat /etc/salt/grains'
    - apache
Now if we restart our minions and run highstate – it will execute all modules which match criteria on given nodes:
$ sudo salt '*' state.highstate
          ID: install-apache
    Function: pkg.installed
        Name: apache2
      Result: True
     Comment: Package apache2 is already installed
     Started: 12:57:15.358265
    Duration: 423.457 ms
          ID: run apache
    Function: service.running
        Name: apache2
      Result: True
     Comment: The service apache2 is already running
     Started: 12:57:15.786915
    Duration: 70.892 ms

Summary for
Succeeded: 2
Failed:    0
Total states run:     2
Total run time: 494.349 ms
Let’s get ip_addrs of our nodes and then test in browser by hitting at port 80 to check if apache was indeed installed. You should see default apache info page.
$ sudo salt '*' network.ip_addrs
Alright but default apache is no good for us, neither it is any fun. Let’s go ahead and customize it for our top secret company we are going to launch some day. Let’s add following block to $salt_root/apache/init.sls:
    - source: salt://{{ sls }}/files/index.html.jinja
    - template: jinja
    - user: root
    - group: root
    - mode: 644
    - watch_in:
        - service: apache2
What’s going on here? So we are managing a file through Salt – this file is the one which Apache was displaying earlier – but now we are modifying it to show content we intend to show. For that we using a file in files directory as source and using templating engine called Jinja. Now let’s look at content of the template (Most of HTML part truncated – check complete file in source code):
{% set machine_id = salt['grains.get']('id','ERROR: ID_NOT_FOUND') %}
{% set greeting = salt['pillar.get']('apache:greetings','ERROR: GREETING_NOT_FOUND') %}
{{ greeting }}
Machine ID is: {{ machine_id }}
All we are doing is embedding some dynamic content in between some static content. We got the machine ID by querying grain and got the greeting message from Pillar. There is a lot more you can do with Jinja and Salt and you should check out documentation here. Finally for this to work, we will have to add following in $pillar_root/apache.sls:
  greetings: Hello from SaltStack generated greeting, you rockstar Salt engineer! And this in $pillar_root/top.sls:
And following in $pillar_root/top.sls file:
    - apache
And now it’s time to see some fun. Let’s first refresh Pillar cache so that latest file changed we made are picked up and then run highstate to deliver the file to target server:
$sudo salt '*' stateutil.refresh_pillar
$sudo salt '1*' state.highstate


          ID: /var/www/html/index.html
    Function: file.managed
      Result: True
     Comment: File /var/www/html/index.html is in the correct state
     Started: 05:40:02.291231
    Duration: 23.73 ms


Summary for
Succeeded: 3
Failed:    0
 Now if you hit browser at port 80 on the machine on which highstate ran successfully – you should see a webpage with our custom page. Now let’s go to our last part – using beacon and reactor to restart apache if it goes down, here is overall scheme of things:
First thing is to configure beacon to watch apache2 service. We will configure this through pillar (And there are other ways like configuring in minion config file), add a file $pillar_root/beacon.sls with code:
      onchangeonly: True
And add entry of beacon in $pillar_root/top.sls (Like we did in case of apache). Before we go to reactor – let’s check if beacon is behaving the way it should. Refresh the pillar on all minions and then in one prompt open salt master & minion in other. On salt master we will fire a command to watch event queue and then stop apache. Ideally this should be seen as an event in salt master terminal:
$ sudo salt-run state.event pretty=True

salt/beacon/	{
    "_stamp": "2016-04-28T06:33:05.244322", 
    "data": {
        "apache2": {
            "running": false
        "id": ""
    "tag": "salt/beacon/"
This confirms that beacon is reporting the change in state of apache service. Now let’s configure a event watcher, this file will reside in /etc/salt/master.d and you will need to uncomment a setting ‘default_include’ in master config, I have done this in code attached if you want to check out.
  - 'salt/beacon/*/service/':
    - /vagrant/salt/srv/salt/apache/apache_down.sls
And the file apache_down.sls will look like:
{% if data['data']['apache2']['running'] == false %}
    - tgt: "{{ data['data']['id'] }}"
    - arg:
      - sudo service apache2 start
{% endif %}
In above snippet – we are digging in data of event and checking if running state is false – and if that is the case then issue command only on that minion to restart apache. The ‘tgt’ attribute is the one which says target only minion from which this event was received. Having built this – it’s time to see it in action. Restart the salt master – so that reactor config is picked up. In one terminal open salt master and start looking at event queue and in another go ahead and stop apache2 service.
salt/job/20160428071412092896/ret/	{
    "_stamp": "2016-04-28T07:14:15.012791", 
    "cmd": "_return", 
    "fun": "", 
    "fun_args": [
        "sudo service apache2 start"
    "id": "", 
    "jid": "20160428071412092896", 
    "retcode": 0, 
    "return": " * Starting web server apache2nAH00558: apache2: Could not reliably determine the server's fully qualified domain name, using Set the 'ServerName' directive globally to suppress this messagen *", 
    "success": true
salt/beacon/	{
    "_stamp": "2016-04-28T07:14:15.027568", 
    "data": {
        "apache2": {
            "running": true
        "id": ""
    "tag": "salt/beacon/"
You will notice that in addition to earlier event from beacon, you will see reactor reacting to event – and it is event from master to minion signified by  ‘salt/job/20160428071412092896/ret’ and once apache comes up – beacon again reporting the event but now “running” is true.
Just to recap: We wrote a first formula which managed Apache with our custom homepage during which we touched upon jinja, grains & pillars. We then built a mini demo of how beacon and reactor works together. If you have any questions or bugs to report feel free to comment/raise issue on Github.
Vishal Biyani

Author Vishal Biyani

More posts by Vishal Biyani

Leave a Reply