I'll be discussing the setup of the blog with this initial post, the main focus will be on the usage of SaltStack.

Our setup is going to consist of the following:

One Ubuntu droplet from DigitalOcean, or your favorite VPS provider.

Our static blog (Pelican) running through nginx and a virtualenv.

Saltstack pulling in our server configuration.

So basically the configuration is going to go like this:

  1. Spin up your droplet (I'm using Ubuntu, but the salt states I'm using should support multiple distros).
  2. Install salt.
  3. Use salt to perform the rest of the setup.
  4. From there it's all blog content related.

For the completed project head over to: https://github.com/gravyboat/hungryadmin-sls

Our first step is to get salt installed:

We'll be going through the steps here: http://docs.saltstack.com/topics/installation/ubuntu.html

For Ubuntu you may or may not need to install the following package, check if you have the 'add-apt-repository' command, I did not:

apt-get install python-software-properties

For me this still didn't provide it, so I had to run

apt-get install add-software-properties-common and I was all set to go.

Ok now I had the add-apt-repository command available I added the saltstack repo: add-apt-repository ppa:saltstack/salt

Since we're just going to have one server here, we're going to configure it as a masterless minion:

http://docs.saltstack.com/topics/tutorials/quickstart.html

So we'll start by installing just the salt-minion with:

apt-get install salt-minion

Now I like to run a quick test here to make sure things are working properly, so lets install nginx real quick.

If you're following along in the quickstart for the masterless minion, scroll down near the bottom and follow their creation with a few slight modifications (we'll update this later once we have the structure all fleshed out):

/srv/salt/top.sls:

base:
  '*':
    - webserver

/srv/salt/webserver.sls

install_nginx:
  pkg.installed:
    - name: nginx

Ok now that those are saved, just run salt-call --local state.highstate -l debug, it should look like the following:

local:
----------
    State: - pkg
    Name:      install_nginx
    Function:  installed
        Result:    True
        Comment:   Package nginx installed
        Changes:   libgd2: {'new': '1', 'old': ''}
                   httpd: {'new': '1', 'old': ''}
                   nginx-common: {'new': '1.2.6-1ubuntu3.2', 'old': ''}
                   nginx-full: {'new': '1.2.6-1ubuntu3.2', 'old': ''}
                   nginx: {'new': '1.2.6-1ubuntu3.2', 'old': ''}
                   libxslt1.1: {'new': '1.1.27-1ubuntu2', 'old': ''}
                   libjpeg-turbo8: {'new': '1.2.1-0ubuntu2', 'old': ''}
                   libgd2-noxpm: {'new': '2.0.36~rc1~dfsg-6.1ubuntu1', 'old': ''}
                   libjpeg8: {'new': '8c-2ubuntu7', 'old': ''}

Awesome, so now nginx is installed by salt.

Now there are a few things we're going to want to configure here, and a few items we'll have to modify later as we go through setting up the blog itself.

The first thing I want to do is install fail2ban. So lets create that sls:

/srv/salt/fail2ban.sls

install_fail2ban:
  pkg.installed:
    - name: fail2ban

and lets update our top.sls again so this gets included:

/srv/salt/top.sls:

base:
  '*':
    - webserver
    - fail2ban

Ok lets run our highstate again: salt-call --local state.highstate -l debug

And you should see output like this:

local:
----------
    State: - pkg
    Name:      install_fail2ban
    Function:  installed
        Result:    True
        Comment:   Package fail2ban installed
        Changes:   python2.7-pyinotify: {'new': '1', 'old': ''}
                   python-pyinotify: {'new': '0.9.3-1.1ubuntu1', 'old': ''}
                   fail2ban: {'new': '0.8.7.1-1', 'old': ''}

----------
    State: - pkg
    Name:      install_nginx
    Function:  installed
        Result:    True
        Comment:   Package nginx is already installed
        Changes:

great, now fail2ban will be installed, by default the service starts but let's make sure it does. Modify your /srv/salt/fail2ban.sls to look like this:

install_fail2ban:
  pkg.installed:
    - name: fail2ban

