Я должен признаться. Я ненавижу docker. Всей своей душой. Это самая ужасная софтина, которую я видел за последние 10 лет.
С одной стороны, я очень уважаю одноименную компанию. Ребята из Docker Inc. реально популяризировали контейнеризацию. Теперь о ней не знает только ленивый. С другой стороны, ничего принципиально нового они не изобрели — контейнеризация на момент, когда Docker "выстрелил", уже существовала более 30 лет (начиная от cgroups, вспомним еще jails и zones, ну, и наконец-то — namespaces & cgroups).
Круто, что docker реально ускоряет разработку во множество раз. Если вести ее правильно, то даже без потери в качестве. В любом случае, docker здесь, от него не деться и приходится им пользоваться.
Но почему у меня этот продукт с логотипом кита вызывает столь разнообразные эмоции? Ниже я перечислю те моменты, от которых бомбит. Возможно, что читатель будет не согласен или, напротив, найдет какие-то вещи, о которых не знал и сочтет интересными.
Disclaimer: все написанное ниже является личным мнением автора и может как отражать реальность, так и не отражать реальность. Материал строго провокационного характера и основной целью является не унизить или обидеть кого бы то ни было, а скорее заставить людей включить голову и осознать масштабы глубин (с).
docker и файрволл.
При создании контейнеров в бриджованных сетях, docker добавляет свои правила файрволла на системе. Это приводит к очень интересным эффектам. Во-первых, просто так становится невозможно сбросить цепочки netfilter (такое может происходить при повторном применении своих правил), т.к. после этого нарушается работоспособность docker-контейнеров и приходится docker-демон перезапускать. Также теряется смысл в утилитах iptables-save/restore, т.к. фактически они сохраняют правила, но не те, которые нужно — приходится фильтровать их вывод. Еще очень интересная задача совместить docker и что-либо, что пытается самостоятельно управлять файрволлом — будь то VPN-клиент/сервер или система управления конфигурацией, которая зорко следит за тем, чтобы правила соответствовали тому, что описал администратор системы.
До последней поры вообще не существовало способа надежно и повторяемо регулировать через файрволл сетевой доступ к контейнеру, но разработчики добавили отдельную цепочку DOCKER-USER, но, честно говоря, толку от нее нет.
А чего стоит возможность получить интересный баг: контейнер с внешних узлов доступен, а с самого хоста, где он расположен — нет. Оказывается, в цепочке INPUT порт контейнера запрещен и по умолчанию не открывается, а внешнее взаимодействие идет через таблицу NAT (такое поведение наблюдается на CentOS при публикации портов через docker run ... -p XX:YY
)
Решение, кстати, простое, но довольно муторное — вообще отобрать у docker-демона возможность вносить изменения в правила файрволла, правда, при этом приходится все необходимые правила создавать руками или через систему управления конфигурацией. Правда, это напоминает закат солнца вручную, т.к. сразу же улетучивается удобство и кажущаяся простота docker'а.
docker и сети
По умолчанию docker-демон создает docker-bridge docker0, на который садятся контейнеры, если при их создании не была указана какая-либо другая сеть. При этом (неожиданность!) в этой сети не работает внутренний DNS докера. Он для контейнеров работает только при создании кастомных сетей (через docker network create
или через docker-compose). Так вот. По умолчанию докер бридж создается на адресах из подсети 172.16.0.0/12. Фактически это означает, что если мудрые администраторы корпоративной сети организовали ее на той же подсети, то можно ловить очень странные проблемы с доступностью узлов. И долго гадать — что же является причиной этих проблемой. Самое печальное, что даже если bip перевесить, то docker-compose игнорирует эту настройку, т.к. сети в нем как будто захардкожены.
При прочих равных рекомендую не пользоваться пробросом портов через -p или --publish, а использовать режим network host mode — получите производительность сети на 5% процентов больше, а по безопасности… Ее и так в докере нет :-) Производительность бриджованной сети ниже из-за того, что docker активно вторгается в настройку файрволла и использует механизмы NAT.
docker — это не про совместимость
Действительно, зачем соблюдать совместимость и придумывать пути миграции для пользователей, если можно этого не делать? Я с этим столкнулся два раза. Первый — когда был старый "добрый" драйвер aufs, но пришлось переходить на overlay2, т.к. последний работает в разы лучше. Так вот — напрямую это сделать нельзя. Только через полную потерю всех volume и image, которые созданы в хост-системе со старым драйвером. Сейчас эта проблема уже практически потеряла актуальность, потому что большинство систем по умолчанию идут уже с overlay2 драйвером. Проверить можно через вывод docker info команды.
Вторая ситуация — ВНЕЗАПНО! образы созданные более свежей версией докер-демона могут не работать на более старых докер-демонах. В обратную сторону (образы от более старой версии докера на более новой) — работает все прекрасно. Это может выстрелить, если имеется достаточно большой парк различных серверов с разношерстной версией докер-демона.
docker hub — это помойка
Docker Inc. для поддержки экосистемы докера разработали первый и самый всеобъемлющий каталог образов для докера — Docker Hub. На нем можно найти как официальные образы, так и любительские. Можно хостить как публичные образа, так и приватные (требуются учетные данные для доступа к ним). Вроде все здорово, не так ли? По факту получается, что большинство любительских образов не поддерживаются, собраны кое-как и имеют уязвимости. Чего уж говорить — если даже официальные образы зачастую запускаются под root'ом и это поведение не изменить. Еще никто никогда не гарантирует, что образ, который сейчас присутствует в хабе будет там завтра. Более того — даже то, что у него есть тег совершенно не означает, что под ним завтра будет такой же образ.
Еще возможность все легко упаковать в образ отшибает у программистов желание думать — как оптимизировать код. Коллеги просто берут и в образ запихивают все подряд, не разбираясь — нужен ли этот модуль или нет. В принципе, это уменьшает количество хлама в хост-системе (а раз так, то и увеличивает стабильность ее работы). С другой стороны — переместив бардак из точки А в точку Б, мы его не уменьшаем. Если смотреть на докеры с этой стороны — они действительно хороши для изоляции интерпретируемых языков с не очень вменяемыми пакетными менеджерам — python, ruby, node.js. Для компилируемых языков вроде golang или виртуальных машин вроде java — паковка в контейнер докера выглядит избыточной.
Какая может быть рекомендация? А простая — любой образ, который используется в проект — по возможности — собирать самостоятельно и класть в свой собственный registry для докер-образов. Стараться собирать минимальный комплект в ПО в контейнере (это и уменьшает поверхность атаки, и сложность поддержики образа, а еще чем меньше образ, тем быстрее он скачивается на хост ноду, что критично для оркестраторов вроде k8s).
Путаница с терминами и с ключами командной строки
registry vs repository, bind mount vs volume (оба могу быть созданы через docker run ... -v
, хотя по факту это разные сущности), tag vs image name, EXPOSE vs expose vs ports, различные типы volumes (анонимные, именованные...). И пр. В принципе, это понятно, т.к. продукт достаточно свежий и терминология в принципе не могла успеть устояться. Я уж не говорю про то, что лучше все всегда смотреть в оригинальном написании, т.к. наши переводчики умудряются перевести так, что изначальный смысл ускользает.
Порядок запуска
Надо уяснить — docker не решает проблему порядка запуска контейнеров. Никак. От слова совсем. Он это не умеет и точка. Для этого используйте внешние средства. Начиная от systemd unit'ов (предпочтительно, но нужно их аккуратно писать — возможно как-нибудь напишу статью по этой теме), кончая портянка на bash.
Да, стоит упомянуть docker-compose. Внезапно, но он тоже НЕ решает эту проблему. Директива depends_on, которая вроде как за это отвечает — бесполезна, т.к. по факту она действительно позволяет в моменте (при выполнении команды docker-compose up) запустить контейнеры последовательно, но тут куча нюансов. Начиная от того, что она по умолчанию не ждет готовности контейнера (это реализовано только в формате 2.4 docker-compose).
Форматы docker-compose
Утилита docker-compose принимает файл описания контейнеров в двух разных конкурирующих между собой форматах — версий 2.*
и 3.*
. Понятно почему так произошло. Формат версии 3.* затачивался под docker swarm и в этой системе оркестрации в принципе часть возможностей не работает. Для начинающих наличие двух форматов вносит ненужную путаницу.
Установка
Есть 100500 способов установки docker и docker-compose. Я столкнулся с кучей различных проблем. Начиная с того, что какие-то умные люди в Убунту сделали пакет docker, который к Docker Inc. не имеет никакого отношения. В результате "правильный" пакет имеет название docker.io или docker-ce. Ну, и, конечно, его лучше устанавливать по официальной инструкции с оф. сайта Docker
Еще хочу добавить, что самые интересные спецэффекты я наблюдал, когда разработчик устанавливал docker в Ubuntu через snap. Как будто какие-то рандомные фичи переставали работать. Сейчас уже не вспомню. Но все проблемы снимало рукой, когда переустанавливали docker по официальной инструкции пакетным менеджером.
docker-compose — если его ставить из репозиториев операционной системы или через пакеты pip — это гарантированный способ навлечь проблем себе на голову. Во-первых, в этом случае docker-compose приносит с собой в систему лишние пакеты и получаются интересные эффекты вроде такого (вариант для Убунты). Во-вторых, если случайно сломается python окружение (внезапно! такое тоже бывает), то docker-compose тоже перестанет работать (например, я влетал в такое — это не проблема компоуза, конечно, но на нем тоже отразилось бы). Поэтому — верный способ ставить компоуз — по инструкции с оф. сайта единым бинарником (если есть сомнения в его целостности — проверяйте md5sum).
docker и безопасность
В слове docker нет буквы Б — это все, что нужно знать про безопасность. Действительно, docker проектировался по типичным канонам agile-методологии в ее худшем сценарии: фичи важнее всего остального. Результат таков, что в архитектуре докера безопасности нет. Первый момент — пользователь, который имеет доступ к докер-демону ЭФФЕКТИВНО имеет root права на хост системе. Например, легко получить доступ ко всей файловой системе:
docker run --rm -it -v /:/rootfs ubuntu bash
Понятно, что разделения контейнеров между различными пользователи на одной системе тоже нет. Каждый видит все контейнеры, запущенные на хосте. Это можно обойти запуском нескольких демонов на одном хосте (пример), но это жуткий костыль и по факту безопасность он не увеличивает.
Еще очень неприятный момент, что все файлы, которые создаются в bind mount (проброс каталога в докер-контейнер), имеют права пользователя внутри контейнера. Пользователи зачастую таким образом разделяют файлы между хост машиной, на которой происходит процесс разработки, и докер контейнером, чтобы лишний раз не пересобирать образ, а измененный код подгружается hot reload'ом или перезапуском контейнера. Причем, что интересно — корневой каталог bind mount при его создании через директиву docker run ... -v ...
имеет владельца root независимо от фактического пользователя внутри контейера. Если же использовать расширенный синтаксис bind mount (docker run ... --mount ...
), то каталог, в который идет монтирование не создается и ему можно задать любые права. Продуманный дизайн? Не, не слышал :-) Здесь могу сказать одно — используйте настоящие volume (которые хранятся по умолчанию в /var/lib/docker/volumes
Не стоит забывать, что есть возможность убежать из песочницы docker и не стоит запускать на своей машине произвольно выбранные образы. Действительно, можно наткнуться на образ, эксплуатирующий CVEшку и скомпрометировать свою систему.
docker и не-Линукс
До сих пор не научился нативно не в linux-kind операционных системах. Это и логично, т.к. docker очень глубоко завязан на механизмы namespaces и cgroups ядра Линукс. Что это означает на практике? А то, что пользователи Mac и Windows имеют возможность работать с docker через Docker Desktop или аналогичное решение, но постоянно возникают вопросы с пробросом volume и скоростью работы приложений (это логично, т.к. задействуется полноценная виртуализация). Но лучше уж так, чем никак.
docker и изоляция
Докер не обеспечивает 100% изоляцию ресурсов между контейнерами. Это может приводить к конкуренции за iops, кэши процессора и пр. и, как следствие, — меньшему быстродействию, чем на выделенных инстансах. Так же есть целая плеяда ресурсов ядра, которые не учитываются при создании контейнеров, и не являются предметом для квотирования. Самый последний вопиющий случай — с dentry
docker и логирование
По умолчанию docker использует json-file драйвер. Он складывает логи от приложения в /var/lib/containers//-json.log При этом их размер может расти бесконтрольно. В теории можно задать максимальный размер и параметры ротации, но кто это делает? Только честно. И к тому же все равно эти параметры "на лету" не изменишь
Есть второй драйвер — journald, который тоже поддерживает команду docker logs (удобно же смотреть логи командой!?). Все остальные драйвера — эту команду не поддерживают, разве что только в Docker-EE, но сколько реальных пользователей энтерпрайз версии вы видели?
Дополнительный вопрос — что если докер не будет успевать перемалывать логи от приложения? Оно блокируется на выводе. С этим, к сожалению, ничего не поделаешь толком.
docker и альтернативы
Сейчас получается интересная ситуация. В принципе, docker становится не нужен. Ранее и сейчас были прекрасные, но незаслуженно неизвестные альтернативы вроде systemd-nspawn. Есть еще очень нехорошая тенденция у разработчиков тащить многосервисные комплексы ПО в докер и использовать его виртуальную машину — это тем более странно, что есть прекрасные Vagrant, VirtualBox и lxc/lxd, если нужны легковесные виртуалки. Еще docker становится лишней прослойкой, если мы говорим про kubernetes — последний ведь умеет уже напрямую работать с containerd, причем даже производительнее.
Прочее
Можно припомнить еще очень многое: и отсутствие нормальной шаблонизации в docker-compose — мы ведь YAML якоря и возможность склейки docker-compose-файлов за таковую не считаем? Практически мертвое состояние docker swarm (kubernetes практически его съел живьем) — эту тему развивать можно практически вечно. Что интересно в Docker Inc. знают обо всем этом (стоит только посмотреть на ишью трекер), только ничего особо не делают (*) либо потому что не хотят, либо потому что не могут. Да в принципе уже и не нужно.
The END
Как итог можно подвести? На самом деле если отбросить все предрассудки — docker — это инструмент. Чтобы им эффективно пользоваться, нужен опыт работы с ним и знание подводных камней. Часть из них перечислена в настоящей статье. Так же docker — это инструмент для управления контейнерами первого поколения. Сейчас идет активная разработка альтернативных средств, например, cri-o/podman/buildah, которые нацелены на большую безопасность и удобство эксплуатации. Появляются средства вроде FireCracker, которые наследуют лучшие черты и от контейнеров (легковесность и скорость запуска) и лучшие черты от больших ВМ (возможность работы на разных ядрах и полноценную изоляцию).
Развитие технологий не стоит на месте и я надеюсь, что разработчики нового учтут недоработки текущий решений и мы получим новые хорошие инструменты для решених наших задач.
(*) — разработчики завезли шаблонизацию через docker-template, multi-stage сборки в docker build, buildkit как более новый и удобный способ сборки и многое другое, но это общей картины все равно не меняет — все эти фичи достаточно сырые и непродуманные.
Автор: gecube