Django en la naturaleza: consejos para la supervivencia de la implementación

Antes de implementar su aplicación web Django en el mundo real, debe asegurarse de que su proyecto esté listo para producción. Es mejor comenzar a implementarlos antes. Le ahorra a usted y a su equipo mucho tiempo y dolor de cabeza. Aquí hay algunos puntos que aprendí a lo largo del camino:

  1. Utilice pipenv (o requirements.txt + venv). Confirme Pipefile y Pipefile.lock (o requirements.txt). Nombra tu venv.
  2. Tenga un guión de inicio rápido.
  3. Escribe pruebas. Usa el marco de prueba de Djangoe hipótesis.
  4. Usar entornoy direnv para administrar su configuración y cargar automáticamente sus variables de entorno.
  5. Asegúrese de que todos los desarrolladores realicen sus migraciones. Squash migraciones de vez en cuando. Reajústelos si es necesario. Diseñe su proyecto para migraciones más fluidas. Lea sobre migraciones.
  6. Utilice la integración continua. Proteja su rama maestra.
  7. Revise la lista de verificación de implementación oficial de Django.
  8. No administre su propio servidor, pero si debe hacerlo, use una estructura de directorio adecuada y use Supervisord, Gunicorn y NGINX.

Esta lista creció orgánicamente a medida que pasaba por el lanzamiento de nuestra primera aplicación Django, y no es exhaustiva. Pero creo que estos son algunos de los puntos más importantes que debe conocer.

Siga leyendo para una discusión sobre cada uno de los puntos.

Administre sus dependencias y entornos virtuales de la manera correcta

Usted y su equipo deben acordar una forma de administrar sus dependencias y entornos virtuales. Recomiendo usar pipenv, que es la nueva forma de administrar tanto sus entornos virtuales como sus dependencias, o usar el viejo enfoque de crear un venv y rastrear sus dependencias con un requirements.txtarchivo.

Usar este requirements.txtenfoque es propenso a errores humanos, ya que los desarrolladores tienden a olvidarse de actualizar la lista de paquetes. Esto no es un problema con pipenv, ya que actualiza automáticamente Pipefile. El inconveniente de pipenv es que no ha existido el tiempo suficiente. Aunque la Python Software Foundation lo recomienda oficialmente, es posible que tenga problemas para ejecutarlo en algunas máquinas. Personalmente, sigo usando el enfoque anterior, pero usaré pipenv para mi próximo proyecto.

Usando venv y requirements.txt

Si está utilizando Python ≥ 3.6 (debería serlo), simplemente puede crear uno con python -m venv ENV. Asegúrese de nombrar su entorno virtual (en lugar de usar .). A veces es necesario eliminar su entorno virtual y crearlo de nuevo. Lo hace más fácil. Además, debe agregar un ENVdirectorio a su. gitignorearchivo (prefiero el nombre ENV en lugar de venv , .env ,… ya que se destaca cuando es la carpeta del proyecto).

Para administrar las dependencias, cada desarrollador ejecuta pip freeze > requirements.txt cada vez que instala un paquete nuevo, lo agrega y lo confirma en el repositorio. Utilizarán pip install -r requirements.txtcada vez que extraigan del repositorio remoto.

Usando pipenv

Si usa pipenv , solo necesita agregar Pipfile y Pipfile.lock a su repositorio.

Tener un guión de inicio rápido

Esto ayuda a garantizar que sus desarrolladores dediquen el menor tiempo posible a trabajar en cosas que no están directamente relacionadas con su trabajo.

Esto no solo ahorra tiempo y dinero, sino que también asegura que todos estén trabajando en entornos similares (las mismas versiones de Python y pip, por ejemplo).

Por lo tanto, intente automatizar tantas tareas de configuración como sea posible.

Además, tener un script de construcción de un solo paso es de lo que se trata el segundo paso de Joel Test.

Aquí hay un pequeño script que utilizo, que les ahorra a mis desarrolladores bastantes pulsaciones de teclas:

#!/bin/bash python3.6 -m venv ENV source ENV/bin/activate pip install --upgrade pip pip install -r requirements.txt source .envrc python ./manage.py migrate python ./manage.py loaddata example-django/fixtures/quickstart.json python ./manage.py runserver

Escribir pruebas

Todo el mundo sabe que escribir exámenes es una buena práctica. Pero muchos lo pasan por alto, en aras de, creen, un desarrollo más rápido. NO LO HAGAS. Las pruebas son una necesidad absoluta cuando se trata de escribir software utilizado en producción, especialmente cuando se trabaja en equipo. La única forma de saber que la última actualización no rompió algo es con pruebas bien escritas. Además, es absolutamente necesario realizar pruebas para la integración y entrega continuas de su producto.

