在测试中使用Docker(5)

我们将会看到下面3个场景:
(1)使用Docker测试一个静态网站。
(2)使用Docker创建并测试一个Web应用。
(3)将Docker用于持续集成。

1. 使用Docker测试静态网页

将Docker作为本地Web开发环境是Docker的一个最简单的应用场景。这样的环境可以完全复制生产环境,并确保用户开发的东西在生产环境中也能运行。下面从将Nginx Web服务器安装到容器来架构一个简单的网站开始。这个网站暂且命名为Sample。

1.1 Sample网站的初始Dockerfile

为了完成网站开发,从这个简单的Dockerfile开始。先来创建一个目录,保存Dockerfile,代码清单:

*** 为Nginx Dockerfile创建一个目录
$ mkdir sample
$ cd sample
$ touch Dockerfile

现在还需要一些Nginx配置文件,才能运行这个网站。首先在这个示例所在的目录里创建一个名为nginx的目录,用来存放这些配置文件。然后我们可以从GitHub上下载作者准备好的示例文件,代码:

** 获取Nginx配置文件
$ mkdir nginx && cd nginx
$ wget https://raw.githubusercontent.com/jamtur01/dockerbook-code/master/code/5/sample/nginx/global.conf
$ wget https://raw.githubusercontent.com/jamtur01/dockerbook-code/master/code/5/sample/nginx/nginx.conf
$ cd ..

现在看一下我们将要为Sample网站创建的Dockerfile,代码:

** 网站测试的基本Dockerfile
FROM ubuntu:14.04
MAINTAINER James Turnbull "james@example.com"
ENV REFRESHED_AT 2014-06-01
RUN apt-get -yqq update && apt-get -yqq install nginx
RUN mkdir -p /var/www/html/website
ADD nginx/global.conf /etc/nginx/conf.d/
ADD nginx/nginx.conf /etc/nginx/nginx.conf
EXPOSE 80



这个简单的Dockerfile内容包括以下几项:
** 安装Nginx。
** 在容器中创建一个目录/var/www/html/website/。
** 将来自我们下载的本地文件的Nginx配置文件添加到镜像中。
** 公开镜像的80端口。

这个Nginx配置文件是为了运行Sample网站而配置的。将文件nginx/global.conf用ADD指令复制到/etc/nginx/conf.d/目录中。配置文件global.conf的内容如代码:

** global.conf文件
server {
		listen 		0.0.0.0:80;
		server_name		_;

		root			/var/www/html/website;
		index			index.html	index.htm;

		access_log		/var/log/nginx/default_access.log;
		error_log		/var/log/nginx/default_error.log;
}

这个文件将Nginx设置为监听80端口,并将网络服务的根路径设置为/var/www/html/website,这个目录是我们用RUN指令创建的。

我们还需要将Nginx配置为非守护进程的模式,这样可以让Nginx在Docker容器里工作。将文件nginx/nginx.conf复制到/etc/nginx目录就可以达到这个目的,nginx.conf文件的内容如代码清单:

** nginx.conf配置文件
user www-data;
worker_process 4;
pid /run/nginx.pid;
daemon off;

events { }

