多容器环境

为为这个容器分配一个 IP 地址在上一节中,我们看到使用 Docker 运行应用程序非常容易。我们从一个简单的静态网站开始,然后尝试使用 Flask 应用程序。只需要几个命令就可以在本地和云端运行。这些应用程序的一个共同点就是它们在一个 容器 中运行。

如果你有生产环境运行服务的经验就会知道,通常一个应用程序不会那么简单。几乎总是涉及数据库(或任何其他类型的持久存储)。像 RedisMemcached 这样的系统已经成为大多数 Web 应用体系结构的必备品。那么在本节中,我们将花一些时间学习如何对依赖不同服务来运行的应用程序进行 Docker 化。

特别是,我们将看到如何运行和管理 多容器 docker 环境。为什么要使用多容器呢?Docker 的一个关键点是它提供隔离的方式。将进程与其依赖绑定在沙箱(也就是容器)中的想法使其如此强大。

正如将应用程序层分离开来是一种很好的策略一样,聪明的做法是将每个服务的容器分开。每一层可能有不同的资源需求,这些需求可能会以不同的速度增长。通过将层分离到不同的容器中,我们可以根据不同的资源需求使用最合适的实例类型来组合每一层。这在整个 微服务 运动中也发挥了很好的作用,这也是Docker (或任何其他容器技术) 处于现代微服务体系结构 最前沿 的主要原因之一。

SF Food Trucks

我们要 Docker 化的应用叫做 SF Food Trucks。构建这个应用的原因是因为它拥有一些有价值的东西(它很像一个真实的应用),至少依赖于一种服务,对本教程而言并不是太复杂。

SF Food Trucks

应用的后端是用 Python(Flask)编写的,搜索引擎使用的 Elasticsearch。和教程中的其他内容一样,Github 上提供了整个源代码。我们将使用它作为我们的候选应用来了解如何构建、运行和部署多容器环境。

首先,在本地克隆仓库。

$ git clone https://github.com/prakhar1989/FoodTrucks
$ cd FoodTrucks
$ tree -L 2
.
├── Dockerfile
├── README.md
├── aws-compose.yml
├── docker-compose.yml
├── flask-app
│ ├── app.py
│ ├── package-lock.json
│ ├── package.json
│ ├── requirements.txt
│ ├── static
│ ├── templates
│ └── webpack.config.js
├── setup-aws-ecs.sh
├── setup-docker.sh
├── shot.png
└── utils
├── generate_geojson.py
└── trucks.geojson

flask-app文件夹包含 Python 应用,而utils文件夹中有一些工具程序可以将数据加载到 Elasticsearch 中。该目录还包含一些 YAML 配置文件和一个 Dockerfile,我们将在本教程中逐步详细的了解这些文件。如果你有什么疑问可以随机查看文件。

既然你很兴奋(希望是这样),让我们想一想如何将应用 Docker 化。我们可以看到该应用包含 Flask 后端服务和 Elasticsearch 服务。将这个应用程序分开的一种简单的方法是使用两个容器 - 一个运行 Flask 进程,另一个运行 Elasticsearch(ES)进程。这样,如果我们的应用变得流行,可以通过添加更多容器来扩展它,如何扩展取决于瓶颈是什么。

所以我们需要两个容器。这应该不难吧!我们已经在上一节中构建了自己的 Flask 容器。对于 Elasticsearch,让我们看看是否可以在 docker hub 上找到一些有用的。

$ docker search elasticsearch
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
elasticsearch Elasticsearch is a powerful open source se... 697 [OK]
itzg/elasticsearch Provides an easily configurable Elasticsea... 17 [OK]
tutum/elasticsearch Elasticsearch image - listens in port 9200. 15 [OK]
barnybug/elasticsearch Latest Elasticsearch 1.7.2 and previous re... 15 [OK]
digitalwonderland/elasticsearch Latest Elasticsearch with Marvel & Kibana 12 [OK]
monsantoco/elasticsearch ElasticSearch Docker image 9 [OK]

不出所料,官方有一个现成的 Elasticsearch 镜像。为了让 ES 运行,我们可以简单的使用docker run在本地运行单节点 ES 容器。

注意:Elastic 是 Elasticsearch 背后的公司,有自己的 注册中心。如果你打算使用Elasticsearch,建议使用注册中心中的镜像。

我们先来拉取镜像吧

$ docker pull docker.elastic.co/elasticsearch/elasticsearch:6.3.2

然后通过指定端口并设置一个环境变量来配置 Elasticsearch 集群作为单节点运行,在开发模式下运行它。

$ docker run -d --name es -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:6.3.2
277451c15ec183dd939e80298ea4bcf55050328a39b04124b387d668e3ed3943

如上所示,我们使用--name es为容器命名,方便在后续命令中使用。启动容器后,我们可以通过运行docker container logs来查看日志,其中包含容器名称(或 容器ID)。如果 Elasticsearch 启动成功,你应该会看到类似于下面的日志。

注意:Elasticsearch 的启动需要几秒钟,所以在看到日志中初始化之前需要稍等片刻。

$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
277451c15ec1 docker.elastic.co/elasticsearch/elasticsearch:6.3.2 "/usr/local/bin/dock…" 2 minutes ago Up 2 minutes 0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp es
$ docker container logs es
[2018-07-29T05:49:09,304][INFO ][o.e.n.Node ] [] initializing ...
[2018-07-29T05:49:09,385][INFO ][o.e.e.NodeEnvironment ] [L1VMyzt] using [1] data paths, mounts [[/ (overlay)]], net usable_space [54.1gb], net total_space [62.7gb], types [overlay]
[2018-07-29T05:49:09,385][INFO ][o.e.e.NodeEnvironment ] [L1VMyzt] heap size [990.7mb], compressed ordinary object pointers [true]
[2018-07-29T05:49:11,979][INFO ][o.e.p.PluginsService ] [L1VMyzt] loaded module [x-pack-security]
[2018-07-29T05:49:11,980][INFO ][o.e.p.PluginsService ] [L1VMyzt] loaded module [x-pack-sql]
[2018-07-29T05:49:11,980][INFO ][o.e.p.PluginsService ] [L1VMyzt] loaded module [x-pack-upgrade]
[2018-07-29T05:49:11,980][INFO ][o.e.p.PluginsService ] [L1VMyzt] loaded module [x-pack-watcher]
[2018-07-29T05:49:11,981][INFO ][o.e.p.PluginsService ] [L1VMyzt] loaded plugin [ingest-geoip]
[2018-07-29T05:49:11,981][INFO ][o.e.p.PluginsService ] [L1VMyzt] loaded plugin [ingest-user-agent]
[2018-07-29T05:49:17,659][INFO ][o.e.d.DiscoveryModule ] [L1VMyzt] using discovery type [single-node]
[2018-07-29T05:49:18,962][INFO ][o.e.n.Node ] [L1VMyzt] initialized
[2018-07-29T05:49:18,963][INFO ][o.e.n.Node ] [L1VMyzt] starting ...
[2018-07-29T05:49:19,218][INFO ][o.e.t.TransportService ] [L1VMyzt] publish_address {172.17.0.2:9300}, bound_addresses {0.0.0.0:9300}
[2018-07-29T05:49:19,302][INFO ][o.e.x.s.t.n.SecurityNetty4HttpServerTransport] [L1VMyzt] publish_address {172.17.0.2:9200}, bound_addresses {0.0.0.0:9200}
[2018-07-29T05:49:19,303][INFO ][o.e.n.Node ] [L1VMyzt] started
[2018-07-29T05:49:19,439][WARN ][o.e.x.s.a.s.m.NativeRoleMappingStore] [L1VMyzt] Failed to clear cache for realms [[]]
[2018-07-29T05:49:19,542][INFO ][o.e.g.GatewayService ] [L1VMyzt] recovered [0] indices into cluster_state

现在,我们试试向 Elasticsearch 容器发送请求。向9200端口的容器发送cURL请求。

$ curl 0.0.0.0:9200
{
"name" : "ijJDAOm",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "a_nSV3XmTCqpzYYzb-LhNw",
"version" : {
"number" : "6.3.2",
"build_flavor" : "default",
"build_type" : "tar",
"build_hash" : "053779d",
"build_date" : "2018-07-20T05:20:23.451332Z",
"build_snapshot" : false,
"lucene_version" : "7.3.1",
"minimum_wire_compatibility_version" : "5.6.0",
"minimum_index_compatibility_version" : "5.0.0"
},
"tagline" : "You Know, for Search"
}

哇塞!看起来不错!这个时候我们让 Flask 容器也运行起来。在我们开始之前,我们需要一个Dockerfile。在上一节中,我们使用python:3-onbuild作为基础镜像。但是,这一次,除了安装使用 pip 安装 Python 依赖项之外,我们还希望应用程序生成用于生产的压缩后的 Javascript 文件。所以,我们还需要 Nodejs。由于我们需要自定义构建步骤,因此我们将从ubuntu基础镜像的Dockerfile开始从头开始构建。

注意:如果你发现现有的镜像不能满足您的需求,可以随意从另一个基础镜像开始,然后自行调整。对于 Docker Hub 上的大多数镜像,一般可以在 Github 上找到相应的 Dockerfile。在开始动手编写前阅读现成的 Dockerfile 是最好方法之一。

我们的 Flask 应用的 Dockerfile 如下所示:

# start from base
FROM ubuntu:14.04
MAINTAINER Prakhar Srivastav <[email protected]>
# install system-wide deps for python and node
RUN apt-get -yqq update
RUN apt-get -yqq install python-pip python-dev curl gnupg
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash
RUN apt-get install -yq nodejs
# copy our application code
ADD flask-app /opt/flask-app
WORKDIR /opt/flask-app
# fetch app specific deps
RUN npm install
RUN npm run build
RUN pip install -r requirements.txt
# expose port
EXPOSE 5000
# start app
CMD [ "python", "./app.py" ]

这里有很多新东西,我们快速浏览一下这个文件。我们从 Ubuntu LTS 基础镜像开始,并使用包管理器apt-get来安装依赖项 Python 和 Node。yqq选项用于禁止输出,并假设所有提示都是 “yes”。

然后,我们使用ADD命令将我们的应用程序复制到容器中卷中 - 位于/opt/flask-app。这是我们代码所在的位置。我们还需要将它设置为工作目录,以便于在这个位置的上下文中运行下面的命令。现在我们已经安装了系统范围的依赖,然后可以安装应用的依赖。首先,我们通过使用 npm 运行我们package.json 文件 中定义的 build 命令来处理 Node 。我们通过安装 Python 包,暴露端口并定义要运行的CMD来结束文件,就像在上一节中所做的那样。

最后,我们可以继续构建镜像并运行容器(替换下面的prakhar1989为你自己的用户名)。

$ git clone https://github.com/prakhar1989/FoodTrucks && cd FoodTrucks
$ docker build -t prakhar1989/foodtrucks-web .

第一次运行时,可能要花一点时间,因为 Docker 客户端要下载 ubuntu 镜像,运行所有命令并准备镜像。在对应用代码进行任何更改后重新运行 docker build 几乎是瞬时的,现在尝试运行我们的应用。

