Почему вам не нужен sshd в Docker-контейнере

Автор оригинала: Jerome Petazzoni
  • Перевод
  • Tutorial
Когда люди запускают своей первый Docker-контейнер, они часто спрашивают: «А как мне попасть внуть контейнера?» и ответ «в лоб» на этот вопрос, конечно: «Так запустите в нём SSH-сервер и приконнектитесь!». Цель этого топика — показать, что на самом деле вам не нужен sshd внутри вашего контейнера (ну, конечно, кроме случая, когда ваш контейнер собственно и предназначен для инкапсуляции SSH-сервера).

Запустить SSH-сервер — заманчивая идея, поскольку это даёт быстрый и простой доступ «внутрь» контейнера. Все умеют пользоваться SSH-клиентами, мы делаем это каждый день, мы знакомы с доступами по паролям и по ключам, перенаправлением портов, ну и вообще доступ по SSH — хорошо знакомая вещь, точно будет работать.

Но давайте подумаем ещё.

Давайте представим себе, что вы собираете Docker-образ для Redis или веб-сервиса на Java. Я хотел бы задать вам несколько вопросов:

Зачем Вам ssh?
Скорее всего вы хотите делать бекапы, проверять логи, может быть перезапускать процессы, править настройки, отлаживать что-то с помощь gdb, strace или подобных утилит. Так вот, это можно делать и без SSH.

Как вы будете управлять ключами и паролями?
Вариантов не много — либо вы их «намертво» зашьёте в образ, либо положите на внешний том. Подумайте, что нужно будет сделать для обновления ключей или паролей. Если они будут вшиты — придётся пересобирать образ, передеплоивать его, перезапускать контейнеры. Не конец света, но как-то не элегантно. Значительно лучшим решением будет положить данные на внешний том и управлять доступом к нему. Это работает, но важно проверить, чтобы контейнер не имел доступа на запись в данный том. Ведь если доступ будет — контейнер может повредить данные, и тогда вы не сможете подсоединиться по SSH. Что ещё хуже — если один том будет использоваться в качестве средства хранения данных для аутентификации в несколько контейнеров — вы потеряете доступ сразу ко всем. Но это только если вы везде будете использовать доступ по SSH.

Как вы будете управлять обновлениями безопасности?
SSH-сервер это вообще-то достаточно надёжная штука. Но всё-же это окно во внешний мир. А значит нам нужно будет устанавливать обновления, следить за безопасностью. Т.е. в любом самом что ни на есть безобидном контейнере у нас теперь будет область, потенциально уязвимая ко взлому извне и требующая внимания. Мы своими руками создали себе проблему.

Достаточно ли «просто добавить SSH-сервер» чтобы всё работало?
Нет. Докер управляет и следит за одним процессом. Если вы хотите управлять несколькими процессами внутри контейнера — вам понадобится что-то типа Monit или Supervisor. Их тоже нужно добавить в контейнер. Таким образом мы превращаем простую концепцию «один контейнер для одной задачи» во что-то сложное, что нужно строить, обновлять, управлять, поддерживать.

Вы ответственны за создание образа контейнера, но вы также ответственны и за управление политиками доступа к контейнеру?
В маленьких компаниях это не имеет значения — скорее всего вы будете выполнять обе функции. Но при построении большой инфраструктуры скорее всего один человек будет создавать образы, и совсем другие люди будут заниматься управлением правами доступа. А значит «вшивание» SSH-сервера в контейнер — не лучший путь.

Но как же мне ...



Делать бекапы?

Ваши данные должны храниться на внешнем томе. После этого вы можете запустить другой контейнер с опцией --volumes-from, который будет иметь доступ к тому же тому. Этот новый контейнер будет специально для выполнения задач бекапа данных. Отдельный профит: в случае обновления\замены инструментов бекапа и восстановления данных вам не нужно обновлять все контейнеры, а только тот один, который предназначен для выполнения этих задач.

Проверять логи?

