SaltStack Tutorial

Automate and Conquer: Your Comprehensive SaltStack Tutorial for Infrastructure Management

Introduction: Unleashing the Power of SaltStack

Before diving into commands and configuration files, let’s understand what SaltStack is and why it’s a compelling choice for modern IT environments.

What Exactly is SaltStack? (Beyond Just Configuration Management)

At its heart, SaltStack is an automation and configuration management engine. But it’s more than just ensuring configuration files are correct. It excels at:

  • Remote Execution: Running arbitrary commands across thousands of systems simultaneously.
  • Configuration Management: Defining the desired state of your systems (packages installed, services running, files in place) and enforcing that state declaratively.
  • Event-Driven Automation: Reacting to events within your infrastructure (like high CPU load, service failures, or new system availability) to trigger automated responses.
  • Infrastructure Orchestration: Coordinating complex deployments and actions across multiple machines in a specific order.

It’s built in Python and leverages the high-performance ZeroMQ messaging library for communication, making it exceptionally fast.

Why Choose SaltStack? Key Differentiators (Speed, Scalability, Event-Driven)

Several factors make SaltStack stand out:

  • Speed: The ZeroMQ backbone allows for near-instantaneous communication between the central controller (Master) and the managed systems (Minions). This translates to rapid command execution and state application, even across vast numbers of machines.
  • Scalability: SaltStack was designed from the ground up to handle tens of thousands of Minions reporting to a single Master. Its architecture supports hierarchical setups (Syndic Masters) for even larger scale.
  • Flexibility: Built with Python, SaltStack is highly extensible. You can easily write custom modules (Execution, State, Grains, Pillar, etc.) to tailor Salt to your specific needs.
  • Event-Driven Core: Unlike many systems where event handling feels tacked on, Salt’s event bus is central to its architecture, enabling powerful reactive automation workflows (Beacons and Reactors).
  • Mature & Robust: SaltStack has been around for years, boasts a large active community, and is used in production by major organizations worldwide.

Core Architecture Demystified: Masters, Minions, and the ZeroMQ Event Bus

Understanding Salt’s architecture is key to using it effectively:

  • Salt Master: The central control server. It sends commands and configuration instructions to the Minions. It listens for incoming connections and events. Typically runs on a dedicated Linux server.
  • Salt Minion: An agent that runs on the managed systems (servers, workstations, cloud instances, etc.). It listens for instructions from the Master, executes them, and reports back the results. Minions can run on various Linux distributions, Windows, macOS, and more.
  • ZeroMQ Event Bus: The high-speed communication backbone. All communication between Master and Minions (commands, results, events) flows over this bus. It uses a publish-subscribe pattern, allowing for efficient broadcasting and targeted messaging.
  • Keys: Communication is secured using public-key cryptography. Each Minion generates a key pair and presents its public key to the Master for acceptance upon first connection.
Code snippet
 
graph LR
    M[Salt Master] -- ZeroMQ (Commands/Configs) --> Min1[Minion 1];
    M -- ZeroMQ (Commands/Configs) --> Min2[Minion 2];
    M -- ZeroMQ (Commands/Configs) --> MinN[Minion N];
    Min1 -- ZeroMQ (Results/Events) --> M;
    Min2 -- ZeroMQ (Results/Events) --> M;
    MinN -- ZeroMQ (Results/Events) --> M;

    style M fill:#f9f,stroke:#333,stroke-width:2px
    style Min1 fill:#ccf,stroke:#333,stroke-width:1px
    style Min2 fill:#ccf,stroke:#333,stroke-width:1px
    style MinN fill:#ccf,stroke:#333,stroke-width:1px

Common Use Cases: Where SaltStack Shines

SaltStack is versatile, finding application in numerous scenarios:

  • Server Provisioning: Setting up new physical or virtual servers from scratch.
  • Application Deployment: Pushing code, configuring dependencies, and managing application lifecycles.
  • Security Compliance & Hardening: Enforcing security policies and configurations across the fleet.
  • Patch Management: Applying updates and patches reliably.
  • Cloud Infrastructure Management: Interacting with cloud provider APIs (via salt-cloud) to spin up or tear down resources.
  • Data Center Automation: Managing complex, multi-tier applications and infrastructure.
  • Continuous Integration/Continuous Deployment (CI/CD): Integrating with CI/CD pipelines to automate testing and deployment phases.

Preparing Your Environment: Installation and Initial Setup

Let’s get our hands dirty and set up a basic SaltStack environment with one Master and one Minion.

Platform Prerequisites and Choosing an Installation Method

  • Master: Typically runs on a Linux distribution (Debian, Ubuntu, RHEL, CentOS, Fedora, etc.). Requires Python 3.x. Needs sufficient RAM and CPU depending on the number of Minions. Network ports 4505 and 4506 need to be open inbound.
  • Minion: Can run on various Linux distributions, Windows, macOS, FreeBSD. Requires Python 3.x. Needs network connectivity back to the Master on ports 4505 and 4506.
  • Installation Methods:
    • Official Repositories (Recommended): SaltStack provides repositories for major Linux distributions (apt, yum/dnf). This is usually the easiest way to install and manage updates.
    • Bootstrap Script: A convenient shell script (bootstrap-salt.sh) that detects the OS and installs Salt automatically. Useful for quick setups or integrating into other scripts.
    • PyPI: Installation via pip. More control but requires manual dependency management.
    • Packages: Manual download and installation of .deb, .rpm, .msi, etc.

We’ll use the official repositories for this guide.

Step-by-Step: Installing the Salt Master

(Example using Ubuntu/Debian)

Import SaltStack Repository Key and Add Repository:

Bash
 
# Ensure prerequisite packages are installed
sudo apt update
sudo apt install curl gnupg

# Import the key (check SaltStack documentation for the latest key URL/method)
curl -fsSL https://repo.saltproject.io/py3/debian/11/amd64/latest/SALTSTACK-GPG-KEY.pub | sudo gpg --dearmor -o /usr/share/keyrings/salt-archive-keyring.gpg

# Add the repository (adjust 'debian' and '11' for your OS/version)
echo "deb [signed-by=/usr/share/keyrings/salt-archive-keyring.gpg arch=amd64] https://repo.saltproject.io/py3/debian/11/amd64/latest buster main" | sudo tee /etc/apt/sources.list.d/salt.list

(Always check the official SaltStack installation guide for the most current repository URLs and commands for your specific OS version.)

Install the salt-master Package:

Bash
 
sudo apt update
sudo apt install salt-master

Configure the Master: Edit the master configuration file: /etc/salt/master. The main setting to check initially is the interface. If your master server has multiple network interfaces, you might need to uncomment and set this to the IP address the minions should connect to.

YAML
 
# /etc/salt/master
# interface: 0.0.0.0  # Binds to all interfaces (default)
# interface: 192.168.1.100 # Example: Bind only to this specific IP

Start and Enable the salt-master Service:

Bash
 
sudo systemctl start salt-master
sudo systemctl enable salt-master
sudo systemctl status salt-master # Verify it's running

Firewall Configuration: Ensure ports 4505 (publish) and 4506 (request/reply) are open for incoming TCP connections from your Minions.

Bash
 
# Example using ufw
sudo ufw allow proto tcp from <your_minion_network_cidr> to any port 4505,4506
# Or be less restrictive (use with caution)
# sudo ufw allow 4505/tcp
# sudo ufw allow 4506/tcp

Step-by-Step: Installing the Salt Minion (Linux/Windows examples)

(Example using Ubuntu/Debian Linux)

Follow steps 1 & 2 from the Master installation to add the repository and update package lists on the Minion machine.

Install the salt-minion Package:

Bash
 
