第17章:部署-Flask Web 开发(第2版)

与 Flask 捆绑在一起的Web开发服务器不够健壮、不够安全、不够高效,无法胜任生产环境的要求。本章讨论将 Flask 应用程序部署到各种生产环境的操作。

部署流程

无论使用何种托管方法,都需要在安装应用程序时,在生产服务器上执行一系列任务。创建或更新数据表就在这些任务之中。

每次安装或升级应用程序时都手动运行这些任务,很容易出错且耗时。相反,可以将执行所有必需任务的命令添加到 flasky.py 中。示例17-1显示了适用于 Flasky 的部署命令实现。

# 示例 17-1. flasky.py: 部署命令
from flask_migrate import upgrade
from app.models import Role, User

@manager.command
def deploy():
    """Run deployment tasks."""
    # 将数据库迁移到最新版
    upgrade()

    # 创建或更新用户角色
    Role.insert_roles()

    # 确保所有用户都关注了自己
    User.add_self_follows()

这个命令调用的函数全部都是以前创建的; 只是通过一个命令将它们一起调用,以简化应用程序的部署。

如果你从 Github 克隆了示例代码,可以使用 git checkout 17a 签出本示例。

这些函数都是以多次执行都不会造成任何问题的方式进行设计的。通过这种方式设计更新函数,可以在每次安装或升级时运行此 deploy 命令,而无需担心由运行的函数的时机错误而导致的副作用。

在生产环境中记录错误

当应用程序在调试模式下运行时,每当发生错误时都会显示 Werkzeug 的交互式调试器。该错误的堆栈跟踪显示在网页上,并且可以查看源代码,甚至可以使用 Flask 基于 Web 的交互式调试器评估每个堆栈帧的上下文中的表达式。

调试器是在开发过程中调试应用程序问题的优秀工具,但显然它不能用于生产部署。在生产中发生的错误是不会显示出来的,用户会只会看到一个500错误页面。幸运的是,这些错误的堆栈跟踪并未完全丢失,因为 Flask 可以将它们写入日志文件。

在启动期间,Flask 创建一个 Python 的 logging.Logger 类的实例,并将其作为 app.logger 附加到应用程序实例上。在调试模式下,该日志写入控制台,但在生产模式下,默认情况下没有为其配置处理程序。除非添加处理程序,否则不存储日志。示例17-2中,配置了一个日志处理程序,该处理程序将在生产配置下运行时发生的错误发送到由 FLASKY_ADMIN 设置的管理员邮箱里。

# 示例 17-2. config.py: 将应用程序的错误通过邮件发送
class ProductionConfig(Config):
    # ...
    @classmethod
    def init_app(cls, app):
        Config.init_app(app)

        # 将错误发送给管理员
        import logging
        from logging.handlers import SMTPHandler
        credentials = None
        secure = None
        if getattr(cls, 'MAIL_USERNAME', None) is not None:
            credentials = (cls.MAIL_USERNAME, cls.MAIL_PASSWORD)
            if getattr(cls, 'MAIL_USE_TLS', None):
                secure = ()
        mail_handler = SMTPHandler(
            mailhost=(cls.MAIL_SERVER, cls.MAIL_PORT),
            fromaddr=cls.FLASKY_MAIL_SENDER,
            toaddrs=[cls.FLASKY_ADMIN],
            subject=cls.FLASKY_MAIL_SUBJECT_PREFIX + ' Application Error',
            credentials=credentials,
            secure=secure)
        mail_handler.setLevel(logging.ERROR)
        app.logger.addHandler(mail_handler)

如果你从 Github 克隆了示例代码,可以使用 git checkout 17b 签出本示例。

云端部署

托管应用程序趋势是使用云服务器,但这可能意味着许多不同的事情。在最基本的层面上,云托管可能意味着应用程序安装在一个或多个虚拟服务器上,这些虚拟服务器的所有操作都物理机器一样,但实际上是由云运营商管理的虚拟机。这类服务器的典型是 Amazon Web Services(AWS)的 EC2 服务提供的服务器(【wrdll 注】国内的阿里云、腾讯云、京东云等也类似)。将应用程序部署到虚拟服务器与部署到本章后面所述的传统的独立服务器类似。

更高级的部署模型是基于容器的。容器将应用程序隔离在其专用的镜像中。容器镜像包含了应用程序和运行这个应用所有需的所有依赖。像 Docker 这样的容器平台,可以将预先生成的容器镜像在所有操作系统上进行安装和执行。

另一种是平台即服务(PaaS)的部署,使应用程序开发人员无需安装和维护应用程序所运行的硬件和软件平台。在 PaaS 模式中,服务提供商提供了可在其上运行应用程序的完整托管平台。应用程序开发人员需要做的所有工作就是将应用程序代码上传到提供商维护的服务器,然后在几秒钟内,该应用自动变为可用。大多数 PaaS 提供商提供了根据需要添加或删除服务器来动态“扩展”应用程序的方法。

