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 {log_format main '$remote_addr - $remote_user [$time_local] "$request" '
default 3000;
include /home/staging/conf/nginx-map.conf;
}
'$status $body_bytes_sent "$http_referer" '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.
'"$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;
}
nginx-map.conf:
master 3000;These are the port number each branch will be available at, with uwsgi.
feature-a 3001;
feature-b 3001;
Server configuration is then done as follows:
/etc/nginx/conf.d/default.d:
server {The branch variable gets its value from each request by extracting the first subdomain.
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;
}
}
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:
So, for the master branch the file created will be:
master.ini:
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_
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
Very helpful, thanks.
ReplyDeleteThis comment has been removed by a blog administrator.
ReplyDelete