Zero to Hero: Recetas de producción de matraces

Publicado: 2022-03-11

Como ingeniero de aprendizaje automático y experto en visión artificial, me encuentro creando API e incluso aplicaciones web con Flask con una frecuencia sorprendente. En esta publicación, quiero compartir algunos consejos y recetas útiles para crear una aplicación completa de Flask lista para producción.

Cubriremos los siguientes temas:

  1. Gestión de la configuración. Cualquier aplicación de la vida real tiene un ciclo de vida con etapas específicas; como mínimo, sería desarrollo, prueba e implementación. En cada etapa, el código de la aplicación debería funcionar en un entorno ligeramente diferente, lo que requiere tener un conjunto diferente de configuraciones, como cadenas de conexión de base de datos, claves de API externas y URL.
  2. Aplicación Flask de alojamiento propio con Gunicorn. Aunque Flask tiene un servidor web incorporado, como todos sabemos, no es adecuado para la producción y debe instalarse detrás de un servidor web real capaz de comunicarse con Flask a través de un protocolo WSGI. Una opción común para eso es Gunicorn, un servidor HTTP WSGI de Python.
  3. Servicio de archivos estáticos y solicitud de proxy con Nginx. Si bien es un servidor web HTTP, Gunicorn, a su vez, es un servidor de aplicaciones no apto para enfrentarse a la web. Es por eso que necesitamos Nginx como proxy inverso y para servir archivos estáticos. En caso de que necesitemos escalar nuestra aplicación a varios servidores, Nginx también se encargará del equilibrio de carga.
  4. Implementación de una aplicación dentro de contenedores Docker en un servidor Linux dedicado. La implementación en contenedores ha sido una parte esencial del diseño de software durante bastante tiempo. Nuestra aplicación no es diferente y estará perfectamente empaquetada en su propio contenedor (de hecho, varios contenedores).
  5. Configuración e implementación de una base de datos PostgreSQL para la aplicación. La estructura de la base de datos y las migraciones serán administradas por Alembic con SQLAlchemy proporcionando mapeo relacional de objetos.
  6. Configuración de una cola de tareas de Celery para manejar tareas de ejecución prolongada. Eventualmente, cada aplicación requerirá esto para descargar tiempo o procesos intensivos de cómputo, ya sea el envío de correo, el mantenimiento automático de la base de datos o el procesamiento de imágenes cargadas, de los subprocesos del servidor web en trabajadores externos.

Creación de la aplicación Flask

Comencemos por crear un código de aplicación y activos. Tenga en cuenta que no abordaré la estructura adecuada de la aplicación Flask en esta publicación. La aplicación de demostración consta de una cantidad mínima de módulos y paquetes en aras de la brevedad y la claridad.

Primero, cree una estructura de directorios e inicialice un repositorio Git vacío.

 mkdir flask-deploy cd flask-deploy # init GIT repo git init # create folder structure mkdir static tasks models config # install required packages with pipenv, this will create a Pipfile pipenv install flask flask-restful flask-sqlalchemy flask-migrate celery # create test static asset echo "Hello World!" > static/hello-world.txt

A continuación, agregaremos el código.

config/__init__.py

En el módulo de configuración, definiremos nuestro pequeño marco de gestión de configuración. La idea es hacer que la aplicación se comporte de acuerdo con la configuración predeterminada seleccionada por la variable de entorno APP_ENV , además, agregar una opción para anular cualquier ajuste de configuración con una variable de entorno específica si es necesario.

 import os import sys import config.settings # create settings object corresponding to specified env APP_ENV = os.environ.get('APP_ENV', 'Dev') _current = getattr(sys.modules['config.settings'], '{0}Config'.format(APP_ENV))() # copy attributes to the module for convenience for atr in [f for f in dir(_current) if not '__' in f]: # environment can override anything val = os.environ.get(atr, getattr(_current, atr)) setattr(sys.modules[__name__], atr, val) def as_dict(): res = {} for atr in [f for f in dir(config) if not '__' in f]: val = getattr(config, atr) res[atr] = val return res

config/configuraciones.py

