En esta práctica vamos a introducir un framework muy popular para la programación de servicios web con Python llamada Flask
. Aprenderemos como poner en marcha una aplicación mínima que ejecutaremos desde dentro de un contenedor docker. Aprovecharemos para introducir el concepto de aplicaciones stateless y aplicaciones stateful junto con el bases de datos en memoria. Aprenderemos además a utilizar variables de entorno y a orquestar contenedores cuando su complejidad o número así lo requiera.
Flask
es un framework para construir aplicaciones web:
Se autodeclara como un microframework:
Werkzeug
a WSGI utility libraryjinja2
which is its template enginePara montar nuestra aplicación web vamos a comenzar creando un directorio de trabajo, en nuestro caso intro-flask
. Dentro él iremos colocando todas las carpetas y los archivos necesarios, comenzando por la carpeta webapp
que será donde desarrollaremos nuestra aplicación web.
Para evitar tener que instalar en nuestro entorno de desarrollo local todas las bibliotecas, frameworks y otros elementos, vamos a utilizar un contenedor que encapsule todo esto. Como desarrollaremos en python, crearemos una imagen a partir de la imagen de python oficial publicada en Dockerhub python:3.8
utilizando el siguiente archivo Dockerfile
FROM python:3.10
MAINTAINER Óscar García <oscar.gpoblacion@uah.es>
# Declaramos la carpeta de trabajo
WORKDIR /app
# Copiamos el archivo requirements.txt dentro de la imagen del contenedor.
# El contenido de este archivo se explica más adelante
COPY requirements.txt /app
# Instalamos las dependencias dentro de la imagen
RUN pip install -r requirements.txt
# Copiamos la aplicación web dentro de la imagen
COPY ./app /app
# Puerto en el que escuchará el contenedor
EXPOSE 5000
CMD python main.py
El archivo requirements.txt
es donde especificaremos las dependencias (paquetes, bibliotecas, etc.) que necesite nuestra aplicación. Inicialmente solo necesitaremos Flask
así es que el contenido de este archivo será:
Flask
En la línea 18 del archivo Dockerfile
se indica que la imagen tendrán una copia de nuestra futura aplicación web, que a su vez estará contenida en el directorio app
. A continuación vamos a crear un esqueleto de nuestra aplicación web. Para ello comenzaremos con una estructura inicial de carpetas que crearemos haciendo, desde webapp
, mkdir -p app/{static,templates}
(más adelante se explicará para que sirven esas carpetas). Dentro de app
crearemos el archivo principal de la aplicación web, que llamaremos main.py
y que tendrá el siguiente contenido:
#!/usr/bin/env python
# Programa: main.py
# Propósito: Creación aplicación web Flask
# Autor: Óscar García
# Fecha: 09/12/2019
import flask
# Crear el objeto que representa la aplicacion web
APP = flask.Flask(__name__)
@APP.route('/')
def index():
""" Muestra la página inicial asociada al recurso `/`
y que estará contenida en el archivo index.html
"""
return flask.render_template('index.html')
if __name__ == '__main__':
APP.debug = True
APP.run(host='0.0.0.0', port=5000)
Este programa en python crea una aplicación web Flask, la coloca en modo debug
y la ejecuta haciendo APP.run()
. En este modo debug
se lanza un servidor web diseñado para dar soporte a las operaciones de desarrollo. Para poner la aplicación en modo producción es necesario lanzar otro tipo de servidor, pero esto se abordará más adelante. Al ponerse en marcha, la aplicación abre el puerto TCP 5000 y queda a la escucha de peticiones HTTP. Por eso pusimos la directiva EXPOSE 5000
en el archivo Dockerfile
. Si la petición web se hace sobre el recurso /
entonces Flask invocará la función index
que está anotada con @APP.route('/')
con este fin. A su vez, esta función pide a flask que interprete y devuelva (render_template
) la plantilla index.html
. Por defecto, Flask buscará una plantilla con este nombre en la carpeta templates
, así es que crearemos index.html
con el siguiente contenido:
<!DOCTYPE html>
<html lang='es'>
<head>
<meta charset="utf-8" />
<title>Mi primera aplicación web</title>
<link type="text/css" rel="stylesheet"
href="{{ url_for('static',
filename='hello.css')}}" />
</head>
<body>
Hola Mundo!
</body>
</html>
También añadiremos una hoja de estilos mínima para nuestro HTML que colocaremos en la carpeta static
y tendrá el siguiente contenido:
#center-div {
text-align:center; /* center horizontally */
vertical-align:middle; /* center vertically */
}
#center-div p {
display:inline; /* you might want to use this instead of "inline-block" */
text-align:center; /* center horizontally */
vertical-align:middle; /* center vertically */
font-size: 4vmin;
}
Con todo esto, nuestro proyecto debería tener la siguiente estructura:
.
└── webapp
├── Dockerfile
├── app
│ ├── main.py
│ ├── static
│ │ └── hello.css
│ └── templates
│ └── index.html
└── requirements.txt
Ya estamos en disposición de construir nuestra imagen. En la carpeta webapp
, donde se encuentra nuestro Dockerfile
haremos docker build . -t webapp
. Si todo sale bien, veremos cómo se construye nuestra imagen:
Sending build context to Docker daemon 6.656kB
Step 1/7 : FROM python:3.8
---> 0a3a95c81a2b
Step 2/7 : MAINTAINER Óscar García <oscar.gpoblacion@uah.es>
---> Using cache
---> 88eb90c9bbfd
Step 3/7 : RUN mkdir -p /app
---> Using cache
---> d36aebc6a5f9
Step 4/7 : WORKDIR /app
---> Using cache
---> 3447c082fcc2
Step 5/7 : COPY requirements.txt /app
---> 6a052968c8ba
Step 6/7 : RUN pip install --no-cache-dir -r requirements.txt
---> Running in ec7014a9ebd7
Collecting Flask
...
Installing collected packages: MarkupSafe, Jinja2, itsdangerous, Werkzeug, click, Flask
Successfully installed Flask-1.1.1 Jinja2-2.10.3 MarkupSafe-1.1.1 Werkzeug-0.16.0 click-7.0 itsdangerous-1.1.0
Removing intermediate container ec7014a9ebd7
---> 935b7923122d
Step 7/7 : COPY ./app /app
---> 8f98de20e126
Successfully built 8f98de20e126
Successfully tagged webapp:latest
Y ahora ya podemos arrancar un contenedor que ejecute nuestra imagen webapp
haciendo docker run -p5000:5000 webapp
. Al hacer esto crearemos un contendor que ejecutará nuestra imagen, concretamente las instrucciones que especificamos con la directiva CMD
en el archivo Dockerfile
, esto es, python main.py
.
Al ponerse en marcha veremos una serie de mensajes informativos, y concretamente uno nos invitará a cargar la URL http://0.0.0.0:5000/
. Al poner esa URL en el navegador, este intentará abrir una conexión al puerto 5000[1] de nuestra máquina, pero ese puerto, gracias al flag -p 5000:5000
está contectado al puerto 5000 del contenedor. Con lo que la conexión tendrá éxito y se mostrará nuestro hola mundo en el navegador.
Si intentamos parar el contenedor haciendo CTRL-C, veremos que dicha secuencia es ignorada, ya que el contenedor no está conectado al terminal. Para pararlo será necesario abrir otro terminal, consultar el identificador del contenedor haciendo docker ps
, y detenerlo haciendo docker stop <hash>
. La próxima vez puede arrancar el contenedor con el flag -d
para hacerlo en modo detached que es similar a ejecutarlo en segundo plano. Si lo hacemos así no bloquearemos el terminal, pero tampoco veremos los logs
, aunque si queremos consultarlos basta con hacer docker logs
Ahora que tenemos arrancado nuestro contenedor, que incluye todo lo necesario para ejecutar nuestra aplicación web, surge la necesidad de continuar con las labores de desarrollo, que casi con total seguridad incluyen modificar el código de main.py
, añadir código nuevo con nuevas funcionalidades, crear nuevas plantillas, etc. Es decir, modificar el contenido de la carpeta /app
de la imagen del contenedor. Por supuesto que podemos hacer las modificaciones necesarias, reconstruir la imagen y lanzar un nuevo contenedor, pero esto no es práctico para desarrollar ya que es un ciclo relativamente lento. Para agilizar este proceso, en lugar de reconstruir la imagen lo que haremos será proyectar la carpeta app
de la máquina de desarrollo sobre la carpeta /app
del contenedor, ocultando la de la imagen. Haremos esto utilizando un bind mount haciendo uso del flag -v
al que hay que pasar la ruta absoluta de la carpeta en el anfitrión y la correspondiente ruta dentro del contenedor, separadas ambas con el carácter de :
. Desde la carpeta webapp
podemos hacer:
docker run -p 5000:5000 -v $(pwd)/app:/app -d webapp
Ahora, si hace modificaciones a cualquier archivo desde el anfitrión, y recarga la página, verá que dichas modificaciones surten efecto sin tener que reconstruir la imagen y volver a lanzar el contenedor.
Cloud Run
En este punto se puede utilizar la imagen construida y usar el servicio Clour Run para ejecugtar dicha imagen.
Para esto tenemos que seguir los siguientes pasos:
cloud registry
para almacenar nuestras imágenes$ docker tag webapp europe-central2-docker.pkg.dev/aso-git-2022-2023/aso-git-repo/webapp:latest
$ gcloud auth configure-docker
$ docker push europe-central2-docker.pkg.dev/aso-git-2022-2023/aso-git-repo/webapp
Una alternativa al paso 2 es usar cloud build
para construir la imagen.
$ gcloud builds submit . --tag=europe-central2-docker.pkg.dev/aso-git-2022-2023/aso-git-repo/webapp:latest
jinja2
Las plantillas son documentos estáticos, típicamente utilizados para formar HTML, pero que disponen de unos símbolos especiales (placeholders) situados en aquellos puntos donde sea interesante sustituirlos por datos concretos. Por ejemplo, si en nuestra página queremos dar la bienvenida a un usuario podríamos hacer:
<!DOCTYPE html>
<html lang='es'>
<head>
...
</head
<body>
<h1>
Hola Mundo!
</h1>
<p>
Hola {{user.username}}, bienvenido/a.
</p>
</body>
</html>
Como se puede ver en la línea 11 del fragmento anterior, el placeholder {{user.username}}
será sustituido por el valor de esa variable durante la materialización de la plantilla (render), que se realiza cuando se ejecuta render_template
en nuestra aplicación web. Modificaremos esta instrucción para inyectar las variables correspondientes.
@APP.route('/')
def index():
""" Muestra la página inicial asociada al recurso `/`
y que estará contenida en el archivo index.html
"""
userinfo = {
'username': 'Oscar'
}
return flask.render_template('index.html', user=userinfo)
Las plantillas admiten construcciones muy complejas y son una piedra angular en el desarrollo de aplicaciones web, pero su estudio se sale del objetivo de esta práctica. No obstante hay abundante documentación en Internet, como por ejemplo la de la página oficial de jinja2
Vamos a introducir dos informaciones adicionales que nos permitirán abordar algunos conceptos interesantes. Vamos a introducir un contador del número de visitas y vamos a informar al usuario del nombre del host que ha servido su petición. Para ello haremos lo siguiente en nuestra aplicación web:
#!/usr/bin/env python
# Programa: main.py
# Propósito: Creación aplicación web Flask dinámica
# Autor: Óscar García
# Fecha: 09/12/2019
import flask
import socket
# Crear el objeto que representa la aplicacion web
APP = flask.Flask(__name__)
@APP.route('/')
def index():
""" Muestra la página inicial asociada al recurso `/`
y que estará contenida en el archivo index.html
"""
userinfo = {
'username': 'Oscar'
}
num_visitas = num_visitas + 1
hostname = socket.gethostname()
return flask.render_template('index.html', num_visitas=num_visitas,
user=userinfo, server_name=hostname)
if __name__ == '__main__':
APP.debug=True
APP.run(host='0.0.0.0', port=5000)
Importamos el paquete socket
en la línea 9 para disponer de la función gethostname()
, creamos un contador para el número de visitas en la línea 13 e inyectamos las variables en la plantilla en la línea 25.
Sin embargo, si lo ejecutamos así, python nos dará un error, ya que estamos referenciando la variable num_visitas
sin haberla inicializado primero. Pero si la inicializamos, cada vez que se invoque a la función index
se inicializaría primero y siempre tendría ese valor inicial. Podríamos declarar el contador como global e inicializarla ahí, pero si reiniciamos el servidor se perdería el valor de la variable. Además hay un problema más grave. Debemos pensar que nuestra aplicación debe escalar horizontalmente, es decir, que en el futuro habrá múltiples copias de este software en varias máquinas, lo que haría que hubiera el mismo número de copias del contador de visitas, cosa que por supuesto no es deseable.
La raíz del problema es que nuestro servicio webapp
es stateless
, o dicho de otra forma, no es capaz de mantener su estado interno, es decir, de recordar el valor de sus variables entre dos invocaciones sucesivas. Los servicios stateless
tienen muchas particularidades que los hacen deseables. No mantener estado hace que sean independientes, más fáciles de testear, más fáciles de escalar, etc. pero claro, no siempre es posible abstrarse del estado. Si se necesita mantener estado es necesario recurrir a algún tipo de servicio de persistencia: bases de datos, en disco o en memoria, relacionales o no, sistemas de archivos, blobs, etc. todas ellas con sus ventajas e inconvenientes.
A continuación vamos a introducir el concepto de bases de datos en memoria (in-memory databases), de forma que podamos guardar el valor del contador y lo podamos consultar y modificar en cada invocación del servicio. Para esto vamos a utilizar una implementación open-source llamada redis.
Redis es una base de datos en memoria, orientada a almacenar datos estructurados, que se emplea como memoria cache, como base de datos y como método de transporte para paso de mensajes. También permite realizar operaciones sobre los datos de forma atómica, así es que es posible construir semáforos distribuidos o arquitecturas pub/sub.
Para experimentar con redis vamos a utilizar la imagen oficial, distribuida a través de Dockerhub. Pero antes hay que tener en cuenta que lo que vamos a ejecutar es un servidor, y que por lo tanto debe ser accesible en red. Podríamos publicar el puerto en el anfitrión como hemos hecho anteriormente con la aplicación web, pero ahora vamos a dar un paso más con objeto de que el contenedor sea accesible para otros contenedores.
En primer lugar crearemos una red privada que denominaremos mynet
haciendo:
$ docker network create mynet
Ahora crearemos un contenedor que ejecute la imagen de redis dentro de esa red y además le asignaremos el nombre redisserver
dentro de esa red para que el resto de participantes se pueda referir a él.
$ docker run --net mynet --name redisserver -d redis
Hay que tener precaución cuando pongamos nombres con --name
a los contenedores, ya que dichos nombres deben ser unívocos. Si queremos parar el contenedor podemos hacer docker stop redisserver
, pero no podremos hacer de nuevo docker run ... --name redisserver
ya que eso crearía un nuevo contenedor y el nombre colisionaría con el anterior. Podemos volver a arrancar el contenedor haciendo docker start redisserver
, pero si queremos arrancarlo con otra configuración diferente, o con otros parámetros, no nos queda más remedio de que borrarlo previamente con docker rm redisserver
Con esto tendremos un contendor docker, ejecutando una imagen redis, en la red mynet
y con el nombre DNS redisserver
. Ahora necesitamos un cliente compatible redis para acceder a nuestro servidor. Existen varias implementaciones. Más adelante usaremos una implementación de python con nuestra aplicación web, pero ahora vamos a usar el que publica oficialmente redis y que además está incluido en la imagen que hemos descargado. Se llama redis-cli
y se invoca indicando con el flag -h
el nombre del servidor (o su dirección IP) con el que queremos conectar. Como viene incluido dentro de la imagen podemos invocarlo de la siguiente forma:
$ docker run --net mynet -it redis redis-cli -h redisserver
redisserver:6379>
La orden redis-cli
muestra un prompt que nos invita a introducir órdenes para el servidor redis. Por ejemplo podemos probar info
que nos proporciona datos sobre el estado del propio servidor redis.
Redis almacena pares <clave, valor>
. Para grabar uno de estos pares podemos utilizar la orden set
de la siguiente forma:
redisserver:6379> set contador_visitas 0
OK
El contador lo podemos incrementar con incr
redisserver:6379> incr contador_visitas
(integer) 1
redisserver:6379> incr contador_visitas
(integer) 2
Y también consultar su valor con get
y borrarlo con del
redisserver:6379> get contador_visitas
"2"
redisserver:6379> del contador_visitas
(integer) 1
redisserver:6379> get contador_visitas
(nil)
Existen muchas otras formas de utilizar redis, pero para cerrar esta introducción haremos un último comentario. Es posible asignar a las variables un tiempo de vida, pasado el cual dejan de existir:
redisserver:6379> set tiempo_en_alcala Soleado EX 10
OK
redisserver:6379> get tiempo_en_alcala
"Soleado"
:clock10: Esperamos 10 segundos …
redisserver:6379> get tiempo_en_alcala
(nil)
Esta forma de funcionamiento es la base de los sistemas de cache. Si necesito saber el tiempo_en_alcala
consulto primero en redis. Si no hay respueta (nil)
, entonces hago una (cara) llamada a un servicio de meteorología que me dice Soleado
. Guardo este valor en redis para futuras referencias y devuelvo el valor Soleado
al interesado. Pero ahora cabe preguntarse hasta cuando responderemos Soleado
, y ahí tenemos dos alternativas:
Vamos a implementar el contador de visitas utilizando redis. Para facilitar la labor emplearemos una biblioteca que nos abstraiga de los detalles de conectar con redis y de su protocolo. En este caso utilizaremos la biblioteca redis-py publicada en el respositorio de paquetes pypi como redis
. Para instalar esta dependencia la añadiremos al archivo requirements.txt
que hicimos previamente.
Flask
redis
Reconstruimos ahora la imagen para que se incorpore esta nueva dependencia:
docker build . -t webapp
Y ahora ya podemos hacer uso de la biblioteca en nuestra aplicación:
#!/usr/bin/env python
# Programa: main.py
# Propósito: Creación aplicación web Flask
# Autor: Óscar García
# Fecha: 09/12/2019
import flask
import socket
import redis
# Crear el objeto que representa la aplicacion web
APP = flask.Flask(__name__)
redis_cli = redis.Redis(host='redisserver', port=6379, db=0)
@APP.route('/')
def index():
""" Muestra la página inicial asociada al recurso `/`
y que estará contenida en el archivo index.html
"""
userinfo = {
'username': 'Oscar'
}
redis_cli.incr('num_visitas')
num_visitas = redis_cli.get('num_visitas')
hostname = socket.gethostname()
return flask.render_template('index.html',
visit_counter=num_visitas.decode("utf-8"),
user=userinfo,
server_name=hostname)
if __name__ == '__main__':
APP.debug = True
APP.run(host='0.0.0.0', port=5000)
Importamos la biblioteca en la línea 10, y cada vez que se cargue index
se incrementa y obtiene el valor del contador en las líneas 24 y 25.
Modifica el fichero index.html
incluyendo entre placeholders la nueva información pasada a render_template
.
...
</head>
<body>
<div id="center-div">
<p>Hola Mundo!</p>
<p>Bienvenido {{user.username}}</p>
<p>Soy el servidor {{server_name}}</p>
<p style="font-size:8vmin">Visita número {{visit_counter}}</p>
</div>
</body>
</html>
Ejecuta webapp
teniendo en cuenta que se debe conectar a la misma red privada que redis:
docker run --net mynet -p5000:5000 -d webapp
Prueba a recargar varias veces la página y verás como el contador de visitas se incrementa consecuentemente. También puedes conectarte con el cliente redis-cli
y probar a borrar el contador de visitas de redis
.
En el archivo main.py
utilizamos la siguiente construcción para crear el cliente con el que acceder a REDIS:
redis_cli = redis.Redis(host='redisserver', port=6379, db=0)
En multitud de ocasiones necesitamos utilizar literales como redisserver
o 6379
para indicar valores que dependen de la configuración del entorno de ejecución. Es decir, que en nuestro caso de desarrollo local el servidor de redis tiene el nombre DNS redisserver
y escucha en el puerto 6379
. Pero si nuestro servicio algún día corre en un entorno de ejecución diferente, uno de producción por ejemplo, es muy probable que el servidor redis tenga un nombre diferente, incluso uno que nosotros nunca llegaremos a saber.
Para solucionar este problema vamos a recurrir a un mecanismo muy utilizado en la actualidad: obtener los valores desde variables de entorno del propio sistema operativo. Antes de ejecutar nuestro contenedor, el entorno se encargará de inyectar en él las variables de entornos necesarias con los valores adecuados. La forma varía ligeramente de un entorno a otro, pero en el caso de docker se utilizando el modificador -e
. Para comprobar qué ocurre al utilizar dicho modificador, podemos abrir una sesión interactiva haciendo:
docker run -e REDIS_LOCATION=redisserver -e REDIS_PORT=6379 -it webapp bash
Una vez dentro de la sesión, podemos usar env
para visualizar todas las variables de entorno. Como son muchas las filtraremos con grep
:
root@6163761d4ee8:/app# env | grep "REDIS_"
REDIS_PORT=6379
REDIS_LOCATION=redisserver
Ahora solo tenemos que modificar main.py
para que lea el valor de esas variables de entorno. Para ello usaremos el paquete os
de python, que nos proporciona acceso a los servicios del sistema operativo.
# ...
import os
# ...
redis_host = os.environ["REDIS_LOCATION"]
redis_port = os.environ["REDIS_PORT"]
redis_cli = redis.Redis(host=redis_host, port=redis_port, db=0)
Ahora podemos invocar de nuevo nuestra aplicación haciendo:
docker run
-e REDIS_LOCATION=redisserver
-e REDIS_PORT=6379
-p80:5000
-it
--net mynet webapp
Un caso que requeriere especial atención es cuando necesitamos indicar contraseñas. No es nuestro caso, pero el acceso a redis podría requerir contraseña:
redis_cli = redis.Redis(host='redisserver', password="lapiz", port=6379, db=0)
Si lo hacemos así, estaríamos revelando la contraseña a todo aquel que leyera el código, ya que formaría parte del código que subimos a control de versiones. Esta es una práctica a todas luces inaceptable. Además, seguro que sea cual sea la contraseña de nuestros entorno de desarrollo, no será la misma que la del entorno de producción, y es muy probable (y deseable) que dicha contraseña de producción sea un secreto no revelado a nadie.
Las variables de entorno ayudan a mitigar este problema, pero los servicios cloud ofrecen servicios más específicos para este caso concreto, que suelen denominarse secretos
. Se pueden ver como variables de entorno cuyo valor no se muestra al operador en prácticamente ningún caso. Para más información puedes consultar la documentación del proveedor, por ejemplo la de Google Secret Manager
A medida que vamos añadiendo nuevos servicios a nuestra arquitectura la labor de parar y arrancar los contenedores se hace más tediosa. Además, las líneas de órdenes cada vez tienen más configuraciones y hacen uso de flags cada vez más complejos. Para facilitar la labor de componer arquitecturas con varios contenedores hay varias herramientas disponibles, pero dos son las más conocidas: docker-compose
y kubernetes
. Sin duda kubernetes
es la elegida para gestionar muchos contenedores en un entorno de producción, pero su complejidad queda fuera del alcance de esta práctica (por ahora). Por su parte docker-compose
es mucho más sencillo de utilizar y por lo tanto es muy apropiado para entornos de desarrollo y para algunos entornos de producción.
Para poder utilizar docker-compose
es necesario instalarlo. Las instrucciones están en https://docs.docker.com/compose/install/ y se resume en ejecutar las siguentes dos líneas:
sudo curl -L "https://github.com/docker/compose/releases/download/1.25.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose.yml
Este archivo contiene la información necesaria para componer nuestra arquitectura de contenedores. A continuación se propone uno de ellos para aunar tanto el servidor redis como la aplicación web que hemos desarrollado hasta ahora. Colocamos este archivo docker-compose.yml
en el directorio intro-flask
:
version: '2.1'
services:
redisserver:
image: redis
expose:
- "6379"
networks:
- mynet
webapp:
build:
context: webapp
image: webapp:latest
expose:
- "5000"
volumes:
- ./webapp/app:/app
depends_on:
- redisserver
networks:
- mynet
networks:
mynet:
name: mynet-network
En este archivo hemos definido dos servicios: redisserver
y webapp
. El primero expone a la red interna mynet
el puerto 6379 y el segundo el puerto 5000. Además, el segundo se construye a partir del archivo Dockerfile que se puede encontrar en la carpeta (context) webapp
y el resultado de la construcción será una imagen etiquetada como webapp:latest
. Además este servicio depende de redisserver
y monta la carpeta ./webapp/app
dentro la carpeta /app
del contenedor, como hacíamos anteriormente para facilitar las labores de desarrollo.
Podemos poner en marcha toda la arquitectura haciendo:
$ docker-compose up
Creating network "mynet-network" with the default driver
Creating intro-flask_redisserver_1 ... done
Creating intro-flask_webapp_1 ... done
Attaching to intro-flask_redisserver_1, intro-flask_webapp_1
...
Sin embargo, no podrá cargar la página web porque el puerto 5000 no está proyectado en el anfitrión. Podríamos modificar esto sustituyendo el expose
de las líneas 16 y 17 por:
ports:
- "5000:5000"
Sin embargo para lo siguiente que vamos a hacer, nos conviene no hacer esta proyección.
Antes de continuar, vamos a introducir algunas operaciones básicas con docker-compose. La primera es simplemente abortar la ejecución anterior de docker-compose up
pulsando Ctrl-C
. Si queremos arrancar todos los contenedores pero en segundo plano, podemos usar el modificador -d
de la orden up
:
$ docker-compose up -d
Podemos comprobar qué contenedores están en marcha haciendo docker-compose ps
, y detenerlos todos haciendo docker-compose stop
Incluso podemos indicar que queremos más de un contenedor de un servicio concreto, por ejemplo podríamos crear 3 instancias de la aplicación webapp
para repartir su carga de trabajo. Para ello haremos docker-compose up -d --scale webapp=3
. Si ahora hacemos docker-compose ps
veremos el resultado:
$ docker-compose ps
Name Command State Ports
-----------------------------------------------------------------------------
intro-flask_redisserver_1 docker-entrypoint.sh redis ... Up 6379/tcp
intro-flask_webapp_1 /bin/sh -c python main.py Up 5000/tcp
intro-flask_webapp_2 /bin/sh -c python main.py Up 5000/tcp
intro-flask_webapp_3 /bin/sh -c python main.py Up 5000/tcp
Sin embargo, necesitamos un componente que reparta las peticiones al puerto 5000 de la máquina anfitrión entre las instancias de nuestros contenedores. Para ello añadiremos a continuación a nuestra configuración un sencillo balanceador de carga.
nginx
Es posible configurar un servidor nginx
como proxy inverso, de forma que reparta las peticiones que le llegan entre nuestras aplicaciones web. Para ello vamos a añadir un nuevo servicio al archivo docker-compose.yml
loadbalancer:
build:
context: ./loadbalancer
image: loadbalancer:latest
ports:
- "80:80"
depends_on:
- webapp
networks:
- mynet
Se trata de un contenedor basado en la imagen oficial de Nginx, al que modificaremos ligeramente para convertirlo en un proxy inverso con balanceo de carga. Para hacer estas modificaciones a la imagen oficial crearemos la carpeta loadbalancer
y pondremos en ella el siguiente archivo Dockerfile
:
FROM nginx
COPY default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
Como se puede observar, la única modificación consiste en sustituir el archivo de configuración default.conf
por el siguiente:
upstream localhost {
# These are references to our backend containers, facilitated by
# Compose, as defined in docker-compose.yml
server intro-flask_webapp_1:5000;
server intro-flask_webapp_2:5000;
server intro-flask_webapp_3:5000;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://localhost;
proxy_set_header Host $host;
}
}
Con todos estos cambios, nuestro proyecto tendrá la siguiente estructura de archivos:
intro-flask/
├── docker-compose.yml
├── loadbalancer
│ ├── Dockerfile
│ └── default.conf
└── webapp
├── Dockerfile
├── app
│ ├── app
│ ├── main.py
│ ├── static
│ └── templates
│ └── index.html
└── requirements.txt
Para poner en marcha todo el sistema haremos:
docker-compose up -d --scale webapp=3
Ahora abrimos un navegador en la máquina anfitriona y lo apuntaremos a http://localhost
. Nótese que ahora ya no especificamos el puerto 5000 porque el balanceador de carga escucha en el puerto 80. Cada vez que recargemos la página observaremos que no solo se incrementa el contador de visitas, sino que ahora el nombre de la máquina que sirve el contenido cambia ya que el balanceador desvía la petición a cada una de las tres instancias de la aplicación web.