Desplegando una aplicación de Flask mediante un contenedor de Docker.
Docker nos permite utilizar imágenes para crear contenedores o procesos. Estos se basan en una tecnología de virtualización bastante ligera en el uso de recursos, que nos permite que una aplicación junto con sus dependencias y configuración, se ejecuten en un completo aislamiento, pero sin la necesidad de utilizar una solución de virtualización completa, del tipo máquina virtual, que necesitan muchos más recursos, y a veces puede tener una degradación significativa en el rendimiento de la máquina que hace de host.
En una máquina que hace de host de los containers, se pueden ejecutar muchos de ellos, todos compartiendo el kernel del host y su hardware. Esto contrasta con las máquinas virtuales que deben emular un sistema completo, con su cpu, su disco, otra hardware, kernel etc.
A pesar de tener que compartir el Kernel es aislamiento de un contenedor es bastante alto. Un contenedor tiene su propio sistema de archivos y puede basarse en un sistema de archivos distinto al que se ejecuta en la máquina host del contenedor. Por ejemplo pueden usarse contenedores en Ubuntu en un host que ejecute Fedora, o viceversa. Aunque el origen es de linux, gracias a la virtualización también se pueden ejecutar contenedores de linux en un host de Windows o Mac
Instalando Docker.
$ sudo docker version
Client: Docker Engine - Community
Version: 20.10.7
API version: 1.41
Go version: go1.13.15
Git commit: f0df350
Built: Wed Jun 2 11:56:38 2021
OS/Arch: linux/amd64
Context: default
Experimental: true
Server: Docker Engine - Community
Engine:
Version: 20.10.7
API version: 1.41 (minimum version 1.12)
Go version: go1.13.15
Git commit: b0f5bc3
Built: Wed Jun 2 11:54:50 2021
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.4.6
GitCommit: d71fcd7d8303cbf684402823e425e9dd2e99285d
runc:
Version: 1.0.0-rc95
GitCommit: b9ee9c6314599f1b4a7f497e1f1f856fe433d3b7
docker-init:
Version: 0.19.0
GitCommit: de40ad0
Construyendo una imagen de contenedor para nuestra aplicación.
Un muy buen sitio para empezar a ver como funciona Docker la encontrarás en este sitio de youtube, "FATZ, Docker, Curso Práctico para principiantes (desde linux)"
https://www.youtube.com/watch?v=NVvZNmfqg6M
La forma más sencilla de crear una imagen de contenedor para cualquier aplicación es iniciar un contenedor ya existente del sistema operativo deseamos utilizar (ubuntu, Debian, Fedora, etc), conectarnos a ese contenedor usando un shell de bash, copiar los archivos fuente de la aplicación e instalar todas las dependencias. Después de instalar todo, podemos tomar una instantánea del contenedor y esa instantánea se convertirá en la imagen que queremos. Sin embargo esto no es muy práctico ya que no es conveniente tener que instalar manualmente la aplicación cada vez que necesitemos crear una nueva imagen.
En su lugar generaremos la imagen de la aplicación a través de un script. El comando que crea imágenes de un contenedor a través de un script es "sudo docker build". Este comando lee y ejecuta las instrucciones de compilación de un archivo llamado Dockerfile, que tendremos que crear. Este archivo, Dockerfile, es básicamente una especie de script de instalación que ejecuta los pasos de instalación para implementar la aplicación, además de algunas configuraciones especificas del contenedor.
Empecemos.
Lo que tenemos que tener claro es que una cosa es la imagen y otra el contenedor. Lo primero que vamos a crear es la imagen de la aplicación y luego veremos como ejecutar el contenedor.
Creamos un directorio donde copiamos todos los archivos del proyecto. Yo le llamaré src.
veremos que existen múltiples versiones en base principalmente al sistema operativo base utilizado por la imagen. Nosotros usaremos la que está señalada abajo.
La razón de utilizar la distribución Alpine Linux, en lugar de otras distribuciones más populares como Debian o Ubuntu, es por su pequeño tamaño. Al final en esta instrucción lo que le estamos diciendo a Docker es que queremos utilizar el módulo de Python y exactamente queremos utilizar la versión 3.9-alpine (para utilizar cualquier otra es copiar y pegar el nombre)
El comando WORKDIR le va a decir en que directorio va a estar mi proyecto dentro del contenedor. Puede ser uno que ya exista y sino existe como es el caso de este ejemplo lo creará. Le diremos que cree la carpeta /code y es allí donde se van a ejecutar los siguientes comandos.
Para que nuestra aplicación se ejecute es necesario que le pasemos una variable de entorno que le diga al contenedor cual es el archivo que tiene que arrancar. Para establecer variables de entorno dentro de Dockerfile simplemente utilizamos el comando ENV y establecemos el nombre y el valor de la variable que queramos crear. De esta forma utilizamos la variable de entorno:
ENV FLASK_APP inicio.py
para crear una variable de entorno que le diga al Docker que el programa que ejecute al comenzar sea inicio.py, que es el archivo principal de la aplicación.
A mayores definimos también la variable de entorno:
ENV FLASK_RUN_HOST 0.0.0.0
para decirle al servidor de Flask que nuestro proyecto sea visible y accesible a través de nuestra red. Ya que sino solo estaría accesible a través del localhost.
La siguiente instrucción
RUN apk add --no-cache gcc musl-dev linux-headers
nos servirá para instalar algunos módulos para que Python funcione más rápidamente en la versión Alpine de Linux. (apk es el administrador de paquetes de Alpine)
Nota:
RUN apk add build-base
Está puesta porque existe una incompatibilidad entre cython y python 3.7 que provoca un error al tratar de instalar el modulo "Error greenlet docker build". Esto se podría solucionar utilizando una imagen de python inferior como FROM python:3.6-stretch que funciona bien en distribuciones más pesadas, pero como yo quiero utilizar la versión alpine que es más ligera este comando soluciona el error. Para más información ver este link.
Necesitamos instalar todos los módulos o bibliotecas que se necesitan para que funcione el proyecto y que se encuentran dentro del archivo requirements.txt. Para ello le decimos en el Dockerfile, que queremos que copie el archivo requierements.txt que está el el directorio scr, donde también está el archivo Dockerfile, al contenedor, dentro del directorio de trabajo y queremos que también lo llame requirements.txt. Para ello es que añadimos:
COPY requirements.txt requirements.txt
Es como copiar de origen a destino, pero no añadimos rutas porque el origen está dentro del directorio donde también está el Dockerfile y el destino sino especificamos nada, por defecto entiende que es el directorio de trabajo que hemos especificado antes (/code)
Una vez copiado queremos que ejecute el comando necesario para que el lea el archivo requirements.txt (que ahora lo tiene en el directorio /code) e instale los módulos necesarios para que funcione el proyecto, uno a uno:
RUN pip install -r requirements.txt
Una vez que haya finalizado la instalación vamos a copiar lo más importante. Todo el código con los archivos que conforman el proyecto y que están están todos dentro de la carpeta src. Para ello le decimos a Docker que copie todo lo que esta dentro del directorio src en donde estamos, a la carpeta de trabajo.
COPY . .
Para finalizar tenemos que ejecutar el servidor gunicorn que servirá nuestra aplicación de Flask. Lo hubiésemos podido ejecutar con el comando:
CMD [ "gunicorn", "-b", "0.0.0.0:5000", "inicio:app" ]
En donde "0.0.0.0:5000" es el puerto donde se servirá la aplicación dentro del contenedor y "inicio:app" en donde inicio hace referencia a nuestra aplicación principal de python que es el archivo inicio.py y app es el nombre del WSGI que ejecutamos [app=Flask(__name__)]
Pero yo he escogido la opción de hacerlo a través de un ENTRYPOINT que es como un pequeño archivo o script ejecutable que le dirá a Docker lo que quiero ejecutar. Se puede poner el nombre que se quiera al archivo, yo le llamo boot.sh. Al estar en linux para asegurarme de que se pueda ejecutar le doy permisos de ejecución con:
RUN chmod +x boot.sh
El comando:
EXPOSE 5000
configura el puerto que este contenedor usará para su servidor. Este es necesario para que Docker pueda configurar la red en el contenedor de la manera más adecuada. Elegí el puerto 5000 que es el standar de Flask pero podría haber sido cualquier otro.
Y para finalizar le digo cual va a ser el ENTRYPOINT:
ENTRYPOINT ["./boot.sh"]
que es el comando o archivo predeterminado que debe ejecutarse cuando se inicia el contenedor. Este es el comando que iniciará el servidor web de nuestra aplicación usando gunicorn para ello. En este ejemplo sencillo preferí usar una secuencia de comandos separada para esto aunque como ya comente tambien se podría haber hecho sin este script y usando un comando CMD.
boot.sh: Docker container start-up script.
#!/bin/sh
# Se podrían haber usado otros comandos a ejecutar como
# flask db upgrade u otros.
exec gunicorn -b :5000 inicio:app
En resumen:
Lo que va hace lo anterior es instalar Python en un contendor, luego creará una carpeta llamada /code, configurará las variables que necesita Flask para funcionar, como cual es el archivo principal y cual es el host, y luego ejecutará los módulos para que podamos ejecutar nuestro proyecto. Finalmente copiamos el código y ejecutamos la aplicación.
Con todo esto ya estamos listos para crear la imagen de nuestro proyecto. (Es como las isos de las instalaciones en windows). Luego a partir de esa imagen podremos ejecutar múltiples instancias de ese contenedor con nuestra aplicación en cualquier ordenador.
Para generar una imagen la instrucción a teclear es:
src $ sudo Docker build -t flask22 .
Tenemos que estar dentro del directorio que contiene el archivo Dockerfile. Si utilizamos el parámetro -t (de tag, etiqueta en ingles) podemos especificar el nombre de nuestra imagen. Si no la utilizamos nos asignará un nombre de imagen por defecto.
Después de que realice todo los pasos acabará creando una imagen de nuestro proyecto. Si tecleamos
src $ sudo Docker images
vemos que tenemos dos imágenes en nuestro equipo. Una que es la de nuestra aplicación y otra python que la ha tenido que descargar en local porque era necesaria para crear la nuestra.
Ejecutando un contenedor.
Con una imagen ya creada, ahora ya podemos ejecutar la versión de contenedor para la aplicación. Para ver que todo funciona bien vamos a ejecutar el contenedor a partir de la imagen creada. Tecleamos
src $ sudo docker run -d -p 8000:5000 --name prueba --rm flask22:latest
Que le está diciendo a Docker que ejecute un contenedor, al que hemos llamado
prueba usando el parámetro --name, que lo haga en segundo plano con el parámetro
-d ya que sino nos bloquearía el prompt del terminal. La opción -p asigna
puertos del contendor a puertos del host. El primer valor, a la izquierda de los
dos puntos, es el puerto donde se verá el programa en nuestro ordenador host y
el de la derecha es el puerto dentro del contenedor. En el ejemplo anterior se
asigna el puerto 5000 que es el que usamos en gunicorn y que se ejecuta dentro
del contenedor al puerto 8000 en el host, por lo que para ver la aplicación
tendremos que acceder al puerto 8000 aunque internamente el contenedor lo esté
ejecutando en el puerto 5000. La opción --rm borrará el contenedor una vez que
este terminado. Si bien esta opción no suele ser necesaria, los contenedores que
terminan o que se interrumpen generalmente ya no son necesarios, por lo que se
pueden eliminar automáticamente. El último argumento es el nombre de la imagen
en la que se basa el contenedor y (opcional) después de 2 puntos la etiqueta que
se usará para el contenedor .
Si ahora abres el navegador y vas al localhost:8000 verás como funciona el proyecto:
Para ver los contenedores que se están ejecutando podemos teclear:
src $ sudo docker ps
Para parar el contenedor se usa:
src $ sudo docker stop prueba
o también se puede usar los 3 primeros dígitos del id del container. Por ejemplo para volver a ejecutarlo tecleamos (en tu caso los 3 primeros dígitos de identifiquen a tu container, sino hubiéramos usado la opción --rm):
src $ sudo docker start 416
que iniciará el contenedor con las mismas opciones con la que lo creamos.
Cada vez que realicemos cambios en la aplicación podemos actualizar la imagen del contenedor ejecutando nuevamente el comando de compilación.
Si hubiésemos querido añadir variables de entorno pero no metiéndolas en el archivo Dockerfile al compilar la imagen lo cual las haría accesibles para todos los contenedores, sino solo para un contenedor específico, en tiempo de ejecución, se puede hacer usando el parámetro u opción -e. Un ejemplo imaginario podría ser por ejemplo:
sudo docker run -d -p 8000:5000 --name prueba --rm -e SECRET_KEY=mi clave secreta \
-e MAIL_SERVER=smtp.gogle.com flask22:latest
Usando servicios de contenedores de Terceros.
Añadiendo un contenedor de Mariadb
$ docker run -d \
--name mariadbc \
-p 3306:3306 \
--rm \
-e MYSQL_RANDOM_ROOT_PASSWORD=yes \
-e MYSQL_DATABASE=mibase \
-e MYSQL_USER=usuario \
-e MYSQL_PASSWORD=password \
-v /home/usuario/src/base_de_mariadb:/var/lib/mysql \
mariadb
src $ docker run --name test1 \
-d -p 8000:5000 \
--rm \
--link mariadbc \
-e DATABASE_URL=mariadb+pymysql://usuario:password@mariadbc/mibase \
flask22
src $ docker exec -it test1 sh
y una vez dentro inicializamos la migración:
/code # flask db init
Creating directory /code/migrations ... done
Creating directory /code/migrations/versions ... done
Generating /code/migrations/alembic.ini ... done
Generating /code/migrations/README ... done
Generating /code/migrations/env.py ... done
Generating /code/migrations/script.py.mako ... done
Please edit configuration/connection/logging settings in '/code/migrations/alembic.ini' before proceeding.
2º Realizamos la primera migración:
/code # flask db migrate -m "docker mariadb"
INFO [alembic.runtime.migration] Context impl MariaDBImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'usuario'
Generating /code/migrations/versions/a9939dd69617_docker_mariadb.py ... done
3 Finalizamos aplicando los cambios.
/code # flask db upgrade
INFO [alembic.runtime.migration] Context impl MariaDBImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> a9939dd69617, docker mariadb
Podemos salir del shell tecleando: exit
Ya está todo. Como nuestro contenedor tiene persistencia todo esto quedará ya guardado en el directorio local (acuérdate que funciona en ambos lados como un espejo: ordenador local <-> Contenedor).
Si quieres, para seguir probando puedes usar un contenedor de phpmyadmin para ver y gestionar la base de datos:
src $ docker run -d --rm \
--name phpmyadminc \
--link mariadbc \
-e PMA_HOST=mariadbc \
-p 8080:80 \
phpmyadmin/phpmyadmin
y entrando en el navegador en el local host a traves del puerto 8080 y usando el usuario y contraseña con la que creasteis el contenedor de mariadb podemos gestionar la base de datos:
Ahora solo queda probar el probar nuestra aplicación varias veces introduciendo varios usuarios, cerrando el contenedor y volviéndolo a ejecutar de nuevo para ver que no se ha perdido la información.
El registro de contenedores de Docker.
$ docker login
$ docker tag flask22:latest <nombre_usuario_en_docker>/flask22:latest
$ docker push <nombre_usuario_en_docker>/flask22:latest
No hay comentarios:
Publicar un comentario