Este es un conjunto de clases de configuración, una de las cuales es seleccionada por la variable APP_ENV . Cuando se ejecuta la aplicación, el código en __init__.py instanciará una de estas clases anulando los valores de campo con variables de entorno específicas, si están presentes. Usaremos un objeto de configuración final cuando inicialicemos la configuración de Flask y Celery más adelante.

 class BaseConfig(): API_PREFIX = '/api' TESTING = False DEBUG = False class DevConfig(BaseConfig): FLASK_ENV = 'development' DEBUG = True SQLALCHEMY_DATABASE_URI = 'postgresql://db_user:db_password@db-postgres:5432/flask-deploy' CELERY_BROKER = 'pyamqp://rabbit_user:rabbit_password@broker-rabbitmq//' CELERY_RESULT_BACKEND = 'rpc://rabbit_user:rabbit_password@broker-rabbitmq//' class ProductionConfig(BaseConfig): FLASK_ENV = 'production' SQLALCHEMY_DATABASE_URI = 'postgresql://db_user:db_password@db-postgres:5432/flask-deploy' CELERY_BROKER = 'pyamqp://rabbit_user:rabbit_password@broker-rabbitmq//' CELERY_RESULT_BACKEND = 'rpc://rabbit_user:rabbit_password@broker-rabbitmq//' class TestConfig(BaseConfig): FLASK_ENV = 'development' TESTING = True DEBUG = True # make celery execute tasks synchronously in the same process CELERY_ALWAYS_EAGER = True

tareas/__init__.py

El paquete de tareas contiene el código de inicialización de Celery. El paquete de configuración, que ya tendrá todas las configuraciones copiadas en el nivel del módulo al momento de la inicialización, se usa para actualizar el objeto de configuración de Celery en caso de que tengamos algunas configuraciones específicas de Celery en el futuro, por ejemplo, tareas programadas y tiempos de espera de los trabajadores.

 from celery import Celery import config def make_celery(): celery = Celery(__name__, broker=config.CELERY_BROKER) celery.conf.update(config.as_dict()) return celery celery = make_celery()

tareas/trabajador_del_celery.py

Este módulo es necesario para iniciar e inicializar un trabajador de Celery, que se ejecutará en un contenedor Docker separado. Inicializa el contexto de la aplicación Flask para tener acceso al mismo entorno que la aplicación. Si eso no es necesario, estas líneas se pueden quitar de forma segura.

 from app import create_app app = create_app() app.app_context().push() from tasks import celery

api/__init__.py

Luego va el paquete API, que define la API REST usando el paquete Flask-Restful. Nuestra aplicación es solo una demostración y tendrá solo dos puntos finales:

  • /process_data : inicia una operación larga ficticia en un trabajador de Celery y devuelve el ID de una nueva tarea.
  • /tasks/<task_id> : devuelve el estado de una tarea por ID de tarea.
 import time from flask import jsonify from flask_restful import Api, Resource from tasks import celery import config api = Api(prefix=config.API_PREFIX) class TaskStatusAPI(Resource): def get(self, task_id): task = celery.AsyncResult(task_id) return jsonify(task.result) class DataProcessingAPI(Resource): def post(self): task = process_data.delay() return {'task_id': task.id}, 200 @celery.task() def process_data(): time.sleep(60) # data processing endpoint api.add_resource(DataProcessingAPI, '/process_data') # task status endpoint api.add_resource(TaskStatusAPI, '/tasks/<string:task_id>')

modelos/__init__.py

Ahora agregaremos un modelo SQLAlchemy para el objeto User y un código de inicialización del motor de la base de datos. Nuestra aplicación de demostración no usará el objeto User de ninguna manera significativa, pero lo necesitaremos para asegurarnos de que las migraciones de la base de datos funcionen y la integración de SQLAlchemy-Flask esté configurada correctamente.

 import uuid from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() class User(db.Model): id = db.Column(db.String(), primary_key=True, default=lambda: str(uuid.uuid4())) username = db.Column(db.String()) email = db.Column(db.String(), unique=True)

Tenga en cuenta cómo el UUID se genera automáticamente como un ID de objeto por expresión predeterminada.

app.py

