Dockerización de aplicaciones de Python

Docker es una herramienta ampliamente aceptada y utilizada por las principales empresas de TI para crear, administrar y proteger sus aplicaciones. Los contenedores, como Docker, permiten a los desarrolladores...

Introducción

Estibador es una herramienta ampliamente aceptada y utilizada por las principales empresas de TI para construir, administrar y asegurar sus aplicaciones.

Los contenedores, como Docker, permiten a los desarrolladores aislar y ejecutar múltiples aplicaciones en un solo sistema operativo, en lugar de dedicar una Máquina virtual para cada aplicación en el servidor. El uso de estos contenedores más livianos conduce a costos más bajos, un mejor uso de los recursos y un mayor rendimiento.

If you're interested in reading more, you should take a look at Docker: una introducción de alto nivel.

En este artículo, escribiremos una aplicación web de Python simple usando Flask y la prepararemos para "dockerizar", luego crearemos una Imagen de Docker y la implementaremos en un entorno de prueba y producción.

Nota: Este tutorial asume que tiene Docker instalado en su máquina. Si no, puedes seguir la Guía de instalación de Docker oficial.

¿Qué es Docker?

Docker es una herramienta que permite a los desarrolladores enviar sus aplicaciones (junto con bibliotecas y otras dependencias), lo que garantiza que puedan ejecutarse exactamente con la misma configuración, independientemente del entorno en el que se implementen.

Esto se hace aislando las aplicaciones en contenedores individuales que, aunque separados por contenedores, comparten el sistema operativo y las bibliotecas adecuadas.

Docker se puede dividir en:

  • Motor acoplable – Una herramienta de empaquetado de software utilizada para contener aplicaciones.
  • Centro acoplable – Una herramienta para administrar tus aplicaciones de contenedores en la nube.

