为为这个容器分配一个 IP 地址在上一节中,我们看到使用 Docker 运行应用程序非常容易。我们从一个简单的静态网站开始,然后尝试使用 Flask 应用程序。只需要几个命令就可以在本地和云端运行。这些应用程序的一个共同点就是它们在一个 容器 中运行。
如果你有生产环境运行服务的经验就会知道,通常一个应用程序不会那么简单。几乎总是涉及数据库(或任何其他类型的持久存储)。像 Redis 和 Memcached 这样的系统已经成为大多数 Web 应用体系结构的必备品。那么在本节中,我们将花一些时间学习如何对依赖不同服务来运行的应用程序进行 Docker 化。
特别是,我们将看到如何运行和管理 多容器 docker 环境。为什么要使用多容器呢?Docker 的一个关键点是它提供隔离的方式。将进程与其依赖绑定在沙箱(也就是容器)中的想法使其如此强大。
正如将应用程序层分离开来是一种很好的策略一样,聪明的做法是将每个服务的容器分开。每一层可能有不同的资源需求,这些需求可能会以不同的速度增长。通过将层分离到不同的容器中,我们可以根据不同的资源需求使用最合适的实例类型来组合每一层。这在整个 微服务 运动中也发挥了很好的作用,这也是Docker (或任何其他容器技术) 处于现代微服务体系结构 最前沿 的主要原因之一。
我们要 Docker 化的应用叫做 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 elasticsearchNAME DESCRIPTION STARS OFFICIAL AUTOMATEDelasticsearch 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.2277451c15ec183dd939e80298ea4bcf55050328a39b04124b387d668e3ed3943
如上所示,我们使用--name es
为容器命名,方便在后续命令中使用。启动容器后,我们可以通过运行docker container logs
来查看日志,其中包含容器名称(或 容器ID)。如果 Elasticsearch 启动成功,你应该会看到类似于下面的日志。
注意:Elasticsearch 的启动需要几秒钟,所以在看到日志中初始化之前需要稍等片刻。
$ docker container lsCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES277451c15ec1 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 baseFROM ubuntu:14.04MAINTAINER Prakhar Srivastav <[email protected]># install system-wide deps for python and nodeRUN apt-get -yqq updateRUN apt-get -yqq install python-pip python-dev curl gnupgRUN curl -sL https://deb.nodesource.com/setup_8.x | bashRUN apt-get install -yq nodejs# copy our application codeADD flask-app /opt/flask-appWORKDIR /opt/flask-app# fetch app specific depsRUN npm installRUN npm run buildRUN pip install -r requirements.txt# expose portEXPOSE 5000# start appCMD [ "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-webUnable 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 container ls
(于 docker ps
相同),看看都有什么。
$ docker container lsCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES277451c15ec1 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 lsNETWORK ID NAME DRIVER SCOPEc2c695315b3a bridge bridge locala875bec5d6fd host host localead0e804a67b 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": {}}]
你可以看到我们的容器 277451c15ec1
在 Containers
部分输出了。我们还可以看到为这个容器分配一个 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 通信。完美😏!
虽然我们已经找到了让容器相互通信的方法,但这种方法仍然存在两个问题:
我们如何告诉 Flask 容器es
主机名代表什么?172.17.0.2
还是其他IP,因为 IP 是会变化的。
由于默认情况下桥接 网络被每个容器共享,所以这种方法不安全。我们如何隔离网络?
一个好消息是 Docker 对我们的问题有很棒的答案。它允许我们定义自己的网络,同时使用docker network
命令来隔离它们。
我们来创建自己的网络。
$ docker network create foodtrucks-net0815b2a3bb7a6608e850d05553cc0bda98187c4528d94621438f31d97a6fea3c$ docker network lsNETWORK ID NAME DRIVER SCOPEc2c695315b3a bridge bridge local0815b2a3bb7a foodtrucks-net bridge locala875bec5d6fd host host localead0e804a67b none null local
使用network create
命令创建了一个新的桥接网络,这是我们目前所需要的。就 Docker 而言,桥接网络使用软件进行桥接,这个软件桥接器允许连接到同一桥接网络的容器进行通信,同时提供与未连接到该桥接网络的容器的隔离。Docker 网桥驱动程序会自动在主机中安装,以便不同网桥上的容器无法直接相互通信。你可以创建其他类型的网络,推荐你在官方 文档 中阅读它们。
现在我们有了一个网络,我们可以使用--net
选项在这个网络中启动我们的容器。在操作前我们先停止在当前桥接桥(默认)网络中运行的 ES 容器。
$ docker container stop eses$ 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.213d6415f73c8d88bddb1f236f584b63dbaf2c3051f09863a3f1ba219edba3673$ 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# lsapp.py node_modules package.json requirements.txt static templates webpack.config.js[email protected]:/opt/flask-app# python app.pyIndex 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-web852fc74de2954bb72471b858dce64d764181dca0cf7693fed201d76da33df794$ docker container lsCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES852fc74de295 prakhar1989/foodtrucks-web "python ./app.py" About a minute ago Up About a minute 0.0.0.0:5000->5000/tcp foodtrucks-web13d6415f73c8 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:5000HTTP/1.0 200 OKContent-Type: text/html; charset=utf-8Content-Length: 3697Server: Werkzeug/0.11.2 Python/2.7.6Date: Sun, 10 Jan 2016 23:58:53 GMT
访问 http://localhost:5000,来看看你伟大的程序可以预览了!虽然这可能看起来有很多步骤,但实际上我们从开始到运行只输入了 4 个命令。我在 bash脚本 中整理了命令。
#!/bin/bash# build the flask containerdocker build -t prakhar1989/foodtrucks-web .# create the networkdocker network create foodtrucks-net# start the ES containerdocker 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 containerdocker 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 的客户端。然而,在 Docker 生态系统中,还有许多其他的开源工具可以很好地与 Docker 一起工作。其中包含:
Docker Machine—在你的计算机、云服务商和自己的数据中心内创建 Docker 主机
Docker Compose—一个用于定义和运行多容器 Docker 应用的工具
Docker Swarm—Docker 的本机集群解决方案
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 --versiondocker-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.2container_name: esenvironment:- discovery.type=single-nodeports:- 9200:9200volumes:- esdata1:/usr/share/elasticsearch/dataweb:image: prakhar1989/foodtrucks-webcommand: python app.pydepends_on:- esports:- 5000:5000volumes:- ./flask-app:/opt/flask-appvolumes:esdata1:driver: local
我来解释下文件中都是什么意思。在父级,我们定义了服务的名称 - es
和web
。对于 Docker 需要运行的每个服务,我们可以从 image
中添加额外的参数。对于es
,我们只是参考 Elastic 注册中心提供的 elasticsearch
镜像。对于 Flask 应用,我们参考在本节开头构建的镜像。
通过其他参数,例如command
,ports
可以提供有关容器的更多信息。volumes
参数指定web
代码所在容器中的挂载点,代码会驻留在这里。不过这完全是可选的,比如你需要访问日志的时候会很有用。稍后我们将看到它在开发过程中有什么用。关于该文件支持的参数的详细信息,请参阅 在线文档 。我们还需要为es
容器添加卷,以便加载的数据在重新启动后可以持久化。我们还指定了depends_on
,告诉 docker 在启动 web
之前启动 es
容器。你也可以在 docker compose docs 上阅读更多相关信息。
注意:你必须在带有 docker-come.yml
文件的目录中执行大多数 Compose 命令
很好😉!现在文件已经准备好了,让我们看看 docker-compose
的实际运行情况。在开始之前,我们需要确保端口没有被占用。所以如果你有 Flask 和 ES 容器运行,先关闭它们。
$ docker stop $(docker ps -q)39a2f5df14ef2a1b77e066e6
现在我们可以运行docker-compose
了。进到 food trucks 目录并运行docker-compose up
。
$ docker-compose upCreating network "foodtrucks_default" with the default driverCreating foodtrucks_es_1Creating foodtrucks_web_1Attaching to foodtrucks_es_1, foodtrucks_web_1es_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] initializedes_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-evRc9MqS2wqwes_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] startedes_1 | [2016-01-11 03:43:55,826][INFO ][gateway ] [Comet] recovered [0] indices into cluster_statees_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 ... doneKilling foodtrucks_es_1 ... done$ docker-compose up -dCreating es ... doneCreating foodtrucks_web_1 ... done$ docker-compose psName Command State Ports--------------------------------------------------------------------------------------------es /usr/local/bin/docker-entr ... Up 0.0.0.0:9200->9200/tcp, 9300/tcpfoodtrucks_web_1 python app.py Up 0.0.0.0:5000->5000/tcp
不出所料,我们可以看到两个容器都运行成功。这些名字从哪儿来的?那些是由 Compose 自动创建的。但Compose 也会自动创建网络吗?好奇。。让我们来看看。
先停止运行服务。我们总是可以使用一个命令中重新启动它们。数据卷会一直存在,所以可以使用 docker-compose 重新启动集群。要销毁集群和数据卷,只需要输入docker-compose down -v
。
$ docker-compose down -vStopping foodtrucks_web_1 ... doneStopping es ... doneRemoving foodtrucks_web_1 ... doneRemoving es ... doneRemoving network foodtrucks_defaultRemoving volume foodtrucks_esdata1
当我们这样操作的时候,也要删除上次创建的foodtrucks
网络。
$ docker network rm foodtrucks-net$ docker network lsNETWORK ID NAME DRIVER SCOPEc2c695315b3a bridge bridge locala875bec5d6fd host host localead0e804a67b none null local
现在我们已经有了一个干净的平台,重新运行我们的服务,看看Compose 还有什么魔法。
$ docker-compose up -dRecreating foodtrucks_es_1Recreating foodtrucks_web_1$ docker container lsCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESf50bb33a3242 prakhar1989/foodtrucks-web "python app.py" 14 seconds ago Up 13 seconds 0.0.0.0:5000->5000/tcp foodtrucks_web_1e299ceeb4caa elasticsearch "/docker-entrypoint.s" 14 seconds ago Up 14 seconds 9200/tcp, 9300/tcp foodtrucks_es_1
到目前为止,一切顺利。是时候看看是否建立了网络。
$ docker network lsNETWORK ID NAME DRIVERc2c695315b3a bridge bridge localf3b80f381ed3 foodtrucks_default bridge locala875bec5d6fd host host localead0e804a67b none null local
可以看到,compose 继续创建了一个名为 foodtrucks_default
的新网络,并将这两个新服务都附加到这个网络中,便于让它们可以被其他服务发现。服务的每个容器都连接到默认网络,并且可以被该网络上的其他容器访问,同时可以在与容器名称相同的主机名中被发现。
$ docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES8c6bb7e818ec 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 es7640cec7feb7 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 lsCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES5450ebedd03c prakhar1989/foodtrucks-web "python app.py" 9 seconds ago Up 6 seconds 0.0.0.0:5000->5000/tcp foodtrucks_web_105d408b25dfe 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/helloHTTP/1.0 404 NOT FOUNDContent-Type: text/htmlContent-Length: 233Server: Werkzeug/0.11.2 Python/2.7.15rc1Date: 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/helloHTTP/1.0 404 NOT FOUNDContent-Type: text/htmlContent-Length: 233Server: Werkzeug/0.11.2 Python/2.7.15rc1Date: Mon, 30 Jul 2018 15:34:38 GMT
纳尼 😱!竟然没用!这是什么鬼?虽然我们确实修改了app.py
,但文件驻留在我们的机器(宿主机)中,但由于 Docker 是基于prakhar1989/foodtrucks-web
镜像运行的容器,所以它不知道这次修改。要验证这一点,可以尝试:
$ docker-compose run web bashStarting es ... done[email protected]:/opt/flask-app# lsapp.py package-lock.json requirements.txt templatesnode_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.yml
的 web
部分。yml 文件如下:
version: "3"services:es:image: docker.elastic.co/elasticsearch/elasticsearch:6.3.2container_name: esenvironment:- discovery.type=single-nodeports:- 9200:9200volumes:- esdata1:/usr/share/elasticsearch/dataweb:build: . # replaced image with buildcommand: python app.pyenvironment:- DEBUG=True # set an env var for flaskdepends_on:- esports:- "5000:5000"volumes:- ./flask-app:/opt/flask-appvolumes:esdata1:driver: local
有了这些更改(差异),然后停止并启动容器。
$ docker-compose down -vStopping foodtrucks_web_1 ... doneStopping es ... doneRemoving foodtrucks_web_1 ... doneRemoving es ... doneRemoving network foodtrucks_defaultRemoving volume foodtrucks_esdata1$ docker-compose up -dCreating network "foodtrucks_default" with the default driverCreating volume "foodtrucks_esdata1" with local driverCreating es ... doneCreating foodtrucks_web_1 ... done
最后让我们通过添加一个新路由来更改app.py
。现在使用 curl 访问
$ curl 0.0.0.0:5000/hellohello world
Wohoo!我们得到了想要的 hello world!可以尝试通过在应用中修改一些其他的来玩。
上面就是我们要介绍的 Docker Compose 全部内容。使用 Docker Compose,你还可以暂停服务,在容器上运行一次性命令,甚至扩展容器的数量。我还建议你查看一些其他 用例 的 Docker Compose。希望我能够向你展示使用 Compose 管理多容器环境是多么容易。最后,我们将应用部署到 AWS!
在上一节中,我们使用docker-compose
在本地运行我们的应用,只用了一个命令:docker-compose up
。现在我们有一个功能正常的应用,我想和全世界分享它,获得一些用户,赚大钱,然后再北京三环买套房子。当然后面的几个需求超出了本教程的范围,下面我们来花点儿时间介绍如何在 AWS 云端部署我们的多容器应用。
如果你已经阅读过这篇文章,你一定深信 Docker 是一项很酷的技术。你不是一个人,随着 Docker 的迅速崛起,几乎所有云服务商都在其平台上添加对 Docker 应用部署的支持。截至今天,你可以在 Google Cloud Platform,AWS,Azure 和许多其他设备上部署容器。我们已经获得了使用 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 --versionecs-cli version 0.1.0 (*cbdc2d5)
首先获取用于登录实例的密钥对。在 EC2控制台 创建一个新的密钥对,下载密钥对然后保存在一个你认为安全的位置。在关闭界面前需要注意另外一件事就是区域名称。我已经命名了我的密钥 - 并且在 ecs
中将区域设置为us-east-1
。这是我在本次演示的其余部分中假设的内容。
接下来配置 CLI。
$ ecs-cli configure --region us-east-1 --cluster foodtrucksINFO[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.microINFO[0000] Created cluster cluster=foodtrucksINFO[0001] Waiting for your cluster resources to be createdINFO[0001] Cloudformation stack status stackStatus=CREATE_IN_PROGRESSINFO[0061] Cloudformation stack status stackStatus=CREATE_IN_PROGRESSINFO[0122] Cloudformation stack status stackStatus=CREATE_IN_PROGRESSINFO[0182] Cloudformation stack status stackStatus=CREATE_IN_PROGRESSINFO[0242] Cloudformation stack status stackStatus=CREATE_IN_PROGRESS
这里我们提供最初下载的密钥对的名称(在我的例子中是ecs
),想要使用的实例数(--size
)以及希望容器运行的实例类型。--capability-iam
选项告诉 CLI 我们确认该命令可以创建 IAM 资源。
最后一步是使用我们的docker-compose.yml
文件。我们需要做一点小的改动,所以不要修改原始版本,只是复制并重命名为aws-compose.yml
。这个 文件 修改如下所示:
es:image: elasticsearchcpu_shares: 100mem_limit: 262144000web:image: prakhar1989/foodtrucks-webcpu_shares: 100mem_limit: 262144000ports:- "80:5000"links:- es
我们对原来的 docker-compose.yml
做的唯一修改是为每个容器提供mem_limit
和cpu_shares
值。我们还删除了version
了services
键,因为 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 upINFO[0000] Using ECS task definition TaskDefinition=ecscompose-foodtrucks:2INFO[0000] Starting container... container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/esINFO[0000] Starting container... container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/webINFO[0000] Describe ECS container status container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-foodtrucks:2INFO[0000] Describe ECS container status container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/es desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-foodtrucks:2INFO[0036] Describe ECS container status container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/es desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-foodtrucks:2INFO[0048] Describe ECS container status container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-foodtrucks:2INFO[0048] Describe ECS container status container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/es desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-foodtrucks:2INFO[0060] Started container... container=845e2368-170d-44a7-bf9f-84c7fcd9ae29/web desiredStatus=RUNNING lastStatus=RUNNING taskDefinition=ecscompose-foodtrucks:2INFO[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 psName State Ports TaskDefinition845e2368-170d-44a7-bf9f-84c7fcd9ae29/web RUNNING 54.86.14.14:80->5000/tcp ecscompose-foodtrucks:2845e2368-170d-44a7-bf9f-84c7fcd9ae29/es RUNNING ecscompose-foodtrucks:2
继续在你的浏览器中打开 http://54.86.14.14,你应该看到 Food Trucks 的所有黑黄色的荣耀!既然我们正在讨论这个话题,让我们看看 AWS ECS 控制台的外观。
我们可以看到上面已经创建了名为 “foodtrucks” 的 ECS 集群,现在正在运行带有 2 个容器实例的任务。花点时间浏览这个控制台,了解一下这里的所有选项。
就这样。只用几个命令,我们就可以在 AWS 云端部署我们优秀的应用程序!