Django2-和-Channel2-实践教程-五-

96 阅读12分钟

Django2 和 Channel2 实践教程(五)

原文:Practical Django 2 and Channels 2

协议:CC BY-NC-SA 4.0

十、如何部署 Django 站点

在这一章中,我们将把我们到目前为止所构建的东西部署到生产中。生产系统是那些最终用户使用的系统。

一般来说,我们可以在互联网上有许多可用的环境,这取决于他们的受众。一些常用的环境是实时站点的“生产”环境和即将发布的版本的“准备”环境,通常只有少数人可以访问。

本章介绍了一种部署环境的方法,而不考虑其用途。

缩小 JS/CSS

当你部署一个网站时,第一个好的实践是确保你提供给客户的资产被尽可能高效地打包(缩小和捆绑)。这确保了客户端浏览器需要传输更少的数据和打开更少的连接。你的网站会感觉更有反应性,用户会为此感谢你。

这是 Webpack(我们在第五章中使用过)使之变得微不足道的操作之一;所以,没有借口不去做。我们将在顶层文件夹的webpack.prod.config.js中创建一个新的 Webpack 配置:

const common = require('./webpack.config')

module.exports = {
  mode: 'production',
  entry: common.entry,
  plugins: common.plugins,
  output: {
    filename: '[name].bundle.js',
    path: common.output.path
  }
};

这种配置重用了开发配置中的大多数设置,只做了一些更改。它使用production模式,该模式开启 Webpack 第 4 版内置的缩小和其他优化功能。

我们将在package.json中集成新的 Webpack 配置,在scripts部分添加一个新条目:

...
    "scripts": {
      "test": "jest",
      "build": "webpack",
      "build-prod": "webpack --config webpack.prod.config.js"
    },
...

我们现在可以使用以下命令来运行它:

$ npm run build-prod

> booktime@1.0.0 build-prod /code
> webpack --config webpack.prod.config.js

Hash: 58037a9e18e1cf5c3bac
Version: webpack 4.12.0
Time: 2430ms
Built at: 08/30/2018 6:40:26 PM

                                      Asset       Size ...
imageswitcher-58037a9e18e1cf5c3bac.bundle.js   100 KiB ...
[14] ./frontend/imageswitcher.js 931 bytes {0} [built]
    + 14 hidden modules

服务静态资产

到目前为止,我们只使用了开发服务器,但这不是我们在生产中要使用的。开发服务器有一种特殊的方式来服务静态资产,但是在生产中我们不能依赖于此。

生产环境需要一种更有效的方式来服务静态资产。如今大多数设置都使用 Nginx 作为反向代理, 1 连接到像 Gunicorn 或 uWSGI 这样的服务器,为应用处理流量。在这些情况下,静态资产直接由 Nginx 提供服务。

另一种可能的设置是使用外部 Django 库,它以最有效的方式处理静态资产,尽管 Nginx 的效率很难超越。

还有一种方法是,根本不要费心从你自己的 web 服务器上提供静态资产,把这个责任交给亚马逊 S3、谷歌云存储或 Minio 等开源服务。这些服务将负责存储静态资产并通过 HTTP 为它们提供服务。

对于 BookTime 网站,我们将使用第二种方法,即使用外部 Django 库。这种方法在性能和易于设置之间取得了良好的平衡。我们将使用一个名为 whiten noise(http://whitenoise.evans.io)的库,因此运行以下代码:

$ pipenv install whitenoise

我们将在所有其他INSTALLED_APPS之上插入 WhiteNoise 应用,在所有其他MIDDLEWARE指令之上插入其中间件,除了SecurityMiddleware

我们还将添加STATIC_ROOT变量,它指定静态资产将被收集到哪里,以及STATICFILES_STORAGE,用于 WhiteNoise 可以提供的一些额外优化。

以下是对booktime/settings.py的建议配置更改:

...

INSTALLED_APPS = [
    "whitenoise.runserver_nostatic",
    ...
]

...

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    ...
]

...

STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")

if not DEBUG:
    STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

...

WhiteNoise 的CompressedManifestStaticFilesStorage存储后端为一个内置的 Django ManifestStaticFilesStorage存储后端添加了自动压缩行为,接下来描述的命令将使用该行为。

正在执行 collectstatic

我们选择的任何服务静态资产的方式,不管有没有 WhiteNoise,都需要在部署阶段执行collectstatic Django 命令,这意味着在运行应用服务器之前。

这个 Django 命令从我们所有的应用中复制所有的静态资产,无论是内置的还是我们自己构建的,都放在一个位置,由STATIC_ROOT指定:

$ ./manage.py collectstatic
197 static files copied to '/.../booktime/staticfiles', 552 post-processed.