$ docker run -P --rm prakhar1989/foodtrucks-web
Unable to connect to ES. Retying in 5 secs...
Unable to connect to ES. Retying in 5 secs...
Unable to connect to ES. Retying in 5 secs...
Out of retries. Bailing out...

完了😰!我们的 Flask 应用竟然不能运行,对。因为它不能连接到 Elasticsearch。那我们如何告诉一个容器关于另一个容器的信息并让它们通信?继续往下看。

Docker 网络

在我们讨论 Docker 提供的专门用于处理此类场景的特性之前,先来尝试是否能够找到解决问题的方法。希望这能让你们对我们将要学习的具体特性有所了解。

好吧,让我们运行 docker container ls(于 docker ps 相同),看看都有什么。

$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
277451c15ec1 docker.elastic.co/elasticsearch/elasticsearch:6.3.2 "/usr/local/bin/dock…" 17 minutes ago Up 17 minutes 0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp es

我们有一个 ES 容器运行在 0.0.0.0:9200 端口上,我们可以直接访问它。如果我们能让 Flask app 连接到这个 URL,它就能连接到 ES,对吧?让我们深入研究一下 Python 代码,看看如何定义连接。

es = Elasticsearch(host='es')

要让它可以运行,我们需要告诉 Flask 容器 ES 容器在0.0.0.0主机上运行(默认情况下是端口9200),这样才可以工作,对吧?很不幸,这样不可以,因为 IP 0.0.0.0是从 宿主机(也就是我的 Mac)访问 ES容器的 IP 。另一个容器没法通过这个 IP 地址上访问,那么问题来了,如果这个 IP 不能访问,那么 ES 容器应该访问哪个 IP 地址?你可以问这个问题我很高兴。

现在是时候探索 Docker 的网络。安装 docker 时,它会自动创建三个网络。

$ docker network ls
NETWORK ID NAME DRIVER SCOPE
c2c695315b3a bridge bridge local
a875bec5d6fd host host local
ead0e804a67b none null local

桥接网络 是默认运行容器时的网络。这意味着当我运行 ES 容器时,它在这个桥接网络中运行。为了验证这一点,让我们检查一下网络

