SaltStack
Ruby Quick Start
Skype: volkov.gl
Live example at GitHub
What is SaltStack?
SaltStack is a provisioning tool.
It manages one or more nodes, applying and maintaining configuration.
Anything like this?
- Chef
- Puppet
- Ansible
- and much more
Several tools providing configuration orchestrating one way or another:
Also check Docker, Vagrant and Otto if you are looking for something special.
Why SaltStack?
- It is easy to start with.
- Less time to start with in comparison with Docker and other containers.
- Great documentation and examples
How it works?

Or we may go masterless and keep configuration on the target server.
Of course the real power, nifties and whistles lays in the world of master-minions setup. One keystroke — and couple of servers configured.
Single server masterless setup is still great for individual developers and small teams when projects only needs a single server.
In the next slides we would target the second option (most configuration is common though).
When to use SaltStack?
Any time. Really.
- Your server gone and you need to setup it again? One keystroke — bam, it is ready!
- You have a production server, and want to setup staging just like that one? Here we go.
- You need to show something, and need to spawn another typical server? Salt at your service.
- The project grew up and should be split into several nodes with different roles? Just specify them and you are set up!
Building Bricks
Commands
If the Salt Master is present it may execute commands across all the minions:
salt '*' cmd.run 'ls -l /etc'
salt -E 'storage.*' disk.usage
salt '*' pkg.install cowsay
salt -G 'os:Ubuntu' network.interfaces
States
States are the main configuration building material. They specifies the state of the target server.
And remember: not all states are equally good for your server.
States: Declarations
Most states declares the desired final state of your target server: installed packages, managed files or granted DB permissions.
These states would only be executed if required.
install vim:
pkg.installed:
- name: vim
https://github.com/myapp/hello-world:
git.latest:
- rev: develop
- target: /var/www/epic_app
States: Executables
Executable states would run every time configuration applied, except you explicitly specify run conditions.
These states has prefix salt.module.*
restart vsftpd:
module.run:
- name: service.restart
- m_name: vsftpd
# m_name gets passed to the execution module as "name"
Pillar
Pillar is basically a set of vocabularies containing configuration variables. It provides recipes reusability.
While states are freely distributed to any authorized client, pillar data is secure and restricted to the target nodes only.
ftpusername: me
ftppassword: oxfm4@8t5gglu^h^&
Testing States Application
If you want to check which changes would be made prior to new configuration application, you should specify test=True flag.
sudo salt 'minion1' state.apply examples test=True
States
United States of SaltStack
Searching the Right One

Executable (.modules) — ignore this
Configurable (.states) — YES!
Inside the Doc

Basic usage
DevOps mode: ON
State Composition
frank: # State ID, Unique, default for name if name is absent
mysql_user.present: # Namespace.State
- host: localhost # Named Params
- password: "bob@cat"
- connection_user: someuser
- connection_pass: somepass
- connection_charset: utf8
- saltenv:
- LC_ALL: "en_US.utf8"
States are YAML entities, described by
- unique ID
- Namespace.StateName
- State parameters
States With Logic
States utilizes template engine, by default they use Jinja.
Be aware of spacing!
# Raw state file
moe:
user.present
larry:
user.present
curly:
user.present
# Template driven state file
{% for usr in ['moe','larry','curly'] %}
{{ usr }}:
user.present
{% endfor %}
Describing a Node
Let the show begin!
Organizing Files
To start with you need a SaltStack installed in Masterless mode.
By default Salt checks /srv/salt for a file top.sls . This is a root state file, something like an entry point for application.
# /srv/salt/top.sls
base:
'*':
- webserver # state files to apply
# /srv/salt/webserver.sls
apache: # ID declaration
pkg: # state declaration
- installed # function declaration
# bash
$ salt-call --local state.highstate
Getting Real
When Hello World just isn't enough.
Here is how structure may look like when you need to setup three Ruby app servers from scratch.