在生产中运行该命令时,将使用ManifestStaticFilesStorage。除了复制所有静态资产之外,这个存储还会将文件哈希附加到所有收集的文件中。这是为了“破坏缓存”:确保当新版本可用时,客户端不使用旧版本的资产。

特定环境变量

对于这个项目,我们将遵循十二因素 app 方法论( https://12factor.net )。我们将外部化所有依赖于环境的变量,比如数据库 URL、调试模式是否打开等等。

为此,我们将使用另一个外部库,该库负责读取环境变量的内容并将其转换为相应的 Django 格式:

$ pipenv install django-environ

这个库将在我们的booktime/settings.py内部的几个地方使用。

变化如下:删除不涉及env()的旧行:

import environ
...

env = environ.Env(
    # set casting, default value
    DEBUG=(bool, False)
)

env.read_env('.env')

DEBUG = env('DEBUG')

REDIS_URL = env('REDIS_URL')
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {"hosts": [REDIS_URL]},
    }
}

DATABASES = {
    "default": env.db()
}

EMAIL_CONFIG = env.email_url('EMAIL_URL')
vars().update(EMAIL_CONFIG)

SECRET_KEY = env('SECRET_KEY')

...

从这段代码中可以看出,我们正在加载库,除了从当前环境中读取之外,还从.env文件中读取。这个.env文件的存在是不必要的;我们将只在本地使用它来简化我们的开发工作流程。

您必须使用您的机器所需的配置来创建这个文件。您可以使用以下带有您自己设置的模板,并将其存储在顶层文件夹内的.env中:

DATABASE_URL=postgres://user:password@localhost/booktime
REDIS_URL=redis://localhost
EMAIL_URL=consolemail://
SECRET_KEY="change-this-$%£4"

该文件包含一些 URL 形式的配置变量。将负责将这些翻译成 Django 要求的格式。我们也在这里管理SECRET_KEY。需要保护SECRET_KEY的价值。如果攻击者发现了这个密钥的值,他就可以用它来危害您的站点。将这一点以及所有其他细节保留在源代码存储库之外是一个很好的实践,现在考虑到我们正在使用环境变量,这是可能的。

然而,不要将.env提交给存储库,因为这将违背我们的目的。

确保不落下任何东西

在将项目部署到本地机器之外的环境之前,我们需要检查的是,我们是否有一些硬编码的配置需要在不同的环境之间变化。

在我们的例子中,当我们在聊天消费者中直接使用 Redis 时,我们留下了一些在部署后无法工作的代码。我们需要在我们的main/consumers.py中解决这个问题:

from django.conf import settings

...

class ChatNotifyConsumer(AsyncHttpConsumer):
    ...

    async def stream(self):
        r_conn = await aioredis.create_redis(settings.REDIS_URL)
        ...

class ChatConsumer(AsyncJsonWebsocketConsumer):
    ...

    async def connect(self):
        ...

        if authorized:
            self.r_conn = await aioredis.create_redis(
                settings.REDIS_URL
            )
            ... 

修复了这段代码后,我们现在确信消费者不再依赖于在本地运行的 Redis。

在杜库/赫罗库部署

我们已经准备好向世界展示我们的工作,将它部署在一个真正的服务器上。我们将为此使用 Dokku,一个与 Heroku 兼容并在任何 Linux 服务器上运行的开源平台即服务(PaaS)。使用 PaaS 代替普通虚拟机(VM)的优势在于设置工作要少得多。

常见部署步骤

首先,我们的项目需要相当于booktime/wsgi.py的东西,但是对于 Django 渠道系统。Channels 不使用 WSGI,而是使用另一种称为 ASGI 的协议。ASGI 允许通道将接收请求的系统从运行代码的系统中分离出来。它在目的上类似于 WSGI,但用于异步系统。

我们需要这样做,因为 Django 的开发服务器会自动这样做,但它只供本地使用。对于生产,这需要单独完成。

booktime/wsgi.py的同一个文件夹中,我们将创建booktime/asgi.py:

"""

ASGI entrypoint. Configures Django and then runs the application

defined in the ASGI_APPLICATION setting.

"""

import os
import django
from channels.routing import get_default_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "booktime.settings")
django.setup()
application = get_default_application()

部署到任何系统都需要这个文件。

从现在开始,这些步骤开始是 Heroku/Dokku 特有的。我们需要在顶层文件夹中创建一个名为Procfile的文件,内容如下:

web: daphne -p $PORT --bind 0.0.0.0 -v2 booktime.asgi:application

该文件列出了 PaaS 的流程主管需要管理的所有流程类型。在我们的例子中,只有一个(web),但是在更复杂的系统中可能有更多,比如后台队列工作器。