$ docker network inspect bridge
[
{
"Name": "bridge",
"Id": "c2c695315b3aaf8fc30530bb3c6b8f6692cedd5cc7579663f0550dfdd21c9a26",
"Created": "2018-07-28T20:32:39.405687265Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"277451c15ec183dd939e80298ea4bcf55050328a39b04124b387d668e3ed3943": {
"Name": "es",
"EndpointID": "5c417a2fc6b13d8ec97b76bbd54aaf3ee2d48f328c3f7279ee335174fbb4d6bb",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]

你可以看到我们的容器 277451c15ec1Containers 部分输出了。我们还可以看到为这个容器分配一个 IP 地址 172.17.0.2。这不正是我们需要的 IP 地址吗?我们通过运行我们的 Flask 容器并尝试访问这个 IP 来查找。

$ docker run -it --rm prakhar1989/foodtrucks-web bash
[email protected]:/opt/flask-app# curl 172.17.0.2:9200
{
"name" : "Jane Foster",
"cluster_name" : "elasticsearch",
"version" : {
"number" : "2.1.1",
"build_hash" : "40e2c53a6b6c2972b3d13846e450e66f4375bd71",
"build_timestamp" : "2015-12-15T13:05:55Z",
"build_snapshot" : false,
"lucene_version" : "5.3.1"
},
"tagline" : "You Know, for Search"
}
[email protected]:/opt/flask-app# exit

这对你来说应该很简单了。我们使用 bash 进程以交互模式启动容器。--rm 会在容器在完成工作后清理容器。我们使用 curl 尝试,但我们需要先安装它。一旦我们这样做了,我们就可以在 172.17.0.2:9200 和 ES 通信。完美😏!

虽然我们已经找到了让容器相互通信的方法,但这种方法仍然存在两个问题:

  1. 我们如何告诉 Flask 容器es主机名代表什么?172.17.0.2还是其他IP,因为 IP 是会变化的。

  2. 由于默认情况下桥接 网络被每个容器共享,所以这种方法不安全。我们如何隔离网络?

一个好消息是 Docker 对我们的问题有很棒的答案。它允许我们定义自己的网络,同时使用docker network命令来隔离它们。

我们来创建自己的网络。

$ docker network create foodtrucks-net
0815b2a3bb7a6608e850d05553cc0bda98187c4528d94621438f31d97a6fea3c
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
c2c695315b3a bridge bridge local
0815b2a3bb7a foodtrucks-net bridge local
a875bec5d6fd host host local
ead0e804a67b none null local

使用network create命令创建了一个新的桥接网络,这是我们目前所需要的。就 Docker 而言,桥接网络使用软件进行桥接,这个软件桥接器允许连接到同一桥接网络的容器进行通信,同时提供与未连接到该桥接网络的容器的隔离。Docker 网桥驱动程序会自动在主机中安装,以便不同网桥上的容器无法直接相互通信。你可以创建其他类型的网络,推荐你在官方 文档 中阅读它们。

现在我们有了一个网络,我们可以使用--net选项在这个网络中启动我们的容器。在操作前我们先停止在当前桥接桥(默认)网络中运行的 ES 容器。

$ docker container stop es
es
$ docker run -d --name es --net foodtrucks-net -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:6.3.2
13d6415f73c8d88bddb1f236f584b63dbaf2c3051f09863a3f1ba219edba3673
$ docker network inspect foodtrucks-net
[
{
"Name": "foodtrucks-net",
"Id": "0815b2a3bb7a6608e850d05553cc0bda98187c4528d94621438f31d97a6fea3c",
"Created": "2018-07-30T00:01:29.1500984Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"13d6415f73c8d88bddb1f236f584b63dbaf2c3051f09863a3f1ba219edba3673": {
"Name": "es",
"EndpointID": "29ba2d33f9713e57eb6b38db41d656e4ee2c53e4a2f7cf636bdca0ec59cd3aa7",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]

如你所见,我们的es容器现在在foodtrucks-net桥接网络中运行。现在来看看当我们在foodtrucks-net网络中启动时会发生什么。

$ docker run -it --rm --net foodtrucks-net prakhar1989/foodtrucks-web bash
[email protected]:/opt/flask-app# curl es:9200
{
"name" : "wWALl9M",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "BA36XuOiRPaghPNBLBHleQ",
"version" : {
"number" : "6.3.2",
"build_flavor" : "default",
"build_type" : "tar",
"build_hash" : "053779d",
"build_date" : "2018-07-20T05:20:23.451332Z",
"build_snapshot" : false,
"lucene_version" : "7.3.1",
"minimum_wire_compatibility_version" : "5.6.0",
"minimum_index_compatibility_version" : "5.0.0"
},
"tagline" : "You Know, for Search"
}
[email protected]:/opt/flask-app# ls
app.py node_modules package.json requirements.txt static templates webpack.config.js
[email protected]:/opt/flask-app# python app.py
Index not found...
Loading data in elasticsearch ...
Total trucks loaded: 733
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
[email protected]:/opt/flask-app# exit

(⊙o⊙) 哇!这样是可以的!在像 foodtrucks-net 这样的自定义网络上,容器不仅可以通过 IP 地址进行通信,还能将容器名称解析为 IP 地址。这个功能称为自动服务发现。现在运行我们的 Flask 容器。

$ docker run -d --net foodtrucks-net -p 5000:5000 --name foodtrucks-web prakhar1989/foodtrucks-web
852fc74de2954bb72471b858dce64d764181dca0cf7693fed201d76da33df794
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
852fc74de295 prakhar1989/foodtrucks-web "python ./app.py" About a minute ago Up About a minute 0.0.0.0:5000->5000/tcp foodtrucks-web
13d6415f73c8 docker.elastic.co/elasticsearch/elasticsearch:6.3.2 "/usr/local/bin/dock…" 17 minutes ago Up 17 minutes 0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp es
$ curl -I 0.0.0.0:5000
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 3697
Server: Werkzeug/0.11.2 Python/2.7.6
Date: Sun, 10 Jan 2016 23:58:53 GMT

访问 http://localhost:5000,来看看你伟大的程序可以预览了!虽然这可能看起来有很多步骤,但实际上我们从开始到运行只输入了 4 个命令。我在 bash脚本 中整理了命令。

#!/bin/bash
# build the flask container
docker build -t prakhar1989/foodtrucks-web .
# create the network
docker network create foodtrucks-net
# start the ES container
docker run -d --name es --net foodtrucks-net -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:6.3.2
# start the flask app container
docker run -d --net foodtrucks-net -p 5000:5000 --name foodtrucks-web prakhar1989/foodtrucks-web

现在假设你准备将应用的链接发给你的朋友,或者在安装了docker 的服务器上运行。你可以用一个命令运行整个应用!

$ git clone https://github.com/prakhar1989/FoodTrucks
$ cd FoodTrucks
$ ./setup-docker.sh

这就是我们完成的东西!我们发现这是一个非常棒的分享和运行你的应用的强大方式!

Docker Compose

到目前为止,我们一直在探索 Docker 的客户端。然而,在 Docker 生态系统中,还有许多其他的开源工具可以很好地与 Docker 一起工作。其中包含:

  1. Docker Machine—在你的计算机、云服务商和自己的数据中心内创建 Docker 主机

  2. Docker Compose—一个用于定义和运行多容器 Docker 应用的工具

  3. Docker Swarm—Docker 的本机集群解决方案

  4. Kubernetes—Kubernetes 是一个开源系统,用于自动化部署、扩展和管理容器化应用程序

在本节中,我们将介绍其中一个工具 Docker Compose,并了解如何使用它来轻松的处理多容器的应用。

Docker Compose 的背景故事非常有意思。大概两年前,一家名为 OrchardUp 的公司推出了一个名为 Fig 的工具。这个项目在 Hacker News 上很受欢迎——奇怪的是,我记得读到过它,但不太了解它。

论坛的 第一个评论 实际上很好地解释了 Fig 的全部内容。

所以在这一点上,这就是 Docker 的意义所在:运行进程。现在 Docker 提供了一个非常丰富的API 来运行进程:容器(也就是运行镜像)之间的共享卷(目录)、从主机到容器的转发端口、显示日志等等。但是,到目前为止,Docker 仍然处于进程级别。

虽然它提供了编排多个容器以创建单个 “应用程序” 的选项,但它没有将此类容器组的管理作为单个实体来处理。这就是 Fig 这类工具的作用:将一组容器作为一个整体来讨论。想想 “运行一个应用程序” (也就是 “运行一个编排好的容器集群”),而不是 “运行一个容器”。

事实证明,很多使用码头的人都同意这种观点。随着 Fig 变得越来越流行,Docker 公司开始注意到了这个问题,然后收购了这家公司,并将 Fig 重新命名为 Docker Compose。

那么 Compose 用来做什么呢?Compose 是一种工具,以一种简单的方式定义和运行多容器 Docker 应用。它提供了一个名为docker-compose.yml的配置文件,仅通过一个命令就可以启动应用程序和它依赖的服务套件。尽管 Compose 是开发和测试环境的理想选择,实际上它适用于所有环境:生产、预发布,开发、测试以及 CI 工作流。

我们来试试可否为前面的 SF-Foodtrucks 应用来创建一个 docker-compose.yml,并测试 Compose 是否可以正常运作。

第一步是安装 Compose。如果你是 Windows 或 Mac,Compose 已经安装在 Docker Toolbox 中。Linux 用户可以按照文档上的 说明 获得 Compose 。由于 Compose 是用 Python 编写的,所以你也可以使用pip install docker-compose的方式安装,测试你的安装。

$ docker-compose --version
docker-compose version 1.21.2, build a133471

安装后我们可以跳到下一步,编写 Compose 文件docker-compose.yml。YAML 的语法非常简单,而且 repo 已经包含了我们将要使用的 docker-compose 文件

version: "3"
services:
es:
image: docker.elastic.co/elasticsearch/elasticsearch:6.3.2
container_name: es
environment:
- discovery.type=single-node
ports:
- 9200:9200
volumes:
- esdata1:/usr/share/elasticsearch/data
web:
image: prakhar1989/foodtrucks-web
command: python app.py
depends_on:
- es
ports:
- 5000:5000
volumes:
- ./flask-app:/opt/flask-app
volumes:
esdata1:
driver: local

我来解释下文件中都是什么意思。在父级,我们定义了服务的名称 - esweb。对于 Docker 需要运行的每个服务,我们可以从 image 中添加额外的参数。对于es,我们只是参考 Elastic 注册中心提供的 elasticsearch 镜像。对于 Flask 应用,我们参考在本节开头构建的镜像。

通过其他参数,例如commandports可以提供有关容器的更多信息。volumes参数指定web代码所在容器中的挂载点,代码会驻留在这里。不过这完全是可选的,比如你需要访问日志的时候会很有用。稍后我们将看到它在开发过程中有什么用。关于该文件支持的参数的详细信息,请参阅 在线文档 。我们还需要为es容器添加卷,以便加载的数据在重新启动后可以持久化。我们还指定了depends_on,告诉 docker 在启动 web 之前启动 es 容器。你也可以在 docker compose docs 上阅读更多相关信息。

注意:你必须在带有 docker-come.yml 文件的目录中执行大多数 Compose 命令

很好😉!现在文件已经准备好了,让我们看看 docker-compose 的实际运行情况。在开始之前,我们需要确保端口没有被占用。所以如果你有 Flask 和 ES 容器运行,先关闭它们。

$ docker stop $(docker ps -q)
39a2f5df14ef
2a1b77e066e6

现在我们可以运行docker-compose了。进到 food trucks 目录并运行docker-compose up

$ docker-compose up
Creating network "foodtrucks_default" with the default driver
Creating foodtrucks_es_1
Creating foodtrucks_web_1
Attaching to foodtrucks_es_1, foodtrucks_web_1
es_1 | [2016-01-11 03:43:50,300][INFO ][node ] [Comet] version[2.1.1], pid[1], build[40e2c53/2015-12-15T13:05:55Z]
es_1 | [2016-01-11 03:43:50,307][INFO ][node ] [Comet] initializing ...
es_1 | [2016-01-11 03:43:50,366][INFO ][plugins ] [Comet] loaded [], sites []
es_1 | [2016-01-11 03:43:50,421][INFO ][env ] [Comet] using [1] data paths, mounts [[/usr/share/elasticsearch/data (/dev/sda1)]], net usable_space [16gb], net total_space [18.1gb], spins? [possibly], types [ext4]
es_1 | [2016-01-11 03:43:52,626][INFO ][node ] [Comet] initialized
es_1 | [2016-01-11 03:43:52,632][INFO ][node ] [Comet] starting ...
es_1 | [2016-01-11 03:43:52,703][WARN ][common.network ] [Comet] publish address: {0.0.0.0} is a wildcard address, falling back to first non-loopback: {172.17.0.2}
es_1 | [2016-01-11 03:43:52,704][INFO ][transport ] [Comet] publish_address {172.17.0.2:9300}, bound_addresses {[::]:9300}
es_1 | [2016-01-11 03:43:52,721][INFO ][discovery ] [Comet] elasticsearch/cEk4s7pdQ-evRc9MqS2wqw
es_1 | [2016-01-11 03:43:55,785][INFO ][cluster.service ] [Comet] new_master {Comet}{cEk4s7pdQ-evRc9MqS2wqw}{172.17.0.2}{172.17.0.2:9300}, reason: zen-disco-join(elected_as_master, [0] joins received)
es_1 | [2016-01-11 03:43:55,818][WARN ][common.network ] [Comet] publish address: {0.0.0.0} is a wildcard address, falling back to first non-loopback: {172.17.0.2}
es_1 | [2016-01-11 03:43:55,819][INFO ][http ] [Comet] publish_address {172.17.0.2:9200}, bound_addresses {[::]:9200}
es_1 | [2016-01-11 03:43:55,819][INFO ][node ] [Comet] started
es_1 | [2016-01-11 03:43:55,826][INFO ][gateway ] [Comet] recovered [0] indices into cluster_state
es_1 | [2016-01-11 03:44:01,825][INFO ][cluster.metadata ] [Comet] [sfdata] creating index, cause [auto(index api)], templates [], shards [5]/[1], mappings [truck]
es_1 | [2016-01-11 03:44:02,373][INFO ][cluster.metadata ] [Comet] [sfdata] update_mapping [truck]
es_1 | [2016-01-11 03:44:02,510][INFO ][cluster.metadata ] [Comet] [sfdata] update_mapping [truck]
es_1 | [2016-01-11 03:44:02,593][INFO ][cluster.metadata ] [Comet] [sfdata] update_mapping [truck]
es_1 | [2016-01-11 03:44:02,708][INFO ][cluster.metadata ] [Comet] [sfdata] update_mapping [truck]
es_1 | [2016-01-11 03:44:03,047][INFO ][cluster.metadata ] [Comet] [sfdata] update_mapping [truck]
web_1 | * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

打开浏览器输入 IP 访问你的应用。惊不惊喜?神不神奇?只要几行配置,我们就让两个 Docker 容器一起运行成功。下面停止服务用分离模式(后台进程)重新运行。

web_1 | * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
Killing foodtrucks_web_1 ... done
Killing foodtrucks_es_1 ... done
$ docker-compose up -d
Creating es ... done
Creating foodtrucks_web_1 ... done
$ docker-compose ps
Name Command State Ports
--------------------------------------------------------------------------------------------
es /usr/local/bin/docker-entr ... Up 0.0.0.0:9200->9200/tcp, 9300/tcp
foodtrucks_web_1 python app.py Up 0.0.0.0:5000->5000/tcp

不出所料,我们可以看到两个容器都运行成功。这些名字从哪儿来的?那些是由 Compose 自动创建的。但Compose 也会自动创建网络吗?好奇。。让我们来看看。

先停止运行服务。我们总是可以使用一个命令中重新启动它们。数据卷会一直存在,所以可以使用 docker-compose 重新启动集群。要销毁集群和数据卷,只需要输入docker-compose down -v

$ docker-compose down -v
Stopping foodtrucks_web_1 ... done
Stopping es ... done
Removing foodtrucks_web_1 ... done
Removing es ... done
Removing network foodtrucks_default
Removing volume foodtrucks_esdata1

当我们这样操作的时候,也要删除上次创建的foodtrucks网络。

$ docker network rm foodtrucks-net
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
c2c695315b3a bridge bridge local
a875bec5d6fd host host local
ead0e804a67b none null local

现在我们已经有了一个干净的平台,重新运行我们的服务,看看Compose 还有什么魔法。

$ docker-compose up -d
Recreating foodtrucks_es_1
Recreating foodtrucks_web_1
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f50bb33a3242 prakhar1989/foodtrucks-web "python app.py" 14 seconds ago Up 13 seconds 0.0.0.0:5000->5000/tcp foodtrucks_web_1
e299ceeb4caa elasticsearch "/docker-entrypoint.s" 14 seconds ago Up 14 seconds 9200/tcp, 9300/tcp foodtrucks_es_1

到目前为止,一切顺利。是时候看看是否建立了网络。

$ docker network ls
NETWORK ID NAME DRIVER
c2c695315b3a bridge bridge local
f3b80f381ed3 foodtrucks_default bridge local
a875bec5d6fd host host local
ead0e804a67b none null local

可以看到,compose 继续创建了一个名为 foodtrucks_default 的新网络,并将这两个新服务都附加到这个网络中,便于让它们可以被其他服务发现。服务的每个容器都连接到默认网络,并且可以被该网络上的其他容器访问,同时可以在与容器名称相同的主机名中被发现。

$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8c6bb7e818ec docker.elastic.co/elasticsearch/elasticsearch:6.3.2 "/usr/local/bin/dock…" About a minute ago Up About a minute 0.0.0.0:9200->9200/tcp, 9300/tcp es
7640cec7feb7 prakhar1989/foodtrucks-web "python app.py" About a minute ago Up About a minute 0.0.0.0:5000->5000/tcp foodtrucks_web_1
$ docker network inspect foodtrucks_default
[
{
"Name": "foodtrucks_default",
"Id": "f3b80f381ed3e03b3d5e605e42c4a576e32d38ba24399e963d7dad848b3b4fe7",
"Created": "2018-07-30T03:36:06.0384826Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.19.0.0/16",
"Gateway": "172.19.0.1"
}
]
},
"Internal": false,
"Attachable": true,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"7640cec7feb7f5615eaac376271a93fb8bab2ce54c7257256bf16716e05c65a5": {
"Name": "foodtrucks_web_1",
"EndpointID": "b1aa3e735402abafea3edfbba605eb4617f81d94f1b5f8fcc566a874660a0266",
"MacAddress": "02:42:ac:13:00:02",
"IPv4Address": "172.19.0.2/16",
"IPv6Address": ""
},
"8c6bb7e818ec1f88c37f375c18f00beb030b31f4b10aee5a0952aad753314b57": {
"Name": "es",
"EndpointID": "649b3567d38e5e6f03fa6c004a4302508c14a5f2ac086ee6dcf13ddef936de7b",
"MacAddress": "02:42:ac:13:00:03",
"IPv4Address": "172.19.0.3/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {
"com.docker.compose.network": "default",
"com.docker.compose.project": "foodtrucks",
"com.docker.compose.version": "1.21.2"
}
}
]