Finalmente, creemos un archivo principal de la aplicación Flask.

 from flask import Flask logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s]: {} %(levelname)s %(message)s'.format(os.getpid()), datefmt='%Y-%m-%d %H:%M:%S', handlers=[logging.StreamHandler()]) logger = logging.getLogger() def create_app(): logger.info(f'Starting app in {config.APP_ENV} environment') app = Flask(__name__) app.config.from_object('config') api.init_app(app) # initialize SQLAlchemy db.init_app(app) # define hello world page @app.route('/') def hello_world(): return 'Hello, World!' return app if __name__ == "__main__": app = create_app() app.run(host='0.0.0.0', debug=True)</td> </tr> <tr> <td>

Aquí estamos:

  • Configuración del registro básico en un formato adecuado con tiempo, nivel e ID de proceso
  • Definición de la función de creación de la aplicación Flask con inicialización de API y "¡Hola, mundo!" página
  • Definición de un punto de entrada para ejecutar la aplicación durante el tiempo de desarrollo

wsgi.py

Además, necesitaremos un módulo separado para ejecutar la aplicación Flask con Gunicorn. Tendrá sólo dos líneas:

 from app import create_app app = create_app()

El código de la aplicación está listo. Nuestro siguiente paso es crear una configuración de Docker.

Construyendo Contenedores Docker

Nuestra aplicación requerirá múltiples contenedores Docker para ejecutarse:

  1. Contenedor de aplicaciones para servir páginas con plantillas y exponer puntos finales de API. Es una buena idea dividir estas dos funciones en la producción, pero no tenemos páginas con plantillas en nuestra aplicación de demostración. El contenedor ejecutará el servidor web Gunicorn que se comunicará con Flask a través del protocolo WSGI.
  2. Contenedor trabajador de apio para ejecutar tareas largas. Este es el mismo contenedor de aplicaciones, pero con un comando de ejecución personalizado para iniciar Celery, en lugar de Gunicorn.
  3. Contenedor de ritmo de apio: similar al anterior, pero para tareas invocadas en un horario regular, como eliminar cuentas de usuarios que nunca confirmaron su correo electrónico.
  4. Contenedor RabbitMQ. Celery requiere un intermediario de mensajes para comunicarse entre los trabajadores y la aplicación, y almacenar los resultados de las tareas. RabbitMQ es una opción común, pero también puede usar Redis o Kafka.
  5. Contenedor de base de datos con PostgreSQL.

Una forma natural de gestionar fácilmente varios contenedores es utilizar Docker Compose. Pero primero, necesitaremos crear un Dockerfile para crear una imagen de contenedor para nuestra aplicación. Pongámoslo en el directorio del proyecto.

 FROM python:3.7.2 RUN pip install pipenv ADD . /flask-deploy WORKDIR /flask-deploy RUN pipenv install --system --skip-lock RUN pip install gunicorn[gevent] EXPOSE 5000 CMD gunicorn --worker-class gevent --workers 8 --bind 0.0.0.0:5000 wsgi:app --max-requests 10000 --timeout 5 --keep-alive 5 --log-level info

Este archivo le indica a Docker que:

  • Instale todas las dependencias usando Pipenv
  • Agregar una carpeta de aplicación al contenedor
  • Exponer el puerto TCP 5000 al host
  • Establezca el comando de inicio predeterminado del contenedor en una llamada de Gunicorn

Analicemos más lo que sucede en la última línea. Ejecuta Gunicorn especificando la clase trabajadora como gevent. Gevent es una biblioteca de concurrencia ligera para multitarea cooperativa. Brinda ganancias de rendimiento considerables en cargas vinculadas de E/S, lo que proporciona una mejor utilización de la CPU en comparación con la multitarea preventiva del sistema operativo para subprocesos. El parámetro --workers es el número de procesos de trabajo. Es una buena idea establecerlo igual a una cantidad de núcleos en el servidor.

