1. 概述

本文将指导我们如何通过Docker引擎API从Docker容器内部获取信息。

2. Setup

我们将从多个方式连接到Docker引擎,并讨论在Linux系统上最常用的方法,这些方法在其他操作系统上同样适用。

然而,在进行设置时,我们需要非常小心,因为启用远程访问代表了一种安全风险。当容器能够访问引擎时,它会破坏与主机操作系统的隔离。

在本节中,我们将假定对主机具有完全控制权。

2.1 通过默认Unix套接字转发

默认情况下,Docker引擎使用位于主机操作系统上的/var/run/docker.sock路径下的Unix套接字:

$ ss -xan | grep var

u_str LISTEN 0      4096              /var/run/docker/libnetwork/dd677ae5f81a.sock 56352            * 0           
u_dgr UNCONN 0      0                                 /var/run/chrony/chronyd.sock 24398            * 0           
u_str LISTEN 0      4096                                      /var/run/nscd/socket 23131            * 0           
u_str LISTEN 0      4096                              /var/run/docker/metrics.sock 42876            * 0           
u_str LISTEN 0      4096                                      /var/run/docker.sock 53704            * 0    
...       

通过这种方式,我们可以严格控制哪些容器可以访问API。这就是Docker CLI在幕后的工作方式。

让我们启动名为alpine的Docker容器,并使用\-v标志将此路径挂载:

$ docker run -it -v /var/run/docker.sock:/var/run/docker.sock alpine

(alpine) $

接下来,让我们在容器中安装一些实用工具:

(alpine) $ apk add curl && apk add jq

fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz
(1/4) Installing ca-certificates (20191127-r2)
(2/4) Installing nghttp2-libs (1.40.0-r1)
...

现在让我们使用带有--unix-socket标志的curlJq来获取并过滤一些容器数据:

(alpine) $ curl -s --unix-socket /var/run/docker.sock http://dummy/containers/json | jq '.'

[
  {
    "Id": "483c5d4aa0280ca35f0dbca59b5d2381ad1aa455ebe0cf0ca604900b47210490",
    "Names": [
      "/wizardly_chatelet"
    ],
    "Image": "alpine",
    "ImageID": "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a",
    "Command": "/bin/sh",
    "Created": 1595882408,
    "Ports": [],
...

在这里,我们对/containers/json端点执行GET操作并获取当前运行的容器。然后我们使用jq美化输出。

稍后我们会详细介绍引擎API的细节。

2.2 启用TCP远程访问

我们还可以通过TCP套接字启用远程访问。

对于使用systemd的Linux发行版,我们需要自定义Docker服务单元。对于其他Linux发行版,我们需要自定义通常位于/etc/dockerdaemon.json

我们只需处理第一种设置,因为大多数步骤相似。

默认的Docker设置包括一个桥接网络。这是所有容器连接的地方,除非另有指定。

因为我们只允许容器访问引擎API,首先我们需要识别它们的网络:

$ docker network ls

a3b64ea758e1        bridge              bridge              local
dfad5fbfc671        host                host                local
1ee855939a2a        none                null                local

让我们查看其详细信息:

$ docker network inspect a3b64ea758e1

[
    {
        "Name": "bridge",
        "Id": "a3b64ea758e1f02f4692fd5105d638c05c75d573301fd4c025f38d075ed2a158",
...
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
...

接下来,让我们查看Docker服务单元的位置:

$ systemctl status docker.service

docker.service - Docker Application Container Engine
     Loaded: loaded (/usr/lib/systemd/system/docker.service; disabled; vendor preset: disabled)
...
     CGroup: /system.slice/docker.service
             ├─6425 /usr/bin/dockerd --add-runtime oci=/usr/sbin/docker-runc
             └─6452 docker-containerd --config /var/run/docker/containerd/containerd.toml --log-level warn

现在让我们看看服务单元的定义:

$ cat /usr/lib/systemd/system/docker.service

[Unit]
Description=Docker Application Container Engine
Documentation=http://docs.docker.com
After=network.target lvm2-monitor.service SuSEfirewall2.service

[Service]
EnvironmentFile=/etc/sysconfig/docker
...
Type=notify
ExecStart=/usr/bin/dockerd --add-runtime oci=/usr/sbin/docker-runc $DOCKER_NETWORK_OPTIONS $DOCKER_OPTS
ExecReload=/bin/kill -s HUP $MAINPID
...

ExecStart属性定义了由systemddockerd可执行文件)运行的命令。我们在其中传递了\-H标志,并指定了相应的网络和监听端口

我们可以直接修改这个服务单元(不推荐),但让我们使用$DOCKER_OPTS变量(在EnvironmentFile=/etc/sysconfig/docker中定义):

$ cat /etc/sysconfig/docker 

## Path           : System/Management
## Description    : Extra cli switches for docker daemon
## Type           : string
## Default        : ""
## ServiceRestart : docker
#
DOCKER_OPTS="-H unix:///var/run/docker.sock -H tcp://172.17.0.1:2375"

在这里,我们使用桥接网络的网关地址作为绑定地址这对应于主机上的docker0接口

$ ip address show dev docker0

3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:6c:7d:9c:8d brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:6cff:fe7d:9c8d/64 scope link 
       valid_lft forever preferred_lft forever

我们还启用了本地Unix套接字,以便Docker CLI仍可以在主机上正常工作。

最后一步是我们需要做的。让我们允许我们的容器包到达主机

$ iptables -I INPUT -i docker0 -j ACCEPT

在这里,我们将Linux防火墙设置为接受通过docker0接口的所有包。

现在,让我们重启Docker服务:

$ systemctl restart docker.service
$ systemctl status docker.service
 docker.service - Docker Application Container Engine
     Loaded: loaded (/usr/lib/systemd/system/docker.service; disabled; vendor preset: disabled)
...
     CGroup: /system.slice/docker.service
             ├─8110 /usr/bin/dockerd --add-runtime oci=/usr/sbin/docker-runc -H unix:///var/run/docker.sock -H tcp://172.17.0.1:2375
             └─8137 docker-containerd --config /var/run/docker/containerd/containerd.toml --log-level wa

再次运行alpine容器:

(alpine) $ curl -s http://172.17.0.1:2375/containers/json | jq '.'

[
  {
    "Id": "45f13902b710f7a5f324a7d4ec7f9b934057da4887650dc8fb4391c1d98f051c",
    "Names": [
      "/unruffled_cray"
    ],
    "Image": "alpine",
    "ImageID": "sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e",
    "Command": "/bin/sh",
    "Created": 1596046207,
    "Ports": [],
...

意识到所有连接到桥接网络的容器都可以访问守护进程API

此外,我们的TCP连接没有加密

4. 结论

在本文中,我们学习了如何使用Docker引擎远程API。

我们首先设置了远程访问,既可以来自UNIX套接字或TCP,然后展示了如何使用远程API。

通过这种方式,我们可以获取容器的详细信息、监听Docker守护进程事件、创建和管理容器以及探索日志输出等操作。