开发工作流

在我们跳到下一部分之前,我还想讲最后一件关于 docker-compose 的事。像前面所描述的,docker-compose 非常适合开发和测试。那么接下来我们看看如何配置 compose 以使我们的开发过程中更容易。

在这个教程中,我们使用了现成的 docker 镜像。虽然我们从头开始构建镜像,但还没有触及任何程序代码,主要局限于编辑 Dockerfile 和 YAML 配置。你一定想知道在开发过程中工作流是什么样的?是否应该为每次更改继续创建 Docker 镜像,然后发布,然后运行它以查看更改是否和预期的一样?我确信这听起来很没劲。一定有更好的方法。在本节中,这就是我们要探索的内容。

我们看看如何在我们刚刚运行的 Foodtrucks 程序中进行修改,首先确保你的应用在运行。

$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5450ebedd03c prakhar1989/foodtrucks-web "python app.py" 9 seconds ago Up 6 seconds 0.0.0.0:5000->5000/tcp foodtrucks_web_1
05d408b25dfe docker.elastic.co/elasticsearch/elasticsearch:6.3.2 "/usr/local/bin/dock…" 10 hours ago Up 10 hours 0.0.0.0:9200->9200/tcp, 9300/tcp es

现在让我们看看我们是否可以改变这个程序,当访问 /hello 的时候显示一个 Hello world !向/hello路由发出请求时的消息。目前程序的响应是 404。

