Docker-网络秘籍(三)

163 阅读1小时+

Docker 网络秘籍(三)

原文:zh.annas-archive.org/md5/15C8E8C8C0D58C74AF1054F5CB887C66

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:容器链接和 Docker DNS

在本章中,我们将涵盖以下内容:

  • 验证容器内的基于主机的 DNS 配置

  • 覆盖默认名称解析设置

  • 配置名称和服务解析的链接

  • 利用 Docker DNS

  • 创建 Docker DNS 别名

介绍

在前几章中,我已经指出 Docker 在网络空间为您做了很多事情。正如我们已经看到的,通过 IPAM 管理 IP 分配是使用 Docker 时并不明显的巨大好处。Docker 为您提供的另一项服务是 DNS 解析。正如我们将在本章中看到的,Docker 可以提供多个级别的名称和服务解析。随着 Docker 的成熟,提供这些类型的服务的选项也在不断增加。在本章中,我们将开始审查基本的名称解析以及容器如何知道使用哪个 DNS 服务器。然后,我们将涵盖容器链接,并了解 Docker 如何告诉容器有关其他容器和它们托管的服务。最后,我们将介绍随着用户定义网络的增加而带来的一些 DNS 增强功能。

验证容器内的基于主机的 DNS 配置

您可能没有意识到,但默认情况下,Docker 为您的容器提供了基本的名称解析手段。Docker 将名称解析选项从 Docker 主机直接传递到容器中。结果是,生成的容器可以本地解析 Docker 主机本身可以解析的任何内容。Docker 用于在容器中实现名称解析的机制非常简单。在本教程中,我们将介绍如何完成这项工作以及如何验证它是否按预期工作。

准备就绪

在本教程中,我们将演示单个 Docker 主机上的配置。假设该主机已安装 Docker,并且 Docker 处于默认配置状态。我们将在主机上更改名称解析设置,因此您需要 root 级别的访问权限。

操作步骤:

让我们在我们的主机docker1上启动一个新的容器,并检查容器如何处理名称解析:

user@docker1:~$ docker run -d -P --name=web8 \
jonlangemak/web_server_8_dns
d65baf205669c871d1216dc091edd1452a318b6522388e045c211344815c280a
user@docker1:~$

user@docker1:~$ docker exec web8 host **www.google.com
www.google.com has address **216.58.216.196
www.google.com has IPv6 address 2607:f8b0:4009:80e::2004 
user@docker1:~ $

看起来容器有能力解析 DNS 名称。如果我们查看我们的本地 Docker 主机并运行相同的测试,我们应该会得到类似的结果:

user@docker1:~$ host www.google.com
www.google.com has address **216.58.216.196
www.google.com has IPv6 address 2607:f8b0:4009:80e::2004
user@docker1:~$ 

此外,就像我们的 Docker 主机一样,容器也可以解析与本地域lab.lab相关的本地 DNS 记录:

user@docker1:~$ docker exec web8 host **docker4
docker4.lab.lab** has address **192.168.50.102
user@docker1:~$

您会注意到,我不需要指定一个完全合格的域名来解析域lab.lab中的主机名docker4。此时,可以安全地假设容器正在从 Docker 主机接收某种智能更新,为其提供有关本地 DNS 配置的相关信息。

注意

请注意,resolv.conf文件通常是您定义 Linux 系统名称解析参数的地方。在许多情况下,它会被其他地方的配置信息自动更改。但是,无论如何更改,它都应该始终是系统处理名称解析的真相来源。

要查看容器正在接收的内容,让我们检查容器的resolv.conf文件:

user@docker1:~$ docker exec -t web8 more **/etc/resolv.conf
::::::::::::::
/etc/resolv.conf
::::::::::::::
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 10.20.30.13
search lab.lab
user@docker1:~$

正如您所看到的,容器已经学会了本地 DNS 服务器是10.20.30.13,本地 DNS 搜索域是lab.lab。它是从哪里获取这些信息的?答案相当简单。当容器启动时,Docker 为每个生成的容器实例生成以下三个文件的实例,并将其保存在容器配置中:

  • /etc/hostname

  • /etc/hosts

  • /etc/resolv.conf

这些文件作为容器配置的一部分存储,然后挂载到容器中。我们可以使用容器内的findmnt工具来检查挂载的来源:

root@docker1:~# docker exec web8 findmnt -o SOURCE
…<Additional output removed for brevity>…
/dev/mapper/docker1--vg-root[**/var/lib/docker/containers/c803f130b7a2450609672c23762bce3499dec9abcfdc540a43a7eb560adaf62a/resolv.conf
/dev/mapper/docker1--vg-root[**/var/lib/docker/containers/c803f130b7a2450609672c23762bce3499dec9abcfdc540a43a7eb560adaf62a/hostname]
/dev/mapper/docker1--vg-root[**/var/lib/docker/containers/c803f130b7a2450609672c23762bce3499dec9abcfdc540a43a7eb560adaf62a/hosts]
root@docker1:~#

因此,虽然容器认为它在其/etc/目录中有hostnamehostsresolv.conf文件的本地副本,但实际文件实际上位于 Docker 主机上的容器配置目录(/var/lib/docker/containers/)中。

当您告诉 Docker 运行一个容器时,它会执行以下三件事:

  • 它检查 Docker 主机的/etc/resolv.conf文件,并将其副本放在容器目录中

  • 它在容器的目录中创建一个hostname文件,并为容器分配一个唯一的hostname

  • 它在容器的目录中创建一个hosts文件,并添加相关记录,包括 localhost 和引用主机本身的记录

每次容器重新启动时,容器的resolv.conf文件都会根据 Docker 主机resolv.conf文件中找到的值进行更新。这意味着每次容器重新启动时,对resolv.conf文件所做的任何更改都会丢失。hostnamehosts配置文件也会在每次容器重新启动时被重写,丢失在上一次运行期间所做的任何更改。

为了验证给定容器正在使用的配置文件,我们可以检查这些变量的容器配置:

user@docker1:~$ docker inspect web8 | grep **HostsPath
“HostsPath”: “/var/lib/docker/containers/c803f130b7a2450609672c23762bce3499dec9abcfdc540a43a7eb560adaf62a/hosts”,
user@docker1:~$ docker inspect web8 | grep **HostnamePath
“HostnamePath”: “/var/lib/docker/containers/c803f130b7a2450609672c23762bce3499dec9abcfdc540a43a7eb560adaf62a/hostname”,
user@docker1:~$ docker inspect web8 | grep **ResolvConfPath
“ResolvConfPath”: “/var/lib/docker/containers/c803f130b7a2450609672c23762bce3499dec9abcfdc540a43a7eb560adaf62a/resolv.conf”,
user@docker1:~$ 

正如预期的那样,这些是我们在容器内部运行findmnt命令时看到的相同挂载路径。这些代表了每个文件的确切挂载路径到容器的/etc/目录中的每个相应文件。

覆盖默认的名称解析设置

Docker 用于为容器提供名称解析的方法在大多数情况下都运行良好。然而,可能会有一些情况,您希望 Docker 为容器提供与 Docker 主机配置的 DNS 服务器不同的 DNS 服务器。在这些情况下,Docker 为您提供了一些选项。您可以告诉 Docker 服务为所有服务生成的容器提供不同的 DNS 服务器。您还可以通过在docker run子命令中提供 DNS 服务器作为选项,手动覆盖此设置。在本教程中,我们将向您展示更改默认名称解析行为的选项以及如何验证设置是否有效。

准备工作

在本教程中,我们将演示单个 Docker 主机上的配置。假设这个主机已经安装了 Docker,并且 Docker 处于默认配置。我们将在主机上更改名称解析设置,因此您需要 root 级别的访问权限。

操作步骤

正如我们在本章的第一个教程中看到的,默认情况下,Docker 为容器提供 Docker 主机本身使用的 DNS 服务器。这是通过复制主机的resolv.conf文件并提供给每个生成的容器。除了名称服务器设置,该文件还包括 DNS 搜索域的定义。这两个选项都可以在服务级别进行配置,以覆盖任何生成的容器,也可以在个体级别进行配置。

为了进行比较,让我们首先检查 Docker 主机的 DNS 配置:

root@docker1:~# more /etc/resolv.conf
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 10.20.30.13
search lab.lab
root@docker1:~#

通过这个配置,我们期望在这个主机上生成的任何容器都会收到相同的名称服务器和 DNS 搜索域。让我们生成一个名为web8的容器,以验证这是否按预期工作:

root@docker1:~# docker run -d -P --name=**web8** \
jonlangemak/web_server_8_dns
156bc29d28a98e2fbccffc1352ec390bdc8b9b40b84e4c5f58cbebed6fb63474
root@docker1:~#
root@docker1:~# docker exec -t web8 more /etc/resolv.conf
::::::::::::::
/etc/resolv.conf
::::::::::::::
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 10.20.30.13
search lab.lab

正如预期的那样,容器接收相同的配置。现在让我们检查容器,看看是否有任何与 DNS 相关的选项被定义:

user@docker1:~$ docker inspect web8 | grep Dns
            “Dns”: [],
            “DnsOptions”: [],
            “DnsSearch”: [],
user@docker1:~$

因为我们使用默认配置,所以在容器内部针对 DNS 服务器或搜索域没有必要配置任何特定的内容。每次容器启动时,Docker 都会将主机的resolv.conf文件的设置应用到容器的 DNS 配置文件中。

如果我们希望 Docker 为容器提供不同的 DNS 服务器或 DNS 搜索域,我们可以通过 Docker 选项来实现。在这种情况下,我们感兴趣的两个选项是:

  • --dns=<DNS 服务器>:指定 Docker 应该为容器提供的 DNS 服务器地址

  • --dns-search=<DNS 搜索域>:指定 Docker 应该为容器提供的 DNS 搜索域

让我们配置 Docker 以为容器提供一个公共 DNS 服务器(4.2.2.2)和一个搜索域lab.external。我们可以通过将以下选项传递给 Docker systemd drop-in 文件来实现:

ExecStart=/usr/bin/dockerd --dns=4.2.2.2 --dns-search=lab.external 

一旦配置了选项,重新加载 systemd 配置,重新启动服务以加载新选项,并重新启动我们的容器web8

user@docker1:~$ sudo systemctl daemon-reload
user@docker1:~$ sudo systemctl restart docker
user@docker1:~$ docker start web8
web8
user@docker1:~$ docker exec -t web8 more /etc/resolv.conf
search lab.external
nameserver 4.2.2.2
user@docker1:~$

您会注意到,尽管此容器最初具有主机的 DNS 服务器(10.20.30.13)和搜索域(lab.lab),但现在它具有我们刚刚指定的服务级 DNS 选项。如果您回想一下之前,我们看到,当我们检查这个容器时,它没有定义特定的 DNS 服务器或搜索域。由于没有指定,Docker 现在使用优先级较高的 Docker 选项的设置。尽管这提供了一定程度的灵活性,但它还不够灵活。在这一点上,此服务器上生成的任何和所有容器都将提供相同的 DNS 服务器和搜索域。为了真正灵活,我们应该能够让 Docker 在每个容器级别上改变名称解析配置。幸运的是,这些选项也可以直接在容器运行时提供。

如何做…

前面的图表定义了 Docker 在启动容器时决定应用哪些名称解析设置时使用的优先级。正如我们在前几章中看到的那样,容器运行时定义的设置始终优先。如果那里没有定义设置,Docker 然后会查看它们是否在服务级别上配置。如果那里没有设置,它将退回到依赖 Docker 主机的 DNS 设置的默认方法。

例如,我们可以启动一个名为web2的容器并提供不同的选项:

root@docker1:~# docker run -d **--dns=8.8.8.8 --dns-search=lab.dmz \
-P --name=web8-2 jonlangemak/web_server_8_dns
1e46d66a47b89d541fa6b022a84d702974414925f5e2dd56eeb840c2aed4880f
root@docker1:~#

如果我们检查容器,我们会看到dnsdns-search字段现在作为容器配置的一部分被定义:

root@docker1:~# docker inspect web8-2
...<Additional output removed for brevity>...
 “Dns”: [
 “8.8.8.8”
            ],
            “DnsOptions”: [],
 “DnsSearch”: [
 “lab.dmz”
            ],
...<Additional output removed for brevity>...
root@docker1:~# 

这确保了如果容器重新启动,它仍将具有最初在第一次运行容器时提供的相同的 DNS 设置。让我们对 Docker 服务进行一些微小的更改,以验证优先级是否按预期工作。让我们将我们的 Docker 选项更改为如下所示:

ExecStart=/usr/bin/dockerd --dns-search=lab.external

现在重新启动服务并运行以下容器:

user@docker1:~$ sudo systemctl daemon-reload
user@docker1:~$ sudo systemctl restart docker
root@docker1:~#
root@docker1:~# docker run -d -P --name=web8-3 \
jonlangemak/web_server_8_dns
5e380f8da17a410eaf41b772fde4e955d113d10e2794512cd20aa5e551d9b24c
root@docker1:~#

因为我们在容器运行时没有提供任何与 DNS 相关的选项,所以我们需要检查的下一个地方将是服务级选项。我们的 Docker 服务级选项包括一个 DNS 搜索域lab.external。我们期望容器会收到该搜索域。然而,由于我们没有定义 DNS 服务器,我们需要回退到 Docker 主机本身上配置的 DNS 服务器。

现在检查它的resolv.conf文件,确保一切按预期工作:

user@docker1:~$ docker exec -t web8-3 more /etc/resolv.conf
search lab.external
nameserver 10.20.30.13
user@docker1:~$

为名称和服务解析配置链接

容器链接提供了一种容器之间在同一主机上轻松通信的方式。正如我们在之前的例子中看到的,大多数容器之间的通信是通过 IP 地址进行的。容器链接通过允许链接的容器通过名称进行通信来改进了这一点。除了提供基本的名称解析外,它还提供了一种查看链接容器提供的服务的方法。在本教程中,我们将回顾如何创建容器链接,并讨论它们的一些局限性。

准备工作

在本教程中,我们将演示在单个 Docker 主机上的配置。假设该主机已安装 Docker,并且 Docker 处于默认配置。我们将在主机上更改名称解析设置,因此您需要 root 级别的访问权限。

如何做…

短语“容器链接”可能暗示着涉及某种网络配置或修改。实际上,容器链接与容器网络几乎没有关系。在默认模式下,容器链接提供了一种容器解析另一个容器名称的方法。例如,让我们在我们的实验主机docker1上启动两个容器:

root@docker1:~# docker run -d -P --name=**web1** jonlangemak/web_server_1
88f9c862966874247c8e2ba90c18ac673828b5faac93ff08090adc070f6d2922 root@docker1:~# docker run -d -P --name=**web2 --link=web1 \
jonlangemak/web_server_2
00066ea46367c07fc73f73bdcdff043bd4c2ac1d898f4354020cbcfefd408449
root@docker1:~#

请注意,当我启动第二个容器时,我使用了一个名为 --link 的新标志,并引用了容器 web1。我们现在会说 web2 现在链接到 web1。但是,它们实际上并没有以任何方式链接。更好的描述可能是说 web2 现在知道了 web1。让我们连接到容器 web2,以便向您展示我的意思:

root@docker1:~# docker exec -it web2 /bin/bash
root@00066ea46367:/# ping **web1** -c 2
PING **web1 (172.17.0.2)**: 48 data bytes
56 bytes from 172.17.0.2: icmp_seq=0 ttl=64 time=0.163 ms
56 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.092 ms
--- web1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.092/0.128/0.163/0.036 ms
root@00066ea46367:/#

看起来 web2 容器现在能够通过名称解析容器 web1。这是因为链接过程将记录插入到 web2 容器的 hosts 文件中:

root@00066ea46367:/# more /etc/hosts
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.2      web1 88f9c8629668
172.17.0.3      00066ea46367
root@00066ea46367:/#

有了这个配置,web2 容器可以通过我们在运行时给容器的名称 (web1) 或 Docker 为容器生成的唯一 hostname 来到达 web1 容器 (88f9c8629668)。

除了更新 hosts 文件之外,web2 还生成了一些新的环境变量:

root@00066ea46367:/# printenv
WEB1_ENV_APACHE_LOG_DIR=/var/log/apache2
HOSTNAME=00066ea46367
APACHE_RUN_USER=www-data
WEB1_PORT_80_TCP=tcp://172.17.0.2:80
WEB1_PORT_80_TCP_PORT=80
LS_COLORS=
WEB1_PORT=tcp://172.17.0.2:80
WEB1_ENV_APACHE_RUN_GROUP=www-data
APACHE_LOG_DIR=/var/log/apache2
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
WEB1_PORT_80_TCP_PROTO=tcp
APACHE_RUN_GROUP=www-data
SHLVL=1
HOME=/root
WEB1_PORT_80_TCP_ADDR=172.17.0.2
WEB1_ENV_APACHE_RUN_USER=www-data
WEB1_NAME=/web2/web1
_=/usr/bin/printenv
root@00066ea46367:/# 

您会注意到许多新的环境变量。Docker 将复制来自链接容器的任何环境变量,这些环境变量是作为容器的一部分定义的。这包括:

  • Docker 镜像中描述的环境变量。更具体地说,来自镜像 Dockerfile 的任何 ENV 变量

  • 通过 --env-e 标志在运行时传递给容器的环境变量

在这种情况下,这三个变量在镜像的 Dockerfile 中被定义为 ENV 变量:

APACHE_RUN_USER=www-data
APACHE_RUN_GROUP=www-data
APACHE_LOG_DIR=/var/log/apache2

因为两个容器镜像都定义了相同的 ENV 变量,我们将看到本地变量以及以 WEB1_ENV_ 为前缀的来自容器 web1 的相同环境变量:

WEB1_ENV_APACHE_RUN_USER=www-data
WEB1_ENV_APACHE_RUN_GROUP=www-data
WEB1_ENV_APACHE_LOG_DIR=/var/log/apache2

此外,Docker 还创建了描述 web1 容器以及其任何暴露端口的其他六个环境变量:

WEB1_PORT=tcp://172.17.0.2:80
WEB1_PORT_80_TCP=tcp://172.17.0.2:80
WEB1_PORT_80_TCP_ADDR=172.17.0.2
WEB1_PORT_80_TCP_PORT=80
WEB1_PORT_80_TCP_PROTO=tcp
WEB1_NAME=/web2/web1

链接还允许您指定别名。例如,让我们使用稍微不同的链接语法停止、删除和重新生成容器 web2

user@docker1:~$ docker stop web2
web2
user@docker1:~$ docker rm web2
web2
user@docker1:~$ docker run -d -P --name=web2 **--link=web1:webserver \
jonlangemak/web_server_2
e102fe52f8a08a02b01329605dcada3005208d9d63acea257b8d99b3ef78e71b
user@docker1:~$

请注意,在链接定义之后,我们插入了 a :webserver. 冒号后面的名称表示链接的别名。在这种情况下,我指定了容器 web1 的别名为 webserver

如果我们检查 web2 容器,我们会看到别名现在也列在 hosts 文件中:

root@c258c7a0884d:/# more /etc/hosts
…<Additional output removed for brevity>… 
172.17.0.2      **webserver** 88f9c8629668 web1
172.17.0.3      c258c7a0884d
root@c258c7a0884d:/# 

别名还会影响链接期间创建的环境变量。它们不会使用容器名称,而是使用别名:

user@docker1:~$ docker exec web2 printenv
…<Additional output removed for brevity>… 
WEBSERVER**_PORT_80_TCP_ADDR=172.17.0.2
WEBSERVER**_PORT_80_TCP_PORT=80
WEBSERVER**_PORT_80_TCP_PROTO=tcp
…<Additional output removed for brevity>… 
user@docker1:~$

此时,您可能想知道这有多动态。毕竟,Docker 通过更新每个容器中的静态文件来提供这个功能。如果容器的 IP 地址发生变化会发生什么?例如,让我们停止容器web1,然后使用相同的镜像启动一个名为web3的新容器:

user@docker1:~$ docker stop web1
web1
user@docker1:~$ docker run -d -P --name=web3 jonlangemak/web_server_1
69fa80be8b113a079e19ca05c8be9e18eec97b7bbb871b700da4482770482715
user@docker1:~$

如果您还记得之前,容器web1的 IP 地址是172.17.0.2。由于我停止了容器,Docker 将释放该 IP 地址的保留,使其可以重新分配给我们启动的下一个容器。让我们检查分配给容器web3的 IP 地址:

user@docker1:~$ docker exec **web3** ip addr show dev eth0
79: eth0@if80: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet **172.17.0.2/16** scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:2/64 scope link
       valid_lft forever preferred_lft forever
user@docker1:~$

正如预期的那样,web3获取了先前属于web1容器的现在开放的 IP 地址172.17.0.2。我们还可以验证容器web2仍然认为这个 IP 地址属于web1容器:

user@docker1:~$ docker exec –t web2 more /etc/hosts | grep 172.17.0.2
172.17.0.2      webserver 88f9c8629668 web1
user@docker1:~$

如果我们再次启动容器web1,我们应该看到它将获得一个新的分配给它的 IP 地址:

user@docker1:~$ docker start **web1
web1
user@docker1:~$ docker exec **web1** ip addr show dev **eth0
81: eth0@if82: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff
    inet **172.17.0.4/16** scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:4/64 scope link
       valid_lft forever preferred_lft forever
user@docker1:~$

如果我们再次检查容器web2,我们应该看到 Docker 已经更新它以引用web1容器的新 IP 地址:

user@docker1:~$ docker exec **web2** more /etc/hosts | grep **web1
172.17.0.4      webserver 88f9c8629668 web1
user@docker1:~$

然而,虽然 Docker 负责更新hosts文件中的新 IP 地址,但它不会负责更新任何环境变量以反映新的 IP 地址:

user@docker1:~$ docker exec web2 printenv
…<Additional output removed for brevity>…
WEBSERVER**_PORT=tcp://**172.17.0.2:80
WEBSERVER**_PORT_80_TCP=tcp://**172.17.0.2:80
WEBSERVER**_PORT_80_TCP_ADDR=**172.17.0.2
…<Additional output removed for brevity>…
user@docker1:~$

此外,应该指出,这个链接只是单向的。也就是说,这个链接不会使容器web1意识到web2容器。Web1不会接收主机记录或引用web2容器的环境变量:

user@docker1:~$ docker exec -it **web1 ping web2
ping: unknown host
user@docker1:~$

另一个配置链接的原因是当您将 Docker 容器间连接ICC)模式设置为false时。正如我们之前讨论过的,ICC 阻止同一网桥上的任何容器直接交流。这迫使它们只能通过发布的端口进行交流。链接提供了一个机制来覆盖默认的 ICC 规则。为了演示,让我们停止并删除主机docker1上的所有容器,然后将以下 Docker 选项添加到 systemd drop-in 文件中:

ExecStart=/usr/bin/dockerd --icc=false

现在重新加载 systemd 配置,重新启动服务,并启动以下容器:

docker run -d -P --name=web1 jonlangemak/web_server_1
docker run -d -P --name=web2 jonlangemak/web_server_2

在 ICC 模式下,您会注意到容器无法直接交流:

user@docker1:~$ docker exec **web1** ip addr show dev eth0
87: eth0@if88: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet **172.17.0.2/16** scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:2/64 scope link
       valid_lft forever preferred_lft forever
user@docker1:~$ docker exec -it **web2** curl http://**172.17.0.2
user@docker1:~$

在上面的例子中,web2无法访问web1上的 Web 服务器。现在,让我们删除并重新创建web2容器,这次将其链接到web1

user@docker1:~$ docker stop web2
web2
user@docker1:~$ docker rm web2
web2
user@docker1:~$ docker run -d -P --name=web2 **--link=web1 \
jonlangemak/web_server_2
4c77916bb08dfc586105cee7ae328c30828e25fcec1df55f8adba8545cbb2d30
user@docker1:~$ docker exec -it **web2** curl http://**172.17.0.2
<body>
  <html>
    <h1><span style=”color:#FF0000;font-size:72px;”>**Web Server #1 - Running on port 80**</span>
    </h1>
</body>
  </html>
user@docker1:~$

我们可以看到,链接建立后,通信按预期允许。再次强调,就像链接一样,这种访问是单向允许的。

应该注意的是,在使用用户定义网络时,链接的工作方式不同。在本教程中,我们涵盖了现在被称为传统链接的内容。连接到用户定义网络将在接下来的两个教程中介绍。

利用 Docker DNS

用户定义网络的引入标志着 Docker 网络的重大变化。虽然提供自定义网络的能力是重大新闻,但名称解析也有了重大改进。用户定义网络可以受益于被称为嵌入式 DNS的功能。Docker 引擎本身现在具有为所有容器提供名称解析的能力。这是与传统解决方案相比的显著改进,传统解决方案中名称解析的唯一手段是外部 DNS 或依赖hosts文件的链接。在本教程中,我们将介绍如何使用和配置嵌入式 DNS。

准备工作

在本教程中,我们将演示在单个 Docker 主机上的配置。假设该主机已安装了 Docker,并且 Docker 处于默认配置状态。我们将在主机上更改名称解析设置,因此您需要 root 级别的访问权限。

操作步骤…

如前所述,嵌入式 DNS 系统仅在用户定义的 Docker 网络上运行。也就是说,让我们提供一个用户定义的网络,然后在其上启动一个简单的容器:

user@docker1:~$ docker network create -d bridge **mybridge1
0d75f46594eb2df57304cf3a2b55890fbf4b47058c8e43a0a99f64e4ede98f5f
user@docker1:~$ docker run -d -P --name=web1 **--net=mybridge1 \
jonlangemak/web_server_1
3a65d84a16331a5a84dbed4ec29d9b6042dde5649c37bc160bfe0b5662ad7d65
user@docker1:~$

正如我们在之前的教程中看到的,默认情况下,Docker 从 Docker 主机获取名称解析配置,并将其提供给容器。可以通过在服务级别或容器运行时提供不同的 DNS 服务器或搜索域来更改此行为。对于连接到用户定义网络的容器,提供给容器的 DNS 设置略有不同。例如,让我们看看刚刚连接到用户定义桥接mybridge1的容器的resolv.conf文件:

user@docker1:~$ docker exec -t web1 more /etc/resolv.conf
search lab.lab
nameserver 127.0.0.11
options ndots:0
user@docker1:~$ 

注意这个容器的名称服务器现在是127.0.0.11。这个 IP 地址代表 Docker 的嵌入式 DNS 服务器,并将用于任何连接到用户定义网络的容器。任何连接到用户定义网络的容器都应该使用嵌入式 DNS 服务器。

最初未在用户定义的网络上启动的容器将在连接到用户定义的网络时进行更新。例如,让我们启动另一个名为web2的容器,但让它使用默认的docker0桥接:

user@docker1:~$ docker run -dP --name=web2 jonlangemak/web_server_2
d0c414477881f03efac26392ffbdfb6f32914597a0a7ba578474606d5825df3f
user@docker1:~$ docker exec -t web2 more /etc/resolv.conf
::::::::::::::
/etc/resolv.conf
::::::::::::::
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 10.20.30.13
search lab.lab
user@docker1:~$

如果我们现在将web2容器连接到我们自定义的网络,Docker 将更新名称服务器以反映嵌入式 DNS 服务器:

user@docker1:~$ docker network connect mybridge1 web2
user@docker1:~$ docker exec -t web2 more /etc/resolv.conf
search lab.lab
nameserver 127.0.0.11
options ndots:0
user@docker1:~$ 

由于我们的两个容器现在都连接到同一个用户定义的网络,它们现在可以通过名称相互访问:

user@docker1:~$ docker exec -t **web1** ping **web2** -c 2
PING web2 (172.18.0.3): 48 data bytes
56 bytes from 172.18.0.3: icmp_seq=0 ttl=64 time=0.107 ms
56 bytes from 172.18.0.3: icmp_seq=1 ttl=64 time=0.087 ms
--- web2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.087/0.097/0.107/0.000 ms

user@docker1:~$ docker exec -t **web2** ping **web1** -c 2
PING web1 (172.18.0.2): 48 data bytes
56 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.060 ms
56 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.119 ms
--- web1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.060/0.089/0.119/0.030 ms
user@docker1:~$

您会注意到名称解析是双向的,并且它在没有任何链接的情况下固有地工作。也就是说,使用用户定义的网络,我们仍然可以定义链接,以便创建本地别名。例如,让我们停止并删除web1web2两个容器,然后重新配置它们如下:

user@docker1:~$ docker run -d -P --name=**web1** --net=mybridge1 \
--link=web2:thesecondserver** jonlangemak/web_server_1
fd21c53def0c2255fc20991fef25766db9e072c2bd503c7adf21a1bd9e0c8a0a
user@docker1:~$ docker run -d -P --name=**web2** --net=mybridge1 \
--link=web1:thefirstserver** jonlangemak/web_server_2
6e8f6ab4dec7110774029abbd69df40c84f67bcb6a38a633e0a9faffb5bf625e
user@docker1:~$

要指出的第一件有趣的事情是,Docker 允许我们链接到尚不存在的容器。当我们运行容器web1时,我们要求 Docker 将其链接到容器web2。那时,web2并不存在。这是链接与嵌入式 DNS 服务器工作方式的一个显着差异。在传统的链接中,Docker 需要在进行链接之前知道目标容器的信息。这是因为它必须手动更新源容器的主机文件和环境变量。第二个有趣的事情是,别名不再列在容器的hosts文件中。如果我们查看每个容器的hosts文件,我们会发现链接不再生成条目:

user@docker1:~$ docker exec -t web1 more /etc/resolv.conf
search lab.lab
nameserver 127.0.0.11
options ndots:0
user@docker1:~$ docker exec -t web1 more /etc/hosts
…<Additional output removed for brevity>… 
172.18.0.2      9cee9ce88cc3
user@docker1:~$

user@docker1:~$ docker exec -t web2 more /etc/hosts
…<Additional output removed for brevity>… 
172.18.0.3      2d4b63452c8a
user@docker1:~$

现在所有的解析都是在嵌入式 DNS 服务器中进行的。这包括跟踪定义的别名及其范围。因此,即使没有主机记录,每个容器也能够通过嵌入式 DNS 服务器解析其他容器的别名:

user@docker1:~$ docker exec -t web1 ping **thesecondserver** -c2
PING thesecondserver (172.18.0.3): 48 data bytes
56 bytes from 172.18.0.3: icmp_seq=0 ttl=64 time=0.067 ms
56 bytes from 172.18.0.3: icmp_seq=1 ttl=64 time=0.067 ms
--- thesecondserver ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.067/0.067/0.067/0.000 ms

user@docker1:~$ docker exec -t web2 ping **thefirstserver** -c 2
PING thefirstserver (172.18.0.2): 48 data bytes
56 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.062 ms
56 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.042 ms
--- thefirstserver ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.042/0.052/0.062/0.000 ms
user@docker1:~$

创建的别名的范围仅限于容器本身。例如,同一用户定义的网络上的第三个容器无法解析链接的一部分创建的别名:

user@docker1:~$ docker run -d -P --name=web3 --net=**mybridge1** \
jonlangemak/web_server_1
d039722a155b5d0a702818ce4292270f30061b928e05740d80bb0c9cb50dd64f
user@docker1:~$ docker exec -it web3 ping **thefirstserver** -c 2
ping: unknown host
user@docker1:~$ docker exec -it web3 ping **thesecondserver** -c 2
ping: unknown host
user@docker1:~$

您会记得,传统的链接还会自动在源容器上创建一组环境变量。这些环境变量引用了目标容器和它可能正在暴露的任何端口。在用户定义的网络中进行链接不会创建这些环境变量:

user@docker1:~$ docker exec web1 printenv
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=4eba77b66d60
APACHE_RUN_USER=www-data
APACHE_RUN_GROUP=www-data
APACHE_LOG_DIR=/var/log/apache2
HOME=/root
user@docker1:~$ 

正如我们在上一个示例中看到的,即使使用传统的链接,也无法保持这些变量的最新状态。也就是说,当处理用户定义的网络时,功能不存在并不是完全令人惊讶。

除了提供本地容器解析外,嵌入式 DNS 服务器还处理任何外部请求。正如我们在前面的示例中看到的,来自 Docker 主机(在我的情况下是lab.lab)的搜索域仍然被传递给容器,并在它们的resolv.conf文件中配置。从主机学习的名称服务器成为嵌入式 DNS 服务器的转发器。这允许嵌入式 DNS 服务器处理任何容器名称解析请求,并将外部请求移交给 Docker 主机使用的名称服务器。这种行为可以在服务级别或在运行时通过传递--dns--dns-search标志来覆盖。例如,我们可以启动web1容器的另外两个实例,并在任何情况下指定特定的 DNS 服务器:

user@docker1:~$ docker run -dP --net=mybridge1 --name=web4 \
--dns=10.20.30.13** jonlangemak/web_server_1
19e157b46373d24ca5bbd3684107a41f22dea53c91e91e2b0d8404e4f2ccfd68
user@docker1:~$ docker run -dP --net=mybridge1 --name=web5 \
--dns=8.8.8.8** jonlangemak/web_server_1
700f8ac4e7a20204100c8f0f48710e0aab8ac0f05b86f057b04b1bbfe8141c26
user@docker1:~$

注意

请注意,即使我们没有明确指定,web4也会接收10.20.30.13作为 DNS 转发器。这是因为这也是 Docker 主机使用的 DNS 服务器,当未指定时,容器会继承自主机。这里为了示例而指定。

现在,如果我们尝试在任何一个容器上解析本地 DNS 记录,我们可以看到,在web1的情况下它可以工作,因为它定义了本地 DNS 服务器,而在web2上的查找失败,因为8.8.8.8不知道lab.lab域:

user@docker1:~$ docker exec -it **web4 ping docker1.lab.lab** -c 2
PING docker1.lab.lab (10.10.10.101): 48 data bytes
56 bytes from 10.10.10.101: icmp_seq=0 ttl=64 time=0.080 ms
56 bytes from 10.10.10.101: icmp_seq=1 ttl=64 time=0.078 ms
--- docker1.lab.lab ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.078/0.079/0.080/0.000 ms

user@docker1:~$ docker exec -it **web5 ping docker1.lab.lab** -c 2
ping: unknown host
user@docker1:~$

创建 Docker DNS 别名

在嵌入式 DNS 之前,将容器别名为不同名称的唯一方法是使用链接。正如我们在之前的示例中看到的,这仍然是用于创建本地化或特定于容器的别名的方法。但是,如果您想要具有更大范围的别名,任何连接到给定网络的容器都可以解析的别名呢?嵌入式 DNS 服务器提供了所谓的网络范围别名,这些别名可以在给定的用户定义网络中解析。在本示例中,我们将向您展示如何在用户定义的网络中创建网络范围的别名。

准备工作

在本示例中,我们将演示在单个 Docker 主机上的配置。假设该主机已安装 Docker,并且 Docker 处于默认配置状态。我们将更改主机上的名称解析设置,因此您需要 root 级别的访问权限。

如何做…

网络别名可以以几种不同的方式定义。它们可以在容器运行时定义,也可以在将容器连接到网络时定义。再次强调,网络别名是仅在容器实现用户定义网络时提供的功能。您不能在不同时指定用户定义网络的情况下创建网络别名。Docker 将阻止您在容器运行时指定它们:

user@docker1:~$ docker run -dP --name=web1 --net-alias=webserver1 \
jonlangemak/web_server_1
460f587d0fb3e70842b37736639c150b6d333fd0b647345aa7ed9e0505ebfd2d
docker: Error response from daemon: Network-scoped alias is supported only for containers in user defined networks.
user@docker1:~$

如果我们创建一个用户定义的网络并将其指定为容器配置的一部分,该命令将成功执行:

user@docker1:~$ docker network create -d bridge **mybridge1
663f9fe0b4a0dbf7a0be3c4eaf8da262f7e2b3235de252ed5a5b481b68416ca2
user@docker1:~$ docker run -dP --name=web1 --**net=mybridge1 \
--net-alias=webserver1** jonlangemak/web_server_1
05025adf381c7933f427e647a512f60198b29a3cd07a1d6126bc9a6d4de0a279
user@docker1:~$

一旦别名被创建,我们可以将其视为特定容器配置的一部分。例如,如果我们现在检查容器web1,我们将在其网络配置下看到一个定义的别名:

user@docker1:~$ docker inspect **web1
…<Additional output removed for brevity>…
                “mybridge1”: {
                    “IPAMConfig”: null,
                    “Links”: null,
                    “Aliases”: [
                        “**webserver1**”,
                        “6916ac68c459”
                    ],
                    “NetworkID”: “a75b46cc785b88ddfbc83ad7b6ab7ced88bbafef3f64e3e4314904fb95aa9e5c”,
                    “EndpointID”: “620bc4bf9962b7c6a1e59a3dad8d3ebf25831ea00fea4874a9a5fcc750db5534”,
                    “Gateway”: “172.18.0.1”,
                    “IPAddress”: “172.18.0.2”,
…<Additional output removed for brevity>…
user@docker1:~$

现在,让我们启动另一个名为web2的容器,并看看我们是否可以解析别名:

user@docker1:~$ docker run -dP --name=web2 **--net=mybridge1 \
jonlangemak/web_server_2
9b6d23ce868bf62999030a8c1eb29c3ca7b3836e8e3cbb7247d4d8e12955f117
user@docker1:~$ docker exec -it **web2** ping **webserver1** -c 2
PING webserver1 (172.18.0.2): 48 data bytes
56 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.104 ms
56 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.091 ms
--- webserver1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.091/0.098/0.104/0.000 ms
user@docker1:~$

这里有几件有趣的事情要指出。首先,定义别名的方法与链接方法有很大不同,不仅仅是范围。通过链接,源容器指定了它希望将目标容器别名为的内容。在网络别名的情况下,源容器设置了自己的别名。

其次,这只能工作是因为容器web2在与web1相同的用户定义网络上。因为别名的范围是整个用户定义的网络,这意味着同一个容器在不同的用户定义网络上可以使用不同的别名。例如,让我们创建另一个用户定义的网络:

user@docker1:~$ docker network create -d bridge **mybridge2
d867d7ad3a1f639cde8926405acd3a36e99352f0e2a45871db5263caf3b59c44
user@docker1:~$

现在,让我们将容器web1连接到它:

user@docker1:~$ docker network connect --**alias=fooserver** mybridge2 web1

回想一下,我们说过您可以在network connect子命令的一部分中定义网络范围的别名:

user@docker1:~$ docker inspect **web1
…<Additional output removed for brevity>…
                “**mybridge1**”: {
                    “IPAMConfig”: null,
                    “Links”: null,
                    “**Aliases**”: [
                        “**webserver1**”,
                        “6916ac68c459”
                    ],
                    “NetworkID”: “a75b46cc785b88ddfbc83ad7b6ab7ced88bbafef3f64e3e4314904fb95aa9e5c”,
                    “EndpointID”: “620bc4bf9962b7c6a1e59a3dad8d3ebf25831ea00fea4874a9a5fcc750db5534”,
                    “Gateway”: “172.18.0.1”,
                    “IPAddress”: “172.18.0.2”,
                    “IPPrefixLen”: 16,
                    “IPv6Gateway”: “”,
                    “GlobalIPv6Address”: “”,
                    “GlobalIPv6PrefixLen”: 0,
                    “MacAddress”: “02:42:ac:12:00:02”
                },
                “**mybridge2**”: {
                    “IPAMConfig”: {},
                    “Links”: null,
                    “**Aliases**”: [
                        “**fooserver**”,
                        “6916ac68c459”
                    ],
                    “NetworkID”: “daf24590cc8f9c9bf859eb31dab42554c6c14c1c1e4396b3511524fe89789a58”,
                    “EndpointID”: “a36572ec71077377cebfe750f4e533e0316669352894b93df101dcdabebf9fa7”,
                    “Gateway”: “172.19.0.1”,
                    “IPAddress”: “172.19.0.2”,
user@docker1:~$

请注意,容器web1现在有两个别名,一个在每个网络上。因为容器web2只连接到一个网络,所以它仍然只能解析与mybridge1网络关联的别名:

user@docker1:~$ docker exec -it **web2 ping webserver1 -c 2
PING webserver1 (172.18.0.2): 48 data bytes
56 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.079 ms
56 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.123 ms
--- webserver1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.079/0.101/0.123/0.000 ms
user@docker1:~$ docker exec -it **web2 ping fooserver -c 2
ping: unknown host
user@docker1:~$

然而,一旦我们将web2连接到mybridge2网络,它现在可以解析两个别名:

user@docker1:~$ docker network connect **mybridge2 web2
user@docker1:~$ docker exec -it **web2 ping webserver1 -c 2
PING webserver1 (172.18.0.2): 48 data bytes
56 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.064 ms
56 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.097 ms
--- webserver1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.064/0.081/0.097/0.000 ms
user@docker1:~$ docker exec -**it web2 ping fooserver -c 2
PING fooserver (172.19.0.2): 48 data bytes
56 bytes from 172.19.0.2: icmp_seq=0 ttl=64 time=0.080 ms
56 bytes from 172.19.0.2: icmp_seq=1 ttl=64 time=0.087 ms
--- fooserver ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.080/0.083/0.087/0.000 ms
user@docker1:~$

有趣的是,Docker 还允许您将相同的别名定义为多个容器。例如,现在让我们启动一个名为web3的第三个容器,并使用与web1webserver1)相同的别名将其连接到mybridge1

user@docker1:~$ docker run -dP **--name=web3 --net=mybridge1 \
--net-alias=webserver1** jonlangemak/web_server_1
cdf22ba64231553dd7e876b5718e155b1312cca68a621049e04265f5326e063c
user@docker1:~$

别名现在已经为容器web1web2定义。但是,尝试从web2解析别名仍然指向web1

user@docker1:~$ docker exec -**it web2 ping webserver1 -c 2
PING webserver1 (172.18.0.2): 48 data bytes
56 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.066 ms
56 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.088 ms
--- webserver1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.066/0.077/0.088/0.000 ms
user@docker1:~$

如果我们断开或停止容器web1,我们应该会看到分辨率现在改变为web3,因为它仍然在网络上活动,并且具有相同的别名:

user@docker1:~$ **docker stop web1
web1
user@docker1:~$ docker exec -it **web2 ping webserver1 -c 2
PING webserver1 (**172.18.0.4**): 48 data bytes
56 bytes from 172.18.0.4: icmp_seq=0 ttl=64 time=0.085 ms
56 bytes from 172.18.0.4: icmp_seq=1 ttl=64 time=0.091 ms
--- webserver1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.085/0.088/0.091/0.000 ms
user@docker1:~$

这个功能可以为您提供一些有趣的选择,特别是在与叠加网络类型配合使用时,可以实现高可用性或故障转移。

值得注意的是,这个功能适用于所有用户定义的网络类型,包括叠加网络类型。我们在这些示例中使用桥接来保持示例简单。

第六章:保护容器网络

在本章中,我们将涵盖以下示例:

  • 启用和禁用 ICC

  • 禁用出站伪装

  • 管理 netfilter 到 Docker 集成

  • 创建自定义 iptables 规则

  • 通过负载均衡器公开服务

介绍

随着您转向基于容器的应用程序,您需要认真考虑的一项内容是网络安全。特别是容器可能导致需要保护的网络端点数量激增。当然,并非所有端点都完全暴露在网络中。然而,默认情况下,那些没有完全暴露的端点会直接相互通信,这可能会引起其他问题。在涉及基于容器的应用程序时,有许多方法可以解决网络安全问题,本章并不旨在解决所有可能的解决方案。相反,本章旨在审查配置选项和相关网络拓扑,这些选项可以根据您自己的网络安全要求以多种不同的方式组合。我们将详细讨论一些我们在早期章节中接触到的功能,如 ICC 模式和出站伪装。此外,我们将介绍一些不同的技术来限制容器的网络暴露。

启用和禁用 ICC

在早期章节中,我们接触到了 ICC 模式的概念,但对其工作机制并不了解。ICC 是 Docker 本地的一种方式,用于隔离连接到同一网络的所有容器。提供的隔离可以防止容器直接相互通信,同时允许它们的暴露端口被发布,并允许出站连接。在这个示例中,我们将审查在默认的docker0桥接上下文以及用户定义的网络中基于 ICC 的配置选项。

准备工作

在这个示例中,我们将使用两个 Docker 主机来演示 ICC 在不同网络配置中的工作方式。假设本实验室中使用的两个 Docker 主机都处于默认配置。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。

操作方法…

ICC 模式可以在原生的docker0桥以及使用桥驱动的任何用户定义的网络上进行配置。在本教程中,我们将介绍如何在docker0桥上配置 ICC 模式。正如我们在前几章中看到的,与docker0桥相关的设置需要在服务级别进行。这是因为docker0桥是作为服务初始化的一部分创建的。这也意味着,要对其进行更改,我们需要编辑 Docker 服务配置,然后重新启动服务以使更改生效。在进行任何更改之前,让我们有机会审查默认的 ICC 配置。为此,让我们首先查看docker0桥的配置:

user@docker1:~$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "d88fa0a96585792f98023881978abaa8c5d05e4e2bbd7b4b44a6e7b0ed7d346b",
        "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,
        "Containers": {},
        "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": {}
    }
]
user@docker1:~$

注意

重要的是要记住,docker network子命令用于管理所有 Docker 网络。一个常见的误解是它只能用于管理用户定义的网络。

正如我们所看到的,docker0桥配置为 ICC 模式(true)。这意味着 Docker 不会干预或阻止连接到这个桥的容器直接相互通信。为了证明这一点,让我们启动两个容器:

user@docker1:~$ docker run -d --name=web1 jonlangemak/web_server_1
417dd2587dfe3e664b67a46a87f90714546bec9c4e35861476d5e4fa77e77e61
user@docker1:~$ docker run -d --name=web2 jonlangemak/web_server_2
a54db26074c00e6771d0676bb8093b1a22eb95a435049916becd425ea9587014
user@docker1:~$

请注意,我们没有指定-P标志,这告诉 Docker 不要发布任何容器暴露的端口。现在,让我们获取每个容器的 IP 地址,以便验证连接:

user@docker1:~$ docker exec **web1** ip addr show dev eth0 | grep inet
    inet **172.17.0.2/16** scope global eth0
    inet6 fe80::42:acff:fe11:2/64 scope link
 user@docker1:~$ docker exec **web2** ip addr show dev eth0 | grep inet
    inet **172.17.0.3/16** scope global eth0
    inet6 fe80::42:acff:fe11:3/64 scope link
user@docker1:~$

现在我们知道了 IP 地址,我们可以验证每个容器是否可以访问另一个容器在其上监听的任何服务:

user@docker1:~$ docker exec -it **web1** ping **172.17.0.3** -c 2
PING 172.17.0.3 (172.17.0.3): 48 data bytes
56 bytes from 172.17.0.3: icmp_seq=0 ttl=64 time=0.198 ms
56 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.082 ms
--- 172.17.0.3 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.082/0.140/0.198/0.058 ms
user@docker1:~$
user@docker1:~$ docker exec **web2** curl -s **http://172.17.0.2
<body>
  <html>
    <h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
    </h1>
</body>
  </html>
user@docker1:~$

根据这些测试,我们可以假设容器被允许在任何监听的协议上相互通信。这是启用 ICC 模式时的预期行为。现在,让我们更改服务级别设置并重新检查我们的配置。为此,在 Docker 服务的 systemd drop in 文件中设置以下配置:

ExecStart=/usr/bin/dockerd --icc=false

现在重新加载 systemd 配置,重新启动 Docker 服务,并检查 ICC 设置:

user@docker1:~$ sudo systemctl daemon-reload
user@docker1:~$ sudo systemctl restart docker
user@docker1:~$ docker network inspect bridge
…<Additional output removed for brevity>…
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "false",
            "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" 
…<Additional output removed for brevity>… 
user@docker1:~$

现在我们已经确认了 ICC 被禁用,让我们再次启动我们的两个容器并运行相同的连接性测试:

user@docker1:~$ docker start web1
web1
user@docker1:~$ docker start web2
web2
user@docker1:~$
user@docker1:~$ docker exec -it **web1** ping **172.17.0.3** -c 2
PING 172.17.0.3 (172.17.0.3): 48 data bytes
user@docker1:~$ docker exec -it **web2** curl -m 1 http://172.17.0.2
curl: (28) connect() timed out!
user@docker1:~$

如您所见,我们的两个容器之间没有连接。但是,Docker 主机本身仍然能够访问服务:

user@docker1:~$ curl **http://172.17.0.2
<body>
  <html>
    <h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
    </h1>
</body>
  </html>
user@docker1:~$ **curl http://172.17.0.3
<body>
  <html>
    <h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span>
    </h1>
</body>
  </html>
user@docker1:~$

我们可以检查用于实现 ICC 的 netfilter 规则,方法是查看过滤表的iptables规则FORWARD链:

user@docker1:~$ sudo iptables -S FORWARD
-P FORWARD ACCEPT
-A FORWARD -j DOCKER-ISOLATION
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j DROP
user@docker1:~$ 

前面加粗的规则是防止在docker0桥上进行容器之间通信的。如果在禁用 ICC 之前检查了这个iptables链,我们会看到这个规则设置为ACCEPT,如下所示:

user@docker1:~$ sudo iptables -S FORWARD
-P FORWARD ACCEPT
-A FORWARD -j DOCKER-ISOLATION
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
user@docker1:~$

正如我们之前所看到的,链接容器允许您绕过这一规则,允许源容器访问目标容器。如果我们移除这两个容器,我们可以通过以下方式重新启动它们:

user@docker1:~$ **docker run -d --name=web1 jonlangemak/web_server_1
9846614b3bac6a2255e135d19f20162022a40d95bd62a0264ef4aaa89e24592f
user@docker1:~$ **docker run -d --name=web2 --link=web1 jonlangemak/web_server_2
b343b570189a0445215ad5406e9a2746975da39a1f1d47beba4d20f14d687d83
user@docker1:~$

现在,如果我们用iptables检查规则,我们可以看到两个新规则添加到了过滤表中:

user@docker1:~$ sudo iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-ISOLATION
-A FORWARD -j DOCKER-ISOLATION
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j DROP
-A DOCKER -s 172.17.0.3/32 -d 172.17.0.2/32 -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
-A DOCKER -s 172.17.0.2/32 -d 172.17.0.3/32 -i docker0 -o docker0 -p tcp -m tcp --sport 80 -j ACCEPT
-A DOCKER-ISOLATION -j RETURN
user@docker1:~$ 

这两个新规则允许web2访问web1的任何暴露端口。请注意,第一个规则定义了从web2(172.17.0.3)到web1(172.17.0.2)的访问,目的端口为80。第二个规则翻转了 IP,并指定端口80作为源端口,允许流量返回到web2

注意

早些时候,当我们讨论用户定义的网络时,您看到我们可以将 ICC 标志传递给用户定义的桥接。然而,目前不支持使用覆盖驱动程序禁用 ICC 模式。

禁用出站伪装

默认情况下,容器允许通过伪装或隐藏其真实 IP 地址在 Docker 主机的 IP 地址后访问外部网络。这是通过 netfilter masquerade规则实现的,这些规则将容器流量隐藏在下一跳中引用的 Docker 主机接口后面。当我们讨论跨主机的容器之间的连接时,我们在第二章配置和监控 Docker 网络中看到了这方面的详细示例。虽然这种类型的配置在许多方面都是理想的,但在某些情况下,您可能更喜欢禁用出站伪装功能。例如,如果您不希望容器完全具有出站连接性,禁用伪装将阻止容器与外部网络通信。然而,这只是由于缺乏返回路由而阻止了出站流量。更好的选择可能是将容器视为任何其他单独的网络端点,并使用现有的安全设备来定义网络策略。在本教程中,我们将讨论如何禁用 IP 伪装以及如何在容器在外部网络中进行遍历时提供唯一的 IP 地址。

准备工作

在本示例中,我们将使用单个 Docker 主机。假设在此实验中使用的 Docker 主机处于其默认配置中。您还需要访问更改 Docker 服务级别设置。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。我们还将对 Docker 主机连接的网络设备进行更改。

如何做…

您会记得,Docker 中的 IP 伪装是通过 netfilter masquerade规则处理的。在其默认配置中的 Docker 主机上,我们可以通过使用iptables检查规则集来看到这个规则:

user@docker1:~$ sudo iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
user@docker1:~$

此规则指定流量的来源为docker0网桥子网,只有 NAT 流量可以离开主机。MASQUERADE目标告诉主机对 Docker 主机的下一跳接口的流量进行源 NAT。也就是说,如果主机有多个 IP 接口,容器的流量将源 NAT 到下一跳使用的任何接口。这意味着根据 Docker 主机接口和路由表配置,容器流量可能潜在地隐藏在不同的 IP 地址后面。例如,考虑一个具有两个接口的 Docker 主机,如下图所示:

如何做…