Используйте внешний том! Да, снова то же самое решение. Ну а что поделаешь, если оно подходит? Если вы будете писать все логи в определённую папку, а она будет на внешнем томе, вы сможете создать отдельный контейнер («инспектор логов») и делать в нём всё, что вам нужно. Опять таки, если вам нужны какие-то специальные инструменты для анализа логов — их можно установить в этот отдельный контейнер, не замусоривая исходный.

Перезапустить мой сервис?

Любой правильно спроектированный сервис может быть перезапущен с помощью сигналов. Когда вы выполняете команду foo restart — она практически всегда посылает процессу определённый сигнал. Вы можете послать сигнал с помощь команды docker kill -s . Некоторые сервисы не реагируют на сигналы, а принимают команды, например из TCP-сокета или UNIX-сокета. К TCP-сокету вы можете приконнектиться извне, а для UNIX-сокета — опять-таки используйте внешний том.

«Но это всё сложно!» — да нет, не очень. Давайте представим, что ваш сервис foo создаёт сокет в /var/run/foo.sock и требует от вас запуска fooctl restart для корректного перезапуска. Просто запустите сервис с -v /var/run (или добавьте том /var/run в Dockerfile). Когда вы хотите перезапустить сервис, запустите тот же образ, но с ключом --volumes-from. Это будет выглядеть как-то так:

# запуск сервиса
CID=$(docker run -d -v /var/run fooservice)
# перезапуск сервиса с помощью внешнего контейнера
docker run --volumes-from $CID fooservice fooctl restart


Отредактировать конфигурацию?

Во-первых следует отличать оперативные изменения конфигурации от фундаментальных. Если вы хотите изменить что-то существенное, что должно отразиться на всех будущих контейнерах, запущенных на основе данного образа — изменение должно быть вшито в сам образ. Т.е. в этом случае SSH-сервер вам не нужен, вам нужна правка образа. «Но как же оперативные изменения?» — спросите вы. «Ведь мне может быть нужно менять конфигурацию по ходу работы моего сервиса, к примеру, добавить виртуальные хосты в конфиг веб-сервера?». В этом случае вам нужно использовать… подождите-подождите… внешний том! Конфигурация должна быть на нём. Вы даже можете поднять специальный контейнер с ролью «редактор конфигов», если хотите, установить там любимый редактор, плагины к нему, да что угодно. И это всё никак не будет влиять на базовый контейнер.
«Но я делаю всего лишь временные правки, экспериментирую с разными значениями и смотрю на результат!». Ок, для получения ответа на этот вопрос читайте следующий раздел.

Отлаживать мой сервис?

И вот мы добрались до случая, когда вам действительно нужен настоящий консольный доступ «внутрь» вашего контейнера. Вам ведь нужно где-то запускать gdb, strace, править конфигурацию, и т.д. И в этом случае вам понадобиться nsenter.

Что такое nsenter


nsenter это маленькая утилита, позволяющая вам попадать внутрь пространств имён (namespaces). Строго говоря, она может как входить в уже существующие пространства имён, так и запускать процессы в новых пространствах имён. «Что это вообще за пространства имён, о которых мы тут говорим?». Это важная концепция, связанная с Docker-контейнерами, позволяющая им быть независимыми друг от друга и от родительской операционной системы. Если не углубляться в детали: с помощью nsenter вы можете получить консольный доступ к существующему контейнеру, даже если внутри него нет SSH-сервера.

Где взять nsenter?

С Гитхаба: jpetazzo/nsenter. Можете запустить
docker run -v /usr/local/bin:/target jpetazzo/nsenter


Это установит nsenter в /usr/local/bin и вы сразу сможете его использовать. Кроме того, в некоторых дистрибутивах nsenter уже встроен.

Как его использовать?

Сначала выясните PID контейнера, внутрь которого хотите попасть:
PID=$(docker inspect --format {{.State.Pid}} <container_name_or_ID>)


Теперь зайдите в контейнер:
nsenter --target $PID --mount --uts --ipc --net --pid


Вы получите консольный доступ «внутрь» контейнера. Если вы хотите сразу запустить скрипт или программу — добавьте их аргументом к nsenter. Работает слегка похоже на chroot, с той лишь разницей, что касаемо контейнеров, а не просто директорий.

Как на счёт удалённого доступа?


