Django, el framework para perfeccionistas con deadlines …

Entradas

django vagrant ansible

Servidor de desarrollo Django con Vagrant y Ansible (parte 1)

Vagrant es una de esas herramientas que a priori parece que no tiene cabida en tu stack, pero una vez que comienzas a utilizarla se vuelve indispensable. En una entrada anterior vimos como hacer uso de la misma con el driver de Digital Ocean, en ese caso para desplegar Droplets, aunque el objetivo principal de esta herramienta es crear entornos de desarrollo aislados.

Vagrant permite que el abastecimiento de las máquinas a través de un script Bash o por medio de cualquier herramienta de orquestación tipo Puppet, Salt, Cheff o Ansible. En esta ocasión utilizaremos Ansible para el aprovisionar nuestro entorno.

En esta serie de artículos veremos como crear un servidor de desarrollo aislado para Django. Te preguntarás, ¿pero si con Python ya tenemos virtualenv? Si, pero ya sabemos que virtualenv no es perfecto y en muchas ocasiones no aísla todo lo bien que debería. Además Vagrant nos permite replicar el entorno de producción en local, con todas las ventajas que conlleva desarrollar en el entorno que nos vamos a encontrar en producción. En este ejemplo utilizaremos Ubuntu 16.04 cómo sistema operativo, Nginx de proxy inverso, Postgree como sistema gestor de base de datos y Gunicorn de servidor web.

Por qué Ansible

La diferencia principal entre Ansible y otras herramientas de orquestación, es que Ansible se comunica con el/los servidor/es vía SSH,  no como Puppet o Chef que necesitan tener instaladas dependencias en el/los servidor/es para poder ser utilizados.

La principal ventaja de Ansible es su simplicidad, las tareas se definen  en formato YAML, tiene una comunidad inmensa (cuenta más seguidores en Github que Salt, Puppet y Cheff juntos). Por si fuera poco está escrito en Python y permite el uso de plantillas Jinja 2 para generar ficheros de configuración.

Gracias a Ansible podremos replicar la máquina orquestada para desarrollo en producción sin demasiado coste adicional, dedicaré un artículo en el futuro para tratar esta problemática.

Requisitos

Antes que nada tenemos que tener instalado en nuestro equipo Vagrant y Ansible. Vagrant lo puedes descargar desde aquíAnsible lo puedes instalar con Brew o con PIP, tal como indican en la documentación oficial.

Vagrantfile

Comenzaremos creando un nuevo directorio para nuestro proyecto:

mkdir -p ~/Projects/ansible-vagrant-django
cd ~/Projects/ansible-vagrant-django

A continuación, ejecutaremos el comando Vagrant init para crear un nuevo Vagrantfile basado en Ubuntu 16.04:

vagrant init geerlingguy/ubuntu1604

Debe haberse generado un fichero llamado Vagrantfile en la raíz del directorio. Este contiene información básica sobre la máquina que queremos aprovisionar, y multiples comentarios, los eliminamos y dejamos el fichero así:

API_VERSION = "2"

Vagrant.configure(API_VERSION) do |config|
  config.vm.box = "geerlingguy/ubuntu1604"
end

Vamos a necesitar una manera de acceder a nuestro servidor web una vez que la máquina esté lista, así que le indicaremos a Vagrant que mapee el puerto 8000 de nuestro equipo al puerto 8000 de la maquina virtual. Para ello, agrega el siguiente código:

config.vm.network "forwarded_port", guest: 80, host: 8080
config.vm.network "public_network"

Para poder acceder vía SSH a nuestra máquina y que sincronice nuestro directorio de trabajo dentro de la máquina, necesitaremos añadir estás dos directivas:

config.ssh.forward_agent = true
config.vm.synced_folder "./", "/var/www/djangoproject"

Por último, indicaremos que queremos aprovisionar la máquina con Ansible, y dónde va a encontrar el fichero con los comandos de ansible:

config.vm.provision :ansible do |ansible|
  ansible.playbook = "provision/vagrant.yml"
end

Una vez añadidos todos los requerimientos nuestro Vagrantfile quedará así:

# -*- mode: ruby -*-
# vi: set ft=ruby :

API_VERSION = "2"

Vagrant.configure(API_VERSION) do |config|

  config.vm.box = "geerlingguy/ubuntu1604"
  
  config.vm.network "forwarded_port", guest: 8000, host: 8000
  config.vm.network "public_network"
  
  config.ssh.forward_agent = true
  config.vm.synced_folder "./", "/var/www/djangoproject"

  config.vm.provision "ansible" do |ansible|
    ansible.playbook = "provision/vagrant.yml"
  end
end

Playbooks de Ansible

Un Playbook de Ansible es un fichero que permite definir todas las tareas que se van a realizar en uno o varios hosts (servidores), en este caso en nuestro servidor de desarrollo.

Las tareas equivalen a uno o multiples comandos bash, cada una de las mismas hace uso de un modulo de Ansible. Los módulos son librerías que utiliza Ansible para controlar servicios, ficheros, paquetes o comandos, como por ejemplo apt, copy o service.

