Структура сети в Docker (Часть 2.2)

Структура сети в Docker (Часть 2.2)

ЛЕТОПИСЕЦ КИТЕЖ-ГРАДА

Введение: Одна из причин, по которой контейнеры и службы Docker настолько эффективны, заключается в том, что вы можете как объединять их вместе, так и подключать к рабочим ресурсам, отличным от Docker. Контейнерам и службам Docker даже не нужно знать, развернуты ли они в Docker. В этой статье будут определены основные сетевые вариации, связанные с Docker, которые в дальнейшем поспособствуют развертыванию желаемых приложений.

Работа сетевых драйверов: Контейнеры Docker взаимодействуют друг с другом, а также с узлом Docker, для обмена данными. Это стало возможным благодаря сетевому взаимодействию в Docker. Фактически, Docker создает Ethernet-адаптер, когда его устанавливают на конечном узле. Итак, когда на хосте, где запущен Docker, выполняется такая команда, как ip address, в качестве результата мы наблюдаем различного рода сведения о сети Docker. Пример исполняемой команды расположен ниже:

ip address

2: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
   link/ether 02:42:c1:58:b5:e8 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:c1ff:fe58:b5e8/64 scope link
      valid_lft forever preferred_lft forever

Как уже упоминалось ранее, Docker поставляется вместе с важным набором настроек по умолчанию, которые позволяют контейнерам обмениваться данными по сети. С точки зрения самой сети Docker связывает любой созданный контейнер с интерфейсом docker0, который выступает в роли моста. Давайте рассмотрим как подключенный контейнер будет работать в режиме моста, являющийся режимом по умолчанию, и проговорим, как обрабатывается сетевой трафик, исходящий и предназначенный для созданного контейнера. Итак, после того как Docker был установлен и запущен, можно обнаружить интерфейс docker0, у которого по умолчанию сеть выражена в виде 172.17.0.1/16. Любой контейнер, запущенный в сети по умолчанию, будет использовать адрес из этой подсети. Давайте в этом убедимся. Запустим самый обыкновенный контейнер на базе Ubuntu:

docker run -it --rm ubuntu /bin/bash

Как вам уже должно быть известно, данная команда загружает образ Docker на базе Ubuntu (если он изначально отсутствует в системе) и сразу же запускает его в виде контейнера, предварительно передав соответствующие параметры для запуска. Проще говоря, мы сразу же окажемся внутри контейнера. Параметр --rm ранее мы не рассматривали, но с помощью него контейнер автоматически будет удален после выхода из окружения контейнера. Так вот, попав внутрь контейнера можно просмотреть его IP-адрес, однако по умолчанию система максимально урезана, поэтому, чтобы вывести даже такую простую информацию, необходимо установить соответствующий пакет:

apt update
apt install -y iproute

После того как репозитории были обновлены, а пакеты – установлены, выполним следующую команду:

ip address
14: eth0@if15: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
   link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff link-netnsid 0
   inet 172.17.0.4/16 brd 172.17.255.255 scope global eth0
      valid_lft forever preferred_lft forever

В данном случае видно, что контейнеру назначен IP-адрес из той же подсети, в которой находится интерфейс docker0 на хостовой системе. Принцип здесь такой же, каким он был представлен в прошлой части, когда рассматривалась работа сети между контейнерами. Если посмотреть на вывод ip route, можно обнаружить следующее:

ip route
default via 172.17.0.1 dev eth0 
172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.4

Данный вывод означает, что весь трафик идет через шлюз сети 172.17.0.1. Учитывая, что сеть, в которой находится контейнер, была создана Docker, можно с уверенностью предположить, что остальная часть сети не знает об этом. То бишь, внешняя сеть ничего не знает о сети 172.17.0.0/16, поскольку она является локальной для узла Docker. При этом кажется любопытным, что контейнер может получить доступ к ресурсам, которые находятся за пределами docker0. В данном случае Docker делает это, скрывая IP-адрес контейнера за IP-адресом узла интерфейса Docker. Поскольку трафик, поступающий из контейнера, физической сетью распознается как IP-адрес узла Docker, прочие сетевые ресурсы знают, как вернуть такой трафик обратно. Для выполнения данного процесса Docker использует инфраструктуру сетевого фильтра Linux. Эти правила можно просмотреть с помощью инструмента командной строки Netfilter Iptables:

iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target    prot opt source              destination
DOCKER    all -- anywhere            anywhere            ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target    prot opt source              destination

Chain OUTPUT (policy ACCEPT)
target    prot opt source              destination
DOCKER    all -- anywhere           !127.0.0.0/8         ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target    prot opt source              destination
MASQUERADE all -- 172.17.0.0/16       anywhere

Пример, представленный выше, описан по самому распространенному сценарию, когда контейнер представляет собой отдельную службу, для доступа к физической сети. Однако, как быть, если есть необходимость предоставить доступ к сервису из одного контейнера другому, не предоставляя при этом доступ к сети узла Docker? Рассмотрим пример, как сопоставить службы между двумя контейнерами, работающими на одном узле Docker. Сопоставление службы из одного контейнера в другой иногда называют режимом сопоставленного контейнера. Этот режим позволяет запускать контейнер, использующий существующую или первичную сетевую конфигурацию другого контейнера. То бишь, сопоставленный контейнер будет использовать тот же IP-адрес и конфигурацию порта, что и основной контейнер. Для примера рассмотрим запуск следующего контейнера:

docker run --detach --publish 80 --name first-nginx nginx

Пока что всё выглядит стандартно. Был запущен контейнер с Nginx внутри, который работает в паре с интерфейсом docker0. Это означает, что контейнер находится в той же сети. Теперь запустим ещё один контейнер, используя образ Ubuntu, но уже с небольшим дополнением:

docker run -dit --name second-nginx --net=container:first-nginx ubuntu

Теперь, если мы выполним представленные ниже две команды, то увидим вывод сетевой информации по каждому из контейнеров:

docker exec first-nginx ip addr
22: eth0@if23: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
   link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff link-netnsid 0
   inet 172.17.0.4/16 brd 172.17.255.255 scope global eth0
      valid_lft forever preferred_lft forever
docker exec second-nginx ip addr
22: eth0@if23: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
   link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff link-netnsid 0
   inet 172.17.0.4/16 brd 172.17.255.255 scope global eth0
      valid_lft forever preferred_lft forever

Как видите, контейнеры разные, но сетевая информация об интерфейсах одинаковая. Если команду exec выполнить не удалось, необходимо выполнить эти две команды внутри контейнера, которые были показаны ранее:

apt update
apt install -y iproute

Стоит также отметить и присущее данному примеру ограничение. Контейнер, который был присоединен к сети другого контейнера, собственные порты выставить наружу не может. Таким образом, хотя это означает, что мы не можем опубликовать порты сопоставленных контейнеров на узле, мы можем использовать их локально. Возвращаясь к примеру, это означает, что мы не можем опубликовать 80 порт контейнера second-nginx на нашем узле, однако контейнер с именем first-nginx может локально использовать неопубликованные службы контейнера second-nginx. Итак, мы рассмотрели процесс работы режима моста и то, как он влияет на контейнеры. Обратимся к ещё одному режиму, который ранее был упомянут как режим хоста. Данный режим привязывает контейнеры непосредственно к интерфейсу самого узла, на котором запущен Docker. Развертывание контейнеров в этом режиме довольно простое с точки зрения Docker. Это не только устраняет необходимость во входящем и исходящем NAT, но и ограничивает возможности развертывания самих контейнеров. Поскольку контейнеры будут находиться в той же сетевой среде, что и физический узел, необходимо с осторожностью запускать их, поскольку порты могут пересекаться с портами самой системы. И это также означает, что при такой конфигурации контейнер будет иметь больший доступ к системе, а Docker не будет знать, какой порт использует запущенный вами контейнер. Развернем тестовый контейнер в режиме хоста, чтобы продемонстрировать более наглядно то, о чем идет речь:

docker run --detach --name nginx --network host nginx

Для достижения данного режима используется параметр --network, в который передается режим host, во время запуска контейнера. В этом случае можно увидеть, что без сопоставления портов мы по-прежнему можем получить доступ к службе, находящейся в контейнере. Docker просто привязывает контейнер к узлу, что означает, что любые контейнерные варианты автоматически сопоставляются с интерфейсами узла. Теперь попробуем запустить ещё один контейнер на базе Nginx:

docker run --detach --name onemorenginx --network host nginx

Docker без проблем позволяет нам создать такой контейнер, но если посмотреть в журнал логов, то мы обнаружим следующее:

docker logs onemorenginx
/docker-entrypoint.sh: Configuration complete; ready for start up
2022/03/29 20:08:55 [emerg] 1#1: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
2022/03/29 20:08:55 [emerg] 1#1: bind() to [::]:80 failed (98: Address already in use)

Когда узел имеет несколько сетевых интерфейсов, можно осуществлять привязку контейнеров к одному и тому же порту, но на разных интерфейсах. Опять же, поскольку это ответственность контейнера, Docker не имеет никакого представления о том, как это было сделано. Решение состоит в том, чтобы изменить способ привязки служб к интерфейсам, поскольку по умолчанию большинство служб привязываются ко всем интерфейсам (0.0.0.0) при запуске. Например, если выполнить следующую команду, можно увидеть, что контейнер Nginx привязан к 0.0.0.0:80 на хосте:

ss -ntpl
State          Recv-Q         Send-Q                    Local Address:Port                    Peer Address:Port                                                                                                                                  
LISTEN         0              511                             0.0.0.0:80                           0.0.0.0:*             users:(("nginx",pid=9975,fd=7),("nginx",pid=9974,fd=7),("nginx",pid=9973,fd=7)          

Таким образом, если назначить прослушивание контейнера на конкретный IP-адреса интерфейса, можно избежать ошибки, которая была представлена выше. Другие режимы рассматривать не будем, так как они достаточно специфичны. Теперь перейдем к базовому рассмотрению команд, связанных с сетью в Docker.

Подкоманды Docker Network: Итак, если выполнить представленную ниже команду, будет получен вывод, содержащий в себе ряд подкоманд для работы с сетью. Рассмотрим каждую из таких подкоманд более подробно:

docker network --help

Network List: И в первую очередь стоит обратить внимание на такую команду как list, которая выводит список всех существующих сетей в системе Docker:

docker network list
8b2b8f20606c  bridge       bridge   local
5428a2583873  host         host     local
af4b85c87138  none         null     local

Из интересных опций можно отметить --quiet, которая выведет только идентификаторы сетей и --filter, который отобразит только те сети, которые попадают под заданный критерий:

docker network list --quiet
docker network list --filter

Чтобы посмотреть все опции, присущие list, используйте следующую команду:

docker network list --help

Network Inspect: Ещё одна опция, предоставляющая информацию о сети, является командой инспектирования, которая выглядит следующим образом:

docker network inspect <name>

В данном случае в примере ниже представлен вывод сети Bridge:

[
   {
       "Name": "bridge",
       "Id": "8b2b8f20606cb93e2ab084b7ed9d70cda1d036baf1316b7bf143470d77a7c621",
       "Created": "2022-03-30T18:30:00.361657403+03:00",
       "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,
       "Attachable": false,
       "Ingress": false,
       "ConfigFrom": {
           "Network": ""
       },
       "ConfigOnly": false,
       "Containers": {},
       "Options": {
       },
       "Labels": {}
   }
]

Вывод команды docker network inspect показывает массу информации по конкретной сети. Среди приведенного выше можно отметить следующее:

Параметр driver: В данном случае сеть использует драйвер моста. Хотя это может показаться очевидным, важно отметить, что все сетевые функции, включая собственные функции, реализуются через драйверы.
Параметр subnet: В данном случае используется ожидаемая подсеть по умолчанию, она же 172.17.0.0/16.
Параметр bridge.default_bridge: В данном случае значение true означает, что Docker будет определять все контейнеры в этот драйвер, если не было указано иначе. То есть, если вы запустите контейнер без параметра --network в сочетании с указанием конкретной сети, контейнер по умолчанию будет использовать сеть моста.
Параметр bridge.host_binding_ipv4: В данном случае будет установлено значение 0.0.0.0. Как было упомянуто ранее, можно указать конкретный адрес в качестве опции Docker, для запускаемой службы.
Параметр bridge.name: В данном случае сеть представляет собой имя моста, которое определено как docker0.
Параметр driver.mtu: В данном случае значение MTU установлено в 1500, что является значением по умолчанию.

Точно также можно проинспектировать любую другую сеть. Чтобы посмотреть все опции, присущие inspect, используйте следующую команду:

docker network inspect --help

Network Create: Следующая опция в нашем списке является самой мощной по своей функциональности, поэтому ей мы уделим больше всего внимания. Речь пойдет о docker network create. Если вызвать параметр --help, то можно увидеть целый список дополнительных подкоманд:

docker network create --help

Рассмотрим некоторые параметры более подробно:

Параметр --attachable: Данный параметр используется, когда необходимо создать оверлейную сеть, которую смогут использовать службы, относящиеся к Docker Swarm или обычные контейнеры, для связи с другими контейнерами, работающими на других демонах Docker.
Параметр --aux-address: Данный параметр позволяет определить IP-адреса, которые Docker не должен назначать контейнерам при их создании. Это эквивалент резервирования IP-адресов в DHCP.
Параметр --driver: Данный параметр определяет, какой драйвер будет использоваться сетью. Среди встроенных драйверов можно отметить следующие – bridge, ipvlan, macvlan, overlay. Но также можно использовать сторонние драйверы.
Параметр --gateway: Данный параметр определяет шлюз для сети. Если он не был указан, Docker будет считать, что это первый доступный IP-адрес в подсети.
Параметр --internal: Данный параметр позволяет изолировать сеть.
Параметр --ip-range: Данный параметр позволяет указать конкретный диапазон для использования адресации в контейнерах.
Параметр --IPAM-driver: Данный параметр позволяет использовать сторонние сетевые драйверы IPAM.
Параметр --IPAM-opt: Данный параметр позволяет задать соответствующую опцию для передачи драйверу IPAM.
Параметр --ipv6: Данный параметр включает сеть IPv6.
Параметр --label: Данный параметр позволяет указать дополнительную информацию о сети, которая будет храниться в виде метаданных.
Параметр --opt: Данный параметр указывает опции, которые можно передать сетевому драйверу.
Параметр --subnet: Данный параметр определяет подсеть, связанную с типом сети, которая будет создана.

Теперь перейдем к непосредственному применению команды create в сочетании с некоторыми опциями, которые были описаны выше. Так, например, чтобы создать сеть без указания каких-либо опций, нужно выполнить эту команду:

docker network create z-network

Чтобы убедиться в том, что сеть была создана, можно выполнить ранее продемонстрированную команду:

docker network ls
NETWORK ID    NAME         DRIVER   SCOPE
8b2b8f20606c  bridge       bridge   local
5428a2583873  host         host     local
af4b85c87138  none         null     local
d6f76c9b9048  z-network    bridge   local

Таким образом, можно сделать вывод о том, что, указывая тот же драйвер Bridge, можно создать столько дополнительных сетей, сколько необходимо, с единственным реальным ограничением, заключающимся в том, что вы должны использовать уникальную IP-адресацию. Расширим базовое представление продемонстрированной ранее команды и добавим несколько опций:

docker network create --driver bridge --subnet=10.15.20.0/24 --gateway=10.15.20.1 --aux-address 1=10.15.20.2 --aux-address 2=10.15.20.3 --opt com.docker.network.bridge.host_binding_ipv4=192.168.88.240 --opt com.docker.network.bridge.name=linuxbridge1 new-z-network

Команда docker network create, помимо непосредственного создания самой сети, также заключает в себе явное определение драйвера, являющийся драйвером Bridge, подсетью, которая выражена в виде 10.15.20.0/24, шлюзом, несколькими арендованными адресами, а также несколькими опциями. Если остальные параметры очевидны, то вот с опциями немного непонятно. Про некоторые из них можно почитать в официальной документации. Далее рассмотрим ещё один пример создания сети:

docker network create --subnet=192.168.50.0/24 --ip-range=192.168.50.128/25 --opt com.docker.network.bridge.enable_ip_masquearde=false new-x-network

Данное создание сети можно определить с такими характеристиками сети, как драйвер в лице Bridge и подсеть, выраженная в виде 192.168.50.0/24, IP-интерфейс шлюза 192.168.50.1, а диапазон контейнерной сети установлен в пределах 192.168.50.128/25 и MASQUERADE отключен. По аналогии с примером выше можно также провести инспектирование. В целом создание сети в Docker не вызывает каких-либо сложностей. Главное правильно оперировать параметрами, которые доступны в рамках выполнения данной команды. Давайте теперь рассмотрим ещё один пример, в котором мы создадим изолированную сеть:

docker network create --internal --opt com.docker.network.bridge.name=int-bridge new-local-network

Выполнив команду проверим, что запрограммировал Docker в Netfilter для созданной сети. Сделать это можно следующим образом:

sudo iptables-save
-A DOCKER-ISOLATION-STAGE-1 ! -s 172.21.0.0/16 -o int-bridge -j DROP
-A DOCKER-ISOLATION-STAGE-1 ! -d 172.21.0.0/16 -i int-bridge -j DROP

Видно, что были добавлены два правила. В первом говорится, что любой трафик, исходящий не из подсети моста и покидающий интерфейс моста, должен быть отброшен. Во втором происходит поиск трафика который не имеет пункта назначения в подсети моста и при этом имеет входной интерфейс моста. Когда трафик попытается выйти за пределы моста во внешнюю сеть, он будет соответствовать второй части правила, поскольку трафик поступает на интерфейс int-bridge.

Network Remove: Очень простая опция, суть которой заключается в том, чтобы удалять, в случае необходимости, ранее созданные сети Docker. Сделать это можно с помощью следующей команды:

docker network remove <name>

Никакими дополнительными опциями не обладает.

Network Prune: Ещё одна опция, которая похожа по своей функциональности на remove. Однако prune удаляет только неиспользуемые сети. То бишь те, которые ранее были созданы, но теперь более никак не задействованы каким-либо контейнером. Команда будет выглядеть следующим образом:

docker network prune <name>

Есть также параметр --force, который удаляет сети без подтверждения со стороны пользователя:

docker network prune --force

Никакими дополнительными опциями не обладает.

Network Connect: Эта опция позволяет подключать тот или иной контейнер к уже существующей сети. То бишь, является дополнительным инструментом в сочетании с опцией create. В самом простом варианте команда будет выглядеть следующим образом:

docker network connect mynetwork nginx

Давайте теперь рассмотрим этот самый просто пример. Создадим свою сеть, а также контейнер, который будет работать в сети Bridge:

docker network create mynetwork
docker run -d --name nginx --network bridge nginx

Если мы запросим информацию о сети контейнера, то получим следующее:

19: eth0@if20: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
   link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff link-netnsid 0
   inet 172.17.0.4/16 brd 172.17.255.255 scope global eth0
      valid_lft forever preferred_lft forever

Теперь подключим наш контейнер к созданной ранее сети:

docker network connect mynetwor nginx

И посмотрим как изменился вывод о сети:

19: eth0@if20: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
   link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff link-netnsid 0
   inet 172.17.0.4/16 brd 172.17.255.255 scope global eth0
      valid_lft forever preferred_lft forever
22: eth1@if23: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
   link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
   inet 172.18.0.2/16 brd 172.18.255.255 scope global eth1
      valid_lft forever preferred_lft forever

Видно, что теперь наш контейнер работает в двух сетях одновременно. Нам это не нужно и чтобы от этого избавиться, необходимо применить опцию disconnect, о которой и пойдет речь дальше. Чтобы посмотреть все опции, присущие connect, используйте следующую команду:

docker network connect --help

Network Disconnect: Опция по своей функциональности обратно пропорциональна опции connect. Отключает контейнер от той сети, в которой он сейчас находится. Выглядеть это будет следующим образом:

docker network disconnect bridge nginx

Теперь, если посмотреть на вывод сети, то мы получим такой результат:

22: eth1@if23: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
   link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
   inet 172.18.0.2/16 brd 172.18.255.255 scope global eth1
      valid_lft forever preferred_lft forever

В данном случае видно, что контейнер был исключен из сети моста. Чтобы посмотреть все опции, присущие disconnect, используйте следующую команду:

docker network disconnect --help

Публикация портов: Как уже стало известно, контейнерные сети в Docker представляют собой простое подключение и маршрутизацию между контейнерами. Для подключения служб, работающих в этих контейнерах, к клиентам внешней сети требуется дополнительный шаг. То бишь, необходимо указать порт TCP или UDP на интерфейсе хоста, а также целевой контейнер и порт контейнера, аналогично тому как это выполняется в случае с NAT в вашей домашней сети, например. NodePort Publication – термин, который используется для того, чтобы сопоставить контейнеры Docker с внешней сетью хоста. Порт настраивается во время запуска или создания контейнера и не может быть изменен после, поэтому единственно верным решением является пересоздание контейнера, если вы хотите изменить порт. Сделать это можно с помощью параметра --publish при выполнении команд docker run / create. Команда будет выглядеть следующим образом:

docker run --detach --publish 80:80 --name ng nginx:latest

После выполнения данной команды будет создан контейнер Nginx, чей порт внутри контейнера будет равен значению 80, но также он будет равен этому значению и снаружи. Таким образом, если мы выполним следующую команду, то получим вот такой результат:

curl http://localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Без выполнения параметра --publish 80:80 порт контейнера Nginx был бы доступен только непосредственно из самого контейнера или любого другого контейнера в этой же сети. Давайте удалим созданный ранее контейнер и запустим его заново, только уже вот в таком виде:

docker run --detach --publish-all --name ng nginx

Теперь наш контейнер доступен из сети хоста, однако какой порт – неясно. Давайте убедимся в этом, выполнив следующую команду:

curl http://localhost

curl: (7) Failed to connect to localhost port 80: Connection refused

Чтобы узнать порт, необходимо выполнить следующую команду:

docker port ng
80/tcp -> 0.0.0.0:49154
80/tcp -> :::49154

В данном случае мы указали имя контейнера, а полученный вывод говорит нам о том, что для того, чтобы обратиться к 80 порту внутри контейнера, необходимо обратиться к порту 49154 в сети хоста. Убедимся в этом:

curl http://localhost:49154

Опять удалим наш контейнер и рассмотрим ещё один сценарий. Сейчас контейнер nginx по умолчанию работает на порту 80 по протоколу TCP, но как быть если мы хотим или нам нужно использовать другой транспортный протокол? Здесь нам сможет помочь параметр --expose. Чаще всего он встречается при написании Dockerfile, однако об этом пойдет речь сильно позже. Пока что, используя данный параметр вот в таком примере, можно получить следующий результат:

docker run --detach --expose=80/udp --publish-all --name ng nginx
80/tcp -> 0.0.0.0:49156
80/tcp -> :::49156
80/udp -> 0.0.0.0:49154
80/udp -> :::49154

Разумеется, в случае с HTTP, если мы обратимся на 80 порт по UDP, то ничего не выйдет, так как HTTP не предназначен для этого, но посыл, я думаю, ясен. Из интересного можно отметить ещё несколько тезисов:

Можно указать целую серию портов, которые будут проброшены наружу. Выглядеть это будет в стиле --publish 8080:80 --publish 9090:443 и т.д.
Если при использовании параметра --publish мы указываем только одно значение, без доветочия (:), то в первую очередь мы определяем порт внутри контейнера.
Помимо самих портов можно определять также и IP-адреса при сопоставлении. В случаях, когда нужен больший контроль над используемыми портами и интерфейсами, можно напрямую опубликовать порты при запуске контейнера. Флаг -p может принимать несколько различных форм с таким синтаксисом: –p <IP-интерфейс хоста>:<порт хоста>:<порт контейнера>, но единственным обязательным параметром является порт контейнера. Все опубликованные порты, представленные ранее, использовали IP-адрес назначения (0.0.0.0), что означает, что они привязаны ко всем IP-интерфейсам хоста Docker.

Принцип работы DNS в Docker: По умолчанию Docker предоставляет своим контейнерам средства для базового разрешения имен. Это значит, что он передает параметры разрешения имен с узла Docker непосредственно в контейнер. В результате чего созданный контейнер может выполнять разрешение адресов также, как это делает сам хост. Механика, используемая Docker для разрешения имен в контейнере, достаточно проста. Создадим контейнер на базе Ubuntu:

docker run -dit --name ubuntu-dns ubuntu
docker exec -it ubuntu /bin/bash

Далее выполним несколько команд уже внутри контейнера для того, чтобы поставить всё необходимое программное обеспечение:

apt update
apt install -y dnsutils host

Теперь можно выйти из контейнера и применить одну из следующих команд:

docker exec ubuntu host www.google.com
docker exec ubuntu dig www.google.com
docker exec ubuntu nslookup www.google.com

Команды имеют разное назначение и, соответственно, разный вывод, но все они демонстрируют то, что, находясь в контейнере, есть возможность сопоставить имя с конкретным IP-адресом. Также можно посмотреть, какие DNS-сервера записаны в файл resolv.conf по умолчанию:

docker exec ubuntu-dns cat /etc/resolv.conf

Метод, который Docker использует для разрешения имен, зачастую работает очень хорошо, потому что Docker копирует содержимое файла resolv.conf с хоста в каждый созданный контейнер. Наряду с настройкой сервера имен этот файл также включает определения доменов поиска DNS. Однако иногда может возникнуть случай, когда нужно, чтобы Docker предоставил контейнеру DNS-сервер, отличный от того, на использование которого настроен узел Docker. В таких случаях Docker может предложить несколько вариантов. Можно сообщить о том, чтобы Docker предоставил другой DNS-сервер для всех контейнеров. Также можно вручную переопределить данный параметр во время запуска контейнера, указав заведомо нужный DNS-сервер в качестве параметра подкоманды docker run / start. Посмотрим что мы имеем на текущий момент:

docker inspect ubuntu | grep Dns
           "Dns": [],
           "DnsOptions": [],
           "DnsSearch": [],

Поскольку мы используем конфигурацию по умолчанию, нет необходимости настраивать что-то конкретное в контейнере в отношении DNS-сервера или поискового домена. При каждом запуске контейнера Docker будет применять настройки файла resolv.conf хоста к файлам конфигурации контейнера с DNS. Но, если мы хотим, чтобы Docker предоставлял контейнеру другой DNS-сервер, нужно применить ряд параметров с помощью соответствующей команды. В данном случае нас интересует два параметра:

--dns=: Уазывает адрес DNS-сервера, который Docker должен предоставить контейнеру.
--dns-search=: Указывает домен поиска DNS, который Docker должен предоставить контейнеру.

Параметры, определенные во время запуска контейнера, всегда имеют приоритет. Если параметры не были определены, Docker проверяет, настроены ли они на уровне службы. Если настроек нет, он возвращается к методу по умолчанию, полагаясь на настройки DNS хоста. Например, мы можем запустить контейнер и назначить ему различные параметры, связанные с DNS:

docker run -dit --name ubuntu --dns=9.9.9.9 --dns-search=mydomain.local ubuntu
docker inspect ubuntu
"Dns": [
               "9.9.9.9"
           ],
           "DnsOptions": [],
           "DnsSearch": [
               "mydomain.local"
           ],

Эти же параметры можно определить в файле /usr/lib/systemd/system/docker.service через сервис Docker. Выглядеть это будет следующим образом:

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

После изменения необходимо перезапустить демон системы:

systemctl daemon-reload

Ещё одной занимательной функцией является связывание контейнеров, которое позволяет одному контейнеру легко взаимодействовать с другим контейнером на том же хосте по имени. Помимо базового разрешения имен также определяются средства для просмотра того, какие службы предоставляет связанный контейнер. На самом деле связывание контейнеров имеет очень мало общего с сетью контейнеров. В режиме по умолчанию связывание контейнеров позволяет одному контейнеру разрешать имя другого. Например, давайте запустим два контейнера для тестов:

docker run -dit --name ubuntu ubuntu
docker run -dit --name=nginx --link=ubuntu nginx

Обратите внимание, что при запуске второго контейнера мы используем новый параметр, известный как --link, который ссылался на контейнер Ubuntu. Можно было бы сказать, что они связаны, но это не так. Правильнее было бы сказать, что теперь Nginx знает об Ubuntu. Давайте подключимся к Nginx:

docker exec -it nginx /bin/bash

И выполним команду ping:

ping ubuntu
PING ubuntu (172.17.0.4) 56(84) bytes of data.
64 bytes from ubuntu (172.17.0.4): icmp_seq=1 ttl=64 time=0.105 ms
64 bytes from ubuntu (172.17.0.4): icmp_seq=2 ttl=64 time=0.070 ms

Теперь контейнер Nginx может обращаться к контейнеру Ubuntu по имени. Это связано с тем, что в файл hosts контейнера Nginx была добавлена новая запись:

cat /etc/hosts
127.0.0.1   localhost
::1   localhost ip6-localhost ip6-loopback
172.17.0.4   ubuntu 5af84b11d097
172.17.0.5   8a42106066e1

До внедрения встроенного DNS единственным способом присвоения контейнеру псевдонима с другим именем было использование ссылок. Этот метод по-прежнему может быть использован, однако что, если есть необходимость использовать псевдоним с большей областью действия, который может разрешить любой контейнер, подключенный к данной сети? Встроенный DNS-сервер предлагает так называемые сетевые псевдонимы, которые могут быть разрешены в пределах определяемой пользователем сети. Если мы создадим определяемую пользователем сеть и укажем её как часть конфигурации контейнера, команда будет выполнена успешно:

docker rm ubuntu nginx -f
docker network create --driver bridge alias
docker run -ditP --name nginx --net alias web-server nginx

После создания псевдонима его можно наблюдать как часть конфигурации конкретного контейнера. Например, если мы теперь проверим контейнер Nginx, то увидим определенный псевдоним в его сетевой конфигурации:

docker inspect nginx
           "Networks": {
               "alias": {
                   "Aliases": [
                       "web-server",
                       "571c22b8d125"

Теперь запустим ещё один контейнер и посмотрим, можно ли обратиться к нему по этому имени:

docker run -ditP --name ubuntu --net alias ubuntu
docker exec -it ubuntu ping web-server
PING web-server (172.18.0.2) 56(84) bytes of data.
64 bytes from nginx.alias (172.18.0.2): icmp_seq=1 ttl=64 time=0.115 ms
64 bytes from nginx.alias (172.18.0.2): icmp_seq=2 ttl=64 time=0.074 ms

Здесь можно отметить несколько моментов. Во-первых, метод определения псевдонимов отличается от метода связывания, продемонстрированный ранее. С помощью линкования исходный контейнер сообщил, что он хочет, чтобы целевой контейнер работал с указанным псевдонимом. Во-вторых, это может сработать только потому, что контейнер Ubuntu находится в той же сети, что и контейнер Nginx.

Послесловие: На этом обзор практической составляющей по использованию сети в Docker завершен. В следующей части речь уже пойдет о томах.

Report Page