daphne是 Django Channels 的内置网络服务器。对于标准的 Django,有很多 WSGI 服务器,比如GunicornuWSGI等等。我们不能把这些用于 Django 频道。目前,ASGI 服务器很少,尽管这在将来可能会改变。

饭桶

Dokku 和 Heroku 都依赖 git 进行新的部署。如果您还没有在您的项目中使用它,您必须开始使用它。好在设置起来真的很简单。如果您不熟悉这个工具,网上有很多关于如何使用它的文档。

在顶层文件夹中,您需要初始化存储库:

$ git init

运行完这个之后,确保您的存储库中有下面的.gitignore文件:

__pycache__/
db.sqlite3
media/
staticfiles/
bundles/
node_modules/
webpack-stats.json
geckodriver.log
.env

提交这个文件,提交您的所有项目代码,您就可以开始您的第一次部署了。

特定于 Dokku 的命令

如果你想测试这个 2 ,你可以很容易地在 DigitalOcean(一家基础设施托管公司)上部署一个预装了 Dokku 的虚拟机。

一旦有了运行的 Dokku 服务器,第一步就是创建应用,如下所示。记住,从现在开始,您需要在新服务器上运行所有的dokku命令。

$ dokku apps:create booktime
-----> Creating booktime... done

您可以在服务器上运行的应用列表中看到该应用:

$ dokku apps:list booktime
=====> My Apps booktime
...

下一步是附加我们的应用将需要的所有资源,从 PostgreSQL 开始:

$ dokku plugin:install https://github.com/dokku/dokku-postgres.git postgres
...
$ dokku postgres:create booktime-database
       Waiting for container to be ready
       Creating container database
       Securing connection to database
=====> Postgres container created: booktime-database
       DSN: postgres://xxx:xxx@dokku-postgres-booktime-database:5432/booktime_database

$ dokku postgres:link booktime-database booktime
-----> Setting config vars
       DATABASE_URL: postgres://xxx:xxx@dokku-postgres-booktime-database:5432/booktime_database
-----> Restarting app booktime
 !     App booktime has not been deployed

然后再说:

$ dokku plugin:install https://github.com/dokku/dokku-redis.git redis
$ dokku redis:create booktime-redis
       Waiting for container to be ready
=====> Redis container created: booktime-redis
=====> Container Information
       Config dir:          /var/lib/dokku/services/redis/booktime-redis/config
       Data dir:            /var/lib/dokku/services/redis/booktime-redis/data
...
       Status:              running
       Version:             redis:4.0.8
$ dokku redis:link booktime-redis booktime
-----> Setting config vars
...
 !     App booktime has not been deployed

我们需要启动的下两个命令是关于指定我们的booktime/settings.py需要的所有环境变量。

我们不打算将.env文件用于生产,因为我们不希望将这些关键信息存储在源存储库中。通过将这些信息放在服务器上,我们可以限制只有拥有服务器访问权限的工程师才能访问这些信息。

$ dokku config:set booktime SECRET_KEY="change this"
-----> Setting config vars
       SECRET_KEY: abcabcabc
-----> Restarting app booktime
 !     App booktime has not been deployed

$ dokku config:set booktime EMAIL_URL="consolemail://"
-----> Setting config vars
       EMAIL_URL: consolemail://
-----> Restarting app booktime
 !     App booktime has not been deployed

SECRET_KEY的值应该是一个随机的字符串,最好超过 40 个字符。对于EMAIL_URL,如果要使用 SMTP 服务器,可以指定这个格式:smtp://user:pass@server.com:25

使用以下命令,我们将指示npm在部署期间安装所有依赖项,因为我们需要运行 Webpack:

$ dokku config:set booktime NPM_CONFIG_PRODUCTION=false

Dokku 特定文件

我们的项目需要用两个构建包来构建,因为我们同时使用了 Node 和 Django。Node 由 Webpack 使用,它编译我们的 React 文件,而我们使用 Django 做其他事情。

构建包为部署和运行技术栈提供支持。他们负责将部署的代码转换成可以在容器内运行的工件。

Buildpacks 是一个在 Heroku 和 Dokku 中同样存在的概念,但是项目对它们的依赖声明是不同的。Heroku 使用一个名为app.json的应用清单,而 Dokku 使用一个极其简单的.buildpacks文件。

下面是我们需要提交的.buildpacks文件的内容:

https://github.com/heroku/heroku-buildpack-nodejs.git
https://github.com/heroku/heroku-buildpack-python.git

剩余的共同步骤

我们正在使用的 Python buildpack 自动执行一个collectstatic步骤,但不幸的是,它不处理 Webpack 编译。我们必须改变这种状况。

这个部署步骤是我们可以通过在我们的存储库中创建一个新文件bin/post_compile并提交它来添加的步骤。这是它的内容:

#!/bin/bash
export PATH=/app/.heroku/node/bin:$PATH
npm run build-prod
./manage.py collectstatic --noinput
./manage.py migrate --noinput

注意,我们还添加了migrate命令。在这里运行这个意味着我们相信我们的master分支包含的迁移已经被审查并且是稳定的。如果你对此没有信心,你可以通过 Dokku 服务器上的dokku run命令手动运行这个步骤:

$ dokku run booktime ./manage.py migrate

我们现在必须配置 git。请记下您的服务器地址。如果您创建了一个新的虚拟机,您可能还没有它的 DNS 条目,在这种情况下,您将必须使用它的 IP 地址。使用找到的值并运行以下命令:

$ git remote add dokku dokku@YOUR_DOKKU_FQDN:booktime

最后,我们可以实时推送我们的网站:

$ git push dokku master
Counting objects: 145, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (136/136), done.
Writing objects: 100% (145/145), 678.92 KiB | 7.89 MiB/s, done.
Total 145 (delta 12), reused 0 (delta 0)
-----> Cleaning up...
-----> Building booktime from herokuish...
-----> Injecting apt repositories and packages ...
-----> Adding BUILD_ENV to build environment...
----->  Warning: Multiple default buildpacks reported the ability to handle this app. The first buildpack in the list below will be used. Detected buildpacks: multi nodejs python
-----> Multipack app detected
=====> Downloading Buildpack: https://github.com/heroku/heroku-buildpack-nodejs.git
=====> Detected Framework: Node.js

-----> Creating runtime environment

       NPM_CONFIG_LOGLEVEL=error
       NPM_CONFIG_PRODUCTION=false
       NODE_VERBOSE=false
       NODE_ENV=production
       NODE_MODULES_CACHE=true

-----> Installing binaries
       engines.node (package.json): unspecified
       engines.npm (package.json): unspecified (use default)

       Resolving node version 8.x...
       Downloading and installing node 8.11.4...
       Using default npm version: 5.6.0

-----> Restoring cache
       Skipping cache restore (not-found)

-----> Building dependencies
       Installing node modules (package.json)
       added 861 packages in 30.635s

-----> Caching build
       Clearing previous node cache
       Saving 2 cacheDirectories (default):
       - node_modules
       - bower_components (nothing to cache)

-----> Pruning devDependencies
       Skipping because NPM_CONFIG_PRODUCTION is 'false'

-----> Build succeeded!
=====> Downloading Buildpack: https://github.com/heroku/heroku-buildpack-python.git
=====> Detected Framework: Python
-----> Installing python-3.6.6
-----> Installing pip
-----> Installing dependencies with Pipenv 2018.5.18...
       Installing dependencies from Pipfile.lock (93c72c)...
-----> $ python manage.py collectstatic --noinput
       194 static files copied to '/tmp/build/staticfiles', 574 post-processed.

       Using release configuration from last framework (Python).
-----> Running post-compile hook

       > booktime@1.0.0 build-prod /tmp/build
       > webpack --config webpack.prod.config.js

       Hash: 116cb912ed906360d169
       Version: webpack 4.17.1
       Time: 3997ms
       Built at: 2018-09-03 09:01:56
       Asset      Size  Chunks            Chunk Names
       imageswitcher-116cb912ed906360d169.bundle.js  100 KiB         0  [emitted]  imageswitcher
       Entrypoint imageswitcher = imageswitcher-116cb912ed906360d169.bundle.js
       [5] ./frontend/imageswitcher.js 931 bytes {0} [built]
       + 14 hidden modules

       1 static file copied to '/tmp/build/staticfiles', 195 unmodified, 412 post-process Operations to perform:
       Apply all migrations: admin, auth, authtoken, contenttypes, main, sessions
       Running migrations:
         Applying contenttypes.0001_initial... OK
         Applying contenttypes.0002_remove_content_type_name... OK
         Applying auth.0001_initial... OK
         Applying auth.0002_alter_permission_name_max_length... OK
         Applying auth.0003_alter_user_email_max_length... OK
         Applying auth.0004_alter_user_username_opts... OK
         Applying auth.0005_alter_user_last_login_null... OK
         Applying auth.0006_require_contenttypes_0002... OK
         Applying auth.0007_alter_validators_add_error_messages... OK
         Applying auth.0008_alter_user_username_max_length...  OK
         Applying auth.0009_alter_user_last_name_max_length...  OK
         Applying main.0001_initial... OK
         Applying admin.0001_initial... OK
         Applying admin.0002_logentry_remove_auto_add...   OK
         Applying admin.0003_logentry_add_action_flag_choices...OK
         Applying authtoken.0001_initial... OK
         Applying authtoken.0002_auto_20160226_1747... OK
         Applying main.0002_address... OK
         Applying main.0003_basket_basketline... OK
         Applying main.0004_auto_20180524_2143... OK
         Applying main.0005_auto_20180605_1835... OK
         Applying main.0006_auto_20180606_0801... OK
         Applying main.0007_order_last_spoken_to... OK
         Applying sessions.0001_initial... OK
       Using release configuration from last framework (Python).
