本文概述
作为一名机器学习工程师和计算机视觉专家, 我发现自己经常使用Flask创建API甚至创建Web应用程序。在这篇文章中, 我想分享一些技巧和有用的秘诀, 以构建一个完整的生产就绪的Flask应用程序。
我们将涵盖以下主题:
- 配置管理。任何实际应用程序都具有特定阶段的生命周期, 至少是开发, 测试和部署。在每个阶段, 应用程序代码应在稍有不同的环境中工作, 这需要具有不同的设置集, 例如数据库连接字符串, 外部API密钥和URL。
- 使用Gunicorn的自托管Flask应用程序。尽管众所周知, Flask具有内置的Web服务器, 但它不适合生产, 需要放置在能够通过WSGI协议与Flask进行通信的真实Web服务器之后。常见的选择是Gunicorn, 这是Python WSGI HTTP服务器。
- 使用Nginx服务静态文件和代理请求。反过来, 作为HTTP Web服务器, Gunicorn是不适合面对Web的应用程序服务器。因此, 我们需要Nginx作为反向代理并提供静态文件。万一我们需要将应用程序扩展到多台服务器, Nginx还将负责负载平衡。
- 在专用Linux服务器上的Docker容器内部署应用程序。长期以来, 容器化部署一直是软件设计的重要组成部分。我们的应用程序没有什么不同, 它将整齐地包装在其自己的容器中(实际上是多个容器)。
- 为应用程序配置和部署PostgreSQL数据库。数据库结构和迁移将由Alembic通过SQLAlchemy进行管理, SQLAlchemy将提供对象关系映射。
- 设置Celery任务队列以处理长时间运行的任务。每个应用程序最终都需要从外部工作程序上的Web服务器线程中卸载时间或计算密集型过程(例如邮件发送, 自动数据库内务处理或上载图像的处理)。
创建Flask应用
首先创建一个应用程序代码和资产。请注意, 我不会在这篇文章中讨论适当的Flask应用程序结构。为了简洁明了, 该演示应用程序包含最少数量的模块和软件包。
首先, 创建目录结构并初始化一个空的Git存储库。
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
接下来, 我们将添加代码。
config / __ init__.py
在配置模块中, 我们将定义我们的小型配置管理框架。这样做的目的是使应用程序根据APP_ENV环境变量选择的配置预设进行操作, 此外, 如果需要, 可以添加一个选项以使用特定的环境变量覆盖任何配置设置。
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 / settings.py
这是一组配置类, 其中一个由APP_ENV变量选择。当应用程序运行时, __ init__.py中的代码将实例化这些类之一, 并使用特定的环境变量(如果存在)覆盖字段值。稍后初始化Flask和Celery配置时, 我们将使用最终配置对象。
class BaseConfig():
API_PREFIX = '/api'
TESTING = False
DEBUG = False
class DevConfig(BaseConfig):
FLASK_ENV = 'development'
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'postgresql://db_user:[email protected]:5432/flask-deploy'
CELERY_BROKER = 'pyamqp://rabbit_user:[email protected]//'
CELERY_RESULT_BACKEND = 'rpc://rabbit_user:[email protected]//'
class ProductionConfig(BaseConfig):
FLASK_ENV = 'production'
SQLALCHEMY_DATABASE_URI = 'postgresql://db_user:[email protected]:5432/flask-deploy'
CELERY_BROKER = 'pyamqp://rabbit_user:[email protected]//'
CELERY_RESULT_BACKEND = 'rpc://rabbit_user:[email protected]//'
class TestConfig(BaseConfig):
FLASK_ENV = 'development'
TESTING = True
DEBUG = True
# make celery execute tasks synchronously in the same process
CELERY_ALWAYS_EAGER = True
任务/__init__.py
任务包包含Celery初始化代码。 Config程序包在初始化时已经将所有设置复制到模块级别, 用于更新Celery配置对象, 以防将来我们会有一些Celery特定的设置, 例如计划任务和工作超时。
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()
任务/celery_worker.py
该模块是启动和初始化Celery worker所必需的, 该芹菜worker将在单独的Docker容器中运行。它初始化Flask应用程序上下文以访问与该应用程序相同的环境。如果不需要, 可以安全地删除这些行。
from app import create_app
app = create_app()
app.app_context().push()
from tasks import celery
api / __ init__.py
接下来是API包, 该包使用Flask-Restful包定义了REST API。我们的应用只是一个演示, 将只有两个端点:
- / process_data –在Celery worker上启动虚拟long操作并返回新任务的ID。
- / tasks / <task_id> –按任务ID返回任务的状态。
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>')
模型/__init__.py
现在, 我们将为User对象添加一个SQLAlchemy模型, 以及一个数据库引擎初始化代码。我们的演示应用程序不会以任何有意义的方式使用User对象, 但我们需要使用它来确保数据库迁移正常进行, 并且SQLAlchemy-Flask集成已正确设置。
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)
请注意, 默认情况下, UUID是如何自动生成为对象ID的。
app.py
最后, 让我们创建一个主要的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>
我们到了:
- 使用时间, 级别和进程ID以适当的格式配置基本日志记录
- 通过API初始化和” Hello, world!”定义Flask应用创建功能页
- 定义在开发期间运行应用程序的入口点
wsgi.py
另外, 我们将需要一个单独的模块来与Gunicorn一起运行Flask应用程序。它只有两行:
from app import create_app
app = create_app()
应用程序代码已准备就绪。我们的下一步是创建Docker配置。
构建Docker容器
我们的应用程序将需要多个Docker容器才能运行:
- 应用程序容器可提供模板化页面并公开API端点。在生产环境中拆分这两个功能是个好主意, 但我们的演示应用程序中没有任何模板化页面。该容器将运行Gunicorn Web服务器, 该服务器将通过WSGI协议与Flask通信。
- 芹菜工人容器执行长任务。这是相同的应用程序容器, 但是带有自定义运行命令来启动Celery, 而不是Gunicorn。
- Celery beat容器-与上面类似, 但是用于定期调用的任务, 例如删除从未确认电子邮件的用户帐户。
- RabbitMQ容器。 Celery需要消息代理在工作人员和应用之间进行通信, 并存储任务结果。 RabbitMQ是常见的选择, 但你也可以使用Redis或Kafka。
- PostgreSQL的数据库容器。
轻松管理多个容器的自然方法是使用Docker Compose。但是首先, 我们将需要创建一个Dockerfile来为我们的应用程序构建容器映像。让我们将其放入项目目录。
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
该文件指示Docker执行以下操作:
- 使用Pipenv安装所有依赖项
- 将应用程序文件夹添加到容器
- 将TCP端口5000暴露给主机
- 将容器的默认启动命令设置为Gunicorn调用
让我们进一步讨论最后一行的情况。它运行Gunicorn, 并将worker类指定为gevent。 Gevent是用于协作式多任务处理的轻量级并发库。与针对线程的OS抢占式多任务处理相比, 它在I / O绑定负载上可观地提高性能, 从而提供更高的CPU利用率。 –workers参数是工作进程数。最好将其设置为等于服务器上的多个内核。
一旦有了用于应用程序容器的Dockerfile, 就可以创建docker-compose.yml文件, 该文件将定义应用程序需要运行的所有容器。
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
我们定义了以下服务:
- broker-rabbitmq – RabbitMQ消息代理容器。连接凭证由环境变量定义
- db-postgres – PostgreSQL容器及其凭证
- 迁移–一个应用容器, 将通过Flask-Migrate执行数据库迁移并退出。 API容器依赖于它, 然后将运行。
- api –主应用程序容器
- api-worker和api-beat –运行Celery worker的容器, 用于处理从API接收的任务和计划任务
每个应用程序容器还将从docker-compose up命令接收APP_ENV变量。
准备好所有应用程序资产后, 让我们将它们放在GitHub上, 这将有助于我们在服务器上部署代码。
git add *
git commit -a -m 'Initial commit'
git remote add origin [email protected]:your-name/flask-deploy.git
git push -u origin master
配置服务器
我们的代码现在位于GitHub上, 剩下的就是执行初始服务器配置和部署应用程序。就我而言, 该服务器是运行AMI Linux的AWS实例。对于其他Linux版本, 说明可能略有不同。我还假定服务器已经有一个外部IP地址, DNS配置有指向该IP的A记录, 并且为该域颁发了SSL证书。
安全提示:不要忘记在主机控制台(或使用iptables)中允许端口80和443用于HTTP(S)通信, 端口22用于SSH(或使用iptables), 并关闭对所有其他端口的外部访问!确保对IPv6协议执行相同的操作!
安装依赖项
首先, 我们需要在服务器上运行Nginx和Docker, 以及Git来提取代码。让我们通过SSH登录并使用软件包管理器进行安装。
sudo yum install -y docker docker-compose nginx git
配置Nginx
下一步是配置Nginx。主要的nginx.conf配置文件通常保持原样。不过, 请务必检查它是否适合你的需求。对于我们的应用, 我们将在conf.d文件夹中创建一个新的配置文件。顶级配置有一个指令, 该指令包含其中的所有.conf文件。
cd /etc/nginx/conf.d
sudo vim flask-deploy.conf
这是Nginx的Flask站点配置文件, 包括电池。它具有以下功能:
- 已配置SSL。你应该拥有适用于你域的有效证书, 例如免费的Let’s Encrypt证书。
- www.your-site.com请求被重定向到your-site.com
- HTTP请求被重定向到安全的HTTPS端口。
- 反向代理配置为将请求传递到本地端口5000。
- Nginx从本地文件夹提供静态文件。
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;
}
}
编辑文件后, 运行sudo nginx -s reload并查看是否有任何错误。
设置GitHub凭证
最好有一个单独的”部署” VCS帐户来部署项目和CI / CD系统。这样一来, 你不必冒险暴露自己帐户的凭据。为了进一步保护项目存储库, 你还可以将该帐户的权限限制为只读访问。对于GitHub存储库, 你需要一个组织帐户才能执行此操作。要部署演示应用程序, 我们只需在服务器上创建一个公钥, 然后在GitHub上注册它即可访问我们的项目, 而无需每次都输入凭据。
要创建新的SSH密钥, 请运行:
cd ~/.ssh
ssh-keygen -b 2048 -t rsa -f id_rsa.pub -q -N "" -C "deploy"
然后登录GitHub并在帐户设置中从〜/ .ssh / id_rsa.pub添加你的公钥。
部署应用
最后的步骤非常简单-我们需要从GitHub获取应用程序代码, 并使用Docker Compose启动所有容器。
cd ~
git clone https://github.com/your-name/flask-deploy.git
git checkout master
APP_ENV=Production docker-compose up -d
在第一次运行时, 省略-d(以分离模式启动容器)可能是个好主意, 以查看终端中每个容器的输出并检查可能的问题。另一个选择是随后使用docker日志检查每个单独的容器。让我们看看我们所有的容器是否都在docker ps上运行。
大。所有五个容器都已启动并正在运行。 Docker根据docker-compose.yml中指定的服务自动撰写分配的容器名称。现在是时候最终测试整个配置如何工作了!最好从外部计算机上运行测试, 以确保服务器具有正确的网络设置。
# 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
而已。我们对运行在AWS实例上的应用程序进行了极简但完全可用于生产的配置。希望它能帮助你快速开始构建真实的应用程序并避免一些常见的错误!完整的代码可在GitHub存储库上找到。
总结
在本文中, 我们讨论了将Flask应用程序构建, 配置, 打包和部署到生产中的一些最佳实践。这是一个非常大的主题, 不可能在一个博客文章中完全涵盖。以下是我们未解决的重要问题列表:
本文不涉及:
- 持续集成和持续部署
- 自动测试
- 日志传送
- API监控
- 将应用程序扩展到多个服务器
- 保护源代码中的凭据
但是, 你可以使用此博客上的其他一些不错的资源来学习如何做到这一点。例如, 要探索日志记录, 请参阅Python日志记录:深入教程, 或有关CI / CD和自动测试的一般概述, 请参阅如何构建有效的初始部署管道。读者, 我将这些的实现留给你练习。
谢谢阅读!
评论前必须登录!
注册