Goal of this document is to explain howChris Arges and I managed to get Apache w/SSL proxy to gunicorn which is serving up a django application with postgresql as the database and everything be deployable through Juju.
As an added bonus I'll also show you how to utilize Launchpad.net's SSO to enable authentication to your web application.
I will also break this document up into 2 parts with the first part concentrating on configuring your django application for deployment and the other doing the actual bootstrapping and deployment. Make sure you read the document in its entirety since they rely on each other to properly work.
Pre-requisites
- Ubuntu 12.04
- Juju 0.7
- You'll want to have an existing django project created which uses postgresql as the database backend. I won't go into details on setting that up since this focuses purely on deploying with Juju, but,Django documentation is excellent in getting up and running.
The rest of the necessary bits are handled by Juju.
The environment
In this document I use LXC as the containers for juju deployment. You could easily use AWS or any other supported cloud technologies. The directory layout for this tutorial looks like this:
- /home/adam/deployer
- djangoapp/
- settings.py
- manage.py
- urls.py
- mydjangoapp/
- models.py
- views.py
- urls.py
- charms/precise/django-deploy-charm
- hooks/
- files/
- templates/
- config.yaml
- metadata.yaml
- README
- revisionThe djangoapp directory houses my django application andcharms/precise/django-deploy-charm is my local charm that handles all of the Juju hooks necessary to get my environment up.
PART UNO: Configuring your Django application
Enable importing of database overrides from Juju
Edit settings.py and append the following:
# Import juju_settings.py for DB overrides in juju environments
try:
from djangoapp.juju_settings import *
except ImportError:
passThis will override the default database so you can choose to ignore the current databases stanza in the settings.py file.
Enable Django to properly prefix URLS if coming from an SSL Referer
This is the first part of telling Django to expect requests from the Apache reverse proxy over HTTPS.
Edit settings.py and add the following somewhere after the python import statements. (location of this is probably optional, but, im cautious so we add it at the beginning)
# Make sure that if we have an SSL referer in the headers that DJANGO
# prefixes all urls with HTTPS
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
Remember that added bonus? Go ahead and define the necessary Launchpad.net bits
For Django to work with Launchpad's SSO service you'll want to make sure that django-openid-auth is installed. This will be handled by Juju and I'll show you that in PART DOS.
Again edit settings.py to include the following OpenID configuration items.
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['lvh.me', 'localhost', 'yourfqdn.com']
For development purposes I usually use lvh.me to associate a fqdn with my loopback.
Add the authorization backend to the AUTHENTICATION_BACKENDS tuple:
# Add support for django-openid-auth
AUTHENTICATION_BACKENDS = (
'django_openid_auth.auth.OpenIDBackend',
'django.contrib.auth.backends.ModelBackend',
)Add some helpful options to associating Launchpad accounts with user accounts in your django app.
OPENID_CREATE_USERS = True
OPENID_UPDATE_DETAILS_FROM_SREG = True
OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/'
OPENID_USE_AS_ADMIN_LOGIN = True
# The launchpad teams and staff_teams were manually created at launchpad.net
OPENID_LAUNCHPAD_TEAMS_REQUIRED = [
'debugmonkeys',
'canonical',
]
OPENID_LAUNCHPAD_STAFF_TEAMS = (
'debugmonkeys',
)
OPENID_STRICT_USERNAMES = True
OPENID_USE_EMAIL_FOR_USERNAME = True
Set the LOGIN_URL path to where redirected users will go to login.
LOGIN_URL = '/openid/login/' LOGIN_REDIRECT_URL = '/'
Summary of Part UNO
This configuration will have your django application prepped for juju deployment with the ability to authenticate against launchpad.net and automatically associate the postgres database settings.
PART DOS: Configure and deploy your juju charm
Defining your config and metadata options
In config.yaml youll want to make sure the following options are defined and set:
options:
requirements:
type: string
default: "requirements.txt"
description: |
The relative path to the requirement file. Note that the charm
won't manually upgrade packages defined in this file.
instance_type:
default: "staging"
type: string
description: |
Selects if we're deploying to production or development.
production == deploying to prodstack
staging == local development (lxc/private cloud)
user_code_runner:
default: "webguy"
type: string
description: The user that runs the code
group_code_runner:
default: "webguy"
type: string
description: The group that runs the code
user_code_owner:
default: "webops_deploy"
type: string
description: The user that owns the code
group_code_owner:
default: "webops_deploy"
type: string
description: The group that owns the code
app_payload:
type: string
description: |
Filename to use to extract the actual django application.
This file must be in the files/ directory.
default: "djangoapp.tar.bz2"
web_app_admin:
type: string
description: Web application admin email
default: "webguy@example.com"
wsgi_wsgi_file:
type: string
description: "The name of the WSGI application."
default: "wsgi"
wsgi_worker_class:
type: string
default: "gevent"
description: "Gunicorn workers type. (eventlet|gevent|tornado)"In the metadata.yaml file we need to define the relation information, create that file with the following:
name: django-deploy-charm
maintainer: [Adam Stokes ]
summary: My Django project
description: |
Django website for My Django App
provides:
website:
interface: http
wsgi:
interface: wsgi
scope: container
requires:
db:
interface: pgsqlThe revision file keeps a positive integer to let Juju know that a new revision with changes are available. It is also recommended to add a README laying out the juju deploy steps for getting your charm up and running.
Write your charm hooks
This is where the magic happens, all charm hooks will reside in the hooks directory and should be executable.
A common include file
Rather than repeating the defining of variables over and over we'll just source it from a common include file. Create a file called common.sh and add the following:
#!/bin/bash
UNIT_NAME=$(echo $JUJU_UNIT_NAME | cut -d/ -f1)
UNIT_DIR=/srv/${UNIT_NAME}
DJANGO_APP_PAYLOAD=$(config-get app_payload)
INSTANCE_TYPE=$(config-get instance_type)
USER_CODE_RUNNER=$(config-get user_code_runner)
GROUP_CODE_RUNNER=$(config-get group_code_runner)
USER_CODE_OWNER=$(config-get user_code_owner)
GROUP_CODE_OWNER=$(config-get group_code_owner)
function ctrl_service {
# Check if there is an upstart or sysvinit service defined and issue the
# requested command if there is. This is used to control services in a
# friendly way when errexit is on.
service_name=$1
service_cmd=$2
( service --status-all 2>1 | grep -w $service_name ) && service $service_name $service_cmd
( initctl list 2>1 | grep -w $service_name ) && service $service_name $service_cmd
return 0
}The install hook
This hook handles the extracting, package installation, and permission settings.
#!/bin/bash
source ${CHARM_DIR}/hooks/common.sh
juju-log "Jujuing ${UNIT_NAME}"
###############################################################################
# Directory Structure
###############################################################################
function inflate {
juju-log "Creating directory structure"
mkdir -p ${UNIT_DIR}
}
###############################################################################
# User / Group permissions
###############################################################################
function set_perms {
juju-log "Setting permissions"
getent group ${GROUP_CODE_RUNNER} ${GROUP_CODE_OWNER} >> /dev/null
if [[ $? -eq 2 ]]; then
addgroup --quiet $GROUP_CODE_OWNER
addgroup --quiet $GROUP_CODE_RUNNER
fi
# Check if the users already exists and create a new user if it doesn't
if [[ ! `users` =~ ${USER_CODE_OWNER} ]]; then
adduser --quiet --system --disabled-password --ingroup \
${GROUP_CODE_OWNER} ${USER_CODE_OWNER}
fi
if [[ ! `users` =~ ${USER_CODE_RUNNER} ]]; then
adduser --quiet --system --disabled-password --ingroup \
${GROUP_CODE_RUNNER} ${USER_CODE_RUNNER}
fi
chown -R $USER_CODE_OWNER:$GROUP_CODE_OWNER ${UNIT_DIR}
usermod -G www-data ${GROUP_CODE_RUNNER}
}
###############################################################################
# Project Install
###############################################################################
function app_install {
tar -xf ${CHARM_DIR}/files/${DJANGO_APP_PAYLOAD} -C ${UNIT_DIR}
juju-log "Installing required packages."
# Additional supporting packages
/usr/bin/apt-add-repository -y ppa:gunicorn/ppa
# Common packages between instances
common_pkgs="python-pip python-dev build-essential libpq-dev python-django python-dateutil python-psycopg2 python-jinja2 pwgen ssl-cert gunicorn"
# Silence apt-get
export DEBIAN_FRONTEND=noninteractive
REQUIREMENTS=$(config-get requirements)
if [[ ${INSTANCE_TYPE} == 'production' ]]; then
apt-get -qq update
# Install required packages
apt-get -qq install -y python-amqplib python-anyjson \
python-bzrlib python-celery python-cherrypy \
python-django-celery python-django-openid-auth \
python-django-south python-launchpadlib python-oauth python-openid \
python-psycopg2 python-requests-oauthlib python-urllib3 python-salesforce \
python-cheetah ${common_pkgs}
else
apt-get -qq update
apt-get -qq install -y ${common_pkgs}
pip install -q -r ${UNIT_DIR}/${REQUIREMENTS} || true
fi
}
###############################################################################
# MAIN
# Steps
# -----
# 1) inflate - build directory stucture
# 2) app_install - install bits
# 3) set_perms - finalizes permission settings
###############################################################################
inflate
app_install
set_perms
One thing to notice in the app_install function is that we are extracting our django application from within the files/ directory. In order to make this work you'll want to manually tar up your django application and place it into that files directory.
# Make sure we are a level above the djangoapp directory $ cd /home/adam/deployer $ tar cjf charms/precise/seg-dashboard/files/djangoapp.tar.bz2 -C djangoapp .
The config-changed hook
This handles the configuring and populating of the django application. Here we are just concerned with symlinking the static assets from the django application.
###############################################################################
# WEB Application Config
# 1) Setup django application specific directory
# 2) Symlinks admin media directory
###############################################################################
# 1)
SETTINGS_PY="${UNIT_DIR}/settings.py"
# 2)
PYTHON_DJANGO_LIB=`python -c "import django; print(django.__path__[0])"`
mkdir -p /var/www/static
if [ ! -L /var/www/static/admin ]; then
ln -s ${PYTHON_DJANGO_LIB}/contrib/admin/static/admin /var/www/static/admin
fi
The db-relation-changed hook
This hook is where we define our Postgresql database settings to be included by the django application.
#!/bin/bash
# Update the juju_settings.py with the new database credentials
source ${CHARM_DIR}/hooks/common.sh
###############################################################################
# Export Database settings
###############################################################################
export DBHOST=`relation-get host`
export DBNAME=`relation-get database`
export DBUSER=`relation-get user`
export DBPASSWD=`relation-get password`
# All values are set together, so checking on a single value is enough
# If $db_user is not set, DB is still setting itself up, we exit awaiting
# next run.
[ -z "$DBUSER" ] && exit 0
cheetah fill --env -p templates/juju_settings.tmpl \
> ${UNIT_DIR}/juju_settings.py
# Setup database
python ${UNIT_DIR}/manage.py syncdb --noinput
# Create admin fixture
cheetah compile --env -p templates/juju_fixtures.tmpl \
> templates/juju_fixtures.py
python templates/juju_fixtures.py \> ${UNIT_DIR}/juju_fixtures.json
python ${UNIT_DIR}/manage.py loaddata ./juju_fixtures.json
juju-log "Updating database(${DBNAME}) credentials and importing fixtures"
ctrl_service gunicorn restart
As you can see we are processing a few templates to import into the django settings and load an admin fixture into the database.
The juju_settings file
This is the database configuration and should reside in the templates directory. Edit juju_settings.tmpl and populate with the following:
# Generated by db-relation-changed hook
# Pull in the project's default settings
from djangoapp.settings import *
# Overrite the database settings
DATABASES['default']['ENGINE'] = 'django.db.backends.postgresql_psycopg2'
DATABASES['default']['HOST'] = '${DBHOST}'
DATABASES['default']['NAME'] = '${DBNAME}'
DATABASES['default']['USER'] = '${DBUSER}'
DATABASES['default']['PASSWORD'] = '${DBPASSWD}'
The juju_fixtures file
Edit juju_fixtures.tmpl and add the following:
<%
import json
from subprocess import Popen, PIPE
def quickrun(cmd):
temp = Popen(cmd, stdout=PIPE).communicate()[0]
return temp.rstrip()
adminpasswd = quickrun(['pwgen', '-s', '64', '1'])
timestamp = quickrun(['date', '+%F %R'])
fixture = { "pk" : 1,
"model" : "auth.user",
"fields" : { "username" : "admin",
"password" : adminpasswd,
"email" : "",
"first_name" : "",
"last_name" : "",
"is_active" : True,
"is_superuser" : True,
"is_staff" : True,
"last_login" : "now",
"groups" : [],
"user_permissions" : [],
"date_joined" : timestamp
}
}
print json.dumps(fixture)
%>The website-relation-joined and website-relation-changed
The changed hook is just a symlink to website-relation-joined in this case. Edit your website-relation-joined file and add the following:
#!/bin/bash
unit_name=${JUJU_UNIT_NAME//\//-}
relation-set port=8080 hostname=`unit-get private-address`
relation-set all_services="
- {service_name: gunicorn, service_port: 8080}
"
We are making sure that apache will have access to the private IP and PORT of the gunicorn application server.
The wsgi-relation-changed and wsgi-relation-joined
Again the changed hook is symlinked to the joined hook. Edit wsgi-relation-joined and add the following:
#!/bin/bash
UNIT_NAME=`echo $JUJU_UNIT_NAME | cut -d/ -f1`
relation-set working_dir="/srv/${UNIT_NAME}/"
relation-set django_settings="${UNIT_DIR}/settings.py"
relation-set python_path=`python -c "import django; print(django.__path__[0])"`
variables="wsgi_wsgi_file wsgi_workers wsgi_worker_class wsgi_worker_connections wsgi_max_requests wsgi_timeout wsgi_backlog wsgi_keep_alive wsgi_extra wsgi_user wsgi_group wsgi_umask wsgi_log_file wsgi_log_level wsgi_access_logfile wsgi_access_logformat port"
declare -A VAR
for v in $variables;do
VAR[$v]=$(config-get $v)
if [ ! -z "${VAR[$v]}" ] ; then
relation-set "$v=${VAR[$v]}"
fi
done
juju-log "Set relation variables: ${VAR[@]}"
service gunicorn restart
Here the gunicorn charm expects a working_dir and a wsgi interface. These are set with the above relations and also a loop is provided if any other gunicorn options were to be overriden from the defaults provided in that charm.
The apache_vhost template
The apache2 virtualhost stanza that will ultimately provide the outside world access to your django application. Edit apache_vhost.tmpl and add the following:
# Managed by juju< VirtualHost *:80 >
ServerName {{ servername }}
Redirect permanent / https://{{ servername }}/< /VirtualHost >< VirtualHost {{ servername }}:443 >
ServerName {{ servername }}
ServerAdmin admin@example.com
CustomLog /var/log/djangoapp-custom.log combined
ErrorLog /var/log/djangoapp-error.log
SSLEngine on
SSLCertificateFile /etc/ssl/certs/ssl-cert-cts.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-cts.key
RequestHeader set X-FORWARDED-SSL "on"
# This ensures django is seeing the https protocol
# and prefixing all URLS with https
RequestHeader set X-FORWARDED_PROTO "https"
ProxyRequests off
ProxyPreserveHost on
Order Allow,Deny
Allow from All
ProxyPass / http://{{ djangoapp_gunicorn }}/
ProxyPassReverse / http://{{ djangoapp_gunicorn }}/
< /VirtualHost >The items such as SSL, Header modification, and Proxy support are loaded through the apache configuration charm which is discussed below.
Summary of the hooks
These hooks provide the groundwork for making the rest of the deployment possible. I realize some of the templates aren't making sense at the moment but read further to link the missing pieces.
The Juju environment setup
Once the hooks are done and youve compressed a tarball of your django application and have it sitting in your files/ directory it is time to bootstrap juju and get on our way to deploying. I am assuming no pre-existing juju setup exists. In the case that you have Juju defined for other things then skip the bootstrap.
Bootstrap your Juju environment
$ juju bootstrap
This will create a sample ~/.juju/environments.yaml file that you can alter. Mine looks like the following for an LXC setup.
environments:
sample:
type: local
control-bucket: juju-364887954bed48b590b9b6bd112a842a
admin-secret: fa8d276204ab4be4b3666cc5afe3bd21
default-series: precise
ssl-hostname-verification: true
data-dir: /home/adam/jujuimgsDeploy the application charm
From your toplevel directory, in my case /home/adam/deployer execute the following to get the django application deployed.
$ juju deploy --repository ./charms local:django-deploy-charm
Deploy the application server (gunicorn) and setup the relation between the application and application server
$ juju deploy gunicorn $ juju add-relation gunicorn django-deploy-charm
Deploy apache2 and add the relation to the application for reverse proxying to work
$ juju deploy apache2 $ juju add-relation apache2:reverseproxy django-deploy-charm
Configure the Apache2 charm to load our Virtual Host and auto generate the necessary certificates for SSL support
$ juju set apache2 "vhost_https_template=$(base64 < templates/apache_vhost.tmpl)" $ juju set apache2 "enable_modules=ssl proxy proxy_http proxy_connect rewrite headers" $ juju set apache2 "ssl_keylocation=ssl-cert-cts.key" $ juju set apache2 "ssl_certlocation=ssl-cert-cts.pem" $ juju set apache2 "ssl_cert=SELFSIGNED"
All of these options are explained in the README of the apache2 charm. But what this does is basically load a jinja2 supported template, enables the necessary modules in apache for proxy, ssl, and header modification support. Since we are doing a SELFSIGNED certificate for development and testing we set the filenames of the certificate and have the apache2 charm generate the certificates automatically.
Deploy postgresql and set up the database relation to our application
$ juju deploy postgresql $ juju add-relation django-deploy-charm:db postgresql:db
Expose our Apache2 service to the world
$ juju expose apache2
Tada
After about 5 or 10 minutes all the services should be deployed and you can get the public facing IP of the apache server by the following:
$ juju status apache2
For completeness this is what a fully deployed juju stack should look like:
machines:
0:
agent-state: running
dns-name: localhost
instance-id: local
instance-state: running
services:
apache2:
charm: cs:precise/apache2-11
exposed: true
relations:
reverseproxy:
- django-deploy-charm
units:
apache2/0:
agent-state: started
machine: 0
open-ports:
- 80/tcp
- 443/tcp
public-address: 10.0.3.218
gunicorn:
charm: cs:precise/gunicorn-7
relations:
wsgi-file:
- django-deploy-charm
subordinate: true
subordinate-to:
- django-deploy-charm
postgresql:
charm: cs:precise/postgresql-30
exposed: false
relations:
replication:
- postgresql
units:
postgresql/0:
agent-state: started
machine: 0
public-address: 10.0.3.119
django-deploy-charm:
charm: local:precise/django-deploy-charm-8
relations:
website:
- apache2
wsgi:
- gunicorn
units:
django-deploy-charm/2:
agent-state: started
machine: 0
public-address: 10.0.3.208
relations:
wsgi:
- gunicorn
subordinates:
gunicorn/2:
agent-state: startedIn this case if I go to https://10.0.3.218 it should bring up your custom django application. If youve setup authentication with Launchpad.net like in the above example visiting https://10.0.3.218/openid/login should redirect your to Launchpad's SSO service and allow you authenticate and redirect back to your django application.
Contributors welcomed!
If you would like to checkout the source for the charm itself you can see it on Github. Would love to make this charm general enough to give people a great starting point for setting up their environments. If modifications to the document are needed please post in the comments section and Ill get those implemented.
Things not done
- This tutorial doesn't cover how to setup static files as the static files live on the application server and not the apache server itself.
- I am aware there is a django charm that could easily be used in place of taring up your django application, it would be worth looking into that charm to further your deployment options.
- Not tested with Golang version of Juju since LXC support is not available yet