-----> Discovering process types Procfile declares types -> web
-----> Releasing booktime (dokku/booktime:latest)...
-----> Deploying booktime (dokku/booktime:latest)...
-----> Attempting to run scripts.dokku.predeploy from app.json (if defined)
-----> App Procfile file found (/home/dokku/booktime/DOKKU_PROCFILE)
-----> DOKKU_SCALE file not found in app image. Generating one based on Procfile...
-----> New DOKKU_SCALE file generated
=====> web=1
-----> Attempting pre-flight checks
       For more efficient zero downtime deployments, create a file CHECKS.
       See http://dokku.viewdocs.io/dokku/deployment/zero-downtime-deploys/ for examples
       CHECKS file not found in container: Running simple container check...
-----> Waiting for 10 seconds ...
-----> Default container check successful!
-----> Running post-deploy
-----> Creating new /home/dokku/booktime/VHOST...
-----> Setting config vars DOKKU_PROXY_PORT: 80
-----> Setting config vars DOKKU_PROXY_PORT_MAP: http:80:5000
-----> Configuring booktime....(using built-in template)
-----> Creating http nginx.conf
-----> Running nginx-pre-reload
       Reloading nginx
-----> Setting config vars
       DOKKU_APP_RESTORE: 1
=====> Renaming container (b6d94002989c) elastic_ardinghelli to booktime.web.1
-----> Attempting to run scripts.dokku.postdeploy from app.json (if defined)
=====> Application deployed:
       http://booktime.YOUR_DOKKU_FQDN

如果你看到类似的输出,恭喜你!您已经成功部署了您的第一个 Django 应用。应用的最终 URL 是部署输出中最后打印的内容。

您可以使用以下命令查看该应用的日志输出:

$ dokku logs booktime -t

此时,如果您还没有这样做,您必须将主机 URL 添加到您的booktime/settings.py中的ALLOWED_HOSTS:

...

if DEBUG:
    ALLOWED_HOSTS = ['*']
else:
    ALLOWED_HOSTS = ['booktime.YOUR_DOKKU_FQDN', 'localhost']

...

提交并再次推送。下次当你浏览网址时,你会看到一个工作网站。

SSL、WebSockets 和 HTTP/2

因为我们的网站正在处理电子商务交易和管理用户数据,所以所有的连接都应该通过安全的渠道进行。现在提供 HTTPS 是一个很好的实践,但是它需要一些额外的设置。

像 Dokku 或 Heroku 这样的平台让我们很容易做到这一点。Heroku 为 SSL 证书提供了有限的内置支持,Dokku 及其 Let's Encrypt 插件为此提供了自动管理。

与 HTTP/1.x 不同,HTTP/2 内置了 SSL/TLS。但是 HTTP/2 不仅仅具有内置的安全性;这是一个比它的前身高效得多的协议,能够在一个连接中多路传输所有 HTTP 请求,并提高了压缩率。

在这些 Dokku 或 Heroku 中,Django 运行在它们的负载平衡器之后,它们管理 SSL、它的证书和 HTTP/2。Django 在这些 PaaS 环境中的作用很小。Dokku 默认提供 HTTP 到 HTTPS 的重定向,而 Heroku 不提供。

可以指示 Django 默认使用安全连接,无论是 HTTP/2 还是 HTTPS/1.x。为此,我们可以添加以下附加设置:

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

这些设置将允许 Django 检测它是否运行在安全连接上,如果不是,就触发重定向。然而,在我们的 Dokku 设置中,这是不必要的,因为 HTTP 总是被重定向。

WebSockets 是 HTTP/1.1 的扩展,也存在于 HTTP/2 中。与普通的 HTTP/1.1 连接不同,这些是长时间运行的连接。他们保持渠道畅通。Dokku 在内部使用 Nginx 作为反向代理,正确地中继这些连接。Heroku 同样能够做到这一点。

亚马逊 S3 上的媒体存储

在我们这样的项目中,可能会有成千上万的产品图像,最好将这些图像的存储和服务委托给一个为此目的而优化的独立系统。有了亚马逊 S3(或类似的)这样的系统,我们可以轻松做到这一点。

将媒体存储委托给一个专用系统还可以让我们不必管理文件系统扩展问题,也不必确保磁盘空间不会耗尽。