本章的后续部分将分别介绍 Heroku(最受欢迎的 PaaS 提供者之一)部署,Docker 容器部署以及适用于独立服务器或虚拟服务器的传统部署。

Heroku 平台

Heroku 是第一批 PaaS 提供商之一,自 2007 年以来一直在开展业务。Heroku 平台非常灵活,支持包括 Python 在内众多编程语言。要将应用程序部署到 Heroku,开发人员需要使用 Git 将应用程序推送到 Heroku 的特殊 Git 服务器,该服务器会自动完成应用程序的安装、升级、配置和部署。

Heroku 使用称为 dynos 的计算单位来衡量服务的使用情况和费用。最常见的 dyno 类型是 web dyno,它代表一个 web 服务器实例。应用程序可以通过部署更多的 Web dynos 来增加其处理请求的能力,每个 web dynos 运行一个应用程序实例。另一种类型的 dyno 是 worker dyno,用于执行后台任务或其他支持任务。

该平台为数据库、电子邮件支持和许多其他服务提供了大量插件。以下各节将详细介绍将 Flasky 部署到 Heroku 中的一些细节。

准备工作

要使用 Heroku,应用程序必须托管在 Git 仓库中。如果您正在使用托管在远程 Git 服务器(如 GitHub 或 Bitbucket )上的应用程序,则克隆该应用程序到本地 Git 仓库中,该仓库非常适用于 Heroku。如果应用程序尚未托管在 Git 仓库中,则需要开发者在计算机上为其创建一个。

创建一个 Heroku 账号

在使用 Heroku 服务之前,你需要创建一个 Heroku 账号。Heroku 提供了一个免费方案,允许你托管一个小型应用,因此它是一个非常好的实验平台。

安装 Heroku CLI

必须先安装 Heroku CLI 才能使用 Heroku 服务。这是一个命令行客户端,用于与服务器进行交互管理。Heroku 为三大主流平台提供了这个工具的安装包。

安装好 CLI 后的第一件事就是使用 heroku login 命令来认证你的 Heroku 账号:

$ heroku login
Enter your Heroku credentials.
Email: <你的Email地址>
Password: <你的密码>

把你的 SSH 公钥上传到 Heroku 是非常必要的,只有这样才能启用 git push 命令。通常 heroku login 命令会自动创建并上传 SSH 公钥。你也可以使用 heroku keys:add 命令来手动上传。

创建一个应用

下一步是创建一个应用。在此之前,这个应用需要通过 git 进行版本控制。如果你像本书的代码一样,将应用托管在了 Github 上,那么你已经有了 git 仓库。如果不是,你需要进行创建。要在 Heroku 上注册这个应用,需要在该应用的根目录下执行以下命令:

$ heroku create <appname>
Creating <appname>... done
https://<appname>.herokuapp.com/ |  https://git.heroku.com/<appname>.git

Heroku 应用的名称必须是全平台唯一的。通过 create 命令的输出可以知道,这个应用部署后,将使用 https://<appname>.herokuapp.com/ 域名。Heroku 还支持绑定自己的域名。

Heroku 还为你的应用创建了一个专属的 git 服务器:https://git.heroku.com/<appname>.gitcreate 命令将这个服务器注册为你本地 git 仓库的远程仓库,该远程仓库的名称为 heroku

$ git remote show heroku
* remote heroku
  Fetch URL: https://git.heroku.com/<appname>.git
  Push  URL: https://git.heroku.com/<appname>.git
  HEAD branch: (unknown)

flask 命令需要一个 FLASK_APP 环境变量,为了在 Heroku 环境中正常运行这个命令,需要在 Heroku 中为应用注册环境变量。可以通过 config 命令来完成这个操作:

$ heroku config:set FLASK_APP=flasky.py
Setting FLASK_APP and restarting <appname>... done, v4
FLASK_APP: flasky.py

配置数据库

Heroku 支持 PostgreSQL 数据库。免费套餐支持一个最多10000行记录的小型数据库。使用下面的命令,将 PostgreSQL 数据库添加到你的应用中:

$ heroku addons:create heroku-postgresql:hobby-dev
Creating heroku-postgresql:hobby-dev on <appname>... free
Database has been created and is available
 ! This database is empty. If upgrading, you can transfer
 ! data from another database with pg:copy
Created postgresql-cubic-41298 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation

为了让应用使用这个数据库,需要设置 DATABASE_URL 环境变量。

配置日志

默认,Heroku 会截获所有日志输出,通过 heroku logs 命令可以在客户端查看这些日志。为此,需要配置日志处理器。

之前在 ProductionConfig 配置了日志处理器,但是,我们现在要配置的是 Heroku 的日志处理,最好的实践还是为该平台定义一个新的配置类。

