最近在Flask Web Development作者博客看到第二版Flask Mega-Tutorial已在2017年底更新,现翻译给大家参考,希望帮助大家学习flask。
这是Flask Mega-Tutorial系列的第十九章,我将在其中部署Microblog到Docker容器平台。
供您参考,以下是本系列文章的列表。
注意1:如果您正在寻找本教程的旧版本,请在此处。
注意2:如果您想在此博客上支持我的工作,或者只是没有耐心等待每周的文章,我将提供完整的本教程版本,打包成电子书或视频集。欲了解更多信息,请访问courses.miguelgrinberg.com。
在第17章中,您学习了传统部署,在传统部署中,您必须关注服务器配置的每个细节。然后在第18章中,当我向您介绍Heroku时,您将带到了另一个极端。这项服务可以完全控制配置和部署任务,使您可以完全专注于应用程序。在本章中,您将学习基于容器(尤其是在Docker容器平台上)的第三种应用程序部署策略。 这种部署的工作量,介于另外两个选项之间。
容器基于轻量级虚拟化技术构建,该技术允许应用程序及其依赖项和配置完全隔离地运行,而无需使用功能强大的虚拟化解决方案(例如虚拟机),因为虚拟化解决方案需要更多的资源,并且有时可能与宿主机相比,性能明显下降。 配置为容器宿主机的系统可以运行大量容器,所有这些容器共享主机的内核并直接访问主机的硬件。这与虚拟机不同,虚拟机必须模拟一个完整的系统,包括CPU,磁盘,其他硬件,内核等。
尽管必须共享内核,但容器中的隔离级别仍然很高。容器具有自己的文件系统,并且可以基于与容器主机使用的操作系统不同的操作系统。例如,您可以在Fedora主机上运行基于Ubuntu Linux的容器,反之亦然。尽管容器是Linux操作系统固有的技术,但由于虚拟化,还可以在Windows和Mac OS X主机上运行Linux容器。 这允许您在开发系统上测试部署操作,并且如果您愿意的话,还可以将容器合并到开发工作流程中去。
尽管Docker不是唯一的容器平台,但它是迄今为止最受欢迎的容器平台,因此我选择了它。Docker有两个版本,一个是免费社区版(CE),另一个是基于订阅的企业版(EE)。就本教程而言,Docker CE完全可以满足要求。
要使用Docker CE,首先必须将其安装在系统上。在Docker网站上有适用于Windows,Mac OS X的安装程序和几种Linux发行版。如果您使用的是Microsoft Windows系统,请务必注意Docker CE依赖Hyper-V。 如有必要,安装程序将为您启用此功能,但请记住,启用Hyper-V会限制诸如VirtualBox等其他虚拟化技术产品的运行。
在系统上安装Docker CE之后,您可以通过在终端窗口或命令提示符下键入以下命令来验证安装是否成功:
$ docker version
Client:
Version: 17.09.0-ce
API version: 1.32
Go version: go1.8.3
Git commit: afdb6d4
Built: Tue Sep 26 22:40:09 2017
OS/Arch: darwin/amd64
Server:
Version: 17.09.0-ce
API version: 1.32 (minimum version 1.12)
Go version: go1.8.3
Git commit: afdb6d4
Built: Tue Sep 26 22:45:38 2017
OS/Arch: linux/amd64
Experimental: true
为Microblog创建容器的第一步是为其创建镜像。容器镜像是用于创建容器的模板。它包含容器文件系统的完整表示,以及与网络,启动选项等有关的各种设置。
为应用程序创建容器镜像的最基本方法是启动要使用的基本操作系统(Ubuntu,Fedora等)的容器,连接到其中运行的bash shell进程,然后手动安装应用程序,可以参照我在第17章中介绍的传统部署指南。安装完所有内容后,您可以为容器保存快照,该快照即成为容器镜像。docker
命令支持这种类型的工作流,但是我不打算讨论它,因为每次需要生成新镜像时都必须手动安装应用程序并不方便。
更好的方法是通过脚本生成容器镜像。创建脚本化容器镜像的命令是docker build
。该命令从名为Dockerfile的文件读取并执行构建指令,我将需要创建该文件。Dockerfile基本上是一种安装程序脚本,执行安装步骤以部署应用程序,以及一些特定于容器的设置。
这是Microblog的基础Dockerfile:
Dockerfile:用于Microblog的Dockerfile
FROM python:3.6-alpine
RUN adduser -D microblog
WORKDIR /home/microblog
COPY requirements.txt requirements.txt
RUN python -m venv venv
RUN venv/bin/pip install -r requirements.txt
RUN venv/bin/pip install gunicorn
COPY app app
COPY migrations migrations
COPY microblog.py config.py boot.sh ./
RUN chmod +x boot.sh
ENV FLASK_APP microblog.py
RUN chown -R microblog:microblog ./
USER microblog
EXPOSE 5000
ENTRYPOINT ["./boot.sh"]
Dockerfile中的每一行都是一个命令。FROM
命令指定将在其上构建新镜像的基础容器镜像。 这样一来,您从一个现有的镜像开始,添加或改变一些东西,并最终得到一个派生的镜像。 镜像由名称和标签来标记,它们之间用冒号分隔。 该标签用作版本控制机制,允许容器镜像提供多个版本。 我选择的镜像的名称是python
,它是Python的官方Docker镜像。 该镜像的标签允许您指定解释器版本和基础操作系统。 3.6-alpine
标签选择安装在Alpine Linux上的Python 3.6解释器。 由于其体积小,Alpine Linux发行版比起更常见的发行版(例如Ubuntu)会更多地被使用。 您可以在Python镜像库中查看Python镜像可用的标签。
RUN
命令在容器的上下文中执行任意命令。这与您在shell提示符下输入命令相似。 adduser -D microblog
命令创建一个名为microblog
的新用户。 大多数容器镜像都使用root
作为默认用户,但以root身份运行应用程序并不是一个好习惯,所以我创建了自己的用户。
WORKDIR
命令设置将要安装应用程序的默认目录。 当我在上面创建microblog
用户时,会自动创建了一个主目录,所以现在我将该目录设置为默认目录。 在Dockerfile中的任何剩余命令执行以及运行容器时,其当前目录为这个默认目录。
COPY
命令将文件从您的计算机传输到容器文件系统。此命令采用两个或多个参数,即源文件/目录和目标文件/目录。 源文件必须与Dockerfile所在的目录相关。 目的地可以是绝对路径,也可以是相对于在之前的WORKDIR
命令中设置的目录的路径。 在这第一个COPY
命令中,我将requirements.txt文件复制到容器文件系统的microblog
用户的主目录中
现在,容器中已经包含了requirements.txt文件,我可以使用以下RUN
命令创建虚拟环境了。首先创建它,然后在其中安装所有依赖。由于依赖文件仅包含通用依赖项,因此我将显式安装gunicorn,并将其用作Web服务器。或者,我可以将gunicorn添加到我的requirements.txt文件中。
后面的三个COPY
命令通过复制app包,含有数据库迁移的migrations目录以及中的microblog.py和config.py脚本。 我还复制了一个新文件,boot.sh,我将在下面讨论它。
RUN chmod
命令确保将此新的boot.sh文件正确设置为可执行文件。 如果您使用的是基于Unix的文件系统,并且您的源文件已被标记为可执行文件,则复制的文件将会已是可执行的。 我显式地对其进行授权,是因为在Windows上很难设置可执行位。 如果您正在使用Mac OS X或Linux,您可能不需要这个步骤,但有了它也不会有什么问题。
ENV
命令在容器内设置环境变量。我需要设置FLASK_APP
,这是使用flask
命令所依赖的。
下面的RUN chown
命令将所有存储在/home/microblog中的目录和文件的所有者设置为新microblog
用户。即使我在Dockerfile顶部附近创建了该用户,所有命令的默认用户仍然保留root
,因此所有这些文件都需要切换到microblog
用户,以便在容器启动时该用户可以正确运行这些文件。
下一行中的USER
命令使得这个新的microblog
用户成为任何后续指令的默认用户,并且也是容器启动时的默认用户。
EXPOSE
命令配置该容器将用于服务的端口。 这是必要的,以便Docker可以适当地在容器中配置网络。 我选择了标准的Flask端口5000,但这其实可以是任意端口。
最后,ENTRYPOINT
命令定义了容器启动时应该执行的默认命令。 这是启动应用程序Web服务器的命令。 为了保持良好的代码组织逻辑,我决定为此创建一个单独的脚本,正是我之前复制到容器的boot.sh文件。 这里是这个脚本的内容:
boot.sh:Docker容器启动脚本。
#!/bin/sh
source venv/bin/activate
flask db upgrade
flask translate compile
exec gunicorn -b :5000 --access-logfile - --error-logfile - microblog:app
这是一个相当标准的启动脚本, 与第17章和第18章的部署启动非常类似。激活虚拟环境,通过迁移框架升级数据库,编译语言翻译,最后使用gunicorn运行服务器。
请注意gunicorn命令之前的exec
。在shell脚本中,exec
触发运行脚本的进程被给定的命令替换,而不是将其作为新进程启动。这很重要,因为Docker将容器的生命周期与在其上运行的第一个进程相关联 在像这样的情况下,启动进程不是容器的主进程,您需要确保主进程取代启动进程,以确保容器不会提前停止。
Docker一个有趣的方面是,容器写入stdout
或stderr
任何内容都将被捕获并存储为容器的日志。 出于这个原因,-access-logfile
和--error-logfile
都配置为-
,它将日志发送到标准输出,以便它们作为日志由Docker存储。
创建了Dockerfile之后,我现在可以构建一个容器镜像:
$ docker build -t microblog:latest .
我为docker build
命令提供的-t
参数设置了新容器镜像的名称和标记。.
表示容器构建的基础目录,这就是Dockerfile所在的目录。 构建过程将执行Dockerfile中的所有命令并创建镜像,该镜像将存储在您自己的机器上。
您可以使用以下docker images
命令获取本地镜像的列表:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
microblog latest 54a47d0c27cf About a minute ago 216MB
python 3.6-alpine a6beab4fa70b 3 months ago 88.7MB
此清单将包括您的新镜像,以及构建它的基础镜像。每当您对应用程序进行更改时,都可以通过再次运行build命令来更新容器镜像。
使用已经创建的镜像,您现在可以运行应用程序的容器版本。通过docker run
命令,通常再搭配大量的参数,就可以完成容器的启动。 我将首先向您展示一个基本的例子:
$ docker run --name microblog -d -p 8000:5000 --rm microblog:latest
021da2e1e0d390320248abf97dfbbe7b27c70fefed113d5a41bb67a68522e91c
--name
选项提供了新容器的名称。-d
选项告诉Docker在后台运行容器。如果没有-d,
容器将作为前台应用程序运行,则会阻止您的命令提示符。-p
选项将容器端口映射到主机端口。第一个端口是主机上的端口,右侧的端口是容器内的端口。 上面的例子暴露了主机端口8000,其对应容器中的端口5000,因此即使内部容器使用5000,您也将在宿主机上访问端口8000来访问应用程序。 一旦容器停止,--rm
选项将使其自动被删除。 虽然这不是必需的,但完成或中断的容器通常不再需要,因此可以自动删除。 最后一个参数是容器使用的容器镜像名称和标签。 运行上述命令后,可以在 http://localhost:8000上访问该应用。
docker run
的输出是分配给新容器的ID。这是一个十六进制长字符串,在随后的命令中您可以使用它来引用容器。 实际上,只有前几个字符是必需的,足以保证ID的唯一性。
如果要查看正在运行的容器,可以使用以下docker ps
命令:
$ docker ps
CONTAINER ID IMAGE COMMAND PORTS NAMES
021da2e1e0d3 microblog:latest "./boot.sh" 0.0.0.0:8000->5000/tcp microblog
您可以看到,即使该docker ps
命令也缩短了容器ID。如果现在要停止容器,则可以使用docker stop
:
$ docker stop 021da2e1e0d3
021da2e1e0d3
如果您还记得的话,应用配置中有许多选项是从环境变量中获取的。例如,Flask密钥、数据库URL和电子邮件服务器选项都是从环境变量导入的。在上面的docker run
示例中,我没有考虑这些,因此所有这些配置选项都将使用默认值。
在一个更实际的示例中,您将在容器内设置这些环境变量。在上一节中,您已经看到Dockerfile中的ENV
命令设置了环境变量对于将变为静态的变量来说,这是一个方便的选项。。但是,对于依赖于安装的变量,将它们作为构建过程的一部分并不方便,因为您希望拥有一个移植性好的容器镜像。如果您想将应用程序作为容器镜像提供给另一个人,您希望该人员能够按原样使用它,而不必使用不同的变量重新构建它。
因此构建时环境变量可能很有用,但是也需要有可以通过docker run
命令设置的运行时环境变量,对于这些变量,可以使用-e
选项来设置。 以下示例设置了密钥和gmail帐户:
$ docker run --name microblog -d -p 8000:5000 --rm -e SECRET_KEY=my-secret-key \
-e MAIL_SERVER=smtp.googlemail.com -e MAIL_PORT=587 -e MAIL_USE_TLS=true \
-e MAIL_USERNAME=<your-gmail-username> -e MAIL_PASSWORD=<your-gmail-password> \
microblog:latest
由于具有许多环境变量定义,docker run
命令行变得如此长的情况并不罕见。
Microblog的容器版本看起来不错,但我还没有真正考虑过很多关于存储的问题。实际上,由于我尚未设置DATABASE_URL
环境变量,因此应用程序正在使用默认SQLite数据库并将数据存储在容器内部的文件系统上。 当您停止并删除容器时,您认为数据去哪里了?该文件将消失!
容器中的文件系统是临时的,这意味着它随着容器的删除而删除。 您可以将数据写入容器内的文件系统,并且容器可以正常读写数据,但如果出于任何原因需要回收容器并将其替换为新的容器,则应用程序保存到容器内的任何数据将永远丢失。
容器应用程序的一种好的设计策略是使应用程序容器变为无状态。如果您有一个包含应用程序代码且没有数据的容器,则可以将其丢弃并替换为新容器,而不会出现任何问题,该容器将成为真正的一次性容器,这在简化升级部署方面非常有用。
当然,这意味着必须将数据放置在应用程序容器之外的某个位置。这就是梦幻般的Docker生态系统发挥作用的地方。Docker容器镜像仓库包含大量的容器镜像。您已经了解了Python容器镜像,我正在使用它作为我的Microblog容器的基础镜像。 除此之外,Docker还为Docker容器镜像仓库中的许多其他语言,数据库和其他服务维护镜像,如果这还不够,Docker容器镜像仓库还允许公司为其产品发布容器镜像,并且像您我这样的常规用户也可以发布自己的镜像。 这意味着安装第三方服务需要做出的努力会减少成只需在Docker容器镜像仓库中找到合适的镜像,并通过带有适当参数的docker run
命令启动它。
因此,我现在要做的是创建两个额外的容器,一个用于MySQL数据库,另一个用于Elasticsearch服务,然后我将使用以下选项,使启动Microblog容器的命令行更长一些:使它能够访问这两个新容器。
像许多其他产品和服务一样,MySQL在Docker镜像仓库中提供了公共容器镜像。就像我自己的Microblog容器一样,MySQL依赖于需要传递给docker run
的环境变量。他们配置密码、数据库名称等。在镜像仓库中有许多MySQL镜像时,我决定使用由MySQL官方团队维护的镜像。 您可以在其镜像仓库页面找到有关MySQL容器镜像的详细信息: https://hub.docker.com/r/mysql/mysql-server/ 。
如果您记得第17章中设置MySQL的繁琐过程,当您看到部署MySQL多么容易时,您将欣赏Docker。这是启动MySQL服务器的docker run
命令:
$ docker run --name mysql -d -e MYSQL_RANDOM_ROOT_PASSWORD=yes \
-e MYSQL_DATABASE=microblog -e MYSQL_USER=microblog \
-e MYSQL_PASSWORD=<database-password> \
mysql/mysql-server:5.7
这就对了!在安装了Docker的任何计算机上,您都可以运行上面的命令,就会得到一个完成安装的MySQL服务器,它具有一个随机生成的root密码,一个名为microblog
的全新数据库和一个名字相同的用户,该用户具备访问这个数据库的所有权限。 请注意,您需要输入正确的密码,以便它可以从MYSQL_PASSWORD
环境变量获得。
现在在应用端,我需要添加一个MySQL客户端程序包,就像在Ubuntu上进行传统部署一样。我将再次使用pymysql
,可以将其添加到Dockerfile中:
Dockerfile:将pymysql添加到Dockerfile中。
# ...
RUN venv/bin/pip install gunicorn pymysql
# ...
每当对应用程序或Dockerfile进行更改时,都需要重建容器镜像:
$ docker build -t microblog:latest .
现在我可以再次启动Microblog,但这一次可以连接到数据库容器,以便两者都可以通过网络进行通信:
$ docker run --name microblog -d -p 8000:5000 --rm -e SECRET_KEY=my-secret-key \
-e MAIL_SERVER=smtp.googlemail.com -e MAIL_PORT=587 -e MAIL_USE_TLS=true \
-e MAIL_USERNAME=<your-gmail-username> -e MAIL_PASSWORD=<your-gmail-password> \
--link mysql:dbserver \
-e DATABASE_URL=mysql+pymysql://microblog:<database-password>@dbserver/microblog \
microblog:lates
--link
选项告诉Docker让正要运行的容器可以访问参数中指定的容器。 该参数包含由冒号分隔的两个名称。 第一部分是要链接的容器的名称或ID,在本例中是我在上面创建的一个名为mysql
的容器。 第二部分定义了一个可以在这个容器中用来引用链接的主机名。 这里我使用dbserver
作为代表数据库服务器的通用名称。
在两个容器之间建立了链接之后,我可以设置DATABASE_URL
环境变量,以便SQLAlchemy可以在另一个容器中使用MySQL数据库。数据库URL将使用dbserver
作为数据库主机名,microblog
作为数据库名称和用户,以及您在启动MySQL时选择的密码。
我在试用MySQL容器时注意到的一件事是,该容器要完全运行并准备接受数据库连接需要花费几秒钟的时间。如果您启动MySQL容器,然后立即启动应用容器,则当boot.sh脚本尝试运行flask db upgrade
时,它可能会由于数据库尚未准备好接受连接而失败。为了使我的解决方案更可靠,我决定在boot.sh中添加一个重试循环:
boot.sh:重试数据库连接。
#!/bin/sh
source venv/bin/activate
while true; do
flask db upgrade
if [[ "$?" == "0" ]]; then
break
fi
echo Upgrade command failed, retrying in 5 secs...
sleep 5
done
flask translate compile
exec gunicorn -b :5000 --access-logfile - --error-logfile - microblog:app
此循环检查flask db upgrade
命令的退出代码,如果它不为零,则认为出了点问题,因此等待五秒钟,然后重试。
Elasticsearch Docker文档演示了如何将该服务作为单一节点以用于开发模式,以及部署两个节点的生产环境服务。 现在,我将使用单节点模式,并使用引擎开源的“oss”镜像。 容器使用以下命令启动:
$ docker run --name elasticsearch -d -p 9200:9200 -p 9300:9300 --rm \
-e "discovery.type=single-node" \
docker.elastic.co/elasticsearch/elasticsearch-oss:7.6.2
docker run
命令与我用于Microblog和MySQL的命令有很多相似之处,但是有一些有趣的区别。首先,有两个-p
选项,这意味着此容器将在两个端口上侦听,而不仅仅是一个。端口9200和9300都映射到主机中的相同端口。
另一个区别在于用于引用容器镜像的语法。对于我在本地构建的镜像,语法是<name>:<tag>
。 MySQL容器使用格式为稍微更完整的<account>/<name>:<tag>
语法,适用于在Docker镜像仓库中引用容器镜像。 我使用的Elasticsearch镜像遵循模式<registry>/<account><name>:<tag>
,其中包括镜像仓库的地址作为第一个组件。 此语法用于未托管在Docker镜像仓库中的镜像。 在本处,Elasticsearch在docker.elastic.co上运行自己的容器镜像仓库服务,而不是使用由Docker维护的主镜像仓库。
因此,既然我已经启动并运行了Elasticsearch服务,我可以修改Microblog容器的启动命令以创建指向它的链接并设置Elasticsearch服务URL:
$ docker run --name microblog -d -p 8000:5000 --rm -e SECRET_KEY=my-secret-key \
-e MAIL_SERVER=smtp.googlemail.com -e MAIL_PORT=587 -e MAIL_USE_TLS=true \
-e MAIL_USERNAME=<your-gmail-username> -e MAIL_PASSWORD=<your-gmail-password> \
--link mysql:dbserver \
-e DATABASE_URL=mysql+pymysql://microblog:<database-password>@dbserver/microblog \
--link elasticsearch:elasticsearch \
-e ELASTICSEARCH_URL=http://elasticsearch:9200 \
microblog:latest
在运行此命令之前,请记住如果仍在运行它,请停止它之前的Microblog容器。另外,请注意在命令中的正确位置为数据库和Elasticsearch服务设置正确的密码。
现在,您应该可以访问http://localhost:8000并使用搜索功能。如果遇到任何错误,可以通过查看容器日志来解决它们。您很可能希望查看Microblog容器的日志,其中将显示任何Python堆栈跟踪:
$ docker logs microblog
现在我已经在Docker上使用三个容器来运行了完整的应用程序,其中两个容器来自公开的第三方镜像。 如果您想提供自己的容器镜像给其他人,那么您必须将它们推送到任何人都可以获取到的Docker镜像仓库中。
要访问Docker镜像仓库,您需要转到 https://hub.docker.com 并为自己创建一个帐户。 确保您选择一个您喜欢的用户名,因为这将用于您发布的所有镜像。
为了能够从命令行访问您的账户,您需要使用docker login
命令登录:
$ docker login
如果您一直跟随我的引导,现在您的计算机上已经有一个名为microblog:latest
的镜像存储在本地。 为了能够将这个镜像推送到Docker镜像仓库中,它需要重新命名以包含该帐户,正如来自MySQL的镜像。 这是通过docker tag
命令完成的:
$ docker tag microblog:latest <your-docker-registry-account>/microblog:latest
如果您再次用docker images
列出您的镜像,您会看到两个Microblog条目,一个是microblog:latest
,另一个还包括您的帐户名。 它们实际上是同一镜像的两个别名。
要将镜像发布到Docker镜像仓库,请使用docker push
命令:
$ docker push <your-docker-registry-account>/microblog:latest
现在您的镜像被公开了,您可以像MySQL和服务那样,说明如何安装它并从Docker镜像仓库运行。
让您的应用程序在Docker容器中运行的最大的好处之一是,一旦该容器在您的本地测试通过了,就可以将它们运行到任何提供Docker支持的平台。 例如,您可以使用第十七章中推荐的Digital Ocean,Linode或Amazon Lightsail上的相同服务器。 即使这些提供商提供的最便宜的产品也足以让Docker运行一些容器。
Amazon Container Service(ECS)使您能够创建一个容器宿主机集群,以在其中运行容器。在集成完备的AWS环境中,提供了水平扩展和负载平衡,以及为容器镜像使用私有容器镜像仓库的功能。
最后,容器编排平台例如Kubernetes通过允许您以简单的YAML格式文本文件描述您的多容器部署逻辑,来提供了更高级别的自动化和便利性, 负载均衡,水平扩展,密钥的安全管理以及滚动升级和回滚。