Building a Django App Server with Chef: Part 2

Alternate title: Actually doing something useful.

Yesterday we covered the basics to getting started with Chef. You should have a remote server configured with chef, and have curl installed! Now lets go ahead and get some useful bits for your Django application.

What we’ll need

So this is going to be based around the way that I set up my servers, so if this is different than you, I’m sorry. However, I think it is a pretty solid way of managing them. A lot of the ideas here are stolen from Travis when he set up the server for Pypants.

So lets assemble a list of things we’re going to want in order to get a super basic Django configuration running:

  • A user to run our code as and who’s home directory we’ll store the data.

  • A basic global python ecosystem, including setuptools and pip

  • A virtualenv to store all the project-specific packages and code in

  • A copy of the project that we’ll be running

Let’s get started.

The finished code for today is located on github, with the tag blog-post-2. It is a copy of the completed steps, so feel free to peek through that and come back here for clarification (or to ask questions).

Setting up our user

For RTD, I run everything under the user docs. So we’ll go ahead and set up that user so that we can get our site set up. We’re going to go ahead and replace our “default” recipe, because right now it isn’t doing anything much useful. The relevant part is below:

cookbooks/main/recipes/default.rb

node[:base_packages].each do |pkg|
    package pkg do
        :upgrade
    end
end

node[:users].each_pair do |username, info|
    group username do
       gid info[:id]
    end

    user username do
        comment info[:full_name]
        uid info[:id]
        gid info[:id]
        shell info[:disabled] ? "/sbin/nologin" : "/bin/bash"
        supports :manage_home => true
        home "/home/#{username}"
    end

    directory "/home/#{username}/.ssh" do
        owner username
        group username
        mode 0700
    end

    file "/home/#{username}/.ssh/authorized_keys" do
        owner username
        group username
        mode 0600
        content info[:key]
    end
end

node[:groups].each_pair do |name, info|
    group name do
        gid info[:gid]
        members info[:members]
    end
end

There’s a lot of stuff going on here, so lets go over it. First you might notice that there’s this node variable, the node data structure is the JSON that you have in your node.json file. It is looping over the keys and values with ruby’s each_pair and pair functions.

The base_packages bit is a cool example of the power of the chef configuration. We have a list of packages that we want to install in our Attributes, and we’re looping over them and setting using the package Resource.

I realize I skipped over the run_list part yesterday, but it basically is just a list of recipes to run. Each of the resources in the default.rb file should be pretty self explanatory. The Chef Resource Documentation is really comprehensive, and will probably be the most referenced document that you use. The main resource’s that we used were group, user, file, directory, let’s take a look at the User declaration in particular.

Everything there should be pretty obvious, as it’s the information that goes into /etc/passwd for the user. However, the supports keyword isn’t obvious at first. This is part of the Common Attributes that can be set on all Resources. It’s a way of passing along configuration options to the Resource. manage_home actually just makes it so that the users home directory is created when the user is created.

So we’re going to have to go ahead and put some data in there for it to work with. Our node.json will now look like this:

node.json

{
  "run_list": [ "main::default", "main::python", "main::readthedocs" ],
  "base_packages": ["git-core", "bash-completion"],

  "users": {
    "docs": {
      "id": 1001,
      "full_name": "Docs User",
      "key": "ssh-rsa key-goes-here eric@Bahamut"
    }
  },

  "groups": {
    "docs": {
      "gid": 201,
      "members": ["docs"]
    }
  }
}

Adding a Basic Python Environment

Now lets go ahead and add a python recipe to build out some basic python stuff that we’ll be needing.

cookbooks/main/recipes/python.rb

node[:ubuntu_python_packages].each do |pkg|
    package pkg do
        :upgrade
    end
end

# System-wide packages installed by pip.
# Careful here: most Python stuff should be in a virtualenv.

node[:pip_python_packages].each_pair do |pkg, version|
    execute "install-#{pkg}" do
        command "pip install #{pkg}==#{version}"
        not_if "[ `pip freeze | grep #{pkg} | cut -d'=' -f3` = '#{version}' ]"
    end
end

Additions to node.json

"ubuntu_python_packages": ["python-setuptools", "python-pip", "python-dev", "libpq-dev"],
"pip_python_packages": {"virtualenv": "1.5.1", "mercurial": "1.7"},

Here we’re adding some global packages that we need. We’re going to install setuptools and pip so that we can install further python packages. python-dev and libpq-dev are so that we have the headers for libraries that need to compile against postgres and python. We’ll also be installing virtualenv and mercurial globally so that we can create our virtualenv and install packages from mercurial.