Veamos como escribir nuestro primer Playbook, lo primero que debemos hacer es crear un directorio provision y dentro de este un fichero denominado vagrant.yml:

mkdir provision
cd provision
touch vagrant.yml

Seguidamente añadimos a nuestro playbook los hosts en los que se van a ejecutar las tareas, en este caso como estamos haciendo uso de Vagrant indicaremos que puede ejecutar las tareas en todos los servidores conocidos y además con permisos de usuario root (become yes):

- hosts: all
  become: yes

Tareas de Ansible

Tal como hemos dicho, las tareas (Tasks) de Ansible realizan su función ejecutando un modulo, son ejecutadas en orden, una cada vez en cada uno de los nodos definidos en la directiva ‘hosts’ (en este caso sólo tenemos el nodo de nuestra máquina virtual).
Cada una de las tareas tienen como mínimo un nombre y el módulo que ejecutan. Por ejemplo, instalar python-pip sería tan sencillo como esto:

- name: Install python pip
  apt: name=python-pip

En este artículo, para evitar que se haga excesivamente extenso, solo crearemos la tarea “instalar los paquetes básicos”, dejaremos para la próxima entrada las tareas referentes a la configuración de PosgtgreeSQL, Nginx, Gunicorn y Django.
Dicha tarea quedaría tal que así:

- name: Install base packages
  apt: name={{ item }} update_cache=yes state=installed
  with_items:
    - build-essential
    - acl
    - ntp
    - htop
    - git
    - libpq-dev
    - python-dev
    - python-pip
    - python-pycurl
    - nginx
    - gunicorn

En esta tarea estamos ejecutando el modulo apt, le estamos indicando que actualice la caché y que el estado de final de la tarea sea “installed”. La directiva “with_items” permite que la tarea se ejecute para cada uno de los paquetes indicados.

Así luce nuestro playbook hasta el momento:

---
- hosts: all
  become: yes
  
  tasks: 
    - name: Install base packages
      apt: name={{ item }} update_cache=yes state=installed
      with_items:
        - build-essential
        - acl
        - ntp
        - htop
        - git
        - libpq-dev
        - python-dev
        - python-pip
        - python-pycurl
        - nginx
        - gunicorn

Si a continuación ejecutamos el comando vagrant up se “levantará” nuestra máquina con la “box” asignada, en este caso Ubuntu 16.04, y la configuración que hemos indicado en el vagrantfile.

Una vez terminado este proceso, y sólo si es la primera vez que se ejecuta, comenzará el aprovisionamiento de la máquina con nuestro playbook de Ansible. Si añadimos tareas a posteriori al playbook podemos volver a ejecutar el aprovisionamiento con el comando vagrant provision.

Resumen

En esta entrada hemos visto cómo configurar un servidor de desarrollo casi genérico usando Vagrant y Ansible. He dejado muchas cosas en el tintero, pero el post se estaba haciendo demasiado largo. En el siguiente entraré en materia en lo referente a la configuración del stack de Django y continuaré profundizando en lo que a Ansible se refiere, trateré temas como Roles, Handlers, Vars y demás.

Si te ha gustado la entrada valora y comparte en tus redes sociales. No dudes en comentar dudas, aportes o sugerencias, estaré encantado de responder.

Este artículo se distribuye bajo una Licencia Creative Commons Reconocimiento-CompartirIgual 4.0 Internacional (CC BY-SA 4.0)

licencia-cc

django-testing

Testing de modelos en Django, buenas prácticas.

El testing es una de las partes más importantes de cualquier proyecto de software, ya que aporta calidad y seguridad a nuestro código.  En esta entrada voy a tratar de exponer algunas prácticas muy recomendables para testear modelos en Django de forma eficiente y segura.

Leer más

django-users

Cómo extender el User de Django

El sistema de usuarios y autenticación integrado en Django es muy completo,  lo podemos adaptar a la mayoria de los casos de uso, pero hay situaciones en las que no podemos utilizarlo “out-of-the-box”. Por ejemplo, si nuestro proyecto tuviese carácter social es probable que necesitásemos almacenar una pequeña biografía, la localización del usuario y alguna otra información de interés. En esta entrada veremos algunas estrategias para extender el User Model que nos proporciona Django según nos convenga.

Leer más

Django-rest

Optimizar Django REST Framework

Django REST Framework es una aplicación Django que permite desarrollar de forma simple APIs muy robustas. Sin embargo, los serializadores propios de la aplicación pueden ser un “kill performance” para nuestro sistema. La raíz del problema es el denominado “N+1 selects problem”, este se da al acceder a un objeto concreto y sus a N objetos relacionados, normalmente a través de claves foráneas. Con pocos datos, este problema no produce un deterioro significativo del rendimiento, pero cuando el número de objetos relacionados crece, el rendimiento del sistema cae en picado.
Para mejorar el rendimiento de DRF haremos uso de los infrautilizados métodos del ORM de Django, “select_related” y “prefetch_related”, lo que nos permitirá realizar un “eager loading” (carga ansiosa, o evaluación ansiosa) de la información.