sudo apt update
sudo apt install salt-minion

Configure the Minion: Edit the minion configuration file: /etc/salt/minion. The crucial setting is master. Uncomment it and set it to the IP address or resolvable hostname of your Salt Master. You should also set a unique id for the minion, although it often defaults to the hostname.

YAML
 
# /etc/salt/minion
master: <your_salt_master_ip_or_hostname>
# id: my-web-server-1 # Optional: defaults to hostname

Start and Enable the salt-minion Service:

Bash
 
sudo systemctl start salt-minion
sudo systemctl enable salt-minion
sudo systemctl status salt-minion # Verify it's running

(Example using Windows – requires downloading the installer)

  1. Download: Go to the SaltStack repository website (repo.saltproject.io) and download the appropriate .exe or .msi installer for your Windows version (check Py3 subdirectory).
  2. Install: Run the installer. During installation, you will be prompted for the Master’s IP/hostname and the Minion ID (defaults to hostname).
  3. Service: The installer typically sets up the salt-minion service to start automatically. You can manage it via the Windows Services console (services.msc).
  4. Firewall: Ensure Windows Firewall allows outbound connections for the salt-minion service to ports 4505 and 4506 on the Master.

The Handshake: Understanding and Managing Minion Keys (salt-key)

When a Minion starts for the first time and connects to the Master specified in its configuration, it generates a cryptographic key pair and sends its public key to the Master. This key is placed in a “pending” state on the Master until explicitly accepted.

On the Salt Master:

List all keys (accepted, denied, pending/unaccepted):

Bash
 
sudo salt-key -L

Output might look like:

Accepted Keys:
Denied Keys:
Unaccepted Keys:
minion1.example.com   # <-- Your new minion's ID
Rejected Keys:

Accept a specific pending key:

Bash
 
sudo salt-key -a minion1.example.com

Accept all pending keys (use with caution in untrusted environments):

Bash
 
sudo salt-key -A

Delete a key (e.g., if decommissioning a minion):

Bash
 
sudo salt-key -d minion1.example.com

Accepting the key establishes a secure, trusted communication channel.

Verification: Confirming Master-Minion Communication (test.ping)

Once the Minion’s key is accepted on the Master, you can test communication using the test.ping execution module.

On the Salt Master:

Bash
 
sudo salt '*' test.ping
    • salt: The main command-line interface for Salt.
    • '*': The target. '*' means target all accepted minions.
    • test.ping: The execution module and function to run.

Expected Output:

minion1.example.com:
    True

If you see True returned from your minion(s), congratulations! Your basic SaltStack environment is up and running. If not, check firewall rules, Master/Minion service status, and the master directive in the minion config file. Check logs (/var/log/salt/master, /var/log/salt/minion) for errors.

Remote Execution: Your Command Center

One of Salt’s most immediate benefits is the ability to run commands across many systems simultaneously. This is handled by Execution Modules.

The Foundation: Understanding Execution Modules

Execution modules are Python modules containing functions that can be executed on Minions directly from the Master. Salt comes with hundreds of built-in modules for tasks related to package management, service control, file manipulation, system information gathering, network configuration, and much more.

The general pattern for using them is: salt '<target>' <module_name>.<function_name> [argument1] [argument2=value] ...

Running Ad-Hoc Commands: Syntax and Power (salt '<target>' <module>.<function> [arguments])

This is the core syntax for imperative, one-off tasks.

  • <target>: Specifies which Minion(s) should execute the command (we’ll cover targeting in detail next). For now, '*' targets all connected and accepted Minions.
  • <module_name>: The name of the execution module (e.g., pkg, service, cmd, file).
  • <function_name>: The specific function within the module to call (e.g., install, start, run, managed).
  • [arguments]: Optional positional or keyword arguments required by the function.

Example: Check uptime on all minions. The cmd module has a function run which executes a shell command.

Bash
 
sudo salt '*' cmd.run 'uptime'

Output:

minion1.example.com:
     17:17:45 up 1 day,  4:23,  1 user,  load average: 0.01, 0.02, 0.00

Essential Modules Showcase: pkg, service, file, cmd

Let’s look at some fundamental modules:

  • pkg Module: Manages system packages.

    • Install a package: sudo salt '*' pkg.install vim
    • Remove a package: sudo salt '*' pkg.remove vim
    • Update all packages: sudo salt '*' pkg.upgrade
    • List installed packages: sudo salt '*' pkg.list_pkgs
    • Note: Salt automatically uses the correct underlying package manager (apt, yum, zypper, etc.).
  • service Module: Manages system services.

    • Start a service: sudo salt '*' service.start nginx
    • Stop a service: sudo salt '*' service.stop nginx
    • Restart a service: sudo salt '*' service.restart nginx
    • Enable service at boot: sudo salt '*' service.enable nginx
    • Check service status: sudo salt '*' service.status nginx
  • file Module: Manages files and directories.

    • Check if a file exists: sudo salt '*' file.file_exists /etc/hosts
    • Get file stats: sudo salt '*' file.stats /etc/hosts
    • Manage file content (more powerful with States, see later): sudo salt '*' file.managed /tmp/mytest source=salt://path/to/source/file (copies file from Master’s file server)
  • cmd Module: Executes shell commands.

    • Run a simple command: sudo salt '*' cmd.run 'ls -l /etc/'
    • Run a script: sudo salt '*' cmd.script salt://scripts/myscript.sh (runs script downloaded from Master)

These are just the tip of the iceberg. You can explore available modules and functions:

  • List all available modules on a minion: sudo salt 'minion1.example.com' sys.list_modules
  • Get help/documentation for a module: sudo salt '*' sys.doc pkg
  • Get help for a specific function: sudo salt '*' sys.doc pkg.install

Precision Targeting: Communicating with the Right Minions

Running commands on '*' (all minions) is useful, but often you need to target specific subsets of your infrastructure. Salt’s targeting system is extremely powerful and flexible.

Default Targeting: Globbing, PCRE, and Lists

These are the most common methods, specified directly in the <target> part of the salt command.

  • Globbing (Default): Uses shell-style wildcards based on the Minion ID.

    • '*': All minions.
    • 'web*' : Minions with IDs starting with web (e.g., web1, webserver-alpha).
    • 'web?.example.com': Minions like web1.example.com, webA.example.com but not web10.example.com.
    • '*(web|db)*': Minions containing web or db in their ID.
  • PCRE (Perl Compatible Regular Expressions): For more complex patterns, preface the target with E@.

    • E@'web\d+': Target minions matching web followed by one or more digits (e.g., web1, web123).
    • E@'(web|db)-[a-z]{3}\.example\.com': Matches web-abc.example.com or db-xyz.example.com.
  • Lists: Provide an explicit, comma-separated list of Minion IDs. Preface with L@.

    • L@minion1,minion2,db-server.internal: Targets exactly these three minions.

System Intel: Targeting with Grains

Grains are static pieces of information about a Minion (OS type, kernel version, IP address, memory, custom tags). They are collected when the Minion starts and are ideal for targeting based on system properties. Preface the target with G@.

  • View all grains for a minion: sudo salt 'minion1.example.com' grains.items
  • Target all Ubuntu minions: G@'os:Ubuntu'
  • Target minions with more than 8GB RAM (grains value is usually in MB): G@'mem_total:range(8192,)' (Syntax might vary slightly, check grains.items output)
  • Target based on kernel version: G@'kernelrelease:5.4.*'
  • Target based on a custom grain (e.g., roles: [webserver, api], a grain you define): G@'roles:webserver'

Custom Data: Targeting with Pillar Information