当部署到 PaaS 时,委派媒体存储也是唯一的方法。当使用前面提到的十二因素应用指南开发应用时,我们不能依赖于本地存储文件。对于我们部署的站点的每个新版本,都将部署一个新的容器,其中包含一个干净的文件系统。如果我们在本地存储用户上传,一旦我们网站的新版本上线,它们将变得不可用。

为此,我们需要安装两个库:

$ pipenv install boto3 django-storages

这将安装 Boto 3 和 django-storages,Boto 3 是 S3 库,django-storages 是另一个 Django 库,它将 Boto 3 链接到 Django。Django-storages 为 S3、Azure、DigitalOcean 和其他一些平台提供支持。

首先,我们需要在 S3 创建一个公众可读的存储桶。我们还需要通过 Amazon Web Services (AWS)身份和访问管理(IAM)创建必要的访问,从而实现 API 访问。我们不会详细介绍这一点,因为网上有很多关于如何做到这一点的教程。

最终,您需要一个访问密钥 ID 和一个 AWS 帐户的秘密访问密钥。不建议您使用与您的超级用户/主 AWS 帐户相关联的密钥,这就是 IAM 允许您为用户设置更合适的访问限制的原因。

以下是需要添加到我们的booktime/settings.py中的一些配置更改:

if env('AWS_ACCESS_KEY_ID', default=None):
    DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
    # AWS_S3_ENDPOINT_URL = "FQDN of Minio, only if you are using it"
    AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID')
    AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY')
    AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME')

如果在环境中指定了 AWS 访问密钥 ID,那么这段配置将更改DEFAULT_FILE_STORAGE。创建访问后,可以通过 IAM 获得访问密钥和密码。存储桶名称是您在 S3 上创建的,作为应用上传文件的目的地。

有兼容 S3 的开源云存储平台,比如 Minio。您也可以使用这些,但是您需要指定一个额外的AWS_S3_ENDPOINT_URL来覆盖 django-storages 的默认行为。

部署此更改后,您将需要设置这些新的环境变量。你可以在 Heroku 上通过仪表盘或命令行界面,或者在你的 Dokku 服务器上使用dokku config:set命令来轻松完成。在您的本地机器上,您应该将这些行包含在您的.env文件中。

设置前端变量

Django 提供了一种称为上下文处理器的抽象,用于在模板渲染阶段注入变量。在过去的章节中,我们使用了请求上下文处理器,但是现在我们将创建一个我们自己的请求上下文处理器。

我们将使用这个上下文处理器在基本模板中注入几个简单的变量,使浏览站点的任何人都可以使用它们的值。正在讨论的变量将是

  • Git 中最后一次提交的修订

  • Google Analytics 引导其 JavaScript 客户端所需的跟踪器 ID

将在新文件main/context_processors.py中创建上下文处理器:

import os
from django.conf import settings
def globals(request):
    data = {}
    data.update({
        'VERSION': os.environ.get("GIT_REV",""),
        'GA_TRACKER_ID': settings.GA_TRACKER_ID,
    })
    return data

这段代码将从名为GIT_REV的环境变量中读取,该变量存在于每个 Dokku 部署中。另一个变量GA_TRACKER_ID是从我们的设置文件中读取的。我们还需要激活这个上下文处理器。

以下是需要应用到booktime/settings.py文件的更改:

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        ...
        "OPTIONS": {
            "context_processors": [
                ...
                "main.context_processors.globals",
            ]
        },
    }
]

...

GA_TRACKER_ID = "123"

完成以上所有工作后,现在可以在 HTML 中打印这些变量,而不需要视图显式地传递这些变量。我们将在main/templates/base.html中这样做,它是站点每个页面的基础。

{% load static %}
<!doctype html>
<html lang="en">
  <head>
    ...
    <!-- Booktime version: {{ VERSION }} -->
    <script charset="utf-8">
        var tracker_id={{ GA_TRACKER_ID }};
    </script>
  </head>
...

您现在可以提交和部署这两个更改了。要验证它是否正常工作,请打开网站上的任何页面并查看 HTML 源代码。

自定义错误页面

在网站上市之前,我们希望确保最终用户不会看到任何过于吓人和无益的错误消息。如果生产中出现异常,我们想让用户放心,它会很快得到修复,他们应该会在稍后的某个时间回到站点。

我们将定制 HTTP 状态 404 和 500 的响应,当 URL 没有映射到任何内容以及代码中出现错误时,就会触发这两个状态。

Django 让这变得非常简单。当DEBUG设置为False时,Django 会在出现 404 或 500 代码错误时自动渲染模板404.html500.html

将以下内容放入顶层文件夹的templates/404.html中:

<!DOCTYPE html>

