Введение
В рамках курса по k8s, который мне предстоит освоить, была необходимость пройти также подготовительный курс по Docker. Было интересно послушать лектора и систематизировать знания по докеру, полученные самостоятельно из практики и документации. Многие так или иначе уже работали с докером и знают основные принципы. Тем не менее, я для себя тезисно записал некоторые моменты, на которые стоит обратить внимание и не забывать при работе, используя все фичи докера.
Лучшие практики
1) При установке пакетов с кучей зависимостей эти самые зависимости выносить в переменные типа ARG в самом начале – таким образом улучшится читаемость Dockerfile и не будет километров строк, а всего одна переменная.
2) Перед командой RUN добавлять set -ex, чтобы были видны подробные логи по каждой команде из докерфайла при сборке образа и при наличии ошибок выполнение сборки останавливалось. ТОже самое справедливо и для entrypoint-скриптов.
3) Порядок выполнения команд в докерфайле файле важен, т.к. каждая команда FROM, RUN, COPY\ADD создает отдельный слой. Оптимальным решением будет разделить этапы:
а) ОС. Всё, что касается базового образа и его настройки, описывается в этом слое командами FROM и RUN.
б) Пакеты и настройка приложения. Все, относящееся к приложению, описывается в данном слое. Это могут быть пакеты приложения (модули, библиотеки), зависимости, системные пакеты и непосредственно настройка приложения. Запускается одна команда RUN для системных пакетов и одна команда RUN для пакетов приложения.
в) Entrypoint. На данном этапе копируются файлы приложения через команду ADD или COPY, а потом запускается entrypoint-скрипт.
Таким образом, засчёт кеширования в докере, скорость сборки будет увеличена в разы ввиду архитектуры, как у “слоеного пирога”, когда каждая команда в докерфайле создает отдельный слой. При изменении первого слоя, будут выполнены заново все последующие, поэтому логично размещать наименее изменяемые файлы в самом начале (настройка ОС и установка системных пакетов) для ускорения сборки.
Также можно выносить все зависимости в отдельный текстовый файл, копировать его в образ, и на основе его содержимого выполнять дальнейшие действия. Таким образом изменения можно вносить только в один файлик, а сам Dockerfile будет выглядит более читаемым. Например, pip install -r requirements.txt выглядит куда понятнее, чем длинная портянка.
4) Есть команда COPY, а есть ADD. В целом делают одинаковое, но разными путями. Если нужно просто скопировать файл, как это часто бывает, то подойдет COPY. ADD же позволяет распаковывать архивы при копировании и прочее, что может быть не совсем ожидаемым при желании просто скопировать файл.
5) Entrypoint – команда, которая должна всегда выполняться в контейнере (можно переопределить через ключ –entrypoint). CMD – аргумент для Entrypoint, его можно переопределить при запуске контейнера, указав новый аргумент в самом конце. В таком случае будут доступны переменные окружения оболочки – это самый ходовой и рекомендуемый сценарий. Тем не менее, более подробно про разницу в командах написано в документации. В этом же пункте отмечу про процесс с PID 1 внутри контейнера. Рекомендуется использовать параметр exec в entrypoint-скрипте, чтобы при выполнении команды docker stop container_name посылался сигнал SIGTERM именно к запущенному процессу, а не entrypoint-скрипту. Более подробно про это описано в моей статье тут.
6) Для скорости сборки рекомендуется использовать Alpine Linux в кач-ве основного – его размер в разы меньше Debian или Centos. Скорость сборки и доставки будет быстрее.
7) После установки пакетов стоит выпиливать весь кеш для пакетов и их зависимостей у пакетных менеджеров, это также экономит место и скорость доставки образа.
8 ) Один контейнер – один процесс. Как правило, в контейнере живет один процесс и контейнер живет, пока запущен процесс. Тем не менее, бывают ситуации, когда в контейнере нужно запустить более одного процесса, для этого можно юзать supervisord. Лучше делать всегда красиво и правильно, но если есть сложности с этим, то можно вполне официально запустить пару и более процессов в одном контейнере, если переделывание на правильный вариант будет затратно с финансовой точки зрения. Здесь тогда стоит обратить внимание на tini.
9) Соблюдая все лучшие практики при сборке образа и проводя проверку образов на эксплоиты, заботиться о безопасности стоит всё же на уровне ниже – настраивая либо сеть, либо отдельные физические\виртуальные серверы, либо всё вместе в комплексе. Докер всего лишь инструмент для доставке и запуска кода, и не более того, поэтому излишние заморочки по секъюрности могут лишь привнести сложности.
10) Исходя из п.9, процесс можно запустить под рутом внутри контейнера, но при запуске контейнера указать, чтобы UID и GID процесса в контейнере не были равны GID и UID хост-машины. Это настраивается через маппинг, например, UID в контейнере будет 1, а на хосте – 2000, т.е. уже непривилегированный пользователь.
11) При использовании докера почти нет overhead, т.е. накладных ресурсов из-за дополнительных слоёв абстракций, т.к. используется cgroups с включенным memory accounting по умолчанию на самом хосте. Таким образом, при типовых сценариях использования overhead`а не будет.
12) Основной и самый ходовой драйвер для хранения почти во всех ОС – overlay2, но лучше уточнять в документации.
13) Приложение должно проверять перед запуском, что его БД готова к работе и запущена, и только после этого запускаться. Такой сценарий обычно описывается в entrypoint.sh. Можно настроить проверку БД на готовность к принятию запросов, импортировать миграции или схему БД для инициализации. Хороший пример для наглядности у Zabbix.
14) Нельзя не упомянуть docker-compose – это обертка над самим докером, написанная на питоне. Удобно применима при мультиконтейнерной системе, т.к. используется декларативный формат YAML, где просто описывается, что нужно запустить, и делается этой одной командой. Помимо этого docker-compose удобен при локальной разработке – разработчики легко у себя могут поднять все окружение самостоятельно парой команд. И конечно же автоматизированное тестирование и CI – для пайплайнов данный инструмент также подходит.
15) Существует возможность создания приватных хранилищ собственных образов, когда их количество становится слишком большим. Из популярных отмечен harbor – опенсорс разработка, который позволяет хранить образы и не только, производить их ротацию.
16) Под капотом докера docker engine – демон, который позволяет запускать контейнеры благодаря control groups (cgroups) и namespaces – технологиям в самом ядре Linux для изоляции процессов на уровне операционной системы.
Вышеописанные практики не зря упоминаются, т.к. многие разработчики не задумываются о правильности, особенно когда оно просто работает, даже если сделано плохо. Поэтому об этом помнят админы, которые занимаются эксплуатацией Нужно помнить, почему все любят и используют докер. Это стандарт, который удобен и легко воспроизводим.
Необходимость применения Docker
И в заключении немного рассуждений о том, нужен ли докер там, где без него обходились ранее. Вопрос спорный, т.к. всегда находятся противники новых технологий – “когда всё работает, зачем изобретать новое”. Но и без холивара понятно, что докер – лишь инструмент для решения той или иной задачи. И необходимость использования кроется исключительно в зависимости от потребностей бизнеса, если речь о продакшене и каких-то коммерческих применениях.
Лично мне удобно поставлять весь софт в уже запакованном контейнере, где единожды настроено всё окружение и я на 100% уверен, что всё запустится, как надо. Да, тоже самое можно сделать через через скрипты, но то будет лишь свой огород и другим придется разбираться, потратив время. Более кошерный вариант использовать системы управления конфигурациями типа Ansible – вариант имеет право на существование и вполне себе может использоваться. Но применим не во всех ситуациях.
Аналогично мне удобно не только поставлять, но и запускать на своих серверах также в контейнерах. К примеру, мониторинг, организованный через Zabbix, запускаю всегда в контейнерах на базе официальных образов. Зачем городить огород и тратить время, когда уже всё настроено и готово для запуска? А вот битрикс-проекты, в основном, запускаю вне докера – там уже всё реализовано вендором через ansible.
Какого-либо overhead`а я не встречал при использовании докер в различных проектах – об этом также спорят и приводят ссылки на различные исследования. Возможно, в моём случае нагрузки были не столь существенные. Что касается БД – да, тут я склонен к классическому развертыванию на ВМ без дополнительной абстракции, хотя опять же в последнее время уже и БД вполне себе живут в кластерах внутри k8s. Это прогресс, но за всем не угонишься, поэтому нужно помнить о том, что всё сугубо индивидуально и зависит от ситуации, но и общепризнанные достижения нельзя упускать из виду и обходить стороной.