С момента появления контейнерной виртуализации в Solaris 10 прошло уже почти 20 лет. И за это время поддержку контейнеров не только добавили в другие операционные системы, но и их использование стало весьма популярным механизмом. В операционных системах, базирующихся на ядре Linux, для этого в ядро был добавлен функционал под названием linux namespaces. А, в свою очередь, для управления namespaces, довольно часто используется программное обеспечение Docker. Углубляться в преимущества контейнерной виртуализации вообще, и docker контейреров в частности не будем, а обратим своё внимание на конкретную задачу, которая может возникнуть при работе с docker контейнерами.
Не смотря на то, что docker контейнер формально представляет собой некий чёрный ящик, иногда возникает необходимость попасть внутрь него и выполнить в контейнере что-то выходящее за рамки его стандартного поведения. Например, это может быть попытка диагностики проблемы в работе контейнера.
Практически все пользователи docker знают про существование команды docker exec
, для использования которой необходимо знать ID контейнера или его имя. Если по какой-то причине они неизвестны, то получить эту информацию можно используя команду docker ps
:
root@docker:~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
32de74bc47f9 mongo:4.4 "docker-entrypoint.s…" 12 minutes ago Up 11 minutes 27017/tcp chat-mongo-1
После чего уже можно выполнять команды в контейнере, например:
root@docker:~# docker exec 32de74bc47f9 cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0
Или, добавив ключи -t
(выделить псевдо-TTY) и -i
(не переназначать стандартный поток ввода на /dev/null
), можно запустить интерактивный shell внутри контейнера:
root@docker:~# docker exec -ti chat-mongo-1 bash
root@32de74bc47f9:/#
Если для запуска контейнеров используется docker compose, то, находясь в каталоге с файлом docker-compose.yml
, зная указанное в этом файле имя контейнера, можно воспользоваться конструкцией вида:
root@docker:/opt/chat# docker compose exec -ti mongo bash
root@32de74bc47f9:/#
Но для всех перечисленных способах запуска команды запускаемая команда должна существовать внутри контейнера. А, следуя идеологии docker, в образах контейнеров установлены только необходимые для работы соответствующего сервиса пакеты, и, следовательно, могут отсутствовать, например, такие полезные сетевые команды как ss(8)
, ip(8)
, iptables(8)
. И такая попытка с треском провалится:
root@docker:/opt/chat# docker compose exec mongo ss -tulpan
OCI runtime exec failed: exec failed: unable to start container process: exec: "ss": executable file not found in $PATH: unknown
Помочь в решении этой задачи может утилита nsenter(1)
, входящая в стандартный пакет util-linux
. Для запускаемой команды данная утилита позволяет сменить только указанные пространства имён указанием таких ключей как:
-m
- mount namespace
-n
- network namespace
-i
- IPC namespace
- ...
Или, указав ключ -a
, можно сменить все namespaces.
Также потребуется указать идентификатор процесса внутри контейнера, который в принципе можно узнать командой ps -ef
, но, зная имя или идентификатор контейнера это лучше сделать командой:
root@docker:~# PID=$(docker inspect --format {{.State.Pid}} chat-mongo-1)
Следует обратить внимание, что изменив mount namespace, запуск команды будет происходить уже в пространстве имён файловой системы контейнера, в котором перечисленных ранее команд нет. Так как упомянутые выше команды работают с сетью, при их запуске ограничимся указанием ключа -n
:
root@docker:~# nsenter -n -t $PID iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER_OUTPUT
-N DOCKER_POSTROUTING
-A OUTPUT -d 127.0.0.11/32 -j DOCKER_OUTPUT
-A POSTROUTING -d 127.0.0.11/32 -j DOCKER_POSTROUTING
-A DOCKER_OUTPUT -d 127.0.0.11/32 -p tcp -m tcp --dport 53 -j DNAT --to-destination 127.0.0.11:43041
-A DOCKER_OUTPUT -d 127.0.0.11/32 -p udp -m udp --dport 53 -j DNAT --to-destination 127.0.0.11:47359
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p tcp -m tcp --sport 43041 -j SNAT --to-source :53
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p udp -m udp --sport 47359 -j SNAT --to-source :53
С командой host(1
), к сожалению, данный фокус сработает не совсем так, как хотелось бы. Если указать ключ -m
, то выяснится, что внутри файловой системы контейнера команды host
нет:
root@docker:~# nsenter -m -n -t $PID host -a www.tune-it.ru
nsenter: failed to execute host: No such file or directory
А без ключа -m
команда будет выполняться в файловой системе хостовой системы и, соответственно, прочитает файл /etc/resolv.conf
хостовой системы, а не контейнера. Для проверки резолвинга в контейнере этой командой надо явно указать nameserver, который используется внутри контейнера:
root@docker:~# nsenter -n -t $PID host -a www.tune-it.ru 127.0.0.11
Trying "www.tune-it.ru"
Using domain server:
Name: 127.0.0.11
Address: 127.0.0.11#53
Aliases:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35003
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;www.tune-it.ru. IN ANY
;; ANSWER SECTION:
www.tune-it.ru. 21600 IN A 195.218.154.202
Received 48 bytes from 127.0.0.11#53 in 3 ms