Ubuntu18.04下使用 Docker 构建并测试 Web 应用程序

构建 Sinatra 应用程序

创建文件夹:

mkdir sample01 && cd sample01
touch Dockerfile

编辑 Dockerfile :

FROM ubuntu:18.04
MAINTAINER nikki [email protected]
ENV REFRESHED_AT 2019-10-19

RUN apt-get update
RUN apt-get -y install ruby-full
RUN apt-get -y install  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" ]

Dockerfile 依旧完成了一些安装任务,并在最后使用 CMD 指定 /opt/webapp/bin/webapp 作为 Web 应用程序的启动文件

构建:

sudo docker build -t nikki01/sinatra .

创建 Sinatra 容器

创建镜像之后,下载 Sinatra Web 应用程序的源代码,因为原书给的链接失效了,所以我将整本书的代码都下载下来

链接:https://pan.baidu.com/s/1mw_I0mBv6n78N2UGDTW_DQ&shfl=sharepset
提取码:m68w

这里用到的文件目录在:

dockerbook-code-master/code/5/sinatra

找到其中的 webapp 目录,将其复制到 sample01 中并查看:

ls -l webapp

应该有 bin、lib、Dockerfile 三个文件夹

给其中的文件添加可执行权限:

chmod +x $PWD/webapp/bin/webapp

使用 docker run 从镜像创建一个新容器:

sudo docker run -d -p 4567 --name webapp -v $PWD/webapp:/opt/webapp nikki01/sinatra

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

CMD 指令指定了要运行的命令,此时可以使用 docker logs 查看输出:

sudo docker logs webapp

使用 docker top 命令查看 Docker 容器里正在运行的进程:

sudo docker top webapp

查看刚才指定的端口映射到本地宿主机的哪个端口:

sudo docker port webapp 4567

在我的机器上是映射到 32772
在这里插入图片描述
目前的 Sinatra 应用没做什么,只是接受输入参数,可以使用 curl 命令(先确保安装了 curl)测试这个程序:

curl -i -H 'Accept:application/json' -d 'name=Foo&status=Bar' http://localhost:32772/json

在这里插入图片描述
可以看到,传入的参数转化成 JSON 散列后的输出:{“name”:“Foo”,“status”:“Bar”}

构建 Redis 镜像和容器

现在我们要扩展 Sinatra 应用程序,加入 Redis 后端数据库,并在 Redis 数据库中存储输入的参数,因此需要构建全新的镜像和容器运行 Redis 数据库,之后利用 Docker 的特性关联两个容器

创建一个新的镜像:

mkdir sample02 && cd sample02
touch Dockerfile

Dockerfile 内容如下:

FROM ubuntu:18.04
MAINTAINER nikki [email protected]
ENV REFRESHED_AT 2019-10-20
RUN apt-get update
RUN apt-get -y install redis-server redis-tools
EXPOSE 6379
ENTRYPOINT ["/usr/bin/redis-server"]
CMD []

Dockerfile 里指定了安装 Redis 服务器,公开 6379 端口,并指定了启动 Redis 服务器的 ENTRYPOINT。构建此镜像:

sudo docker build -t nikki01/redis .

构建容器:

sudo docker run -d -p 6379 --name redis nikki01/redis

查看 6379 映射到宿主机的哪个端口:

sudo docker port redis 6379

我的是 32773 端口
在这里插入图片描述
在本地安装 redis-tools 包:

sudo apt-get -y install redis-tools

使用 redis-cli 命令确认 Redis 服务器工作是否正常:

redis-cli -h 127.0.0.1 -p 32773

在这里插入图片描述

连接到 Redis 容器

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

  • 第一种方法涉及 Docker 的网络栈,到现在为止,我们的 Docker 容器都是公开端口并绑定到本地网络接口的,这样可以把容器里的服务在本地 Docker 宿主机所在的外部网络上公开
  • 第二种做法是内部网络

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

ip a show docker0

在这里插入图片描述
docker0 接口有符合 RFC1918 的私有 IP 地址,范围是 172.16 ~ 172.30。接口本身的地址 172.17.0.1 是这个 Docker 网络的网关地址,也是所有 Docker 容器的网关地址

Docker 会默认使用 172.17.x.x作为子网地址,除非已经有别人占用了这个子网,如果这个子网被占用了,Docker 会在 172.16 ~ 172.30 这个范围内尝试创建子网

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

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

