Hace tiempo en la saga de tutoriales Django, explicaba como lanzar Django en producción, con Apache, WSGI y MySQL.

Hoy vamos a ver algún detalle más del deploy de Django 1.7 en (pro)ducción y (pre)producción sobre AWS con Ubuntu 14.04 (aunque para otros entornos linux debería ser similar), y como automatizarlo utilizando fabric.

Antes de nada, una pincelada sobre Fabric: Es una potente herramienta en python que nos permite gestionar operaciones locales o remotas en clusters de máquinas, facilitándonos el deploy de aplicaciones, la ejecución de tareas, o el inicio/finalización de servicios.

¿Que te vas a encontrar?

Subir Django a AWS

Esto es algo que ya he ido desgranando en anteriores posts, pero voy a enlazarte los pasos.

OJO: En los tutoriales que enlazo, configuraba un linux Debian en AWS, y hoy voy a trabajar con Ubuntu14.04. En general los pasos deberían ser los mismos. Como detalle, el usuario por defecto de Ubuntu es ubuntu, mientras que el de Debian es admin.

  • Configurar AWS
  • Subir proyecto Django a AWS: Aquí, introduzco una novedad, que luego aprovecharemos con fabric. En lugar de clonar 1 vez el repositorio, lo clono 2 veces (como miProyecto_pre y miProyecto_pro) . De forma análoga, creo también 2 virtualenvs (miProyecto_env_pre, myProyecto_env_pro)
  • Preparando el entorno de producción: de este tutorial, lo importante es la instalación y configuración de MySQL y sus dependencias, así como el backend de MySQL para Django, instalar Apache2 e instalar mod-wsgi (la configuración de virtual host la automatizaremos con Fabric)

Crear espacio SWAP

Un detalle interesante que no comenté en anteriores Posts es la configuración de un espacio de memoria SWAP (memoria en disco que ofrece soporte a Linux cuando va justo de RAM). Tener un espacio swap es una buena práctica que te evitará problemas cuando el SO se quede sin RAM.

Nos conectamos por SSH, y comprobamos si ya tenemos memoria swap.

ubuntu@ipAWS:~$ free -m

Si no tienes, crea una dándole una cantidad de espacio consecuente con la memoria disponible que tienes en disco (yo le daré 1GB):

ubuntu@ipAWS:~$ sudo fallocate -l 1G /swapfile
ubuntu@ipAWS:~$ sudo chmod 600 /swapfile
ubuntu@ipAWS:~$ sudo mkswap /swapfile
ubuntu@ipAWS:~$ sudo swapon /swapfile

Comprobamos de nuevo que se ha creado bien con free -m

Para acabar, imagino que quieres que este cambio se mantenga si tienes que reiniciar la máquina. para ello editamos el archivo fstab:

ubuntu@ipAWS:~$ sudo vim /etc/fstab

Y añadimos la siguiente linea al final:

/swapfile none swap sw 0 0

Crear archivos de Virtual Hosts

Aunque aún no lo vemos, aquí empieza la magia de fabric.

Sin fabric, para que nuestro servidor ejecute la aplicación django, crearíamos un archivo en /etc/apache2/sites-available con los paths de nuestro virtualenv, y demás, y cada vez que quisieramos realizar cambios en la configuración del servidor, lo haríamos a mano en dicho archivo.

En cambio, lo que vamos a hacer es crear en nuestro repositorio del proyecto un archivo de configuración de Virtual Host para cada entorno (PRE y PRO), y más adelante, vía fabric, nos encargaremos de que este archivo se actualice en /etc/apache2/sites-available.

Así que en mi propio ordenador, en la carpeta de mi proyecto Django, por ejemplo, creo una nueva carpeta que llamaré rsc, es decir, miProyecto/rsc.

Dentro de esta carpeta creo un archivo de VirtualHost para pre (miProyecto_pre.conf) y otro para pro (miProyecto_pro.conf), con el contenido:

<VirtualHost *:80>
    ServerName pre.miDominio.com
    ServerAdmin yo@miDominio.com

    LogLevel warn
    DocumentRoot /home/ubuntu/miProyecto_pre

    WSGIPassAuthorization On
    WSGIScriptAlias / /home/ubuntu/miProyecto_pre/miProyecto/wsgi.py
    WSGIDaemonProcess miProyecto_pre python-path=/home/ubuntu/miProyecto_pre:/home/ubuntu/virtualenvs/miProyecto_env_pre/lib/python2.7/site-packages
    WSGIProcessGroup miProyecto_pre

    #This solves an issue caused by Python 2.3, reported here https://code.google.com/p/modwsgi/wiki/ApplicationIssues#Python_Simplified_GIL_State_API 
    WSGIApplicationGroup %{GLOBAL}

    ErrorLog "/var/log/apache2/miProyecto_pre"
    CustomLog "/var/log/apache2/miProyecto_pre" common

    #Sirvo los archivos estaticos (y multimedia) directamente desde apache, sin pasar por django
    Alias /static /home/ubuntu/miProyecto_pre/static/
    Alias /media /home/ubuntu/miProyecto_pre/media/

    #Doy permisos de acceso de Apache al archivo wsgi.py
    <Directory /home/ubuntu/miProyecto_pre/miProyecto>
        <Files wsgi.py>
            Require all granted
        </Files>
    </Directory>

    #Doy permisos de acceso de Apache al directorio estatico    
    <Directory /home/ubuntu/miProyecto_pre/static>
        Options None
        AllowOverride None
        Order allow,deny
        Allow from all
        Require all granted
    </Directory>

    #Doy permisos de acceso de Apache al directorio media    
    <Directory /home/ubuntu/miProyecto_pre/media>
        Options None
        AllowOverride None
        Order allow,deny
        Allow from all
        Require all granted
    </Directory> 

</VirtualHost>

Evidentemente, en el de PRO, utilizaremos referencias a miProyecto_pro

OJO: Sería un detalle que te asegures de que todas los paths son correctos, y apuntan a donde tienen que apuntar, tanto respecto a tu proyecto Django, como a tu virtualenv.

 

DETALLE: Aunque podríamos tener todos los virtualhosts (entornos de pre y pro) en el mismo archivo, al establecerlos en archivos separados evitamos que en el futuro, si al cambiar la configuración del entorno de pre introducimos un error, falle también el entorno de pro al reiniciar apache.

Un settings.py para cada entorno

De forma similar a los archivos de settings acabarán teniendo configuraciones distintas para cada entorno.

Estructura del código

Para organizar un poco las cosas, creo una carpeta conf a la misma altura de mi fichero de settings actual (miProyecto/miProyecto/conf), donde creo un archivo de settings base, y otro para pre y pro:

miProyecto/
miProyecto/
conf/
__init__.py
base.py
pre.py
pro.py

base.py

En base.py pondré todas las variables comunes, o el valor por defecto, que luego sobreescribiré en los otros archivos. Quedará similar al siguiente archivo:

"""Common settings and globals."""

from os import environ
from os.path import abspath, basename, dirname, join, normpath
from sys import path
from django.core.exceptions import ImproperlyConfigured


def get_env_setting(setting):
    """ Get the environment setting or return exception """
    try:
        return environ[setting]
    except KeyError:
        error_msg = "Set the %s env variable" % setting
        raise ImproperlyConfigured(error_msg)


########## PATH CONFIGURATION
# Absolute filesystem path to the Django project directory:
DJANGO_ROOT = dirname(dirname(abspath(__file__)))
# Absolute filesystem path to the top-level project folder:
SITE_ROOT = dirname(DJANGO_ROOT)
# Site name:
SITE_NAME = basename(DJANGO_ROOT)
# Add our project to our pythonpath, this way we don't need to type our project
# name in our dotted import paths:
path.append(DJANGO_ROOT)
########## END PATH CONFIGURATION



########## SECRET CONFIGURATION
SECRET_KEY = 'this is secret!!!'
########## END SECRET CONFIGURATION


########## DEBUG CONFIGURATION
DEBUG = False
TEMPLATE_DEBUG = DEBUG
########## END DEBUG CONFIGURATION


########## HOST CONFIGURATION
ALLOWED_HOSTS = []
########## END HOST CONFIGURATION