http {
	sendfile on;
	tcp_nopush on;
	tcp_nodelay on;
	keepalive_timeout 65;
	types_hash_max_size 2048;
	include /etc/nginx/mime.types;
	default_type application/octet-stream;
	access_log /var/log/nginx/access.log;
	error_log /var/log/nginx/error.log;
	gzip on;
	gzip_disable "msie6";
	include /etc/nginx/conf.d/*.conf;
}

在这个配置文件里,daemon off;选项阻止Nginx进入后台,强制其在前台运行。这是因为要想保持Docker容器的活跃状态,需要其中运行的进程不能中断。默认情况下,Nginx会以守护进程的方式启动,这会导致容器只是短暂运行,在守护进程被fork启动后,发起守护进程的原始进程就会退出,这时容器就停止运行了。

这个文件通过ADD指令复制到/etc/nginx/nginx.conf。

读者应该注意到了两个ADD指令的目标有细微的差别。第一个指令以目录/etc/nginx/conf.d/结束,而第二个指令指定了文件/etc/nginx/nginx.conf。将文件复制到Docker镜像时,这两种风格都是可以用的。

注意:读者可以在代码网站(http://www.dockerbook.com/code/index.html)或者Docker Book网站(https://github.com/jamtur01/dockerbook-code)里找到所有的代码和示例配置文件。读者需要下载或者复制粘贴nginx.conf和global.conf配置文件到之前创建的nginx目录里,保证其可以用于docker build命令。

1.2 构建Sample网站和Nginx镜像

利用之前的Dockerfile,可以用docker build命令构建出新的镜像,并将这个镜像命名为jamtur01/nginx,代码:

** 构建新的Nginx镜像
$ sudo docker build -t jamtur01/nginx .

这将构建并命名一个新镜像。下面来看看构建的执行步骤。使用docker history命令来查看构建新镜像的步骤和层级,代码清单:

** 展示Nginx镜像的构建历史
$ sudo docker history jamtur01/nginx

history命令从新构建的jamtur01/nginx镜像的最后一层开始,追朔到最开始的父镜像ubuntu:14.04。这个命令也展示了每步之间创建的新层,以及创建这个层所使用的Dockerfile里的命令。

1.3 从Sample网站和Nginx镜像构建容器

现在可以使用jamtur01/nginx镜像,并开始从这个镜像构建可以用来测试Sample网站的容器。为此,需要添加Sample网站的代码。现在下载这段代码到sample目录,代码清单:

下载Sample网站
$ mkdir website && cd website
$ wget https://raw.githubusercontent.com/jamtur01/dockerbook-code/master/code/5/sample/website/index.html
$ cd ..

这将在sample目录中创建一个名为website的目录,然后为Sample网站下载index.html文件,放到website目录中。

现在来看看如何使用docker run命令来运行一个容器,代码清单:

** 构建第一个Nginx测试容器
$ sudo docker run -d -p 80 --name website -v $PWD/website:/var/www/html/website jamtur01/nginx nginx

注意:可以看到,在执行docker run时传入了nginx作为容器的启动命令。一般情况下,这个命令无法让Nginx以交互的方式运行。我们已经提供给Docker的配置里加入了指令daemon off, 这个指令让Nginx启动后以交互的方式在前台运行。

可以看到,我们使用docker run命令从jamtur01/nginx镜像创建了一个名为website的容器。读者已经见过了大部分选项,不过-v选项是新的。-v这个选项允许我们将宿主机的目录作为卷,挂载到容器里。

现在稍微偏题一下,我们来关注以下卷这个概念。卷在Docker里非常重要,也很有用。卷是在一个或者多个容器内被选定的目录,可以绕过分层的联合文件系统(Union File System),为Docker提供持久数据库或者共享数据。这意味着对卷的修改会直接生效,并绕过镜像。当提交或者创建镜像时,卷不被包含在镜像里。

提示:卷可以在容器间共享。即便容器停止,卷里的内容依旧存在。

回到刚才的例子。当我们因为某些原因不能把应用或者代码构建到镜像中时,就体现出卷的价值了。例如:
** 希望同时对代码做开发和测试。
** 代码改动很频繁,不想在开发过程中重构镜像。
** 希望在多个容器间共享代码。

-v 选项通过指定一个目录或者登上与容器上与该目录分离的本地宿主机来工作,这两个目录用":"分隔。如果容器目录不存在,Docker会自动创建一个。

也可以通过在目录后加上rw或者ro来指定容器内目录的读写状态,代码清单:

** 控制卷的写状态
** sudo docker run -d -p 80 --name website -v $PWD/website:/var/www/html/website:ro jamtur01/nginx nginx

这将使目的目录/var/www/html/website变为只读状态。

在Nginx网站容器里,我们通过卷将$PWD/website挂载到容器的/var/www/html/website目录,顺利挂载了正在开发的本地网站。在Nginx配置里(在配置文件/etc/nginx/conf.d/global.conf中),已经指定了这个目录为Nginx服务器的工作目录。

提示:这里使用的website目录包含在本书的源代码中(http://dockerbook.com/code/5/website/)以及GitHub(https://github.com/jamtur01/dockerbook-code/tree/master/code/5/website)。读者可以在对应目录里看到刚刚下载的index.html文件。

现在,如果使用docker ps 命令查看正在运行的容器,可以看到名为website的容器正处于活跃状态,容器的80端口被映射到宿主机的49161端口,代码清单:

** 查看Sample网站容器
$ sudo docker ps -l

如果在Docker的宿主机上浏览49161端口,就会看到Sample网站。

提示:记住,如果用户在使用BootDocker或者Docker Toolbox,需要注意这两个工具都会在本地创建一个虚拟机,这个虚拟机具有自己独立的网络接口和IP地址。需要连接到虚拟机的地址。而不是localhost或者用户的本地主机的IP地址。

1.4 修改网站

我们已经得到了一个可以工作的网站!现在,如果要修改网站,该怎么办?可以直接打开本地宿主机的website目录下的index.html文件并修改,代码清单:

** 修改Sample网站
$ vi $PWD/website/index.html

显然这个修改太简单了,不过可以看出,更复杂的修改也并不困难。更重要的是,正在测试网站的运行环境,完全是生产环境里的真实状态。现在可以给每个用于生产的网站服务环境(如Apache,Nginx)配置一个容器,给不同的开发框架的运行环境(如PHP,或者Ruby on Rails)配置一个容器,或者给后端数据库配置一个容器等等。

2. 使用Docker构建并测试Web应用程序

现在来看一个更复杂的例子,测试一个更大的Web应用程序。我们将要测试一个基于Sinatra的Web应用程序,而不是静态网站,然后我们将基于Docker来对这个应用进行测试。Sinatra是一个基于Ruby的Web应用框架,它包含一个Web应用库,以及简单的领域专用语言(即DSL)来构建Web应用程序。与其他复杂的Web应用框架(如Ruby on Rails)不同,Sinatra并不遵循MVC模式,而关注于让开发者创建快速,简单的Web应用。

因此,Sinatra非常适合用来创建一个小型的示例应用进行测试。在这个例子里,我们将创建一个应用程序,它接收输入的URL参数,并以JSON散列的结构输出到客户端。通过这个例子,我们也将展示一下如何将Docker容器链接起来。

2.1 构建Sinatra应用程序

我们先来创建一个sinatra目录,用来存放应用程序的代码,以及构建时我们所需的所有相关文件,代码清单如下:

** 为测试web应用程序创建目录
$ mkdir -p sinatra
$ cd sinatra

在sinatra目录下,让我们从Dockerfile开始,构建一个基础镜像,并用这个镜像来开发Sinatra Web应用程序,代码清单:

*** 测试用web应用程序的Dockerfile
FROM ubuntu:14.04
MAINTAINER James Turnbull "james@example.com"
ENV REFRESHED_AT 2014-06-01

RUN apt-get update -yqq && apt-get -yqq install ruby ruby-dev build-essential redis-tools
RUN gem install --no-rdoc --no-ri sinatra json redis

RUN mkdir -p /opt/webapp

EXPOSE 4567

CMD [ "/opt/webapp/bin/webapp" ]

可以看到,我们已经创建了另一个基于Ubuntu的镜像,安装了Ruby和RubyGem,并且使用了gem命令安装了sinatra, json 和 redis gem。sinatra是Sinatra的库,json用来提供对JSON的支持。redis gem在后面会用到,用来和Redis数据库进行集成。

我们已经创建了一个目录存放新的Web应用程序,并公开了WEBrick的默认端口4567。

最后,使用CMD指定/opt/webapp/bin/webapp作为Web应用程序的启动文件。

现在使用docker build命令来构建新的镜像,代码如下:

** 构建新的Sinatra镜像
$ sudo docker build -t jamtur01/sinatra .

2.2 创建Sinatra容器

我们已经创建了镜像,现在让我们下载Sinatra Web应用程序的源代码。这份代码可以在本书的官网(http://dockerbook.com/code/5/sinatra/webapp/)或Docker Book网站(https://github.com/jamtur01/dockerbook-code)找到。这个应用程序在webapp目录下,由bin和lib两个目录组成。

现在将其下载到sinatra目录中,代码清单:
$ cd sinatra
$ wget --cut-dirs=3 -nH -r --reject Dockerfile,index.html --no-parent http://dockerbook.com/code/5/sinatra/webapp/
$ ls -l webapp
...

下面我们就来快速浏览一下webapp源代码的核心,其源代码保存在sinatra/webapp/lib/app.rb文件中,代码清单:

** Sinatra app.rb源代码
require "rubygems"
require "sinatra"
require "json"

class App< Sinatra::Application

	set :bind, '0.0.0.0'
	
	get '/' do
		"<h1>DockerBook Test Sinatra app</h1>"
	end

	post '/json/?' do
		params.to_json
	end
end
>

可以看到,这个程序很简单,所有访问/json端点的POST请求参数都会被转换为JSON的格式输出。

这里还要使用chmod命令保证webapp/bin/webapp这个文件可以执行,代码清单:

** 确保webapp/bin/webapp可以执行
$ chmod +x webapp/bin/webapp

现在我们就可以基于我们的镜像,通过docker run命令启动一个容器。要启动容器,我们需要在sinatra目录下,因为我们需要将这个目录下的源代码通过卷挂载到容器中去,代码清单:

** 启动第一个Sinatra容器
$ sudo docker run -d -p 4567 --name webapp -v $PWD/webapp:/opt/webapp jamtur01/sinatra

这里从jamtur01/sinatra镜像创建了一个新的名为webapp的容器,指定了一个新卷,使用存放新Sinatra Web应用程序的webapp目录,并将这个卷挂载到在Dockerfile里创建的目录/opt/webapp。

我们没有在命令行中指定要运行的命令,而是使用在镜像的Dockerfile中CMD指令设置的命令,代码清单:

** Dockerfile中的CMD指令
...
CMD [ "/opt/webapp/bin/webapp" ]
...

从这个镜像启动容器时,将会执行这一命令。

也可以使用docker logs命令查看被执行的命令都输出了什么,代码清单:

** 检查Sinatra容器的日志
$ sudo docker logs webapp

运行docker logs命令时加上-f标志可以达到与执行tail -f命令一样的效果--持续输出容器的STDERR和STDOUT里的内容,代码清单:

** 跟踪Sinatra容器的日志
$ sudo docker logs -f webapp
...

可以使用docker top命令来查看Docker容器里正在运行的进程,代码清单:

** 使用docker top来列出Sinatra进程
$ sudo docker top webapp

** 查看Sinatra的端口映射
$ sudo docker port webapp 4567
0.0.0.0:49160

目前,Sinatra应用还很基础,没做什么。它只是接收输入参数,并将输入转化为JSON输出。现在可以使用curl命令来测试这个应用程序了,代码清单:

** 测试Sinatra应用程序
$ curl -i -H 'Accept: application/json' -d 'name=Foo&status=Bar' http://localhost:49160/json
HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
Content-Length: 29
X-Frame-Optoins: SAMEORIGIN
Connection: Keep-Alive
...
{"name":"Foo","status":"Bar"}

可以看到,我们给Sinatra应用程序传入了一些URL参数,并看到这些参数转化成JSON散列后的输出:{"name":"Foo","status":"Bar"}。

成功!然后试试看,我们能不能通过链接到运行在另一个容器里的服务,把当前的示例应用程序容器扩展为真正的应用程序栈。

2.3 扩展Sinatra应用程序来使用Redis

现在我们将要扩展Sinatra应用程序,加入Redis后端数据库,并在Redis数据库中存储输入的URL参数。为了达到这个目的,我们要下载一个新版本的Sinatra应用程序。我们还将创建一个运行Redis数据库的镜像和容器。之后,要利用Docker的特性来关联两个容器。

(1)升级我们的Sinatra应用程序
让我们下载一个升级版的Sinatra应用程序开始,这个升级版中增加了链接Redis的配置。在sinatra目录中,我们下载了我们这个应用的启用了Redis的版本,并保持到一个新目录webapp_redis中,代码清单:

** 下载升级版的Sinatra Web应用程序
$ cd sinatra
$ wget --cut-dirs=3 -nH -r --reject Dockerfile,index.html --no-parent http://dockerbook.com/code/5/sinatra/webapp_redis/
$ ls -l webapp_redis
...

我们看到新应用程序已经下载,现在让我们看一下lib/app.rb文件中的核心代码,代码清单:

** app.rb文件
require "rubygems"
require "sinatra"
require "json"
require "redis"
class App < Sinatra::Application
		redis = Redis.new(:host=>'db',:port=>'6379')
....
>

注意:可以在 http://dockerbook.com/code/5/sinatra/webapp_redis/或者 Docker Book网站( https://github.com/jamtur01/dockerbook-code )上获取升级版的启用了Redis的Sinatra应用程序的完整代码。

我们可以看到新版本的代码和前面的代码几乎一样,只是增加了对Redis的支持。我们创建了一个到Redis的链接,用来连接名为db的宿主机上的Redis数据库,端口为6379.我们在POST请求处理中,将URL参数保存到了Redis数据库中,并在需要的时候通过GET请求从中取回这个值。

我们同样需要确保webapp_redis/bin/webapp文件在使用之前具备可执行权限,这可以通过chmod命令来实现,代码清单:

** 使webapp_redis/bin/webapp文件可执行
$ chmod +x webapp_redis/bin/webapp

(2)构建Redis数据库镜像
为了构建Redis数据库,要创建一个新的镜像。我们需要在sinatra目录下创建一个redis目录,用来保存构建Redis容器所需的所有相关文件,代码清单:

** 为Redis容器创建目录
$ mkdir -p sinatra/redis
$ cd sinatra/redis

在siantra/redis目录中,让我们从Redis镜像的另一个Dockerfile开始,代码清单:

** 用于Redis镜像的Dockerfile
FROM ubuntu:14.04
MAINTAINER James Turnbull "james@example.com"
ENV REFRESHED_AT 2014-06-01
RUN apt-get -yqq update && apt-get -yqq install redis-server redis-tools
EXPOSE 6379
ENTRYPOINT [ "/usr/bin/redis-server" ]
CMD []

我们在Dockerfile里指定了安装Redis服务器,公开6379端口,并指定了启动Redis服务器的ENTRYPOINT。现在来构建这个镜像,命名为jamtur01/redis,代码清单:

** 构建Redis镜像
$ sudo docker build -t jamtur01/redis .

现在从这个新镜像构建容器,代码清单:
$ sudo docker run -d -p 6379 --name redis jamtur01/redis
0a206261f079

可以看到,我们从jamtur01/redis镜像启动了一个新容器,名字是redis。

注意,我们指定了-p标志来公开 6379 端口.看看这个端口映射到宿主机的哪个端口,代码清单:
** 查看Redis端口
$ sudo docker port redis 6379
0.0.0.0:49161

Redis的端口映射到了4961端口。试着连接到这个Redis实例。

我们需要在本地安装Redis客户端作测试。在Ubuntu系统上,客户端程序一般在redis-tools包里,代码清单:
$ sudo apt-get -y install redis-tools

而在Red Hat及相关系统上,包名则为redis,代码清单:

** 在Red Hat等上安装Redis包
$ sudo yum install -y -q redis

然后可以使用redis-cli命令来确认Redis服务器工作是否正常,代码清单:

** 测试Redis连接
$ redis-cli -h 127.0.0.0 -p 49161
redis 127.0.0.1:49161>

这里使用Redis客户端连接到127.0.0.1的端口49161端口,验证了Redis服务器正在正常工作。可以使用quit命令来退出Redis CLI接口。

2.4 将Sinatra应用程序连接到Redis容器

现在来更新Sinatra应用程序,让其连接到Redis并存储传入的参数。为此,需要能够与Redis服务器对话。要做到这一点,可以用以下几种方法。

** Docker的内部网络。
** 从Docker 1.9及之后的版本开始,可以使用Docker Networking以及docker network命令。
** Docker链接。一个可以将具体容器链接到一起来进行通信的抽象层。

那么,我们应该选择哪种方法呢?第一种方法,Docker的内部网络这种解决方案并不灵活,强大。我们针对这种方式的讨论,也只是为了介绍Docker网络是如何工作的。我们不推荐采用这种方式来连接Docker容器。

两种比较现实的连接Docker容器的方式是Docker Networking和Docker链接(Docker link)。具体应该选择哪种方式取决于用户运行的Docker版本。如果用户正在使用Docker 1.9 或者更新版本,推荐使用Docker Networking,如果使用的是Docker 1.9之前的版本,应该选择Docker 链接。

在Docker Networking和Docker链接之间也有一些区别。这也是我们推荐使用Docker Networking而不是链接的原因。

** Docker Networking可以将容器连接到不同宿主机上的容器。

** 通过Docker Networking连接的容器可以在无需更新链接的情况下,对停止,启动或者重启容器。而使用Docker链接,则可能需要更新一些配置,或者重启相应的容器来维护Docker容器之间的链接。

** 使用Docker Networking, 不必事先创建容器再去连接它。同样,也不必关心容器的运行顺序,可以在网络内部获得容器名解析和发现。

2.5 Docker内部连网

第一种方法涉及Docker自己的网络栈。到目前为止,我们看到的Docker容器都是公开端口并绑定到本地网络接口的,这样可以把容器里的服务在本地Docker宿主机所在的外部网络上(比如,把容器里的80端口绑定到本地宿主机的更高端口上)公开。除了这种用法,Docker这个特性还有种用法没有用过,那就是内部网络。

在安装Docker时,会创建一个新的网络接口,名字是docker0.每个Docker容器都会在这个接口上分配一个IP地址。来看看目前Docker宿主机上这个网络接口的信息,代码清单:

提示:Docker自1.5.0版本开始支持IPV6,要启动这一功能,可以在运行Docker守护进程时加上--ipv6标志。

** docker0网络接口
$ ip a show docker0

可以看到,docker0接口有符合RFC1918的私有IP地址,范围是172.16··172.30.接口本身的地址172.17.42.1是这个Docker网络的网关地址,也是所有Docker容器的网关地址。

接口docker0是一个虚拟的以太网桥,用于连接容器和本地宿主网络。如果进一步查看Docker宿主机的其他网络接口,会发现一系列名字以veth开头的接口,代码清单:

** veth接口
vethec6a Link encap:Ethernet HWaddr 86:e1:95:da:e2:5a
		inet6	addr: fe80:95ff:feda:e25a:/64 Scope:Link
...

Docker每创建一个容器就会创建一组互联的网络接口。这组接口就像管道的两端(就是说,从一端发送的数据会在另一端接收到)。这组接口其中一端作为容器的eth0接口,而另一端统一命名为类似vethec6a这种名字,作为宿主机的一个端口。可以把veth接口认为是虚拟网线的一端。这个虚拟网线一端插在名为docker0的网桥上,另一端插到容器里。通过把每个veth*接口绑定到docker0网桥,Docker创建了一个虚拟子网,这个子网由宿主机和所有的Docker容器共享。

进入容器里面,看看这个子网管道的另一端,代码清单:

** 容器内的eth0接口
$ sudo docker run -t -i ubuntu /bin/bash
root@b9107458f16a:/# ip a show eth0

可以看到,Docker给容器分配了IP地址172.17.0.29作为宿主机接口的另一端。这样就能够让宿主网络和容器互相通信了。

让我们从容器内跟踪对外通信的路由,看看是如何建立连接的,代码清单:

root@b9107458f16a:/# apt-get -yqq update && apt-get install -yqq traceroute
...
root@b9107458f16a:/# traceroute google.com

可以看到,容器地址后的下一跳是宿主网络上docker0接口的网关IP 172.17.42.1。

不过Docker网络还有另一个部分配置才能允许建立连接:防火墙规则和NAT配置。这些配置允许Docker在宿主网络和容器间路由。现在来看一下宿主机上的IPTables NAT配置,代码清单:

*** Docker 的iptables 和 NAT配置
$ sudo iptables -t nat -L -n

这里有几个值得注意的IPTables规则。首先,我们注意到,容器默认是无法访问的。从宿主网络与容器通信时,必须明确指定打开的端口。下面我们以DNAT(即目标NAT)这个规则把容器里的访问路由到Docker宿主机的49161端口。

提示:想了解更多关于Docker的高级网络配置,https://docs.docker.com/articles/networking/

Redis容器的网络

下面我们用docker inspect命令来查看新的Redis容器的网络配置,代码清单:

** Redis容器的网络配置
$ sudo docker inspect redis



docker inspect命令展示了Docker容器的细节,这些细节包括配置信息和网络状况。也可以在命令里使用-f标志,只获取IP地址,代码清单:
$ sudo docker inspect -f '{{ .NetworkSettings.IPAddress }}' redis 
172.17.0.18

通过运行docker inspect命令可以看到,容器的IP地址为172.17.0.18,并使用了docker0接口作为网关地址。还可以看到6379端口被映射到本地宿主机的49161端口。只是,因为运行在本地的Docker宿主机上,所以不是一定要用映射后的端口,也可以直接使用172.17.0.18地址与Redis服务器的6379端口通信,代码清单:

** 直接与Redis容器通信
$ redis-cli -h 172.17.0.18
redis 172.17.0.18:6379>

在确认完可以连接到Redis服务之后,可以使用quit命令退出Redis接口。

注意:Docker默认会把公开的端口绑定到所有的网络接口上。因此,也可以通过localhost或者127.0.0.1来访问Redis服务器。

因此,虽然第一眼看上去这是让容器互联的一个好方案,但可惜的是,这种方法有两个大问题:第一,要在应用程序里对Redis容器的IP地址做硬编码;第二,如果重启容器,Docker会改变容器的IP地址。现在用docker restart命令来看看地址的变化,代码清单如下: (如果使用docker kill命令杀死容器再重启,也会得到同样的结果。)

重启Redis容器
$ sudo docker restart redis

让我们查看一下容器的IP地址,代码清单:

** 查找重启后Redis容器的IP地址
$ sudo docker inspect -f '{{ .NetworkSettings.IPAddress }}' redis
172.17.0.19

可以看到,Redis容器有了新的IP地址172.17.0.19,这就意味着,如果在Sinatra应用程序里硬编码了原来地址,那么现在就无法让应用程序连接到Redis数据库了。这可不那么好用。

谢天谢地,从Docker 1.9开始,Docker连网已经灵活得多。让我们来看一下,如何用新的连网框架连接容器。

2.6 Docker Networking

容器之间的连接用网络创建,这被称为Docker Networking,也是Docker 1.9发布版本中的一个新特性。Docker Networking 允许用户创建自己的网络,容器可以通过这个网上互相通信。实质上,Docker Networking 以新的用户管理的网络补充了现有的docker0。更重要的是,现在容器可以跨越不同的宿主机来通信,并且网络配置可以更灵活地定制。Docker Networking 也和 Docker Compose以及Swarm进行了集成。

注意:Docker Networking支持也是可插拔的,也就是说可以增加网络驱动以支持来自不同网络设备提供商(如Cisco和VMware)的特定拓扑和网络框架。

下面我们就来看一个简单的例子,启动前面的Docker链接例子中使用的Web应用程序以及Redis容器。要想使用Docker网络,需要先创建一个网络,然后再这个网络下启动容器,代码清单:

** 创建Docker网络
$ sudo docker network create app

这里用docker network 命令创建了一个桥接网络,命名为app,这个命令返回新创建的网络的网络ID。

然后可以用docker network inspect命令查看新创建的这个网络,代码清单:

** 查看app网络
$ sudo docker network inspect app

我们可以看到这个新网络是一个本地的桥接网络(这非常像docker0网络),而且现在还没有容器在这个网络中运行。

提示:除了运行于单个主机上的桥接网络,我们也可以创建一个overlay网络,overly网络允许我们跨多台宿主机进行通信。可以在docker多宿主机文档(https://docs.docker.com/engine/userguide/networking/get-started-overlay/)中获取更多关于overlay网络的信息。

可以使用docker network ls 命令列出当前系统中的所有网络,代码清单:

** docker network ls命令
$ sudo docker network ls

也可以使用docker network rm命令删除一个Docker网络。下面我们先启动Redis容器开始,在之前创建的app网络中添加一些容器,代码清单:

** 在Docker网络中创建Redis容器
$ sudo docker run -d --net=app --name db jamtur01/redis

这里我们基于jamtur01/redis镜像创建了一个名为db的新容器。我们同时指定了一个新的标志--net,--net标志指定了新容器将会在哪个网络中运行。


这时,如果再次运行docker network inspect命令,将会看到这个网络更详细的信息,代码清单:

** 更新后的app网络
$ sudo docker network inspect app

现在在这个网络中,我们可以看到一个容器,它有一个MAC地址,并且IP地址为172.18.0.2。

接着,我们再在我们创建的网络下增加一个运行启用了Redis的Sinatra应用程序的容器,要做到这一点,需要先回到sinatra/webapp目录下,代码清单:

*** 链接Redis容器
$ cd sinatra/webapp
$ sudo docker run -p 4567 \
	--net=app --name webapp -t -i \
	-v $PWD/webapp:/opt/webapp jamtur01/siantra \
	/bin/bash

root@305c5f27dbd1:/#

注意:这是启用了Redis的Sinatra应用程序,我们在前面Docker链接的例子中用过。其代码可以从 http://dockerbook.com/code/5/sinatra/webapp_redis/或者Docker Book网站(https://github.com/jamtur01/dockerbook-code)获取。

我们在app网络下启动了一个名为webapp的容器。我们以交互的方式启动了这个容器,以便我们可以进入里面看看它内部发生了什么。

由于这个容器是在app网络内部启动的,因此Docker将会感知到所有在这个网络下运行的容器,并且通过/etc/hosts文件将这些容器的地址保存到本地DNS中。我们就在webapp容器中看看这些信息,代码清单:

** webapp容器的/etc/hosts文件
cat /etc/hosts

我们可以看到/etc/hosts文件包含了webapp容器的IP地址,以及一条localhost记录。同时,该文件还包含两条关于db容器的记录。第一条是db容器的主机名和IP地址172.18.0.2。第二条记录则将app网络名作为域名后缀添加到主机名后面,app网络内部的任何主机都可以使用hostname.app的形式来被解析,这个例子里db.app。代码清单:

** Pinging db.app
$ ping db.app

但是,在这个例子里看,我们只需要db条目就可以让我们的应用程序正常工作流,我们的Redis连接代码里使用的也是db这个主机名,代码清单:

** 代码中指定的Redis DB主机名
redis = Redis.new(:host => 'db', :port => '6379')

现在就可以启动我们的应用程序,并且让Sinatra应用程序通过db和webapp两个容器间的连接,将接收到的参数写入Redis中,db和webapp容器间的连接也是通过app网络建立的。重要的是,如果任何一个容器重启了,那么它们的IP地址信息则会自动在/etc/hosts文件中更新。也就是说,对底层容器的修改并不会对我们的应用程序正常工作产生影响。

让我们在容器内启动我们的应用程序,代码清单:

** 启动启用了Redis的Sinatra应用程序
root@305c5f27dbd1:/# nohup /opt/webapp/bin/webapp &

这里我们以后台运行的方式启动了这个Sinatra应用程序,下面我们就来检查一些我们的Sinatra容器为这个应用程序绑定了哪个端口,代码清单:

** 检查Sinatra容器的端口映射情况
$ sudo docker port webapp 4567
0.0.0.0:49161

很好,我们看到容器中的4567端口被绑定到了宿主机上的49161端口。让我们利用这些信息在Docker宿主机上,通过curl命令来测试一些我们的应用程序,代码清单:

** 测试启用了Redis的Sinatra应用程序
$ curl -i -H 'Accept: application/json' -d 'name=Foo&status=Bar' http://localhost:49161/json

接着我们再来确认一下Redis实例是否已经接收到了这次更新,代码清单:

** 确认Redis容器数据
$ curl -i http://localhost:49161/json

我们连接到了已经连接到Redis的应用程序,然后检查了一下是否存在一个名为params的键,并查询这个键,看我们的参数(name=Foo和status=Bar)是否已经保存到Redis中,一起工作正常。

(1)将已有容器连接到Docker网络
也可以将正在运行的容器通过docker network connect命令添加到已有的网络中。因此,我们可以将已经存在的容器添加到app网络中。假设已经存在的容器名为db2,这个容器里也运行着Redis,让我们将这个容器添加到app网络中去,代码清单:

** 添加已有容器到app网络
$ sudo docker network connect app db2

现在如果查看app网络的详细信息,应该会看到3个容器,代码清单:

** 添加db2容器后的app网络
$ sudo docker network inspect app

所有这3个容器的/etc/hosts文件都将会包含webapp,db和db2容器的DNS信息。

我们也可以通过docker network disconnect命令断开一个容器与指定网络的链接,代码清单:

** 从网络中断开一个容器
$ sudo docker network disconnect app db2

这条命令会从app网络中断开db2容器。

一个容器可以同时隶属于多个Docker容器,所以可以创建非常复杂的网络模型。

提示:Docker官方文档(http://docs.docker.com/engine/userguide/networking/)有很多关于Docker Networking的详细信息。

(2)通过Docker链接连接容器
连接容器的另一种选择就是使用Docker链接。在Docker 1.9之前,这是首选的容器连接方式,并且只有在运行1.9之前版本的情况下才推荐这种方式。让一个容器链接到另一个容器是一个简单的过程,这个过程要引用容器的名字。

考虑到还在使用低于Docker 1.9版本的用户,我们来看看Docker链接是如何工作的。让我们从新建一个Redis容器开始(或者也可以重用之前创建的那个容器),代码清单:

** 启动一个Redis容器
$ sudo docker run -d --name redis jamtur01/redis

提示:还记得容器的名字是唯一的吗?如果要重建一个容器,在创建另一个名叫为redis的容器之前,需要先用docker rm命令删除掉旧的redis容器。

现在我们已经在新容器里启动了一个Redis实例,并使用--name标志将新容器命名为redis。

注意:读者也注意到了,这里没有公开容器的任何端口。一会儿就能看到这么做的原因。

现在让我们启动Web应用程序容器,并把它连接到新的Redis容器上去,代码清单:

** 链接Redis容器
$ sudo docker run -p 4567 --name webapp --link redis:db -t -i -v $PWD/webapp_redis:/opt/webapp jamtur01/sinatra /bin/bash
root@811bd6d588cb:/#

提示:还需要使用docker rm命令停止并删除之前的webapp容器。

这个命令做了不少事情,我们要逐一解释。首先,我们使用-p标志公开了4567端口,这样就能从外面访问Web应用程序。

我们还使用了--anme标志给容器命名为webapp,并使用了-v标志把Web应用程序目录作为卷挂载到了容器里。

然而,这次我们使用了一个新标志--link。--link标志创建了两个容器间的客户-服务链接。这个标志需要两个参数:一个是要链接的容器的名字,另一个是链接的别名。这个例子中,我们创建了客户联系,webapp容器是客户,redis容器是"服务",并且为这个服务增加了db作为别名。这个别名让我们可以一致地访问容器公开的信息。而无须关注底层容器的名字。链接让服务容器有能力与客户容器通信,并且能分享一些连接细节,这些细节有助于在应用程序中配置并使用这个链接。

链接也能得到一些安全上的好处。注意启动Redis容器时并没有使用-p标志公开Redis的端口。因为不需要真没做。通过把容器连接在一起,可以让客户容器直接访问任意服务容器的公开端口(即客户webapp容器可以连接到服务redis容器的6379端口)。更妙的是,只有使用--link标志链接到这个容器的容器才能链接到这个端口。容器的端口不需要对本地宿主机公开,现在我们已经拥有一个非常安全的模型。通过这个安全模型,就可以限制容器化应用程序被攻击面,减少应用暴露的网络。

 提示:如果用户希望,处于安全原因(或者其它原因),可以强制Docker只允许有链接的容器之间互相通信。为此,可以在启动Docker守护进程时加上--icc=false标志,关闭所有没有链接的容器间的通信。

也可以把多个容器链接在一起。比如,如果想让这个Redis实例服务于多个Web应用程序,可以把每个Web应用程序的容器和同一个Redis容器链接在一起,代码清单:

** 链接Redis容器
$ sudo docker run -p 4567 --name webapp2 --link redis:db ...
...
$ sudo docker run -p 4567 --name webapp3 --link redis:db ...
...

我们也能够指定多次--link标志连接到多个容器。

提示:容器链接目前只能工作于同一台Docker宿主机中,不能链接位于不同Docker宿主机上的容器。对于多宿主机网络环境,需要使用Docker Networking,或者使用Docker Swarm。Docker Swarm可以用于完成多台宿主机上的Docker守护进程之间的编排。

最后,让容器启动时加载shell,而不是服务守护进程,这样可以查看容器是如何链接在一起的。Docker在父容器里的以下两个地方写入了链接信息。

** /etc/hosts文件中。
** 包含连接信息的环境变量中。



先来看看/etc/hosts文件。代码清单:

** webapp的/etc/hosts文件
root@811bd6d588cb:/# cat /etc/hosts

这里可以看到一些有用的项。第一项是容器自己的IP地址和主机名(主机名是容器ID的一部分)。第二项是由该连接指令创建的,它是Redis容器的IP地址,名字,容器ID和从该链接的别名衍生的主机名db。现在试着ping一下db容器,代码清单:

root@811bd6d588cb:/# ping db

提示:容器的主机名也可以不是其ID的一部分。可以在执行docker run命令时使用-h或者--hostname标志来为容器设定主机名。

如果在运行容器时指定--add-host选项,也可以在/etc/hosts文件中添加相应的记录。例如,我们可能想添加运行Docker的主机名和IP地址到容器中,代码清单:

** 在容器内添加/etc/hosts记录
$ sudo docker run -p 4567 --add-host=docker:10.0.0.1 --name webapp2 --link redis:db ...

这将会在容器的/etc/hosts文件中添加一个名为docker ,IP地址为10.0.0.1的宿主机记录。

提示:还记得之前提到过,重启容器时,容器的IP地址会发生变化的事情么?从Docker 1.3 开始,如果被链接的容器重启了,/etc/host文件中的IP地址会用新的IP地址更新。

我们已经连接到了Redis数据库,不过在真的利用这个链接之前,我们先来看看环境变量里包含的其他链接信息。

让我们运行env命令来查看环境变量,代码清单:

** 显示用于连接的环境变量
root@811bd6d588cb:/# env

可以看到不少环境变量,其中一些以DB开头。Docker在连接webapp和redis容器时,自动创建了这些以DB开头的环境变量。以DB开头是因为DB是创建连接时使用的别名。

这些自动创建的环境变量包含以下信息:

** 子容器的名字。
** 容器里运行的服务所使用的协议,IP和端口号。
** 容器里运行的不同服务所指定的协议,IP和端口号。
** 容器里由Docker设置的环境变量的值。

具体的变量会因容器的配置不同而有所不同(容器的Dockerfile中由ENV和EXPOSE指令定义的内容)。重要的是,这些变量包含一些我们可以在应用程序中用来进行持久的容器间链接的信息。

2.7 使用容器连接来通信

那么如何使用这个链接呢?有以下两种方法可以让应用程序连接到Redis。

** 使用环境变量里的一些连接信息。
** 使用DNS和/etc/hosts信息。

先试试第一种方法,看看Web应用程序的lib/app.rb文件是如何利用这些新的环境变量的,代码清单:

** 通过环境变量建立到Redis的连接
require 'uri'
...
uri=URI.parse(ENV['DB_PORT'])
redis=Redis.new(:host=>uri.host, :port=>uri.port)
... 

这里使用Ruby的URI模块来解析DB_PORT环境变量,然后我们使用解析后的宿主机和端口数出来配置Redis的连接信息。我们的应用程序现在就可以使用该连接信息来找到在已连接容器中的Redis了。这种抽象模式避免了我们在代码中对Redis的IP地址和端口进行硬编码,但是它仍然是一种简陋的服务发现方式。

还有一种方法,就是更灵活的本地DNS,这也是我们将要选用的解决方案,代码清单:

** 使用主机名连接Redis
redis=Redis.new(:host=>'db', :port=>'6379')

提示:也可以在docker run命令中加入--dns或者--dns-search标志来为某个容器单独配置DNS。你可以设置本地DNS解析的路径和搜索域。在https//docs.docker.com/articles/networking/上可以找到更详细的配置信息。如果没有这两个标志,Docker会根据宿主机的信息来配置DNS解析。可以在/etc/resolv.conf文件中查看DNS解析的配置情况。

我们的应用程序会在本地查找名叫db的宿主机,找到/etc/hosts文件里的相关项并解析宿主机到正确的IP地址。这也解决了硬编码IP地址的问题。

2.8 连接容器小结

我们已经了解了所有能让Docker容器互相连接的方式。在Docker 1.9及之后版本中我们推荐使用Docker Networking,而在Docker 1.9之前的版本中则建议使用Docker链接。无论采用哪种方式,都已看到,我们可以轻而易举地创建一个包含以下组件的Web应用程序栈:

** 一个运行Sinatra的Web服务器容器。
** 一个Redis数据库容器。
** 这两个容器间的一个安全连接。

应该也能看到,基于这个概念,我们可以轻易地扩展出任意数量的应用程序栈,并由此来管理复杂的本地开发环境,比如:

** Wordpress, HTML, CSS 和 JavaScript;
** Ruby on Rails;
** Django和Flask;
** Node.js
** Play1;
** 用户喜欢的其它框架。

这样就可以在本地环境构建,复制,迭代开发用于生产的应用程序,甚至很复杂的多层应用程序。

3. Docker用于持续集成

到目前为止,所有的测试例子都是本地的,围绕单个开发者的(也就是说,如何让本地开发者使用Docker来测试本地网站或者应用程序)。现在来看看在多开发者的持续集成(http://en.wikipedia.org/wiki/Continuous_integration)测试场景中如何使用Docker。

Docker 很擅长快速创建和处理一个或多个容器。这个能力显然可以为持续集成测试这个概念提供帮助。在测试场景里,用户需要频繁安装软件,或者部署到多台宿主机上,运行测试,再清理宿主机为下一次运行做准备。

在持续集成环境里,每天要执行好几次安装并分发到宿主机的过程。这为测试生命周期增加了构建和配置开销。打包和安装也消耗了很多时间,而且这个过程很恼人,尤其是需求变化频繁或者需要复杂,耗时的处理步骤进行清理的情况下。

Docker 让部署以及这些步骤和宿主机的清理变得开销很低。为了演示这一点,我们将使用Jenkins CI 构建一个测试流水线:首先,构建一个运行Docker的Jenkins服务器。为了更有意思些,我们会让Docker递归地运行在Docker内部。

提示:可以在https://github.com/jpetazzo/dind读到更多关于在Docker中运行的Docker细节。

一旦Jenkins运行起来,将展示最基本的单容器测试运行,最后将展示多容器的测试场景。

提示:除了Jenkins,还有许多其他的持续集成工具,包括Strider(http://stridercd.com)和Drone.io(https://drone.io/)这种直接利用Docker的工具,这些工具都是真正基于Docker的。另外,Jenkins也提供了一个插件,这样就可以不用使用我们将要看到的Docker-in-Docker这种方式了。使用Docker插件可能更简单,但我觉得使用Docker-in-Docker这种方式更有趣。

3.1 构建Jenkins和Docker服务器

为了提供一个Jenkins服务器,从Dockerfile开始构建了一个安装了Jenkins和Docker的Ubuntu 14.04镜像。我们先创建一个jenkins目录,来存放构建所需的所有相关文件,代码清单:

** 为Jenkins创建目录
$ mkdir jenkins
$ cd jenkins

在jenkins目录中,我们从Dockerfile开始,代码清单:

** Jenkins和Docker服务器的Dockerfile
FROM ubuntu:14.04
MAINTAINER james@example.com
ENV REFRESHED_AT 2014-06-01

RUN apt-get update -qq && apt-get install -qqy curl apt-transport-https
RUN apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E86F3A912879C070ADBF76221572C52609D

RUN echo deb https://apt.dockerproject.org/repo ubuntu-trusty main > /etc/apt/sources.list.d/docker.list
RUN apt-get update -qq && apt-get install -qqy iptables cacertificates openjdk-7-jdk git-core docker-engine

ENV JENKINS_HOME /opt/jenkins/data
ENV JENKINS_MIRROR http://mirrors.jenkins-ci.org

RUN mkdir -p $JENKINS_HOME/plugins
RUN curl -sf -o /opt/jenkins/jenkins.war -L $JENKINS_MIRROR/warstable/latest/jenkins.war

RUN for plugin in chucknorris greenballs scm-api git-client git ws-cleanup ;\
			do curl -sf -o $JENKINS_HOME/plugins/${plugin}.hpi \
			-L $JENKINS_MIRROR/plugins/${plugin}/latest/${plugin}.hpi ; done

ADD ./dockerjenkins.sh /usr/local/bin/dockerjenkins.sh
RUN chmod +x /usr/local/bin/dockerkenkins.sh

VOLUME /var/lib/docker

EXPOSE 8080

ENTRYPOINT [ "/usr/local/bin/dockerjenkins.sh" ]

可以看到,Dockerfile继承自ubuntu:14.04镜像,之后做了很多事情。确实,这是目前为止见过的最复杂的Dockerfile。来看看都做了什么。

首先,它设置了Ubuntu环境,加入了需要的Docker APT仓库,并加入了对应的GPG key。之后更新了包列表,并安装执行Docker和Jenkins所需要的包。

然后,我们创建了/opt/jenkins目录,并把最新稳定版本的Jenkins下载到这个目录。还需要一些Jenkins插件,给Jenkins提供额外的功能(比如支持Git版本控制)。

我们还使用ENV指令把JENKINS_HOME和JENKINS_MIRROR环境变量设置为Jenkins的数据目录和镜像站点。

然后我们指定了VOLUME指令。VOLUME指令从容器运行的宿主机上挂载一个卷。在这里,为了"骗过"Docker,指定/var/lib/docker作为卷。这是因为/var/lib/docker目录是Docker用来存储其容器的目录。这个位置必须是真实的文件系统,而不能是像Docker镜像层那样的挂载点。

那么,我们使用VOLUME指令告诉Docker进程,在容器运行内部使用宿主机的文件系统作为容器的存储。这样,容器内嵌Docker的/var/lib/docker目录将保存在宿主机系统的/var/lib/docker/volumes目录下的某个位置。

我们已经公开了Jenkins默认的8080端口。

最后,我们指定了一个要运行的shell脚本(可以在http://dockerbook.com/code/5/jenkins/dockerjenkins.sh找到)作为容器的启动命令。这个shell脚本(由ENTRYPOINT指令指定)帮助在宿主机上配置Docker,允许在Docker里运行Docker,开启Docker守护进程,并且启动Jenkins。在https://github.com/jpetazzo/dind可以看到更多关于为什么需要一个shell脚本来允许Docker中运行Docker的信息。

现在让我们来获取这个shell脚本。我们继续在jenkins目录下工作,刚刚我们在这个目录下创建了Dockerfile文件,代码清单:

** 获取dockerjenkins.sh脚本
$ cd jenkins
$ wget https://raw.githubusercontent.com/jamtur01/dockerbook-code/master/code/5/jenkins/dockerjenkins.sh
$ chmod 0755 dockerjenkins.sh

已经有了Dockerfile,用docker build命令来构建一个新的镜像,代码清单:

** 构建Docker-Jenkins镜像
$ sudo docker build -t jamtur01/dockerjenkins .

我们非常没创意的把新的镜像命名为jamtur01/dockerjenkins。现在可以使用docker run命令从这个镜像创建容器了,代码清单:

** 运行Docker-Jenkins镜像
$ sudo docker run -p 8080:8080 --name jenkins --privileged \ 
	-d jamtur01/dockerjenkins
......

可以看到,这里使用了一个新标志--privileged来运行容器。--privileged标志很特别,可以启动Docker的特权模式,这种模式允许我们以其宿主机具有的(几乎)所有能力来运行容器,包括一些内核特性和设备访问。这是让我们可以在Docker中运行Docker必要的魔法。

警告:让Docker运行在--privileged特权模式会有一些安全风险。在这种模式下运行容器对Docker宿主机拥有root访问权限。确保已经对Docker宿主机进行了恰当的安全保护,并且只在确实可信的域里使用特权访问Docker宿主机,或者仅在有类似信任的情况下运行容器。

还可以看到,我们使用了-p标志在本地宿主机的8080端口上公开8080端口。一般来说,这不是一种好的做法,不过足以让一台Jenkins服务器运行起来。

可以看到新容器jenkins已经启动了。我们可以查看一下启动后的日志,代码清单:

** 检查Docker Jenkins容器的日志
$ sudo docker logs jenkins
....

要么不断地检查日志,要么使用-f标志运行docker logs命令,直到看到与代码清单如下类似的消息:

** 检查Jenkins的启动和执行
INFO: Jenkins is fully up and running

太好了。现在Jenkins服务器应该可以通过8080端口在浏览器访问了。

3.2 创建新的Jenkins作业

现在Jenkins服务器已经运行,让我们来创建一个Jenkins作业吧。单击create new jobs(创建新作业)链接,打开了创建新作业的向导。

上一篇:(springboot+vue)后端利用vue前端传来的ID值来播放具体视频的功能实现


下一篇:SpringBoot在启动其项目后,访问不到webapp下的内容