Loading
Skype: volkov.gl
Live example at GitHub
SaltStack is a provisioning tool.
It manages one or more nodes, applying and maintaining configuration.
Several tools providing configuration orchestrating one way or another:
Also check Docker, Vagrant and Otto if you are looking for something special.
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).
Any time. Really.
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 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.
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
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 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^&
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
Executable (.modules) — ignore this
Configurable (.states) — YES!
Basic usage
DevOps mode: ON
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
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 %}
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
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.
Our entry point, which defines state files to apply.
base:
'*':
- user
- swap
- webserver
- misc
- ruby_server
- mysql_server
- ssh
- puma
deploy:
group.present: []
user.present:
- gid: deploy
- home: /home/deploy
- groups:
- sudo
- deploy
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:
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 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.
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-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_key_access:
ssh_auth.present:
- user: deploy
- source: salt://files/authorized_keys
- config: /home/deploy/.ssh/authorized_keys
/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"™.
Copy here, past there, change a few variables, and you have another server live
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 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
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
# 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 }}
# ...