fail2ban_service:
  service.running:
    - name: fail2ban
    - watch:
      - pkg: install_fail2ban
      - file: /etc/fail2ban/fail2ban.conf
    - require:
      - pkg: install_fail2ban

So we'll get details back on our other items, but what we're focusing on is this:

----------
    State: - service
    Name:      fail2ban_service
    Function:  running
        Result:    False
        Comment:   The following requisites were not found:
                   watch: {'file': '/etc/fail2ban/fail2ban.conf'}

        Changes:

Now you can see the result here is 'False', does that mean things failed? Let's modify the fail2ban.conf and see. Odd, after adding a line to the fail2ban.conf file I still get the following:

----------
    State: - service
    Name:      fail2ban_service
    Function:  running
        Result:    False
        Comment:   The following requisites were not found:
                   watch: {'file': '/etc/fail2ban/fail2ban.conf'}

        Changes:

Ok lets modify our fail2ban.sls to just require the package, let's also add a require on the service to ensure it tries to start after fail2ban is installed. (this isn't required in 0.17 and forward since they process in order, but it's nice to have):

install_fail2ban:
  pkg.installed:
    - name: fail2ban

fail2ban_service:
  service.running:
    - name: fail2ban
    - watch:
      - pkg: install_fail2ban
    - require:
      - pkg: install_fail2ban

Now things are looking better:

----------
    State: - service
    Name:      fail2ban_service
    Function:  running
        Result:    True
        Comment:   The service fail2ban is already running
        Changes:

So why did this fail before? The reason it fails is because Salt doesn't understand that we want to modify the fail2ban.conf, because we didn't declare it inside of the fail2ban.sls. Imagine it like someone has handed you a stack of papers, each with a number on them. They then ask you to find a numbered paper to read them the details on, well they call out 7, and you sort through the stack of papers, but you don't have that paper! How can you provide details about something you don't possess or have in your hand? It's exactly the same with Salt, if you don't say 'hey this is the file, this is the content', and then tell it to watch that file for changes, it doesn't know what to do because it doesn't think the file exists! Since we don't have anything specific going on inside the fail2ban.conf, we aren't going to modify it.

What we DO need to modify however is the sshd_config file, so we can change the port, and disable root login for security purposes. So lets start by creating an ssh directory for Salt, we don't want to clog up our main directory, we'll move the other content as well, and change the naming scheme to better represent both the files, and to meet the requirements Salt has set.

First lets make some directories for our existing content, create the following:

mkdir /srv/salt/fail2banmkdir /srv/salt/nginxmkdir /srv/salt/ssh

Now move the files:

mv /srv/salt/fail2ban.sls /srv/salt/fail2ban/init.sls

mv /srv/salt/webserver.sls /srv/salt/nginx/init.sls

cp /etc/ssh/sshd_config /srv/salt/ssh/sshd_config

Now you're thinking to yourself 'woah woah woah, why did this guy change the file names to inits??'. The reasoning behind this is now that they're no longer in top level directories, we still want them to get applied, and the init just inherits the name of the directory, which is great for having a base file that would get configured everywhere.

So just to make sure we didn't break anything, let's run our highstate again:

salt-call --local state.highstate -l debug

local:
----------
    State: - pkg
    Name:      install_fail2ban
    Function:  installed
        Result:    True
        Comment:   Package fail2ban is already installed
        Changes:
----------
    State: - service
    Name:      fail2ban_service
    Function:  running
        Result:    True
        Comment:   The service fail2ban is already running
        Changes:

Wait a second where did nginx go? Remember how we moved webserver.sls to be init.sls in the nginx dir? Well we didn't update our top.sls, so lets do that now:

base:
  '*':
    - nginx
    - fail2ban

Lets run the highstate again:

local:
----------
    State: - pkg
    Name:      install_fail2ban
    Function:  installed
        Result:    True
        Comment:   Package fail2ban is already installed
        Changes:
----------
    State: - pkg
    Name:      install_nginx
    Function:  installed
        Result:    True
        Comment:   Package nginx is already installed
        Changes:
----------
    State: - service
    Name:      fail2ban_service
    Function:  running
        Result:    True
        Comment:   The service fail2ban is already running
        Changes:

Awesome, now things are looking a lot better! Lets move on to managing our sshd_config. I'm going to assume familiarity with the sshd_config, I've modified the default port, as well as the ability for root to login, modify whatever you want, and let's create our init.sls:

install_ssh:
  pkg.installed:
    - name: ssh

ssh_service:
  service.running:
    - enable: True
    - name: ssh
    - require:
      - pkg: install_ssh
    - watch:
      - file: sshd_config

sshd_config:
  file.managed:
    - name: /etc/ssh/sshd_config
    - source: salt://ssh/sshd_config
    - mode: '0644'
    - user: root
    - group: root
    - require:
      - pkg: install_ssh

Add our new ssh content to the top.sls:

base:
  '*':
    - nginx
    - fail2ban
    - ssh

Ok we've done quite a bit here. So we install the package, and ensure the service is running, and the requires are in place, and we're watching our sshd_config file. We also set up the sshd_config so that all our changes get applied properly. You'll notice that I've put single quotes around the mode, due to the way YAML is formatted, you can't have a leading 0 or it treats the value like a hexadecimal value, so just wrap it in single quotes. Let's see what our output looks like now:

local:
----------
    State: - pkg
    Name:      install_ssh
    Function:  installed
        Result:    True
        Comment:   Package ssh is already installed
        Changes:
----------
    State: - file
    Name:      sshd_config
    Function:  managed
        Result:    True
        Comment:   File /etc/ssh/sshd_config is in the correct state
        Changes:
----------
    State: - pkg
    Name:      install_fail2ban
    Function:  installed
        Result:    True
        Comment:   Package fail2ban is already installed
        Changes:
----------
    State: - pkg
    Name:      install_nginx
    Function:  installed
        Result:    True
        Comment:   Package nginx is already installed
        Changes:
----------
    State: - service
    Name:      fail2ban_service
    Function:  running
        Result:    True
        Comment:   The service fail2ban is already running
        Changes:
----------
    State: - service
    Name:      ssh_service
    Function:  running
        Result:    True
        Comment:   The service ssh is already running
        Changes:

Awesome, so everything seems to be going well, lets modify our /srv/salt/ssh/sshd_config for fun (I'm just going to add a comment), and re-run the highstate with salt-call --local state.highstate -l debug:

local:
----------
    State: - pkg
    Name:      install_ssh
    Function:  installed
        Result:    True
        Comment:   Package ssh is already installed
        Changes:
----------
    State: - file
    Name:      sshd_config
    Function:  managed
        Result:    True
        Comment:   File /etc/ssh/sshd_config updated
        Changes:   diff: ---
+++
@@ -15,6 +15,7 @@
 # Site-wide defaults for some commonly used options.  For a comprehensive
 # list of available options, their meanings and defaults, please see the
 # ssh_config(5) man page.
+#  test

 Host *
 #   ForwardAgent no


----------
    State: - pkg
    Name:      install_fail2ban
    Function:  installed
        Result:    True
        Comment:   Package fail2ban is already installed
        Changes:
----------
    State: - pkg
    Name:      install_nginx
    Function:  installed
        Result:    True
        Comment:   Package nginx is already installed
        Changes:
----------
    State: - service
    Name:      fail2ban_service
    Function:  running
        Result:    True
        Comment:   The service fail2ban is already running
        Changes:
----------
    State: - service
    Name:      ssh_service
    Function:  running
        Result:    True
        Comment:   Service restarted
        Changes:   ssh: True

You can see that we've added that comment line, and then the service was restarted because it's watching the sshd_config file, just like we wanted! Now modify that back, no reason to waste a comment line. Ok, so we've got ssh locked down in some fashion, nginx is installed, and we've installed fail2ban as well. We've already got Python installed, but we're missing things like virtualenv which are key.

Let's create /srv/salt/python/ so we can get Python and the other associated items configured (and we can show more cool Salt stuff). So we're going to start breaking things out here. Let's pretend for a second this isn't a single machine, but an environment. You wouldn't want to install setuptools on a machine that only needs python would you? No of course not, so we break out our /srv/salt/python/ directory into two files for right now, the first is /srv/salt/python/init.sls, it looks like this:

install_python:
  pkg.installed:
    - name: python

Super easy right? Just make sure python is installed.

Let's get pip installed as well, let's make another sls, /srv/salt/python/pip.sls. This may seem verbose, but for the time being it isn't a lot of work and we want to keep each item seperate. So create a pip.sls:

install_python_pip:
  pkg.installed:
    - name: python-pip

And modify the top.sls again:

base:
'*':
  - nginx
  - fail2ban
  - ssh
  - python.pip

Run our salt-call --local state.highstate -l debug again and we get this nice big wall of spam:

State: - pkg
Name:      install_python_pip
Function:  installed
    Result:    True
    Comment:   Package python-pip installed
    Changes:   build-essential: {'new': '11.6ubuntu4', 'old': ''}
               c++-compiler: {'new': '1', 'old': ''}
               libmpfr4: {'new': '3.1.1-1', 'old': ''}
               libppl-c4: {'new': '1.0-1ubuntu2', 'old': ''}
               libalgorithm-merge-perl: {'new': '0.08-2', 'old': ''}
               dpkg-dev: {'new': '1.16.10ubuntu1', 'old': ''}
               linux-libc-dev: {'new': '3.8.0-29.42', 'old': ''}
               cpp-4.7: {'new': '4.7.3-1ubuntu1', 'old': ''}
               libalgorithm-diff-xs-perl: {'new': '0.04-2build3', 'old': ''}
               gcc: {'new': '4:4.7.3-1ubuntu10', 'old': ''}
               make: {'new': '3.81-8.2ubuntu2', 'old': ''}
               libitm1: {'new': '4.7.3-1ubuntu1', 'old': ''}
               libquadmath0: {'new': '4.7.3-1ubuntu1', 'old': ''}
               libfile-fcntllock-perl: {'new': '0.14-2', 'old': ''}
               c-compiler: {'new': '1', 'old': ''}
               g++: {'new': '4:4.7.3-1ubuntu10', 'old': ''}
               libcloog-ppl1: {'new': '0.16.1-1', 'old': ''}
               libgcc-4.7-dev: {'new': '4.7.3-1ubuntu1', 'old': ''}
               libmpc2: {'new': '0.9-4build1', 'old': ''}
               libdpkg-perl: {'new': '1.16.10ubuntu1', 'old': ''}
               libstdc++-dev: {'new': '1', 'old': ''}
               libc6-dev: {'new': '2.17-0ubuntu5', 'old': ''}
               libstdc++6-4.7-dev: {'new': '4.7.3-1ubuntu1', 'old': ''}
               libc-dev-bin: {'new': '2.17-0ubuntu5', 'old': ''}
               manpages-dev: {'new': '3.44-0ubuntu1', 'old': ''}
               python-pip: {'new': '1.3.1-0ubuntu1', 'old': ''}
               libalgorithm-diff-perl: {'new': '1.19.02-3', 'old': ''}
               libppl12: {'new': '1.0-1ubuntu2', 'old': ''}
               gcc-4.7: {'new': '4.7.3-1ubuntu1', 'old': ''}
               linux-kernel-headers: {'new': '1', 'old': ''}
               patch: {'new': '2.6.1-3ubuntu2', 'old': ''}
               c++abi2-dev: {'new': '1', 'old': ''}
               fakeroot: {'new': '1.18.4-2ubuntu1', 'old': ''}
               libc-dev: {'new': '1', 'old': ''}
               cpp: {'new': '4:4.7.3-1ubuntu10', 'old': ''}
               g++-4.7: {'new': '4.7.3-1ubuntu1', 'old': ''}
               libgmpxx4ldbl: {'new': '2:5.0.5+dfsg-2ubuntu3', 'old': ''}

Great so pip is now installed on our server.

Ok so we've got pip installed, lets get virtualenv taken care of. This is just a copy of our pip.sls, so copy it over: cp /srv/salt/python/pip.sls /srv/salt/python/virtualenv.sls, it should look like this:

install_python_virtualenv:
  pkg.installed:
    - name: pyton-virtualenv

Let's modify our top.sls to look like this (add virtualenv, and get rid of pip for the time being):

base:
  '*':
    - nginx
    - fail2ban
    - ssh
    - python.virtualenv

Let's run it with salt-call --local state.highstate -l debug again:

  State: - pkg
  Name:      install_python_virtualenv
  Function:  installed
      Result:    True
      Comment:   The following packages were installed/updated: python-virtualenv.
      Changes:   python-virtualenv: { new : 1.9.1-0ubuntu1
old :
}

Next we want to install git, so create /srv/salt/git/init.sls (you'll need to create the directory), and we'll populate our file with the following:

install_git:
  pkg.installed:
    - name: git

Easy enough stuff, at some point we'll look at coming back to make this OS agnostic, but for now we don't want to get too crazy.

Now you might be thinking "Don't we need to add this to our top.sls?", well we're not going to worry about that, because we'll be making some drastic changes shortly.

Ok we have virtualenv installed, and git to pull down our content. So the next step is to add our project, let's make a new directory: /srv/salt/hungryadmin, and create app.sls. Now the reason we're doing this is we want items like python/virtualenv.sls, and ngingx/init.sls to just be our DEFAULT items, so you could apply it to any server in our environment (if we had more than one). From here we can extend things, so I could have multiple subdirectories (maybe I host multiple static blogs, or a code repo, or anything), that have different applications running in them. So lets set up our static blog in the app.sls:

{% set hungryadmin_venv = salt['pillar.get']('hungryadmin:venv') %}
{% set hungryadmin_proj = salt['pillar.get']('hungryadmin:proj') %}
{% set hungryadmin_user = salt['pillar.get']('hungryadmin:user') %}

include:
  - git
  - python.pip
  - python.virtualenv

hungryadmin_venv:
  virtualenv.managed:
    - name: {{ hungryadmin_venv }}
    - runas: {{ hungryadmin_user }}
    - require:
      - pkg: install_python_virtualenv

hungryadmin_git:
  git.latest:
    - name: https://github.com/gravyboat/hungryadmin.git
    - target: {{ hungryadmin_proj }}
    - runas: {{ hungryadmin_user }}
    - force: True
    - require:
      - pkg: install_git
      - virtualenv: hungryadmin_venv
    - watch_in:
        - service: nginx_service

refresh_pelican:
  cmd.run:
    - user: {{ hungryadmin_user }}
    - name: {{ hungryadmin_venv }}/bin/pelican -s {{hungryadmin_proj}}/pelicanconf.py
    - require:
      - virtualenv: hungryadmin_venv
    - watch:
      - git: hungryadmin_git

hungryadmin_pkgs:
  pip.installed:
    - bin_env {{ hungryadmin_venv }}
    - requirements: {{ hungryadmin_proj }}/requirements.txt
    - require:
      - git: hungryadmin_git
      - pkg: install_python_pip
      - virtualenv: hugryadmin_venv

Ok, so we've now got an app.sls that's going to take care of a lot of things. Now I know you're thinking "what is all this pillar crap that he's using?", well we are going to get to that in a minute, the key thing here is that you understand what each of these items do, it's pretty easy to tell right? for the hungryadmin_venv variable, it's clearly the location of our virtual environment, and our hungryadmin_user, is simply our user for the virtual environment. The only slightly confusing one here is hungryadmin_proj, but even that we can figure out. We know we're going to pull our git content into the virtual environment right? So we know it has something to do with that.

Next let's modify our top.sls so it looks like this:

base:
  '*':
    - nginx
    - fail2ban
    - ssh
    - hungryadmin.app

So why aren't we including git, or any of the python content any longer? Because we don't need to! We've already included them in the app.sls for hungryadmin, so there's no need to include them again. Now that we've modified the top.sls lets take care of those variables I had earlier. So those values (as you can see when I defined them) are pillar values. Now the best way to think of pillar data is really just global variables, it's the first thing that the Salt team state in the pillar docs, and it makes the most sense. So let's get the pillar data going. Create the following files:

/srv/pillar/top.sls /srv/pillar/hungryadmin.sls

and populate them with this data:

/srv/pillar/top.sls:

base:
  '*':
    - hungryadmin

/srv/pillar/hungryadmin.sls:

# hungryadmin environment settings

{% set hungryadmin_user = 'woody' %}
{% set hungryadmin_venv = '/home/{0}/hungryadmin'.format(hungryadmin_user) %}
{% set hungryadmin_proj = '{0}/site'.format(hungryadmin_venv) %}
{% set hungryadmin_url = 'hungryadmin.com' %}
{% set hungryadmin_root = '{0}/output'.format(hungryadmin_proj) %}

hungryadmin:
  user: {{ hungryadmin_user }}
  venv: {{ hungryadmin_venv }}
  proj: {{ hungryadmin_proj }}
  url: {{ hungryadmin_url }}
  root: {{ hungryadmin_root }}

OK so basically what we've just done is say 'hey for all servers, load in these pillar files', that happens in the top.sls. Then in the hungryadmin.sls, we set our variables, so we can reference them like hungryadmin_user which will return 'woody' and so on. If we wanted we could add another section for other items.

Now that we have this done, we need to tell Salt where to look for our pillar data. To do this edit the /etc/salt/minion (since we aren't using a master in this configuration), find the line that mentions pillar root:

#pillar_roots:
#base:
#  - /srv/pillar

and change it so it looks like:

pillar_roots:
  base:
    - /srv/pillar

Note that this is the default setting, I'm mentioning it here so you can take a look at the configuration file. Run the highstate again using salt-call --local state.highstate -l debug, and you should see everything get set up and configured. We create the virtual environment, and pull in out git repo. Now assuming we have our git repo hooked up properly you should be able to run a basic python server. I'm not going to get into the details here because we're mostly focusing on Salt. The only thing we have left to do for this is to hook up nginx so that it's actually serving up content properly, so let's get to it!

We're going to start by modifying our app.sls, then we'll update nginx.

for the app.sls:

{% set hungryadmin_venv = salt['pillar.get']('hungryadmin:venv') %}
{% set hungryadmin_proj = salt['pillar.get']('hungryadmin:proj') %}
{% set hungryadmin_user = salt['pillar.get']('hungryadmin:user') %}

include:
  - git
  - nginx
  - python.pip
  - python.virtualenv

{{ hungryadmin_user }}:
user.present:
  - shell: /bin/bash
  - home: /home/{{ hungryadmin_user }}
  - uid: 2150
  - gid: 2150
  - require:
    - group: {{ hungryadmin_user }}
group.present:
  - gid: 2150


hungryadmin_venv:
  virtualenv.managed:
    - name: {{ hungryadmin_venv }}
    - runas: {{ hungryadmin_user }}
    - require:
      - pkg: install_python_virtualenv
      - user: {{ hungryadmin_user }}

hungryadmin_git:
  git.latest:
    - name: https://github.com/gravyboat/hungryadmin.git
    - target: {{ hungryadmin_proj }}
    - runas: {{ hungryadmin_user }}
    - force: True
    - require:
      - pkg: install_git
      - virtualenv: hungryadmin_venv
    - watch_in:
      - service: nginx_service

hungryadmin_pkgs:
  pip.installed:
    - bin_env: {{ hungryadmin_venv }}
    - requirements: {{ hungryadmin_proj }}/requirements.txt
    - require:
      - git: hungryadmin_git
      - pkg: install_python_pip
      - virtualenv: hungryadmin_venv

hungryadmin_nginx_conf:
  file.managed:
    - name: /etc/nginx/conf.d/hungryadmin.conf
    - source: salt://hungryadmin/files/hungryadmin.conf
    - template: jinja
    - user: root
    - group: root
    - mode: 644
    - require:
      - git: hungryadmin_git
      - pkg: install_nginx
    - watch_in:
      - service: nginx_service

remove_default_sites_enabled:
  file.managed:
    - name: /etc/nginx/sites-enabled/default
    - watch_in:
      - service: nginx_service

We've now added our conf file for this host, but we need to write that conf file now, so create /srv/salt/hungryadmin/files, and then hungryadmin.conf inside of that. It's content's look like this:

server {

    listen [::]:80;

    server_name {{ salt['pillar.get']('hungryadmin:url') }};
    root {{ salt['pillar.get']('hungryadmin:root') }};

    location = / {
        # Instead of handling the index, just
        # rewrite / to /index.html
        rewrite ^ /index.html;
    }

    location / {
        # Serve a .gz version if it exists
        gzip_static on;
        # Try to serve the clean url version first
        try_files $uri.htm $uri.html $uri =404;
    }

    location = /favicon.ico {
        # This never changes, so don't let it expire
        expires max;
    }

    location ^~ /theme {
        # This content should very rarely, if ever, change
        expires 1y;
    }
}

Now we need to make sure the nginx_service items work correctly. We'll also do some nginx configuration changes to improve performance:

The nginx init should now look like this:

install_nginx:
 pkg.installed:
   - name: nginx

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

 nginx_config:
   file.managed:
     - name: /etc/nginx/nginx.conf
     - source: salt://nginx/files/nginx.conf
     - user: root
     - group: root
     - mode: 644
     - watch_in:
       - service: nginx_service

We'll also need to create the nginx.conf file:

user www-data;
worker_processes 4;
pid /run/nginx.pid;

events {
    worker_connections 768;
    # multi_accept on;
}

http {

    ##
    # Basic Settings
    ##

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    # server_tokens off;

    # server_names_hash_bucket_size 64;
    # server_name_in_redirect off;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    ##
    # Logging Settings
    ##

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    ##
    # Gzip Settings
    ##

    gzip on;
    gzip_disable "msie6";

    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 9;
    gzip_buffers 32 4k;
    gzip_types
        text/plain
        text/css
        text/js
        text/xml
        text/javascript
        application/javascript
        application/x-javascript
        application/json
        application/xml
        application/xml+rss;

    # gzip_http_version 1.1;
    # gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

    ##
    # nginx-naxsi config
    ##
    # Uncomment it if you installed nginx-naxsi
    ##

    #include /etc/nginx/naxsi_core.rules;

    ##
    # nginx-passenger config
    ##
    # Uncomment it if you installed nginx-passenger
    ##

    #passenger_root /usr;
    #passenger_ruby /usr/bin/ruby;

    ##
    # Virtual Host Configs
    ##

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Once these changes are complete run the highstate and everything should be set to go.

You should be able to visit the site if you modify your host file to point towards the IP address, nice job!

At this point we have our server configured for SSH access, as well as fail2ban, we've got all the required Python items installed for our static blog, we're pulling our content down from GitHub, and we've got nginx configured to serve the content!

We're pretty much done, depending on which blog tool you decide to use, it might be nice to extend how the virtualenv is run in the event it needs to be rebuilt, but I'm sure you're equipped to figure that out now! Lets look at how our directory structure turned out:

./pillar: hungryadmin.sls top.sls

./salt: fail2ban git hungryadmin nginx python ssh top.sls

./salt/fail2ban: init.sls

./salt/git: init.sls

./salt/hungryadmin: app.sls files

./salt/hungryadmin/files: hungryadmin.conf

./salt/nginx: init.sls files

./salt/python: init.sls pip.sls requirements.txt virtualenv.sls

./salt/ssh: init.sls sshd_config

Ok great, so we've not got the basics of a blog ready to go. All I have to do for my Pelican blog is create my posts, build it, and then push it to github. Then run Salt and my server is ready to go! I hope this helped you out!

Edit 2016-12-13: There have been some changes over the past couple of years, make sure to check the repo for the latest release of the code.