# 示例 17-3. config.py: Heroku 配置
class HerokuConfig(ProductionConfig):
    @classmethod
    def init_app(cls, app):
        ProductionConfig.init_app(app)

        # log to stderr
        import logging
        from logging import StreamHandler
        file_handler = StreamHandler()
        file_handler.setLevel(logging.INFO)
        app.logger.addHandler(file_handler)

通过环境变量 FLASK_CONFIG 可以让应用程序知道使用哪个配置。

$ heroku config:set FLASK_CONFIG=heroku
Setting FLASK_CONFIG and restarting <appname>... done, v4
FLASK_CONFIG: heroku

为了增加应用的安全性,建议给应用程序配置一个复杂的密钥。这个密钥可以通过环境变量 SECERT_KEY 来设置。

配置邮件支持

Heroku 没有提供 SMTP 服务器,所以需要配置一个外部服务器。原书推荐的是使用 Gmail 的 SMTP 服务器,读者也可以使用国内的 SMTP 服务器,比如网易邮箱。另外,使用外部服务器存在诸多限制,后文会讲述。在做 PaaS 部署时,请注意这些限制。

在根目录添加 requirements.txt 文件

Heroku 通过应用根目录里的 requirements.txt 文件安装依赖包。Heroku 平台的 requirements.txt 文件除了需要包含生产环境中的所有包之外,还需要添加 psycopg2 包,来启用 SQLAlchemy 对 PostgreSQL 的支持。

【wrdll 注】由于本站一开始就使用 PostgreSQL 代替了原书中的 SQLite,所以 psycopg2 本身就已经存在了。另外,现在需要使用 psycopg2-binary 来代替 psycopg2 包。两者的 API 完全一样。

我们把这些依赖信息保存在 requirements/heroku.txt 文件中,然后定义根目录里的 requirements.txt 文件:

-r requirements/heroku.txt

使用 Flask-SSLify 开启安全的 HTTP (HTTPS)

为了保护用户密码等敏感信息不被窃取,我们需要为应用程序开启 HTTPS 支持。

Heroku 允许所有应用通过 httphttps 两种协议进行访问。由于应用程序运行在 Heroku 的域名上,所以会使用 Heroku 自己的 SSL 证书。为了对整个应用开启 HTTPS,只需要将应用的所有 HTTP 请求转发到 HTTPS 请求上。Flask-SSLify 扩展就是做这个事的。

Flask-SSLify 可以通过 pip 安装:

(venv) $ pip install flask-sslify

然后在工厂函数中启用这个扩展:

# 示例 17-5. app/__init__.py: 将所有请求转发到 HTTPS
def create_app(config_name):
    # ...
    if app.config['SSL_REDIRECT']:
        from flask_sslify import SSLify
        sslify = SSLify(app)
    # ...

由于 HTTPS 只需要在生产环境中启用,并且还需要平台支持,所以为了方便切换是否开启 HTTPS,我们定义了一个的配置项 SSL_REDIRECT。在 Config 基类中,将其设置为 False,那么默认情况下是不需要开启 HTTPS 的。之后,在 HerokuConfig 类中,对其进行修改,从而启用 HTTPS。

 # 示例 17-6. config.py: HTTPS 的配置
class Config:
    # ...
    SSL_REDIRECT = False

class HerokuConfig(ProductionConfig):
    # ...
    SSL_REDIRECT = True if os.environ.get('DYNO') else False

使用 Heroku 时,客户端并非直接连接到应用程序,而是通过一个反向代理服务器进行连接。这会产生一些小问题,比如在使用带 _external=True 参数的 url_for() 时,生成的绝对 URL 还是基于 HTTP 协议的。Werkzeug 提供了一个 WSGI 中间件,用于检查来自代理服务器的自定义头并相应地更新请求对象。

 # 示例 17-7. config.py: 为反向代理提供支持
class HerokuConfig(ProductionConfig):
    # ...
    @classmethod
    def init_app(cls, app):
        # ...

        # 处理反向代理服务器的 HTTP 请求头
        from werkzeug.contrib.fixers import ProxyFix
        app.wsgi_app = ProxyFix(app.wsgi_app)

ProxyFix中间件不但适用于 Heroku,在其它使用了反向代理的环境中都适用。

在生产环境中运行 Web 服务器

Heroku 希望应用程序启动它们自己的,用于生产环境的 Web 服务器并将其配置为侦听环境变量 PORT 中设置的端口号上的请求。Flask 自带的开发 Web 服务器在这种情况下性能会很差,因为它不适合在生产环境中运行。两个适用于生产环境下 Flask 应用程序的 Web 服务器是 Gunicorn 和 uWSGI。

Gunicorn 为例,可以通过 pip 进行安装:

(venv) $ pip install gunicorn

要通过 Gunicorn 来运行应用程序,使用下面的命令:

(venv) $ gunicorn flasky:app
[2017-08-03 23:54:36 -0700] [INFO] Starting gunicorn 19.7.1
[2017-08-03 23:54:36 -0700] [INFO] Listening at: http://127.0.0.1:8000 (68982)
[2017-08-03 23:54:36 -0700] [INFO] Using worker: sync
[2017-08-03 23:54:36 -0700] [INFO] Booting worker with pid: 68985

