null

Запуск команд в docker контейнере

С момента появления контейнерной виртуализации в 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

 

Коротко о себе:

Работаю в компании Tune-IT и тьютором кафедры Вычислительной техники в СПбГУИТМО.

Очень люблю команду cat, core solaris и IPv6.