Composing Setup
Top.sls again
Our entry point, which defines state files to apply.
base:
'*':
- user
- swap
- webserver
- misc
- ruby_server
- mysql_server
- ssh
- puma
Create deploy user
deploy:
group.present: []
user.present:
- gid: deploy
- home: /home/deploy
- groups:
- sudo
- deploy
Creating Swap
Cmd is a module. Cmd.run would be executed each time states applied — unless we add a condition.
Conditions are important: even if a module would change nothing it would produce "changed" entry in the log.
/swapfile:
cmd.run:
- name: "fallocate -l 1024M /swapfile && chmod 600 /swapfile && mkswap /swapfile"
- unless: ls -lh /swapfile # condition to check before execution
mount.swap:
- require: # require statements allow to control SaltStack execution flow
- cmd: /swapfile
Nginx
nginx:
pkg:
- installed
service.running:
- watch:
- pkg: nginx
- file: /etc/nginx/sites-available/exampleone
/srv/www:
file.directory:
- user: deploy
- group: deploy
- mode: 755
- makedirs: True
/srv/www/exampleone.com/shared/config/secrets.yml:
file.managed: # setup managed file
- source: salt://files/srv/exampleone.secrets.yml
- user: deploy
- group: deploy
- mode: 644
- makedirs: True
/etc/nginx/sites-available/exampleone:
file.managed:
- source: salt://files/nginx/exampleone
- user: root
- group: root
- mode: 640
/etc/nginx/sites-enabled/exampleone:
file.symlink:
- target: /etc/nginx/sites-available/exampleone
- require:
- file: /etc/nginx/sites-available/exampleone
/etc/nginx/sites-available/exampletwo:
file.managed:
- source: salt://files/nginx/exampletwo
- user: root
- group: root
- mode: 640
/etc/nginx/sites-enabled/exampletwo:
file.symlink:
- target: /etc/nginx/sites-available/exampletwo
- require:
- file: /etc/nginx/sites-available/exampletwo
/etc/nginx/sites-available/examplethree:
file.managed:
# ...
NodeJS
NodeJS repo:
cmd.wait:
- name: curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
- watch:
- pkg: nodejs
nodejs:
pkg.installed
Cmd.wait is another useful executable with built-in condition check. It wouldn't run unless NodeJS package should be installed.
Ruby Essentials
fetch_keys:
cmd.run:
- name: >
gpg --keyserver hkp://keys.gnupg.net
--recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
- user: deploy
- unless: gpg --list-keys | grep 4096R/D39DC0E3
rvm-deps:
pkg.installed:
- pkgs:
- bash
- coreutils
- gzip
- bzip2
- gawk
- sed
- curl
- git-core
mri-deps:
pkg.installed:
- pkgs:
- build-essential
- openssl
- libreadline6
- libreadline6-dev
- curl
- git-core
# ...
### exampleone ###
ruby-2.2.2:
rvm.installed:
- default: True
- user: deploy
- require:
- pkg: rvm-deps
- pkg: mri-deps
- user: deploy
exampleone:
rvm.gemset_present:
- ruby: ruby-2.2.2
- user: deploy
- require:
- rvm: ruby-2.2.2
### exampletwo ###
ruby-2.0.0-p247:
rvm.installed:
- user: deploy
- require:
- pkg: rvm-deps
- pkg: mri-deps
- user: deploy
exampletwo:
rvm.gemset_present:
- ruby: ruby-2.0.0-p247
- user: deploy
- require:
- rvm: ruby-2.0.0-p247
### examplethree ###
# ...
bundler-system:
gem.installed:
- name: bundler
- user: deploy
MySQL Setup
mysql-server:
pkg.installed
MySQL user:
mysql_user.present:
- name: deploy
- host: localhost
- password: SomeCoolPassword
- connection_charset: utf8
- require:
- pkg: mysql-server
ruby-mysql-deps:
pkg.installed:
- pkgs:
- libmysqlclient-dev
db-exampleone:
mysql_database.present:
- name: exampleone
mysql_grants.present:
- grant: all privileges
- database: exampleone.*
- user: deploy
db-exampletwo:
mysql_database.present:
- name: exampletwo
mysql_grants.present:
- grant: all privileges
- database: exampletwo.*
- user: deploy
db-examplethree:
mysql_database.present:
- name: examplethree
mysql_grants.present:
- grant: all privileges
- database: examplethree.*
- user: deploy
SSH Access
ssh_key_access:
ssh_auth.present:
- user: deploy
- source: salt://files/authorized_keys
- config: /home/deploy/.ssh/authorized_keys
Run, Puma! Run!
/etc/init/puma.conf:
file.managed:
- source: salt://files/puma/etc/init/puma.conf
- user: root
- group: root
- mode: 640
/etc/init/puma-manager.conf:
file.managed:
- source: salt://files/puma/etc/init/puma-manager.conf
- user: root
- group: root
- mode: 640
/etc/puma.conf:
file.managed:
- source: salt://files/puma/etc/puma.conf
- user: root
- group: root
- mode: 640
Puma-manager is the official puma upstart script. It requires ./config/puma.rb in your app folder and "just works"™.
Done!
Copy here, past there, change a few variables, and you have another server live
Done!
Copy here, past there, change a few variables, and you have another server live
"Few variables" they said… Change deploy user name they said…
# webserver.sls
/srv/www:
file.directory:
- user: deploy
- group: deploy
- mode: 755
- makedirs: True
# user.sls
deploy:
group.present: []
user.present:
- gid: deploy
- home: /home/deploy
- groups:
- sudo
- deploy
# puma.conf
setuid deploy
setgid deploy
# mysql_server.sls
MySQL user:
mysql_user.present:
- name: deploy
#...
db-exampleone:
mysql_database.present:
- name: exampleone
mysql_grants.present:
- grant: all privileges
- database: exampleone.*
- user: deploy
# ruby_server.sls
exampleone:
rvm.gemset_present:
- ruby: ruby-2.2.2
- user: deploy
- require:
- rvm: ruby-2.2.2
bundler-system:
gem.installed:
- name: bundler
- user: deploy
Pillar
Vars. Vars never always changing
Mixing Pillar in
Pillar has the same structure as state files:
top.sls points to YAML vocabularies
# pillar/top.sls
base:
'*':
- rails_data
# pillar/rails_data.sls
rails_data:
user: deploy
mysql:
password: SomeVeryStr0ngPa55worD
ssh_public_key: >
PastYourPubKeyHereAndRecieveYourSshAccess
ToDeployUserWithoutSmsAndRegistration== webowner@example.com
servers:
- exampleone:
name: exampleone
ruby: ruby-2.0.0-p247
gemset: exampleone
domain: exampleone.com
db: exampleonedb
- exampletwo:
name: exampletwo
ruby: ruby-2.0.0-p247
gemset: exampletwo
domain: exampletwo.com
db: exampletwodb
- examplethree:
name: examplethree
ruby: ruby-2.2.1
gemset: examplethree
domain: examplethree.com
db: examplethreedb
Fat free. Typo free.