flasky:app 参数告诉 Gunicorn,应用对象的实例在哪里。冒号前面是这个实例所在的包或模块的名字,冒号后面是应用对象实例的名字。注意,Gunicorn 默认使用 8000 端口,而不是 Flask 开发服务器使用的 5000 端口。

警告!Gunicorn 无法在 Windows 平台运行。另一个推荐的 uWSGI 虽然可以在 Windows 运行,但很难在 Windows 平台安装成功。换句话说,不推荐把包括 Flask 在内的 Web 应用(含 Django)部署在 Windows 平台。

添加一个 Procfile

Heroku 需要知道使用哪个命令来启动应用。这个命令由一个名为 Procfile 的特殊文件指定。这个文件必须放在项目的根目录下,其内容如下:

web: gunicorn flasky:app

这个文件的格式非常简单,每一行设置一个任务的名字,之后用冒号分隔该任务要执行的命令。web 是任务的名字,Heroku 用这个名字来启用 Web 服务器。Heroku 会将每个任务运行的端口设置到对应的 PORT 环境变量中,由于 Gunicorn 会自动读取这个环境变量,所以不需要做额外的操作。

如果你从 Github 克隆了示例代码,可以使用 git checkout 17c 签出本示例。

使用 Heroku Local 进行测试

Heroku CLI 包含了一个 local 命令,用于在本地运行应用程序,这与在 Heroku 服务器上运行的方式非常类似。但是,在本地运行应用程序时,FLASK_APP 等环境变量不可用。heroku local 命令在项目根目录下的 .env 文件里查找环境变量。以下是一个 .env 文件的示例:

FLASK_APP=flasky.py
FLASK_CONFIG=heroku
MAIL_USERNAME=<your-gmail-username>
MAIL_PASSWORD=<your-gmail-password>

警告:由于 .env 文件包含了密码信息,千万不要把它加到版本控制里面。

在启动应用程序之前,需要运行 deploy 命令,来设置数据库。这个命令可以通过 local:run 来执行:

(venv) $ heroku local:run flask deploy
[OKAY] Loaded ENV .env File as KEY=VALUE Format
INFO  Context impl SQLiteImpl.
INFO  Will assume non-transactional DDL.
INFO  Running upgrade  -> 38c4e85512a9, initial migration
INFO  Running upgrade 38c4e85512a9 -> 456a945560f6, login support
INFO  Running upgrade 456a945560f6 -> 190163627111, account confirmation
INFO  Running upgrade 190163627111 -> 56ed7d33de8d, user roles
INFO  Running upgrade 56ed7d33de8d -> d66f086b258, user information
INFO  Running upgrade d66f086b258 -> 198b0eebcf9, caching of avatar hashes
INFO  Running upgrade 198b0eebcf9 -> 1b966e7f4b9e, post model
INFO  Running upgrade 1b966e7f4b9e -> 288cd3dc5a8, rich text posts
INFO  Running upgrade 288cd3dc5a8 -> 2356a38169ea, followers
INFO  Running upgrade 2356a38169ea -> 51f5ccfba190, comments

heroku local 命令从 Procfile 文件里读取任务,并进行执行:

(venv) $ heroku local
[OKAY] Loaded ENV .env File as KEY=VALUE Format
11:37:49 AM web.1 |  [INFO] Starting gunicorn 19.7.1
11:37:49 AM web.1 |  [INFO] Listening at: http://0.0.0.0:5000 (91686)
11:37:49 AM web.1 |  [INFO] Using worker: sync
11:37:49 AM web.1 |  [INFO] Booting worker with pid: 91689

通过 heroku local 命令还可以模拟使用多个 dynos 对应用程序进行扩展。下例运行了3个 web 任务,每个 web 任务监听不同的端口:

(venv) $ heroku local web=3

使用 git push 进行部署

最后一步操作是将应用程序上传到 Heroku 服务器上。请确保对代码的所有更改都提交到了本地 git 仓库,然后使用 git push heroku master 将应用上传到远程的 heroku

$ git push heroku master
Counting objects: 502, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (426/426), done.
Writing objects: 100% (502/502), 108.03 KiB | 0 bytes/s, done.
Total 502 (delta 303), reused 146 (delta 61)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Python app detected
remote: -----> Installing python-3.6.2
remote: -----> Installing pip
remote: -----> Installing requirements with pip
...
remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote:
remote: -----> Compressing...
remote:        Done: 49.4M
remote: -----> Launching...
remote:        Released v8
remote:        https://<appname>.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/<appname>.git
 * [new branch]      master -> master

至此,这个应用已经完成部署并处理运行状态,但它还不能正常工作。因为并没有在服务器上执行 deploy 命令,对数据库进行初始化。可以使用 Heroku 的客户端运行以下命令执行 deploy