¿Por qué contenedores? {#por qué los contenedores}

Es importante entender la importancia y utilidad de los contenedores. Aunque es posible que no marquen una gran diferencia con una sola aplicación implementada en el servidor o en proyectos domésticos, los contenedores pueden ser un salvavidas cuando se trata de aplicaciones sólidas y con muchos recursos, especialmente si comparten el mismo servidor o si se implementan en muchos entornos diferentes.

Esto se resolvió inicialmente con Máquinas Virtuales como VMWare e Hipervisores, aunque han demostrado no ser óptimo cuando se trata de eficiencia, velocidad y portabilidad.

Los contenedores Docker son alternativas ligeras a las máquinas virtuales: a diferencia de las máquinas virtuales, no necesitamos preasignar RAM, CPU u otros recursos para ellos y no necesitamos iniciar una nueva máquina virtual para todas y cada una de las aplicaciones ya que estamos trabajando con un solo sistema operativo.

Los desarrolladores no necesitan cargar con el envío de versiones especiales de software para diferentes entornos y pueden concentrarse en crear la lógica comercial central detrás de la aplicación.

Configuración del proyecto

Matraz es un micro-framework de Python utilizado para crear aplicaciones web simples y avanzadas. Debido a su facilidad de uso y configuración, lo usaremos para nuestra aplicación de demostración.

Si aún no tiene Flask instalado, es fácil hacerlo con un solo comando:

1
$ pip install flask

Después de que se haya instalado Flask, cree una carpeta de proyecto, llamada FlaskApp para ver un ejemplo. En esta carpeta, cree un archivo base, llamado algo así como app.py.

Dentro de app.py importe el módulo Flask ​​y cree una aplicación web usando lo siguiente:

1
2
3
from flask import Flask

app = Flask(__name__)`

A continuación, definamos la ruta básica / y el controlador de solicitudes correspondiente:

1
2
3
4
5
6
@app.route("/")
def index():
  return """
  <h1>Python Flask in Docker!</h1>
  <p>A sample web-app for running Flask inside Docker.</p>
  """

Finalmente, iniciemos la aplicación si el script se invoca como el programa principal:

1
2
if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0')
1
$ python3 app.py

Navegue su navegador a http://localhost:5000/. ¡Debería aparecer el mensaje "Dockerzing Python app using Flask"!

Captura de pantalla de la aplicación web

Dockerización de la aplicación {#dockerización de la aplicación}

Para ejecutar una aplicación con Docker, debemos crear un contenedor con todas las dependencias que se usan en él, que en nuestro caso es solo Flask. Para hacer esto, incluiremos un archivo requirements.txt que contiene las dependencias requeridas y crearemos un Dockerfile que se base en el archivo para construir una imagen.

Además, cuando lancemos el contenedor, tendremos que tener acceso a los puertos HTTP en los que se ejecuta la aplicación.

Preparación de la solicitud

Incluir dependencias en el archivo requirements.txt es muy fácil. Simplemente necesitamos incluir el nombre y la versión de la dependencia:

1
Flask==1.0.2

A continuación, debemos asegurarnos de que todos los archivos de Python necesarios para que se ejecute nuestra aplicación estén dentro de una carpeta de nivel superior, por ejemplo, llamada app.

También se recomienda que el punto de entrada principal se llame app.py, ya que es una buena práctica nombrar el objeto Flask creado en el script como app para facilitar la implementación.

1
2
3
4
5
6
docker-flask-tutorial
    ├── requirements.txt
    ├── Dockerfile
    └── app
        └── app.py
        └── <other .py files>

Crear un Dockerfile

Un Dockerfile es esencialmente un archivo de texto con instrucciones claramente definidas sobre cómo construir una imagen de Docker para nuestro proyecto.

A continuación, crearemos una imagen de Docker basada en Ubuntu 16.04 y Python 3.X:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
FROM ubuntu:16.04

MAINTAINER Madhuri Koushik "[correo electrónico protegido]"

RUN apt-get update -y && \
    apt-get install -y python3-pip python3-dev

COPY ./requirements.txt /requirements.txt

WORKDIR /

RUN pip3 install -r requirements.txt

COPY . /

ENTRYPOINT [ "python3" ]

CMD [ "app/app.py" ]

Aquí hay algunos comandos que merecen una explicación adecuada:

  • FROM - Cada Dockerfile comienza con una palabra clave FROM. Se usa para especificar la imagen base a partir de la cual se construye la imagen. La siguiente línea proporciona metadatos sobre el mantenedor de la imagen.
  • EJECUTAR: podemos agregar contenido adicional a la imagen ejecutando tareas de instalación y almacenando los resultados de estos comandos. Aquí, simplemente actualizamos la información del paquete, instalamos python3 y pip. Usamos pip en el segundo comando RUN para instalar todos los paquetes en el archivo requirements.txt.
  • COPY - El comando COPY se usa para copiar archivos/directorios desde la máquina host al contenedor durante el proceso de construcción. En este caso, estamos copiando los archivos de la aplicación, incluidos requirements.txt.
  • WORKDIR - establece el directorio de trabajo en el contenedor que se utiliza para EJECUTAR, COPIAR, etc...
  • ENTRYPOINT - Define el punto de entrada de la aplicación
  • CMD - Ejecuta el archivo app.py en el directorio app.

Cómo se construyen las imágenes de Docker

Las imágenes de Docker se construyen usando el comando docker build. Al construir una imagen, Docker crea las llamadas "capas". Cada capa registra los cambios resultantes de un comando en el Dockerfile y el estado de la imagen después de ejecutar el comando.

Docker almacena en caché internamente estas capas para que, al reconstruir imágenes, necesite volver a crear solo aquellas capas que han cambiado. Por ejemplo, una vez que carga la imagen base para ubuntu:16.04, todas las compilaciones posteriores del mismo contenedor pueden reutilizarla, ya que no cambiará. Sin embargo, durante cada reconstrucción, es probable que el contenido del directorio de la aplicación sea diferente y, por lo tanto, esta capa se reconstruirá cada vez.

Cada vez que se reconstruye una capa, todas las capas que la siguen en el Dockerfile también deben reconstruirse. Es importante tener este hecho en cuenta al crear Dockerfiles. Por ejemplo, primero COPIAMOS el archivo requirements.txt e instalamos las dependencias antes de COPIAR el resto de la aplicación. Esto da como resultado una capa Docker que contiene todas las dependencias. No es necesario reconstruir esta capa incluso si otros archivos en la aplicación cambian, siempre que no haya nuevas dependencias.

Por lo tanto, optimizamos el proceso de compilación de nuestro contenedor separando la instalación pip del despliegue del resto de nuestra aplicación.

Creación de la imagen de Docker

Ahora que nuestro Dockerfile está listo y entendemos cómo funciona el proceso de compilación, avancemos y creemos la imagen de Docker para nuestra aplicación:

1
$ docker build -t docker-flask:latest .

Aplicación en ejecución en modo de depuración con reinicio automático {#aplicación en ejecución en modo de depuración con reinicio automático}

Debido a las ventajas de la creación de contenedores descrita anteriormente, tiene sentido desarrollar aplicaciones que se implementarán en contenedores dentro del propio contenedor. Esto garantiza que, desde el principio, el entorno en el que se crea la aplicación esté limpio y, por lo tanto, elimina las sorpresas durante la entrega.

Sin embargo, al desarrollar una aplicación, es importante tener ciclos rápidos de reconstrucción y prueba para verificar cada paso intermedio durante el desarrollo. Para este propósito, los desarrolladores de aplicaciones web dependen de las funciones de reinicio automático proporcionadas por marcos como Flask. También es posible aprovechar esto desde dentro del contenedor.

Para habilitar el reinicio automático, iniciamos el contenedor Docker asignando nuestro directorio de desarrollo al directorio de la aplicación dentro del contenedor. Esto significa que Flask observará los archivos en el host (a través de esta asignación) en busca de cambios y reiniciará la aplicación automáticamente cuando detecte algún cambio.

Además, también necesitamos reenviar los puertos de la aplicación desde el contenedor al host. Esto es para permitir que un navegador que se ejecuta en el host acceda a la aplicación.

Para lograr esto, iniciamos el contenedor Docker con las opciones mapeo de volumen y reenvío de puertos:

1
$ docker run --name flaskapp -v$PWD/app:/app -p5000:5000 docker-flask:latest

Esto hace lo siguiente:

  • Inicia un contenedor basado en la imagen docker-flask ​​que creamos anteriormente.
  • El nombre de este contenedor está configurado como flaskapp. Sin la opción --name, Docker elige un nombre arbitrario (y muy interesante) para el contenedor. Especificar un nombre explícitamente nos ayudará a ubicar el contenedor (para detener, etc.)
  • La opción -v monta la carpeta de la aplicación en el host en el contenedor.
  • La opción -p asigna el puerto del contenedor al host.

Ahora se puede acceder a la aplicación en http://localhost:5000 o http://0.0.0.0:5000/:

Screenshot of web-app

Si hacemos cambios en la aplicación cuando el contenedor se está ejecutando y guardamos el archivo, Flask detecta los cambios y reinicia la aplicación:

Screenshot of web-app

Para detener el contenedor, presione [Ctrl]{.kbd}-[C]{.kbd} y elimine el contenedor ejecutando docker rm matrazapp.

Ejecución de la aplicación en modo de producción

Si bien ejecutar la aplicación con Flask directamente es lo suficientemente bueno para el desarrollo, necesitamos usar un método de implementación más sólido para la producción.

Normalmente, una aplicación web Flask en producción puede necesitar manejar múltiples conexiones paralelas y, por lo tanto, generalmente se implementa en un servidor web compatible con WSGI.

Una alternativa popular es nginx + uwsgi y en esta sección veremos cómo configurar nuestra aplicación web para la producción. Nginx es un servidor web de código abierto y uWSGI es un " servidor contenedor de aplicaciones rápido y autorrecuperable".

Primero, creamos una fachada que iniciará nuestra aplicación en modo de desarrollo o producción y, según el modo, elegirá ejecutar nginx o Python directamente.

Llamaremos a este archivo launch.sh y será un simple script de shell. Este archivo está basado en punto-de-entrada.sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/bash

if [ ! -f /debug0 ]; then
  touch /debug0

  while getopts 'hd:' flag; do
    case "${flag}" in
      h)
        echo "options:"
        echo "-h        show brief help"
        echo "-d        debug mode, no nginx or uwsgi, direct start with 'python3 app/app.py'"
        exit 0
        ;;
      d)
        touch /debug1
        ;;
      *)
        break
        ;;
    esac
  done
fi

if [ -e /debug1 ]; then
  echo "Running app in debug mode!"
  python3 app/app.py
else
  echo "Running app in production mode!"
  nginx && uwsgi --ini /app.ini
fi

A continuación, creamos un archivo de configuración uWSGI para nuestra aplicación y una [configuración de nginx](https://www.nginx .com/resources/wiki/start/topics/examples/full/).

Esencialmente, este archivo describe el punto de entrada de nuestra aplicación a uWSGI/nginx:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[uwsgi]
plugins = /usr/lib/uwsgi/plugins/python3
chdir = /app
module = app:app
uid = nginx
gid = nginx
socket = /run/uwsgiApp.sock
pidfile = /run/.pid
processes = 4
threads = 2

Finalmente, modificamos nuestro Dockerfile para incluir nginx y uWSGI. Además de instalar nginx, uWSGI y el complemento uWSGI Python3, ahora también copia nginx.conf en la ubicación adecuada y configura los permisos de usuario necesarios para ejecutar nginx.

Además, el Dockerfile ENTRYPOINT está configurado para el script de shell que nos ayuda a ejecutar el contenedor en modo de depuración o producción:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
FROM ubuntu:16.04

MAINTAINER Madhuri Koushik "[correo electrónico protegido]"

RUN apt-get update -y && \
    apt-get install -y python3-pip python3-dev && \
    apt-get install -y nginx uwsgi uwsgi-plugin-python3

COPY ./requirements.txt /requirements.txt
COPY ./nginx.conf /etc/nginx/nginx.conf

WORKDIR /

RUN pip3 install -r requirements.txt

COPY . /

RUN adduser --disabled-password --gecos '' nginx\
  && chown -R nginx:nginx /app \
  && chmod 777 /run/ -R \
  && chmod 777 /root/ -R

ENTRYPOINT [ "/bin/bash", "/launcher.sh"]

Ahora, podemos reconstruir la imagen:

1
$ docker build -t docker-flask:latest .

Y ejecuta la aplicación usando nginx:

1
$ docker run -d --name flaskapp --restart=always -p 80:80 docker-flask:latest

Esta imagen es independiente y solo necesita que se especifique la asignación de puertos durante la implementación. Esto iniciará y ejecutará el comando en segundo plano. Para detener y eliminar este contenedor, ejecute el siguiente comando:

1
$ docker stop flaskapp && docker rm flaskapp

Además, si necesitamos depurar o agregar funciones, podemos ejecutar fácilmente el contenedor en modo de depuración montando nuestra propia versión del árbol fuente:

1
$ docker run -it --name flaskapp -p 5000:5000 -v$PWD/app:/app docker-flask:latest -d

Gestión de dependencias externas

Cuando se envían aplicaciones como contenedores, un elemento clave que se debe recordar es que aumentan las responsabilidades del desarrollador con respecto a la administración de dependencias. Además de identificar y especificar las dependencias y versiones correctas, también son responsables de la instalación y configuración de estas dependencias en el entorno del contenedor.

Afortunadamente, requirements.txt es un mecanismo sencillo para especificar dependencias. Se le puede agregar cualquier paquete que esté disponible a través de pip.

Pero, de nuevo, cada vez que se modifica el archivo requirements.txt, es necesario reconstruir la imagen de Docker.

Instalación de dependencias al inicio

Ocasionalmente, puede ser necesario instalar dependencias adicionales en el momento del inicio. Supongamos que está probando un paquete nuevo durante el desarrollo y no quiere volver a crear la imagen de Docker cada vez o quiere usar la última versión disponible en el momento del lanzamiento. Es posible lograr esto modificando el iniciador para ejecutar pip al inicio del inicio de la aplicación.

De manera similar, también podemos instalar dependencias de paquetes adicionales a nivel del sistema operativo. Modifiquemos launcher.sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/bin/bash

if [ ! -f /debug0 ]; then
    touch /debug0

    if [ -e requirements_os.txt ]; then
        apt-get install -y $(cat requirements_os.txt)
    fi
    if [ -e requirements.txt ]; then
        pip3 install -r requirements.txt
    fi

    while getopts 'hd' flag; do
        case "${flag}" in
            h)
                echo "options:"
                echo "-h        show brief help"
                echo "-d        debug mode, no nginx or uwsgi, direct start with 'python3 app/app.py'"
                exit 0
                ;;
            d)
                echo "Debug!"
                touch /debug1
                ;;
        esac
    done
fi

if [ -e /debug1 ]; then
    echo "Running app in debug mode!"
    python3 app/app.py
else
    echo "Running app in production mode!"
    nginx && uwsgi --ini /app.ini
fi

Ahora, en requirements_os.txt, podemos especificar una lista de nombres de paquetes separados por espacios en una sola línea y estos, junto con los paquetes en requirements.txt, se instalarán antes de iniciar la aplicación.

Aunque esto se proporciona como una conveniencia durante el desarrollo, no es una buena práctica instalar dependencias durante el tiempo de inicio por varias razones:

  • Derrota uno de los objetivos de la creación de contenedores, que es corregir y probar las dependencias que no cambian debido al cambio del entorno de implementación.
  • Agrega sobrecarga adicional al inicio de la aplicación, lo que aumentará el tiempo de inicio del contenedor.
  • Extraer dependencias cada vez que se inicia la aplicación es un mal uso de los recursos de la red.

Conclusión

En este artículo, nos sumergimos en Docker, una herramienta de creación de contenedores ampliamente utilizada. Creamos una aplicación web simple con Flask, una imagen Docker personalizada basada en Ubuntu para ejecutar nuestra aplicación web en modo de desarrollo y producción.

Finalmente, configuramos la implementación de nuestra aplicación web usando nginx y uWSGI dentro del contenedor Docker y exploramos métodos para instalar dependencias externas.

La contenedorización es una tecnología poderosa que permite el desarrollo y la implementación rápidos de aplicaciones en la nube y esperamos que pueda aplicar lo que aprendió aquí en sus propias aplicaciones.