Una vez que tengamos un Dockerfile para el contenedor de la aplicación, podemos crear un archivo docker-compose.yml , que definirá todos los contenedores que la aplicación requerirá para ejecutarse.

 version: '3' services: broker-rabbitmq: image: "rabbitmq:3.7.14-management" environment: - RABBITMQ_DEFAULT_USER=rabbit_user - RABBITMQ_DEFAULT_PASS=rabbit_password db-postgres: image: "postgres:11.2" environment: - POSTGRES_USER=db_user - POSTGRES_PASSWORD=db_password migration: build: . environment: - APP_ENV=${APP_ENV} command: flask db upgrade depends_on: - db-postgres api: build: . ports: - "5000:5000" environment: - APP_ENV=${APP_ENV} depends_on: - broker-rabbitmq - db-postgres - migration api-worker: build: . command: celery worker --workdir=. -A tasks.celery --loglevel=info environment: - APP_ENV=${APP_ENV} depends_on: - broker-rabbitmq - db-postgres - migration api-beat: build: . command: celery beat -A tasks.celery --loglevel=info environment: - APP_ENV=${APP_ENV} depends_on: - broker-rabbitmq - db-postgres - migration

Definimos los siguientes servicios:

  • broker-rabbitmq : un contenedor de agente de mensajes de RabbitMQ. Las credenciales de conexión están definidas por variables de entorno
  • db-postgres : un contenedor de PostgreSQL y sus credenciales
  • migration : un contenedor de aplicaciones que realizará la migración de la base de datos con Flask-Migrate y saldrá. Los contenedores API dependen de ello y se ejecutarán después.
  • api : el contenedor principal de la aplicación
  • api-worker y api-beat : contenedores que ejecutan trabajadores de Celery para tareas recibidas de la API y tareas programadas

Cada contenedor de aplicaciones también recibirá la variable APP_ENV del comando docker-compose up .

Una vez que tengamos listos todos los activos de la aplicación, los pondremos en GitHub, lo que nos ayudará a implementar el código en el servidor.

 git add * git commit -a -m 'Initial commit' git remote add origin [email protected]:your-name/flask-deploy.git git push -u origin master

Configuración del servidor

Nuestro código está ahora en un GitHub, y todo lo que queda es realizar la configuración inicial del servidor e implementar la aplicación. En mi caso, el servidor es una instancia de AWS que ejecuta AMI Linux. Para otras versiones de Linux, las instrucciones pueden diferir ligeramente. También asumo que el servidor ya tiene una dirección IP externa, el DNS está configurado con un registro que apunta a esta IP y se emiten certificados SSL para el dominio.

Consejo de seguridad: ¡No olvide habilitar los puertos 80 y 443 para el tráfico HTTP(S), el puerto 22 para SSH en su consola de alojamiento (o usando iptables ) y cierre el acceso externo a todos los demás puertos! ¡Asegúrese de hacer lo mismo con el protocolo IPv6 !

Instalación de dependencias

Primero, necesitaremos que Nginx y Docker se ejecuten en el servidor, además de Git para extraer el código. Iniciemos sesión a través de SSH y usemos un administrador de paquetes para instalarlos.

 sudo yum install -y docker docker-compose nginx git

Configuración de Nginx

El siguiente paso es configurar Nginx. El archivo de configuración principal nginx.conf suele estar bien tal como está. Aún así, asegúrese de comprobar si se adapta a sus necesidades. Para nuestra aplicación, crearemos un nuevo archivo de configuración en una carpeta conf.d La configuración de nivel superior tiene una directiva para incluir todos los archivos .conf .

 cd /etc/nginx/conf.d sudo vim flask-deploy.conf