$ heroku run flask deploy
Running flask deploy on <appname>... up, run.3771 (Free)
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.

创建完数据表,并完成配置后,可以对应用程序进行重启,以便使用更新后的数据库:

$ heroku restart
Restarting dynos on <appname>... done

这个应用现在已完整地部署到了 https://<appname>.herokuapp.com

检查应用程序的日志

应用程序生成的日志被 Heroku 截获,要查看日志的内容,请使用 logs 命令:

$ heroku logs

为升级进行部署

当需要升级 Heroku 应用程序时,需要重复相同的过程。在将所有更改提交到 Git 存储库后,以下命令将执行升级:

$ heroku maintenance:on
$ git push heroku master
$ heroku run flask deploy
$ heroku restart
$ heroku maintenance:off

maintenance 允许 Heroku CLI 在应用升级过程中的下线期间,显示一个提示页面,告诉用户正在应用正在升级。

Docker 容器

现在,你应该对 Heroku 部署非常熟悉了,这是一个相当高级的部署选项。在本节中,你将学习到如何通过容器(特指 Docker)进行部署。它不像 PaaS 那么自动化,但它提供了更高的扩展性,并且不与某个特定的服务商进行捆绑。

容器是一种特殊的虚拟机,它运行在宿主操作系统的内核之上,而标准虚拟机具有自己的虚拟化内核和硬件。由于虚拟化停止在内核上,容器比虚拟机更加轻便和高效,但它们需要内置在专门的操作系统上。Linux 内核完全支持容器。

安装 Docker

最流行的容器平台是 Docker,它有免费的社区版(称为 Docker CE)和基于订阅的企业版(Docker EE)。Docker 可以安装在包括云服务器在内的三大主流操作系统中。开发并测试一个“容器化”应用的最简单的方式就是在你电脑上安装 Docker CE。MacOS 和 Windows 可以在 Docker Store 中下载安装程序。在 Docker Store 中,还提供了在 CentOS、Fedora、Debian 和 Ubuntu 等 Linux 发行版中安装 Docker CE 的教程。

安装完 Docker CE 后,你需要使用 docker 命令来访问 Docker CE 服务。

“$ docker version
Client:
 Version:      17.06.0-ce
 API version:  1.30
 Go version:   go1.8.3
 Git commit:   02c1d87
 Built:        Fri Jun 23 21:31:53 2017
 OS/Arch:      darwin/amd64

Server:
Version:      17.06.0-ce
API version:  1.30 (minimum version 1.12)
Go version:   go1.8.3
Git commit:   02c1d87
Built:        Fri Jun 23 21:51:55 2017
OS/Arch:      linux/amd64
Experimental: true

Windows 版的 Docker 需要微软的 Hyper-V 技术。安装程序不会帮你启用这个功能。所以在安装 Docker 之前,请先开启 Hyper-V(【wrdll 注】Hyper-V 中在 Windows 8/10 中可用,并且只有高级版本的 Windows 才会有该功能。家庭版等没有这个功能)。如果你的系统不支持 Hyper-V,可以使用基于 VirtualBox 的 Docker Toolbox。

构建一个容器镜像

使用容器技术的第一件是,为你的应用程序构建一个容器镜像。镜像是容器文件系统的一个快照,用于给启动新容器提供模板。可以通过 Dockerfile 来构建镜像。示例17-9展示了构建一个运行本书项目容器的 Dockerfile

# 示例 17-9. Dockerfile: 容器镜像构建脚本
FROM python:3.6-alpine

ENV FLASK_APP flasky.py
ENV FLASK_CONFIG docker

RUN adduser -D flasky
USER flasky

WORKDIR /home/flasky

COPY requirements requirements
RUN python -m venv venv
RUN venv/bin/pip install -r requirements/docker.txt

COPY app app
COPY migrations migrations
COPY flasky.py config.py boot.sh ./
# 运行时配置
EXPOSE 5000
ENTRYPOINT ["./boot.sh"]

Dockerfile 中的构建命令,请自行到其官网上查看文档。或通过本站录制的视频教程进行学习。

我们需要为应用编写基于 Docker 的配置类。

# 示例 17-10. config.py: Docker 配置
class DockerConfig(ProductionConfig):
    @classmethod
    def init_app(cls, app):
        ProductionConfig.init_app(app)

        # log to stderr
        import logging
        from logging import StreamHandler
        file_handler = StreamHandler()
        file_handler.setLevel(logging.INFO)
        app.logger.addHandler(file_handler)

config = {
    # ...
    'docker': DockerConfig,
    # ...
}

Dockerfile 中最后的 ENTRYPOINT 命令用于指定当容器启动时,如何执行应用。拷贝到容器中的 boot.sh 文件用作容器的启动脚本,它的内容如下:

#!/bin/sh
# 示例 17-11. boot.sh: 容器启动脚本
source venv/bin/activate
flask deploy
exec gunicorn -b 0.0.0.0:5000 --access-logfile - --error-logfile - flasky:app