$ curl -I 0.0.0.0:5000/hello
HTTP/1.0 404 NOT FOUND
Content-Type: text/html
Content-Length: 233
Server: Werkzeug/0.11.2 Python/2.7.15rc1
Date: Mon, 30 Jul 2018 15:34:38 GMT

为什么会这样?由于这是一个 Flask 应用,所以可以在app.py中找到答案。在 Flask 中,使用 @ app.route 语法来定义路由。在这个文件理,你会看到,我们只有三条路由定义 //debug/search/路由显示主应用程序,debug路由用于返回一些调试信息,最后的search路由用于查询elasticsearch。

$ curl 0.0.0.0:5000/debug
{
"msg": "yellow open sfdata Ibkx7WYjSt-g8NZXOEtTMg 5 1 618 0 1.3mb 1.3mb\n",
"status": "success"
}

根据上文我们将如何添加新路由hello?你应该猜到了!使用你喜欢的编辑器打开flask-app/app.py然后进行以下修改:

@app.route('/')
def index():
return render_template("index.html")
# add a new hello route
@app.route('/hello')
def hello():
return "hello world!"

再请求一次试试

$ curl -I 0.0.0.0:5000/hello
HTTP/1.0 404 NOT FOUND
Content-Type: text/html
Content-Length: 233
Server: Werkzeug/0.11.2 Python/2.7.15rc1
Date: Mon, 30 Jul 2018 15:34:38 GMT

