Imagen Docker para aplicaciones Python/Flask

La suma de las partes
May 14, 2019 by
Bacalov, Damian
Imagen de Odoo y bloque de texto

Production Ready

En el artículo anterior vimos qué requisitos básicos debía cumplir una imagen Docker para ser considerada Production Ready.

Definidas las bases, te voy a mostrar cómo crear imágenes Docker Production Ready para tus aplicaciones Python/Flask.

Spoiler alert! Si no podés esperar y querés ver el producto final, te invito a ver directamente mi proyecto de Github docker-python-flask.



¿Cómo publicamos aplicaciones Python/Flask en producción? 

Antes de crear la imagen Docker, primero quería saber cuál es la forma recomendada hoy en día para publicar aplicaciones Python/Flask en servidores tradicionales. Recurrí a  DigitalOcean y hallé la dirección correcta: 

  1. Python/Flask

  2. Servidor uWSGI

  3. Servidor Web Nginx

  4. Unix Socks para comunicación entre Nginx y uWSGI

¡Perfecto! Ya sé lo que quiero; ahora necesito incluir todo esto en una imagen Docker respetando las características que hacen que un contenedor sea  ProductionReady 


¿Cómo incluimos todo esto en un contenedor Docker?

Las primeras pruebas estuvieron basadas en Gabriela Melo, Bradley Zhou y Scott Zelenka. Fue increíble ver mis aplicaciones dentro de contenedores por primera vez, sin embargo, el tamaño de las imágenes me pareció exagerado. 

Gabriela utiliza la imagen python:3.5 como base que pesa 922MB, Scott utiliza la imagen nginx:latest de unos 109MB (sobre el que debemos instalar Python), Bradley utiliza python:3.6-slim que tiene un tamaño de 138MB.


Alpine Linux, primer intento

El siguiente paso era evidente, reemplazar las imágenes base por Alpine:latest, la imagen recomendada para prácticamente todos nuestros contenedores. Sin embargo, al intentar ejecutar pip3 install uwsgi, se produce un error que me resultaba imposible solucionar.

Fernando Koff resolvió este problema instalando todos los paquetes necesarios para compilar uwsgi. No obstante, el resultado es una imagen más grande de lo deseado.


Alpine Linux, segundo intento

Todo indicaba que el camino correcto era utilizar el paquete uwsgi para Alpine, pero conseguir documentación fue una misión imposible. Por suerte, encontré a Roman Dodin, que me enseño cómo configurarlo.

Mi propuesta está basada en esta imagen, solo que con dos cambios. Primero, no estaba convencido de usar Supervisord, un gestor de servicios tan pesado como innecesario, creo yo, para lo que estaba buscando. Segundo, el código fuente de mi aplicación no incluye algunos archivos que realmente no tienen que ver con el desarrollo: nginx.conf, flask-site-nginx.conf, supervisord.conf y uwsgi.ini.

Yoan Blanc comparte una propuesta minimalista muy interesante, pero utiliza dos contenedores separados. Es muy probable que utilice sus ideas cuando despliegue mis aplicaciones en Kubernetes.


Uniendo todas las partes

A esta altura tenía una idea bien definida de cómo quería mis contenedores, sólo necesitaba unir las partes. La base que usé fue la imagen de Aleksander S., la más pequeña usando Alpine, Python3 y uWSGI a la que le agregué los paquetes nginx y bash.

Luego, en lugar de configurar nginx y el sitio web correspondiente, decidí simplemente sobreescribir el archivo /etc/nginx/conf.d/default.conf directamente desde el Dockerfile. De esta manera, no necesité un archivo extra en mi repositorio GIT.

Por último, elegí crear un script bash para iniciar los servicios tal como indica la página oficial de docker.com y no utilizar Supervisord ya que es un servicio bastante costoso en megabytes.


Aplicación Flask

Bien, tengo mi contenedor que es pequeño, no usa servidores de debugging y con pocos archivos extra en el repositorio GIT. Para que el contenedor sea realmente Production Ready, necesitamos dos características más:

  1. El código debe ser único para todos los ambientes

  2. Los desarrolladores no deben conocer las credenciales de producción

Para cumplir con la regla de código único alcanzaría con incluir, en el repositorio GIT, las configuraciones de todos los ambientes y elegir la correcta con una variable de entorno según el ambiente. Sin embargo esta estrategia va en contra de la segunda premisa ya que, al estar en el repositorio GIT, los desarrolladores conocerán las credenciales de todos los ambientes.

Para solucionar ambos puntos mi propuesta es la siguiente: en el archivo __init__.py de mi aplicación, pregunto por la existencia de una variable de entorno. Si existe, sobreescribe la configuración de la aplicación.

Ejemplo:


if "MODULE_NAME" in os.environ:

   app.config["MY_MODULE"].update(

       name=os.environ["MODULE_NAME"]

)



De este modo, el desarrollador solo conoce las credenciales del entorno de Desarrollo y esto es lo que queda en el repositorio GIT. Pero al iniciar el contenedor en otro ambiente, simplemente agregamos “-e MODULE_NAME=my_name” a comando docker run y la configuración será la apropiada.


El producto final

Les dejo tanto el repositorio GIT como la imagen en docker hub para que puedan revisarlo, probarlo y contarme qué les parece. ¡Hasta la próxima!