进入容器,查看子网管道的另一端:

sudo docker run -t -i ubuntu:18.04 /bin/bash
/# ip a show eth0

如果提示 bash: ip: command not found

请运行:

/# apt update
/# apt -y install iproute2

结果如下:
在这里插入图片描述
可以看到,Docker 给容器分配了 IP 地址 172.17.0.3 作为宿主虚拟接口的另一端,这样就能让宿主网络与容器互相通信了

接下来从容器内跟踪对外通信的路由,看看是如何建立连接的:

/# apt update && apt -y install traceroute
/# traceroute baidu.com

如果你的结果像这样:
在这里插入图片描述
说明没有达到我们希望跟踪数据包的目的,原因是 traceroute 命令默认使用UDP协议来探测包,而很多路由器都禁用了traceroute 命令的 UDP 包,即不会处理 traceroute 命令发送的 UDP 包,导致无法探测出路径中的路由器。可以使用选项 -I 来强制 traceroute 命令使用 ICMP 协议,例如:

/# traceroute -I baidu.com

结果如下:
在这里插入图片描述
从上面可以看出,数据包从 localhost 到目标主机 baidu.com 经过 18 个路由,有些路由无法探测出其 IP 地址

容器地址后的下一跳是宿主网络上 docker0 接口的网关 IP 172.17.0.1

不过 Docker 网络还有另一个部分配置才能允许建立连接:防火墙规则和 NAT 配置。这些配置允许 Docker 在宿主网络和容器间路由。现在查看一下宿主机上的 IPTables NAT 配置(新开一个终端):

sudo iptables -t nat -L -n

结果如下:

在这里插入图片描述

首先,容器默认是无法访问的。从宿主网络与容器通信时,必须明确指定打开的端口。下面以 DNAT(即目标 NAT)这个规则为例,这个规则把容器里的访问路由映射到 Docker 宿主机的 32773 端口

连接 Redis

用 docker inspect 命令查看新的 Redis 容器的网络配置,会展示 Docker 的细节:

sudo docker inspect redis

从 Ports 中可以看出 6379 端口被映射到本地宿主机的 32773 端口

此外,图中 Bridge 对应是 “”,按道理应该是使用 docker0 接口作为网关地址,我往上翻了一下发现:

在这里插入图片描述

Driver 是 overlay2(18.06.0上的默认存储驱动,在这之前的版本都是默认桥接方式,因此上面的规则适合之前版本),它和桥接模式的区别这里就不赘述了,感兴趣的可以看一下:Docker network命令

可以在命令中用 -f 只获取 IP 地址

sudo docker inspect -f '{{ .NetworkSettings.IPAddress }}' redis

我这里是 172.17.0.2

在这里插入图片描述

利用 redis-cli 让 172.17.0.2 地址与 Redis 服务器的 6379 端口通信:

redis-cli -h 172.17.0.2

这样就实现通信了,但是这种方法存在两个问题:

  • 要在应用程序里对 Redis 容器的 IP 地址做硬编码
  • 如果重启容器,Docker 会改变容器的 IP 地址
sudo docker restart redis
sudo docker inspect -f '{{ .NetworkSettings.IPAddress }}' redis

因为我这里是 overlay2 方式,所以 IP 地址没变(?),具体我之后再深究

让 Docker 容器互连

新建一个 Redis 容器:

sudo docker run -d --name redis01 nikki01/redis

启动 Web 应用程序容器,并把它连接到新的 Redis 容器上:

sudo docker run -p 4567 --name webapp01 --link redis01:db -t -i -v $PWD/webapp:/opt/webapp nikki01/sinatra /bin/bash

-p 标志公开了 4567 端口,这样就能从容器外面访问 Web 应用程序

–name 给容器命名为 webapp01,并使用了 -v 标志把 Web 应用程序作为卷挂载到了容器里

–link 标志创建了两个容器间的父子连接,这个标志需要两个参数:

  • 连接的容器名字
  • 连接后容器的别名

这个例子中,我们把新容器连接到 redis 容器,并使用 db 作为别名。别名可以让我们访问公开的信息,而无需关注底层容器的名字。连接让父容器有能力访问子容器,并且把子容器的一些连接细节分享给父容器,这些细节有助于配置应用程序并使用这个连接

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

也可以把多个容器连接在一起:

sudo docker run -p 4567 --name webapp02 --link redis:db ...
sudo docker run -p 4567 --name webapp03 --link redis:db ...