在左侧示例中,流量正在采用默认路由,因为4.2.2.2的目的地在主机的路由表中没有更具体的前缀。在这种情况下,主机执行源 NAT,并在流经 Docker 主机到外部网络时将流量的源从172.17.0.2更改为10.10.10.101。但是,如果目的地落入172.17.0.0/16,容器流量将被隐藏在右侧示例中所示的192.168.10.101接口后面。

Docker 的默认行为可以通过操纵--ip-masq Docker 选项来更改。默认情况下,该选项被认为是true,可以通过指定该选项并将其设置为false来覆盖。我们可以通过在 Docker systemd drop in 文件中指定该选项来实现这一点:

ExecStart=/usr/bin/dockerd --ip-masq=false

现在重新加载 systemd 配置,重新启动 Docker 服务,并检查 ICC 设置:

user@docker1:~$ sudo systemctl daemon-reload
user@docker1:~$ sudo systemctl restart docker
user@docker1:~$

user@docker1:~$ sudo iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKERuser@docker1:~$

注意,masquerade规则现在已经消失。在此主机上生成的容器流量将尝试通过其实际源 IP 地址路由到 Docker 主机外部。在 Docker 主机上进行tcpdump将捕获此流量通过原始容器 IP 地址退出主机的eth0接口:

user@docker1:~$ sudo tcpdump –n -i **eth0** dst 4.2.2.2
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
09:06:10.243523 IP **172.17.0.2 > 4.2.2.2**: ICMP echo request, id 3072, seq 0, length 56
09:06:11.244572 IP **172.17.0.2 > 4.2.2.2**: ICMP echo request, id 3072, seq 256, length 56

由于外部网络不知道172.17.0.0/16在哪里,这个请求将永远不会收到响应,有效地阻止了容器与外部世界的通信。

虽然这可能是阻止与外部世界通信的一种有用手段,但并不完全理想。首先,您仍然允许流量出去;响应只是不知道要返回到哪里,因为它试图返回到源。此外,您影响了 Docker 主机上所有网络的所有容器。如果docker0桥接分配了一个可路由的子网,并且外部网络知道该子网的位置,您可以使用现有的安全工具来制定安全策略决策。

例如,假设docker0桥接被分配了一个子网172.10.10.0/24,并且我们禁用了 IP 伪装。我们可以通过更改 Docker 选项来指定新的桥接 IP 地址来实现这一点:

ExecStart=/usr/bin/dockerd --ip-masq=false **--bip=172.10.10.1/24

与以前一样,离开容器并前往外部网络的流量在穿过 Docker 主机时不会改变。假设一个小的网络拓扑,如下图所示:

如何做…

假设从容器到4.2.2.2的流量。在这种情况下,出口流量应该天然工作:

  • 容器生成流量到4.2.2.2,并使用它的默认网关,即docker0桥接 IP 地址

  • Docker 主机进行路由查找,未能找到特定的前缀匹配,并将流量转发到其默认网关,即交换机。

  • 交换机进行路由查找,未能找到特定的前缀匹配,并将流量转发到其默认路由,即防火墙。

  • 防火墙进行路由查找,未能找到特定的前缀匹配,确保流量在策略中被允许,执行隐藏 NAT 到公共 IP 地址,并将流量转发到其默认路由,即互联网。

因此,没有任何额外的配置,出口流量应该能够到达目的地。问题在于返回流量。当来自互联网目的地的响应返回到防火墙时,它将尝试确定如何返回到源。这个路由查找可能会失败,导致防火墙丢弃流量。

注意

在某些情况下,边缘网络设备(在本例中是防火墙)将所有私有 IP 地址路由回内部(在本例中是交换机)。在这种情况下,防火墙可能会将返回流量转发到交换机,但交换机没有特定的返回路由,导致了同样的问题。

为了使其工作,防火墙和交换机需要知道如何将流量返回到特定的容器。为此,我们需要在每个设备上添加特定的路由,将docker0桥接子网指向docker1主机:

操作步骤...

一旦这些路由设置好,在 Docker 主机上启动的容器应该能够连接到外部网络:

user@docker1:~$ docker run -it --name=web1 jonlangemak/web_server_1 /bin/bash
root@132530812e1f:/# **ping 4.2.2.2
PING 4.2.2.2 (4.2.2.2): 48 data bytes
56 bytes from 4.2.2.2: icmp_seq=0 ttl=50 time=33.805 ms
56 bytes from 4.2.2.2: icmp_seq=1 ttl=50 time=40.431 ms

在 Docker 主机上进行tcpdump将显示流量以原始容器 IP 地址离开:

user@docker1:~$ sudo tcpdump –n **-i eth0 dst 4.2.2.2
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
10:54:42.197828 IP **172.10.10.2 > 4.2.2.2**: ICMP echo request, id 3328, seq 0, length 56
10:54:43.198882 IP **172.10.10.2 > 4.2.2.2**: ICMP echo request, id 3328, seq 256, length 56

这种类型的配置提供了使用现有安全设备来决定容器是否可以访问外部网络资源的能力。但是,这也取决于安全设备与您的 Docker 主机的距离。例如,在这种配置中,Docker 主机上的容器可以访问连接到交换机的任何其他网络端点。执行点(在本例中是防火墙)只允许您限制容器与互联网的连接。此外,为每个 Docker 主机分配可路由的 IP 空间可能会引入 IP 分配约束,特别是在大规模情况下。

管理 netfilter 到 Docker 的集成

默认情况下,Docker 会为您执行大部分 netfilter 配置。它会处理诸如发布端口和出站伪装之类的事情,并允许您阻止或允许 ICC。但是,这都是可选的,您可以告诉 Docker 不要修改或添加任何现有的iptables规则。如果这样做,您将需要生成自己的规则来提供类似的功能。如果您已经广泛使用iptables规则,并且不希望 Docker 自动更改您的配置,这可能会对您有吸引力。在本教程中,我们将讨论如何禁用 Docker 自动生成iptables规则,并向您展示如何手动创建类似的规则。

准备工作

在本示例中,我们将使用单个 Docker 主机。假设在本实验中使用的 Docker 主机处于其默认配置中。您还需要访问更改 Docker 服务级别的设置。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。

如何做…

正如我们已经看到的,当涉及到网络配置时,Docker 会为您处理很多繁重的工作。它还允许您在需要时自行配置这些内容。在我们自己尝试配置之前,让我们确认一下 Docker 实际上在我们的iptables规则方面为我们配置了什么。让我们运行以下容器:

user@docker1:~$ docker run -dP --name=web1 jonlangemak/web_server_1
f5b7b389890398588c55754a09aa401087604a8aa98dbf55d84915c6125d5e62
user@docker1:~$ docker run -dP --name=web2 jonlangemak/web_server_2
e1c866892e7f3f25dee8e6ba89ec526fa3caf6200cdfc705ce47917f12095470
user@docker1:~$

运行这些容器将产生以下拓扑结构:

如何做…

注意

稍后给出的示例将不直接使用主机的eth1接口。它只是用来说明 Docker 生成的规则是以涵盖 Docker 主机上的所有物理接口的方式编写的。

正如我们之前提到的,Docker 使用iptables来处理以下项目:

  • 出站容器连接(伪装)

  • 入站端口发布

  • 容器之间的连接

由于我们使用的是默认配置,并且我们已经在两个容器上发布了端口,我们应该能够在iptables中看到这三个项目的配置。让我们首先查看 NAT 表:

注意

在大多数情况下,我更喜欢打印规则并解释它们,而不是将它们列在格式化的列中。每种方法都有权衡,但如果您喜欢列表模式,您可以用-vL替换-S

user@docker1:~$ sudo iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A POSTROUTING -s 172.17.0.3/32 -d 172.17.0.3/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 32768 -j DNAT --to-destination 172.17.0.2:80
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 32769 -j DNAT --to-destination 172.17.0.3:80
user@docker1:~$

让我们回顾一下前面输出中每个加粗行的重要性。第一个加粗行处理了出站隐藏 NAT 或MASQUERADE

-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

该规则正在寻找符合两个特征的流量:

  • 源 IP 地址必须匹配docker0桥的 IP 地址空间

  • 该流量不是通过docker0桥出口的。也就是说,它是通过其他接口如eth0eth1离开的

结尾处的跳转语句指定了MASQUERADE的目标,它将根据路由表将容器流量源 NAT 到主机的 IP 接口之一。

接下来的两行加粗的内容提供了类似的功能,并为每个容器提供了所需的 NAT。让我们来看其中一个:

-A DOCKER ! -i docker0 -p tcp -m tcp --dport 32768 -j DNAT --to-destination 172.17.0.2:80

该规则正在寻找符合三个特征的流量:

  • 流量不是通过docker0桥接进入的

  • 流量是 TCP

  • 流量的目的端口是32768

最后的跳转语句指定了DNAT的目标和容器的真实服务端口(80)的目的地。请注意,这两条规则在 Docker 主机的物理接口方面是通用的。正如我们之前看到的,主机上的任何接口都可以进行端口发布和出站伪装,除非我们“明确限制范围。

我们要审查的下一个表是过滤表:

user@docker1:~$ sudo iptables -t filter -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-ISOLATION
-A FORWARD -j DOCKER-ISOLATION
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
-A DOCKER -d 172.17.0.3/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
-A DOCKER-ISOLATION -j RETURN
user@docker1:~$

同样,您会注意到默认链的链策略设置为ACCEPT。在过滤表的情况下,这对功能有更严重的影响。这意味着除非在规则中明确拒绝,否则一切都被允许。换句话说,如果没有定义规则,一切仍然可以工作。Docker 在默认策略未设置为ACCEPT的情况下插入这些规则。稍后,当我们手动创建规则时,我们将把默认策略设置为DROP,以便您可以看到规则的影响。前面的规则需要更多的解释,特别是如果您不熟悉iptables规则的工作原理。让我们逐一审查加粗的线。

第一行加粗的线负责允许来自外部网络的流量返回到容器中。在这种情况下,规则是特定于容器本身生成流量并期望来自外部网络的响应的实例:

-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

该规则正在寻找符合两个特征的流量:

  • 流量是通过docker0桥接离开的

  • 流量具有RELATEDESTABLISHED的连接状态。这将包括作为现有流或与之相关的会话

最后的跳转语句引用了ACCEPT的目标,这将允许流量通过。

第二行加粗的线允许容器与外部网络的连接:

-A FORWARD -i docker0 ! -o docker0 -j ACCEPT

该规则正在寻找符合两个特征的流量:

  • 流量是通过docker0桥接进入的

  • 流量不是通过docker0桥接离开的

这是一种非常通用的方式来识别来自容器并且通过docker0桥接以外的任何其他接口离开的流量。最后的跳转语句引用了ACCEPT的目标,这将允许流量通过。与第一条规则结合起来,将允许从容器生成的流向外部网络的流量工作。

加粗的第三行允许容器间的连接:

-A FORWARD -i docker0 -o docker0 -j ACCEPT

该规则正在寻找符合两个特征的流量:

  • 流量是通过docker0桥进入的

  • 流量通过docker0桥出口

这是另一种通用的方法来识别源自docker0桥上容器的流量,以及目标是docker0桥上的流量。结尾处的跳转语句引用了一个ACCEPT目标,这将允许流量通过。这与我们在早期章节中看到的在禁用 ICC 模式时转换为DROP目标的规则相同。

最后两行加粗的允许发布的端口到达容器。让我们检查其中一行:

-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT

该规则正在寻找符合五个特征的流量:

  • 流量是发送到已发布端口的容器

  • 流量不是通过docker0桥进入的

  • 流量通过docker0桥出口

  • 协议是 TCP

  • 端口号是80

这个规则特别允许发布的端口工作,通过允许访问容器的服务端口(80)。结尾处的跳转语句引用了一个ACCEPT目标,这将允许流量通过。

手动创建所需的 iptables 规则

现在我们已经看到 Docker 如何自动处理规则生成,让我们通过一个示例来了解如何自己建立这种连接。为此,我们首先需要指示 Docker 不创建任何iptables规则。为此,在 Docker systemd drop in 文件中将--iptables Docker 选项设置为false

ExecStart=/usr/bin/dockerd --iptables=false

我们需要重新加载 systemd drop in 文件并重新启动 Docker 服务,以便 Docker 重新读取服务参数。为了确保从空白状态开始,如果可能的话,重新启动服务器或手动清除所有iptables规则(如果您不熟悉管理iptables规则,最好的方法就是重新启动服务器以清除它们)。在接下来的示例中,我们假设我们正在使用空规则集。一旦 Docker 重新启动,您可以重新启动两个容器,并确保系统上没有iptables规则存在:

user@docker1:~$ docker start web1
web1
user@docker1:~$ docker start web2
web2
user@docker1:~$ sudo iptables -S
-P INPUT **ACCEPT
-P FORWARD **ACCEPT
-P OUTPUT **ACCEPT
user@docker1:~$

如您所见,当前没有定义iptables规则。我们还可以看到过滤表中默认链策略设置为ACCEPT。现在让我们将过滤表中的默认策略更改为每个链的DROP。除此之外,让我们还包括一条规则,允许 SSH 进出主机,以免破坏我们的连接:

user@docker1:~$ sudo iptables -A INPUT -i eth0 -p tcp --dport 22 \
-m state --state NEW,ESTABLISHED -j ACCEPT
user@docker1:~$ sudo iptables -A OUTPUT -o eth0 -p tcp --sport 22 \
-m state --state ESTABLISHED -j ACCEPT
user@docker1:~$ sudo iptables -P INPUT DROP
user@docker1:~$ sudo iptables -P FORWARD DROP
user@docker1:~$ sudo iptables -P OUTPUT DROP

现在让我们再次检查过滤表,以确保规则已被接受:

user@docker1:~$ sudo iptables -S
-P INPUT **DROP
-P FORWARD **DROP
-P OUTPUT **DROP
-A INPUT -i eth0 -p tcp -m tcp --dport 22 -m state --state NEW,ESTABLISHED -j ACCEPT
-A OUTPUT -o eth0 -p tcp -m tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
user@docker1:~$

此时,容器web1web2将不再能够相互到达:

user@docker1:~$ docker exec -it web1 ping 172.17.0.3 -c 2
PING 172.17.0.3 (172.17.0.3): 48 data bytes
user@docker1:~$

注意

根据您的操作系统,您可能会注意到此时web1实际上能够 ping 通web2。最有可能的原因是br_netfilter内核模块尚未加载。没有这个模块,桥接的数据包将不会被 netfilter 检查。要解决这个问题,您可以使用sudo modprobe br_netfilter命令手动加载模块。要使模块在每次启动时加载,您还可以将其添加到/etc/modules文件中。当 Docker 管理iptables规则集时,它会负责为您加载模块。

现在,让我们开始构建规则集,以重新创建 Docker 自动为我们构建的连接。我们要做的第一件事是允许容器的入站和出站访问。我们将使用以下两条规则来实现:

user@docker1:~$ sudo iptables -A FORWARD -i docker0 ! \
-o docker0 -j ACCEPT
user@docker1:~$ sudo iptables -A FORWARD -o docker0 \
-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

尽管这两条规则将允许容器从外部网络生成和接收流量,但此时连接仍然无法工作。为了使其工作,我们需要应用masquerade规则,以便容器流量将被隐藏在docker0主机的接口后面。如果我们不这样做,流量将永远不会返回,因为外部网络对容器所在的172.17.0.0/16网络一无所知:

user@docker1:~$ sudo iptables -t nat -A POSTROUTING \
-s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

有了这个设置,容器现在将能够到达外部网络的网络端点:

user@docker1:~$ docker exec -it **web1** ping **4.2.2.2** -c 2
PING 4.2.2.2 (4.2.2.2): 48 data bytes
56 bytes from 4.2.2.2: icmp_seq=0 ttl=50 time=36.261 ms
56 bytes from 4.2.2.2: icmp_seq=1 ttl=50 time=55.271 ms
--- 4.2.2.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 36.261/45.766/55.271/9.505 ms
user@docker1:~$

然而,容器仍然无法直接相互通信:

user@docker1:~$ docker exec -it web1 ping 172.17.0.3 -c 2
PING 172.17.0.3 (172.17.0.3): 48 data bytes
user@docker1:~$ docker exec -it web1 curl -S http://172.17.0.3
user@docker1:~$

我们需要添加最后一条规则:

sudo iptables -A FORWARD -i docker0 -o docker0 -j ACCEPT

由于容器之间的流量既进入又离开docker0桥,这将允许容器之间的互联:

user@docker1:~$ docker exec -it **web1** ping **172.17.0.3** -c 2
PING 172.17.0.3 (172.17.0.3): 48 data bytes
56 bytes from 172.17.0.3: icmp_seq=0 ttl=64 time=0.092 ms
56 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.086 ms
--- 172.17.0.3 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.086/0.089/0.092/0.000 ms
user@docker1:~$
user@docker1:~$ docker exec -it **web1** curl **http://172.17.0.3
<body>
  <html>
    <h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span>
    </h1>
</body>
  </html>
user@docker1:~$

唯一剩下的配置是提供一个发布端口的机制。我们可以首先在 Docker 主机上配置目标 NAT 来实现这一点。即使 Docker 没有配置 NAT 规则,它仍然会代表你跟踪端口分配。在容器运行时,如果你选择发布一个端口,Docker 会为你分配一个端口映射,即使它不处理发布。明智的做法是使用 Docker 分配的端口以防止重叠:

user@docker1:~$ docker port web1
80/tcp -> 0.0.0.0:**32768
user@docker1:~$ docker port web2
80/tcp -> 0.0.0.0:**32769
user@docker1:~$

user@docker1:~$ sudo iptables -t nat -A PREROUTING ! -i docker0 \
-p tcp -m tcp --dport **32768** -j DNAT --to-destination **172.17.0.2:80
user@docker1:~$ sudo iptables -t nat -A PREROUTING ! -i docker0 \
-p tcp -m tcp --dport **32769** -j DNAT --to-destination **172.17.0.3:80
user@docker1:~$

