vault-boy

Байка

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

Монтажник: Теперь мне нужно за комп.

Я: Да, прошу.

Стоит заметить, что уже тогда у меня на компе стояла Убунта.
Монтажник сел, осмотрелся, не увидел ничего знакомого и повернулся ко мне.

Монтажник: Как мне открыть Пуск?

Я: Тут нету Пуска. Что хотите сделать?

Монтажник: Зайти в “Панель Управления”.

Я: Тут нет “Панели Управления”. Что вы хотите сделать, а не куда зайти?

Монтажник: Открыть “Сетевые Подключения”.

Я: Тут нет “Сетевых Подключений”. Что вы хотите сделать, а не открыть?

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

Монтажник: Узнать MAC-адрес компьютера.

"Пронесло, -- подумал я про себя, -- он таки уловил, куда я клоню".
Открыв хоткеем терминал, я написал "ifconfig" и показал на нужную строчку.
MAC-адрес был сообщен "на базу".
Вскоре компьютер получил IP-адрес и интернеты заработали.

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

Дело в том, что этот случай хорошо иллюстрирует ситуацию, когда люди слишком сильно привыкают к определенному методу решения проблемы. Иметь привычки — это не плохо. Плохо, когда в силу привычки единственный метод решения становится синонимом самой проблемы. И когда этот золотой метод перестает работать, человек пытается починить метод, вместо того, чтобы решать первоначальную проблему. Метод мог морально устареть, или же сама проблема могла видоизмениться. Если вовремя не сделать шаг назад от любимого метода и не осмотреться, можно однажды всерьез задуматься на тему: “Как удалить гланды через задницу, не задев желудок”.

В своей предыдущей статье я уделил много внимания выправлению отношения читателя к контейнерам. В этой статье я хочу продолжить эту тему и привести несколько практических примеров. На вопрос “Как мне сделать это?” я буду отвечать “Этого делать не нужно, на самом деле вы хотите вот это”. Список этих вопросов, разбитый по категориям, — и есть оглавление.

Контейнер — это виртуалка (нет)

Ниже описан типичный процесс развертывания виртуалки. Новичку в мире контейнеров почти неизбежно захочется сделать как минимум одно из этих действий с контейнерами.

Задать IP-адрес

Правильная цель: Обеспечить сетевую связность контейнера.

Да, Докер дает возможность задавать IP-адреса контейнеров вручную, но в абсолютном большинстве случаев в этом нет необходимости:

  • Между собой контейнеры общаются по хостнеймам.

  • Во внешний мир из них должен смотреть, как правило, только один, и, желательно, через реверс-прокси.

Дайте Докеру самому раздать адреса контейнерам. Он, как минимум, гарантирует, что не будет конфликтов и дублирования. Это в тривиальном случае одинокого Докер-сервера. Все становится интереснее в кластерном режиме Docker Swarm. В Сворме может находиться несколько нод, географически удаленных друг от друга. Для прозрачного взаимодействия контейнеров, запущенных на разных нодах, Сворм создает оверлейную сеть на основе технологии VXLAN. То, для чего в традиционных сетях требуется сетевой инженер уровня не ниже CCNP, Докер делает сам, главное ему не мешать.

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

Зайти в контейнер по SSH

“Ок-ок, это все понятно, но как мне зайти в контейнер по SSH и посмотреть, что там происходит?”

Правильная цель: Получить доступ к изолированной среде контейнера.

SSH — это протокол удаленного доступа и передачи файлов. Не цель, а лишь один из методов. Почему этот метод нам не подходит? Причин хватает:

  • Нам нужен SSH-демон внутри контейнера

  • Нам нужен пользователь с известным паролем или заложенным внутрь контейнера ключом

  • Нам нужно обновлять в контейнерах SSH и зависимые криптобиблиотеки, чтобы закрывать уязвимости

Ничто из этого не входит в непосредственные задачи абсолютного большинства контейнеров (если только мы не решили контейнеризировать SSH-демон, что является довольно сомнительной идеей). Контейнеры должны оставаться максимально модульными и специализированными, а все усилия по настройке контейнера должны быть направлены исключительно на то, чтобы он мог выполнять свою единственную функцию.

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

“Ок, SSH — неподходящий метод, какой нам подходит?”

В Докере нет выделенного способа “подключиться” к контейнеру, но мы можем запускать любые команды внутри контейнера через docker exec.

Bash — хороший кандидат. Запустив его внутри контейнера мы, по сути, получим тот самый желанный доступ внутрь контейнера, пропуская всяческие процедуры аутентификации. Таким образом docker exec -it <имя_контейнера> bash или docker-compose exec <имя_сервиса> bash — наша “замена SSH”.

Донастроить работающий контейнер и сохранить его состояние

“Лады, я попал в контейнер и подкрутил там один момент, как мне теперь сохранить результат?”