Descripción del problema

Cuando construimos una vista de DRF a menudo incluimos los datos de más de una tabla relacionada, desafortunadamente desde que hacemos uso del serializador de DRF para dicha vista, corremos el riesgo de aplastar el rendimiento de nuestra API.

Veamos un ejemplo:

class CustomerSerializer(serializers.ModelSerializer):
    order_descriptions = serializers.StringRelatedField(many=True) 
    orders = OrderSerializer(many=True, read_only=True)

 

El serializador anterior hace lo siguiente:

  1. Recupera todos los “customers”, realiza una consulta a la base de datos.
  2. Para el primer “customer” obtenido, recupera sus “orders”. Realiza otra consulta a la base de datos.
  3. Para el segundo “customer” obtenido, recupera sus “orders”. Vuelve a realizar otra consulta a la base de datos.
  4. Se realizarán tantas consultas como “customers” tenga en la base de datos.

Desde que “OrderSerializer” tenga otra relación anidada las consultas aumentarán drásticamente. Para que nos hagamos una idea, una sitio web pequeño empieza a tener problemas a partir de las 50 peticiones a la base de datos.

Planteamiento de la solución

Nuestro enfoque para solucionar este problema se denomina evaluación ansiosa (eager loading). Esencialmente, lo que hacemos es indicar a Django que vamos a realizar la consulta repetidamente.

queryset = queryset.prefetch_related('orders')

 

Ahora el serializador anterior simplemente haría los siguiente:

  1. Obtendría todos los “customers”, solo haría dos peticiones a la base de datos, uno para recuperar los customers y otro para recuperar todos los orders relacionados.
  2. En esta ocasión para cada uno de los customers obtenidos no tendría que realizar consultas extras para obtener cada uno de sus orders relacionados.

Vamos a establecer un patrón común, para  ello cada vez que construyamos un serializador le añadiremos un método estático llamado “setup_eager_loading”:

class CustomerSerializer(serializers.ModelSerializer):
    orders = OrderSerializer(many=True, read_only=True)

    def setup_eager_loading(cls, queryset):
        queryset = queryset.prefetch_related('orders')
        return queryset

 

Cada vez que vayamos a usar el serializador llamaremos al metodo “setup_eager_loading” en el “Queryset” antes de evaluar el serializador:

customers = Customers.objects.all()
customer_qs = CustomerSerializer.setup_eager_loading(customers)  # Set up eager loading to avoid N+1 selects
post_data = CustomerSerializer(customer_qs, many=True).data

 

O, si tenemos un APIView o un ViewSet, lo podremos llamar en el metodo “get_queryset”:

def get_queryset(self):
    queryset = Customers.objects.all()
    # Set up eager loading to avoid N+1 selects
    queryset = self.get_serializer_class().setup_eager_loading(queryset)  
    return queryset

 

En resumen, el ORM de Django evalua ansiosamente los datos requeridos para obtener los “customers” y los “orders” relacionados y los ha almacena en caché. Acceder a los datos de dicha caché es prácticamente instantáneo en comparación con realizar una petición a la base de datos.

Veamos otro ejemplo, vamos a optimizar los problemas de rendimiento de un sitio web de organización de eventos. La estructura de datos sería la siguiente:

from django.contrib.auth.models import User

class Event:
    """ A single occasion that has many `attendees` from a number of organizations."""
    creator = models.ForeignKey(User)
    name = models.TextField()
    event_date = models.DateTimeField()

class Attendee:
    """ A party-goer who (usually) represents an `organization`, who may attend many `events`."""
    events = models.ManyToManyField(Event, related_name='attendees')
    organization = models.ForeignKey(Organization, null=True)

class Organization:
    name = models.TextField()

 

Nuestro serializador de eventos quedaría tal que así:

class EventSerializer(serializers.ModelSerializer):
    creator = serializers.StringRelatedField()
    attendees = AttendeeSerializer(many=True)
    unaffiliated_attendees = AttendeeSerializer(many=True)

    @staticmethod
    def setup_eager_loading(queryset):
        """ Perform necessary eager loading of data. """
        # select_related for "to-one" relationships
        queryset = queryset.select_related('creator')

        # prefetch_related for "to-many" relationships
        queryset = queryset.prefetch_related(
            'attendees',
            'attendees__organization')

        # Prefetch for subsets of relationships
        queryset = queryset.prefetch_related(
            Prefetch('unaffiliated_attendees', 
                queryset=Attendee.objects.filter(organization__isnull=True))
            )
        return queryset

 

Llamar a “setup_eager_loading” antes de usar “EventSerializer”, nos garantiza que sólo tendremos dos grandes consultas en lugar de N + 1 consultas pequeñas, con lo cual el rendimiento será drásticamente mejor.

Referencias: