«1. Обзор

В этом руководстве мы рассмотрим, как получить доступ к информации о контейнере Docker изнутри контейнера с помощью API-интерфейса Docker Engine.

2. Настройка

Мы можем подключиться к движку Docker несколькими способами. Мы рассмотрим наиболее полезные из них в Linux, но они также работают и в других операционных системах.

Однако мы должны быть очень осторожны, поскольку включение удаленного доступа представляет угрозу безопасности. Когда контейнер может получить доступ к движку, он нарушает изоляцию от основной операционной системы.

В части настройки мы будем считать, что у нас есть полный контроль над хостом.

2.1. Перенаправление сокета Unix по умолчанию

По умолчанию движок Docker использует сокет Unix, смонтированный в /var/run/docker.sock на хост-ОС:

$ 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)
...

Теперь давайте используем curl с параметром – флаг unix-socket и Jq для извлечения и фильтрации некоторых данных контейнера:

(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": [],
...

Здесь мы выполняем GET на конечной точке /containers/json и получаем текущие запущенные контейнеры. Затем мы улучшаем вывод с помощью jq.

Мы рассмотрим детали API движка чуть позже.

2.2. Включение удаленного доступа TCP

Мы также можем включить удаленный доступ с помощью сокета TCP.

Для дистрибутивов Linux, которые поставляются с systemd, нам нужно настроить сервисный модуль Docker. Для других дистрибутивов Linux нам нужно настроить файл daemon.json, обычно расположенный в /etc/docker.

Мы рассмотрим только первый тип настройки, так как большинство шагов похожи.

Настройка 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 определяет, какая команда выполняется systemd (исполняемый файл dockerd). Мы передаем ему флаг -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-соединение не зашифровано.

3. API Docker Engine

Теперь, когда мы настроили удаленный доступ, давайте взглянем на API.

Мы рассмотрим лишь несколько интересных вариантов, но всегда можно просмотреть полную документацию, чтобы узнать больше.

Давайте получим некоторую информацию о нашем контейнере:

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

{
  "Id": "45f13902b710f7a5f324a7d4ec7f9b934057da4887650dc8fb4391c1d98f051c",
  "Created": "2020-07-29T18:10:07.261589135Z",
  "Path": "/bin/sh",
  "Args": [],
  "State": {
    "Status": "running",
...

Здесь мы используем URL-адрес /containers/{container-id}/json для получения подробной информации о нашем контейнере.

В этом случае мы запускаем команду hostname, чтобы получить идентификатор контейнера.

Далее давайте послушаем события демона Docker:

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

Теперь в другом терминале запустим контейнер hello-world:

$ docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.
...

Вернувшись в наш контейнер alpine, мы получим кучу события:

{
  "status": "create",
  "id": "abf881cbecfc0b022a3c1a6908559bb27406d0338a917fc91a77200d52a2553c",
  "from": "hello-world",
  "Type": "container",
  "Action": "create",
...
}
{
  "status": "attach",
  "id": "abf881cbecfc0b022a3c1a6908559bb27406d0338a917fc91a77200d52a2553c",
  "from": "hello-world",
  "Type": "container",
  "Action": "attach",
...

До сих пор мы занимались ненавязчивыми вещами. Время немного встряхнуться.

Давайте создадим и запустим контейнер. Во-первых, мы определяем его манифест:

(alpine) $ cat > create.json << EOF
{
  "Image": "hello-world",
  "Cmd": ["/hello"]
}
EOF

Теперь давайте вызовем конечную точку /containers/create, используя манифест:

(alpine) $ curl -X POST -H "Content-Type: application/json" -d @create.json http://172.17.0.1:2375/containers/create

{"Id":"f96a6360ad8e36271cc75a3cff05348761569cf2f089bbb30d826bd1e2d52f59","Warnings":[]}

«

(alpine) $ curl -X POST http://172.17.0.1:2375/containers/f96a6360ad8e36271cc75a3cff05348761569cf2f089bbb30d826bd1e2d52f59/start

Затем мы используем идентификатор для запуска контейнера:

(alpine) $ curl http://172.17.0.1:2375/containers/f96a6360ad8e36271cc75a3cff05348761569cf2f089bbb30d826bd1e2d52f59/logs?stdout=true --output -

Hello from Docker!
KThis message shows that your installation appears to be working correctly.

;To generate this message, Docker took the following steps:
3 1. The Docker client contacted the Docker daemon.
...

Наконец, мы можем изучить журналы:

Обратите внимание, что в начале каждой строки появляются странные символы. Это происходит потому, что поток, по которому передаются журналы, мультиплексируется, чтобы различать stderr и stdout.

В результате вывод требует дальнейшей обработки.

(alpine) $ cat create.json

{
  "Tty":true,	
  "Image": "hello-world",
  "Cmd": ["/hello"]
}

Этого можно избежать, просто включив параметр TTY при создании контейнера:

4. Заключение

В этом руководстве мы узнали, как использовать удаленный API Docker Engine.