Making Django and Rails Play Nice, Part 4: Nginx Conditionals and Passenger

Saturday, April 14, 2012, at 05:42AM

By Eric Richardson

I got distracted and never managed to post the last piece of my look at making Django and Rails play nice together for KPCC's new beta website. Part one looked at mapping generic relationships with MySQL views, part two at building interoperable sessions and part three at creating an interwoven caching model.

Once you've got all those pieces in place and have both applications happily running off the same data, though, you still need a way to route incoming requests to one or the other. That could be something that you handle in logic on the load balancer, but our setup instead left this as something to solve as the request came into the web server. Turns out, though, nginx made it all quite easy.

The Requirements

In order to declare our deployment a success, we needed the ability to gradually roll features and users from the existing Django site to the new Ruby on Rails frontend.

Some paths needed to be conditional based on a beta cookie, while others needed to only point to the Django site to pick up things that we hadn't yet implemented in Rails. Still others needed to always point to Rails, to allow ground-up development (such as [the new videos section) to exist only on the new site regardless of cookie status.

Nginx to the rescue!

It turns out that those requirements are exactly the sort of thing nginx is good at handling. While "www.scpr.org" is defined in a "server" block in the nginx config, deeper constructs such as conditionals have all the same rights to define settings related to how the request will be handled.

Early in the process of testing out whether this idea would work, I deployed nginx with Passenger to see whether Passenger's "experimental" wsgi support might work for hosting our Django app. It turned out to run quite well, which allowed me to know that all I needed to do in nginx was set root to either the Django or Rails apps and let Passenger handle the rest.

Defining a map

nginx has a nifty map construct that makes up the bulk of our conditional logic:

map $uri $test {
    ~^/$                1;
    ~^/assets/          2;
    default             0;
}

That takes $uri and tests it against the map, putting the result in $test. The tilde in front of the keys allows regular expressions to be used. Our simple test here will return one (can go either way, based on cookie status) when the user is visiting the homepage, two (Rails-only) if they're looking for something under /assets/, and zero (Django-only) if they're trying to get anything else. Our production map is a lot longer, but you get the idea.

Once we have that, we need to add the cookie test. This gets a little funkier. If $test2 contains the results of your cookie test, typically you would want to say something like:

if ( $test1 == 0 || ($test1 == 1 && $test2 == 0) ) {
    # django
} else {
    # rails
}

nginx, though, doesn't support testing multiple variables in a conditional. That meant we needed to combine the two:

# default to old site
set $site 0;
set $test2 "";

# possible match...  two tests are combined
if ($test = 1) {
   set $test2 A;
}

# map based on existence of a cookie
if ($cookie_scprbeta = "true") {
    # use new site
    set $test2 "${test2}B";
}

if ($test2 = "AB") {
     set $site 1;
}

if ($test = 2) {
     # always map to the new site
    set $site 1;
}

Default to site zero, but use site one if either a) we have a conditional match from the map and have the cookie set or b) we have a map match that is only on Rails.

From there we simply set our root based on the $site value:

if ($site = 0) {
        # run old site
        root /web/django_app/public/;
        passenger_min_instances 4;
}

if ($site = 1) {
        # run the new site
        root /web/rails_app/current/public/;
        rails_env "production";
        passenger_min_instances 4;
}

Ta-da! Seamless handoff in the same visit.