使用 Docker 分配的端口,我们可以为每个容器定义一个入站 NAT 规则,将入站连接转换为 Docker 主机上的外部端口到真实的容器 IP 和服务端口。最后,我们只需要允许入站流量:

user@docker1:~$ sudo iptables -A FORWARD -d **172.17.0.2/32** ! -i docker0 -o docker0 -p tcp -m tcp --dport **80** -j ACCEPT
user@docker1:~$ sudo iptables -A FORWARD -d **172.17.0.3/32** ! -i docker0 -o docker0 -p tcp -m tcp --dport **80** -j ACCEPT

一旦这些规则配置好了,我们现在可以测试来自 Docker 主机外部的已发布端口的连接:

手动创建所需的 iptables 规则

创建自定义 iptables 规则

在前面的配方中,我们介绍了 Docker 如何处理最常见的容器网络需求的iptables规则。然而,可能会有一些情况,您希望扩展默认的iptables配置,以允许更多的访问或限制连接的范围。在这个配方中,我们将演示如何实现自定义的iptables规则的一些示例。我们将重点放在限制连接到运行在您的容器上的服务的源的范围,以及允许 Docker 主机本身连接到这些服务。

注意

后面提供的示例旨在演示您配置iptables规则集的选项。它们在这些示例中的实现方式可能或可能不适合您的环境,并且可以根据您的安全需求以不同的方式和位置部署。

准备就绪

我们将使用与前一个配方相同的 Docker 主机和相同的配置。Docker 服务应该配置为使用--iptables=false服务选项,并且应该定义两个容器——web1web2。如果您不确定如何达到这种状态,请参阅前一个配方。为了定义一个新的iptables策略,我们还需要清除 NAT 和 FILTER 表中的所有现有iptables规则。这样做的最简单方法是重新启动主机。

注意

当默认策略为拒绝时刷新iptables规则将断开任何远程管理会话。如果您没有控制台访问权限,要小心不要意外断开自己!

如果您不想重新启动,可以将默认的过滤策略更改回allow。然后,按照以下步骤刷新过滤和 NAT 表:

sudo iptables -P INPUT ACCEPT
sudo iptables -P FORWARD ACCEPT
sudo iptables -P OUTPUT ACCEPT
sudo iptables -t filter -F
sudo iptables -t nat -F

怎么做...

此时,您应该再次拥有两个运行的容器和一个空的默认iptables策略的 Docker 主机。首先,让我们再次将默认的过滤策略更改为deny,同时确保我们仍然允许通过 SSH 进行管理连接:

user@docker1:~$ sudo iptables -A INPUT -i eth0 -p tcp --dport 22 \
-m state --state NEW,ESTABLISHED -j ACCEPT
user@docker1:~$ sudo iptables -A OUTPUT -o eth0 -p tcp --sport 22 \
-m state --state ESTABLISHED -j ACCEPT
user@docker1:~$ sudo iptables -P INPUT DROP
user@docker1:~$ sudo iptables -P FORWARD DROP
user@docker1:~$ sudo iptables -P OUTPUT DROP

因为我们将专注于过滤表周围的策略,让我们将 NAT 策略放在上一篇配方中未更改的状态下。这些 NAT 覆盖了每个容器中服务的出站伪装和入站伪装:

user@docker1:~$ sudo iptables -t nat -A POSTROUTING -s \
172.17.0.0/16 ! -o docker0 -j MASQUERADE
user@docker1:~$ sudo iptables -t nat -A PREROUTING ! -i docker0 \
-p tcp -m tcp --dport 32768 -j DNAT --to-destination 172.17.0.2:80
user@docker1:~$ sudo iptables -t nat -A PREROUTING ! -i docker0 \
-p tcp -m tcp --dport 32769 -j DNAT --to-destination 172.17.0.3:80

您可能有兴趣配置的项目之一是限制容器在外部网络上可以访问的范围。您会注意到,在以前的示例中,容器被允许与外部任何东西通信。这是因为过滤规则相当通用:

sudo iptables -A FORWARD -i docker0 ! -o docker0 -j ACCEPT

此规则允许容器与除docker0之外的任何接口上的任何东西通信。与其允许这样做,我们可以指定我们想要允许出站的端口。因此,例如,如果我们发布端口80,然后我们可以定义一个反向或出站规则,只允许特定的返回流量。让我们首先重新创建我们在上一个示例中使用的入站规则:

user@docker1:~$ sudo iptables -A FORWARD -d 172.17.0.2/32 \
! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
user@docker1:~$ sudo iptables -A FORWARD -d 172.17.0.3/32 \
! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT

现在我们可以轻松地用特定规则替换更通用的出站规则,只允许端口80上的返回流量。例如,让我们放入一个规则,允许容器web1只在端口80上返回流量:

user@docker1:~$ sudo iptables -A FORWARD -s 172.17.0.2/32 -i \
docker0 ! -o docker0 -p tcp -m tcp --sport 80 -j ACCEPT

如果我们检查一下,我们应该能够从外部网络访问web1上的服务:

操作步骤...

然而,此时容器web1除了在端口80上无法与外部网络上的任何东西通信,因为我们没有使用通用的出站规则:

user@docker1:~$ docker exec -it web1 ping 4.2.2.2 -c 2
PING 4.2.2.2 (4.2.2.2): 48 data bytes
user@docker1:~$

为了解决这个问题,我们可以添加特定的规则,允许来自web1容器的 ICMP 之类的东西:

user@docker1:~$ sudo iptables -A FORWARD -s 172.17.0.2/32 -i \
docker0 ! -o docker0 -p icmp -j ACCEPT

上述规则与前一篇配方中的状态感知返回规则相结合,将允许 web1 容器发起和接收返回的 ICMP 流量。

user@docker1:~$ sudo iptables -A FORWARD -o docker0 -m conntrack \
--ctstate RELATED,ESTABLISHED -j ACCEPT
user@docker1:~$ docker exec -it **web1 ping 4.2.2.2** -c 2
PING 4.2.2.2 (4.2.2.2): 48 data bytes
56 bytes from 4.2.2.2: icmp_seq=0 ttl=50 time=33.892 ms
56 bytes from 4.2.2.2: icmp_seq=1 ttl=50 time=34.326 ms
--- 4.2.2.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 33.892/34.109/34.326/0.217 ms
user@docker1:~$

web2容器的情况下,其 Web 服务器仍然无法从外部网络访问。如果我们希望限制可以与 Web 服务器通信的流量源,我们可以通过更改入站端口80规则或指定出站端口80规则中的目的地来实现。例如,我们可以通过在出口规则中指定目标来将流量源限制为外部网络上的单个设备:

user@docker1:~$ sudo iptables -A FORWARD -s 172.17.0.3/32 **-d \
10.20.30.13** -i docker0 ! -o docker0 -p tcp -m tcp --sport 80 \
-j ACCEPT

现在,如果我们尝试使用外部网络上 IP 地址为10.20.30.13的实验室设备,我们应该能够访问 Web 服务器:

[user@lab1 ~]# ip addr show dev eth0 | grep inet
    inet **10.20.30.13/24** brd 10.20.30.255 scope global eth0
 [user@lab2 ~]# **curl http://docker1.lab.lab:32769
<body>
  <html>
    <h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span>
    </h1>
</body>
  </html>
[user@lab1 ~]#

但是,如果我们尝试使用具有不同 IP 地址的不同实验室服务器,连接将失败:

[user@lab2 ~]# ip addr show dev eth0 | grep inet
    inet **10.20.30.14/24** brd 10.20.30.255 scope global eth0
[user@lab2 ~]# **curl http://docker1.lab.lab:32769
[user@lab2 ~]#

同样,这条规则可以作为入站规则或出站规则实现。

以这种方式管理iptables规则时,您可能已经注意到 Docker 主机本身不再能够与容器及其托管的服务进行通信:

user@docker1:~$ ping 172.17.0.2 -c 2
PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
ping: sendmsg: Operation not permitted
ping: sendmsg: Operation not permitted
--- 172.17.0.2 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 999ms
user@docker1:~$

这是因为我们一直在过滤表中编写的所有规则都在转发链中。转发链仅适用于主机正在转发的流量,而不适用于源自主机或目的地为主机本身的流量。为了解决这个问题,我们可以在过滤表的INPUTOUTPUT链中放置规则。为了允许容器之间的 ICMP 流量,我们可以指定以下规则:

user@docker1:~$ sudo iptables -A OUTPUT -o docker0 -p icmp -m \
state --state NEW,ESTABLISHED -j ACCEPT
user@docker1:~$ sudo iptables -A INPUT -i docker0 -p icmp -m \
state --state ESTABLISHED -j ACCEPT

添加到输出链的规则查找流向docker0桥(流向容器)的流量,协议为 ICMP,并且是新的或已建立的流量。添加到输入链的规则查找流向docker0桥(流向主机)的流量,协议为 ICMP,并且是已建立的流量。由于流量是从 Docker 主机发起的,这些规则将匹配并允许容器的 ICMP 流量工作:

user@docker1:~$ ping **172.17.0.2** -c 2
PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
64 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.081 ms
64 bytes from 172.17.0.2: icmp_seq=2 ttl=64 time=0.021 ms
--- 172.17.0.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.021/0.051/0.081/0.030 ms
user@docker1:~$

然而,这仍然不允许容器本身对默认网关进行 ping。这是因为我们添加到输入链的规则仅匹配进入docker0桥的流量,只寻找已建立的会话。为了使其双向工作,您需要向第二条规则添加NEW标志,以便它也可以匹配容器向主机生成的新流量:

user@docker1:~$ sudo iptables -A INPUT -i docker0 -p icmp -m \
state --state NEW,ESTABLISHED -j ACCEPT

由于我们添加到输出链的规则已经指定了新的或已建立的流量,容器到主机的 ICMP 连接现在也将工作:

user@docker1:~$ docker exec -it **web1** ping  
PING 172.17.0.1 (172.17.0.1): 48 data bytes
56 bytes from 172.17.0.1: icmp_seq=0 ttl=64 time=0.073 ms
56 bytes from 172.17.0.1: icmp_seq=1 ttl=64 time=0.079 ms
^C--- 172.17.0.1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.073/0.076/0.079/0.000 ms
user@docker1:~$

通过负载均衡器公开服务

隔离容器的另一种方法是使用负载均衡器作为前端。这种操作模式有几个优点。首先,负载均衡器可以为多个后端节点提供智能负载均衡。如果一个容器死掉,负载均衡器可以将其从负载均衡池中移除。其次,您实际上是将容器隐藏在负载均衡虚拟 IPVIP)地址后面。客户端认为他们直接与容器中运行的应用程序进行交互,而实际上他们是在与负载均衡器进行交互。在许多情况下,负载均衡器可以提供或卸载安全功能,如 SSL 和 Web 应用程序防火墙,使基于容器的应用程序更容易以安全的方式进行扩展。在本教程中,我们将学习如何做到这一点以及 Docker 中可用的一些功能,使这更容易实现。

准备工作

在以下示例中,我们将使用多个 Docker 主机。我们还将使用用户定义的覆盖网络。假设您知道如何为覆盖网络配置 Docker 主机。如果不知道,请参阅第三章中的创建用户定义的覆盖网络教程,用户定义的网络

如何做…

负载均衡不是一个新概念,在物理和虚拟机空间中是一个众所周知的概念。然而,使用容器进行负载均衡增加了额外的复杂性,这可能会使事情变得更加复杂。首先,让我们看看在没有容器的情况下负载均衡通常是如何工作的:

如何做…

在这种情况下,我们有一个简单的负载均衡器配置,其中负载均衡器为单个后端池成员(192.168.50.150)提供 VIP。流程如下:

  • 客户端向托管在负载均衡器上的 VIP(10.10.10.150)发出请求

  • 负载均衡器接收请求,确保它具有该 IP 的 VIP,然后代表客户端向后端池成员发出请求

  • 服务器接收来自负载均衡器的请求,并直接回应负载均衡器

  • 负载均衡器然后回应客户端

在大多数情况下,对话涉及两个不同的会话,一个是客户端和负载均衡器之间的会话,另一个是负载均衡器和服务器之间的会话。每个都是一个独立的 TCP 会话。

现在,让我们展示一个在容器空间中可能如何工作的示例。查看以下图中显示的拓扑:

操作步骤…

在这个例子中,我们将使用基于容器的应用服务器作为后端池成员,以及基于容器的负载均衡器。让我们做出以下假设:

  • 主机docker2docker3将为许多支持许多不同 VIP 的不同网络演示容器提供托管

  • 我们将为每个要定义的 VIP 使用一个负载均衡器容器(haproxy实例)

  • 每个演示服务器都公开端口80

鉴于此,我们可以假设主机网络模式对于负载均衡器主机(docker1)以及托管主机(docker2docker3)都不可行,因为它需要容器在大量端口上公开服务。在用户定义网络引入之前,这将使我们不得不处理docker0桥上的端口映射。

这很快就会成为一个管理和故障排除的问题。例如,拓扑可能真的是这样的:

操作步骤…

在这种情况下,负载均衡器 VIP 将是主机docker1上的发布端口,即32769。Web 服务器本身也在发布端口以公开其 Web 服务器。让我们看一下负载均衡请求可能是什么样子:

  • 外部网络的客户端生成对http://docker1.lab.lab:32769的请求。

  • docker1主机接收请求并通过haproxy容器上的发布端口转换数据包。这将把目的地 IP 和端口更改为172.17.0.2:80

  • haproxy容器接收请求并确定被访问的 VIP 具有包含docker2:23770docker3:32771的后端池。它选择docker3主机进行此会话,并向docker3:32771发送请求。

  • 当请求穿过主机docker1时,它执行出站MASQUERADE并隐藏容器在主机的 IP 接口后面。

  • 请求被发送到主机的默认网关(MLS),然后转发请求到主机docker3

  • docker3主机接收请求并通过web2容器上的发布端口转换数据包。这将把目的地 IP 和端口更改为172.17.0.3:80

  • web2容器接收请求并向docker1回复

  • docker3主机接收到回复,并通过入站发布端口将数据包翻译回去。

  • 请求在docker1接收,通过出站MASQUERADE进行翻译,并传递到haproxy容器。

  • 然后haproxy容器回应客户端。docker1主机将haproxy容器的响应翻译回自己的 IP 地址和端口32769,响应返回到客户端。

虽然可行,但要跟踪这些内容是很多的。此外,负载均衡器节点需要知道每个后端容器的发布端口和 IP 地址。如果容器重新启动,发布端口可能会改变,从而使其无法访问。在大型后端池中进行故障排除也会是一个头痛的问题。

因此,虽然这当然是可行的,但引入用户定义的网络可以使这更加可管理。例如,我们可以利用覆盖类型网络来进行后端池成员的管理,并完全消除大部分端口发布和出站伪装的需要。这种拓扑结构看起来更像这样:

如何做…

让我们看看构建这种配置需要做些什么。我们需要做的第一件事是在其中一个节点上定义一个用户定义的覆盖类型网络。我们将在docker1上定义它,并称之为presentation_backend

user@docker1:~$ docker network create -d overlay \
--internal presentation_backend
bd9e9b5b5e064aee2ddaa58507fa6c15f49e4b0a28ea58ffb3da4cc63e6f8908
user@docker1:~$

注意

请注意,当我创建这个网络时,我传递了--internal标志。你会记得在第三章中,用户定义的网络,这意味着只有连接到这个网络的容器才能访问它。

接下来我们要做的是创建两个 Web 容器,它们将作为负载均衡器的后端池成员。我们将在docker2docker3主机上进行操作:

user@docker2:~$ docker run -dP **--name=web1 --net \
presentation_backend** jonlangemak/web_server_1
6cc8862f5288b14e84a0dd9ff5424a3988de52da5ef6a07ae593c9621baf2202
user@docker2:~$

user@docker3:~$ docker run -dP **--name=web2 --net \
presentation_backend** jonlangemak/web_server_2
e2504f08f234220dd6b14424d51bfc0cd4d065f75fcbaf46c7b6dece96676d46
user@docker3:~$

剩下要部署的组件是负载均衡器。如前所述,haproxy有一个负载均衡器的容器镜像,所以我们将在这个例子中使用它。在运行容器之前,我们需要准备一个配置,以便将其传递给haproxy使用。这是通过将一个卷挂载到容器中来完成的,我们很快就会看到。配置文件名为haproxy.cfg,我的示例配置看起来像这样:

global
    log 127.0.0.1   local0
defaults
    log     global
    mode    http
    option  httplog
    timeout connect 5000
    timeout client 50000
    timeout server 50000
    stats enable
    stats auth user:docker
    stats uri /lbstats
frontend all
            bind *:80
            use_backend pres_containers

backend **pres_containers
    balance **roundrobin
            server web1 web1:80 check
            server web2 web2:80 check
    option httpchk HEAD /index.html HTTP/1.0

在前面的配置中有几个值得指出的地方:

  • 我们将haproxy服务绑定到端口80上的所有接口

  • 任何命中端口80的容器的请求都将被负载均衡到名为pres_containers的池中

  • pres_containers池以循环轮询的方式在两个服务器之间进行负载均衡:

  • web1的端口80

  • web2的端口80

这里的一个有趣的地方是,我们可以按名称定义池成员。这是与用户定义的网络一起出现的一个巨大优势,这意味着我们不需要担心跟踪容器 IP 地址。

我将这个配置文件放在了我的主目录中名为haproxy的文件夹中:

user@docker1:~/haproxy$ pwd
/home/user/haproxy
user@docker1:~/haproxy$ ls
haproxy.cfg
user@docker1:~/haproxy$

一旦配置文件就位,我们可以按照以下方式运行容器:

user@docker1:~$ docker run -d --name haproxy --net \
presentation_backend -p 80:80 -v \
~/haproxy:/usr/local/etc/haproxy/ haproxy
d34667aa1118c70cd333810d9c8adf0986d58dab9d71630d68e6e15816741d2b
user@docker1:~$

您可能想知道为什么我在连接容器到“内部”类型网络时指定了端口映射。回想一下前几章中提到的端口映射在所有网络类型中都是全局的。换句话说,即使我目前没有使用它,它仍然是容器的一个特性。因此,如果我将来连接一个可以使用端口映射的网络类型到容器中,它就会使用端口映射。在这种情况下,我首先需要将容器连接到覆盖网络,以确保它可以访问后端 web 服务器。如果haproxy容器在启动时无法解析池成员名称,它将无法加载。

此时,haproxy容器已经可以访问其池成员,但我们无法从外部访问haproxy容器。为了做到这一点,我们将连接另一个可以使用端口映射的接口到容器中。在这种情况下,这将是docker0桥:

user@docker1:~$ docker network connect bridge haproxy
user@docker1:~

在这一点上,haproxy容器应该可以在以下 URL 外部访问:

  • 负载均衡 VIP:http://docker1.lab.lab

  • HAProxy 统计信息:http://docker1.lab.lab/lbstats

如果我们检查统计页面,我们应该看到haproxy容器可以通过覆盖网络访问每个后端 web 服务器。我们可以看到每个的健康检查都返回200 OK状态:

如何做到这一点…

现在,如果我们检查 VIP 本身并刷新几次,我们应该看到来自每个容器的网页:

如何做到这一点…

这种拓扑结构为我们提供了比我们最初在容器负载均衡方面的概念更多的优势。基于覆盖网络的使用不仅提供了基于名称的容器解析,还显著减少了流量路径的复杂性。当然,无论哪种情况,流量都会采用相同的物理路径,但我们不需要依赖那么多不同的 NAT 来使流量工作。这也使整个解决方案变得更加动态。这种设计可以很容易地复制,为许多不同的后端覆盖网络提供负载均衡。

第七章:使用 Weave Net

在本章中,我们将涵盖以下操作:

  • 安装和配置 Weave

  • 运行连接到 Weave 的容器

  • 理解 Weave IPAM

  • 使用 WeaveDNS

  • Weave 安全性

  • 使用 Weave 网络插件

介绍

Weave Net(简称 Weave)是 Docker 的第三方网络解决方案。早期,它为用户提供了 Docker 本身没有提供的额外网络功能。例如,Weave 在 Docker 开始支持用户定义的覆盖网络和嵌入式 DNS 之前,提供了覆盖网络和 WeaveDNS。然而,随着最近的发布,Docker 已经开始从网络的角度获得了与 Weave 相同的功能。也就是说,Weave 仍然有很多可提供的功能,并且是第三方工具如何与 Docker 交互以提供容器网络的有趣示例。在本章中,我们将介绍安装和配置 Weave 的基础知识,以便与 Docker 一起工作,并从网络的角度描述 Weave 的一些功能。虽然我们将花一些时间演示 Weave 的一些功能,但这并不是整个 Weave 解决方案的操作指南。本章不会涵盖 Weave 的许多功能。我建议您查看他们的网站,以获取有关功能和功能的最新信息(www.weave.works/)。

安装和配置 Weave

在这个示例中,我们将介绍安装 Weave 以及如何在 Docker 主机上提供 Weave 服务。我们还将展示 Weave 如何处理希望参与 Weave 网络的主机的连接。

准备工作

在这个示例中,我们将使用与第三章中使用的相同的实验室拓扑,用户定义的网络,在那里我们讨论了用户定义的覆盖网络:

准备工作

您将需要一些主机,最好其中一些位于不同的子网。假设在本实验中使用的 Docker 主机处于其默认配置中。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。

如何做…

Weave 是通过 Weave CLI 工具安装和管理的。一旦下载,它不仅管理与 Weave 相关的配置,还管理 Weave 服务的提供。在您希望配置的每个主机上,您只需运行以下三个命令:

  • 将 Weave 二进制文件下载到您的本地系统:
user@docker1:~$ sudo curl -L git.io/weave -o \
/usr/local/bin/weave
  • 使文件可执行:
user@docker1:~$ sudo chmod +x /usr/local/bin/weave
  • 运行 Weave:
user@docker1:~$ **weave launch

如果所有这些命令都成功完成,您的 Docker 主机现在已准备好使用 Weave 进行 Docker 网络。要验证,您可以使用weave status命令检查 Weave 状态:

user@docker1:~$ weave status
        Version: 1.7.1 (up to date; next check at 2016/10/11 01:26:42)

        Service: router
       Protocol: weave 1..2
           Name: 12:d2:fe:7a:c1:f2(docker1)
     Encryption: disabled
  PeerDiscovery: enabled
        Targets: 0
    Connections: 0
          Peers: 1
 TrustedSubnets: none

        Service: ipam
         Status: idle
          Range: 10.32.0.0/12
  DefaultSubnet: 10.32.0.0/12

        Service: dns
         Domain: weave.local.
       Upstream: 10.20.30.13
            TTL: 1
        Entries: 0

        Service: proxy
        Address: unix:///var/run/weave/weave.sock

        Service: plugin
     DriverName: weave
user@docker1:~$ 

此输出为您提供了有关 Weave 的所有五个与网络相关的服务的信息。它们是routeripamdnsproxyplugin。此时,您可能想知道所有这些服务都在哪里运行。保持与 Docker 主题一致,它们都在主机上的容器内运行:

如何操作...

正如您所看到的,有三个与 Weave 相关的容器在主机上运行。运行weave launch命令生成了所有三个容器。每个容器提供 Weave 用于网络容器的独特服务。weaveproxy容器充当一个 shim 层,允许直接从 Docker CLI 利用 Weave。weaveplugin容器实现了 Docker 的自定义网络驱动程序。"weave"容器通常被称为 Weave 路由器,并提供与 Weave 网络相关的所有其他服务。

每个容器都可以独立配置和运行。使用weave launch命令运行 Weave 意味着您想要使用所有三个容器,并使用一组合理的默认值部署它们。但是,如果您希望更改与特定容器相关的设置,您需要独立启动容器。可以通过以下方式完成:

weave launch-router
weave launch-proxy
weave launch-plugin

如果您希望在特定主机上清理 Weave 配置,可以发出weave reset命令,它将清理所有与 Weave 相关的服务容器。为了开始我们的示例,我们将只使用 Weave 路由器容器。让我们清除 Weave 配置,然后在我们的主机docker1上只启动该容器:

user@docker1:~$ weave reset
user@docker1:~$ weave launch-router
e5af31a8416cef117832af1ec22424293824ad8733bb7a61d0c210fb38c4ba1e
user@docker1:~$

Weave 路由器(weave 容器)是我们需要提供大部分网络功能的唯一容器。让我们通过检查 weave 容器配置来查看默认情况下传递给 Weave 路由器的配置选项:

user@docker1:~$ docker inspect weave
…<Additional output removed for brevity>…
        "Args": 
            "**--port**",
            "6783",
            "**--name**",
            "12:d2:fe:7a:c1:f2",
            "**--nickname**",
            "docker1",
            "**--datapath**",
            "datapath",
            "**--ipalloc-range**",
            "10.32.0.0/12",
            "**--dns-effective-listen-address**",
            "172.17.0.1",
            "**--dns-listen-address**",
            "172.17.0.1:53",
            "**--http-addr**",
            "127.0.0.1:6784",
            "**--resolv-conf**",
            "/var/run/weave/etc/resolv.conf" 
…<Additional output removed for brevity>… 
user@docker1:~$

在前面的输出中有一些值得指出的项目。IP 分配范围被给定为10.32.0.0/12。这与我们默认在docker0桥上处理的172.17.0.0/16有很大不同。此外,还定义了一个 IP 地址用作 DNS 监听地址。回想一下,Weave 还提供了 WeaveDNS,可以用来解析 Weave 网络上其他容器的名称。请注意,这个 IP 地址就是主机上docker0桥接口的 IP 地址。

现在让我们将另一个主机配置为 Weave 网络的一部分:

user@docker2:~$ sudo curl -L git.io/weave -o /usr/local/bin/weave
user@docker2:~$ sudo chmod +x /usr/local/bin/weave
user@docker2:~$ **weave launch-router 10.10.10.101
48e5035629b5124c8d3bedf09fca946b333bb54aff56704ceecef009b53dd449
user@docker2:~$

请注意,我们以与之前相同的方式安装了 Weave,但是当我们启动路由器容器时,我们指定了第一个 Docker 主机的 IP 地址。在 Weave 中,这就是我们将多个主机连接在一起的方式。您希望连接到 Weave 网络的任何主机只需指定 Weave 网络上任何现有节点的 IP 地址。如果我们检查新连接的节点上的 Weave 状态,我们应该看到它显示为已连接:

user@docker2:~$ weave status
        Version: 1.7.1 (up to date; next check at 2016/10/11 03:36:22)
        Service: router
       Protocol: weave 1..2
           Name: e6:b1:90:cd:76:da(docker2)
     Encryption: disabled
  PeerDiscovery: enabled
        Targets: 1
        Connections: 1 (1 established)
        Peers: 2 (with 2 established connections)
 TrustedSubnets: none
…<Additional output removed for brevity>…
user@docker2:~$

安装了 Weave 后,我们可以继续以相同的方式连接另外两个剩余的节点:

user@**docker3**:~$ weave launch-router **10.10.10.102
user@**docker4**:~$ weave launch-router **192.168.50.101

在每种情况下,我们将先前加入的 Weave 节点指定为我们尝试加入的节点的对等体。在我们的情况下,我们的加入模式看起来像下面的图片所示:

如何做...

然而,我们也可以让每个节点加入到任何其他现有节点,并且得到相同的结果。也就是说,将节点docker2docker3docker4加入到docker1会产生相同的最终状态。这是因为 Weave 只需要与现有节点通信,以获取有关 Weave 网络当前状态的信息。由于所有现有成员都有这些信息,因此无论加入新节点时与哪个节点通信都无所谓。如果现在检查任何 Weave 节点的状态,我们应该看到我们有四个对等体:

user@docker4:~$ weave status
        Version: 1.7.1 (up to date; next check at 2016/10/11 03:25:22)

        Service: router
       Protocol: weave 1..2
           Name: 42:ec:92:86:1a:31(docker4)
     Encryption: disabled
  PeerDiscovery: enabled
        Targets: 1
 Connections: 3 (3 established)
 Peers: 4 (with 12 established connections)
 TrustedSubnets: none 
…<Additional output removed for brevity>… 
user@docker4:~$

我们可以看到这个节点有三个连接,分别连接到其他两个加入的节点。这给我们总共四个对等体,共有十二个连接,每个 Weave 节点有三个连接。因此,尽管只在三个节点之间配置了对等连接,但最终我们得到了所有主机之间的容器连接的全网格:

如何做...

现在 Weave 的配置已经完成,我们在所有启用 Weave 的 Docker 主机之间建立了一个完整的网状网络。您可以使用weave status connections命令验证每个主机与其他对等体的连接情况。

user@docker1:~$ weave status connections
-> **192.168.50.102**:6783   established fastdp 42:ec:92:86:1a:31(**docker4**)
<- **10.10.10.102**:45632    established fastdp e6:b1:90:cd:76:da(**docker2**)
<- **192.168.50.101**:38411  established fastdp ae:af:a6:36:18:37(**docker3**)
user@docker1:~$ 

您会注意到,此配置不需要配置独立的键值存储。

还应该注意,可以使用 Weave CLI 的connectforget命令手动管理 Weave 对等体。如果在实例化 Weave 时未指定 Weave 网络的现有成员,可以使用 Weave connect 手动连接到现有成员。此外,如果从 Weave 网络中删除成员并且不希望其返回,可以使用forget命令告诉网络完全忘记对等体。

运行 Weave 连接的容器

Weave 是一个有趣的例子,展示了第三方解决方案与 Docker 交互的不同方式。它提供了几种不同的与 Docker 交互的方法。第一种是 Weave CLI,通过它不仅可以配置 Weave,还可以像通过 Docker CLI 一样生成容器。第二种是网络插件,它直接与 Docker 绑定,允许您将 Docker 容器配置到 Weave 网络上。在本教程中,我们将演示如何使用 Weave CLI 将容器连接到 Weave 网络。Weave 网络插件将在本章的后续教程中介绍。

注意

Weave 还提供了一个 API 代理服务,允许 Weave 在 Docker 和 Docker CLI 之间透明地插入自己。本章不涵盖该配置,但他们在此链接上有关于该功能的广泛文档。

www.weave.works/docs/net/latest/weave-docker-api/

准备工作

假设您正在构建本章第一个教程中创建的实验室。还假设主机已安装了 Docker 和 Weave。我们还假设在上一章中定义的 Weave 对等体已经就位。

如何做…

使用 Weave CLI 管理容器连接时,有两种方法可以将容器连接到 Weave 网络。

第一种方法是使用weave命令来运行一个容器。Weave 通过将weave run后面指定的任何内容传递给docker run来实现这一点。这种方法的优势在于,Weave 知道了连接,因为它实际上是在告诉 Docker 运行容器。

这使得 Weave 处于一个完美的位置,可以确保容器以适当的配置启动,以便在 Weave 网络上工作。例如,我们可以使用以下语法在主机docker1上启动名为web1的容器:

user@docker1:~$ **weave** run -dP --name=web1 jonlangemak/web_server_1

请注意,run命令的语法与 Docker 的相同。

注意

尽管有相似之处,但有几点不同值得注意。Weave 只能在后台或-d模式下启动容器。此外,您不能指定--rm标志在执行完毕后删除容器。

一旦以这种方式启动容器,让我们看一下容器的接口配置:

user@docker1:~$ docker exec web1 ip addr
…<Loopback interface removed for brevity>…
20: **eth0**@if21: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet **172.17.0.2/16** scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:2/64 scope link
       valid_lft forever preferred_lft forever
22: **ethwe**@if23: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1410 qdisc noqueue state UP
    link/ether a6:f2:d0:36:6f:bd brd ff:ff:ff:ff:ff:ff
    inet **10.32.0.1/12** scope global ethwe
       valid_lft forever preferred_lft forever
    inet6 fe80::a4f2:d0ff:fe36:6fbd/64 scope link
       valid_lft forever preferred_lft forever
user@docker1:~$

请注意,容器现在有一个名为ethwe的额外接口,其 IP 地址为10.32.0.1/12。这是 Weave 网络接口,除了 Docker 网络接口(eth0)之外添加的。如果我们检查,我们会注意到,由于我们传递了-P标志,Docker 已经将容器暴露的端口发布到了eth0接口上。

user@docker1:~$ docker port **web1
80**/tcp -> 0.0.0.0:**32785
user@docker1:~$ sudo iptables -t nat -S
…<Additional output removed for brevity>-A DOCKER ! -i docker0 -p tcp -m tcp --dport **32768** -j DNAT --to-destination **172.17.0.2:80
user@docker1:~$

这证明了我们之前看到的所有端口发布功能仍然是通过 Docker 网络结构完成的。Weave 接口只是添加到现有的 Docker 本机网络接口中。

连接容器到 Weave 网络的第二种方法可以通过两种不同的方式实现,但基本上产生相同的结果。可以通过使用 Weave CLI 启动当前停止的容器,或者将正在运行的容器附加到 Weave 来将现有的 Docker 容器添加到 Weave 网络。让我们看看每种方法。首先,让我们以与通常使用 Docker CLI 相同的方式在主机docker2上启动一个容器,然后使用 Weave 重新启动它:

user@docker2:~$ **docker** run -dP --name=web2 jonlangemak/web_server_2
5795d42b58802516fba16eed9445950123224326d5ba19202f23378a6d84eb1f
user@docker2:~$ **docker stop web2
web2
user@docker2:~$ **weave start web2
web2
user@docker2:~$ docker exec web2 ip addr
…<Loopback interface removed for brevity>…
15: **eth0**@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet **172.17.0.2/16** scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:2/64 scope link
       valid_lft forever preferred_lft forever
17: **ethwe**@if18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1410 qdisc noqueue state UP
    link/ether e2:22:e0:f8:0b:96 brd ff:ff:ff:ff:ff:ff
    inet **10.44.0.0/12** scope global ethwe
       valid_lft forever preferred_lft forever
    inet6 fe80::e022:e0ff:fef8:b96/64 scope link
       valid_lft forever preferred_lft forever
user@docker2:~$

因此,正如您所看到的,当使用 Weave CLI 重新启动容器时,Weave 已经处理了将 Weave 接口添加到容器中。类似地,我们可以在主机docker3上启动我们的web1容器的第二个实例,然后使用weave attach命令动态连接到 Weave 网络:

user@docker3:~$ docker run -dP --name=web1 jonlangemak/web_server_1
dabdf098964edc3407c5084e56527f214c69ff0b6d4f451013c09452e450311d
user@docker3:~$ docker exec web1 ip addr
…<Loopback interface removed for brevity>…
5: **eth0**@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet **172.17.0.2/16** scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:2/64 scope link
       valid_lft forever preferred_lft forever
user@docker3:~$ 
user@docker3:~$ **weave attach web1
10.36.0.0
user@docker3:~$ docker exec web1 ip addr
…<Loopback interface removed for brevity>…
5: **eth0**@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet **172.17.0.2/16** scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:2/64 scope link
       valid_lft forever preferred_lft forever
15: **ethwe**@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1410 qdisc noqueue state UP
    link/ether de:d6:1c:03:63:ba brd ff:ff:ff:ff:ff:ff
    inet **10.36.0.0/12** scope global ethwe
       valid_lft forever preferred_lft forever
    inet6 fe80::dcd6:1cff:fe03:63ba/64 scope link
       valid_lft forever preferred_lft forever