如果你从 Github 克隆了示例代码,可以使用 git checkout 17d 签出本示例。

现在,这个用于 Flasky 的容器镜像可以使用下面的命令进行构建了:

“$ docker build -t flasky:latest .
Sending build context to Docker daemon  51.08MB
Step 1/14 : FROM python:3.6-alpine
 ---> a6beab4fa70b
 ...
Successfully built 930e17a89b42
Successfully tagged flasky:latest

docker build 命令的 -t 参数,用于给镜像指定名称和标签,名称和标签通过冒号分隔。latest 标签通常用于最新版的镜像。build 命令最后的 .,用来将当前目录设置为构建的最顶层目录。Docker 会在该目录查找 Dockerfile 文件,并将该目录及其子目录里的所有文件,设置为可添加到容器镜像里。

构建成功后,这个镜像保存在本地镜像仓库里。docker images 命令可以查看本地仓库里的所有镜像。

运行容器

当应用程序所属的容器镜像构建完成之后,剩下的工作就只有运行该容器了。docker run 命令可以很容易完成这个任务:

$ docker run --name flasky -d -p 8000:5000 \
  -e SECRET_KEY=57d40f677aff4d8d96df97223c74d217 \
  -e MAIL_USERNAME=<your-gmail-username> \
  -e MAIL_PASSWORD=<your-gmail-password> flasky:latest

--name 选项用来设置容器的名称,给容器命名不是必须的,如果没有命令,Docker 将对其进行随机命名。

-d 选项用于将容器以后台运行的方式进行启动。

-p 用于将宿主操作系统的8000端口映射到容器中的5000端口。

-e 用来给容器定义环境变量。这里定义的环境变量,在 Dockerfile 中,使用 ENV 命令来获取。

docker run 命令的最后一个参数是,指定要运行的镜像和标签。它必须和 docker build 中的 -t 参数指定的完全一致。

docker ps 命令会列出正在运行的容器,通过它可以确认我们这个容器是否在运行。

$ docker ps
CONTAINER ID  IMAGE          CREATED     STATUS     PORTS                  NAMES
71357ee776ae  flasky:latest  4 secs ago  Up 8 secs  0.0.0.0:8000->5000/tcp flasky

现在,这个容器正在运行,你可以在你电脑上通过 http://localhost:8000 来访问它。

要停止这个容器,使用 docker stop 命令:

$ docker stop 71357ee776ae
71357ee776ae

【wrdll 注】其参数可以是 docker run 通过 --name 指定的名字,也可以是 Docker 为每个容器生成的唯一ID。下同。

停止了一个容器后,这个容器还保留在系统里。要将其进行删除,使用 docker rm 命令。

$ docker rm 71357ee776ae
71357ee776ae

可以将 stoprm 合并为一个命令(带 -f 选项的 rm):

$ docker rm -f 71357ee776ae
71357ee776ae

检查运行中的容器

当容器出现异常行为时,需要对其进行调试。最明显的调试机制是将日志记录添加到应用程序,然后使用 docker logs 命令监视正在运行的容器。但是,在某些情况下,在正在运行的容器上打开一个 Shell 会话可能会更方便,以便对它进行更仔细的检查。docker exec 命令让它变的可行:

docker exec -it 71357ee776ae sh

在这例子中,Docker 会打开一个 sh 的 Shell 会话,并且不会中断容器。 -it 选项用于连接一个可交互的终端会话。如果容器包含了诸如 bashpython 等更高级的 Shell,也可以用同样的方法来操作。

对容器进行故障排除时,常见策略是创建一个加载了其他工具的特殊映像,例如稍后可以从 shell 会话中调用的调试器。

将你的容器镜像发布到外部仓库

容器镜像可以发布到非本地仓库,如官方的 Docker Hub。

【wrdll 注】相较于原书介绍的,将构建好的镜像(小则几十、几百MB,大则几GB)提交到 Docker Hub 的方式,本站更推荐通过共享 Dockerfile 的方式进行发布。有关原书介绍的内容,请自行查阅相关资料。

使用外部数据库

目前这个部署在容器里的 Flasky 有个缺点:使用与应用程序在同一个容器内的 SQLite 数据库。这使得升级非常困难,因为一旦容器停止,数据库就会随之消失。

更好的方法是将数据库服务器与应用程序容器分开托管。这使得升级应用程序的同时保留数据库变得非常简单,因为只需要将应用程序容器进行替换就行了。

Docker建议以模块化方式来构建应用程序,其中每个服务托管在自己的容器中。有许多可用于 MySQL、PostgreSQL 和其他数据库服务器的公共容器镜像。可以使用 docker run 命令将其中的任何一个直接部署到你的系统里。以下命令将 MySQL 5.7 数据库服务器部署到你的系统:

$ docker run --name mysql -d -e MYSQL_RANDOM_ROOT_PASSWORD=yes \
  -e MYSQL_DATABASE=flasky -e MYSQL_USER=flasky \
  -e MYSQL_PASSWORD=<database-password> \
  mysql/mysql-server:5.7

为了连接到一个 MySQL 数据库,SQLAlchemy 需要一个 MySQL 驱动包,比如 pymysql。这个依赖包可以添加到 requirements/docker.txt 文件中。

如果你从 Github 克隆了示例代码,可以使用 git checkout 17e 签出本示例。

由于修改了 requirements/docker.txt 文件,所以需要使用 docker build 重新构建 flasky:latest 镜像。

然后重新运行:

$ docker run -d -p 8000:5000 --link mysql:dbserver \
  -e DATABASE_URL=mysql+pymysql://flasky:<database-password>@dbserver/flasky \
  -e MAIL_USERNAME=<your-gmail-username> -e MAIL_PASSWORD=<your-gmail-password> \
  flasky:latest

--link 选项,用于建立和另一个容器的连接。注意,DATABASE_URL 中,“主机”部分需要使用 --link 选项指定的名字,而不是常规的 localhost 或某个 IP 地址。

使用 Docker Compose 进行容器编排

容器化的应用程序通常由多个运行中的容器组成。在上一节中你已经看到主应用程序和数据库服务器在独立容器中运行。随着应用程序复杂度的增长,它可能需要更多的容器。 某些应用程序可能还需要额外的服务,如消息队列或缓存。那些利用微服务体系结构的分布式应用程序,由多个子应用程序构成,每个子应用程序都在自己的容器中运行。必须处理高负载或需要容错的应用程序将希望通过在负载均衡器背后运行多个实例来扩展。

随着作为应用程序一部分的容器数量的增加,如果单独使用 Docker,则管理和协调这些容器的任务将变得越来越困难。构建在 Docker 之上的容器编排框架可以帮助完成此任务。

随 Docker 一起安装的 Compose 工具集提供了基本的编排功能。使用 Compose,在 docker-compose.yml 配置文件中对应用程序所需的容器进行描述。然后,可以使用单个 docker-compose命令启动与该应用程序相关的所有容器。示例17-12显示了一个 docker-compose.yml 文件,它代表了容器化的 Flasky 及其 MySQL 服务。

version: '3'
services:
  flasky:
    build: .
    ports:
      - "8000:5000"
    env_file: .env
    links:
      - mysql:dbserver
    restart: always
  mysql:
    image: "mysql/mysql-server:5.7"
    env_file: .env-mysql
    restart: always

version 用于指定使用的 Compose 版本。services 定义了应用程序的所有子容器。此例中,有两个: flaskymysql

编排系统的典型问题是容器的启动顺序错误——或者顺序正确,但是没有给基础服务容器足够的时间来启动和初始化,然后再启动依赖它们的高级容器。本案例中, mysql 容器要首先启动,这样才能确保 flasky 启动时,数据库已经在运行了。

Compose 会以正确的顺序启动 mysqlflasky 容器,因为它从 flasky 容器的 links 里识别出了两者的依赖关系。但 Compose 不会花时间去等 mysql 完成启动。在设计分布式系统时,在与外部服务的所有连接中实施重试是一个好习惯。示例17-13显示了如何通过重试 flask deploy 命令使启动 flasky 容器的 boot.sh 脚本更加健壮,该命令会重试数据库升级,直至成功为止。

#!/bin/sh
# 示例 17-13. boot.sh: 等待数据库启动
source venv/bin/activate

while true; do
    flask deploy
    if [[ "$?" == "0" ]]; then
        break
    fi
    echo Deploy command failed, retrying in 5 secs...
    sleep 5
done

exec gunicorn -b :5000 --access-logfile - --error-logfile - flasky:app

如果你从 Github 克隆了示例代码,可以使用 git checkout 17f 签出本示例。

现在,可以通过 docker-compose up 命令来启动应用了:

docker-compose up -d --build

--build 表示在启动该应用之前,需要进行镜像的构建。构建完成之后,mysqlflasky 两个容器会按正确的顺序进行启动。

Compose 把所有容器的日志合并到一起,可以通过 docker-compose logs 查看。如果要对日志进行监控,可以使用 docker-compose logs -f

docker-compose ps 命令会显示应用所属的所有运行中的容器的状态:

$ docker-compose ps
     Name                Command          State           Ports
------------------------------------------------------------------------
flasky_flasky_1   ./boot.sh               Up      0.0.0.0:8000->5000/tcp
flasky_mysql_1    /entrypoint.sh mysqld   Up      3306/tcp, 33060/tcp

要对应用进行升级时,重新运行 docker-compose up 命令即可。Compose 会重新构建容器,并将旧容器进行替换。

要停止应用程序,使用 docker-compose down 命令,或使用 docker-compose rm --stop --force 来停止并删除这些容器。

清理旧容器和镜像