Pillar is data defined on the Master and securely passed to specific Minions (often used for secrets or configuration parameters). You can also target Minions based on the Pillar data assigned to them. Preface with P@.

  • Target minions assigned a role pillar with the value database: P@'role:database'
  • Target minions in a specific datacenter pillar: P@'datacenter:london'

(We’ll cover creating Pillar data later)

Complex Queries: Mastering Compound Matchers

Combine multiple targeting methods using boolean operators (and, or, not) for highly specific selections. Preface with C@. (Note: Often, the C@ prefix can be omitted if the expression is complex enough for Salt to infer it’s a compound matcher).

  • Target Ubuntu web servers: C@'G@os:Ubuntu and P@role:webserver' (or just 'G@os:Ubuntu and P@role:webserver')
  • Target all minions except database servers: '* and not P@role:database'
  • Target web servers in London OR any server in Paris: ( G@roles:webserver and P@datacenter:london ) or P@datacenter:paris

Grouping Minions with Node Groups

For frequently used complex targets, you can define Node Groups in the Master configuration file (/etc/salt/master).

YAML
 
# /etc/salt/master
nodegroups:
  webservers: 'G@roles:webserver and G@os:Ubuntu'
  dbservers: 'P@role:database'
  london_servers: 'P@datacenter:london and not L@db-backup-lon'
  all_prod: '( N@webservers or N@dbservers ) and P@environment:prod'

(Restart the salt-master service after modifying nodegroups)

Now you can target using the group name, prefaced with N@:

  • sudo salt -N webservers test.ping
  • sudo salt -N all_prod pkg.upgrade

Targeting is fundamental to applying actions selectively and safely across your infrastructure.

Declarative Configuration: Introducing Salt States

While remote execution is great for ad-hoc tasks, the real power of configuration management lies in defining the desired state of your systems and letting Salt ensure they reach and maintain that state. This is done using Salt States.

Infrastructure as Code: The Philosophy of Salt States (SLS)

Salt States are typically written in YAML format (though other renderers like Python or JSON are possible) using a .sls file extension. They describe what the system should look like, not how to get there (that’s Salt’s job). This is a declarative approach.

Key benefits:

  • Idempotency: Applying the same state multiple times has the same effect as applying it once. Salt checks the current state before making changes.
  • Readability: YAML is human-readable, making states relatively easy to understand.
  • Version Control: States are text files, perfect for storing in Git or other VCS for tracking changes and collaboration.
  • Reusability: States can be parameterized and reused across different systems.

States are usually stored on the Salt Master in the Salt File Server directory, typically /srv/salt/.

Anatomy of an SLS File: Resources, IDs, Functions, and Arguments

An SLS file consists of one or more State Declarations (also called Resources). Each declaration follows this structure:

YAML
 
# <State ID>:
#   <State Module>.<function>:
#     - <argument_name>: <argument_value>
#     - <another_argument>: <value>
#     - <list_argument>:
#       - item1
#       - item2
#     - require: # Example of a requisite
#       - pkg: nginx

# Example: Install and enable Apache on Debian/Ubuntu
install_apache:           # Unique State ID (descriptive name)
  pkg.installed:          # State Module (pkg) and function (installed)
    - name: apache2       # Argument: name of the package

ensure_apache_running:    # Another State ID
  service.running:        # State Module (service) and function (running)
    - name: apache2       # Argument: name of the service
    - enable: True        # Argument: ensure service starts on boot
    - require:            # Requisite: This state requires 'install_apache' to succeed first
      - pkg: install_apache # Reference the 'pkg' type from the ID 'install_apache'
  • State ID: A unique identifier for this specific declaration within the SLS file (and often globally across states applied in one run). Can be anything descriptive, but convention often mirrors the primary resource being managed (e.g., the package name, service name, file path).
  • State Module: Similar to execution modules, but focused on declaring state (e.g., pkg, service, file, user, group).
  • Function: The specific state function to use (e.g., installed, removed, running, dead, managed, directory, present). These functions are typically idempotent.
  • Arguments: Parameters passed to the state function. name is very common, specifying the target resource (package name, service name, file path). Other arguments modify the behavior (e.g., enable: True, source: salt://..., user:, group:, mode:).
  • Requisites: Define dependencies between state declarations (covered in the next section).

Writing Your First State: Managing a Web Server Setup

Let’s create a simple state to install nginx, ensure its configuration file is in place (from the master), and ensure the service is running.

Create directories on the Master:

Bash
 
sudo mkdir -p /srv/salt/nginx/files

Create a sample Nginx config file on the Master:

Bash
 
# /srv/salt/nginx/files/nginx.conf
# A very basic placeholder config
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 768;
}