<html>

    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width"/>
        <title>404</title>
    </head>
    <body>
        <h1>We could not find this page</h1>
        <p>Please contact our support.</p>
    </body>

</html>

并将以下内容放入templates/500.html:

<!DOCTYPE html>

<html>

    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width"/>
        <title>500</title>
    </head>
    <body>
        <h1>An error has occurred</h1>
        <p>Our support team has been notified.</p>
    </body>

</html>

这两个模板故意保持简单,以便在处理初始错误时最小化出现渲染错误的机会。

错误报告

虽然对最终用户隐藏错误是一种好的做法,但我们需要确保在出现问题时,BookTime 的 it 团队得到通知。因此,我们将配置 Django 在发生错误时向管理员发送电子邮件。将此代码添加到booktime/settings.py:

...

ADMINS = (
    ('Booktime IT', 'systems@booktime.domain'),
)

EMAIL_SUBJECT_PREFIX = "[Booktime] "

在其默认配置中,Django 会在错误/异常发生时触发给所有ADMINS的电子邮件。这仅发生在DEBUG标志设置为False的部署中。

这个报告非常简单,但对于初始设置来说已经足够了。稍后,您可能希望使用第三方系统来进行错误聚合和一般管理,例如 Sentry ( https://sentry.io )。

确保测试通过

每次我们部署站点时,我们都希望确保一切正常。为此,我们依靠测试。我们的测试一定会通过。如果它们通过了,我们将有理由确信我们即将发布的网站新版本正在按预期工作。

在项目的初始阶段,我们还没有一个持续集成(CI)系统。这将是最好的解决方案,因为它会在工程师提交失败的测试时通知他们,但是建立 CI 系统需要更多的工作。对我们来说,一个简单的权宜之计是在 Heroku/Dokku 编译阶段运行测试。

我们将把我们的bin/post_compile命令改为:

#!/bin/bash
export PATH=/app/.heroku/node/bin:$PATH
npm run build-prod
./manage.py collectstatic --noinput
./manage.py migrate --noinput
npm test -- frontend
./manage.py test --noinput

这样,如果所有这些命令都成功,我们将发布;否则,我们不会。

让我们继续尝试一下:

$ git push dokku master
Counting objects: 39, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (38/38), done.
Writing objects: 100% (39/39), 3.98 KiB | 370.00 KiB/s, done.
Total 39 (delta 25), reused 0 (delta 0)
remote: Preparing /tmp/dokku_git.PJBq (identifier dokku_git.PJBq)
remote: ~/booktime /tmp/dokku_git.PJBq ~/booktime
remote: /tmp/dokku_git.PJBq ~/booktime
-----> Cleaning up...
-----> Building booktime from herokuish...

...

-----> Running post-compile hook

       > booktime@1.0.0 build-prod /tmp/build
       > webpack --config webpack.prod.config.js

...

       > booktime@1.0.0 test /tmp/build
       > jest "frontend"

remote: PASS frontend/imageswitcher.test.js
remote: - ImageBox switches images correctly (25ms)
remote:
remote: Test Suites: 1 passed, 1 total
remote: Tests:       1 passed, 1 total
remote: Snapshots:   0 total
remote: Time:        2.833s
remote: Ran all test suites matching /frontend/i.
       Creating test database for alias 'default'...
       System check identified no issues (0 silenced).

...

remote: .E
remote: =======================================================
remote: ERROR: setUpClass (main.tests.test_e2e.FrontendTests)
remote: -------------------------------------------------------
remote: Traceback (most recent call last):
remote:   File "/app/.heroku/python/lib/python3.6/site-packages/selenium/webdriver/common/service .py", line 76, in start
remote:     stdin=PIPE)
remote:   File "/app/.heroku/python/lib/python3.6/subprocess.py", line 709, in __init__
remote:     restore_signals, start_new_session)
remote:   File "/app/.heroku/python/lib/python3.6/subprocess.py", line 1344, in _execute_child
remote:     raise child_exception_type(errno_num, err_msg, err_filename)
remote:   FileNotFoundError: [Errno 2] No such file or directory: 'geckodriver': 'geckodriver'
remote:

remote:   During handling of the above exception, another exception occurred:
remote:
remote:   Traceback (most recent call last):
remote:   File "/tmp/build/main/tests/test_e2e.py", line 15, in setUpClass
remote:     cls.selenium = WebDriver()
remote:   File "/app/.heroku/python/lib/python3.6/site-packages/selenium/webdriver/firefox/webdriver.py", line 157, in __init__
remote:     self.service.start()
remote:   File "/app/.heroku/python/lib/python3.6/site-packages/selenium/webdriver/common/service.py", line 83, in start
remote:     os.path.basename(self.path), self.start_error_message)
remote:   selenium.common.exceptions.WebDriverException: Message: 'geckodriver' executable needs to be in PATH.
remote:
remote:
remote: =======================================================
remote: FAIL: test_invoice_renders_exactly_as_expected (main.tests.test_admin.TestAdminViews)
remote: -------------------------------------------------------
remote: Traceback (most recent call last):
remote:   File "/tmp/build/main/tests/test_admin.py", line 105, in test_invoice_renders_exactly_as_expected
remote:     self.assertEqual(content, expected_content)
remote: AssertionError: '\n<![111 chars].min.450fc463b8b1.css">\n <title>Invoice</t[1573 chars]tml>' != '\n<![111 chars].min.css">\n    <title>Invoice</title>\n  </he[1560 chars]tml>'
remote: Diff is 1919 characters long. Set self.maxDiff to None to see it.
remote:
remote: -------------------------------------------------------
remote: Ran 25 tests in 16.839s
remote:
remote: FAILED (failures=1, errors=1)
        Destroying test database for alias 'default'...
To ...:booktime
 ! [remote rejected] master -> master (pre-receive hook declined) error: failed to push some refs to 'dokku@...:booktime'

正如您所看到的,有几个测试不工作。第一个是硒测试。不幸的是,没有办法运行这个测试,因为它依赖于启动 Firefox。我们必须跳过它。

我们将使用 Django 的测试标记特性来标记这个测试。是main/tests/test_e2e.py的一个小变化:

from django.test import tag
...

@tag('e2e')
class FrontendTests(StaticLiveServerTestCase):
    ...

这样,我们现在可以从部署前运行的测试中排除它,这意味着将最后一行bin/post_compile改为

./manage.py test --noinput --exclude-tag=e2e

还有最后一个破测试,是关于发票的。这发生在本章,在项目中引入 WhiteNoise 的时候。WhiteNoise 不会为每个{% static %}标签评估生成相同的输出。

我们还将改变 PDF 上的测试,因为作为一种低级格式,它的二进制输出取决于太多因素,即使视觉上可能是相等的。

我们将以这种方式改变main/tests/test_admin.py:

import re
...

def compare_bodies(content, expected_content):
    c_match = re.search(r '<body>(.*)</body>', content, re.DOTALL|re.M)
    e_match = re.search(r '<body>(.*)</body>', expected_content, re.DOTALL|re.M)
    if c_match and e_match:
        return c_match.group(1) == e_match.group(1)
    return False

class TestAdminViews(TestCase):
    ...

    def test_invoice_renders_exactly_as_expected(self):
        ...

        response = self.client.get(
            reverse(
                "admin:invoice", kwargs={"order_id": order.id}
            )
        )
        self.assertEqual(response.status_code, 200)
        content = response.content.decode("utf8")

        with open(
            "main/fixtures/invoice_test_order.html","r"
        ) as fixture:
            expected_content = fixture.read()

        self.assertTrue(compare_bodies(content, expected_content))

        response = self.client.get(
            reverse(
                "admin:invoice", kwargs={"order_id": order.id}
            ),
            {"format": "pdf"},
        )
        self.assertEqual(response.status_code, 200)
        content = response.content

        with open(
            "main/fixtures/invoice_test_order.pdf", "rb"
        ) as fixture:
            expected_content = fixture.read()

        self.assertEqual(content[:5], expected_content[:5])

我们对断言进行了如下更改:

  • 测试 HTML 版本的<body>元素的内容。这将消除问题,并仍然给我们带来测试的好处。

  • 仅通过测试文件签名来测试 PDF。我们将测试 PDF 是相同的二进制文件改为测试它是有效的 PDF。就目前而言,这是一个可以接受的妥协。

摘要

本章举例说明了每个项目在发布时必须经历的许多常见步骤。在成品中,可伸缩性、安全性和日志记录都是需要注意的重要事情,但在开发阶段,它们不是主要关注的问题。

我们已经看到了如何使用 JavaScript 和 Python 工具来优化静态资产的传输和执行速度,因为我试图选择最适合这项工作的工具,而不考虑语言。

尽管这本书不是关于部署和配置服务器的,但是没有提到如何做也不能给你完整的描述。PaaS 不是唯一的部署方式,但它肯定是最简单的方式之一,这也是我一开始就选择它的原因。

我们还探索了与 S3 等对象存储的集成。对于需要管理大量媒体文件的网站,选择这些工具可以省去很多麻烦。将这些与 Django 集成很容易。

我希望你喜欢这本书。现在,您已经掌握了处理大多数 Django 项目的必要知识。请尽情享受吧!

Footnotes 1

https://en.wikipedia.org/wiki/Reverse_proxy

  2

https://www.digitalocean.com/products/one-click-apps/dokku/