Django tiene un marco de prueba decente. También puede usar marcos de prueba basados ​​en propiedades como Hypothesis, que lo ayudan a escribir pruebas más cortas y matemáticamente rigurosas para su código. En muchos casos, escribir pruebas basadas en propiedades es más rápido. En la práctica, puede terminar utilizando estos dos marcos para escribir pruebas completas y fáciles de leer y escribir.

Usar variables de entorno para la configuración

Su archivo settings.py es donde almacena todas las configuraciones importantes del proyecto: la URL de su base de datos, rutas a los medios y carpetas estáticas, etc. Estos tendrán valores diferentes en su máquina de desarrollo y su servidor de producción. La mejor forma de abordar esto es utilizar variables de entorno. El primer paso es actualizar su settings.py para leer de las variables de entorno, usando environment :

import environ import os root = environ.Path(__file__) - 2 # two folders back (/a/b/ - 2 = /) env = environ.Env(DEBUG=(bool, False),) # set default values and casting GOOGLE_ANALYTICS_ID=env('GOOGLE_ANALYTICS_ID') SITE_DOMAIN = env('SITE_DOMAIN') SITE_ROOT = root() DEBUG = env('DEBUG') # False if not in os.environ DATABASES = { 'default': env.db(), # Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ } public_root = root.path('./public/') MEDIA_ROOT = public_root('media') MEDIA_URL = '/media/' STATIC_ROOT = public_root('static') STATIC_URL = '/static/' AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID') AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY') ..

Para evitar cargar sus envvars manualmente, configure direnv en sus máquinas de desarrollo y almacene los envvars en un .envrcarchivo en el directorio de su proyecto. Ahora, siempre que ingrese cda la carpeta de proyectos, las variables de entorno se cargan automáticamente. Agregue .envrca su repositorio (si todos los desarrolladores usan la misma configuración) y asegúrese de ejecutar direnv allowcada vez que haya un cambio en el .envrcarchivo.

No lo use direnven su servidor de producción. En su lugar, cree un archivo llamado .server.envrc , agréguelo .gitignorey coloque allí la configuración de producción. Ahora, cree un script, runinenv.shpara obtener automáticamente las variables de entorno .server.envrc, active el entorno virtual y ejecute el comando proporcionado. Verá cómo se usa en la siguiente sección. Así es como runinenv.shdebería verse (enlace a GitHub).

#!/bin/bash WORKING_DIR=/home/myuser/example.com/example-django cd ${WORKING_DIR} source .server.envrc source ENV/bin/activate exec [email protected]

Maneje sus migraciones correctamente

Las migraciones de Django son geniales, pero trabajar con ellas, especialmente en equipo, está lejos de ser sencillo.

Primero, debe asegurarse de que todos los desarrolladores confirmen los archivos de migración. Sí, es posible que termine teniendo conflictos, especialmente si trabaja con un equipo grande, pero es mejor que tener un esquema inconsistente.

In general, dealing with migrations is not that easy. You need to know what you are doing, and you need to follow some best practices to ensure a smooth workflow.

One thing that I figured is that it usually helps if you squash the migrations from time to time (e.g. on a weekly basis). This helps with reducing the number of files and the size of the dependency graph, which in turn leads to faster build time, and usually fewer (or easier to handle) conflicts.

Sometimes it’s easier to reset your migrations and make them anew, and sometimes you need to manually fix the conflicting migrations or merge them together. In general, dealing with migrations is a topic which deserves its own post, and there are some good reads on this topic:

  • Django migrations and how to manage conflicts
  • How to Reset Migrations

Moreover, how your project’s migrations end up depends on your project architecture, your models, and so on. This is especially important when your code-base grows, when you have multiple apps, or when you have complicated relations between models.

I highly recommend you to read this great post about scaling Django apps, which covers the topic pretty well. It also has a tiny, useful migration script.

Use Continuous Integration

The idea behind CI is simple: run tests as soon as new code is pushed.

Use a solution which integrates well with your version controlling platform. I ended up using CircleCI. CI is especially helpful when you work with multiple developers, since you can be sure their code passes all the tests before they send a pull request. Again, as you can see, it’s very important to have well covered tests in place. Moreover, make sure your master branch is protected.

Deployment checklist