########## APP CONFIGURATION & MIDDLEWARE
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'mi_proyecto',
)

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
########## END APP CONFIGURATION & MIDDLEWARE


########## URL CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf
ROOT_URLCONF = '%s.urls' % SITE_NAME
########## END URL CONFIGURATION


########## WSGI CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application
WSGI_APPLICATION = '%s.wsgi.application' % SITE_NAME
########## END WSGI CONFIGURATION


########## DATABASE CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.',
        'NAME': '',
        'USER': '',
        'PASSWORD': '',
        'HOST': '',
        'PORT': '',
    }
}
########## END DATABASE CONFIGURATION


########## INTERNATIONALIZATION CONFIGURATION
# sEE: https://docs.djangoproject.com/en/1.7/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = False
########## END INTERNATIONALIZATION CONFIGURATION


########## MEDIA FILE STORAGE CONFIGURATION
MEDIA_ROOT = normpath(join(SITE_ROOT, 'media'))
MEDIA_URL = '/media/'
########## END MEDIA FILE STORAGE CONFIGURATION


########## STATIC FILE CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root
STATIC_ROOT = normpath(join(SITE_ROOT, 'static/assets'))
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
STATIC_URL = '/static/assets/'
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
STATICFILES_DIRS = (
    normpath(join(SITE_ROOT, 'static')),
)
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
STATICFILES_FINDERS = (
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
)
########## END STATIC FILE CONFIGURATION


########## EMAIL CONFIGURATION
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = environ.get('EMAIL_HOST', 'smtp.mihost.com')
EMAIL_HOST_PASSWORD = environ.get('EMAIL_HOST_PASSWORD', 'pwd')
EMAIL_HOST_USER = environ.get('EMAIL_HOST_USER', 'admin@mihost.com')
EMAIL_PORT = environ.get('EMAIL_PORT', 587)
EMAIL_SUBJECT_PREFIX = '[%s] ' % SITE_NAME
EMAIL_USE_TLS = True
SERVER_EMAIL = EMAIL_HOST_USER
DEFAULT_FROM_EMAIL = SERVER_EMAIL
########## END EMAIL CONFIGURATION

pre.py

En las settings de pre, lo primero que haré es cargar las settings de base.py, y luego sobreescribiré lo que me parezca:

"""Pre-production settings and globals."""

from __future__ import absolute_import
from os import environ
from miProyecto.conf.base import *


DEBUG = True

########## HOST CONFIGURATION
ALLOWED_HOSTS = ['pre.miDominio.com',]
########## END HOST CONFIGURATION

########## DATABASE CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
'''
DATABASES = {
    'default': dj_database_url.parse(get_env_setting('DB_URL')),
}
'''
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'miproyecto_pre',
        'USER': 'miproyectoUser',
        'PASSWORD': 'pwd',
        'HOST': '127.0.0.1',
        'PORT': '3306',
    }
}
########## END DATABASE CONFIGURATION

pro.py

De forma análoga, en las settings de pro, cargo primero las de pre, y luego toco las que me parezcan (igual que en pre, de momento solo coto ALLOWED_HOSTS y la BBDD):

"""Production settings and globals."""

from __future__ import absolute_import
from os import environ
from miProyecto.conf.base import *


DEBUG = True

########## HOST CONFIGURATION
ALLOWED_HOSTS = ['miDominio.com',]
########## END HOST CONFIGURATION

########## DATABASE CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
'''
DATABASES = {
    'default': dj_database_url.parse(get_env_setting('DB_URL')),
}
'''
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'miproyecto_pro',
        'USER': 'miproyectoUser',
        'PASSWORD': 'pwd',
        'HOST': '127.0.0.1',
        'PORT': '3306',
    }
}
########## END DATABASE CONFIGURATION

Archivo wsgi.py

En este archivo no vamos a hacer nada especial, pero prefiero enseñar como lo tengo para evitar dudas.

import os

os.environ['DJANGO_SETTINGS_MODULE'] = "miProyecto.settings"
os.environ.setdefault("LANG", "en_US.UTF-8")
os.environ.setdefault("LC_ALL", "en_US.UTF-8")

#obtenemos la aplicacion
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()


# SSL settings
#os.environ['HTTPS'] = "on"

