20
May 13

Taking SaltStack to The Next Level With Pillars and Python States

This is a cross post from a post I wrote for the MLS Digital Dev Blog. -Justin

Introduction

For those of us who come from traditional sysops backgrounds, learning Salt means un-learning many of our shell scripting habits. Instead of linearly specifying the operations you need a server to perform like this (contrived example, the salt-minion package would ask to install python):

#!/bin/bash
apt-get update
apt-get install python 
apt-get install salt-minion
cp ~/temp/salt-minion-config /etc/salt/minion
chown root.root /etc/salt/minion
chmod 600 /etc/salt/minion
/etc/init.d/salt-minion restart

You instead specify a series of states that will be applied the server. States are evaluated independently from each other and may live in separate files. Each specifies its own dependencies and SaltStack will make sure they get applied in the correct order. The example above translates to something like:

python:
  pkg:
    - installed

salt-minion:
  pkg:
    - installed
    - require:
      - pkg: python

/etc/salt/minion:
  file:
    - managed
    - source: salt://salt/salt-minion-config
    - mode: 600
    - user: root
    - group: root
    - require:
      - pkg: salt-minion

salt-minion:
  service:
    - running
    - watch:
      - file: /etc/salt/minion
    - require:
      - pkg: salt-minion

Ok, so the Salt version is a tad longer. But even for this not-so-realistic example, there are several benefits:

  • In the shell script version, the minion will be restarted every time. In the Salt version, the minion is only restarted when the config file is changed.
  • It would require a lot more tricky shell script coding if we decided to split the installation of python into a separate file.
  • Where does the salt-minion-config file even come from? The shell script version will need it delivered to ~/temp somehow. Salt handles this for us.

Real Power

The real power of Salt starts to become apparent when you realize you can start adding flow control logic to your jinja states. Example to add users to a server:

{% for usr in 'Tom','Dick','Harry' %}
{{ usr }}:
  user:
    - present
    - fullname: {{ usr }}
    - home: True
    - shell: /bin/bash
    # group names for sudo very by platform
    - optional_groups:
      - admin
      - ubuntu
      - wheel
  ssh_auth:
    - present
    - user: {{ usr }} 
    - source: salt://users/keys/{{ usr }}_id_rsa.pub
    - require:
      - user: {{ usr }}
{% endfor %} 

Slick huh? We have a list of usernames and we auto-generate the rest of the jinja file to create each user’s corresponding state. We even use a simple naming convention to get their SSH keys out there!

This is a real example that we used here at MLS Digital to add users to all of our servers. It worked great, but as the team grew, the need to constantly be editing the user list became tedious. Furthermore, we started to find that we didn’t want every developer to have full access to every server.

The next level

We had just recently started using Salt Pillars to achieve better separation between data and code and we thought this would be a great application for pillars. Our first attempt resulted in this:

pillar/top.sls:

'prod_servers*':
  - produsers
'dev_servers*':
  - devusers

salt/top.sls:

'*_servers*':
  - sudoers

pillar/users/produsers.sls:

sudoers_users: ['Tom','Jane','Harry','Sue','Mike','Janet']

pillar/users/devusers.sls:

sudoers_users: ['Dick', 'Tom','Jane','Harry','Sue','Mike','Janet']

salt/users/sudoers.sls:

{% for usr in pillar['sudoers_users'] %}
{{ usr }}:
  user:
    - present
    - fullname: {{ usr }}
    - home: True
    - shell: /bin/bash
    # group names for sudo very by platform
    - optional_groups:
      - admin
      - ubuntu
      - wheel
  ssh_auth:
    - present
    - user: {{ usr }} 
    - source: salt://users/keys/{{ usr }}_id_rsa.pub
    - require:
      - user: {{ usr }}
{% endfor %} 

Jinja templates allow basic flow control structures, such as loops, conditionals, and macros. Pillars allow you to assign chunks of data (think strings and lists) scoped in the same way you scope the states themselves. Pulling the data out of the states allows you to create more dynamic states and achieve better re-usability across your code base. In this example, we are using the same basic sudoers state, but getting different behavior depending on the target server. In case you were wondering, Dick crashed production with some untested code and is on temporary production probation :)

Trouble in paradise

That’s pretty nice! But us OCD programmer types like to keep it DRY and seeing the produsers’ names listed twice makes us start twitching… uncontrollably.

The first thought was to try and aggregate the separate user lists in the pillars themselves. It looked something like:

pillar/users/produsers.sls:

sudoers_users: ['Tom','Jane','Harry','Sue','Mike','Janet']

pillar/users/devusers.sls:

{% if 'sudoers_users' in pillar %}
sudoers_users: pillar['sudoers_users'].extend(['Dick'])
{% else %}
sudoers_users: ['Dick']
{% endif %}

Unfortunately, that doesn’t work and several similar variations failed as well. The pillar constructs are simply not available to be used by the pillars themselves.

The even next-er level