Правильная цель: Заставить контейнер работать так, как нам нужно, всегда.

Строгий и дисциплинирующий ответ прозвучит так: “Никак, ты все это сделал зря, твои труды напрасны”.

Почему оно так? Потому что мы хотим, чтобы наши контейнеры рождались на свет здоровыми, а не требовали немедленной пересадки ног и химиотерапии только для того, чтобы не быть бесполезным овощем. Вы не знаете, когда, где и кем могут быть запущены ваши контейнеры. Рассчитывать на то, что сторонний пользователь так же как и вы “залезет внутрь и все подправит” — глупо.

“Это все круто, но контейнер работает не так, как надо. Проблему нужно решать!”

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

Хранение данных

Еще один больной момент: как хранить данные в мире контейнеров, которые не должны хранить состояния.

Как мне вытащить пользовательские данные из контейнера?

Правильная цель: Сохранять наработанные контейнером данные.

Если встал такой вопрос, значит с пониманием принципов работы Докера что-то не так. Никакая полезная информация не должна храниться внутри контейнера.

Для хранения информации вне контейнера у нас ровно два варианта:

  • подмонтировать каталог с хоста внутрь контейнера

  • создать специальный раздел (Docker Volume) и подмонтировать его внутрь контейнера

У обоих вариантов есть свои преимущества и недостатки, а значит — подходящее место и время для их использования. Если быть кратким:

  • хостовые маунты — для разработки

  • разделы — для продакшена

Хостовые маунты хороши тем, что позволяют быстро доставить измененный код приложения внутрь контейнера без его пересборки. По факту — это мгновенная синхронизация изменений. Минус хостовых маунтов в том, что они щемят портируемость нашего приложения.

Каким именно образом? Они накладывают конкретные ожидания на хостовую файловую систему. Если контейнер будет запущен на некоем произвольном докер-сервере с параметром -v /path/on/host:/app, то этот контейнер прямо-таки требует, чтобы либо /path/on/host был пустой, либо там лежали данные, созданные его предшественником. Если этот каталог подмонтирован в другой контейнер, то начнется беспредел и драка.

Мы скатываемся в проблему ручного назначения IP-адресов, только в профиль.

“Но в чем разница, если по дефолту раздел — это просто папочка в глубине /var/lib/docker?”

Как минимум в том, что Докер о ней знает, и у нас есть средства управления разделом прямиком из Докера. Что это нам дает?

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

Во-вторых, пользователь, создавший контейнер, может удалить раздел, созданный для этого контейнера, в отличие от подмонтированного /path/on/host, для удаления которого придется быть рутом.

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

Ну и в конце концов, мы не ограничены единственным дефолтным драйвером local. Есть вагон драйверов для распределенных файловых систем и облачных провайдеров. Добиться прозрачной работы всех контейнеров кластера Докер-серверов с каким-нибудь амазоновским хранилищем без использования разделов — дисциплина специальной олимпиады.

Как сделать бэкап раздела?

“Ладно, на продакшене — только разделы. А еще на продакшене есть бэкапы, как мне сделать бэкап раздела?”

Правильная цель: Сделать бэкап данных приложения.

Опять же, разделы — это всего лишь абстракция для хранения важных данных. Идея делать бэкапы любого приложения абсолютно одинаковым способом, неким магическим “docker volume backup”, выглядит заманчиво. Но если еще раз подумать, от чего будет больше толку, от mysqldump > dump.sql или от бинарных потрохов /var/lib/mysql? Приложение знает о своих данных больше, чем Докер. Оно может сделать бэкап своих данных более умным образом.

Например, в случае СУБД мы можем сделать дамп только определенных баз, можем решить, в какой кодировке сложить данные и т.д. Полученный дамп будет человеко-читаем и намного лучше портируем. Например, PostgreSQL прямым текстом посылает, если ему подмонтировать потроха от другой версии.

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

Если есть возможность, бэкап приложения надо делать средствами самого приложения.

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

docker run --volumes-from stupid_container -v $(pwd):/backup busybox tar cvf /backup/backup.tar /path/to/data

Что делает эта команда:

  • Запускает контейнер busybox (минималистичный Linux-дистрибутив)

  • Монтирует в него разделы контейнера stupid_container

  • Монтирует текущий каталог в /backup

  • Архивирует tar’ом содержимое /path/to/data в текущий хостовый каталог

Разворачивается бэкап с точностью до наоборот:

docker run --volumes-from stupid_container -v $(pwd):/backup busybox tar xvf /backup/backup.tar

Это решение будет работать одинаково хорошо как на одиночном докер-сервере, так и в кластерном режиме. Уровень абстракции, создаваемый разделами, избавляет нас от необходимости шариться по серверам в поисках места, куда физически сложены желаемые данные.