user@docker3:~$

正如我们在前面的输出中所看到的,容器在我们手动将其附加到 Weave 网络之前没有 ethwe 接口。附加是动态完成的,无需重新启动容器。除了将容器添加到 Weave 网络外,您还可以使用 weave detach 命令动态将其从 Weave 中移除。

在这一点上,您应该已经连接到了 Weave 网络的所有容器之间的连通性。在我的情况下,它们被分配了以下 IP 地址:

  • web1 在主机 docker1 上:10.32.0.1

  • web2 在主机 docker2 上:10.44.0.0

  • web1 在主机 docker3 上:10.36.0.0

user@docker1:~$ **docker exec -it web1 ping 10.44.0.0 -c 2
PING 10.40.0.0 (10.40.0.0): 48 data bytes
56 bytes from 10.40.0.0: icmp_seq=0 ttl=64 time=0.447 ms
56 bytes from 10.40.0.0: icmp_seq=1 ttl=64 time=0.681 ms
--- 10.40.0.0 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.447/0.564/0.681/0.117 ms
user@docker1:~$ **docker exec -it web1 ping 10.36.0.0 -c 2
PING 10.44.0.0 (10.44.0.0): 48 data bytes
56 bytes from 10.44.0.0: icmp_seq=0 ttl=64 time=1.676 ms
56 bytes from 10.44.0.0: icmp_seq=1 ttl=64 time=0.839 ms
--- 10.44.0.0 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.839/1.257/1.676/0.419 ms
user@docker1:~$

这证明了 Weave 网络正在按预期工作,并且容器位于正确的网络段上。

了解 Weave IPAM

正如我们在前几章中多次看到的那样,IPAM 是任何容器网络解决方案的关键组成部分。当您开始在多个 Docker 主机上使用常见网络时,IPAM 的关键性变得更加清晰。随着 IP 分配数量的增加,能够通过名称解析这些容器也变得至关重要。与 Docker 一样,Weave 为其容器网络解决方案提供了集成的 IPAM。在本章中,我们将展示如何配置和利用 Weave IPAM 来管理 Weave 网络中的 IP 分配。

做好准备

假设您正在基于本章第一个配方中创建的实验室进行构建。还假设主机已安装了 Docker 和 Weave。Docker 应该处于其默认配置状态,Weave 应该已安装但尚未进行对等连接。如果您需要删除先前示例中定义的对等连接,请在每个主机上发出 weave reset 命令。

如何做…

Weave 对 IPAM 的解决方案依赖于整个 Weave 网络使用一个大的子网,然后将其划分为较小的部分,并直接分配给每个主机。然后主机从分配给它的 IP 地址池中分配容器 IP 地址。为了使其工作,Weave 集群必须就要分配给每个主机的 IP 分配达成一致意见。它首先在集群内部达成共识。如果您大致知道您的集群将有多大,您可以在初始化期间向 Weave 提供具体信息,以帮助它做出更好的决定。

注意

本教程的目标不是深入讨论 Weave 与 IPAM 使用的共识算法的细节。有关详细信息,请参阅以下链接:

www.weave.works/docs/net/latest/ipam/

为了这个示例,我们假设您不知道您的集群有多大,我们将假设它将从两个主机开始并从那里扩展。

重要的是要理解,Weave 中的 IPAM 在您首次配置容器之前处于空闲状态。例如,让我们从在主机docker1上配置 Weave 开始:

user@docker1:~$ **weave launch-router --ipalloc-range 172.16.16.0/24
469c81f786ac38618003e4bd08eb7303c1f8fa84d38cc134fdb352c589cbc42d
user@docker1:~$

您应该注意到的第一件事是添加参数--ipalloc-range。正如我们之前提到的,Weave 是基于一个大子网的概念工作。默认情况下,这个子网是10.32.0.0/12。在 Weave 初始化期间,可以通过向 Weave 传递--ipalloc-range标志来覆盖此默认设置。为了使这些示例更容易理解,我决定将默认子网更改为更易管理的内容;在这种情况下,是172.16.16.0/24

让我们还在主机docker2上运行相同的命令,但是传递主机docker1的 IP 地址,以便它可以立即进行对等连接:

user@docker2:~$ **weave launch-router --ipalloc-range \
172.16.16.0/24 10.10.10.101
9bfb1cb0295ba87fe88b7373a8ff502b1f90149741b2f43487d66898ffad775d
user@docker2:~$

请注意,我再次向 Weave 传递了相同的子网。每个运行 Weave 的主机上的 IP 分配范围相同是至关重要的。只有同意相同 IP 分配范围的主机才能正常运行。现在让我们检查一下 Weave 服务的状态:

user@docker2:~$ weave status
…<Additional output removed for brevity>…
Connections: 1 (1 established)
        Peers: 2 (with 2 established connections)
 TrustedSubnets: none

        Service: **ipam
         Status: **idle
          Range: **172.16.16.0/24
  DefaultSubnet: **172.16.16.0/24
…<Additional output removed for brevity>… 
user@docker2:~$

输出显示两个对等点,表明我们对docker1的对等连接成功。请注意,IPAM 服务显示为idle状态。idle状态意味着 Weave 正在等待更多对等点加入,然后才会做出关于哪些主机将获得哪些 IP 分配的决定。让我们看看当我们运行一个容器时会发生什么:

user@docker2:~$ weave run -dP --name=web2 jonlangemak/web_server_2
379402b05db83315285df7ef516e0417635b24923bba3038b53f4e58a46b4b0d
user@docker2:~$

如果我们再次检查 Weave 状态,我们应该看到 IPAM 现在已从idle更改为ready

user@docker2:~$ weave status
…<Additional output removed for brevity>… 
    Connections: 1 (1 established)
          Peers: 2 (with 2 established connections)
 TrustedSubnets: none

        Service: **ipam
         Status: **ready
          Range: 172.16.16.0/24
  DefaultSubnet: 172.16.16.0/24 
…<Additional output removed for brevity>… 
user@docker2:~$

连接到 Weave 网络的第一个容器迫使 Weave 达成共识。此时,Weave 已经决定集群大小为两,并已尽最大努力在主机之间分配可用的 IP 地址。让我们在主机docker1上运行一个容器,然后检查分配给每个容器的 IP 地址:

user@docker1:~$ weave run -dP --name=web1 jonlangemak/web_server_1
fbb3eac42115**9308f41d795638c3a4689c92a9401718fd1988083bfc12047844
user@docker1:~$ **weave ps
weave:expose 12:d2:fe:7a:c1:f2
fbb3eac42115** 02:a7:38:ab:73:23 **172.16.16.1/24
user@docker1:~$

使用weave ps命令,我们可以看到我们刚刚在主机docker1上生成的容器收到了 IP 地址172.16.16.1/24。如果我们检查主机docker2上的容器web2的 IP 地址,我们会看到它获得了 IP 地址172.16.16.128/24

user@docker2:~$ **weave ps
weave:expose e6:b1:90:cd:76:da
dde411fe4c7b** c6:42:74:89:71:da **172.16.16.128/24
user@docker2:~$

这是非常合理的。Weave 知道网络中有两个成员,所以它直接将分配分成两半,基本上为每个主机提供自己的/25网络分配。docker1开始分配/24的前半部分,docker2则从后半部分开始。

尽管完全分配了整个空间,这并不意味着我们现在用完了 IP 空间。这些初始分配更像是预留,可以根据 Weave 网络的大小进行更改。例如,我们现在可以将主机docker3添加到 Weave 网络,并在其上启动web1容器的另一个实例:

user@docker3:~$ **weave launch-router --ipalloc-range \
172.16.16.0/24 10.10.10.101
8e8739f48854d87ba14b9dcf220a3c33df1149ce1d868819df31b0fe5fec2163
user@docker3:~$ **weave run -dP --name=web1 jonlangemak/web_server_1
0c2193f2d756**9943171764155e0e93272f5715c257adba75ed544283a2794d3e
user@docker3:~$ weave ps
weave:expose ae:af:a6:36:18:37
0c2193f2d756** 76:8d:4c:ee:08:db **172.16.16.224/24
user@docker3:~$ 

因为网络现在有更多成员,Weave 只是进一步将初始分配分成更小的块。根据分配给每个主机上容器的 IP 地址,我们可以看到 Weave 试图保持分配在有效的子网内。以下图片显示了第三和第四个主机加入 Weave 网络时 IP 分配的情况:

如何做…

重要的是要记住,尽管分配给每台服务器的分配是灵活的,但当它们为容器分配 IP 地址时,它们都使用与初始分配相同的掩码。这确保容器都假定它们在同一个网络上,并且彼此直接连接,无需有路由指向其他主机。

为了证明初始 IP 分配必须在所有主机上相同,我们可以尝试使用不同的子网加入最后一个主机docker4

user@docker4:~$ weave launch-router --ipalloc-range 172.64.65.0/24 10.10.10.101
9716c02c66459872e60447a6a3b6da7fd622bd516873146a874214057fe11035
user@docker4:~$ weave status
…<Additional output removed for brevity>…
        Service: router
       Protocol: weave 1..2
           Name: 42:ec:92:86:1a:31(docker4)
     Encryption: disabled
  PeerDiscovery: enabled
        Targets: 1
        Connections: 1 (1 failed)
…<Additional output removed for brevity>…
user@docker4:~$

如果我们检查 Weave 路由器容器的日志,我们会发现它无法加入现有的 Weave 网络,因为定义了错误的 IP 分配:

user@docker4:~$ docker logs weave
…<Additional output removed for brevity>… 
INFO: 2016/10/11 02:16:09.821503 ->[192.168.50.101:6783|ae:af:a6:36:18:37(docker3)]: **connection shutting down due to error: Incompatible IP allocation ranges (received: 172.16.16.0/24, ours: 172.64.65.0/24)
…<Additional output removed for brevity>… 

加入现有的 Weave 网络的唯一方法是使用与所有现有节点相同的初始 IP 分配。

最后,重要的是要指出,不是必须以这种方式使用 Weave IPAM。您可以通过在weave run期间手动指定 IP 地址来手动分配 IP 地址,就像这样:

user@docker1:~$ weave run **1.1.1.1/24** -dP --name=wrongip \
jonlangemak/web_server_1
259004af91e3b0367bede723c9eb9d3fbdc0c4ad726efe7aea812b79eb408777
user@docker1:~$

在指定单个 IP 地址时,您可以选择任何 IP 地址。正如您将在后面的配方中看到的那样,您还可以指定用于分配的子网,并让 Weave 跟踪该子网在 IPAM 中的分配。在从子网分配 IP 地址时,子网必须是初始 Weave 分配的一部分。

如果您希望手动为某些容器分配 IP 地址,可能明智的是在初始 Weave 配置期间配置额外的 Weave 参数,以限制动态分配的范围。您可以在启动时向 Weave 传递--ipalloc-default-subnet参数,以限制动态分配给主机的 IP 地址的范围。例如,您可以传递以下内容:

weave launch-router --ipalloc-range 172.16.16.0/24 \
--ipalloc-default-subnet 172.16.16.0/25

这将配置 Weave 子网为172.16.16.0/25,使较大网络的其余部分可用于手动分配。我们将在后面的教程中看到,这种类型的配置在 Weave 如何处理 Weave 网络上的网络隔离中起着重要作用。

使用 WeaveDNS

自然而然,在 IPAM 之后要考虑的下一件事是名称解析。无论规模如何,都需要一种方法来定位和识别容器,而不仅仅是 IP 地址。与较新版本的 Docker 一样,Weave 为解析 Weave 网络上的容器名称提供了自己的 DNS 服务。在本教程中,我们将审查 WeaveDNS 的默认配置,以及它是如何实现的,以及一些相关的配置设置,让您可以立即开始运行。

准备工作

假设您正在构建本章第一个教程中创建的实验室。还假设主机已安装了 Docker 和 Weave。Docker 应该处于默认配置状态,并且 Weave 应该已成功地与所有四个主机进行了对等连接,就像我们在本章的第一个教程中所做的那样。

如何做…

如果您一直在本章中跟随到这一点,您已经配置了 WeaveDNS。WeaveDNS 随 Weave 路由器容器一起提供,并且默认情况下已启用。我们可以通过查看 Weave 状态来看到这一点:

user@docker1:~$ weave status
…<Additional output removed for brevity>…
        Service: dns
         Domain: weave.local.
       Upstream: 10.20.30.13
            TTL: 1
        Entries: 0
…<Additional output removed for brevity>…

当 Weave 配置 DNS 服务时,它会从一些合理的默认值开始。在这种情况下,它检测到我的主机 DNS 服务器是10.20.30.13,因此将其配置为上游解析器。它还选择了weave.local作为域名。如果我们使用 weave run 语法启动容器,Weave 将确保容器以允许其使用此 DNS 服务的方式进行配置。例如,让我们在主机docker1上启动一个容器:

user@docker1:~$ weave run -dP --name=web1 jonlangemak/web_server_1
c0cf29fb07610b6ffc4e55fdd4305f2b79a89566acd0ae0a6de09df06979ef36
user@docker1:~$ docker exec –t web1 more /etc/resolv.conf
nameserver 172.17.0.1
user@docker1:~$

启动容器后,我们可以看到 Weave 已经配置了容器的resolv.conf文件,与 Docker 的默认配置不同。回想一下,默认情况下,在非用户定义的网络中,Docker 会给容器分配与 Docker 主机本身相同的 DNS 配置。在这种情况下,Weave 给容器分配了一个名为172.17.0.1的名称服务器,默认情况下是分配给docker0桥接口的 IP 地址。您可能想知道 Weave 如何期望容器通过与docker0桥接口通信来解析自己的 DNS 系统。解决方案非常简单。Weave 路由器容器以主机模式运行,并绑定到端口53的服务。