被连接的容器必须运行在用一个 Docker 宿主机上,不同 Docker 宿主机上运行的容器无法连接

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

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

查看 /etc/hosts 文件:

/# cat /etc/hosts

在这里插入图片描述
还记得 172.17.0.2 和 172.17.0.3 吗,它们分别是 redis01 和 webapp01 的 IP 地址

现在试着 ping 一下 db 容器:

/# ping db

如果你没有 ping 命令,那么:

apt update
apt install -y iputils-ping

结果如下:

在这里插入图片描述

我们已经连接到了 Redis 数据库,先看看环境变量里包含的其他连接信息:

/# env

在这里插入图片描述

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

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

这些环境变量会随容器不同而变化,取决于容器是如何配置的。更重要的是,这些连接信息可以让容器内的应用程序使用相同的方法与别的容器进行连接,而不用关心被连接的容器的具体细节

使用容器连接来通信

给 Sinatra 应用程序加入一些连接信息,以便于 Redis 通信,有以下两种方法可以让应用程序连接到 Redis:

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

先试试第一种方法,在dockerbook-code-master/code/5/sinatra/ 中的 webapp/lib/app.rb:

在这里插入图片描述
这里和书中的代码不一样,书中代码:

require 'uri'
...
uri=URO.parse([ENV'DB_PORT'])
redis = Redis.new(:host => uri.host, "port => uri.port")

这里使用 Ruby 的 URI 模块来解析 DB_PORT 环境变量,并使用解析后的结果配置 Redis 连接,应用程序现在可以使用这个连接信息找到相连的 Redis 容器。通过环境变量,这里不再需要硬编码 IP 地址和端口进行连接。这是一种发现服务的方法

另外一种方法,在 dockerbook-code-master/code/5/sinatra/ 中的 webapp_redis/lib/app.rb:

在这里插入图片描述

图中

redis = Redis.new(:host => 'db', :port => '6379')

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

在容器里启动应用程序,看看 DNS 本地解析能不能工作:

/# nohub /opt/webapp/bin/webapp &

你可能会发现它找不到 nohub,别着急下载,进入 /usr/bin/,然后就可以看到 nohub 了,如果还没有请参考:-bash: nohup: command not found

不过你可能会有疑问, /opt/webapp/ 中并没有任何东西,没错,就是没有,原版书上这里可能有误,所以你需要先 down 文件下来:

/# /opt/webapp  git clone https://github.com/Nikkio3o/book.git

接下来你只需要找到 /book/dockerbook-code-master/code/5/sinatra/webapp/bin/webapp 就可以了,如果它提示你 Permission denied,你就这样:

/# sudo /usr/bin/nohub sudo /opt/webapp/book/dockerbook-code-master/code/5/sinatra/webapp/bin/webapp &

没有 sudo 的话就安装 sudo

这里启动了 Sinatra 应用程序并让其在后台运行,现在在 Docker 宿主机(新开一个终端)上再次使用 curl 命令测试应用程序:

curl -i -H 'Accept:application/json' -d 'name=Foo&status=Bar' http://localhost:32780/json

为什么是 32780 呢,因为:

sudo docker ps

在这里插入图片描述
你可能得到

在这里插入图片描述

这样的结果,这个具体原因可能跟现在版本容器运行机制有关,所以直接进入 /opt/webapp/book/dockerbook-code-master/code/5/sinatra/webapp/bin/ 这个目录,然后:

chmod +x webapp
./webapp

然后再运行

curl -i -H 'Accept:application/json' -d 'name=Foo&status=Bar' http://localhost:32780/json

就可以看到:
在这里插入图片描述
现在来确认一下 Redis 实例接收到了这个更新:

curl -i http://localhost:32780/json "[{\"name\":\"Foo\",\"status\":\"Bar\"}]"

在这里插入图片描述

我也不知道为什么是 404。。

现在已经连接到了应用程序(应用程序连接到了 Redis),应用程序会检查 Redis 里存储的键,找出一个叫 params 的键,然后查询这个键来看看我们的两个参数(name=Foo 和 status=Bar)已经存入了 Redis

至此,用于演示 Web 应用程序栈的例子终于写完了,这个 Web 应用程序栈由以下几部分组成:

  • 一个运行 Sinatra 的 Web 服务器容器
  • 一个 Redis 数据库容器
  • 这两个容器间的一个安全连接
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章