Creating a virtualenv

We’re going to introduce the first new Chef concept here, which is called a Definition.

  • Definition (cookbooks/*/definitions/*.rb)

    A definition is a custom Resource that you build to abstract a set of operations. Pretty simple

This is a definition that Jacob published and then I updated to make the permissions correct. It allows you to set up a virtualenv:

cookbooks/main/definitions/virtualenv.rb

define :virtualenv, :action => :create, :owner => "root", :group => "root", :mode => 0755, :packages => {} do
    path = params[:path] ? params[:path] : params[:name]
    if params[:action] == :create
        # Manage the directory.
        directory path do
            owner params[:owner]
            group params[:group]
            mode params[:mode]
        end
        execute "create-virtualenv-#{path}" do
            user params[:owner]
            group params[:group]
            command "virtualenv #{path}"
            not_if "test -f #{path}/bin/python"
        end
        params[:packages].each_pair do |package, version|
            pip = "#{path}/bin/pip"
            execute "install-#{package}-#{path}" do
                user params[:owner]
                group params[:group]
                command "#{pip} install #{package}==#{version}"
                not_if "[ `#{pip} freeze | grep #{package} | cut -d'=' -f3` = '#{version}' ]"
            end
        end
    elsif params[:action] == :delete
        directory path do
            action :delete
            recursive true
        end
    end
end

As you can see, it takes a bunch of arguments, then just wraps up a bunch of Resource definitions in a nice little package. There is a little bit of magic with the pip freezing things, but it’s basically just how we’re checking to make sure that a package isn’t instead before we install it. We are using only using the directory and execute Resources here.

Now we’re going to use this virtualenv Definition, and create the home virtualenv for our site. I like to keep my virtualenv’s in ~/sites/<domain>, so this will go into /home/docs/sites/readthedocs.org/. Since this is becoming specific to the site we’re building, it’s going to go into a readthedocs recipe:

cookbooks/main/recipes/readthedocs.rb

directory "/home/docs/sites/" do
    owner "docs"
    group "docs"
    mode 0775
end

virtualenv "/home/docs/sites/readthedocs.org" do
    owner "docs"
    group "docs"
    mode 0775
end

This will set up a basic virtualenv in our directory.

Getting our site set up

To get our site set up, we need to pull in the source code, and make sure our virtualenv has all the requirements. This code is a little bit hacky, and could probably be abstracted out a bit, but it will work for now. We’re going to go ahead and add some things to our readthedocs Recipe.

Additions to cookbooks/main/recipes/readthedocs.rb

directory "/home/docs/sites/readthedocs.org/run" do
    owner "docs"
    group "docs"
    mode 0775
end

git "/home/docs/sites/readthedocs.org/checkouts/readthedocs.org" do
  repository "git://github.com/rtfd/readthedocs.org.git"
  reference "HEAD"
  user "docs"
  group "docs"
  action :sync
end

script "Install Requirements" do
  interpreter "bash"
  user "docs"
  group "docs"
  code <<-EOH
  /home/docs/sites/readthedocs.org/bin/pip install -r /home/docs/sites/readthedocs.org/checkouts/readthedocs.o
rg/deploy_requirements.txt
  EOH
end

I like to have my runtime files in the venv/run directory, so we’ll go ahead and create that directory. Then comes the fun part.

We are checking the Readthedocs source out of github with the git Resource. Chef only supports git and svn as far as I can tell, so luckily I’m using git.

Then we’re going to install from the pip requirements file. This is using the script Resource, which allows you to inline a bash, ruby, python, or more script inside your Recipe. This is using a hard coded bash script to install the requirements, which sucks, but will work for now.

Note: Chef appears to buffer output and not show itself as doing anything when running the script Resource here, so it will look like your build will hang while it installs your pip requirements file for the first time.

Done for now

Alright, this post has gotten long enough, so we’re done for today. But we’re in a pretty awesome spot, I think. We now have our app server set up with a runnable version of our code. You can go ssh in and play around, you should be able to run simple manage.py commands inside the virtualenv and whatnot (after a syncdb).

Tomorrow we’ll talk about deploying our code with Nginx and Gunicorn. I’ve been having trouble with Upstart, so we might switch our deployment to Supervisord, but we’ll see how it goes.

Don’t forget to check out the finished code on Github to see the actual running examples.



Hey there! I'm Eric and I work on communities in the world of software documentation. Feel free to email me if you have comments on this post!