删除容器:

docker rm -f 容器名或ID

删除镜像:

docker rmi 镜像名或ID

在生产环境中使用 Docker

许多人认为 Docker 只是一个开发和测试平台。 尽管前面部分介绍的技术可用于在生产环境中通过 Docker部署应用程序,但仍有一些需要考虑的限制和安全问题:

监视和警报

如果一个容器化的应用程序崩溃会发生什么? Docker 可以重新启动一个意外退出的容器,但它不会监视你的容器,也不会在行为不正常时发送警报。

日志

Docker 为每个容器维护一个单独的日志流。 虽然 Compose 会提供合并后的日志流,但没有长期的存储或搜索和过滤功能来改善这一点。

案例管理

通过环境变量配置密码和其他凭据是不安全的,因为 Docker 通过 docker inspect 命令或 API 公开了预先配置的环境变量。

可靠性和扩展性

为了提高容错能力或适应不断增加的负载需求,有必要在多台主机上以及一台或多台负载均衡器后面运行多个应用程序实例。

这些限制通常通过构建在 Docker 或其他容器运行时上的更完善的编排框架解决。诸如 Docker Swarm(现已合并到 Docker 中),Apache Mesos 和 Kubernetes 等框架是构建强大容器部署的不错选择。

传统的部署

到目前为止,你已经看到 Heroku 和 Docker 如何部署。为了完成对部署策略的讨论,本节将介绍一个传统的托管选项,其中包括购买或租用的服务器(无论是物理的还是虚拟的),并手动设置所有必需的组件。这显然是最费力的选项,但当你有终端访问生产服务器硬件时,它可以是一个方便的选项。以下各节将为你介绍所涉及的工作。

将 Flask 应用部署到 CentOS》一文介绍了更详细的部署步骤。

服务器设置

在托管应用程序之前,必须在服务器上执行几个管理任务:

  • 安装 PostgreSQLMySQL 等数据库服务。使用 SQLite 数据库也是可以的,但由于其修改现有数据库模式的许多限制,因此不推荐用于生产环境中。

  • 安装 SendmailPostfix 等邮件传输代理(MTA),用于给用户发送邮件。在生产环境中使用 Gmail 是不可能的,因为此服务具有非常严格的限额,并且在服务条款中特别说明了禁止用于商业用途。

  • 安装能胜任生产环境的 Web 服务器,比如 GunicornuWSGI

  • 安装进程监控工具,比如 Supervisor。它会在 Web 服务器崩溃或服务器重启后立即重新启动 Web 服务器。

  • 安装并配置 SSL 证书,以启用 HTTPS。

  • (可选,但强烈推荐)安装反向代理服务器,比如 nginxApache。此服务器可以直接提供静态文件,并将请求转发到应用程序的 Web 服务器,该服务器正在本地主机上的专用端口上监听。

  • 保证服务器安全。这包括几个旨在减少服务器漏洞的任务,例如安装防火墙,删除未使用的软件和服务等。

请使用自动化框架创建脚本部署,而不是手动执行这些任务。这些框架包括:Ansible, Chef, Puppet

导入环境变量

为了使在部署平台上配置环境变量更加简单和统一,可以将所有环境变量存放在 .evn 文件中,然后使用 python-dotenv 包来将这些环境变量导入到生产环境所在平台里。

# Example 17-14. flasky.py: 从 .env 文件中导入环境变量
import os
from dotenv import load_dotenv

dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
if os.path.exists(dotenv_path):
    load_dotenv(dotenv_path)

开启日志

对于 类Unix 的服务器,可以将日志记录发送到 syslog 守护程序。示例 17-15 新建了一个 ProductionConfig 的子类 UnixConfig,用于 类Unix 服务器。

# 示例 17-15. config.py: Unix配置范例
class UnixConfig(ProductionConfig):
    @classmethod
    def init_app(cls, app):
        ProductionConfig.init_app(app)

        # 将日志发送给 syslog
        import logging
        from logging.handlers import SysLogHandler
        syslog_handler = SysLogHandler()
        syslog_handler.setLevel(logging.WARNING)
        app.logger.addHandler(syslog_handler)

这个配置将应用程序的日志写入到 syslog 配置的文件中,通常是 /var/log/messages/var/log/syslogsyslog 服务可以配置成将应用程序的日志写到独立的日志文件中,或者将日志发送给其它机器。

如果你从 Github 克隆了示例代码,可以使用 git checkout 17g 签出本示例。

本文并非《Flask Web Development(2nd edition)》的全文翻译。而只是在阅读该书时,对自认为重点的内容进行记录以及思考。由于水平有限,本文所述内容难免出现不足或错误。请通过购买正版图书来进行更系统地学习,原书地址。本站将依照本系列文章录制视频教程,届时本文未作记录的部分以及项目实际开发中所需要的知识都会在视频教程中进行补充。

flask Flask Web Development(2nd edition) 2018-03-30 13:44 2845263