DETALLE: Vemos como defino las settings de Django con 1 único archivo, que no es ni base.py, ni pre.py ni pro.py. Gracias a fabric, crearé un enlace desde settings.py al archivo necesario, segun el entorno.

Instalar fabric

De nuevo en local, con nuestro entorno virtual activado, hacemos:

(miProyecto)$: pip install fabric
(miProyecto)$: pip freeze > requirements.txt

Instalar dnspython

Más adelante veremos como utilizo este módulo desde fabric. Lo importante es que lo necesitaré, así que:

(miProyecto)$: pip install dnspython
(miProyecto)$: pip freeze > requirements.txt

Script fabfile.py

La magia de fabric se escribe completamente en este archivo (fabfile.py), que tendremos al mismo nivel que el manage.py.

Una vez tenga el archivo creado, lo ejecutaré con una llamada tipo fab micomando -i path/A/mi/clave/aws.pem.

Concretamente, yo voy a encadenar varios comandos, con los cuales decidiré si trabajo en local o en remoto, el entorno de trabajo (pre/pro), y la acción a ejecutar, así que en realidad mi llamada tipo quedará así: fab remote pre deploy -i path/A/mi/clave/aws.pem.

Durante los próximos minutos voy a ir desgranando el contenido del fabfile.

Definición de variables

Añadiré las variables que necesite (paths, urls, etc) al objeto env:

from contextlib import contextmanager as _contextmanager
from fabric.api import *
from fabric.contrib.files import exists as remote_exists
from os.path import exists as local_exists
import dns.resolver

# Defaults
env.repo_url = 'ssh://hg@bitbucket.org/yo/miProyecto'
env.remote_logs_dir = '/var/log/miProyecto'
env.apache2_sites_available = '/etc/apache2/sites-available'
env.apache2_sites_enabled = '/etc/apache2/sites-enabled'
env.user = 'ubuntu'
env.hostnames = {
    'local': 'localhost',
    'pre': 'pre.miDominio.com',
    'pro': 'miDominio.com'
}
env.remote_project_dirs = {
    'local': '/home/ubuntu/miProyecto',
    'pre': '/home/ubuntu/miProyecto_pre',
    'pro': '/home/ubuntu/miProyecto_pro'
}
env.root_dir = '/home/ubuntu'
env.virtualenv_dir = '/home/ubuntu/.virtualenvs'

Como siempre, asegúrate de los paths que pones están bien 😉

Activar el virtualenv

Continuando con el mismo archivo, creo un método para activar el virtualenv:

@_contextmanager
def virtualenv():   
    with cd('%(virtualenv_dir)s/miProyecto_%(tag)s' % env):
        with prefix('/bin/bash %(virtualenv_dir)s/miProyecto_%(tag)s/bin/activate' % env):
            yield

Decidir el target

La verdad es que la idea de deployar en local no la estoy comentando, pero sería una posibilidad querer probar el servidor de producción en una máquina local, así que en fabric permito la opción:

# Targets
def localhost():
    """Deploy to current local server"""
    env.target = 'local'

def remote():
    """Deploy to remote server"""
    env.target = 'remote'

Configurar el entorno

# Configuration
def devel():
    """Set the target to devel."""
    require('target', provided_by=TARGETS)

    env.hosts = [env.hostnames['local']]
    env.tag = 'devel'
    env.branch = 'default'
    env.remote_project_dir = env.remote_project_dirs['local']
    env.remote_manager_dir = env.remote_project_dir
    env.remote_settings_dir = env.remote_project_dir + '/miProyecto'
    env.remote_staticfiles_dir = env.remote_project_dir + '/static'
    env.remote_media_dir = env.remote_project_dir + '/media'
    env.remote_resources_dir = env.remote_project_dir + '/rsc'

def pre():
    """Set the target to pre."""
    require('target', provided_by=TARGETS)

    env.tag = 'pre'
    env.branch = 'default'

    if env.target == 'local':
        env.hosts = [env.hostnames['local']]
        env.remote_project_dir = env.remote_project_dirs['local']
    elif env.target == 'remote':
        env.remote_project_dir = env.remote_project_dirs['pre']
        res = dns.resolver.query(env.hostnames['pre'])
        env.hosts = [x.address for x in res]
        print 'Deploy to %s -> %r' % (env.hostnames['pre'], env.hosts)

    env.remote_manager_dir = env.remote_project_dir
    env.remote_settings_dir = env.remote_project_dir + '/miProyecto'
    env.remote_staticfiles_dir = env.remote_project_dir + '/static'
    env.remote_media_dir = env.remote_project_dir + '/media'
    env.remote_resources_dir = env.remote_project_dir + '/rsc'

def pro():
    """Set the target to pro."""
    require('target', provided_by=TARGETS)

    env.tag = 'pro'
    env.branch = 'stable'

    if env.target == 'local':
        env.hosts = [env.hostnames['local']]
        env.remote_project_dir = env.remote_project_dirs['local']
    elif env.target == 'remote':
        env.remote_project_dir = env.remote_project_dirs['pro']
        res = dns.resolver.query(env.hostnames['pro'])
        env.hosts = [x.address for x in res]
        print 'Deploy to %s -> %r' % (env.hostnames['pro'], env.hosts)

    env.remote_manager_dir = env.remote_project_dir
    env.remote_settings_dir = env.remote_project_dir + '/miProyecto'
    env.remote_staticfiles_dir = env.remote_project_dir + '/static'
    env.remote_media_dir = env.remote_project_dir + '/media'
    env.remote_resources_dir = env.remote_project_dir + '/rsc'

#Variables
TARGETS = [localhost, remote]
CONFIGURATION = [devel, pre, pro]

DETALLE: Aquí es donde utilizamos el paquete dnspython que comentaba antes, para recuperar la dirección IP de la máquina a partir del dominio, con la función dns.resolver.query

Proceso de deploy:

Funciones que dependen del target

Creo una función a la que le paso el comando a ejecutar, y en función de si quiero trabajar en remoto o local, lo procesa de la forma adecuada. Del mismo modo, creo una función para comprobar si un directorio existe.

def my_run(runcmd):
    if env.target == 'remote':
        run(runcmd)
    elif env.target == 'local':
        local(runcmd, capture=False)

def my_exists(folder):
    if env.target == 'remote':
        return remote_exists(folder, use_sudo=False, verbose=False)
    elif env.target == 'local':
        return local_exists(folder)

Actualizar el repositorio

Creo un método interno para actualizar el repositorio en función del entorno (para pre utilizaré la rama default, mientras que para pro tendré una rama stable). Como se puede ver, si detecto que el repositorio no existe, lo clono.

# Internal Commands
def _update_repo():
    if env.tag == 'devel': #Update to  most actual version
        my_run('rm -Rf %(remote_project_dir)s' % env)
        my_run('cp -R ./../ %(remote_project_dir)s' % env)
    else:
        """Pull repository and update to desired revision."""
        if my_exists(env.remote_project_dir):
            my_run('cd %(remote_project_dir)s; hg pull -f %(repo_url)s;' % env)
        else:
            my_run('hg clone %(repo_url)s %(remote_project_dir)s;' % env)

        my_run('cd %(remote_project_dir)s; hg update -C %(branch)s' % env)

Actualizar requerimientos

Utilizo el comando pip install -r requirements.txt que ya hemos visto otras veces. En este caso, es importante destacar como activamos el entorno virtual.

def _update_requirements():
    """Update required Python libraries."""
    require('tag', provided_by=TARGETS)
    print 'Update Environment Requirements'
    with virtualenv():
        my_run('%(virtualenv_dir)s/miProyecto_%(tag)s/bin/pip install -r %(remote_project_dir)s/requirements.txt' % env)

Establecer el fichero de settings

Ya he comentado antes que esto iba a pasar. Como mi proyecto Django espera encontrar las settings en miProyecto/settings.py, lo que hago es crear un enlace simbólico para ese path, vinculado al archivo conf/pre.py, conf/pro.py o incluso conf/devel.py (este no lo hemos creado antes, pero seria el homólogo a pre.py si quisiéramos ejecutar en local).

Aprovechamos esta función para crear también el directorio de logs, y copiar los archivos de configuración de virtualhost al lugar apropiado de apache, y activarlos.