user@docker1:~$ docker network inspect **host
…<Additional output removed for brevity>… 
"Containers": {        "03e3e82a5e0ced0b973e2b31ed9c2d3b8fe648919e263965d61ee7c425d9627c": {
                "Name": "**weave**",
…<Additional output removed for brevity>…

如果我们检查主机上绑定的端口,我们可以看到 weave 路由器正在暴露端口53

user@docker1:~$ sudo netstat -plnt
Active Internet connections (only servers)
…<some columns removed to increase readability>…
Proto Local Address State       PID/Program name
tcp   **172.17.0.1:53** LISTEN      **2227/weaver

这意味着 Weave 容器中的 WeaveDNS 服务将在docker0桥接口上监听 DNS 请求。让我们在主机docker2上启动另一个容器:

user@docker2:~$ **weave run -dP --name=web2 jonlangemak/web_server_2
b81472e86d8ac62511689185fe4e4f36ac4a3c41e49d8777745a60cce6a4ac05
user@docker2:~$ **docker exec -it web2 ping web1 -c 2
PING web1.weave.local (10.32.0.1): 48 data bytes
56 bytes from 10.32.0.1: icmp_seq=0 ttl=64 time=0.486 ms
56 bytes from 10.32.0.1: icmp_seq=1 ttl=64 time=0.582 ms
--- web1.weave.local ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.486/0.534/0.582/0.048 ms
user@docker2:~$ 

只要两个容器都在 Weave 网络上并且具有适当的设置,Weave 将自动生成一个包含容器名称的 DNS 记录。我们可以使用weave status dns命令从任何 Weave 启用的主机上查看 Weave 知道的所有名称记录:

user@docker2:~$ weave status dns
web1         10.32.0.1       86029a1305f1 12:d2:fe:7a:c1:f2
web2         10.44.0.0       56927d3bf002 e6:b1:90:cd:76:da
user@docker2:~$ 

在这里,我们可以看到目标主机的 Weave 网络接口的容器名称、IP 地址、容器 ID 和 MAC 地址。

这很有效,但依赖于容器配置了适当的设置。这是另一种情况,使用 Weave CLI 会非常有帮助,因为它确保这些设置在容器运行时生效。例如,如果我们在主机docker3上使用 Docker CLI 启动另一个容器,然后将其连接到 Docker,它将不会获得 DNS 记录:

user@docker3:~$ docker run -dP --name=web1 jonlangemak/web_server_1
cd3b043bd70c0f60a03ec24c7835314ca2003145e1ca6d58bd06b5d0c6803a5c
user@docker3:~$ **weave attach web1
10.36.0.0
user@docker3:~$ docker exec -it **web1 ping web2
ping: unknown host
user@docker3:~$

这有两个原因不起作用。首先,容器不知道在哪里查找 Weave DNS,并且试图通过 Docker 提供的 DNS 服务器来解析它。在这种情况下,这是在 Docker 主机上配置的一个:

user@docker3:~$ **docker exec -it web1 more /etc/resolv.conf
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 10.20.30.13
search lab.lab
user@docker3:~$

其次,当容器被连接时,Weave 没有在 WeaveDNS 中注册记录。为了使 Weave 在 WeaveDNS 中为容器生成记录,容器必须在相同的域中。为此,当 Weave 通过其 CLI 运行容器时,它会传递容器的主机名以及域名。当我们在 Docker 中运行容器时,我们可以通过提供主机名来模拟这种行为。例如:

user@docker3:~$ docker stop web1
user@docker3:~$ docker rm web1
user@docker3:~$ docker run -dP **--hostname=web1.weave.local \
--name=web1 jonlangemak/web_server_1
04bb1ba21b692b4117a9b0503e050d7f73149b154476ed5a0bce0d049c3c9357
user@docker3:~$

现在当我们将容器连接到 Weave 网络时,我们应该看到为其生成的 DNS 记录:

user@docker3:~$ weave attach web1
10.36.0.0
user@docker3:~$ weave status dns
web1         10.32.0.1       86029a1305f1 12:d2:fe:7a:c1:f2
web1         10.36.0.0       5bab5eae10b0 ae:af:a6:36:18:37
web2         10.44.0.0       56927d3bf002 e6:b1:90:cd:76:da
user@docker3:~$

注意

如果您希望该容器还能够解析 WeaveDNS 中的记录,还需要向容器传递--dns=172.17.0.1标志,以确保其 DNS 服务器设置为docker0桥的 IP 地址。

您可能已经注意到,我们现在在 WeaveDNS 中有相同容器名称的两个条目。这是 Weave 在 Weave 网络中提供基本负载平衡的方式。例如,如果我们回到docker2主机,让我们尝试多次 ping 名称web1

user@docker2:~$ **docker exec -it web2 ping web1 -c 1
PING **web1.weave.local (10.32.0.1):** 48 data bytes
56 bytes from 10.32.0.1: icmp_seq=0 ttl=64 time=0.494 ms
--- web1.weave.local ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.494/0.494/0.494/0.000 ms
user@docker2:~$ **docker exec -it web2 ping web1 -c 1
PING **web1.weave.local (10.36.0.0):** 48 data bytes
56 bytes from 10.36.0.0: icmp_seq=0 ttl=64 time=0.796 ms
--- web1.weave.local ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.796/0.796/0.796/0.000 ms
user@docker2:~$ **docker exec -it web2 ping web1 -c 1
PING **web1.weave.local (10.32.0.1):** 48 data bytes
56 bytes from 10.32.0.1: icmp_seq=0 ttl=64 time=0.507 ms
--- web1.weave.local ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.507/0.507/0.507/0.000 ms
user@docker2:~$

请注意,在第二次 ping 尝试期间,容器解析为不同的 IP 地址。由于 WeaveDNS 中有相同名称的多个记录,我们可以仅使用 DNS 提供基本负载平衡功能。Weave 还将跟踪容器的状态,并将死掉的容器从 WeaveDNS 中移除。例如,如果我们在docker3主机上杀死容器,我们应该看到web1记录中的一个被移出 DNS,只留下web1的单个记录:

user@docker3:~$ docker stop web1
web1
user@docker3:~$ weave status dns
web1         10.32.0.1       86029a1305f1 12:d2:fe:7a:c1:f2
web2         10.44.0.0       56927d3bf002 e6:b1:90:cd:76:da
user@docker3:~$

注意

有许多不同的配置选项可供您自定义 WeaveDNS 的工作方式。要查看完整指南,请查看[www.weave.works/docs/net/latest/weavedns/](http:// www.weave.works/docs/net/la…

Weave 安全性

Weave 提供了一些属于安全性范畴的功能。由于 Weave 是一种基于覆盖的网络解决方案,它提供了在物理或底层网络中传输覆盖流量的加密能力。当您的容器可能需要穿越公共网络时,这可能特别有用。此外,Weave 允许您在某些网络段内隔离容器。Weave 依赖于为每个隔离的段使用不同的子网来实现此目的。在本配方中,我们将介绍如何配置覆盖加密以及如何为 Weave 网络中的不同容器提供隔离。

准备工作

假设您正在构建本章第一个配方中创建的实验室。还假设主机已安装了 Docker 和 Weave。Docker 应该处于默认配置状态,Weave 应该已安装但尚未进行对等连接。如果您需要删除先前示例中定义的对等连接,请在每个主机上发出weave reset命令。

如何做…

配置 Weave 以加密覆盖网络相当容易实现;但是,必须在 Weave 的初始配置期间完成。使用前面配方中的相同实验拓扑,让我们运行以下命令来构建 Weave 网络:

  • 在主机docker1上:
weave launch-router **--password notverysecurepwd \
--trusted-subnets 192.168.50.0/24** --ipalloc-range \
172.16.16.0/24 --ipalloc-default-subnet 172.16.16.128/25
  • 在主机docker2docker3docker4上:
weave launch-router **--password notverysecurepwd \
--trusted-subnets 192.168.50.0/24** --ipalloc-range \
172.16.16.0/24 --ipalloc-default-subnet \
172.16.16.128/25 10.10.10.101

您会注意到我们在主机上运行的命令基本相同,只是最后三个主机指定docker1作为对等体以构建 Weave 网络。在任何情况下,在 Weave 初始化期间,我们传递了一些额外的参数给路由器:

  • --password:这是启用 Weave 节点之间通信加密的参数。与我的示例不同,您应该选择一个非常安全的密码来使用。这需要在运行 weave 的每个节点上相同。

  • --trusted-subnets:这允许您定义主机子网为受信任的,这意味着它们不需要加密通信。当 Weave 进行加密时,它会退回到比通常使用的更慢的数据路径。由于使用--password参数会打开端到端的加密,定义一些子网不需要加密可能是有意义的

  • --ipalloc-range:在这里,我们定义更大的 Weave 网络为172.16.16.0/24。我们在之前的配方中看到了这个命令的使用:

  • --ipalloc-default-subnet:这指示 Weave 默认从更大的 Weave 分配中的较小子网中分配容器 IP 地址。在这种情况下,那就是172.16.16.128/25

现在,让我们在每个主机上运行以下容器:

  • docker1
weave run -dP --name=web1tenant1 jonlangemak/web_server_1
  • docker2
weave run -dP --name=web2tenant1 jonlangemak/web_server_2
  • docker3
weave run **net:172.16.16.0/25** -dP --name=web1tenant2 \
jonlangemak/web_server_1
  • docker4
weave run **net:172.16.16.0/25** -dP --name=web2tenant2 \
jonlangemak/web_server_2

请注意,在主机docker3docker4上,我添加了net:172.16.16.0/25参数。回想一下,当我们启动 Weave 网络时,我们告诉 Weave 默认从172.16.16.128/25中分配 IP 地址。只要在更大的 Weave 网络范围内,我们可以在容器运行时覆盖这一设置,并为 Weave 提供一个新的子网来使用。在这种情况下,docker1docker2上的容器将获得172.16.16.128/25内的 IP 地址,因为这是默认设置。docker3docker4上的容器将获得172.16.16.0/25内的 IP 地址,因为我们覆盖了默认设置。一旦您启动了所有容器,我们可以确认这一点:

user@docker4:~$ weave status dns
web1tenant1  172.16.16.129   26c58ef399c3 12:d2:fe:7a:c1:f2
web1tenant2  172.16.16.64    4c569073d663 ae:af:a6:36:18:37
web2tenant1  172.16.16.224   211c2e0b388e e6:b1:90:cd:76:da
web2tenant2  172.16.16.32    191207a9fb61 42:ec:92:86:1a:31
user@docker4:~$

正如我之前提到的,使用不同的子网是 Weave 提供容器分割的方式。在这种情况下,拓扑将如下所示:

如何操作...

虚线象征着 Weave 在覆盖网络中为我们提供的隔离。由于tenant1容器位于与tenant2容器不同的子网中,它们将无法连接。这样,Weave 使用基本的网络来实现容器隔离。我们可以通过一些测试来证明这一点:

user@docker4:~$ docker exec -**it web2tenant2** curl http://**web1tenant2
<body>
  <html>
    <h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
    </h1>
</body>
  </html>
user@docker4:~$ docker exec -it **web2tenant2** curl http://**web1tenant1
user@docker4:~$ docker exec -it **web2tenant2** curl http://**web2tenant1
user@docker4:~$
user@docker4:~$ **docker exec -it web2tenant2 ping web1tenant1 -c 1
PING web1tenant1.weave.local (172.16.16.129): 48 data bytes
--- web1tenant1.weave.local ping statistics ---
1 packets transmitted, 0 packets received, **100% packet loss
user@docker4:~$

web2tenant2容器尝试访问其自己租户(子网)中的服务时,它按预期工作。尝试访问tenant1中的服务将不会收到响应。但是,由于 DNS 在 Weave 网络中是共享的,容器仍然可以解析tenant1中容器的 IP 地址。

这也说明了加密的例子,以及我们如何指定某些主机为受信任的。无论容器位于哪个子网中,Weave 仍然在所有主机之间建立连接。由于我们在 Weave 初始化期间启用了加密,所有这些连接现在应该是加密的。但是,我们还指定了一个受信任的网络。受信任的网络定义了不需要在它们之间进行加密的节点。在我们的情况下,我们指定192.168.50.0/24为受信任的网络。由于有两个具有这些 IP 地址的节点,docker3docker4,我们应该看到它们之间的连接是未加密的。我们可以在主机上使用 weave status connections 命令来验证这一点。我们应该得到以下响应:

  • docker1(截断输出):
<- 10.10.10.102:45888    established encrypted   sleeve 
<- 192.168.50.101:57880  established encrypted   sleeve 
<- 192.168.50.102:45357  established encrypted   sleeve 
  • docker2(截断输出):
<- 192.168.50.101:35207  established encrypted   sleeve 
<- 192.168.50.102:34640  established encrypted   sleeve 
-> 10.10.10.101:6783     established encrypted   sleeve 
  • docker3(截断输出):
-> 10.10.10.101:6783     established encrypted   sleeve 
-> 192.168.50.102:6783   established unencrypted fastdp
-> 10.10.10.102:6783     established encrypted   sleeve 
  • docker4(截断输出):
-> 10.10.10.102:6783     established encrypted   sleeve 
<- 192.168.50.101:36315  established unencrypted fastdp
-> 10.10.10.101:6783     established encrypted   sleeve 

您可以看到所有的连接都显示为加密,除了主机docker3192.168.50.101)和主机docker4192.168.50.102)之间的连接。由于两个主机需要就受信任的网络达成一致,主机docker1docker2将永远不会同意它们的连接是未加密的。

使用 Weave 网络插件

Weave 的一个特点是它可以以几种不同的方式操作。正如我们在本章的前几个示例中看到的,Weave 有自己的 CLI,我们可以使用它直接将容器配置到 Weave 网络中。虽然这当然是一种紧密集成的工作方式,但它要求您利用 Weave CLI 或 Weave API 代理与 Docker 集成。除了这些选项,Weave 还编写了一个原生的 Docker 网络插件。这个插件允许您直接从 Docker 中使用 Weave。也就是说,一旦插件注册,您就不再需要使用 Weave CLI 将容器配置到 Weave 中。在本示例中,我们将介绍如何安装和使用 Weave 网络插件。

准备工作

假设您正在基于本章第一个示例中创建的实验室进行构建。还假设主机已经安装了 Docker 和 Weave。Docker 应该处于默认配置状态,Weave 应该已安装,并且所有四个主机已成功互联,就像我们在本章的第一个示例中所做的那样。

如何做…

与 Weave 的其他组件一样,利用 Docker 插件非常简单。您只需要告诉 Weave 启动它。例如,如果我决定在主机docker1上使用 Docker 插件,我可以这样启动插件:

user@docker1:~$ **weave launch-plugin
3ef9ee01cc26173f2208b667fddc216e655795fd0438ef4af63dfa11d27e2546
user@docker1:~$ 

就像其他服务一样,该插件以容器的形式存在。在运行了前面的命令之后,您应该看到插件作为名为weaveplugin的容器运行:

如何做…

一旦运行,您还应该看到它注册为一个网络插件:

user@docker1:~$ docker info
…<Additional output removed for brevity>… 
Plugins:
 Volume: local
 Network: host **weavemesh** overlay bridge null<Additional output removed for brevity>user@docker1:~$ 

我们还可以将其视为使用docker network子命令定义的网络类型:

user@docker1:~$ docker network ls
NETWORK ID        NAME              DRIVER            SCOPE
79105142fbf0      bridge            bridge            local
bb090c21339c      host              host              local
9ae306e2af0a      none              null              local
20864e3185f5      weave             weavemesh         local
user@docker1:~$ 

在这一点上,通过 Docker 直接将容器连接到 Weave 网络可以直接完成。您只需要在启动容器时指定weave的网络名称。例如,我们可以运行:

user@docker1:~$ docker run -dP --name=web1 --**net=weave** \
jonlangemak/web_server_1
4d84cb472379757ae4dac5bf6659ec66c9ae6df200811d56f65ffc957b10f748
user@docker1:~$

如果我们查看容器接口,我们应该看到我们习惯在 Weave 连接的容器中看到的两个接口:

user@docker1:~$ docker exec web1 ip addr
…<loopback interface removed for brevity>…
83: **ethwe**0@if84: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1410 qdisc noqueue state UP
    link/ether 9e:b2:99:c4:ac:c4 brd ff:ff:ff:ff:ff:ff
    inet **10.32.0.1/12** scope global ethwe0
       valid_lft forever preferred_lft forever
    inet6 fe80::9cb2:99ff:fec4:acc4/64 scope link
       valid_lft forever preferred_lft forever
86: **eth1**@if87: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
    inet **172.18.0.2/16** scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe12:2/64 scope link
       valid_lft forever preferred_lft forever
user@docker1:~$

但是,您可能会注意到eth1的 IP 地址不在docker0桥上,而是在我们在早期章节中看到的docker_gwbridge上使用的。使用网关桥而不是docker0桥的好处是,网关桥默认情况下已禁用 ICC。这可以防止 Weave 连接的容器意外地在docker0桥上跨通信,如果您启用了 ICC 模式。

插件方法的一个缺点是 Weave 不在中间告诉 Docker 有关 DNS 相关的配置,这意味着容器没有注册它们的名称。即使它们注册了,它们也没有接收到解析 WeaveDNS 所需的正确名称解析设置。我们可以指定容器的正确设置的两种方法。在任何一种情况下,我们都需要在容器运行时手动指定参数。第一种方法涉及手动指定所有必需的参数。手动完成如下:

user@docker1:~$ docker run -dP --name=web1 \
--hostname=web1.weave.local --net=weave --dns=172.17.0.1 \
--dns-search=weave.local** jonlangemak/web_server_1
6a907ee64c129d36e112d0199eb2184663f5cf90522ff151aa10c2a1e6320e16
user@docker1:~$

为了注册 DNS,您需要在前面的代码中显示的四个加粗设置:

  • --hostname=web1.weave.local:如果您不将容器的主机名设置为weave.local中的名称,DNS 服务器将不会注册该名称。

  • --net=weave:它必须在 Weave 网络上才能正常工作。

  • --dns=172.17.0.1:我们需要告诉它使用在docker0桥 IP 地址上监听的 Weave DNS 服务器。但是,您可能已经注意到,该容器实际上并没有在docker0桥上拥有 IP 地址。相反,由于我们连接到docker-gwbridge,我们在172.18.0.0/16网络中有一个 IP 地址。在任何一种情况下,由于两个桥都有 IP 接口,容器可以通过docker_gwbridge路由到docker0桥上的 IP 接口。

  • --dns-search=weave.local:这允许容器解析名称而无需指定完全限定域名FQDN)。

一旦使用这些设置启动容器,您应该看到 WeaveDNS 中注册的记录:

user@docker1:~$ weave status dns
web1         10.32.0.1       7b02c0262786 12:d2:fe:7a:c1:f2
user@docker1:~$

第二种解决方案仍然是手动的,但涉及从 Weave 本身提取 DNS 信息。您可以从 Weave 中注入 DNS 服务器和搜索域,而不是指定它。Weave 有一个名为dns-args的命令,将为您返回相关信息。因此,我们可以简单地将该命令作为容器参数的一部分注入,而不是指定它,就像这样:

user@docker2:~$ docker run --hostname=web2.weave.local \
--net=weave **$(weave dns-args)** --name=web2 -dP \
jonlangemak/web_server_2
597ffde17581b7203204594dca84c9461c83cb7a9076ed3d1ed3fcb598c2b77d
user@docker2:~$

当然,这并不妨碍需要指定网络或容器的 FQDN,但它确实减少了一些输入。此时,您应该能够看到 WeaveDNS 中定义的所有记录,并能够通过名称访问 Weave 网络上的服务。

user@docker1:~$ weave status dns
web1         10.32.0.1       7b02c0262786 12:d2:fe:7a:c1:f2
web2         10.32.0.2       b154e3671feb 12:d2:fe:7a:c1:f2
user@docker1:~$

user@docker2:~$ **docker exec -it web2 ping web1 -c 2
PING web1 (10.32.0.1): 48 data bytes
56 bytes from 10.32.0.1: icmp_seq=0 ttl=64 time=0.139 ms
56 bytes from 10.32.0.1: icmp_seq=1 ttl=64 time=0.130 ms
--- web1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.130/0.135/0.139/0.000 ms
user@docker1:~$

您可能注意到这些容器的 DNS 配置并不完全符合您的预期。例如,resolv.conf文件并未显示我们在容器运行时指定的 DNS 服务器。

user@docker1:~$ docker exec web1 more /etc/resolv.conf
::::::::::::::
/etc/resolv.conf
::::::::::::::
search weave.local
nameserver 127.0.0.11
options ndots:0
user@docker1:~$

然而,如果您检查容器的配置,您会看到正确的 DNS 服务器被正确定义。

user@docker1:~$ docker inspect web1
…<Additional output removed for brevity>…
            "**Dns**": [
                "**172.17.0.1**"
            ],
…<Additional output removed for brevity>…
user@docker1:~$

请记住,用户定义的网络需要使用 Docker 的嵌入式 DNS 系统。我们在容器的resolv.conf文件中看到的 IP 地址引用了 Docker 的嵌入式 DNS 服务器。反过来,当我们为容器指定 DNS 服务器时,嵌入式 DNS 服务器会将该服务器添加为嵌入式 DNS 中的转发器。这意味着,尽管请求仍然首先到达嵌入式 DNS 服务器,但请求会被转发到 WeaveDNS 进行解析。

请注意

Weave 插件还允许您使用 Weave 驱动程序创建额外的用户定义网络。然而,由于 Docker 将其视为全局范围,它们需要使用外部密钥存储。如果您有兴趣以这种方式使用 Weave,请参考www.weave.works/docs/net/latest/plugin/上的 Weave 文档。