Cleaning up managed files
Okay, we may use Jinja in state files. But what about Nginx, Puma and other static configs?
We may clean them up as well, just specify the template engine, and you are set up.
{% for server in salt['pillar.get']('rails_data:servers') %}
/etc/nginx/sites-available/{{ server.name }}:
file.managed:
- template: jinja
- source: salt://files/nginx/vhost
Excerpts
# webserver.sls
/etc/nginx/sites-available/exampleone:
file.managed:
- source: salt://files/nginx/exampleone
- user: root
- group: root
- mode: 640
/etc/nginx/sites-available/exampletwo:
file.managed:
- source: salt://files/nginx/exampletwo
- user: root
- group: root
- mode: 640
/etc/nginx/sites-available/examplethree:
file.managed:
- source: salt://files/nginx/examplethree
- user: root
- group: root
- mode: 640
# webserver.sls with Pillar
{% for server in
salt['pillar.get']('rails_data:servers') %}
/etc/nginx/sites-available/{{ server.name }}:
file.managed:
- template: jinja
- source: salt://files/nginx/vhost
- user: root
- group: root
- mode: 640
- defaults: # provide template variables
domain: {{ server.domain }}
appname: {{ server.name }}_app
{% endfor %}
# puma.conf
# ...
setuid deploy
setgid deploy
# ...
# puma.conf with Pillar
# ...
{%- set user =
salt['pillar.get']('rails_data:user') %}
setuid {{ user }}
setgid {{ user }}
# ...
Questions?
No Updates?
Some pepper?
saltstack-rails
By Alexei Volkov
saltstack-rails
- 464