def _set_settings():
    # Fix symlinks to set the valid settings configuration
    my_run('cd %(remote_settings_dir)s; ln -s -f conf/%(tag)s.py settings.py' % env)

    # Create log dir and set up proper ownership
    my_run('sudo mkdir -p %(remote_logs_dir)s' % env)
    my_run('sudo chown -R www-data:www-data %(remote_logs_dir)s' % env)

    # Copy and set links for apache2 configuration
    my_run('cd %(remote_resources_dir)s; sudo cp miProyecto_%(tag)s.conf %(apache2_sites_available)s/111-miProyecto_%(tag)s.conf' % env)
    my_run('cd %(apache2_sites_enabled)s; sudo ln -sf %(apache2_sites_available)s/111-miProyecto_%(tag)s.conf' % env)

Copiar los archivos estáticos

Utilizo el comando manage.py collectstatic para crear todos los archivos estaticos. Antes, eso sí, creo el directorio si no existe y elimino su contenido si lo hubiera:

def _set_staticfiles():
    """Place static files in the correct destination to be served"""
    my_run('mkdir -p %(remote_staticfiles_dir)s' % env)
    my_run('rm -fR %(remote_staticfiles_dir)s/*' % env)
    with virtualenv():
        my_run('cd %(remote_manager_dir)s; %(virtualenv_dir)s/miProyecto_%(tag)s/bin/python manage.py collectstatic --noinput' % env)

    #Override user permissions again as previous command was launched as root
    my_run('sudo chown -R www-data:www-data %(remote_logs_dir)s' % env)

Inicializar la carpeta media

Creo la carpeta media si no existe, y le doy permisos al usuario bajo el cual se ejecuta Apache www-data, para que podamos subir ficheros desde nuestra aplicación Django.

def _make_media_dir():
    # Create media dir and set up proper ownership
    my_run('mkdir -p %(remote_media_dir)s' % env)
    my_run('sudo chown -R www-data:www-data %(remote_media_dir)s' % env)

Reiniciar Apache

def _restart_webservers():
    """Restart webserver on the server with init.d."""
    my_run('sudo service apache2 restart')

Migrar la base de datos

Como estamos trabajando con Django 1.7, solo necesitamos ejecutar manage.py migrate (realiza la migración, y además crea las tablas necesarias, como hacía anteriormente syncdb).

def _migrate_db():
    my_run('cd %(remote_manager_dir)s; %(virtualenv_dir)s/miProyecto_%(tag)s/bin/python manage.py migrate' % env)

La primera vez que lo ejecutemos en un entorno, nos pedirá crear el superusuario de nuestra app Django (si quieres evitar diálogos de este estilo, por el motivo que sea, puedes hacerlo pasando el parámetro –noinput).

DETALLE: La generación de archivos de migraciones (manage.py makemigrations), debe hacerse siempre en local, no en el servidor. Los archivos de migraciones resultantes, deben subirse al repositorio, para que solo con ejecutar manage.py migrate, ya se actualice la BBDD.

Comando deploy

Finalmente, tenemos la función deploy, que como vemos es una suma de los pasos anteriores:

def deploy():
    """Deploy the site doing upload, update and restart. Needs source and configuration."""
    require('target', provided_by=TARGETS)
    require('tag', provided_by=CONFIGURATION)
    require('hosts', provided_by=CONFIGURATION)

    _update_repo()
    _update_requirements()
    _set_settings()
    _set_staticfiles()
    _make_media_dir()

    _migrate_db()

    _restart_webservers()

Ejecutar script de deploy con fabric

Finalmente, para realizar el deploy en el servidor AWS remotamente, utilizaremos la clave privada que nos descargamos en el momento de crear el servidor, pasándosela al script, ya que se conectará por SSH.

No olvidemos un punto importante, y es que como he instalado fabric usando el entorno virtual, para utilizarlo deberé activar primero el virtualenv.

$:workon miProyecto

Para hacer el deploy del entorno de preproducción:

(miProyecto)$:fab remote pre deploy -i ~/.ssh/miClaveAWS.pem

Para hacer el deploy del entorno de producción:

(miProyecto)$:fab remote pro deploy -i ~/.ssh/miClaveAWS.pem