Docekr学习笔记四构建并测试web应用程序
- 构建Sinatra应用程序创建sinatra工作目录,编写Dockerfile文件构建一个镜像,并利用该镜像生成容器应用。重点我们将讲述如何将多个容器连接形成应用栈。
$ md sinatra && cd sinatra $ vi Dockerfile FROM ubuntu MAINTAINER Kael Wang zb55@foxmail.com ENV REFRESHED_AT 2016-12-12 RUN apt-get update RUN apt-get -y 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"] $ docker build -t kaelwang/sinatra . Sending build context to Docker daemon 10.24 kB Step 1 : FROM ubuntu ---> 44f18eb3c13f Step 2 : MAINTAINER Kael Wang zb55@foxmail.com ---> Using cache ---> 0230268896a0 Step 3 : ENV REFRESHED_AT 2016-12-12 ---> Using cache ---> 4aad3f178a57 Step 4 : RUN apt-get update ---> Using cache ---> 254a26583566 Step 5 : RUN apt-get -y install ruby ruby-dev build-essential redis-tools ---> Using cache ---> 3a0a3f602557 Step 6 : RUN gem install --no-rdoc --no-ri sinatra json redis ---> Using cache ---> 933b8b6d6d84 Step 7 : RUN mkdir -p /opt/webapp ---> Using cache ---> 710d3db42f72 Step 8 : EXPOSE 4567 ---> Using cache ---> 01c8718f0607 Step 9 : CMD /opt/webapp/bin/webapp ---> Running in 2acb0e4a3bc3 ---> d9d3d801d9e1 Removing intermediate container 2acb0e4a3bc3 Successfully built d9d3d801d9e1 docker run -d -p 4567 --name webapp -v $PWD/webapp:/opt/webapp kaelwang/sinatra 19a8e0112a32e93505dcf49d2852f9035d5ecbbf4a3fe960269e78a1187af755 $ docker logs webapp [2016-07-27 06:40:04] INFO WEBrick 1.3.1 [2016-07-27 06:40:04] INFO ruby 2.3.1 (2016-04-26) [x86_64-linux-gnu] == Sinatra (v1.4.7) has taken the stage on 4567 for development with backup from WEBrick [2016-07-27 06:40:04] INFO WEBrick::HTTPServer#start: pid=1 port=4567 $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 19a8e0112a32 kaelwang/sinatra "/opt/webapp/bin/weba" 5 seconds ago Up 3 seconds 0.0.0.0:32770->4567/tcp webapp
目前,我们已经建立了sinatra容器应用,此应用还很基础,指示接受输入参数,并将输入转化为JSON输出。现在,我们用curl命令来测试这个应用程序。
$ curl -i -H 'Accept:application/json' -d 'name=Foo&status=Bar' http://localhost:32770/json HTTP/1.1 200 OK Content-Type: text/html;charset=utf-8 Content-Length: 29 X-Xss-Protection: 1; mode=block X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN Server: WEBrick/1.3.1 (Ruby/2.3.1/2016-04-26) Date: Wed, 27 Jul 2016 06:46:33 GMT Connection: Keep-Alive {"name":"Foo","status":"Bar"}
我们可以看到,给sinatra应用程序传入了一些参数,并转化为了json散列后输出:{“name”:”Foo”,”status”:”Bar”}成功。
我们添加一个服务,这个服务运行在另一个容器里,把当前的示例应用程序扩展为真正的应用栈。
- 构建Redis镜像和容器扩展sinatra应用程序,加入Redis后端数据库,并在Redis数据库中存储输入的参数。为了达到这个目的,要构建全新的镜像和容器运行redis数据库。之后,利用Docker的特性来关联两个容器。
$ md redisdocker&&cd redisdocker $ vi Dockerfile FROM ubuntu MAINTAINER Kael Wang zb55@foxmail.com ENV REFRESHED_AT 2016-12-12 RUN apt-get update RUN apt-get -y isntall redis-server redis-tools EXPOSE 6379 ENTRYPOINT ["/usr/bin/redis-server"] CMD [] $ docker build -t kaelwang/redis . Sending build context to Docker daemon 2.048 kB Step 1 : FROM ubuntu ---> 44f18eb3c13f Step 2 : MAINTAINER Kael Wang zb55@foxmail.com ---> Using cache ---> 0230268896a0 Step 3 : ENV REFRESHED_AT 2016-12-12 ---> Using cache ---> 4aad3f178a57 Step 4 : RUN apt-get update ---> Using cache ---> 254a26583566 Step 5 : RUN apt-get -y install redis-server redis-tools ---> Using cache ---> d0cf8437fb46 Step 6 : EXPOSE 6379 ---> Using cache ---> 7a8d1aa862b4 Step 7 : ENTRYPOINT /usr/bin/redis-server ---> Running in c0ce2aeb9eda ---> dd4f375c2236 Removing intermediate container c0ce2aeb9eda Step 8 : CMD ---> Running in a386c0559e6c ---> e730c0e265d3 Removing intermediate container a386c0559e6c Successfully built e730c0e265d3
从这个镜像构建容器
$ docker run -d -p 6379 --name redis kaelwang/redis 6fdcfc399370872475d654563c24c54c3c9a1c67806a2d7eee7a2eaea8175269 $ docker port redis 6379/tcp -> 0.0.0.0:32771
我们用本机redis测试一下容器里的redis服务是否工作
$ redis-cli -h 127.0.0.1 -p 32771 127.0.0.1:32771>
- Docker容器网络知识现在来更新sinatra应用程序,让其连接到redis并存储传入的参数。为此,需要能够与redis服务器对话,要做到这一点,可以有几种方法。第一种涉及Docker自己的网络栈。到目前为止,我们看到的Docker容器都是公开端口并绑定到本地网络接口的,这样可以吧容器里的服务在本地 Docker宿主机所在的外部网络上公开,除了这样方法,Docker这个特性还有种内部网络。在安装Docker时,会创建一个新的网络接口,名字是 docker0.每个Docker容器都会在这个接口上分配一个ip地址。
$ ip a show docker0 3: docker0: <broadcast,multicast,up,lower_up> mtu 9001 qdisc noqueue state UP group default link/ether 02:42:3e:8e:ae:4d brd ff:ff:ff:ff:ff:ff inet 172.17.0.1/16 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::42:3eff:fe8e:ae4d/64 scope link valid_lft forever preferred_lft forever
docker0是一个虚拟的以太网桥,用于连接容器和本地宿主机。docker0复合RFC1918的私有ip地址,范围是172.16.x.x到172.30.x.x同时Docker网络必须还要依赖防火墙和NAT规则才能允许被建立连接
$ iptables -t nat -L -n Chain PREROUTING (policy ACCEPT) target prot opt source destination DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL Chain INPUT (policy ACCEPT) target prot opt source destination Chain OUTPUT (policy ACCEPT) target prot opt source destination DOCKER all -- 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL Chain POSTROUTING (policy ACCEPT) target prot opt source destination MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0 MASQUERADE tcp -- 172.17.0.3 172.17.0.3 tcp dpt:80 MASQUERADE tcp -- 172.17.0.4 172.17.0.4 tcp dpt:4567 MASQUERADE tcp -- 172.17.0.5 172.17.0.5 tcp dpt:6379 Chain DOCKER (2 references) target prot opt source destination DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.3:80 DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:32770 to:172.17.0.4:4567 DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:32771 to:172.17.0.5:6379
容器默认是无法访问的,从宿主网络与容器通信时,必须明确指定打开的端口,下面我们以DNAT即目标NAT这个规则为例,这个规则吧容器里的访问路由到Docker宿主机的32771端口。
- 通过Docker内网连接Redis
$ docker inspect redis . . . "NetworkSettings": { "Bridge": "", "SandboxID": "b4bb7703aa7d658f8309abbfcdf7fe575ac5faa3546805e33dc44be86e3932cb", "HairpinMode": false, "LinkLocalIPv6Address": "", "LinkLocalIPv6PrefixLen": 0, "Ports": { "6379/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "32771" } ] }, "SandboxKey": "/var/run/docker/netns/b4bb7703aa7d", "SecondaryIPAddresses": null, "SecondaryIPv6Addresses": null, "EndpointID": "8e96a4e055c46a34331f3ae3b3c455ecdcddb6a16b2650fe53cdb638127e5255", "Gateway": "172.17.0.1", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "IPAddress": "172.17.0.5", "IPPrefixLen": 16, "IPv6Gateway": "", "MacAddress": "02:42:ac:11:00:05", "Networks": { "bridge": { "EndpointID": "8e96a4e055c46a34331f3ae3b3c455ecdcddb6a16b2650fe53cdb638127e5255", "Gateway": "172.17.0.1", "IPAddress": "172.17.0.5", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "MacAddress": "02:42:ac:11:00:05" } } . . .
docker inspect命令展示Docker容器的细节,这里只显示网络信息。容器的ip地址为172.17.0.5,并使用网关172.17.0.1即 docker0。也可以看到容器6379端口被映射到本地宿主机32771端口。只是,因为运行在本地Docker宿主机上,所以不是一定要用映射的端 口,也可以直接使用Docker0分配的内网地址
$ redis-cli -h 172.17.0.5 172.17.0.5:6379>
但是,通过这种方法连接两个容器,有两个大问题,第一,要在应用内对redis容器的ip地址做硬编程,第二,如果重启或容器意外中断后恢复,Docker会改变容器的ip地址。
- 引用容器名字让两个容器相连首先删掉原来我们构建的Sinatra和redis容器,然后重新构建一个新的Sinatra和redis容器
$ docker stop redis sinatra $ docker rm redis sinatra
实现连接
$ docker run -d --name redis kaelwang/redis a843b4e5adaa3630a08ebdbe8828a6028fbf6331ba537f082f4484dff8a6b27b $ docker run -p 4567 --name webapp --link redis:db -t -i -v $PWD/webapp:/opt/webapp kaelwang/sinatra /bin/bash root@d205ff072682:/# cat /etc/hosts 172.17.0.4 d205ff072682 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters 172.17.0.5 db a843b4e5adaa redis root@d205ff072682:/# env HOSTNAME=d205ff072682 DB_NAME=/webapp/db DB_PORT_6379_TCP_PORT=6379 TERM=xterm DB_PORT=tcp://172.17.0.5:6379 DB_PORT_6379_TCP=tcp://172.17.0.5:6379 LS_COLORS=rs=0:di=01;34:ln=01;...(此处信息较多省略) DB_ENV_REFRESHED_AT=2016-12-12 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin REFRESHED_AT=2016-12-12 PWD=/ DB_PORT_6379_TCP_ADDR=172.17.0.5 DB_PORT_6379_TCP_PROTO=tcp SHLVL=1 HOME=/root _=/usr/bin/env root@d205ff072682:/#
–link标志创建了两个容器间的父子相连,这个标志需要两个参数,一个是要连接的容器名字,另一个是连接后容器的别名。此处别名是db,别名让 我们可以访问公开的信息,而无需关注底层容器的名字,连接让父容器有能力访问子容器,并且把子容器的一些连接细节分享给父容器。这些细节有助于配置应用程 序并使用这个连接。
连接也能得到一些安全上的额好处。启动redis容器时,并没有使用-p标志公开redis的端口。通过父容器直接访问任意子容器的公开端口。且只 有使用的–link标志连接到这个容器的容器才能连接到这个端口,容器的端口不需要对本地宿主机公开,现在我们已经拥有了一个非常安全的模型,在这个模 型里,容器话的应用程序限制了可被攻击的界面,减少了公开暴露的网络。也可以吧多个容器连接到一起,比如一个redis示例服务于多个web应用服务器。 出于一些原因,我们可以在启动Docker守护进程时加上–icc=false标志,关闭所有没有连接的容器间的通讯,这样强制Docker只允许有连 接的容器之间相互通信。
我们通过env查看环境变量发现有一些以别名DB开头的,Docker在连接webapp和redis容器时,自动创建了这些以DB开头的环境变量。 这些自动创建的环境变量包含以下信息。
- 子容器的名字
- 容器里运行的服务所使用的协议,ip和端口
- 容器里运行的不同的服务所指定的协议,ip和端口
- 容器里由Docker设置的环境变量的值。
- 使用容器连接来通信
- 使用环境变量里的一些连接信息。
- 使用DNS和/etc/hosts信息。
应用程序会在本地查找名叫db的主机,找到/etc/hosts文件里的项解析到正确的ip地址,这也解决了硬编码IP地址的问题,因为hosts文件会随着你的Docker启动时自动更新相应信息。
这个web应用程序栈由以下几部分组成
- 一个运行Sinatra和web服务器容器
- 一个Redis数据库容器
- 两个容器间的一个安全连接
这样就可以在本地环境构建,复制,迭代开发用于生产的应用程序,甚至很多复杂的多层应用程序。
0 条评论