After some research and some experimenting, we realized that between the limitations in the Pillar system and the minimal tools available in Jinja, that we would have to write our Python state. Oh, did I mention you can do that? Yes! All you have to do is prefix the sls file with #!py and Salt will execute everything inside the run() function. The only requirement is that you return a valid Salt high state data structure.

pillar/users/mls.sls:

users_add_mls: ['Tom','Jane','Harry','Sue','Mike','Janet']

pillar/users/probation.sls:

users_add_probation: ['Dick']

salt/users/sudoers.sls:

#!py

def run():
    '''
    Manage sudo enabled user adds/removes
    '''

    users_root = []
    [users_root.extend(value) for key,value in pillar.iteritems() if key.startswith('users_add')]

    generated_user_hsd = {}

    for user in users_root:
        generated_user_hsd[user] = {}

        generated_user_hsd[user]['user'] = [
            'present',
            {'fullname' : user},
            {'home' : True},
            {'shell' : '/bin/bash'},
            {'optional_groups' : ['admin','ubuntu','wheel']}
        ]
        generated_user_hsd[user]['ssh_auth'] = [
            'present',
            {'user' : user},
            {'source' : 'salt://users/keys/' + user + '_id_rsa.pub'},
            {'require' : [{'user': user}]}
        ]

    return generated_user_hsd

It certainly is refreshing to have the full power of Python available! Our new state is now smart enough to search for available pillars with users_add as part of their name, aggregate the results, and generate the correct high state data structure for Salt to process. This version above is very similar to what we currently run in production and it works quite well.

Wrapping it up

We started with shell scripting, progressed to generated jinja states, and ended up with custom python states. What fun! A small warning though. Writing a custom python state should be your last resort. There is a major tradeoff between readbility and keeping things DRY. We try to stick to jinja states unless the benefits of the custom code are significant. As you become more familar with Salt, you will start to get a feel for what level of complexity is required for various tasks.

Remember, you’re DevOps now, not some unix cowboy. This is code. You’re a programmer and all the best practice coding rules apply.

20
Jan 13

Be The Master Of Your Minions (An Introduction To Salt)

Happy new year fzysqr readers! What better way could we possibly celebrate the new year than by trying a new infrastructure automation system? Late last year, I began rolling out Salt a.k.a. SaltStack (for SEO help I’m told).

Continue reading →

28
Aug 12

Quick Tip – Wrong SSH Key Cached With Github After Changing Users and Keys

If you find that you are unable to access Github after switching out your private keys, you may have a key cached with ssh agent. To diagnose:

ssh git@github.com -v

Look for something along the lines of “debug1: Remote: Forced command: gerve someuser”. If you see this, you have a cached key. To fix:

killall ssh-agent
exec ssh-agent bash
ssh-key -D
ssh-key

You should see “All identifies removed” followed by “Identity added: //.ssh/id_rsa”. Now you should be able to successfully do:

ssh git@github.com
07
Apr 12

A Smarter FileBasedCache for Django

A quick follow up on my previous post about a few minor issues we recently experienced with the Django FileBasedCache. I mentioned that we commented out a line of code in the framework in order to get the application running correctly in production. Of course that wasn’t going to be the permanent fix.

Continue reading →

05
Apr 12

Update to VimRepressed

So apparently OSX Lion and Vim plugins implemented in python are not playing well together.

Calls to sys.stdout.write() and sys.stderr.write() appear to be crashing vim core. I say appear becauses it nearly impossible to debug. Rather than bang my head against the wall all night or do some brew compiling gymnastics, I just switched the stdout/stderr calls to use an even more pythonic style:

print >> sys.stderr, 'Blah'

Works great. Get your fresh code fixes here.

27
Mar 12

Django Cache and Burn

As part of our Amazon EC2 design, we decided to use S3 as our persistence layer; durability and easy storage growth for our data is very important to us. We knew that S3 would be to slow on its own for real time access. Luckily for us, the use profile of our application fits perfectly with a write-through cache strategy. Our users generally need a single dataset accessible for a week or two before they finish their analysis and move on, most likely never to access it again. Django’s file based cache seemed ideal. We could point it at the generous (and otherwise unused) instance storage on our EBS backed AMIs and cache several hundred gigabytes of data. If we lose a sever there would be performance penalty as the working dataset was built back up through a read-through strategy, but nothing unacceptable for a short period of time.

Continue reading →

15
Mar 12

A guide to hosting your HIPAA app in Amazon Web Services

In November, my team and I began an epic journey. We left our long-time homes in the bountiful lands of physical hosting in search of the mysical realm of “the clouds”. Cost of living in physical land was simply out of control.

Last weekend, we arrived.

Continue reading →

03
Mar 12

Use sed for quick and dirty templates in your deploy scripts