纳尼 😱!竟然没用!这是什么鬼?虽然我们确实修改了app.py,但文件驻留在我们的机器(宿主机)中,但由于 Docker 是基于prakhar1989/foodtrucks-web镜像运行的容器,所以它不知道这次修改。要验证这一点,可以尝试:

$ docker-compose run web bash
Starting es ... done
[email protected]:/opt/flask-app# ls
app.py package-lock.json requirements.txt templates
node_modules package.json static webpack.config.js
[email protected]:/opt/flask-app# grep hello app.py
[email protected]:/opt/flask-app# exit

这里我们要做的是验证我们的修改并非是容器中运行的 app.py。通过运行命令来完成这个操作,它有点像 docker run 的兄弟docker compose run,但是可以为服务提供额外的参数(在我们的 例子 中是web)。只要运行 bash,shell 就会按照 Dockerfile 中指定的方式在 /opt/flask-app 中打开。通过 grep 命令,可以看到我们的修改不在文件中。

来看看我们如何解决它。首先,我们需要告诉 docker compose 不要使用镜像,而是在本地使用这些文件。我们还需要将调试模式设置为true以便 Flask 知道在app.py被修改时重新加载服务。替换 docker-compose.ymlweb 部分。yml 文件如下:

version: "3"
services:
es:
image: docker.elastic.co/elasticsearch/elasticsearch:6.3.2
container_name: es
environment:
- discovery.type=single-node
ports:
- 9200:9200
volumes:
- esdata1:/usr/share/elasticsearch/data
web:
build: . # replaced image with build
command: python app.py
environment:
- DEBUG=True # set an env var for flask
depends_on:
- es
ports:
- "5000:5000"
volumes:
- ./flask-app:/opt/flask-app
volumes:
esdata1:
driver: local

有了这些更改(差异),然后停止并启动容器。

$ docker-compose down -v
Stopping foodtrucks_web_1 ... done
Stopping es ... done
Removing foodtrucks_web_1 ... done
Removing es ... done
Removing network foodtrucks_default
Removing volume foodtrucks_esdata1
$ docker-compose up -d
Creating network "foodtrucks_default" with the default driver
Creating volume "foodtrucks_esdata1" with local driver
Creating es ... done
Creating foodtrucks_web_1 ... done

最后让我们通过添加一个新路由来更改app.py。现在使用 curl 访问

$ curl 0.0.0.0:5000/hello
hello world

Wohoo!我们得到了想要的 hello world!可以尝试通过在应用中修改一些其他的来玩。

上面就是我们要介绍的 Docker Compose 全部内容。使用 Docker Compose,你还可以暂停服务,在容器上运行一次性命令,甚至扩展容器的数量。我还建议你查看一些其他 用例 的 Docker Compose。希望我能够向你展示使用 Compose 管理多容器环境是多么容易。最后,我们将应用部署到 AWS!

AWS Elastic 容器服务

在上一节中,我们使用docker-compose在本地运行我们的应用,只用了一个命令:docker-compose up。现在我们有一个功能正常的应用,我想和全世界分享它,获得一些用户,赚大钱,然后再北京三环买套房子。当然后面的几个需求超出了本教程的范围,下面我们来花点儿时间介绍如何在 AWS 云端部署我们的多容器应用。

如果你已经阅读过这篇文章,你一定深信 Docker 是一项很酷的技术。你不是一个人,随着 Docker 的迅速崛起,几乎所有云服务商都在其平台上添加对 Docker 应用部署的支持。截至今天,你可以在 Google Cloud PlatformAWSAzure 和许多其他设备上部署容器。我们已经获得了使用 Elastic Beanstalk 部署单个容器应用的入门知识,在本节中我们将介绍 AWS 的 Elastic 容器服务(或ECS)。