Если вам нужен удалённый доступ к докер-контейнеру, у вас есть как минимум два способа сделать это:
  • SSH на хост-машину, а дальше использование nsenter
  • SSH на хост-машину со специальным ключом, дающим возможность запустить определённую команду (в нашему случае — nsenter)


Первый путь достаточно прост, но он требует прав рута на хостовой машине (что с точки безопасности не очень хорошо). Второй путь предполагает использование специальной возможности "command" авторизационных ключей SSH. Вы наверняка видел «классический» authorized_keys типа вот такого:

ssh-rsa AAAAB3N…QOID== jpetazzo@tarrasque

(Конечно, реальный ключ намного длиннее.) Вот в нём вы и можете указать определённую команду. Если вы хотите дать определённому пользователю проверять количество свободной ОЗУ на вашей машине, используя SSH-доступ, но не хотите давать ему полный доступ к консоли, вы можете написать в authorized_keys следующее:

command="free" ssh-rsa AAAAB3N…QOID== jpetazzo@tarrasque


Теперь, когда пользователь приконнектиться с использованием этого ключа, сразу будет запущена команда free. И ничего другого не может быть запущено. (Технически, вы возможно захотите добавить no-port-forwarding, смотрите детали в manpage по authorized_keys). Идея этого механизма в разделении полномочий и ответственности. Алиса создаёт образы контейнеров, но не имеет доступа к продакшн-серверам. Бетти имеет право на удалённый доступ для отладки. Шарлотта — только на просмотр логов. И т.д.

Выводы


Действительно ли это ну вот прямо УЖАСНО запускать SSH-сервер в каждом Docker-контейнере? Давайте будем честными — это не катастрофа. Более того, это может быть даже единственным вариантом, когда у вас нет доступа к хостовой системе, но непременно нужен доступ к самому контейнеру. Но, как мы увидели из статьи, есть много способов обойтись без SSH-сервера в контейнере, имея доступ ко всему необходимому функционалу и получив в то же время весьма более элегантную архитектуру системы. Да, в докере можно сделать и так, и так. Но перед тем как превращать свой Docker-контейнер в такой себе «мини-VPS», убедитесь, что это правда необходимо.
Инфопульс Украина
96,00
Creating Value, Delivering Excellence
Поделиться публикацией

