Open source repositories at: github and bitbucket. Current project: erp4all

Friday, December 27, 2013

Nginx, uwsgi setup for multiple branch staging deployment

When developing a project it's advised to have different branches for different "features". Master branch should, ideally, be empty of direct commits, only merges of completed features. I want to have a setup where I can easily deploy all my development branches in a staging environment where everybody can inspect and test. The desired url scheme would be:
  • master.project-dev.tld
  • feature-a.project-dev.tld
  • feature-b.project-dev.tld
I came up with a setup with nginx and uwsgi's emperor mode to accomplish that.

Nginx

Nginx has a map module, with which you can map a variable's value to some other variable with another value. Values can be hardcoded in nginx.conf or can be loaded from a file. The map directive must be in the main part of nginx.conf.

nginx.conf:

user  nginx;worker_processes  1;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    map $branch $flaskport {
        default 3000;
        include /home/staging/conf/nginx-map.conf;
    }
             log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;
    #gzip  on;
    include /etc/nginx/conf.d/*.conf;
}
The file /home/staging/conf/nginx-map.conf has all the mappings of branch names to ports. When a new branch is created this file must be updated with a new entry.

nginx-map.conf:
master 3000;
feature-a 3001;
feature-b 3001;
These are the port number each branch will be available at, with uwsgi.
Server configuration is then done as follows:

/etc/nginx/conf.d/default.d:
server {
    listen 80;
    server_name  project-dev.tld;
    return       301 http://master.project-dev.tld$request_uri;
}
server {
    listen       80;
    server_name  ~^(?<branch>.+)\.project-dev\.tld$;
    access_log /home/staging/log/nginx-access.log;
    error_log /home/staging/log/nginx-error.log;
    location /static {
        alias /home/staging/project/$branch/flask-app/static;
        expires 1y;
        add_header Cache-Control "public";
    }
    location / {
        if (!-e /home/staging/project/$branch) {
            return 404;
        }
        uwsgi_pass 127.0.0.1:$flaskport;
        include uwsgi_params;
        proxy_pass_header       Server;
        proxy_set_header        Host            $http_host;
        proxy_set_header        X-Real-IP       $remote_addr;
        proxy_set_header        X-Scheme        $scheme;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
The branch variable gets its value from each request by extracting the first subdomain.
For the root location we check if the branch exists or else we return 404 error.
And we proxy to uwsgi listening to port flaskport. This variable is populated by the map directive. Static content is served from each branch's static folder. Everything else is standard.


Continuous Integration

When the CI tool finishes building and testing, it connects to the staging machine and runs the shell script deploy.sh with the branch name as argument. This script checks nginx-map.conf file for an entry with the specified branch and gets the port number. Pulls branch from git repository, creates or updates the virtualenv dependencies, and creates an ini file in ~/uwsgi directory.

deploy.sh:

#!/bin/bash
HOME_DIR=/home/staging
USER=staging
PROJECT_DIR=$HOME_DIR/project
if [ $# -lt 1 ]; then
    echo "Usage: deploy.sh <branch>"
    exit 1
fi
BRANCH=$PROJECT_DIR/$1
BRANCH_PORT=$(grep $1 $HOME_DIR/conf/nginx-map.conf|cut -d " " -f 2)
if [ -z $BRANCH_PORT ];then
    echo "No port found. Update nginx-map.conf"
    exit 1
fi
BRANCH_PORT=${BRANCH_PORT%?}
if [ ! -e $PROJECT_DIR/$1 ]; then
    mkdir $BRANCH
    cd $BRANCH
    git clone -b $1 git@github.com:ourteam/project.git .
else
    cd $BRANCH
    git reset --hard
    git pull origin $1
fi
if [ ! -e $HOME_DIR/venv ]; then
    virtualenv $HOME_DIR/venv
fi
source $HOME_DIR/venv/bin/activate
pip install -r requirements/requirements_staging.txt
cat << _EOF_ > $HOME_DIR/uwsgi/$1.ini
[uwsgi]
socket = 127.0.0.1:$BRANCH_PORT
master = 1
processes = 2
threads = 1
uid = $USER
gid = $USER
chdir = $BRANCH/flask-app
pp = $BRANCH/flask-app
logto = $HOME_DIR/log/$1.log
module = run
callable = app
virtualenv = $HOME_DIR/venv
_EOF_

So, for the master branch the file created will be:

master.ini:


[uwsgi]
socket = 127.0.0.1:3000
master = 1
processes = 2
threads = 1
uid = staging
gid = staging
chdir = /home/staging/project/master/flask-app
pp = /home/staging/project/master/flask-app
logto = /home/staging/log/master.log
module = run
callable = app
virtualenv = /home/staging/venv

Uwsgi

Starting uwsgi in emperor mode is now easy:
uwsgi --emperor /home/staging/uwsgi --logto /home/staging/log/emperor.log

This will start a master process checking the directory /home/staging/uwsgi for uwsgi configuration files. For every file it finds it will spawn a new uwsgi process with the settings provided by the file. If file is deleted it will stop the process, If it is updated (touched) it will reload the settings. If the process dies it will try to restart it. There are a lot of settings for emperor, controlling its behavior, uwsgi documentation is very helpful here.


DNS

Last but not least is DNS configuration. For all this to function properly your DNS record must have an entry for every branch created. Another way is to have a wildcard CNAME record. This will much whatever subdomain the request has. A proper set up for this would be:

@ IN A your.ip.address.here
* CNAME project-dev.tld.

Conclusion

The above procedure can be used for projects like django, flask and others. You can try it with  an example project I have created at github. It has a master branch and two feature branches, feature-a and feature-b. You can pull every branch with deploy.sh and test uwsgi and nginx setup. All files in this post and directory structure can be found at nginx-uwsgi-multiple repository.


Happy hacking,

Andreas Porevopoulos

2 comments: