從零到英雄:燒瓶生產食譜

已發表: 2022-03-11

作為一名機器學習工程師和計算機視覺專家,我發現自己使用 Flask 創建 API 甚至 Web 應用程序的頻率驚人地頻繁。 在這篇文章中,我想分享一些技巧和有用的方法來構建一個完整的生產就緒 Flask 應用程序。

我們將涵蓋以下主題:

  1. 配置管理。 任何現實生活中的應用程序都有一個具有特定階段的生命週期——至少,它會是開發、測試和部署。 在每個階段,應用程序代碼應該在稍微不同的環境中工作,這需要一組不同的設置,例如數據庫連接字符串、外部 API 密鑰和 URL。
  2. 使用 Gunicorn 的自託管 Flask 應用程序。 雖然 Flask 有一個內置的 Web 服務器,但眾所周知,它不適合生產,需要放在一個能夠通過 WSGI 協議與 Flask 通信的真實 Web 服務器後面。 一個常見的選擇是 Gunicorn——一個 Python WSGI HTTP 服務器。
  3. 使用 Nginx 提供靜態文件和代理請求。 作為 HTTP Web 服務器,Gunicorn 反過來又是一個不適合面向 Web 的應用程序服務器。 這就是為什麼我們需要 Nginx 作為反向代理並提供靜態文件的原因。 如果我們需要將應用程序擴展到多台服務器,Nginx 也會負責負載平衡。
  4. 在專用 Linux 服務器上的 Docker 容器內部署應用程序。 長期以來,容器化部署一直是軟件設計的重要組成部分。 我們的應用程序也不例外,將整齊地打包在自己的容器中(實際上是多個容器)。
  5. 為應用程序配置和部署 PostgreSQL 數據庫。 數據庫結構和遷移將由 Alembic 管理,SQLAlchemy 提供對象關係映射。
  6. 設置一個 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

接下來,我們將添加代碼。

配置/__init__.py

在 config 模塊中,我們將定義我們的微型配置管理框架。 這個想法是使應用程序根據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

配置/設置.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: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

任務/__init__.py

tasks 包包含 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 所必需的,它將在一個單獨的 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 上啟動一個 dummy 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 的。

應用程序.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 容器來運行:

  1. 應用程序容器,用於提供模板頁面並公開 API 端點。 在生產中拆分這兩個功能是個好主意,但我們的演示應用程序中沒有任何模板頁面。 該容器將運行 Gunicorn Web 服務器,該服務器將通過 WSGI 協議與 Flask 通信。
  2. 芹菜工人容器執行長任務。 這是同一個應用程序容器,但使用自定義運行命令來啟動 Celery,而不是 Gunicorn。
  3. Celery beat 容器——與上麵類似,但用於定期調用的任務,例如刪除從未確認其電子郵件的用戶的帳戶。
  4. RabbitMQ 容器。 Celery 需要消息代理來在工作人員和應用程序之間進行通信,並存儲任務結果。 RabbitMQ 是常見的選擇,但您也可以使用 Redis 或 Kafka。
  5. 帶有 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,將工人類指定為 gevent。 Gevent 是一個用於協作多任務的輕量級並發庫。 它在 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 容器及其憑據
  • migration – 一個應用容器,它將使用 Flask-Migrate 執行數據庫遷移並退出。 API 容器依賴於它,並將在之後運行。
  • api - 主應用程序容器
  • api-workerapi-beat – 為從 API 接收的任務和計劃任務運行 Celery 工作者的容器

每個應用程序容器還將從 docker 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,並關閉對所有其他端口的外部訪問! 一定要對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 站點配置文件,包括電池。 它具有以下特點:

  1. SSL 已配置。 您應該擁有適用於您的域的有效證書,例如免費的 Let's Encrypt 證書。
  2. www.your-site.com請求被重定向到your-site.com
  3. HTTP 請求被重定向到安全的 HTTPS 端口。
  4. 反向代理配置為將請求傳遞到本地端口 5000。
  5. 靜態文件由 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 logs檢查每個單獨的容器。 讓我們看看我們所有的容器是否都使用docker ps.

image_alt_text

偉大的。 所有五個容器都已啟動並運行。 Docker Compose 根據 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 和自動化測試的一般概述,請參閱如何構建有效的初始部署管道。 我把這些的實現留給讀者作為練習。

謝謝閱讀!