Django’s official website provides a handy deployment checklist. It helps you ensure your project’s security and performance. Make sure you follow through those guidelines.

If you must manage your own server…

There are many good reasons to not manage your own server. Docker gives you more portability and security, and serverless architectures, such as AWS Lambda, can provide you with even more benefits for less money.

But there are cases where you need more control over your server (if you need more flexibility, if you have services cannot work with containerized apps — such as security monitoring agents, and so on).

Use a proper directory structure

The first thing to do is to use a proper folder structure. If all you want to serve on your server is the Django app, you can simple clone your repository and use that as your main directory. But that’s rarely the case: usually you also need to have some static pages (home page, contacts, …). They should be separate from your Django code base.

A good way to go about it is to create a parent repository, which has different parts of your project as submodules. Your Django developers work on a django repository, your designers work on the homepage repository, … and you integrate all of them in a repository:

example.com/ example-django/ homepage/

Use Supervisord, NGINX, and Gunicorn

Sure, manage runserver works, but only for a quick test. For anything serious, you need to use a proper application server. Gunicorn is the way to go.

Keep in mind that any application server is a long-running process. And you need to make sure that it keeps running, is automatically restarted after a server failure, logs errors properly, and so on. For this purpose, we use supervisord.

Supervisord needs a configuration file, in which we tell how we want our processes to run. And it is not limited to our application server. If we have other long-running processes (e.g. celery), we should defined them in /etc/supervisor/supervisord.conf. Here is an example (also on GitHub):

[supervisord] nodaemon=true logfile=supervisord.log [supervisorctl] [inet_http_server] port = 127.0.0.1:9001 [rpcinterface:supervisor] supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [program:web-1] command=/home/myuser/example.com/example-django/runinenv.sh gunicorn example.wsgi --workers 3 --reload --log-level debug --log-file gunicorn.log --bind=0.0.0.0:8000 autostart=true autorestart=true stopsignal=QUIT stdout_logfile=/var/log/example-django/web-1.log stderr_logfile=/var/log/example-django/web-1.error.log user=myuser directory=/home/myuser/example.com/example-django [program:celery-1] command=/home/myuser/example.com/example-django/runinenv.sh celery worker --app=example --loglevel=info autostart=true autorestart=true stopsignal=QUIT stdout_logfile=/var/log/example-django/celery-1.log stderr_logfile=/var/log/example-django/celery-1.error.log user=myuser directory=/home/myuser/example.com/example-django [program:beat-1] command=/home/myuser/example.com/example-django/runinenv.sh celery beat --app=example --loglevel=info autostart=true autorestart=true stopsignal=QUIT stdout_logfile=/var/log/example-django/beat-1.log stderr_logfile=/var/log/example-django/beat-1.error.log user=myuser directory=/home/myuser/example.com/example-django [group:example-django] programs=web-1,celery-1,beat-1

Note how we use runinenv.sh here (lines 14, 24 and 34). Also, pay attention to line 14, where we tell gunicorn to dispatch 3 workers. This number depends on the number of cores your server has. The recommended number of workers is: 2*number_of_cores + 1.

You also need a reverse proxy to connect your application server to the outside world. Just use NGINX, since it has a wide user base, and it’s very easy to configure (you can also find this code on GitHub):

server { server_name www.example.com; access_log /var/log/nginx/example.com.log; error_log /var/log/nginx/example.com.error.log debug; root /home/myuser/example.com/homepage; sendfile on; # if the uri is not found, look for index.html, else pass everthing to gunicorn location / { index index.html; try_files $uri $uri/ @gunicorn; } # Django media location /media { alias /home/myuser/example.com/example-django/public/media; # your Django project's media files } # Django static files location /static { alias /home/myuser/example.com/example-django/public/static; # your Django project's static files } location @gunicorn { proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; #proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_redirect off; proxy_pass //0.0.0.0:8000; } client_max_body_size 100M; listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { server_name example.com; listen 443 ssl; ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem; # managed by Certbot return 301 //www.example.com$request_uri; } server { if ($host = www.example.com) { return 301 //$host$request_uri; } # managed by Certbot if ($host = example.com) { return 301 //$host$request_uri; } # managed by Certbot listen 80 default_server; listen [::]:80 default_server; server_name example.com www.example.com; return 301 //$server_name$request_uri; }

Store the configuration file in /etc/nginx/sites-available, and create a symbolic link to it in /etc/nginx/sites-enabled.

I hope you’ve found this post helpful. Please let me know what you think about it, and show it some ❤ if you will.