Quantcast
Channel: Planet Ubuntu
Viewing all articles
Browse latest Browse all 12025

Adam Stokes: Juju end to end deployment: Apache2+SSL, Gunicorn, Django, Postgresql

$
0
0

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
    - revision

The 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:
    pass

This 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: pgsql

The 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/jujuimgs

Deploy 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: started

In 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

Viewing all articles
Browse latest Browse all 12025

Trending Articles