SaltStack is one of the least popular Automation and Configuration Management tool, similar to Ansible, Chef, and Puppet (also stuff that not a CM, it's IaC tool but if you are skilled enough can be used as CM: Terraform and Pulumi). Unlike Ansible, it have agent that needs to be installed to the target servers. Unlike Chef and Puppet, it uses Python. Like Ansible, SaltStack can be use for infrastructure lifecycle management too. To install Salt, you need to run these commands:
sudo add-apt-repository ppa:saltstack/salt
sudo apt install salt-master salt-minion salt-cloud
# for minion:
sudo vim /etc/salt/minion
# change line "#master: salt" into:
# master: 127.0.0.1 # should be master's IP
# id: thisMachineId
sudo systemctl restart salt-minion
There's simpler way to install, bootstrap master and minion, it's using a bootstrap script:
curl -L https://bootstrap.saltstack.com -o install_salt.sh
sudo sh install_salt.sh -M # for master
sudo sh install_salt.sh # for minion
# don't forget change /etc/salt/minion, and restart salt-minion
To the minion will send the key to master, to list all possible minion, run:
sudo salt-key -L
To accept/add minion, you can use:
sudo salt-key --accept=FROM_LIST_ABOVE
sudo salt-key -A # accept all unaccepted, -R reject all
# test connection to all minion
sudo salt '*' test.ping
# run a command
sudo salt '*' cmd.run 'uptime'
If you don't want open port, you can also use salt-ssh, so it would work like Ansible:
On SaltStack, there's 5 things that you need to know:
sudo add-apt-repository ppa:saltstack/salt
sudo apt install salt-master salt-minion salt-cloud
# for minion:
sudo vim /etc/salt/minion
# change line "#master: salt" into:
# master: 127.0.0.1 # should be master's IP
# id: thisMachineId
sudo systemctl restart salt-minion
There's simpler way to install, bootstrap master and minion, it's using a bootstrap script:
curl -L https://bootstrap.saltstack.com -o install_salt.sh
sudo sh install_salt.sh -M # for master
sudo sh install_salt.sh # for minion
# don't forget change /etc/salt/minion, and restart salt-minion
To the minion will send the key to master, to list all possible minion, run:
sudo salt-key -L
To accept/add minion, you can use:
sudo salt-key --accept=FROM_LIST_ABOVE
sudo salt-key -A # accept all unaccepted, -R reject all
# test connection to all minion
sudo salt '*' test.ping
# run a command
sudo salt '*' cmd.run 'uptime'
If you don't want open port, you can also use salt-ssh, so it would work like Ansible:
pip install salt-ssh
# create /etc/salt/roster containing:
ID1:
host: minionIp1
user: root
sudo: True
ID2_usually_hostname:
host: minionIp2
user: root
sudo: True
To execute a command on all roster you can use salt-ssh:
# create /etc/salt/roster containing:
ID1:
host: minionIp1
user: root
sudo: True
ID2_usually_hostname:
host: minionIp2
user: root
sudo: True
To execute a command on all roster you can use salt-ssh:
salt-ssh '*' cmd.run 'hostname'
On SaltStack, there's 5 things that you need to know:
- Master (the controller)
- Minion (the servers/machines being controlled)
- States (current state of servers)
- Modules (same like Ansible modules)
- Grains (facts/properties of machines, to gather facts call: salt-call --grains)
- Execution (execute action on machines, salt-call moduleX.functionX, for example: cmd.run 'cat /etc/hosts | grep 127. ; uptime ; lsblk', or user.list_users, or grains.ls -- for example to list all grains available properties, or grains.get ipv4_interfaces, or grains.get ipv4_interfaces:docker0)
- States (idempotent multiplatform for CM)
- Renderers (module that transform any format to Python dictionary)
- Pillars (user configuration properties)
- salt-call (primary command)
To run locally just add --local. To run on every host we can use salt '*' modulename.functionname. The wildcard can be changed with compound filtering argument, more detail and example here.
To start using salt with file mode, create a directory called salt, and create a top.sls file (it's just a yaml combined with jinja) which contains list of host, filters, and state module you want to call, usually it saved on /srv/ directory, containing:
Then create a directory for each those items containing init.sls or create files for each those items with .sls extension. For example requirements.sls:
id_for_this:
schedule.present:
- name: highstate
- run_on_start: True
- function: state.highstate
- minutes: 60
- maxrunning: 1
- enabled: True
- returner: rawfile_json
- splay: 600
Full example can be something like this:
base:
'*': # every machine
'*': # every machine
- requirements # state module to be called
- statemodule0
dev1:
'osrelease:*22.04*': # only machine with specific os version
- match: grain
- statemodule1
dev2:
'os:MacOS': # only run on mac
- match: grain
- statemodule2/somesubmodule1
prod:
'os:Pop and host:*.domain1': # only run on PopOS with tld domain1
- match: grain
- statemodule3
- statemodule4
'os:Pop': # this too will run if the machine match
- match: grain
- statemodule5
- statemodule0
dev1:
'osrelease:*22.04*': # only machine with specific os version
- match: grain
- statemodule1
dev2:
'os:MacOS': # only run on mac
- match: grain
- statemodule2/somesubmodule1
prod:
'os:Pop and host:*.domain1': # only run on PopOS with tld domain1
- match: grain
- statemodule3
- statemodule4
'os:Pop': # this too will run if the machine match
- match: grain
- statemodule5
Then create a directory for each those items containing init.sls or create files for each those items with .sls extension. For example requirements.sls:
essential-packages: # ID = what this state module do
pkg.installed: # module name, function name
- pkgs:
- bash
- build-essentials
- git
- tmux
- byobu
- zsh
- curl
- htop
- python-software-properties
- software-properties-common
- apache2-utils
file.managed:
- name: /tmp/a/b
- makedirs: True
- user: root
- group: wheel
- mode: 644
- source: salt://files/b # will copy ./files/b to machine
file.managed:
- name: /tmp/a/b
- makedirs: True
- user: root
- group: wheel
- mode: 644
- source: salt://files/b # will copy ./files/b to machine
file.managed:
- name: /tmp/a/c
- contents: # this will create a file with specific lines
- line 1
- line 2
service.running:
- name: myservice1
- watch: # will restart the service if these changed
- file: /etc/myservice.conf
- file: /tmp/a/b
file.append:
- name: /tmp/a/c
- text: 'some line' # will append to that file
cmd.run:
- name: /bin/someCmd1 param1; /bin/cmd2 --someflag2
- contents: # this will create a file with specific lines
- line 1
- line 2
service.running:
- name: myservice1
- watch: # will restart the service if these changed
- file: /etc/myservice.conf
- file: /tmp/a/b
file.append:
- name: /tmp/a/c
- text: 'some line' # will append to that file
cmd.run:
- name: /bin/someCmd1 param1; /bin/cmd2 --someflag2
- onchanges:
- file: /tmp/a/c # run cmd above if this file changed
file.directory: # ensure directory created
- name: /tmp/d
- user: root
- dirmode: 700
archive.extracted: # extract from specific archive file
file.directory: # ensure directory created
- name: /tmp/d
- user: root
- dirmode: 700
archive.extracted: # extract from specific archive file
- name: /tmp/e
- source: https://somedomain/somefile.tgz
- force: True
- keep_source: True
- clean: True
- source: https://somedomain/somefile.tgz
- force: True
- keep_source: True
- clean: True
To apply run: salt-call state.apply requirements
Some other example, we can create a template with jinja and yaml combined, like this:
statemodule0:
file.managed:
- name: /tmp/myconf.cfg # will copy file based on jinja condition
file.managed:
- name: /tmp/myconf.cfg # will copy file based on jinja condition
{% if '/usr/bin' in grains['pythonpath'] %}
- source: salt://files/defaultpython.conf
{% elif 'Pop' == grains['os'] %}
- source: salt://files/popos.conf
{% else %}
- source: salt://files/unknown.conf
{% endif %}
- makedirs: True
cmd.run:
- name: echo
- onchanges:
- source: salt://files/defaultpython.conf
{% elif 'Pop' == grains['os'] %}
- source: salt://files/popos.conf
{% else %}
- source: salt://files/unknown.conf
{% endif %}
- makedirs: True
cmd.run:
- name: echo
- onchanges:
- file: statemodule0 # refering statemodule0.file.managed.name
To create a python state module, you can create a file containing something like this:
#!py
def run():
config = {}
a_var = 'test1' # we can also do a loop, everything is dict/array
config['create_file_{}'.format(a_var)] = {
'file.managed': [
{'name': '/tmp/{}'.format(a_var)},
{'makedirs': True},
{'contents': [
'line1',
'line2'
]
},
],
}
return config
To include another state module, you can specify on statemodulename/init.sls, something like this:
def run():
config = {}
a_var = 'test1' # we can also do a loop, everything is dict/array
config['create_file_{}'.format(a_var)] = {
'file.managed': [
{'name': '/tmp/{}'.format(a_var)},
{'makedirs': True},
{'contents': [
'line1',
'line2'
]
},
],
}
return config
To include another state module, you can specify on statemodulename/init.sls, something like this:
include:
- .statemodule2 # if this a folder, will run the init.sls inside
- .statemodule3 # if this a file, will run statemodule3.sls
- .statemodule2 # if this a folder, will run the init.sls inside
- .statemodule3 # if this a file, will run statemodule3.sls
To run all state it you can call salt-call state.highstate or salt-call state.apply without any parameter.
It would execute top.sls file and the includes in order recursively.
To create a scheduled state, you can create a file containing something like this:
It would execute top.sls file and the includes in order recursively.
To create a scheduled state, you can create a file containing something like this:
schedule.present:
- name: highstate
- run_on_start: True
- function: state.highstate
- minutes: 60
- maxrunning: 1
- enabled: True
- returner: rawfile_json
- splay: 600
Full example can be something like this:
install nginx:
pkg.install:
- nginx
/etc/nginx/nginx.conf: # used as name
file.managed:
soruce: salt://_files/nginx.j2
template: jinja
require:
- install nginx
run nginx:
pkg.install:
- nginx
/etc/nginx/nginx.conf: # used as name
file.managed:
soruce: salt://_files/nginx.j2
template: jinja
require:
- install nginx
run nginx:
service.running:
name: nginx
enable: true
watch:
- /etc/nginx/nginx.conf
name: nginx
enable: true
watch:
- /etc/nginx/nginx.conf
Next, to create a pillar config, just create a normal sls file, containing something like this:
user1:
active: true
sudo: true
ssh_keys:
- ssh-rsa censored user1@domain1
nginx:
server_name: 'foo.domain1'
active: true
sudo: true
ssh_keys:
- ssh-rsa censored user1@domain1
nginx:
server_name: 'foo.domain1'
To reference this on other salt file, you can use jinja something like this:
{% set sn = salt['pillar.get']('nginx:server_name') -%}
server {
listen 443 ssl;
server_name {{ sn }};
...
server {
listen 443 ssl;
server_name {{ sn }};
...
That's it for now, next if you want to learn more is to create your own executor module or other topics here.