SaltStack on Your VPS Series
    Part 2 of 6

    States, Formulas & the SLS File Format

    Move from imperative commands to declarative configuration management with Salt state files, requisites, and Jinja2 templating.

    35 minutes

    The SLS File Format

    Salt state files use the .sls extension. They are YAML with optional Jinja2 templating:

    Basic state example
    nginx:
      pkg.installed:
        - name: nginx
    
      service.running:
        - name: nginx
        - enable: True
        - require:
          - pkg: nginx

    The state ID and the name argument don't have to match — the ID is just a label, name is what gets acted on.

    The top.sls File

    Maps which states apply to which minions:

    /srv/salt/top.sls
    base:
      '*':
        - common
    
      'web-*':
        - nginx
        - php
    
      'db-*':
        - mysql

    File Structure

    /srv/salt/
      top.sls
      common.sls
      nginx.sls
      php.sls
      mysql/
        init.sls        <- referenced as 'mysql'
        config.sls      <- referenced as 'mysql.config'
        replication.sls <- referenced as 'mysql.replication'

    Writing Your First Real State

    /srv/salt/common.sls
    common_packages:
      pkg.installed:
        - pkgs:
          - vim
          - curl
          - wget
          - htop
          - git
          - unzip
          - ufw
    
    ntp:
      pkg.installed:
        - name: chrony
      service.running:
        - name: chrony
        - enable: True
        - require:
          - pkg: ntp
    
    set_timezone:
      timezone.system:
        - name: UTC
    # Apply to all minions
    sudo salt '*' state.apply common
    
    # Or apply everything in top.sls
    sudo salt '*' state.highstate

    Core State Modules

    pkg — Package Management

    install_apache:
      pkg.installed:
        - name: apache2
    
    remove_telnet:
      pkg.removed:
        - name: telnet

    service — Service Management

    nginx_service:
      service.running:
        - name: nginx
        - enable: True
        - reload: True

    file — File and Directory Management

    nginx_config:
      file.managed:
        - name: /etc/nginx/nginx.conf
        - source: salt://nginx/files/nginx.conf
        - user: root
        - group: root
        - mode: '0644'
    
    web_root:
      file.directory:
        - name: /var/www/html
        - user: www-data
        - group: www-data
        - mode: '0755'
        - makedirs: True

    user and group — Account Management

    deploy_user:
      user.present:
        - name: deploy
        - uid: 5000
        - home: /home/deploy
        - shell: /bin/bash
        - groups:
          - deploy
          - www-data

    cmd — Running Commands (Use Sparingly)

    run_migrations:
      cmd.run:
        - name: php artisan migrate --force
        - cwd: /var/www/app
        - unless: test -f /var/www/app/.migrated

    Requisites: Controlling Execution Order

    # require: only runs if dependency succeeds
    nginx_service:
      service.running:
        - name: nginx
        - require:
          - pkg: install_nginx
    
    # watch: triggers when watched state changes
    nginx_reload_on_config:
      service.running:
        - name: nginx
        - watch:
          - file: nginx_config
    
    # onchanges: run only if the watched state made a change
    run_bundle_install:
      cmd.run:
        - name: bundle install
        - cwd: /var/www/app
        - onchanges:
          - file: gemfile_managed

    Jinja2 Templating

    {% set arch = grains['osarch'] %}
    
    install_packages:
      pkg.installed:
        - pkgs:
          - nginx
          - curl
          {% if arch == 'amd64' %}
          - libssl3
          {% endif %}
    
    app_config:
      file.managed:
        - name: /etc/myapp/config.ini
        - contents: |
            hostname={{ grains['fqdn'] }}
            ip={{ grains['ipv4'][0] }}
            env=production

    Custom Grains

    Define custom grains in /etc/salt/grains on a minion:

    roles:
      - webserver
      - loadbalancer
    environment: production
    datacenter: nyc1
    # Then target by custom grain
    salt -G 'role:webserver' state.apply nginx

    The Salt Fileserver

    State files can reference files stored on the master using the salt:// URI scheme:

    nginx_config:
      file.managed:
        - name: /etc/nginx/nginx.conf
        - source: salt://nginx/files/nginx.conf

    Testing States Before Applying

    # Dry-run — shows what Salt would do without making changes
    sudo salt 'web-01' state.apply nginx test=True
    
    # Apply a single state
    sudo salt 'web-01' state.apply nginx
    
    # Apply multiple states
    sudo salt 'web-01' state.apply nginx,php,common
    
    # Show verbose output
    sudo salt 'web-01' state.apply nginx -l debug

    Community Formulas

    Salt has community-maintained formulas on GitHub at github.com/saltstack-formulas:

    cd /srv
    git clone https://github.com/saltstack-formulas/nginx-formula.git

    Add it to your master config's file_roots:

    file_roots:
      base:
        - /srv/salt
        - /srv/nginx-formula

    What's Next

    You now understand how to write, structure, and apply Salt states. In Part 3, we provision cloud VMs using Salt Cloud with OpenStack, so Salt can both create your servers and configure them in a single workflow.