Aquí hay un archivo de configuración del sitio Flask para Nginx, baterías incluidas. Tiene las siguientes características:

  1. SSL está configurado. Debe tener certificados válidos para su dominio, por ejemplo, un certificado gratuito de Let's Encrypt.
  2. Las solicitudes de www.your-site.com se redirigen a your-site.com
  3. Las solicitudes HTTP se redireccionan al puerto HTTPS seguro.
  4. El proxy inverso está configurado para pasar solicitudes al puerto local 5000.
  5. Los archivos estáticos son servidos por Nginx desde una carpeta local.
 server { listen 80; listen 443; server_name www.your-site.com; # check your certificate path! ssl_certificate /etc/nginx/ssl/your-site.com/fullchain.crt; ssl_certificate_key /etc/nginx/ssl/your-site.com/server.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; # redirect to non-www domain return 301 https://your-site.com$request_uri; } # HTTP to HTTPS redirection server { listen 80; server_name your-site.com; return 301 https://your-site.com$request_uri; } server { listen 443 ssl; # check your certificate path! ssl_certificate /etc/nginx/ssl/your-site.com/fullchain.crt; ssl_certificate_key /etc/nginx/ssl/your-site.com/server.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; # affects the size of files user can upload with HTTP POST client_max_body_size 10M; server_name your-site.com; location / { include /etc/nginx/mime.types; root /home/ec2-user/flask-deploy/static; # if static file not found - pass request to Flask try_files $uri @flask; } location @flask { add_header 'Access-Control-Allow-Origin' '*' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization'; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range'; proxy_read_timeout 10; proxy_send_timeout 10; send_timeout 60; resolver_timeout 120; client_body_timeout 120; # set headers to pass request info to Flask proxy_set_header Host $http_host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $remote_addr; proxy_redirect off; proxy_pass http://127.0.0.1:5000$uri; } }

Después de editar el archivo, ejecute sudo nginx -s reload y vea si hay algún error.

Configuración de credenciales de GitHub

Es una buena práctica tener una cuenta de VCS de "implementación" separada para implementar el proyecto y el sistema CI/CD. De esta forma, no corre el riesgo de exponer las credenciales de su propia cuenta. Para proteger aún más el repositorio del proyecto, también puede limitar los permisos de dicha cuenta al acceso de solo lectura. Para un repositorio de GitHub, necesitará una cuenta de organización para hacerlo. Para implementar nuestra aplicación de demostración, solo crearemos una clave pública en el servidor y la registraremos en GitHub para obtener acceso a nuestro proyecto sin ingresar credenciales cada vez.

Para crear una nueva clave SSH, ejecute:

 cd ~/.ssh ssh-keygen -b 2048 -t rsa -f id_rsa.pub -q -N "" -C "deploy"

Luego inicie sesión en GitHub y agregue su clave pública desde ~/.ssh/id_rsa.pub en la configuración de la cuenta.

Implementación de una aplicación

Los pasos finales son bastante sencillos: necesitamos obtener el código de la aplicación de GitHub e iniciar todos los contenedores con Docker Compose.

 cd ~ git clone https://github.com/your-name/flask-deploy.git git checkout master APP_ENV=Production docker-compose up -d

Puede ser una buena idea omitir -d (que inicia el contenedor en modo separado) para una primera ejecución para ver la salida de cada contenedor directamente en la terminal y verificar posibles problemas. Otra opción es inspeccionar cada contenedor individual con docker logs después. Veamos si todos nuestros contenedores se ejecutan con docker ps.

imagen_alt_texto

Genial. Los cinco contenedores están en funcionamiento. Docker Compose asignó nombres de contenedores automáticamente en función del servicio especificado en docker-compose.yml. ¡Ahora es el momento de probar finalmente cómo funciona toda la configuración! Es mejor ejecutar las pruebas desde una máquina externa para asegurarse de que el servidor tenga la configuración de red correcta.

 # test HTTP protocol, you should get a 301 response curl your-site.com # HTTPS request should return our Hello World message curl https://your-site.com # and nginx should correctly send test static file: curl https://your-site.com/hello-world.txt

Eso es todo. Tenemos una configuración minimalista, pero totalmente lista para la producción, de nuestra aplicación que se ejecuta en una instancia de AWS. ¡Espero que te ayude a comenzar a construir una aplicación de la vida real rápidamente y evitar algunos errores comunes! El código completo está disponible en un repositorio de GitHub.

Conclusión

En este artículo, discutimos algunas de las mejores prácticas para estructurar, configurar, empaquetar e implementar una aplicación Flask en producción. Este es un tema muy amplio, imposible de cubrir completamente en una sola publicación de blog. Aquí hay una lista de preguntas importantes que no abordamos:

Este artículo no cubre:

  • Integración continua y despliegue continuo
  • Pruebas automáticas
  • Envío de registros
  • Monitoreo de API
  • Ampliación de una aplicación a varios servidores
  • Protección de credenciales en el código fuente

Sin embargo, puede aprender cómo hacerlo utilizando algunos de los otros excelentes recursos de este blog. Por ejemplo, para explorar el registro, consulte Registro de Python: un tutorial detallado, o para obtener una descripción general sobre CI/CD y pruebas automatizadas, consulte Cómo crear una canalización de implementación inicial eficaz. Dejo la implementación de estos como un ejercicio para usted, el lector.

¡Gracias por leer!