Комментарии 30

    +8
    Позволю себе перефразировать последний абзац: есть много способов обойтись без контейнера и оставить SSH-сервер, адекватную архитектуру системы, сохранив при этом время. Ничего УЖАСНОГО, разные ситуации требуют разных решений.
      +19
      Или даже: смотрите какие костыли можно изобрести, чтобы не ставить ссш.
        0
        Банальное: «Когда у тебя в руках молоток, все задачи кажутся гвоздями.».
        По делу: ставить ssh сервер внутри контейнера — вот это костыль, поскольку противоречит идеологии докера.
          +2
          Предствьте себе микросервисы в контейнерах и 50-100 контейнеров (однопроцессных) на одном хосте или вируталке…
          Зачем вам еще 100-50 супервайзеров и 100-50 ssshd процессов на этой виртуалке? ;)) Статья очень правильная если вы используете docker для написания микросервисов. Истинных Microservices.;)
          Но Docker можно использовать и для дургих архитектурных стилей или задачь… тогда нет ничего страшного в sshd
        +1
        Хорошая статья. Отдельное спасибо за то, что не сильно категоричны.
          +1
          А чем плох способ подключаться через attach, а в качестве начального скрипта контейнера запускать /bin/bash? (это вопроc)
            0
            Ничем, речь идет о проникновее в контер после того как он уже как-то запущен до это-го без остановки процесса
            +5
            Докер управляет и следит за одним процессом. Если вы хотите управлять несколькими процессами внутри контейнера — вам понадобится что-то типа Monit или Supervisor. Их тоже нужно добавить в контейнер. Таким образом мы превращаем простую концепцию «один контейнер для одной задачи» во что-то сложное, что нужно строить, обновлять, управлять, поддерживать.

            Postfix — почтовая система. Сам бог велел его запихать в контейнер. Весь, целиком, со всеми десятками процессов, которые порождает master при подключениях, время от времени и при прочих условиях.
            То же самое cyrus imap — master и куча процессов, которые он при необходимости порождает
            firebird в режиме classic — опять же, супервизор и куча воркеров
            nginx — кому-то надо объяснять?
            uwsgi — вообще есть режим императора, в котором процесс-супервизор запускает или убивает мастер, который уже порождает воркеров — мало этого, мастеры могут стартовать от имени разных учётных записей (на супервизоре должна быть капабилити «менять uid»).

            Задача != процесс. Задача может быть в виде нескольких процессов, каждый из которых может состоять из нескольких потоков.

            nsenter это маленькая утилита, позволяющая вам попадать внутрь пространств имён (namespaces). Строго говоря, она может как входить в уже существующие пространства имён, так и запускать процессы в новых пространствах имён. «Что это вообще за пространства имён, о которых мы тут говорим?». Это важная концепция, связанная с Docker-контейнерами, позволяющая им быть независимыми друг от друга и от родительской операционной системы. Если не углубляться в детали: с помощью nsenter вы можете получить консольный доступ к существующему контейнеру, даже если внутри него нет SSH-сервера.

            можно же было сказать прямо: Docker использует для работы Linux Namespaces, а nsenter — это низкоуровневая утилита для взаимодействия с этими namespaces

            кстати, это не единственное средство. Например, с netns можно управляться непосредственно с помощью утилиты ip пакета iproute2
              +2
              Стоило ли писать такую тираду для того, чтобы в статью добавить одно слово? Понятно что должно быть так:
              Если вы хотите управлять несколькими независимыми процессами внутри контейнера — вам понадобится что-то типа Monit или Supervisor.
              Если у вас в контейнере куча процессов, но один из них главный и «следит за порядком», то больших отличий от ситуации, когда у вас там всего один процесс нету. А вот когда их там много и они ничего друг о друге не знают — тогда ой.
              –2
              Отлично. SSH нам незачем, если можно влезать в чужие namespace. Получается, я таким образом могу влезать в те контейнеры, к внутренностям которых мне изначально никто доступа не давал?
                0
                А что вы делаете на чужом сервере с Docker?
                Docker не замена виртуалок, а продвинутый чрут, если угодно.
                Так что нет, не можете, потому что вас на сервер не пустят.
                  0
                  Прошу прощения, но я ни слова не сказал про чужие сервера.
                    +4
                    А если вы на сервере с Docker, то это — ваш сервер и какое еще «влезать в те контейнеры, к внутренностям которых мне изначально никто доступа не давал».
                    Вы просто не понимаете что такое Docker и зачем он нужен.
                      +1
                      Да и более того, flint, вы можете с таким же успехом в OpenVZ-контейнеры влезать. Собственно, любой хостер может влезть в вашу «виртуалку» на OpenVZ, если вы ее используете, и ничто его не остановит.
                        0
                        Вот это меня и печалит: вроде бы и новое поколение контейнеров (если предыдущими считать OVZ или джейлы FreeBSD), а проблема так и не решена. Даже более того, раз появляются подобные инструменты, то это как бы и не проблема, а фича. Ernillew в общем, прав, я не очень понимаю, почему вокруг Docker стоит такой шум.
                          +1
                          Я вам больше скажу, почему столько шума я тоже не понимаю. Правда я все собираюсь посмотреть Docker в варианте CoreOS, пока не собрался, может тогда пойму. Меня самого lxc вполне устраивает, после того ужаса, что был когда-то в OpenVZ(предупреждаю защитников OpenVZ, я его не видел долго и не хочу, мне хватило).
                          Я вижу применения докера на машинах разработчиков и на тестовых серверах где разрабы развлекаются, по крайней мере пока так вижу.
                            +1
                            Я смотрел на Docker больше как на средство дистрибуции не пакетов даже, а системы из нескольких связанных процессов (не Docker-way, само собой, но как способ упростить нетривиальный деплой). Собственно, мой первый вопрос был вызван тем, что по какой-то причине я был свято убежден, что с приходом libcontainer проблему подключения к любому контейнеру начали каким-то образом решать. Ну окей, не lxc-attach, так nsenter :(
                              +1
                              А посмотрите на темплейты lxc в таком разрезе. Думаю вполне подойдет.
                              У lxc сейчас в стабильной уже достаточно долго ветке есть темлейты которые могут разворачиваться сразу готовыми.
                            0
                            Ну я тоже не особо понимаю, но, в целом, Docker предоставляет возможность распространять ваше приложение, или тестовое окружение, без необходимости «засорять» систему, либо ставить какие-то другие версии библиотек. Лично я вижу в Docker, во-первых, систему, с помощью которой можно изолировать один сервис от другого, причем достаточно умную систему, которая не пожирает память на диске для каждого приложения, во-вторых, это такой удобный chroot, когда вам нужно единоразово что-то запустить типа как в виртуалке, но не в виртуалке, а в-третьих, многие приложения и дистрибутивы (в общем, контейнеры) собраны в онлайн-дистрибутиве, и их не нужно искать и устанавливать вручную.
                              0
                              У меня нет админского опыта (я разработчик), возможно что-то делал неправильно, но в моем случае мне показалось, что так просто избавиться от засорения системы с помощью Docker'а не получается. Такая ситуация: приложение с большим количеством зависимостей, регулярно обновляющихся. Обновить их в одном контейнере — задача несложная. Однако если контейнер нужно разворачивать еще где-то еще (скажем, для масштабирования), то приходится мейнтейнить еще и Dockerfile с реестром, то есть делать одно и то же в разных местах. Если держать эти зависимости в прямо в контейнерах, то при откате версии в случае беды, надо бы накатить пропущенные security-апдейты. После пары таких случаев подмывает держать их на монтируемом томе. А и данные нужно где-то хранить, подключаешь другой том. И сидишь, думаешь, что что-то идет не так: вроде и контейнеризация, а возни только прибавилось.
                  +1
                  Ответ на описанные беды — запуск docker из под linux container runtime.
                  Волшебный ключ "-e lxc" можно добавить в /etc/default/docker

                  Потом:
                  ~/.bashrc добавляем алиас:
                  
                  docker_attach() {
                    lxc-attach -n `docker inspect $1 | grep -i '"Id"' | sed -r "s/.*([0-9a-z]{64}).*/\1/g"` /bin/bash
                  }
                  


                  Ну а дальше docker_attach <container_name> и вот он полноценный баш.

                  Плюс, гибкое управление сетью, ресурсами, мониторинг и прочите плюшки lxc.
                    +1
                    Да, но сам докер больше не сфокусирован на LXC, они больше надеются на свой libcontainer.
                      –1
                      libcontainer не что иное как собственная библиотека доступа к Linux Container для уменьшения зависимостей.
                      И на данном этапе развития проекта я не вижу причин не использовать «родные lxc интерфейсы», а Вы?
                      Тем более если речь идет про development или testing окружение.

                      И, повторюсь, lxc — поможет избежать ломки при переходе контейнеризации от систем к службам.
                      А дальше, как мне кажется, либо человек научиться работать с Докер не заглядывая внутрь контейнера, либо не научиться вообще :)
                        +1
                        Как это «libcontainer не что иное как собственная библиотека доступа к Linux Container»? Вот же:
                        blog.docker.com/wp-content/uploads/2014/03/docker-execdriver-diagram.png
                          0
                          Вы ошибаетесь. с последними версиями докера, ваше решение не сработает.
                            0
                            Неужели… Это в каких? В ещё не выпущенных?
                            В текущей версии 1.2.0 предложенное мной решение прекрасно работает!
                            Вы бы проверяли свои слова, перед комментариями…
                              0
                              Ок я видимо пропустил ваше:
                              Волшебный ключ "-e lxc"
                              тогда работает… Прошу прощения.
                              Но ради это-го придется контейнеры запускать под с lxc.
                        0
                        А что есть -e lxc?
                        +7
                        Пора уже заводить хаб — docker.
                          0
                          В docker 1.3 есть exec

                          docker exec -i -t some-cool-container bash 
                          

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

                          Самое читаемое