A few years ago, my team and I invested the effort to build a continuous integration environment. We use TeamCity and we are quite happy with it. Once you have a platform for automated builds, you quickly become interested in automated deploys. Embarking on the painful journey to get your software to “1-click” production deploys is one of the healthiest investments you can make in your dev infrastructure.

These days we are a ways past simple deploys. Our CI infrastructure runs tests, deploys code, migrates databases, deploys infrastructure, and even moves datasets around our environments. All this good automation is glued together with various build scripts–some in python, some in bash.

Continue reading →

13
Nov 11

node grinding the crack (between Windows and Linux)

Let me apologize up front for the terrible title. I have been recently inspired.

I met some fantastic people at the fantastic Keeping It Realtime Conference last week. The speakers were great. The conversations were fantastic. The parties were fun. I will go again next year. There was so much information to consume that I am just now sorting some of it out. In my mind, the experience plays backs like ten thousand snapshots in rapid succession. A blur of events, conversations, people, code, and crappy wifi. A couple of faces stand out: the crew from Microsoft, Paul Batum, Glenn Block, and of course Scott Hanselman. They left a big impression on me. Not because I was in shock that Microsoft was actually represented at this event (though I was), or that these guys were deep behind enemy lines (you should have seen the mac/win ratio), or because they were all wickedly smart (they were). No, it was that Microsoft seems to be genuinely making a go of supporting node on Windows and it actually looks pretty good.

Okay. So what does this have to do with base jumping wingsuit insanity? Well, dear reader, not much. But I’ll try and tie it back. If you browse back through my archives or follow me on twitter, you will get a lot of Linux and node content. But every so often, something Windowsy slips through. You see, the truth is: I am .NET developer. Or at least I used to be. Now, I manage a team of developers–a mixed team. We are about 50/50 .NET and Django with a sprinkle of node on top. Our various products have to integrate with each other and I make platform decisions on a daily basis where I am pitting a Windows stack against a Linux one. In short, I “grind the crack” between them. But without the wingsuit and inevitable spectacular death.

Back to the conference. At the opening event, I met Glenn and Paul. I had a great discussion with them about where my team is and where we are going. I like node and I see some great use cases for it in our business. At one point, Glenn asked me, “What would it take to you get you choose windows?” I answered that it was probably too late for us, “We have node programs running on Linux. Why would we switch?” I told him that I wholeheartedly supported node on Windows for the good of the node community, but that we were already past the point of needing it ourselves.

Fast forward two days. I make sure to attend Glenn’s and Tomasz’s talk on node for Windows (the main point behind the recent 0.6.X release by the way). I didn’t expect to hear anything I wasn’t already aware of. And I was surprised. Floored even. They have actually done a great job creating a Windows “story” for node. They want you to run node with IIS. If you do that, you get some pretty awesome stuff for free:

  • built-in process management ala forever
  • load balancing between node processes
  • graceful auto-refresh of the node process when code changes
  • remote node-inspector (hell yes!)
  • logs over http

Yes, I know you can get all these things on Linux. And, maybe there are arguments to be made about scaling (haven’t seen benchmarks, so I have no idea). But you know what? I don’t have scaling issues. We have a modest user base. Our biggest challenges are building a great experience for our users, integrating disparate systems, and maintaining our automated deployment and testing infrastructure. With iisnode, I can just include the node javascript along side our existing .NET app and ship it with the same TeamCity deployment. I won’t even need to restart IIS!

We were already planning on using node and WebSockets to bring some realtime features to our ASP.NET MVC app and Microsoft just made my life simpler. Glenn: you got me back (at least partly). I am excited to see what you guys do next.

Bravo.

24
Sep 11

Quick vim tip #1 (and I am not dead)

Sorry for the summer hiatus. Work, vacation, and mountain biking put a serious squeeze on my free time hacking and blogging activities. I have been working on some cool stuff lately and the cold weather is starting to arrive here in Spokane, so I should have plenty of blog posts coming up.

Anyway, quick vim tip #1. If I have a chunk of a code with a string:

var blah = "Some string that I want to replace" + previouslyDefinedVariable

I want to replace it with “new string”. In vim, do:

  1. Put your cursor on the first character inside the quotes of the string you want to end up with, in our case, the n from “new string”.
  2. Type yi” – yank inbetween quotes
  3. Navigate to the string you want to replace
  4. Type di” – delete inbetween quotes
  5. “0P – paste content from yank buffer
  6. Marvel at your vim prowess.

Got any great vim tips? I would love to here about them. Send them to vimtips@fzysqr.com. Check out my full vim config fork of scrooloose’s config on github (here)[https://github.com/jslatts/vimfiles].

Updated Cory Schmitt wrote in with a helpful suggestion that saves two keystrokes!

  1. Put your cursor on the first character inside the quotes of the string you want to end up with, in our case, the n from “new string”.
  2. Type yi” – yank inbetween quotes
  3. Navigate to the string you want to replace
  4. Type vi”p – visual mode select inbetween quotes and paste over
  5. Congratulate yourself for being awesome.