http {
    # ... basic http settings ...
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

(This is just an example, use a valid Nginx config)

Create the SLS file on the Master:

Bash
 
sudo nano /srv/salt/nginx/init.sls

(Conventionally, init.sls is the main file for a state named after the directory, here nginx)

YAML
 
# /srv/salt/nginx/init.sls
install_nginx:
  pkg.installed:
    - name: nginx

manage_nginx_config:
  file.managed:
    - name: /etc/nginx/nginx.conf  # Path on the Minion
    - source: salt://nginx/files/nginx.conf # Path on Master (relative to /srv/salt/)
    - user: root
    - group: root
    - mode: '0644'
    - require:            # Require nginx package to be installed first
      - pkg: install_nginx
    - watch_in:           # If this file changes, trigger the service state
      - service: ensure_nginx_running

ensure_nginx_running:
  service.running:
    - name: nginx
    - enable: True
    - require:            # Require the package installation
      - pkg: install_nginx
    # Note: The 'watch_in' in manage_nginx_config implicitly creates a 'watch' requisite here

Applying Specific States: The state.apply Command

To apply only the nginx state (defined in nginx/init.sls or just nginx) to a specific target:

  • On the Master:
    Bash
     
    sudo salt 'web*' state.apply nginx
    
    • state.apply: The execution module function used to apply states.
    • nginx: The name of the state to apply. Salt looks for /srv/salt/nginx.sls or /srv/salt/nginx/init.sls.

Salt will analyze the SLS file, check the current state of the target minion(s), and make only the necessary changes to match the declared state. The output will show which states succeeded, failed, and what changes were made.

Orchestrating System Configuration: The state.highstate Command

Applying individual states is useful for testing or specific tasks. However, the standard way to manage the complete configuration of your minions is via the highstate.

The highstate process involves:

  1. Minion requests its configuration from the Master.
  2. Master consults the top.sls file (see below) to determine which states apply to that specific minion.
  3. Master compiles and sends the applicable state definitions back to the Minion.
  4. Minion executes the compiled states, ensuring its configuration matches the declarations.

To trigger a highstate run on targeted minions from the Master:

Bash
 
sudo salt 'db*' state.highstate

This command tells the targeted minions (db* in this case) to perform steps 1-4.

You can also run highstate directly on a Minion (useful for testing):

Bash
 
sudo salt-call state.highstate

(salt-call is a command run on the minion itself)

The Master Map: Understanding the top.sls File

How does the Master know which states apply to which minions during a highstate? It uses the top.sls file located in the root of your Salt File Server environment (e.g., /srv/salt/top.sls).

The top.sls file maps targets (minions) to the SLS files they should include in their highstate.

YAML
 
# /srv/salt/top.sls

# Define environments (optional but recommended)
base: # 'base' is the default environment
  '*': # Target: All minions
    - core_config # Apply states defined in core_config.sls or core_config/init.sls
    - users

  'G@os:Ubuntu': # Target: Ubuntu minions
    - ubuntu_specific

  'web*': # Target: Minions with IDs starting with 'web'
    - apache # Apply apache.sls or apache/init.sls
    - php

  'N@dbservers': # Target: Minions in the 'dbservers' nodegroup
    - postgresql
    - backups

  'L@minion1,minion2': # Target: Specific list of minions
    - monitoring_agent

  'C@G@os:CentOS and P@role:appserver': # Target: Compound matcher
    - tomcat
    - java
  • Environments: Sections like base: define Salt environments (more on this later). base is the default.
  • Targets: Uses the same targeting syntax as the salt command (*, globs, grains, pillar, lists, compound, nodegroups).
  • SLS References: A list of state names (SLS files/directories) to apply to the matched targets.

When state.highstate runs, the Master evaluates top.sls for the specific minion calling in, aggregates all matching SLS references, compiles them into a single configuration set, and sends it to the minion for execution.

State Management Fundamentals: Dependencies and Ordering

SLS files describe what state is desired, but often the order in which things happen matters. You can’t configure a service before the package containing it is installed. Salt uses Requisites to manage these dependencies.

Ensuring Order: Requisites (require, watch, prereq)

Requisites are added to a state declaration and refer to other state declarations.

  • require: The most common requisite. The requiring state will only execute if the required state executes successfully first.

    YAML
     
    # /srv/salt/apache/init.sls
    install_apache:
      pkg.installed:
        - name: apache2
    
    configure_apache:
      file.managed:
        - name: /etc/apache2/sites-available/myapp.conf
        - source: salt://apache/files/myapp.conf
        - require: # This file management depends on the package being installed
            - pkg: install_apache
    
  • watch: Similar to require, but adds reactivity. If the watched state changes, the watching state will be triggered again (often used to restart services when config files change).

    YAML
     
    # /srv/salt/apache/init.sls (continued)
    enable_apache_site:
      file.symlink:
        - name: /etc/apache2/sites-enabled/myapp.conf
        - target: /etc/apache2/sites-available/myapp.conf
        - require:
            - file: configure_apache # Require the config file state
    
    ensure_apache_running:
      service.running:
        - name: apache2
        - enable: True
        - watch: # If any of these change, restart Apache
            - file: configure_apache
            - file: enable_apache_site
            - pkg: install_apache # Less common, but ensures service runs after install/update
    

    How it works: The watch requisite in ensure_apache_running effectively tells Salt: “If configure_apache or enable_apache_site makes changes, run my service.running state again.” Salt’s service.running state, when triggered by a watch, typically performs a restart or reload.

  • prereq: A check. The prerequisite state declaration is executed only to see if it would make changes. If it would, the requiring state runs. If the prerequisite state is already satisfied (no changes needed), the requiring state is skipped. Less common, used for checks before potentially disruptive actions.

Handling Failures: Requisite_in and OnFail Requisites

Sometimes it’s more logical to define dependencies in the opposite direction or react to failures.

  • require_in / watch_in: Declares that other states require this state. Syntactic sugar, often makes states more self-contained.

    YAML
     
    # Alternative way to write the watch dependency from previous example
    configure_apache:
      file.managed:
        - name: /etc/apache2/sites-available/myapp.conf
        # ... other args ...
        - watch_in: # Tell the service state to watch this file
            - service: ensure_apache_running
    
    ensure_apache_running:
      service.running:
        - name: apache2
        # ... other args ...
        # No 'watch' needed here if defined via 'watch_in' elsewhere
    
  • onfail / onchanges: Triggers another state only if the current state fails (onfail) or succeeds and makes changes (onchanges). Useful for cleanup or notification actions.

Conditional Logic: onlyif and unless Conditions

These allow you to make state execution conditional based on the output of a shell command executed on the Minion.

  • onlyif: The state declaration runs only if the specified shell command returns an exit code of 0 (success).

    YAML
     
    create_special_file:
      file.managed:
        - name: /opt/myapp/special.flag
        - contents: "Processed"
        - onlyif: "test -f /opt/myapp/needs_processing.trigger" # Only run if trigger file exists
    
  • unless: The state declaration runs only if the specified shell command returns a non-zero exit code (failure).

    YAML
     
    initialize_database:
      cmd.run:
        - name: "/opt/myapp/scripts/init_db.sh"
        - unless: "test -f /opt/myapp/db_initialized.flag" # Only run if the flag file DOES NOT exist
    

Requisites and conditionals provide fine-grained control over the execution flow and dependencies within your Salt states, enabling complex configurations.

Managing Data: Grains and Pillar

Hardcoding values like IP addresses, usernames, or even package names directly into states isn’t ideal. Salt provides two primary mechanisms for managing and distributing data: Grains and Pillar.

Grains: Gathering Static Minion Data (Facts)

Grains are pieces of information about a Minion, collected by the Minion itself when it starts. They typically represent relatively static data like:

  • Operating system (os, osrelease, oscodename)
  • Kernel version (kernelrelease)
  • Hostname (id, host)
  • Network interfaces and IP addresses (ip4_interfaces, fqdn_ip4)
  • CPU and Memory (cpu_model, num_cpus, mem_total)
  • Salt version (saltversion)
  • Virtualization type (virtual)

Use Cases:

  • Targeting minions (G@os:Ubuntu).
  • Conditional logic within states (using Jinja templating).
  • Providing context for configuration files.

Viewing Grains:

  • View all grains for specific minions: sudo salt 'minion*' grains.items
  • View a specific grain value: sudo salt 'minion*' grains.get os
  • View grains available for targeting: sudo salt '*' grains.ls

Exploring Core Grains and Creating Custom Grains

Salt collects many useful grains by default (core grains). Sometimes, however, you need Minion-specific information not automatically gathered, like its physical location, intended role, or environment designation. You can create Custom Grains.

Methods for Creating Custom Grains:

In Minion Configuration (/etc/salt/minion or /etc/salt/minion.d/grains.conf):

YAML
 
# /etc/salt/minion.d/grains.conf
grains:
  roles:
    - webserver
    - frontend
  datacenter: london
  environment: production

(Requires restarting the salt-minion service)

Using a grains module in /srv/salt/_grains/: (More powerful, uses Python) Create a Python file on the Master in /srv/salt/_grains/:

Bash
 
sudo mkdir -p /srv/salt/_grains
sudo nano /srv/salt/_grains/my_custom_grains.py
Python
 
# /srv/salt/_grains/my_custom_grains.py
import os # Example: using python module

def get_server_role():
    # Complex logic to determine role based on hostname, file existence, etc.
    grains = {}
    hostname = __grains__['host'] # Access existing grains
    if 'web' in hostname:
        grains['complex_role'] = 'web'
    elif os.path.exists('/etc/myapp/is_database'):
         grains['complex_role'] = 'database'
    else:
         grains['complex_role'] = 'unknown'
    return grains # Return a dictionary

Sync custom modules (including grains) to minions:

Bash
 
sudo salt '*' saltutil.sync_grains
# Or sync all custom types: sudo salt '*' saltutil.sync_all

Refresh grains data on minions:

Bash
 
sudo salt '*' saltutil.refresh_grains

Now check the new grain: sudo salt '*' grains.get complex_role

Pillar: Distributing Secure and Sensitive Data to Minions

While Grains are facts from the Minion, Pillar is data generated on the Master and securely transmitted to specific, targeted Minions. It’s the primary mechanism for managing:

  • Secrets: API keys, passwords, SSL certificate private keys.
  • Configuration Parameters: Usernames, database connection strings, application settings.
  • Minion-Specific Variables: Data that differs between minions but isn’t a static “fact” (e.g., which software license key to use).

Key Characteristics:

  • Defined on the Master (typically in /srv/pillar/).
  • Targeted to specific minions using a top.sls file (similar to states, but in the pillar directory: /srv/pillar/top.sls).
  • Transmitted securely over the encrypted ZeroMQ bus.
  • Available only to the Minion(s) it’s targeted at.
  • Ideal for use in State files (via Jinja) to avoid hardcoding sensitive or variable data.

Viewing Assigned Pillar Data (on the Minion or via Master):

  • On the Minion: sudo salt-call pillar.items
  • From the Master: sudo salt 'minion1.example.com' pillar.items (Shows pillar assigned to that minion)
  • Get a specific pillar key: sudo salt 'minion1.example.com' pillar.get api_key

Defining Pillar Data and Assigning it via the top.sls

Create Pillar directories on the Master:

Bash
 
sudo mkdir -p /srv/pillar

Create Pillar SLS files (YAML):

Bash
 
sudo nano /srv/pillar/users.sls
YAML
 
# /srv/pillar/users.sls
admin_user: admin
sudo_group: wheel # Or 'sudo' on Debian/Ubuntu
Bash
 
sudo nano /srv/pillar/secrets.sls
YAML
 
# /srv/pillar/secrets.sls
# WARNING: Storing plain text secrets here is common but NOT best practice
# Consider using GPG renderer or HashiCorp Vault integration for production
database:
  user: db_user
  password: insecure_password_123
api:
  key: abcdef1234567890

Create the Pillar top.sls file:

Bash
 
sudo nano /srv/pillar/top.sls
YAML
 
# /srv/pillar/top.sls
base:
  '*': # Target: All minions
    - users # Assign data from users.sls

  'P@role:database': # Target: Minions with Pillar role 'database'
    - secrets # Assign data from secrets.sls (only to these minions)

  'minion1.example.com': # Target: Specific minion
    - minion1_specific_settings # Assign data from minion1_specific_settings.sls
  • Refresh Pillar data for Minions: Minions don’t automatically get new Pillar data. You need to tell them to refresh it.

    Bash
     
    sudo salt '*' saltutil.refresh_pillar
    

    After refreshing, minions targeted in pillar/top.sls will have access to the specified data.

Accessing Grain and Pillar Data in States and Commands

Both Grains and Pillar data are easily accessible within State files (using Jinja templating, see next section) and even in execution module commands.

Accessing in Commands:

Bash
 
# Install package based on OS grain
sudo salt -G 'os:Ubuntu' pkg.install apache2
sudo salt -G 'os:CentOS' pkg.install httpd

# Get a pillar value directly
sudo salt 'db*' pillar.get database:user

Accessing in States/Jinja: (Preview)

YAML
 
# Example within an SLS file using Jinja
create_admin_user:
  user.present:
    - name: {{ pillar['admin_user'] }} {# Access pillar data #}
    - groups:
      - {{ pillar['sudo_group'] }}
    - shell: /bin/bash

install_webserver:
  pkg.installed:
    # Access grain data
    {% if grains['os'] == 'Ubuntu' %}
    - name: apache2
    {% elif grains['os'] == 'CentOS' %}
    - name: httpd
    {% endif %}

Grains and Pillar are essential for creating dynamic, reusable, and secure Salt configurations. Remember the key difference: Grains = Facts FROM Minion, Pillar = Secure/Config Data TO Minion.

Dynamic Configurations: Templating with Jinja

Hardcoding values in states makes them rigid. Templating allows you to dynamically generate parts of your SLS files or configuration files managed by Salt, using data from Grains and Pillar. Salt’s default templating engine is Jinja2.

Why Template? Making States Dynamic and Reusable

  • Avoid Repetition (DRY): Define loops to create multiple users, manage multiple virtual hosts, or install lists of packages defined in Pillar.
  • Adaptability: Generate configuration files specific to the Minion’s environment (e.g., use different IP addresses based on Grains, different API keys based on Pillar).
  • Conditional Logic: Include or exclude entire blocks of state declarations based on Grains or Pillar data (e.g., only install monitoring tools on production servers).
  • Readability (when used well): Can make complex configurations more concise.

Jinja Basics within Salt SLS Files

Salt processes SLS files through the renderer (Jinja by default) before parsing the YAML. Jinja syntax is enclosed in special delimiters:

{{ ... }}: For expressions (outputs a string). Used to print variables or the results of function calls.

YAML
 
motd_file:
  file.managed:
    - name: /etc/motd
    - contents: "Welcome to {{ grains['id'] }} running {{ grains['os'] }} {{ grains['osrelease'] }}"

{% ... %}: For statements (logic like loops, conditionals).

YAML
 
{% set user_list = pillar.get('app_users', []) %} {# Get list from pillar, default to empty #}

{% for user in user_list %}
create_app_user_{{ user }}: {# Dynamic State ID #}
  user.present:
    - name: {{ user }}
    - shell: /bin/false
    - home: /var/www/{{ user }}
{% endfor %}


{% if grains['os_family'] == 'RedHat' %}
install_epel_repo:
  pkg.installed:
    - name: epel-release
{% endif %}
  • {# ... #}: For comments (not included in the final rendered YAML).

Leveraging Grains, Pillar, and Salt Functions in Templates

Within Jinja templates in Salt, you have access to:

  • grains dictionary: All the Minion’s grains (grains['os'], grains.get('custom_grain', 'default_value')).
  • pillar dictionary: All Pillar data assigned to the Minion (pillar['api_key'], pillar.get('optional_setting', True)).
  • salt dictionary: Allows you to call Salt Execution Modules directly from Jinja! (salt['cmd.run']('some_command'), salt['network.ip_addrs']()). Use with caution, as it can slow down state compilation and potentially break idempotency if not used carefully.
  • opts dictionary: Access to the Minion’s configuration options.

Example: Dynamic Apache Virtual Host Configuration

Pillar Data (/srv/pillar/apache_vhosts.sls):

YAML
 
apache_vhosts:
  site1:
    domain: site1.example.com
    docroot: /var/www/site1
  site2:
    domain: site2.example.com
    docroot: /var/www/site2
    ssl_enabled: True # Example extra setting

Map Pillar in /srv/pillar/top.sls:

YAML
 
base:
  'G@roles:webserver':
    - apache_vhosts

Jinja Template for Apache Config (/srv/salt/apache/files/vhost.conf.j2): (Note the .j2 extension – convention for Jinja template files)

Apache
 
# /srv/salt/apache/files/vhost.conf.j2
<VirtualHost *:{{ port | default('80') }}>
    ServerName {{ domain }}
    DocumentRoot {{ docroot }}

    ErrorLog ${APACHE_LOG_DIR}/{{ domain }}-error.log
    CustomLog ${APACHE_LOG_DIR}/{{ domain }}-access.log combined

    <Directory {{ docroot }}>
        Options Indexes FollowSymLinks
        AllowOverride None
        Require all granted
    </Directory>

    {# Example of using an extra pillar value #}
    {% if ssl_enabled %}
    # Redirect to HTTPS would normally go here in a real setup
    # This is just demonstrating the 'if' condition
    {# Add SSL configuration here if needed #}
    {% endif %}
</VirtualHost>

SLS File to Manage VHosts (/srv/salt/apache/vhosts.sls):

YAML
 
# /srv/salt/apache/vhosts.sls
include:
  - apache # Assumes apache/init.sls installs/starts apache

# Ensure vhost directory exists
vhost_config_dir:
  file.directory:
    - name: /etc/apache2/sites-available # Adjust path for CentOS/RHEL if needed
    - user: root
    - group: root
    - mode: '0755'

# Loop through vhosts defined in pillar
{% for site_name, site_data in pillar.get('apache_vhosts', {}).items() %}

manage_vhost_config_{{ site_name }}:
  file.managed:
    - name: /etc/apache2/sites-available/{{ site_data.domain }}.conf
    - source: salt://apache/files/vhost.conf.j2 # Use the Jinja template
    - template: jinja # Explicitly state renderer (usually auto-detected)
    - context: # Pass variables into the template's context
        domain: {{ site_data.domain }}
        docroot: {{ site_data.docroot }}
        ssl_enabled: {{ site_data.get('ssl_enabled', False) }} # Safely get optional value
        port: 80 # Example static value passed in
    - user: root
    - group: root
    - mode: '0644'
    - require:
      - file: vhost_config_dir
    - watch_in: # Restart apache if config changes
      - service: apache # Assumes 'apache' service state is defined elsewhere

enable_apache_site_{{ site_name }}:
  cmd.run: # Using cmd.run here, though file.symlink or a2ensite module might be better
    - name: a2ensite {{ site_data.domain }}.conf
    - unless: test -L /etc/apache2/sites-enabled/{{ site_data.domain }}.conf
    - require:
      - file: manage_vhost_config_{{ site_name }}
    - watch_in:
      - service: apache

{% endfor %}

This example demonstrates looping through Pillar data, using file.managed with a Jinja template (.j2 file), passing context variables, and making state IDs dynamic. Templating is a cornerstone of creating flexible and maintainable Salt states.

Organizing Your Salt Codebase

As your Salt deployment grows, managing potentially hundreds of SLS files, templates, and pillar data requires a structured approach.

Structuring States and Pillar for Maintainability

A common and effective way to organize your Salt File Server (/srv/salt) and Pillar (/srv/pillar) roots is by function or component.

/srv/salt/
├── top.sls           # Master state mapping
├── core/             # Core system settings (e.g., NTP, SSH config, base packages)
│   ├── init.sls
│   └── files/
│       └── sshd_config.j2
├── users/            # User management states
│   └── init.sls
├── apache/           # Apache web server states
│   ├── init.sls      # Installs/configures base apache
│   ├── vhosts.sls    # Manages virtual hosts (perhaps using pillar/jinja)
│   └── files/
│       ├── apache.conf.j2
│       └── vhost.conf.j2
├── postgresql/       # PostgreSQL database states
│   └── init.sls
├── myapp/            # Custom application deployment
│   ├── init.sls
│   └── files/
│       └── app.properties.j2
└── _grains/          # Custom grain modules (Python)
    └── my_grains.py
└── _modules/         # Custom execution/state modules (Python)
    └── my_module.py
└── _renderers/       # Custom renderers (e.g., GPG for pillar)
    └── gpg.py

/srv/pillar/
├── top.sls           # Master pillar mapping
├── core.sls          # Core settings (e.g., domain name, NTP servers)
├── users.sls         # User definitions, passwords (use encryption!)
├── secrets/          # Secrets (again, encryption!)
│   ├── database.sls
│   └── api_keys.sls
└── roles/            # Role-specific pillar data
    ├── webserver.sls
    └── database.sls

Key Principles:

  • Modularity: Group related states (e.g., all Apache config under apache/). Use include: within SLS files to pull in dependencies from other modules (include: - users).
  • init.sls: Use init.sls as the entry point for a component (e.g., apache/init.sls handles basic installation/service). Applying state apache will automatically use apache/init.sls.
  • Separation: Keep states (/srv/salt) separate from configuration data/secrets (/srv/pillar).
  • Naming Conventions: Use clear, consistent names for SLS files and state IDs.
  • Version Control: Store /srv/salt and /srv/pillar in a Git repository.

Managing Different Deployments: Salt Environments (Base, Prod, Dev)

Salt Environments allow you to maintain different versions of your states and pillar data, typically corresponding to deployment stages like development, testing, and production.

Definition: Environments are defined as top-level keys in your Master configuration (/etc/salt/master) under file_roots and pillar_roots.

YAML
 
# /etc/salt/master
file_roots:
  base: # Default environment
    - /srv/salt/base
  dev:
    - /srv/salt/dev
    - /srv/salt/base # Often include base for fallback
  prod:
    - /srv/salt/prod
    - /srv/salt/base # Prod might also overlay base

pillar_roots:
  base:
    - /srv/pillar/base
  dev:
    - /srv/pillar/dev
    - /srv/pillar/base
  prod:
    - /srv/pillar/prod
    - /srv/pillar/base

(Restart salt-master after changes)

Directory Structure: You would replicate your state/pillar structure under each environment directory (e.g., /srv/salt/dev/apache/init.sls, /srv/salt/prod/apache/init.sls). Files in more specific environments overlay those in less specific ones (e.g., dev overlays base).

top.sls: You can specify environments in your top.sls files (both state and pillar).

YAML
 
# /srv/salt/base/top.sls (or just /srv/salt/top.sls if using simple setup)
base:
  '*':
    - core
dev: # Assign states ONLY if minion is in 'dev' env
  'G@environment:dev': # Target dev minions
    - myapp             # Uses /srv/salt/dev/myapp/init.sls
prod: # Assign states ONLY if minion is in 'prod' env
   'G@environment:prod':
    - myapp             # Uses /srv/salt/prod/myapp/init.sls

Assigning Minions to Environments: Typically done via Grains or Pillar. A common way is to set a grain environment: dev or environment: prod on the minions.

Targeting/Applying: You can specify the environment when running commands:

Bash
 
sudo salt -G 'environment:dev' state.apply myapp saltenv=dev
sudo salt 'prod-db*' state.highstate saltenv=prod
sudo salt 'prod-db*' pillar.items saltenv=prod

If saltenv is omitted, it defaults to base. A minion running highstate will use the environment specified in its own config (/etc/salt/minion -> saltenv: dev) or default to base.

Environments are crucial for safely testing changes before rolling them out to production.

Leveraging the Salt File Server

The Salt File Server is the mechanism by which the Master serves files (SLS, templates, configuration files, scripts, binaries) to Minions.

  • Backend: By default, it uses the local filesystem directories specified in file_roots (e.g., /srv/salt/base). Other backends like Git (gitfs) are popular and powerful, allowing Salt to serve files directly from Git repositories.
  • salt:// URL: This special URL scheme is used within states and commands to refer to files on the Master’s file server, relative to the environment’s root.
    • salt://nginx/files/nginx.conf: Refers to /srv/salt/<env>/nginx/files/nginx.conf (where <env> is the active environment, e.g., base).
    • salt://scripts/my_setup.sh: Refers to /srv/salt/<env>/scripts/my_setup.sh.
  • Usage: Primarily used in file.managed states (source: argument), cmd.script (source:), and sometimes for distributing custom modules/grains if not using saltutil.sync_*.

Understanding the file server, environments, and a good directory structure is key to managing a scalable and maintainable Salt infrastructure.

10. Advanced SaltStack Capabilities

Beyond basic remote execution and state management, Salt offers powerful features for complex automation.

10.1. Salt Orchestrate: Coordinating Multi-Node, Multi-Step Processes

While state.highstate ensures individual minions reach their desired state, Orchestration manages workflows across multiple minions in a defined sequence. It runs on the Master and uses familiar SLS syntax but calls execution modules or state modules targeting different minions.

  • Use Cases:

    • Deploying a multi-tier application (e.g., set up DB server, then app server, then web server).
    • Performing rolling updates across a cluster (update node 1, check health, update node 2…).
    • Database migrations requiring coordination between app servers and DB servers.

Orchestration Runner: Uses the state.orchestrate (or state.sls with run_type=orchestrate) execution module on the Master.

Bash
 
sudo salt-run state.orchestrate deployments.my_app_deploy saltenv=base

Orchestration SLS File (/srv/salt/deployments/my_app_deploy.sls):

YAML
 
# Example Orchestration SLS (runs on Master)

# Step 1: Ensure DB servers are configured via highstate
configure_db_servers:
  salt.state: # Use the 'salt.state' state module within orchestration
    - tgt: 'N@dbservers' # Target DB servers
    - sls: # Can apply specific states or highstate
      - postgresql
      - backups
    - highstate: True # Or just run highstate for the target

# Step 2: Run a specific script on app servers AFTER DBs are done
migrate_app_database:
  salt.function: # Use the 'salt.function' state module to run an execution module
    - tgt: 'N@appservers'
    - fun: cmd.script # Function to run (execution module)
    - arg: # Arguments for the function
      - salt://scripts/app_db_migrate.sh
    - require: # Depends on the previous step
      - salt: configure_db_servers

# Step 3: Bring web servers online by running highstate
configure_web_servers:
  salt.state:
    - tgt: 'N@webservers'
    - highstate: True
    - require:
      - salt: migrate_app_database # Depends on app server step

Orchestration unlocks powerful cross-machine workflow automation directly within Salt.

Event-Driven Automation: Beacons and Reactors Explained

Salt’s event bus isn’t just for commands; it enables reactive automation.

  • Beacons: Modules running on Minions that monitor system aspects (CPU load, disk usage, service status, file changes, specific log entries). When a defined condition is met, the Beacon sends an event to the Master’s event bus.

    • Configuration: Defined in Minion configuration (/etc/salt/minion.d/beacons.conf) or via Pillar.
    YAML
     
    # /etc/salt/minion.d/beacons.conf Example
    beacons:
      load: # Monitor system load average
        - averages:
            1m:
              - 0.0 # Trigger if 1-min load > 8.0 (example threshold)
              - 8.0
        - interval: 60 # Check every 60 seconds
      service: # Monitor a service
        - services:
            nginx:
              enabled: True # Ensure it's supposed to be running
              status: stopped # Trigger ONLY if it stops
    

    (Requires salt-minion restart or saltutil.sync_beacons + saltutil.update_beacons)

Reactors: Configurations on the Master that listen for specific event tags on the event bus. When a matching event is received (often from a Beacon, but can be any Salt event), the Reactor triggers a predefined action, usually running an Orchestration SLS file or a remote execution command.

  • Configuration: Defined in Master configuration (/etc/salt/master.d/reactor.conf).

YAML

# /etc/salt/master.d/reactor.conf Example
reactor:
  # Tag matches events from the 'load' beacon above when threshold is crossed
  - 'salt/beacon/*/load/critical':
    - /srv/reactor/high_load_alert.sls # Path to the reaction SLS file

  # Tag matches events from the 'service' beacon when nginx stops
  - 'salt/beacon/*/service/nginx/stopped':
    - /srv/reactor/restart_nginx.sls

(Requires salt-master restart)

Reaction SLS File (/srv/reactor/restart_nginx.sls): Looks like an Orchestration file, uses salt.runner or salt.wheel or salt.local to trigger actions.

YAML
# /srv/reactor/restart_nginx.sls
{% set minion_id = data['id'] %} {# Get minion ID from the event data #}

restart_nginx_service:
  salt.local: # Run command targeting the minion that sent the event
    - tgt: {{ minion_id }}
    - fun: service.start # Function to execute
    - arg:
      - nginx

Beacons and Reactors allow Salt to automatically respond to infrastructure events, enabling self-healing, auto-scaling, and other advanced automation patterns.

Agentless Control: Introduction to Salt SSH

While Salt’s primary mode uses the Master-Minion architecture with ZeroMQ, it also offers Salt SSH for agentless management.

  • How it Works: Instead of a persistent Minion agent and ZeroMQ, Salt SSH connects to target systems via standard SSH, copies over the necessary Salt components temporarily (salt-thin), executes the command or state, retrieves the results, and cleans up.

  • Use Cases:

    • Managing devices where installing a full Minion is difficult or impossible (network appliances, IoT devices).
    • Initial bootstrapping of systems before the Minion is installed.
    • Managing systems in environments with strict outbound firewall rules preventing Minion connections back to the Master.
    • Occasional management without needing a persistent agent.

Command: Uses salt-ssh instead of salt. Requires SSH access (key-based auth recommended) to the target.

Bash
 
# Requires target info in /etc/salt/roster file or passed via args
sudo salt-ssh my-ssh-target test.ping
sudo salt-ssh user@192.168.1.50 pkg.install tcpdump

# Apply states via Salt SSH
sudo salt-ssh my-ssh-target state.apply users

Roster File (/etc/salt/roster): Defines target systems and their SSH connection details.

YAML
 
# /etc/salt/roster Example
my-ssh-target:
  host: 10.0.0.1
  user: root
  priv: /root/.ssh/id_rsa # Path to private key

webserver_dmz:
  host: 192.168.100.10
  user: admin
  sudo: True # Use sudo for commands

Salt SSH provides flexibility but is generally slower than the standard ZeroMQ approach due to the overhead of SSH connections and temporary file transfers for each command.

Best Practices and Security Hardening

Deploying SaltStack effectively and securely requires attention to detail.

Securing the Salt Master and Minions

  • Master Security:
    • Firewall: Restrict access to ports 4505 and 4506 strictly to known Minion IP ranges.
    • Least Privilege: Run the salt-master process as a non-root user if possible (though often runs as root for broad system access).
    • Harden OS: Apply standard OS security hardening practices to the Master server.
    • Master Configuration: Review /etc/salt/master for security-sensitive settings (auto_accept, publish_port, ret_port, file_roots permissions). Avoid auto_accept: True in production.
    • Key Management: Carefully manage Minion keys using salt-key. Regularly audit accepted keys.
  • Minion Security:
    • Master Verification: Ensure Minions verify the Master’s public key (master_finger in /etc/salt/minion) to prevent man-in-the-middle attacks.
    • Harden OS: Secure the underlying operating system of the Minion.
  • Transport Security: Salt’s ZeroMQ communication is encrypted using AES encryption once keys are exchanged. Ensure your initial key exchange is secure.

Managing Pillar Data Securely (e.g., GPG, Vault Integration)

Storing secrets in plain text Pillar files (/srv/pillar/*.sls) is highly discouraged in production.

  • Salt’s GPG Renderer: Encrypt pillar files or individual values using GPG. Salt decrypts them on the Master before sending them securely to the targeted Minion.

    • Encrypt file: gpg -e -r <recipient_key_id> secrets.sls -> secrets.sls.gpg

SLS file using the renderer:

YAML
 
#!gpg # Tells Salt to use the GPG renderer

# Encrypted content goes here...
# -----BEGIN PGP MESSAGE-----
# ...
# -----END PGP MESSAGE-----
    • Requires GPG keys configured on the Salt Master.
  • HashiCorp Vault Integration (salt.modules.vault): Use the vault execution module (often via pillar.get‘s ext_pillar mechanism or directly in Jinja) to fetch secrets directly from a running Vault instance at runtime. This is a very common and secure pattern.

Configure ext_pillar in /etc/salt/master:

YAML
 
ext_pillar:
  - vault:
      url: https://vault.example.com:8200
      role_id: <salt_master_role_id> # AppRole auth recommended
      secret_id: <salt_master_secret_id>
      policies: ['salt-master-policy']
      path: secret/data/salt/{minion_id} # Example path structure
    • Access in Pillar/States: {{ pillar.get('my_db_password') }} (Value fetched from Vault path secret/data/salt/<minion_id>/my_db_password)
  • Other External Pillar Sources: Integrate with other secret management systems or databases.

Effective State Design Patterns

  • Formulas: Pre-written, reusable sets of states for common software (Apache, MySQL, Redis, etc.). Often found on GitHub (search “SaltStack Formula”). Use them as a starting point.
  • Modularity: Break down complex configurations into smaller, focused SLS files/components (as shown in organization section). Use include and require.
  • Idempotency: Ensure your states are truly idempotent. Test them thoroughly. Avoid cmd.run states where pkg, service, or file states would be more appropriate and inherently idempotent.
  • Use Pillar/Grains: Abstract variable data out of states and into Pillar or use Grains for system-specific facts.
  • Templating: Leverage Jinja effectively but avoid overly complex logic within templates; push complexity into custom modules if necessary.
  • Testing: Use state.apply for testing individual states. Consider tools like kitchen-salt (Test Kitchen integration) for automated testing of states in isolated environments.

Troubleshooting Common SaltStack Issues

  • Minion Not Connecting:
    • Check salt-minion service status on the minion.
    • Verify master: directive in /etc/salt/minion.
    • Check Master/Minion logs (/var/log/salt/*).
    • Check firewall rules (ports 4505, 4506) on Master and Minion.
    • Check DNS resolution if using hostnames.
    • Check salt-key -L on Master; is the key accepted, pending, or rejected?
  • Commands Not Working / Timeouts:
    • Increase timeout (-t <seconds>) on salt command: sudo salt -t 60 '*' test.ping.
    • Check Master/Minion logs for errors during execution.
    • Check system resources (CPU/RAM/Network) on Master and Minions.
  • States Failing:
    • Run with higher log level: sudo salt 'minion*' state.highstate -l debug or sudo salt-call state.highstate -l debug on the minion.
    • Examine the state return data carefully; it usually indicates which state ID failed and why.
    • Check YAML syntax (yamllint or salt '*' state.show_sls <state_name>).
    • Check Jinja syntax errors (salt '*' state.show_sls <state_name>).
    • Test individual state declarations using state.single: sudo salt 'minion*' state.single pkg.installed name=nginx.
  • Pillar Data Not Available:
    • Did you run sudo salt '*' saltutil.refresh_pillar?
    • Check /srv/pillar/top.sls targeting. Does the minion match?
    • Check YAML syntax in pillar SLS files.
    • Check Master logs for Pillar compilation errors.
    • Run sudo salt 'minion*' pillar.items to see what Pillar the minion is receiving.
  • Job Management:
    • See running/recent jobs: sudo salt-run jobs.list_jobs
    • Check the status/return of a specific Job ID (JID): sudo salt-run jobs.lookup_jid <jid>
Summary: Your SaltStack Journey Recap

Congratulations! You’ve journeyed through the core concepts and capabilities of SaltStack.

Key Concepts Revisited
  • Architecture: Master, Minions, ZeroMQ Event Bus.
  • Remote Execution: Running commands (salt '*' <module>.<func>).
  • Targeting: Selecting minions (Glob, List, Grain, Pillar, Compound, Nodegroups).
  • States (SLS): Declarative configuration management (YAML, state.apply, state.highstate).
  • top.sls: Mapping states and pillar to minions.
  • Requisites/Conditionals: Managing dependencies and flow (require, watch, onlyif).
  • Grains: Static facts from minions.
  • Pillar: Secure/variable data to minions.
  • Jinja: Templating for dynamic configurations.
  • Organization: Structuring states/pillar, using Environments.
  • Advanced: Orchestration, Beacons/Reactors, Salt SSH.
  • Security: Hardening, secure Pillar management.
Next Steps: Exploring Further and Community Resources

SaltStack is a deep and powerful tool. To continue learning:

  • Official SaltStack Documentation: The definitive source. (docs.saltproject.io)
  • SaltStack Community: Forums, Slack channel, IRC.
  • SaltStack Tutorials: Many online resources and blog posts cover specific use cases.
  • SaltStack Formulas: Explore pre-built states on GitHub.
  • Experiment: Set up a test environment and try things out! Build states for software you use regularly.
  • Custom Modules: Learn to write your own Execution or State modules in Python for ultimate flexibility.
  • Salt Cloud: Explore Salt’s capabilities for provisioning and managing cloud instances.
  • Testing: Investigate kitchen-salt or similar tools for testing your states.
Frequently Asked Questions (FAQs)
How does SaltStack compare to Ansible, Puppet, or Chef?
  • SaltStack: Python-based, very fast (ZeroMQ), strong remote execution & event-driven features, flexible (Python extensibility), moderately steep learning curve. Master/Minion (default) or Agentless (Salt SSH).
  • Ansible: Python-based, agentless (SSH), YAML-focused (Playbooks), gentler learning curve, large module library (“batteries included”), slower execution per task than Salt due to SSH overhead but simple setup.
  • Puppet: Ruby-based, agent-based (Master/Agent), strong modeling (DSL), mature ecosystem, enforces state rigorously, can have a steeper learning curve than Ansible.
  • Chef: Ruby-based, agent-based (Chef Server/Client) or Agentless (Chef Solo), uses Ruby DSL for “Recipes,” highly flexible, potentially the most complex of the four.

The “best” choice depends on your team’s skills (Python vs. Ruby), specific needs (speed vs. agentless simplicity vs. modeling strength), and existing infrastructure.

Can SaltStack manage cloud resources (AWS, Azure, GCP)?

Yes, primarily through Salt Cloud. Salt Cloud is a component/command (salt-cloud) that interfaces with cloud provider APIs to provision, query, and destroy virtual machines and associated resources (networks, storage). Once instances are provisioned, the Salt Minion can be automatically installed, and standard Salt states can manage their configuration.

What’s the best way to handle secrets management in Salt?

Avoid plain text in Pillar files for production. Recommended methods:

  1. HashiCorp Vault Integration: Use ext_pillar with the Vault module. Secrets are fetched dynamically and securely at runtime. This is often considered the best practice.
  2. GPG Renderer: Encrypt Pillar files or specific values using GPG keys managed on the Master. Salt decrypts them before sending to Minions.
  3. Other ext_pillar sources: Integrate with other secure datastores or KMS systems.
Is SaltStack suitable for small environments?

Yes. While SaltStack scales to tens of thousands of minions, it works perfectly well for managing just a handful of servers or even a single machine. The setup overhead is relatively low, and the benefits of declarative configuration and repeatable deployments apply regardless of size. Salt SSH can be particularly useful for very small setups where installing minions feels like overkill.

How can I debug failing Salt states effectively?
  • Increase Log Verbosity: Use -l debug or -l trace with state.apply, state.highstate, or salt-call. Check logs (/var/log/salt/master, /var/log/salt/minion).
  • Check YAML/Jinja Syntax: Use salt '*' state.show_sls <state_name> to see the compiled state structure before execution. Tools like yamllint can help.
  • Isolate the Problem: Use state.apply <state_name> to test individual SLS files. Use state.single <module.function> <args...> to test specific state declarations.
  • Check Dependencies: Ensure require and watch requisites are correctly defined.
  • Check Pillar/Grains: Verify the necessary data is available using pillar.items and grains.items. Remember to refresh pillar (saltutil.refresh_pillar).
  • Use test=True: Run states in dry-run mode: sudo salt '*' state.highstate test=True. This shows what would change without actually changing anything.
What is SaltStack Enterprise (formerly vRA SaltStack Config)?

SaltStack Enterprise, now part of the VMware Aria suite (as Aria Automation Config), is the commercial offering built on top of open-source Salt. It adds features like:

  • A graphical user interface (GUI) for management, reporting, and visualization.
  • Role-Based Access Control (RBAC) for granular permissions.
  • Enterprise-level support from VMware.
  • Compliance and auditing features.
  • Job scheduling and reporting enhancements.

It uses the same core Salt engine but provides an enterprise-friendly management layer.

Popular Courses

Leave a Comment