AWS ECS 是一种可扩展且灵活的容器管理服务,支持 Docker 容器。它允许你通过一个简单的 API 在 EC2 实例上操作 Docker 集群。在 Beanstalk 合理的默认设置下,ECS 允许你根据需要调整环境。在我看来,这让我觉得 ECS 在开始的时候非常复杂。

还好 ECS 提供了一个友好的 CLI 工具,可以解析 Compose 文件并自动在 ECS 上配置集群!因为我们已经有了一个功能齐全的 docker-compose,在 AWS 上运行它不会花费太多精力。来开始吧!

第一步是安装 CLI。在 官方文档 中非常清楚地描述了如何在 Mac 和 Linux 上安装 CLI。安装完成后,验证安装:

$ ecs-cli --version
ecs-cli version 0.1.0 (*cbdc2d5)

首先获取用于登录实例的密钥对。在 EC2控制台 创建一个新的密钥对,下载密钥对然后保存在一个你认为安全的位置。在关闭界面前需要注意另外一件事就是区域名称。我已经命名了我的密钥 - 并且在 ecs中将区域设置为us-east-1。这是我在本次演示的其余部分中假设的内容。

EC2 Keypair

接下来配置 CLI。

$ ecs-cli configure --region us-east-1 --cluster foodtrucks
INFO[0000] Saved ECS CLI configuration for cluster (foodtrucks)

我们使用 configure 命令,其中包含我们希望集群驻留的区域名称和集群名称。确保你提供与创建密钥对时相同的区域名称。如果你之前还没在电脑上配置过 AWS CLI,那么你可以查看 官方指南,它详细解释了你该如何顺利进行。

下一步使用 CLI 创建 CloudFormation 模板。

$ ecs-cli up --keypair ecs --capability-iam --size 2 --instance-type t2.micro
INFO[0000] Created cluster cluster=foodtrucks
INFO[0001] Waiting for your cluster resources to be created
INFO[0001] Cloudformation stack status stackStatus=CREATE_IN_PROGRESS
INFO[0061] Cloudformation stack status stackStatus=CREATE_IN_PROGRESS
INFO[0122] Cloudformation stack status stackStatus=CREATE_IN_PROGRESS
INFO[0182] Cloudformation stack status stackStatus=CREATE_IN_PROGRESS
INFO[0242] Cloudformation stack status stackStatus=CREATE_IN_PROGRESS

这里我们提供最初下载的密钥对的名称(在我的例子中是ecs),想要使用的实例数(--size)以及希望容器运行的实例类型。--capability-iam选项告诉 CLI 我们确认该命令可以创建 IAM 资源。

最后一步是使用我们的docker-compose.yml文件。我们需要做一点小的改动,所以不要修改原始版本,只是复制并重命名为aws-compose.yml。这个 文件 修改如下所示:

es:
image: elasticsearch
cpu_shares: 100
mem_limit: 262144000
web:
image: prakhar1989/foodtrucks-web
cpu_shares: 100
mem_limit: 262144000
ports:
- "80:5000"
links:
- es

我们对原来的 docker-compose.yml 做的唯一修改是为每个容器提供mem_limitcpu_shares值。我们还删除了versionservices键,因为 AWS 还不支持 Compose 文件格式的 第2版。因为我们的应用会运行在t2.micro实例上,所以我分配了 250mb 的内存。在我们进入下一步之前,需要做的另一件事是在 Docker Hub 上发布我们的镜像。在撰写本文时,ecs-cli 还不支持build命令 - Compose 完美支持 build。

$ docker push prakhar1989/foodtrucks-web

终于到头了!现在让我们运行最后一个命令,它将在 ECS 上部署我们的应用程序!

$ ecs-cli compose --file aws-compose.yml up
INFO[0000] Using ECS task definition TaskDefinition=ecscompose-foodtrucks:2
INFO[0000] Starting container... container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/es
INFO[0000] Starting container... container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/web
INFO[0000] Describe ECS container status container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-foodtrucks:2
INFO[0000] Describe ECS container status container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/es desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-foodtrucks:2
INFO[0036] Describe ECS container status container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/es desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-foodtrucks:2
INFO[0048] Describe ECS container status container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-foodtrucks:2
INFO[0048] Describe ECS container status container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/es desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-foodtrucks:2
INFO[0060] Started container... container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/web desiredStatus=RUNNING lastStatus=RUNNING taskDefinition=ecscompose-foodtrucks:2
INFO[0060] Started container... container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/es desiredStatus=RUNNING lastStatus=RUNNING taskDefinition=ecscompose-foodtrucks:2

上面的调用方式与我们使用 Compose 相似,这不是巧合。--file 参数会指定一个 Compose 文件,这将覆盖默认文件(docker-compose.yml)。顺利的话,你应该在最后一行看到desiredStatus=RUNNING lastStatus=RUNNING

我们的应用是实时的,该如何访问它呢?

ecs-cli ps
Name State Ports TaskDefinition
845e2368-170d-44a7-bf9f-84c7fcd9ae29/web RUNNING 54.86.14.14:80->5000/tcp ecscompose-foodtrucks:2
845e2368-170d-44a7-bf9f-84c7fcd9ae29/es RUNNING ecscompose-foodtrucks:2

继续在你的浏览器中打开 http://54.86.14.14,你应该看到 Food Trucks 的所有黑黄色的荣耀!既然我们正在讨论这个话题,让我们看看 AWS ECS 控制台的外观。

任务

我们可以看到上面已经创建了名为 “foodtrucks” 的 ECS 集群,现在正在运行带有 2 个容器实例的任务。花点时间浏览这个控制台,了解一下这里的所有选项。

就这样。只用几个命令,我们就可以在 AWS 云端部署我们优秀的应用程序!