THIRDEDmON Docker: Up & Running Shipping ReliaЫe Containers in Production Sean Р. Капе with Karl Matthias Вeijing • Вoston • Famham • Sebastopol • Tokyo o·REILLY' Книги для программистов: https://clcks.ru/books-for-it Шон П. Кейн, Карл Мапиас Docker Вводный курс 3-е изда.,.ие Астана «АЛИСТ» 2024 Книги для программистов: https://clcks.ru/books-for-it УДК 004.2 ББК 32.973-018.2 юз КЗЗ КейнШ.П. Docker. Вводный курс. - 3-е изд.: Пер. с анrл. / Ш. П. Кейн, К. Матrиас. Астана: АЛИСТ, 2024. - 352 с.: ил. ISBN 978-601-09-7541-5 Книга подробно описывает инструментарий Docker и возможности использова­ ния контейнеров для развертывания программного обеспечения. Рассказано об ин­ теграции Docker и контейнеров Linux с облачными сервисами и Kubemetes. Описа­ ны методы сборки образов Open Container lnitiative (OCI), развертывания и адми­ нистрирования образов с использованием командной строки. Показано, как образы OCI упрощают управление зависимостями и ускоряют процесс развертывания при­ ложений. Даны практические рекомендации по настройке и тестированию контей­ неров, подробно рассмотрены инструменты оркестрации, обеспечения безопасности и конфигурирования Docker. В третьем издании особое внимание уделено инстру­ менту BuildKit, поддержке мультиархитектурных образов, а также контейнеров в режиме rootless. Для разработчиков, системных администраторов, архитекторов и технических специалистов УДКОО4.2 ББК 32.973-018.2 © 2024 ALIST LLP Authori.7.ed Russian translation ofthe English edition of Docker: Up & Running, ЗЕ, ISBN 9781098131821 © 2023 Sean Р. Кале and Karl Matthias. This translation is puЫished and sold Ьу perrnission of O'Reilly Media, !пс., which owns or controls all rights to puЫish and sell the same. Авторизованный перевод с английского языка на русский издания Docker: Up & Running, ЗЕ, ISBN 9781098131821 © 2023 Sean Р. Капе, Karl Matthias. Перевод опубликован и продается с разрешения компании-правообладателя O'Reilly Media, !пс. ISBN 978-1-098-13182-1 (англ.) ISBN 978-601-09-7541-5 (каз.) О Sean Р. Kane, Кarl Matthias, 2023 О Издание на русском язьrке. ТОО "АЛИСТ", 2024 Книги для программистов: https://clcks.ru/books-for-it Оглавление Отзывы о книге ............................................................................................................. 13 Вступительное слово ..................................................................................................... 15 Предисловие ................................................................................................................... 17 Кому следует прочесть эту книгу ................................................................................................17 Зачем читать эту книrу..................................................................................................................17 Содержание книги .........................................................................................................................18 Обозначения ...................................................................................................................................19 Использование примеров кода .....................................................................................................20 Онлайн-обучение O'Reilly ............................................................................................................ 20 Контактная информация ............................................................................................................... 21 Благодарности ................................................................................................................................ 21 Глава 1. Введение .......................................................................................................... 23 Ожидания от Docker ......................................................................................................................23 Преимущества рабочего процесса Docker .............................................................................25 В чем Docker не поможет..............................................................................................................27 Важная терминология ...................................................................................................................28 Заключение.....................................................................................................................................29 Глава 2. Общие сведения о Docker ............................................................................. 30 Упрощение рабочих процессов ....................................................................................................30 Широкая поддержка и внедрение ................................................................................................33 Архитектура ...................................................................................................................................35 Клиент-серверная архитектура ...............................................................................................35 Сетевые порты и сокеты Unix.................................................................................................36 Надежные инструменты ..........................................................................................................37 Интерфейс командной строки Docker ....................................................................................37 Docker Engine API ....................................................................................................................38 Сеть для контейнеров ..............................................................................................................38 Как использовать весь поте1ЩИал Docker....................................................................................40 Контейнеры - это не виртуальные машины ........................................................................40 Ограниченная изоляция........... : ...............................................................................................41 Контейнеры легковесны ..........................................................................................................42 Сдвиг в сторону неизменяемой инфраструктуры .................................................................42 Приложения без сохранения состояния .................................................................................43 Хранение состояния во внешнем хранилище ........................................................................44 Рабочий процесс Docker................................................................................................................44 Контроль изменений ................................................................................................................45 Слои файловой системы...............................................................................................45 Теги образов ..................................................................................................................45 Книги для программистов: https://clcks.ru/books-for-it 6 Оглавление Сборка ....................................................................................................................................... 46 Тестирование ............................................................................................................................47 Упаковка ...................................................................................................................................48 Развертывание ..........................................................................................................................48 Экосистема Docker ...................................................................................................................49 Оркестрация ..................................................................................................................49 Неизменяемые атомарные хосты ................................................................................49 Дополнительные инструменты ....................................................................................50 Закточение.....................................................................................................................................51 Глава 3. Установка Doker ............................................................................................ 52 Клиент Docker ................................................................................................................................52 Linux..........................................................................................................................................53 UЬuntu Linux 22.04 (64-разрядная) ..............................................................................53 Fedora Linux 36 (64-разрядная) ....................................................................................54 macOS, Мае OS Х.....................................................................................................................55 Установщик с графическим интерфейсом..................................................................55 Установка Homebrew....................................................................................................55 Microsoft Windows 11 ..............................................................................................................56 Вкточение режима контейнеров Linux для Windows ...............................................56 Установка Chocolatey ...................................................................................................56 Сервер Docker ................................................................................................................................57 Linux с systemd .........................................................................................................................58 Сервер на базе виртуальной машины без Linux ....................................................................58 Vagrant ...........................................................................................................................58 Тестирование системы ..................................................................................................................63 UЬuntu .......................................................................................................................................63 Fedora ........................................................................................................................................63 Alpine Linux ..............................................................................................................................64 Сервер Docker ................................................................................................................................64 Закточение.....................................................................................................................................66 Глава 4. Работа с образами Docker............................................................................. 67 Структура Dockerfile .....................................................................................................................68 Сборка образа.................................................................................................................................70 Запуск образа .................................................................................................................................73 Аргументы сборки ...................................................................................................................74 Переменные среды для конфигурации...................................................................................74 Пользовательские базовые образы ...............................................................................................75 Хранение образов ..........................................................................................................................76 Публичные реестры .................................................................................................................76 Частные реестры ......................................................................................................................77 Аутентификация для входа в реестр ...................................................................................... 77 Создание учетной записи Docker Hub ........................................................................ 78 Вход в реестр.................................................................................................................78 Отправка образов в репозиторий ................................................................................80 Поиск образов в Docker Hub ........................................................................................81 Управление частным реестром ...............................................................................................82 Тестирование частного реестра ...................................................................................84 Книги для программистов: https://clcks.ru/books-for-it Оглавление 7 Оmимизация образов ....................................................................................................................85 Контроль размера образов.......................................................................................................85 Многоэтапные сборки ..................................................................................................90 Суммирование слоев................................................................................................................92 Использование кеша слоев ......................................................................................................94 Кеширование каталога.............................................................................................................99 Решение проблем со сборками ...................................................................................................103 Отладка образов без BuildКit ................................................................................................103 Отладка образов с BuildКit ...................................................................................................105 Мулыиархитектурные сборки ...................................................................................................107 Заюпочение...................................................................................................................................111 Глава 5. Работа с контейнерами ............................................................................... 112 Что такое контейнер? ..................................................................................................................112 История контейнеров .............................................................................................................113 Создание контейнера...................................................................................................................114 Базовая конфигурация ...........................................................................................................115 Имя контейнера...........................................................................................................115 Метки ...........................................................................................................................115 Имя хоста.....................................................................................................................116 DNS ..............................................................................................................................118 МАС-адрес ..................................................................................................................118 Тома хранилиша .....................................................................................................................119 Квоты на ресурсы ...................................................................................................................121 Доля процессорного времени ....................................................................................122 Привязка к процессору...............................................................................................124 Упрощение квот на ресурсы процессора..................................................................125 Память .........................................................................................................................126 Блочный ввод-вывод .......................................... : .......................................................128 u/imit.............................................................................................................................129 Запуск контейнера .......................................................................................................................130 Автоматический перезапуск контейнера...................................................................................131 Остановка контейнера.................................................................................................................131 Принудительное завершение контейнера .................................................................................133 Приостановка и возобновление контейнера .............................................................................133 Очистка контейнеров и образов .................................................................................................134 Контейнеры Windows ..................................................................................................................136 Заюпочение...................................................................................................................................140 Глава 6. Сбор информации в Docker ....................................................................... 141 Просмотр версии Docker.............................................................................................................141 Информация о сервере ................................................................................................................143 Загрузка обновлений образов .....................................................................................................144 Изучение контейнера ..................................................................................................................145 Изучение командной оболочки ..................................................................................................147 Возврат результата ......................................................................................................................148 Что происходит в контейнере .....................................................................................................149 docker container ехес ...............'...............................................................................................149 docker vo/ume ..........................................................................................................................150 Журналирование..........................................................................................................................152 docker container /ogs...............................................................................................................152 Расширенные возможности журналирования .....................................................................154 Книги для программистов: https://clcks.ru/books-for-it В Оглавление Мониторинг в Docker ..................................................................................................................156 Статистика контейнера ..........................................................................................................156 Статистика в командной строке ................................................................................156 Эндпоинт API stats .....................................................................................................158 Проверки работоспособности контейнеров.........................................................................160 docker system events ................................................................................................................163 cAdvisor ...................................................................................................................................164 Мониторинг с помощью Prometheus ..........................................................................................166 Самостоятельная работа .............................................................................................................170 Заключение...................................................................................................................................170 Глава 7. Отладка контейнеров ................................................................................. 171 Вьmод процесса ...........................................................................................................................171 Изучение процесса ......................................................................................................................176 Контроль процессов ....................................................................................................................178 Изучение сети ..............................................................................................................................180 История образа.............................................................................................................................183 Изучение контейнера ..................................................................................................................184 Изучение файловой системы ......................................................................................................185 Заключение................................................................................................................................... 186 Глава 8. Принципы работы Docker Compose ......................................................... 187 Настройка Docker Compose ........................................................................................................188 Запуск сервисов ........................................................................................................................... 196 Принципы работы Rocket.Chat ...................................................................................................198 Применение Docker Compose .....................................................................................................205 Управление конфигурацией .......................................................................................................207 Значения по умолчанию ........................................................................................................207 Обязательные значения .........................................................................................................209 Файл dotenv .................... :.......................................................................................................210 Заключение...................................................................................................................................211 Глава 9. Переход на контейнеры в рабочей среде ................................................. 212 ,Подготовка к развертыванию .....................................................................................................212 Docker в рабочих средах .............................................................................................................213 Управление заданиями ..........................................................................................................214 Ограничения ресурсов ...........................................................................................................215 Сети .........................................................................................................................................216 Конфигурация ........................................................................................................................216 Упаковка и поставка ..............................................................................................................217 Журналирование ....................................................................................................................217 Мониторинг ............................................................................................................................217 Планирование .........................................................................................................................218 Распределенные планировщики ................................................................................218 Оркестрация ................................................................................................................219 Обнаружение сервисов ..........................................................................................................220 Заключение по рабочей среде ...............................................................................................222 Docker в конвейере DevOps ........................................................................................................222 Краткий обзор ........................................................................................................................222 Внешние зависимости ...........................................................................................................225 Заключение...................................................................................................................................226 Книги для программистов: https://clcks.ru/books-for-it Оглавление 1 9 Глава 1 О. Масштабирование контейнеров ............................................................. 227 Режим Docker Swarm ...................................................................................................................228 Kubernetes .....................................................................................................................................238 Инструмент Minjkube ............................................................................................................239 Общие сведения о Minikube.......................................................................................239 Установка Minikube ....................................................................................................239 Запуск Kubernetes .......................................................................................................242 Команды minikube .......................................................................................................244 Kubernetes Dashboard ..................................................................................................244 Контейнеры и поды Kubernetes .................................................................................245 Практический пример ................................................................................................247 Развертывание реалистичного стека .........................................................................249 Определение сервиса ..................................................................................................250 Определение PersistentVo/umeC/aim .........................................................................251 Определение Dep/oyment............................................................................................252 Развертывание приложения .......................................................................................253 Увеличение масштаба ................................................................................................255 kubectl API ...................................................................................................................256 Kubernetes, интегрированный в Docker Desktop .................................................................258 Kind .........................................................................................................................................259 Amazon ECS и Fargate .................................................................................................................261 Базовая настройка АWS ........................................................................................................262 Настройка ролей IАМ ............................................................................................................262 Настройка А WS CLI ..............................................................................................................262 Установка ....................................................................................................................263 Конфигурация .............................................................................................................263 Экземпляры контейнеров ......................................................................................................264 Задачи......................................................................................................................................265 Тестирование задачи ..............................................................................................................273 Остановка задачи ...................................................................................................................273 Заключение...................................................................................................................................275 Глава 11. Расширенные концепции ......................................................................... 276 Контейнеры в деталях .................................................................................................................276 cgroups ....................................................................................................................................277 Файловая система /sys ................................................................................................ 278 Пространства имен ................................................................................................................281 Изучение пространств имен ......................................................................................283 Безопасность ................................................................................................................................284 U/D О ....................................................................................................................•.....•............285 Режим root/ess ........................................................................................................................288 Привилегированные контейнеры .........................................................................................292 Secure Computing Mode .........................................................................................................295 SELinux и AppArmor ..............................................................................................................299 Демон Docker ..........................................................................................................................300 Расширенная конфигурация .......................................................................................................301 Сети .........................................................................................................................................302 Сеть хоста ....................................................................................................................304 Конфигурирование сетей ...........................................................................................306 Хранилище ................................................................................................................................... 308 Книги для программистов: https://clcks.ru/books-for-it 10 1 Оглавление nsenter ...........................................................................................................................................312 Отладка контейнеров без командной оболочки ..................................................................313 Структура Docker ........................................................................................................................315 Альтернативные среды выполнения ..........................................................................................320 gVisor ......................................................................................................................................320 Заюпочение...................................................................................................................................323 Глава 12. Развитие экосистемы ................................................................................ 324 Клиентские инструменты ...........................................................................................................324 nerdctl ......................................................................................................................................324 podman и buildah ....................................................................................................................325 Универсальные инструменты разработчика .............................................................................327 Rancher Desktop ......................................................................................................................327 Podman Desktop ......................................................................................................................328 Заюпочение...................................................................................................................................329 Глава 13. Устройство контейнерной платформы ................................................. 330 Приложение двенадцати факторов ............................................................................................331 Кодовая база ...........................................................................................................................331 Зависимости............................................................................................................................332 Конфигурация ........................................................................................................................333 Сторонние службы .................................................................................................................334 Сборка, выпуск, выполнение ................................................................................................335 Процессы ................................................................................................................................335 Привязка портов .....................................................................................................................336 Параллелизм ...........................................................................................................................336 Утилизируемость .................... :..............................................................................................337 Паритет среды разработки и рабочей среды .......................................................................337 Журналы .................................................................................................................................338 Администрирование ..............................................................................................................338 Заюпочение по 12 факторам....................................................•.............................................339 Манифест реактивных систем .................................................................................................... 339 Быстрый отклик .....................................................................................................................339 Отказоустойчивость...............................................................................................................339 Масштабируемость ................................................................................................................ 339 Ориентация на события .........................................................................................................340 Заюпочение...................................................................................................................................340 Глава 14. Заключение ................................................................................................. 341 Дальнейшее развитие ..................................................................................................................341 Зачем нужен Docker ....................................................................................................................342 Рабочий процесс Docker..............................................................................................................343 Меньше программных пакетов для развертывания..................................................................343 Оптимизация хранения и получения .........................................................................................344 Результат ......................................................................................................................................345 Итоги.............................................................................................................................................345 Предметный указатель............................................................................................... 346 Об авторах..................................................................................................................... 349 Об изображении на обложке ...................................................................................... 350 Книги для программистов: https://clcks.ru/books-for-it Моей жене и детям, благодаря которым в моей жизни столько смысла. Моим родителям, которые научwzи меня слушать свой разум и сердце. Моей сестре, которая вдохновляет меня смотреть на мир глазами других людей. -ШонП. Кейн Моей маме, которая npuвwza мне любовь к чтению, и моему папе, который читал мне. Моей жене и детям, которые всегда поддерживают меня. - Карл Маттиас Книги для программистов: https://clcks.ru/books-for-it Отзывы о книге В книге "Docker. Вводный курс" описываются не только теоретические преимущества Docker, но и практические аспекты использования контейнеров в рабочей среде. - Келси Хайтауэр (Kelsey Нightower), руководитель по работе с разработчиками, Google Cloud Platform Книга "Docker. Вводный курс" подробно описывает базовые принцшn,1 и практические аспекты работы с Docker на основе многолетнего опыта. - Лиз Райс (Liz Rice), руководитель по опенсорс-проектам с eBPF в Isovalent Книга "Docker. Вводный курс" поможет вам строить современные, надежные и высокодоступные системы. -Михай Тодор (Mihai Todor), главный инженер, TLCP Несколько лет назад мне пришлось перейти с виртуальных машин на контейнеры. Мне нравится сначала изучать новые технологии с точки зрения пользователя и только потом углубляться в технические детали. Благодаря книге "Docker. Вводный курс" я на практике познакомился с Docker и контейнерами, и довольно быстро стал применять эти навыки в работе. - Фабиана Фиденчио (Fablano Fidencio), инженер по облачной оркестрации, Intel Corporation Книги для программистов: https://clcks.ru/books-for-it Вступительное слово Контейнеры сегодня используются везде - от локальных сред разработки до кон­ вейеров непрерывной интеграции и масштабных рабочих нагрузок. В чем секрет их популярности, что ждет нас дальше и какая нам от этого польза? В прошлом многие технологии обещали возможность переносить приложения между платформами без переписывания кода, но мы не всегда получали желаемое, и нам в любом случае нужна была среда выполнения (и дополнительные зависимо­ сти) для запуска приложения. С контейнерами мы можем собрать приложение один раз, а затем запускать где угодно. С их помощью мы упаковываем приложения, не­ обходимую среду выполнения, файлы конфигурации и все зависимости в один про­ граммный пакет. Если на целевом компьютере есть среда выполнения контейнеров, приложение будет работать. Больше не нужно переживать о том, что приложение корректно работало на компьютере разработчика, но запускается с проблемами в другой среде. Мы управляем жизненным циклом контейнера и приложениями, упакованными в него, с помощью стандартного API. Этот API предоставляет единый интерфейс для разнородных компонентов развертывания, так что специалистам по сопровож­ дению не приходится разбираться в тонкостях развертывания и запуска приложе­ ний, а значит, они могут сосредоточиться на своих основных обязанностях управлении инфраструктурой, обеспечении безопасности, соблюдении требований и поддержании непрерывной работы. Кроме того, интерфейс открывает простор для инноваций. Оркестраторы контейне­ ров, вроде Kubemetes и Nomad, с помощью этой плоскости управления повышают уровень абстракции, упрощая управление контейнеризированными рабочими на­ грузками в большом масштабе. Технологии service mesh, например lstio, дополняют оркестраторы, решая такие задачи, как обнаружение сервисов и обеспечение безо­ пасности в стеке приложений. Стандартный API заметно упрощает жизнь и разработчикам, ведь с помощью од­ ной команды можно создать целую среду разработки. В конвейере непрерывной интеграции мы можем запускать контейнеры с базами данных, очередями или дру­ гими зависимостями, которые нужны приложению, для интеграционного, "дымово­ го" (smoke) и сквозного тестирования. Наконец, благодаря переносимости контей­ неров разработчики несут ответственность за свою работу в рабочей среде, под­ держивая принципы DevOps. Книги для программистов: https://clcks.ru/books-for-it 16 1 Вступительное слово Версии сред выполнения регулярно обновляются, в одном проекте может сочетать­ ся несколько языков программирования, практики DevOps, вроде сине-зеленого развертывания и канареечных релизов, стали нормой, а приложения достигают не­ мыслимых масштабов, и в этих условиях разработчики по всему миру используют контейнеры для сборки и развертывания своих приложений. Контейнерами больше никого не удивишь. Более того, они стали стандартом для упаковки и развертыва­ ния приложений. Работать с контейнерами не так просто. Я знаю, что говорю, ведь я применяю их почти десять лет и вдоба�;юк учу этому людей по всему миру. Опираясь на богатый опыт, Шон и Карл написали понятное и подробное руко­ водство по использованию контейнеров с Docker. В этой книге вы найдете все, что понадобится для начала и продуктивного продолжения работы с Docker - от уста­ новки до осознанной практики и сборки образов, работы с контейнерами, проверки сборок и среды выполнения, а также развертывания контейнеров в рабочей среде. Более того, Шон и Карл вдаются в мельчайшие детали, рассказывая о таких базо­ вых компонентах, как контрольные группы и пространства имен в Linux, благодаря которым возможна работа контейнеров. Экосистема Docker непрерывно развивает­ ся и расширяется, и книга не обходит эту тему стороной. В предисловии ко второму изданию "Docker. Вводный курс" Лора Тако (Laura Tacho) подметила, что при использовании облачных технологий, вроде виртуаль­ ных машин и контейнеров, мы не обязаны ограничиваться чем-то одним - они прекрасно сочетаются друг с другом. С этим нельзя не согласиться, ведь сегодня появляются технологии на основе легких виртуальных машин, такие как Kata Containers, которые позволяют нам реализовать главные преимущества обеих тех­ нологий - изолированность виртуальных машин и переносимость контейнеров. Контейнеры сегодня присутствуют везде, и рано или поздно придется с чего-то на­ чать долгий и трудный путь их освоения. Если в качестве первого шага вы взяли эту книгу, вы сделали правильный выбор. Два опытных специалиста покажут вам дорогу, и я желаю вам удачи на этом пути, даже если с этой книгой вы сможете обойтись без нее. Счастливой контейнеризации! - Раджу Ганди (Raju Gandhi), основатель DejМacro Software, LLC, и автор книг "Head First Software Architecture", "Head First Git и JavaScript Next" @looselytyped Колумбус, Огайо Апрель 2023 года Книги для программистов: https://clcks.ru/books-for-it Предисловие Эта книга предназначена для всех, кто хочет с практической точки зрения разо­ браться в контейнерах Linux и понять, как с их помощью улучшить разработку и сопровождение приложений. В большинстве современных рабочих систем и про­ цессов интеграции разработчики и специалисты по сопровождению должны четко понимать, как работают контейнеры Linux и как с их помощью можно повысить воспроизводимость и предсказуемость системы. Вы также узнаете, как собирать, тестировать, развертывать и отлаживать контейнеры Linux в экосистеме Docker. Кроме того, мы рассмотрим некоторые популярные инструменты оркестрации для контейнеров Linux. Наконец, мы обсудим рекомендации по безопасности и органи­ зации контейнерной среды. Кому следует прочесть эту книгу Эта книга будет полезна тем, кто хочет наладить сложный рабочий процесс разра­ ботки и развертывания программного обеспечения в большом масштабе. Если вас интересуют контейнеры Linux, Docker, Kubernetes, DevOps и большие масштаби­ руемые инфраструктуры, эта книга для вас. Зачем читать эту книгу Сегодня в Интернете можно найти много обсуждений, статей и выступлений о Docker, и в некоторых даже предсказывается закат Docker. Зачем же тратить драгоценные часы на чтение этой книги? Да, сегодня есть альтернативы, но именно с Docker началось широкое распростра­ нение контейнеров Linux. Пока разработчики Docker не создали формат образов контейнеров и не поспособствовали созданию множества базовых библиотек для систем контейнеризации, работать с контейнерами Linux было очень сложно. В основном они использовались крупными облачными хостинrами для масштаби­ рования и защиты систем от недоверенноrо пользовательского кода. Docker все изменил. Вы можете найти много информации о Docker и контейнерах Linux, но экосистема постоянно развивается, и лучшие практики меняются. Если вы вычитали какой-то прием в статье четырехлетней давности, он уже может утратить свою актуальность, Книги для программистов: https://clcks.ru/books-for-it 18 Предисловие даже если все еще будет работать. Пока мы писали первое издание книги, компания Docker, lnc. успела выпустить четыре версии Docker и несколько важных инстру­ ментов. За семь лет между первым и третьим изданиями книги многое изменилось. Docker стал стабильнее, появилось немало дополнительных инструментов с похо­ жими функциями. Теперь нам доступны неплохие варианты почти для каждого аспекта рабочего процесса DevOps. Потребуется немало времени и сил, чтобы изу­ чить возможности контейнеров Linux и Docker, понять, как они впишутся в ваш рабочий процесс, и правильно интегрировать новые инструменты в свои системы. За девять лет мы успели помочь нескольким компаниям в создании и сопровожде­ нии комбинации платформ для контейнеров Linux, включая Docker, Mesos и Kubernetes. Впервые мы реализовали Docker в рабочей среде всего через несколько месяцев после его выхода и хотим поделиться с вами опытом, который накопили за все это время. Надеемся, что наши открытия помогут вам избежать ошибок, с которыми в свое время сталкивались мы. Конечно, в документации к Docker (https://docs.docker.com/) вы найдете много полезного, но мы попробуем описать более полную картину и поделиться советами, опираясь на свой опыт. После прочтения книги вы будете хорошо понимать, что собой представляют кон­ тейнеры Linux, какие возможности предлагает Docker и как с помощью этих инст­ рументов можно оптимизировать процессы - от локальной разработки до рабочей (production) среды. Мы рассмотрим несколько интересных технологий и даже опробуем их на практике. Содержание книги Материал книги распределен следующим образом: ♦ В главах 1 и 2 мы в общих чертах знакомимся с Docker и вариантами его приме­ нения. ♦ В главе 3 приводятся инструкции по установке Docker. ♦ В главах 4-6 мы рассматриваем клиент Docker, образы и контейнеры, а также методы работы с ними. ♦ В главе 7 описывается отладка образов и контейнеров. ♦ Глава 8 посвящена Docker Compose. Вы узнаете, как с его помощью значительно упростить развертывание сложных сервисов на основе контейнеров. ♦ В главе 9 мы обсудим, как обеспечить плавный переход в рабочую среду. ♦ В главе 1 О мы поговорим о масштабном развертывании контейнеров в публич­ ном и частном облаках. ♦ В главе 11 мы подробно рассмотрим расширенные концепции, которые потре­ буют более глубокого понимания Docker и помогут вам начать реализацию Docker в рабочей среде. ♦ Глава 12 посвящена альтернативным инструментам, которые пригодятся в кон­ тейнеризированных средах Linux. Книги для программистов: https://clcks.ru/books-for-it Предисловие 1 19 ♦ В главе 13 описываются общие принципы проектирования контейнерных плат­ форм нового поколения. ♦ В главе 14 мы подводим итоги и делаем выводы, в том числе вспоминаем, какие темьi обсуждали и как эти знания помогут вам разрабатывать и масштабировать сервисы. Мы понимаем, что мало кто читает техническую литературу от корки до корки, и предисловие обычно пропускают, но если вы все же его читаете, вот несколько советов по изучению книги: ♦ Если вы еще не работали с контейнерами Linux, читайте книгу с самого начала. В первых двух главах вы познакомитесь с общими принципами Docker и кон­ тейнеров Linux и узнаете, что они собой представляют, как работают _и какую пользу приносят. ♦ Если хотите начать с установки и запуска Docker на компьютере, сразу перейди­ те к главам 3 и 4, в которых приводятся инструкции по установке Docker, созда­ нию и загрузке образов, запуску контейнеров и так далее. ♦ Если вы знакомы с основами Docker и хотите узнать больше о том, как исполь­ зовать его для разработки, изучите главы 5-8, в которых мы даем рекомендации по работе с Docker и рассматриваем Docker Compose. ♦ Если вы уже используете Docker для разработки, а теперь хотите реализовать его преимущества в рабочей среде, начните с главы 9 и читайте до главы 12. В этих главах мы говорим о развертывании контейнеров, использовании кон­ тейнерных платформ и других углубленных вопросах. ♦ Архитекторов программного обеспечения и платформ может заинтересовать глава 13, где мы рассматриваем современные контейнеризированные приложе­ ния и горизонтально масштабируемые архитектуры. Обозначения В этой книге приняты следующие обозначения: ♦ Курсив - новые термины. ♦ Полужирный шрифт - URL-aдpeca, адреса электронной почты. ♦ моноширинный шрифт - листинги, а также элементы кода, например имена перемен­ ных и функций, базы данных, типы данных, переменные окружения, инструкции и ключевые слова. ♦ Моноширинный шриФ'1' полужирю.�й - команды или другой текст, который нужно вво­ дить без изменений. ♦ <Моноширинный шрифт в угловых скобках> - текст, который нужно заменить на значе­ ния пользователя или значения, определяемые контекстом. ; Эrот "ачо, обо"ачает соает или подскащ, . Книги для программистов: https://clcks.ru/books-for-it 20 Предисловие & � Этот зна,о, обозна,ает приме,ание. Этот зна,о, обозна,ает предупреждение или предостережение. Использование примеров кода Дополнительный материал (примеры кода, упражнения и т. д.) можно загрузить по адресу https://github.com/Ыuewhalebook/docker-up-and-running-3rd-edition. Это практическое руководство поможет вам в работе. Как правило, если к книге прилагается код, вы можете использовать его в своих программах и документации. Вам не требуется наше разрешение, если вы не воспроизводите значительную часть кода. Например, вы можете не спрашивать разрешение, чтобы вставить несколько фрагментов кода из книги в свою программу. Если вы продаете или распространяе­ те несколько примеров из книг O'Reilly, необходимо получить разрешение. Если вы отвечаете на вопрос, цитируя эту книгу или приводя примеры кода, это можно делать без разрешения. Если вы включаете значительную часть примеров кода из этой книги в документацию к продукту, спросите разрешение. Мы будем благодарны, если вы сошлетесь на источник, но это не обязательно. В ссылке на источник обычно указывается название, автор, издательство и ISBN книги. Например: Docker: Up & Running, 3е, Ьу Sean Р. Капе with Karl Matthias (O'Reilly). Copyright 2023 Sean Р. Капе and Karl Matthias, 978-1-098-13182-1. Если вы хотите использовать примеры кода из этой книги и вам кажется, что для этого требуется разрешение, напишите нам по адресу permissions@oreilly.com. Онлайн-обучение O'Reilly Уже более 40 лет O'Reilly Media (https://oreilly.com/) предлагает обучение, знания и аналитику в сфере технологий и бизнеса, помогая компаниям развиваться и про­ цветать. Наша уникальная сеть экспертов и новаторов делится своими знаниями и умениями в книгах, статьях и на платформе онлайн-обучения. На платформе онлайн-обучения O'Reilly вам доступны онлайн-курсы, обучающие программы, интерактивные среды программирования и большая коллекция текстовых и видеоматериалов от O'Reilly и более чем 200 других издателей. Узнайте больше на сайте https:// oreilly.com. Книги для программистов: https://clcks.ru/books-for-it Предисловие 1 21 Контактная информация Адрес для вопросов и отзывов об этой книге: O'Reilly Media, lnc. 1005 Gravenstein Highway North Sebastopol, СА 95472 800-998-9938 (США и Канада) 707-829-0515 (международный и местный) 707-829-0104 (факс). Список ошибок, примеры и дополнительную информацию по этой книге вы найдете на веб-странице https://oreil.ly/docker-up-and-running-Зe. Комментарии и вопросы по этой книге можно присылать по адресу bookquestions@oreilly.com. Новости и информация о книгах и курсах: https://oreilly.com. Liлkedin: https:/Лinkedin.com/company/oreilly-media. Twitter: https://twitter.com/oreillymedia 1 • УouTube: https://youtube.com/oreillymedia2 . Благодарности Мы хотим поблагодарить всех тех, кто помогал нам с каждым изданием этой книги: ♦ Ник Бендерс (Nic Benders), Бьорн Фриман-Бенсон (Bjorn Freeman-Benson) и Да­ на Лоусон (Dana Lawson) из New Relic - за то, что оказали нам неоценимую помощь с первым изданием и помогли найти время для работы над книгой. ♦ Роланд Трич (Roland Tritsch) и Nitro Software - за поддержку Карла в работе над вторым изданием. ♦ Лорел Рума (Laurel Ruma) из O'Reilly - за то, что предложила нам написать книгу о Docker, и Майк Лукидс (Mike Loukides) - за то, что все контролировал. ♦ Отдельная благодарность редактору первого издания, Брайану Андерсону (Brian Aлderson), который объяснил, какая работа нам предстоит, и помогал нам на каждом этапе. ♦ Никки Макдональд (Nikki McDonald) и Вирджиния Уилсон (Virginia Wilson)­ зa то, что помогали нам в работе над актуальным вторым изданием книги. ♦ Джон Девинс (John Devins), Мишель Кронин (Michele Cronin) и Элизабет Ферм (Elizabeth Faerm) - за те усилия, которые они приложили к выпуску третьего издания. 1 Организация запрещена в РФ. -Ред. 2 Организация запрещена в РФ. - Ред. Книги для программистов: https://clcks.ru/books-for-it 22 1 Предисловие ♦ Евгений (Джим) Брикман (Yevgeniy (Jim) Brikman), автор превосходной кни­ ги "Teгraform: Up & Running", - за то, что разрешил создать сайт https:// dockerupandrunning.com на основе его дизайна. ♦ Требуется особый талант, чтобы без лишних слов познакомить новую аудито­ рию с новой технологией. Мы очень благодарны Ларсу Херману (Lars Herr­ mann), Лоре Франк Тако (Laura Frank Tacho) и Раджу Ганди (Raju Ghandi) за на­ писание предисловий к изданиям книги. ♦ Редакторы черновиков, которые следили, чтобы мы не сбились с пути при напи­ сании книги: Ксения Бурлаченко (Ksenia Burlachenko), за первую рецензию и полную техническую рецензию; Эндрю Т. Бейкер (Andrew Т. Baker), Себастьен Гоасген (Sebastien Goasguen), Генри Гомес (Henri Gomez), Челси Франк (Chelsey Frank), Рашид Заруали (Rachid Zarouali), Вернер Дижкерман (Werner Dijkerrnan), Предраг Кнежевич (Predrag Кnezevic) с Вишвеш Рави Шримали (Vishwesh Ravi Shrimali). ♦ Отдельная благодарность Элис Голдфас (Alice Goldfuss) и Тому Офферману (Тот Offermann) за подробную и полезную обратную связь при написании пер­ вого издания, и Михаю Тодору (Mihai Todor) за поддержку, техническую рецен­ зию и полную обратную связь по второму изданию. ♦ Джилиан Макгарви (Gillian McGarvey), Мелани Ярбро (Melanie Yarbrough), Джастин Биллинг (Justin Billing), Рейчел Монаган (Rachel Monaghan) и Соня Саруба (Sonia Saruba)- за литературное редактирование рукописи, благодаря которому книга выглядит так, будто мы умеем грамотно писать. Уже добавлено 517 запятых, и это не предел. ♦ Сью Клефстад (Sue Кlefstad) - за помощь в подготовке третьего издания, а также Венди Каталано (Wendy Catalano) и Эллен Траутман (Ellen Troutman)­ зa работу над индексацией предыдущих изданий. ♦ Особая благодарность Нику Адамсу (Nick Adams) и всем сотрудникам O'Reilly Media, благодаря которым книга прилично выглядит во всех форматах. ♦ Нашим коллегам в New Relic и Nitro, которые вместе с нами познавали Docker, чтобы мы смогли поделиться с вами нашим общим опытом. ♦ Grains of Wrath Brewery, World Cup Coffee, McMenamins Ringlers Pub, Old Town Pizza, А Beer at а Тime!, Taylor's Three Rock и другим заведениям, которые раз­ решали нам сидеть за столиками и подключаться к розеткам, даже когда все давно было съедено. ♦ Нашим семьям - за !]Оддержку и возможность поработать в тишине. ♦ И, наконец, всем, кто поддерживал и вдохновлял нас на протяжении всего пути. Также издательство АЛИСТ выражает искреннюю благодарность научному редак­ тору Ксении Кириловой за помощь в подготовке издания этой книги на русском языке. Книги для программистов: https://clcks.ru/books-for-it ГЛАВА 1 Введение Впервые мир неожиданно услышал о Docker 15 марта 2013 года, когда Соломон Хайкс, основатель и генеральный директор компании dotCloud, рассказал о нем в пятиминутном блиц-докладе на конференции разработчиков на Python (https:// us.pycon.org/) в Санта-Кларе, штат Калифорния. На тот момент испытать Docker успели всего 40 человек за пределами dotCloud. Уже через несколько недель о Docker заговорили многие. Исходный код бьш опуб­ ликован в щкрытом доступе на GitHub (https://github.com/moby/moby). В сле­ дующие несколько месяцев в отрасли все чаще высказывалось мнение, что Docker перевернет подход к разработке, поставке и выполнению программного обеспе­ чения. Всего через год о Docker знал каждый разработчик, но мало кто понимал, зачем нужен этот инструмент и почему вокруг него столько шума. Docker обещает возможность легко инкапсулировать процесс создания распростра­ няемого программного пакета для любого приложения, чтобы можно было развер­ тывать его в произвольном масштабе в любой среде, а также оптимизировать рабо­ чие процессы и скорость работы команд, использующих методологию Agile. Ожидания от Docker Поначалу многие люди, не знакомые с Docker, воспринимали его как платформу виртуализации, когда на самом деле это бьш первый широкодоступный инструмент для более новой технологии - контейнеризации. Docker и контейнеры Linux по­ влияли на самые разные сегменты отрасли, использующие такие инструменты и технологии, как Vagrant, KVM, OpenStack, Mesos, Capistrano, AnsiЫe, Chef, Puppet и т. д. Заметили, что общего между решениями, которые потеряли свои позиции на рынке после появления Docker? Да, все эти инструменты имеют разные примене­ ния, но Docker навGеrда изменил соответствующие рабочие процессы. В основном это связано с тем, как Docker повлиял на представления о конвейере непрерывной интеграции и поставки (CI/CD)- специалисты уже не хотят тратить время на про­ движение конвейера DevOps вручную и ждут, что этот процесс будет полностью автоматизирован и переходы с этапа на этап не потребуют участия человека. Пере­ численные технологии предназначены для повышения продуктивности, и как раз это преимущество привлекло столько внимания к Docker. Docker вбирает в себя лучшее из самых эффективных технологий последнего десятилетия и позволяет заметно усовершенствовать почти каждый этап кон,вейееа. Книги для программистов: https://clcks.ru/books-for-it 24 1 Глава 1 Если детально сравнить Docker и лучший инструмент в каждой сфере (например, управление конфигурациями), скорее всего, Docker вас не поразит. В чем-то он лучше, в чем-то отстает, но его сильная сторона - набор возможностей для реше­ ния широкого ряда задач, ведь он сочетает в себе возможности для удобного тести­ рования и развертывания приложений, как в Vagrant и Capistrano, простой подход к администрированию систем виртуализации, а также интерфейсы, упрощающие автоматизацию и оркестрацию рабочих процессов. Новые технологии появляются и исчезают постоянно, и мы привыкли восприни­ мать их со здоровой долей скептицизма. Когда Docker только появился, он мог бы стать для нас очередной технологией, решающей конкретный ряд задач для разра­ ботчиков или команды сопровождения. Если воспринимать Docker только ·как технологию псевдовиртуализации или развертывания, он вряд ли сможет приятно удивить. Но Docker умеет гораздо больше. Налаживать коммуникации и процессы между командами разработчиков сложно и дорого даже в небольших организациях. При этом мы живем в мире, где обмен ин­ формацией между разработчиками все чаще становится залогом успеха. Инстру­ мент, который упрощает коммуникации и при этом помогает разрабатывать более надежное программное обеспечение, - это настоящий подарок. Именно поэтому стоит рассмотреть использование Docker. Это не панацея, и для эффективной реа­ лизации Docker в организации потребуется продуманная стратегия, но Docker и контейнеры Linux предлагают эффективный подход к решению реальных проблем и помогают компаниям быстрее поставлять на рынок качественное программное обеспечение. Хорошо отлаженный рабочий процесс для контейнеров Linux упро­ щает работу техническим специалистам и позволяет бизнесу ощутимо сэкономить. Где у компаний самое слабое место? Нелегко поставлять качественное программ­ ное обеспечение с той скоростью, какую ожидают от компаний. Если раньше в компании работали два разработчика, а потом штат разросся до нескольких ко­ манд, становится по-настоящему сложно управлять коммуникациями при выпуске новых версий. Программисты должны разбираться в тонкостях среды, в которую они поставляют свой продукт, а специалисты по сопровождению должны понимать принципы работы приложения, которое они обслуживают. Обычно эти навыки сле­ дует развивать, потому что они помогают видеть целостную картину и, следова­ тельно, разрабатывать более надежное программное обеспечение. С другой сторо­ ны, эти навыки сложно эффективно масштабировать при расширении организации. Когда несколько команд работают с одной средой, они активно взаимодействуют друг с другом, хотя этот процесс часто не приносит пользы напрямую. Например, когда разработчикам приходится просить у инженеров по сопровождению вы­ пуск 1.2. l определенной библиотеки, это замедляет работу и не добавляет прямой бизнес-ценности. Если бы разработчики могли сами обновлять используемую биб­ лиотеку, писать код, тестировать его с новой версией и поставлять, весь процесс занимал бы гораздо меньше времени, а развертывание изменения стало менее рис­ кованным. Если бы инженеры по сопровождению могли обновлять версию про­ граммного обеспечения в хост-системе, не координируя усилия с несколькими Книги для программистов: https://clcks.ru/books-for-it Введение 1 25 командами разработчиков, они работали бы быстрее. Docker помогает создать слой изоляции, который сокращает потребность в коммуникациях между людьми. Кроме того, Docker требует определенную программную архитектуру, побуждая разработчиков лучше продумывать приложения. ПодХод Docker - использовать мельчайшие одноразовые контейнеры. При развертывании вся среда выполнения старого приложения выбрасывается вместе с ним. Ни один компонент среды при­ ложения не переживет само приложение. Эта простая идея многое меняет, ведь теперь приложения не смогут случайно использовать программные пакеты, остав­ шиеся от предыдущего релиза. Это значит, что временные изменения при отладке имеют меньше шансов остаться в будущих выпусках, которые переняли их из ло­ кальной файловой системы. Получается, что приложения можно легко переносить между серверами, ведь их состояние напрямую включается в программный пакет развертывания и не изменяется. Или отправляется во внешнюю зависимость, на­ пример базу данных, кеш или файловый сервер. В результате приложение становится не только более масштабируемым, но и более надежным. Экземпляры контейнера приложения появляются и исчезают, мало влияя на стабильность работы фронтенда. Эти разумные принципы доказали свою со­ стоятельность и для приложений, не задействующих Docker, но Docker требует придерживаться этого подхода для контейнерных приложений. И это хорошо. Преимущества рабочего процесса Docker Сложно классифицировать все преимущества Docker. При правильной реализации он приносит много пользы организациям, командам, разработчикам и инженерам сопровождения. Он упрощает принятие решений об архитектуре, ведь все прило­ жения, по сути, выглядят одинаково снаружи, с точки зрения хост-системы. Он по­ могает создавать инструменты и применять их в разных приложениях. Обычно за все хорошее нужно расплачиваться, но с Docker идти на такие компромиссы почти не приходится. Вот еще несколько преимуществ, которые мы получаем от Docker и контейнеров Linux: ♦ Упаковка программного обеспечения знакомым разработчику способом. У мно­ гих компаний есть отдельные инженеры по выпуску и сборке, которые обладают специальными навыками и управляют всеми инструментами, чтобы создавать пакеты программного обеспечения для поддерживаемых платформ. Инструмен­ ты Linux, например rpm, mock, dpkg и pbuilder, могут быть сложны для исполь­ зования, и их и нужно изучать отдельно. Docker объединяет все требования в один формат упаковки - стандарт Open Container Initiative (OCI) (https:// opencontainers.org/). ♦ Объединение прwюжения и файловых систем в единый стандартизированный формат образа. Раньше приходилось упаковывать не только приложение, но и множество его зависимостей, включая библиотеки и демоны, но невозможно было гарантировать, что среда выполнения будет идентичной. При нативной компиляции это значит, что система сборки должна содержать точно такие же версии общих библиотек, что и рабочая среда. Из-за этого было сложно освоить Книги для программистов: https://clcks.ru/books-for-it 26 1 Глава 1 упаковку приложений, и у многих компаний этот процесс не был налажен. Пользователи Scientific Linux (https://scientificlinux.org/), например, часто пы­ тались развернуть пакет от сообщества, протестированный на Red Hat Enterprise Linux (https://www.redhat.com/en/technologies/linux-platforms/enterprise-linux), в надежде, что различия будут минимальными. С помощью Docker мы развер­ тываем приложение вместе со всеми файлами, которые ему понадобятся. Мно­ гослойные образы Docker обеспечивают эффективный подход, который гаран­ тирует, что приложение будет выполняться в ожидаемой среде. ♦ Использование программных пакетов для тестирования и поставки одинаковых артефактов сборки во все системы и среды. Когда разработчик отправляет из­ менения в систему контроля версий, собирается новый образ Docker, который проходит через весь процесс тестирования и развертывается в рабочей среде, при этом ни на одном этапе не требуется повторная компиляция или упаковка, если только мы сами этого не захотим. ♦ Разделение программного обеспечения от оборудования без потери ресурсов. Традиционные корпоративные решения для виртуализации, такие как VMware, обычно используются как уровень абстракции между оборудованием и выпол­ няемыми на нем приложениями, при этом потребляя много ресурсов. Гиперви­ зоры, которые управляют виртуальными машинами и их ядрами, тоже потреб­ ляют аппаратные ресурсы, забирая их у размещенных приложений. Контейнер же - это просто очередной процесс, который обычно взаимодействует напря­ мую с ядром Linux и потому может задействовать больше ресурсов, пока не из­ расходует квоту или все ресурсы системы. Когда Docker только появился, контейнеры Linux применялись уже несколько лет - как и некоторые другие технологии, на которых он построен. Однако Docker представляет собой уникальную комбинацию вариантов архитектуры и рабочих процессов, в которой целое становится гораздо сильнее, чем сумма его частей. Бла­ годаря Docker контейнеры Linux, которые находились в свободном доступе с 2008 года, стали понятными и применимыми для всех компьютерных инженеров. Docker помогает относительно легко вписать контейнеры в существующий рабочий про·цесс и операции реальных компаний. Проблемы, о которых мы говорили ранее, волновали многих, поэтому интерес к Docker возрастал гораздо быстрее, чем мож­ но было ожидать. Docker ускоренно развивался с момента своего первого появления в 2013 году и теперь обладает огромным набором возможностей и используется в огромном количестве рабочих проектов по всему миру. Он стал основной для многих совре­ менных распределенных систем и вдохновил многих инженеров на развитие этого подхода. Теперь многие компании благодаря Docker и контейнерам Linux упроща­ ют сложные процессы поставки приложений. Книги для программистов: https://clcks.ru/books-for-it Введение 27 В чем Docker не поможет Docker решает широкий ряд задач, для которых традиционно применялись другие инструменты. Однако широта иногда идет в ущерб глубине. Например, некоторые организации полностью отказываются от своего инструмента управления конфигу­ рациями, когда переходят на Docker. Да, частично Docker заменяет традиционные инструменты, но главное его преимущество в том, что он хорошо сочетается с ни­ ми и даже работает лучше в комбинации. Далее мы рассмотрим категории инстру­ ментов, которые Docker не заменяет напрямую, но можно получить превосходные результаты, если использовать их совместно: ♦ Корпоративная платформа виртуализации (VMware, КУМ и т. д.). Контейнер не является виртуальной машиной в традиционном смысле. Виртуальные машины содержат полноценную операционную систему, которая запускается поверх гипервизора, управляемого операционной системой хоста. Гипервизоры создают уровни виртуального аппаратного обеспечения, на которых можно запускать дополнительные операционные системы на одном физическом компьютере. При таком подходе можно легко использовать много виртуальных машин с самыми разными операционными системами на одном хаете. Контейнеры, с другой сто­ роны, делят одно ядро с хостом. Следовательно, контейнеры потребляют мень­ ше системных ресурсов, но должны выполняться на той же операционной сис­ теме (например, Linux). ♦ Облачная платформа (OpenStack, CloudStack и т. д.). В этой категории контейне­ ры тоже в общих чертах похожи на более традиционные облачные платформы. Оба варианта служат для горизонтального масштабирования в ответ на измене­ ние спроса. Однако Docker - это не облачная платформа. Он предназначен только для развертывания, выполнения и администрирования контейнеров на уже имеющихся хостах Docker. Он не позволяет создавать новые хает-системы (экземпляры), хранилище объектов, блочное хранилище и другие ресурсы, кото­ рые часто используются на облачной платформе. Правда, когда вы начнете рас­ ширять инструментарий Docker, вы будете замечать все больше и больше пре­ имуществ, которые обычно ожидают от облака. ♦ Управление конфигурацией (Puppet, Chef и т. д.). Docker помогает организациям управлять приложениями и их зависимостями, но не полностью заменяет более традиционный подход к управлению конфигурацией. В Dockerfile определено, как контейнер должен выглядеть на момент сборки, но эти файлы не управляют состоянием контейнера и не предназначены для управления хает-системой Docker. Правда, при использовании Docker нам и не требуется сложный код управления конфигурацией. Когда все больше серверов становятся хостами Docker, база кода для управления конфигурацией в компании заметно сокраща­ ется, и Docker служит для поставки более сложных приложений внутри стандар­ тизированных образов OCI. ♦ Фреймворк развертывания (Capistrano, Fabric и т. д.). Docker упрощает разные аспекты развертывания благодаря образам контейнеров, которые инкапсулиру­ ют все зависимости приложения и позволяют развертывать его в любых средах Книги для программистов: https://clcks.ru/books-for-it 28 Глава 1 без изменений. Правда, сам по себе Docker невозможно использовать для авто­ матизации сложного процесса развертывания - для связывания всех частей крупного рабочего процесса потребуются дополнительные инструменты. С дру­ гой стороны, Docker и другие инструменты для работы с контейнерами Linux, например Kubemetes (k8s), предоставляют хорошо определенный интерфейс для развертывания, так что метод развертывания контейнера будет одинаков на всех хостах и одного поставленного процесса развертывания хватит для большинства или даже для всех приложений на основе Docker. ♦ Среда разработки (Vagrant и т. д.). Vagrant представляет собой инструмент управления виртуальными машинами для разработчиков. Он часто используется для симуляции серверных стеков, очень похожих на реальную рабочую среду, в которой будет развернуто приложение. Кроме того, Vagrant позволяет легко запускать программное обеспечение Linux на рабочих станциях macOS и Windows. Виртуальные машины, управляемые с помощью таких инструментов, как Vagrant, помогают разработчикам избежать типичной ситуации, когда про­ граммное обеспечение прекрасно выполняется на компьютере разработчика, но не в других средах. Правда, как и в предыдуших примерах, когда вы начнете по­ стоянно работать с Docker, вам придется все реже имитировать различные рабо­ чие системы при разработке, поскольку обычно это будут одинаковые серверы с контейнерами Linux, которые можно легко воспроизводить локально. ♦ Инструмент управления рабочими нагрузками (Mesos, Kubemetes, Swarm и т. д.). Уровень оркестрации (включая встроенный режим Swarm) помогает коор­ динировать работу в пуле хостов с контейнерами Linux, отслеживать текущее состояние всех хостов и их ресурсов и вести список запущенных контейнеров. Эти системы позволяют автоматизировать выполнение регулярных задач, чтобы поддерживать работоспособность рабочего кластера. Кроме того, они предос­ тавляют инструменты, которые упрощают людям работу с динамичными кон­ тейнеризированными рабочими нагрузками. В каждой из этих категорий мы видим, какую важную роль сыграли Docker и кон­ тейнеры Linux. Контейнеры Linux позволяют выполнять программное обеспечение в контролируемой изолированной среде, а простой в использовании интерфейс ко­ мандной строки (Command Line Interface, или CLI) и стандарт образов контейнера, представленный Docker, заметно упростили работу с контейнерами и предложили воспроизводимый способ собирать программное обеспечение для любых сред. Важная терминология Вот несколько терминов, которыми мы будем пользоваться в этой книге: ♦ Клиент Docker - это команда docker, используемая для управления большей частью рабочего процесса Docker и взаимодействия с удаленными серверами Docker. ♦ Сервер Docker - это команда dockerd для запуска серверного процесса Docker, который собирает и запускает контейнеры через клиент. Книги для программистов: https://clcks.ru/books-for-it Введение 1 29 ♦ Образы Docker или OCI - образы Docker и OCI состоят из нескольких слоев файловой системы и важных метаданных. Они представляют все файлы, необ­ ходимые для выполнения контейнерного приложения. Один образ можно ско­ пировать на несколько хостов. У образа обычно есть адрес репозитория, имя и тег. Тег служит для идентификации выпуска образа (например, docker.io/ superorbltal/wordchain:vl.0.1). Образ Docker - это любой образ, совместимый с инструментами Docker, а образ OCI- это образ, соответствующий стандарту Open Container Initiative, который гарантированно будет работать с любым инст­ рументом, соответствующим требованиям OCI. ♦ Контейнер Linux - это контейнер, созданный из образа Docker или OCI. Опре­ деленный контейнер может существовать всего один раз, но из одного образа можно создать несколько контейнеров. Термин контейнер Docker неточен, т. к. Docker просто использует функционал контейнеров операционной системы. ♦ Минимальный, или неизменяемый, хает - это небольшой, тщательно настроен­ ный образ ОС, например Fedora CoreOS (https://getfedora.org/en/coreos), кото­ рый поддерживает хостинг контейнеров и атомарные обновления. Заключение Без определенной подготовки сложно сразу понять всю философию Docker. В сле­ дующей главе мы в общих чертах рассмотрим, что такое Docker, как с ним нужно работать и какие преимущества он дает при правильном применении. Книги для программистов: https://clcks.ru/books-for-it ГЛАВА 2 Общие сведения о Docker Прежде чем перейти к конфигурированию и установке,. мы поговорим о том; что представляет собой Docker и кюще преимущества он дает. Это мощная, но не слишком сложная технология. В данной главе мы рассмотрим общие · принципы работы Docker и ,контейнеров Linux, их главные достоинства и варианты примене­ ния. Если вы читаете эту книгу, скорее всего, у вас есть причины использовать кон­ тейнеры, но 'дополнительное понимание никогда не помешает. Не волнуйтесь, эта глава не будет длинной. Уже в следующей гл.аве вы узнаете, как установить Docker и запустить его в своей системе. Упрощение рабочих процессов ' Поскольку Docker - это программное обеспечение, не все понимают, что при правильной реализации он может улучшить бизнес-процерсы и коммуникации между командами специалистов. Давайте копнем глубже и узнаем, как Docker и контейне­ ры Linux упрощают рабочие процессы и взаимодействия. Обычно все начинается с развертывания. Традиционно цикл поставки приложения· в рабочую среду выгля­ дит так, как показано на рис. 2.1: 1. Разработчики приложения запрашивают ресурсы у инженеров сопровождения. 2. Инженеры сопровождения выделяют ресурсы и передают их разработчикам. 3. Разработчики пишут скрипт для развертьmания. 4. Инженеры сопровождения и разработчики несколько раз вносят корректировки. 5. Разработчики обнаруживают дополнительные зависимости приложения. 6. Инженеры сопровождения устанавливают требуемые зависимости. 7. Этапы с 4-го по 6-й повторяются п раз. 8. Приложение развертывается. Как показывает опыт, развертывание нового приложения в рабочую среду тради­ ционным способом может занять большую часть недели, если речь идет о сложной новой системе. Это не самый продуктивный подход, и хотя практики DevOps зна­ чительно упрощают задачу, все-таки разработчики прикладывают много усилий и активно взаимодействуют друг с другом. Это технически сложный и дорогой про­ цесс. Хуже того, он ограничивает инновации, которые •команды разработчиков мог­ ли бы внедрить в будушем. Если развертывание программного обеспечения отниКниги для программистов: https://clcks.ru/books-for-it Общие сведения о Oocker 1 31 мает много сил и времени и требует привлечения другой команды специалистов, разработчики пытаются встраивать новые функции в существующее приложение, чтобы лишний раз не запускать сложный процесс развертывания. Или еще хуже­ они могут просто избегать проблем, которые требуют написания нового программ­ ного кода. Команда разработки Команда сопровождения Запрос ресурсов (1) Подготовка ресурсов (2) Скрипт развертывания (3) Приложение развернуто (8) Рис. 2.1. Традиционный процесс развертывания (без Docker) Автоматизированные системы развертывания, вроде Heroku (https://www. heroku.com/), работающие по модели push-to-deploy, показывают разработчикам, как легко нам всем работалось бы, если бы мы могли контролировать свое прило­ жение и большую часть зависимостей. Разработчики часто говорят, насколько Heroku и подобные платформы облегчают им жизнь. Если вы занимаетесь сопро­ вождением программного обеспечения, скорее всего, вы слышали жалобы на то, как медленно работают внутренние системы по сравнению с автоматизированными решениями, подобными Heroku, которые используют технологию контейнеров Linux и с помощью которых развернуть приложение можно путем нескольких на­ жатий кнопок. Heroku - это полноценная среда, не просто движок для контейнеров. Docker не пытается воспроизвести все функции, доступные в Heroku, зато предоставляет чет­ кое разделение обязанностей и инкапсуляцию зависимостей, помогая повысить производительность. Docker даже дает более детализированный контроль, чем Heroku, потому что разработчики могут управлять почти всем, включая точные версии файлов и пакетов, которые поставляются с приложением. Некоторые инст­ рументы и оркестраторы, построенные на Docker (например, Kubemetes, Docker Swarm и Mesos), стремятся воспроизвести простоту, предоставляемую системами вроде Heroku. Эти платформы расширяют возможности Docker, поддерживая более функциональные и сложные среды, но при использовании одного только Docker мы Книги для программистов: https://clcks.ru/books-for-it 32 Глава 2 получаем все основные преимущества без дополнительных сложностей, связанных с большими системами. Продукты Docker предоставляются по принципу "батарейки в комплекте, но можно заменить". Это значит, что у них есть все функции, которые могут понадобиться для работы, но их можно легко заменять для создания собственных решений. Docker использует репозиторий образов для передачи ответственности, чтобы зада­ чи по сборке образа приложения можно было отделить от задач по развертыванию и сопровождению контейнера. На практике это означает, что разработчики могут сами собирать приложение с нужными зависимостями, запускать его в средах раз­ работки и тестирования, а потом просто поставлять в точности такой же пакет со всеми зависимостями в рабочую среду. Поскольку снаружи все эти пакеты выгля­ дят одинаково, инженеры по сопровождению могут собирать или устанавливать стандартные инструменты для развертывания и запуска приложений. В этом случае цикл, показанный на рис. 2.1, выглядит как на рис. 2.2: 1. Разработчики собирают образ Docker и отправляют его в реестр. 2. Инженеры по сопровождению предоставляют детали конфигурации для контей­ нера и выделяют ресурсы. 3. Разработчики запускают развертывание. Команда разработки Команда сопровождения Сборка образа Предоставление данных конфиrурации Развертывание Рис. 2.2. Процесс развертывания с Docker Это возможно, потому что Docker позволяет обнаруживать все проблемы с зависи­ мостями еще на этапах разработки и тестирования. К тому времени, как приложе­ ние окажется готово к первому развертыванию, вся эта работа уже будет продела­ на, причем без активных коммуникаций между разработчиками и инженерами сопровождения. В хорошо продуманном конвейере созданием и развертыванием нового сервиса не придется заниматься никому, кроме самих разработчиков. Это гораздо проще и быстрее. Более того, это позволяет повысить надежность про­ граммного обеспечения, поскольку еще до выпуска оно будет протестировано в среде развертывания. Книги для программистов: https://clcks.ru/books-for-it Общие сведения о Docker 1 33 Широкая поддержка и внедрение Docker широко поддерживается коммерческими решениями, в том числе почти всеми крупными провайдерами публичных облаков. Например, Docker и контейне­ ры Linux применяются в Amazon Web Services (АWS) для разных продуктов, вклю­ чая Amazon Elastic Container Service (Amazon ECS), Amazon Elastic Kubernetes Service (Amazon EKS), Amazon Fargate и Amazon Elastic Beanstalk. Контейнеры Linux можно использовать в Google Арр Engine (GAE), Google Kubernetes Engine, Red Hat OpenShift, ШМ Cloud, Microsoft Azure и других платформах. На конферен­ ции DockerCon 2014 Эрик Брюэр из Google объявил, что Google будет использовать Docker как основной формат внутренних контейнеров. Для компаний такие объяв­ ления были хорошим маркетингом, а для сообщества Docker это означало, что в поддержание стабильности и развития платформы будет вкладываться много средств. Более того, формат образов Docker для контейнеров Linux стал единым стандартом среди облачных провайдеров, что позволяет переносить облачные приложения ме­ жду платформами без изменений кода. Когда Docker выпустил библиотеку разра­ ботки libswarm, один инженер из Orchard продемонстрировал, как можно развер­ нуть контейнер Linux в разнородной среде из нескольких облаков одновременно. Раньше это было бы слишком сложно, потому что у каждого облачного провайдера был свой API или набор инструментов для управления инстансами. Инстансы обычно были мельчайшим элементом, которым можно управлять с помощью API. Если в 2014 году мы только мечтали об этом, сейчас это тренд, и крупнейшие ком­ пании продолжают инвестировать в Docker, поддержку платформы и инструменты. Поскольку большинство провайдеров так или иначе предлагают оркестрацию Docker и контейнеров Linux, а также саму среду выполнения контейнеров, сейчас Docker поддерживается почти для любых рабочих нагрузок в стандартных рабочих средах. Если вся система построена вокруг Docker и контейнеров Linux, приложе­ ния можно развертывать одинаково независимо от облачной платформы. Раньше о такой гибкости можно было только мечтать. В 2017 году Docker передал среду выполнения containerd (https://thenewstack.io/ docker-donate-container-runtime-containerd-cloud-native-computing-foundation) фонду Cloud Native Computing Foundation (CNCF), и в 2019 году проект был завер­ шен и получил статус graduated. Сегодня контейнеры Linux все чаще применяются в разработке, поставке и рабочей среде. В 2022 году Docker стал уступать свою долю на рынке более новым версиям Kubemetes, которым не требовался демон Docker, но даже эти версии Kubernetes активно задействуют среду выполнения containerd, изначально разработанную Docker. Docker по-прежнему используется во многих рабочих процессах разработ­ ки и конвейерах CI/CD. Какие ОС совместимы с Docker? Клиент Docker работает напрямую на большинст­ ве главных операционных систем, а сервер может работать на Linux или Win­ dows Server. Большая часть экосистемы построена на серверах Linux, но развивается поддержка и для других платформ. Скорее всего, мы и дальше будем чаще всего использовать контейнеры и серверы Linux. Книги для программистов: https://clcks.ru/books-for-it 34 1 Глава 2 Мы можем запускать контейнеры Windows нативно (без виртуальной машины) на 64-разрядных версиях Windows Server 2016 и выше. Правда, для 64-разрядных версий Windows 10+ Professional все еще нужен Hyper-V, который предоставляет ядро Windows Server для контейнеров Windows. Подробнее об этом мы поговорим в главе 5 в разделе "Контейнеры Windows". Стоит отметить, что на Windows можно запускать контейнеры Linux без виртуаль­ ной машины, если использовать WSL 2 (Windows Subsystem for Linux, версия 2). Чтобы поддержать растущий спрос на инструменты Docker в средах разработки, Docker выпустил простые для использования реализации для macOS и Windows. Кажется, что все работает нативно, но на самом деле реализована небольшая вир­ туальная машина Linux, которая предоставляет сервер Docker и ядро Linux. Docker традиционно разрабатывается на дистрибутиве Ubuntu Linux, но сейчас по возмож­ ности поддерживается большинство дистрибутивов Linux и другие популярные операционные системы. Red Hat, например, полностью перешли на контейнеры, и все их платформы превосходно поддерживают Docker. Поскольку в мире Linux сейчас почти все работает на контейнерах, появляются такие дистрибутивы, как Red Hat Fedora CoreOS, созданные специально для контейнерных рабочих нагрузок Linux. В первые годы после выхода Docker некоторые конкуренты и поставщики услуг выражали беспокойство по поводу проприетарного формата образов Docker. У кон­ тейнеров на Linux не было стандартного формата образов, поэтому в Docker, lnc. создали свой вариант в соответствии с потребностями своего бизнеса. Поставщики услуг и вендоры коммерческих решений не хотели создавать плат­ формы, завися от прихотей конкурента, и в этот период в сторону Docker раздава­ лись публичные обвинения. Чтобы улучшить свою репутацию и расширить распро­ странение на рынке, в июне 2015 года в Docker, Inc. решили поддержать инициати­ ву Open Container lnitiative (OCI) (https://www.opencontainers.org/). Первая полная спецификация была выпущена в июле 2017 года, и она по большей части повторяла вторую версию формата образов Docker. Теперь можно пройти сертификацию OCI для образов контейнеров и сред выполнения контейнеров. Основная среда выполнения с сертификацией OCI - это containerd (https:// containerd.io/) - высокоуровневая среда по умолчанию в современных версиях Docker и Kubemetes. Перечисленные далее низкоуровневые среды выполнения, сертифицированные OCJ, могут использоваться средой containerd для администрирования и создания контей­ неров: ♦ runc (https://github.com/opencontainers/runc) - часто используется как низко­ уровневая среда по умолчанию в containerd. ♦ crun (https://github.com/containers/crun) - написана на С; она быстрая и по­ требляет мало памяти. ♦ Kata Containers (https://katacontainers.io/) от lntel, Hyper и OpenStack Foun­ dation - виртуализированная среда выполнения, в которой может работать смесь контейнеров и виртуальных машин. Книги для программистов: https://clcks.ru/books-for-it Общие сведения о Docker 1 35 ♦ gVisor (https://github.com/google/gvisor) от Google - изолированная среда вы­ полнения, полностью реализованная в пространстве пользователя. ♦ NаЫа Containers (https://naЫa-containers.github.io/) - еще одна изолированная среда выполнения, которая помогает значительно сократить поверхность атаки для контейнеров Linux. Также продолжают развиваться решения для развертывания контейнеров и оркест­ рирования целых систем контейнеров. Многие из них поставляются с открытым исходным кодом и доступны как для локальной, так и для облачной среды или по модели SaaS (Software as а Service - программное обеспечение как услуга) от раз­ ных поставщиков в публичных или частных облаках. Учитывая огромные инвести­ ции в развитие контейнеров Linux, скорее всего, Docker продолжит играть важную роль в современном Интернете. Архитектура Docker - эффективная и функциональная технология, а потому связанные с ним инструменты и процессы не отличаются простотой. Docker действительно довольно сложно устроен, но со стороны пользователя представляет собой понятную мо­ цель "клиент-сервер". За Docker API находятся несколько компонентов, включая containerd и runc, но базовое взаимодействие осуществляется между клиентом и сервером через API. За кажущейся простотой скрывается использование различных механизмов ядра, включая iptaЫes, виртуальные мосты, контрольные группы Linux (cgroups), пространства имен Linux, capabilities (привилегии) Linux, secure computing mode (безопасный режим вычислений), драйверы различных файловых систем и многое другое. О некоторых из этих аспектов мы поговорим в главе 11. А пока давайте узнаем, как работают клиент и сервер, и кратко рассмотрим сетевой уровень, находящийся под контейнерами Linux в Docker. Клиент-серверная архитектура Проще всего представить Docker в виде двух частей: клиент и сервер/демон (рис. 2.3). К ним может добавляться третий компонент- реестр (registry), который к.ранит образы Docker и их метаданные. Сервер отвечает за сборку, выполнение и СсрвРр IJo<:kf'I Hd хосте или lJM L111LJX Клиент Docker на локальной рабочей станции Рис. 2.3. Клиент-серверная архитектура Docker Книги для программистов: https://clcks.ru/books-for-it 36 1 Глава 2 администрирование контейнеров, а с помощью клиента мы отдаем серверу коман­ ды. Демон (https://en.wikipedia.org/wiki/Daemon_(computing)) Docker может вы­ полняться на любом числе серверов в инфраструктуре, и один клиент может обра­ щаться к любому числу серверов. Клиенты управляют всеми коммуникациями, но серверы Docker могут напрямую взаимодействовать с реестрами образов, если так велит клиент. Клиенты отдают серверам команды, а серверы отвечают за размеще­ ние контейнерных приложений и управление ими. По структуре Docker немного отличается от другого клиент-северного программно­ го обеспечения. У него есть клиент docker и сервер dockerd, но сервер не является монолитным и оркестрирует еще несколько компонентов от имени клиента, вклю­ чая containerd-shim-runc-v2, который используется для взаимодействия с runc и containerd. Docker аккуратно прячет все сложности за простым серверным API, так что в большинстве случаев мы видим только взаимодействие клиента и сервера. У каждого хоста Docker обычно есть один сервер Docker, который может управлять любым числом контейнеров. С сервером можно общаться с помощью интерфейса командной строки Docker - с самого сервера или, если приняты надлежащие меры безопасности, с удаленного клиента. Чуть позже мы поговорим об этом подробнее. Сетевые порты и сокеты Unix Интерфейс командной строки docker и демон dockerd взаимодействуют через сокеты Unix и сетевые порты. Компания Docker, Inc. зарегистрировала в IANA (lnternet Assigned Numbers Authority - Администрация адресного пространства Интернета, https://www.iana.org/) три порта для демона и клиента Docker: ТСР-порт 2375 для незашифрованного трафика, порт 2376 для зашифрованных SSL-соединений и порт 2377 для режима Docker Swarm. При желании можно легко настроить другие порты. По умолчанию установщик Docker осуществляет коммуникацию с локаль­ ным демоном Docker только через сокет Unix. Это самые безопасные системные настройки по умолчанию, но их легко можно менять. Правда, настоятельно реко­ мендуется не использовать сетевые порты с Docker, потому что у демона нет меха­ низмов аутентификации пользователей и управления доступом на основе ролей. Сокет Unix может располагаться по разным путям в разных операционных систе­ мах, но обычно находится здесь: /var/run/docker.sock. При желании можно выбрать другое расположение, указав его при установке или изменив конфигурацию серве­ ра позже, а затем перезагрузив демон. В принципе, параметры по умолчанию под­ ходят для большинства сценариев. Как и с другими решениями, выбор параметров по умолчанию позволяет сэкономить много времени и сил. Недавние версии Docker Desktop моrут создавать файл docker.sock в корневом каталоге пользователя внутри .docker/run/, а потом просто создавать ссылку /var/run/docker.sock к этому местоположению. Книги для программистов: https://clcks.ru/books-for-it Общие сведения о Docker 1 37 Надежные инструменты Одна из причин популярности Docker - простые и надежные инструменты. С мо­ мента выпуска возможности Docker постоянно расширяются благодаря усилиям сообщества. Инструменты, которые поставляются с Docker, поддерживают сборку образов Docker, базовое развертывание в индивидуальные демоны Docker, распре­ деленный режим Swarm и все функции, необходимые для управления удаленным сервером Docker. Кроме режима Swarm сообщество работает над решениями для управления целыми кластерами серверов Docker, а также для планирования и орке­ страции развертываний контейнеров. Когда мы говорим о Docker Swarm или режиме Swarm (https://docs.docker.com/ engine/swarm) в этой книге, мы имеем в виду встроенный функционал Swarm в клиенте и сервере Docker, который использует базовую библиотеку SwarmKit. � В Интернете могут попадаться упоминания более старой, отдельной версии Docker Swarm, которая сегодня обычно называется классическим Docker Swarm (https:// github.com/docker-archive/classicswarm). Docker также запустил свой набор инструментов для оркестрации, включая Compose (https://github.com/docker/compose), Docker Desktop (https://www. docker.com/products/docker-desktop) и режим Swarm, который обеспечивает связ­ ный процесс развертывания для разработчиков. Продукты Docker для оркестрации в рабочей среде обычно заметно уступают Kubemetes от Google, хотя не забывайте, что Kubemetes активно использовал Docker до выпуска версии 1.24 в начале 2022 года (https://kubernetes.io/Ыog/2020/12/02/dockershim-faq). Инструменты оркестрации Docker все же имеют свое применение. Например, Compose очень удобен для локальной разработки. Поскольку у Docker есть как инструмент командной строки, так и удаленный REST API, к нему можно добавлять инструменты на любом языке. При помощи инструмента командной строки мы пишем shеll-скрипты, и все, что способен де­ лать клиент, можно запрограммировать с использованием REST API. Docker CLI так широко известен, что многие другие интерфейсы командной строки для кон­ тейнеров Linux, например podman (https://podman.io/) и nerdctl (https://github.com/ containerd/nerdctl), используют его аргументы для совместимости и простоты вне­ дрения. Интерфейс командной строки Docker CLI Docker - это главный интерфейс, через который большинство людей взаимо­ действуют с Docker. Клиент Docker - это программа на Go (https://golang.org/), которая компилируется и выполняется на всех популярных архитектурах и опера­ ционных системах. Интерфейс командной строки поставляется в комплекте с Docker на разных платформах и напрямую компилируется из кода Go. Вот непол­ ный список задач, которые можно выполнять с помощью Docker CLI: ♦ сборка образа контейнера; ♦ извлечение образов из реестра в демон Docker и обратно; Книги для программистов: https://clcks.ru/books-for-it 38 Глава 2 ♦ запуск контейнера на сервере Docker на переднем плане или в фоновом режиме; ♦ извлечение журналов Docker с удаленного сервера; ♦ интерактивное выполнение команды в работающем контейнере на удаленном сервере; ♦ мониторинг статистики о контейнере; ♦ просмотр процессов, запущенных в контейнере. Наверное, вы уже представляете себе, как из этих задач можно составить рабочий процесс сборки, развертывания и сопровождения приложений. Интерфейс команд­ ной строки Docker - не единственный способ взаимодействия с Docker, и даже не всегда самый эффективный. Docker Engine API У демона Docker, как и у остальных современных программ, есть API. Именно че­ рез его интерфейс командной строки Docker взаимодействует с демоном. Посколь­ ку API задокументирован и общедоступен, внешние инструменты часто использу­ ют его напрямую. Это удобный механизм, с помощью которого можно в любом инструменте создавать, проверять и администрировать все образы и контейнеры, находящиеся под управлением демона Docker. Вряд ли новички сразу будут обра­ щаться к Docker АРТ напрямую, но это очень удобный способ, который стоит иметь в виду. По мере внедрения Docker в организации вы будете все чаще замечать, как удобно работать с инструментами через API. На сайте Docker можно найти подробную документацию для API (https://dockr.ly/ 2wxCHnx). По мере развития экосистемы появились хорошие реализации библио­ тек Docker API для всех популярных языков. Компания Docker предлагает SDK для Python и Go (https://dockr.ly/2w_xCHnx), а у третьих сторон есть неплохие библио­ теки для других языков. Например, мы несколько лет используем эти библиотеки для Go (https://github.com/fsouza/go-dockerclient) и Ruby (https://github.com/ upserve/docker-api), и они не только хорошо продуманы, но и своевременно обновляются с выпусками новых версий Docker. API позволяет выполнять почти те же задачи, что и интерфейс командной строки Docker. Правда, есть пара исключений - конечные точки, которым требуется по­ токовая передача или терминальный доступ: использование удаленных командных оболочек (shell) или выполнение контейнера в интерактивном режиме. В этих слу­ чаях обычно проще задействовать одну из упомянутых эффективных клиентских библиотек или интерфейс командной строки. Сеть для контейнеров Контейнеры Linux в основном состоят из процессов, выполняемых прямо в хост­ системе, но на сетевом уровне они ведут себя совсем не так, как другие процессы. Изначально Docker поддерживал только одну сетевую модель, а сейчас доступны различные конфигурации, которые подойдут для большинства приложений. ОбычКниги для программистов: https://clcks.ru/books-for-it Общие сведения о Docker 1 39 но контейнеры запускаются в конфигурации по умолчанию - в режиме моста. Давайте посмотрим, как это работает. Для этого попробуйте представить, что каждый контейнер Linux ведет себя как хост в частной сети. Сервер Docker в этом случае выступает как виртуальный мост, а контейнеры - клиенты, которые находятся за ним. Мост - это просто сетевое устройство, передающее трафик с одной стороны на другую. Представьте себе вир­ туальную мини-сеть, в которой каждый контейнер похож на хост, подключенный к этой сети. Фактически у каждого контейнера есть виртуальный интерфейс Ethemet, подключенный к мосту Docker, и IР-адрес, назначенный виртуальному интерфейсу (рис. 2.4). Docker позволяет привязывать отдельные порты и группы портов на хосте к контейнерам, чтобы можно было обращаться к ним извне. Тра­ фиком в основном управляет библиотека vpnkit (https://github.com/moby/vpnkit). Виртуальная сеть Сервер Docker Входящий запрос к открытому порту ТСР 10520 elhO 10.0.0.7 NAT dockerO 172.16.23.7 1111 • • •' "ii 172.16.23.1 1 1 Исходящий запрос • от контейнера Рис. 2.4. Сеть на типичном сервере Docker Docker выделяет частные подсети из неиспользуемого блока частных подсетей (RFC 1918, https://www.rfc-editor.org/rfc/rfc1918). Он определяет, какие блоки не используются на хосте, и назначает один из них виртуальной сети. Она соединяется мостом с локальной сетью хоста через интерфейс на сервере, который называется ctockerO. Это означает, что по умолчанию все контейнеры находятся в сети и могут взаимодействовать друг с другом напрямую, но для обращения к хосту или внеш­ нему миру им требуется интерфейс виртуального моста ctockerO. Существует много способов настроить сетевой уровень Docker: от назначения сво­ их сетевых блоков до настройки пользовательского интерфейса моста. Многие вы­ бирают механизмы по умолчанию, но иногда приложению требуется более сложная или специализированная конфигурация. Больше сведений о сетях Docker можно найти в документации (https://dockr.Iy/2otp461). Подробнее об этом мы поговорим в главе 11. Книги для программистов: https://clcks.ru/books-for-it 40 1 Глава 2 При создании рабочего процесса Docker советуем начать с сети по умолчанию. В дальнейшем может оказаться, что виртуальная сеть по умолчанию вам не под­ ходит. Можно настроить сеть отдельно для каждого контейнера или полностью � отключить виртуальную сеть для контейнера, указав параметр --net=host для docker container run. В этом режиме контейнеры Linux используют сетевые уст­ ройства и адреса хоста, обходясь без виртуальных интерфейсов или мостов. При работе с сетью хоста необходимо помнить о безопасности. Возможны и другие топологии сети. Мы поговорим о них в главе 11. Как использовать весь потенциал Docker Как и большинство инструментов, Docker прекрасно подходит для одних ситуаций и не годится для других. Например, стеклянную банку можно открыть молотком, но это не всегда удобно. Лучше заранее изучить, как эффективнее всего применять инструмент, или хотя бы определить, подходит ли он для ваших целей. В первую очередь Docker предназначен для приложений, которые не хранят со­ стояние (stateless) или у которых состояние хранится отдельно, в базе данных или кеше. Такие приложения проще всего контейнеризовать. Docker заставляет при­ держиваться разумных принципов разработки подобных приложений, и позже мы поговорим о том, почему это хорошо. Правда, если мы попытаемся поместить в Docker СУБД, то будем, образно выражаясь, плыть против течения. Нельзя ска­ зать, что это невозможно или запрещено - просто это не самый подходящий сце­ нарий для Docker, и если вы начнете с этого, скорее всего, почти сразу разочаруе­ тесь. Развертывать базы данных на Docker можно, но потребуются усилия. Знаком­ ство с Docker лучше всего начать с веб-фронтендов, бэкенд API и коротких задач, например скриптов сопровождения, для которых обычно можно использовать cron. Если сначала вы попробуете разобраться с контейнеризацией приложений без сохранения состояния или с внешним состоянием, то у вас будет фундамент, на ко­ тором потом можно экспериментировать с другими вариантами применения. Мы рекомендуем начать с приложений без сохранения состояния, и только набравшись опыта, переходить к другим сценариям. Сообщество постоянно работает над под­ держкой приложений с сохранением состояния в Docker, и, скорее всего, нас ждут подвижки в этой области. Контейнеры - это не виртуальные машины Чтобы лучше понять Docker, попробуйте представить себе контейнеры Linux не как виртуальные машины, а как очень легкие обертки для одного процесса Unix. При реализации этот процесс может порождать другие процессы, но внутри него может находиться только статически компилируемый двоичный код (см. раздел "Виешние зависимости" главы 9). Контейнеры "эфемерны" - они появляются и исчезают гораздо быстрее и проще, чем традиционная виртуальная машина. Виртуальные машины функционируют как замена реального оборудования, кото­ рое можно установить в стойку и оставить там на несколько лет. Они работают вместо реального сервера, а потому могут существовать долго. Даже в облаке, где Книги для программистов: https://clcks.ru/books-for-it Общие сведения о Docker 1 41 виртуальные машины активируются и отключаются по запросу, обычно они рабо­ тают по несколько дней. Контейнер может существовать месяцами, а может созда­ ваться для минутной задачи, а затем уничтожаться. Это нормально, но виртуальные машины обычно используются совсем не так. Еще один пример: если мы запускаем Docker на mac или Windows, то используем виртуальную машину Linux для выполнения dockerd, сервера Docker. А на Linux можно запускать dockerd нативно, т. е. виртуальная машина ни для чего не понадо­ бится (рис. 2.5). Dщ:ker с виртуальной машиной (Windows, macOS и т. д.) Нативный Docker (Linux) Рис. 2.5. Типичные варианты установки Docker Ограниченная изоляция Контейнеры изолированы друг от друга, но изоляция не такая полная, как можно ожидать. Мы можем установить ограничения на ресурсы, но по умолчанию кон­ тейнеры будут разделять одни и те же ресурсы ЦП и памяти в хост-системе, как и следует процессам Unix. Получается, что без дополнительных ограничений кон­ тейнеры будут состязаться за ресурсы. Возможно, вас это устраивает, но нужно будет продумать некоторые аспекты. В Docker рекомендуется ограничивать потребление ЦП и памяти, но в большинстве случаев не существует лимитов по умолчанию, как у виртуальных машин. Часто много контейнеров используют одни и те же уровни файловой системы. Это одно из преимуществ Docker, но при обновлении общего образа придется пере­ строить и заново развернуть контейнеры, задействующие старый образ. Контейнеризованные процессы - это процессы на самом сервере Docker. Они вы­ полняются на том же экземпляре ядра Linux, что и операционная система хоста. Все процессы контейнера отображаются в обычных выходных данных ps на сервере Docker. Этот подход существенно отличается от гипервизора, в котором изоляция процессов предполагает отдельный экземпляр ядра ОС на каждую виртуальную машину. Из-за слабой изоляции может возникнуть искушение предоставить больше ресур­ сов хоста, например общие файловые системы для хранения состояния. Дважды подумайте, прежде чем предоставлять ресурсы хоста контейнеру, - они должны Книги для программистов: https://clcks.ru/books-for-it 42 Глава 2 использоваться только этим контейнером. Мы поговорим о безопасности контейне­ ров позже, но, как правило, можно усилить изоляцию с помощью политик Security­ Enhanced Linux (SELinux) (https://www.redhat.com/en/topics/linux/what-is-selinux) или AppArmor (https://apparmor.net/), не нарушая существующие барьеры. По умолчанию многие контейнеры используют UID О дпя запуска процессов. По­ скольку контейнер ограничен, может показаться, что это безопасно, но на самом деле это не совсем так. Все выполняется на одном ядре, поэтому из-за многих ти­ пов уязвимостей или просто ошибок конфигурации пользователь root контейнера · может получить несанкционированный доступ к системным ресурсам, файлам и процессам хоста. Подробнее о решении этих проблем мы поговорим в разделе "Безопасность" главы 11. Контейнеры легковесны Чуть позже мы обсудим, как это работает, а пока просто упомянем, что создание контейнера может занимать очень мало места на диске. Простой тест покажет: только что созданный из существующего образа контейнер занимает всего 12 КБ дискового пространства. Это очень мало. Виртуальная машина, созданная из. эта­ лонного образа (golden image), может весить сотни и тысячи мегабайт, потому что ей как минимум требуется полноценная операционная система. Новый контейнер занимает так мало места, потому что представляет собой просто ссылку на много­ слойный образ файловой системы и включает немного метаданных о конфигура­ ции. По умолчанию для контейнера не создаются никакие копии данных. Контей­ нер - это процесс в существующей системе, который, возможно, просто будет считывать информацию с диска, и ему может не потребоваться собственная копия данных, пока не придет время записать данные, уникальные для конкретного экземпляра контейнера. Благодаря такой легкости контейнеры целесообразны в ситуациях, в которых соз­ давать виртуальную машину было бы слишком затратно или известно, что процесс просуществует очень короткое время. Вряд ли кто-то станет запускать целую вир­ туальную машину, чтобы выполнить команду curl на сайте из удаленного располо­ жения. Зато для этой цели очень удобен контейнер. Сдвиг в сторону неизменяемой инфраструктуры Если вы развертываете большую часть приложений в контейнерах, можно упро­ стить управление конфигурацией, перейдя на неизменяемую инфраструктуру, в которой компоненты заменяются полностью, а не правятся на месте. Идея неиз­ меняемой инфраструктуры набирает популярность, потому что в реальности очень сложно поддерживать кодовую базу для управления действительно идемпотентной конфигурацией. По мере разрастания кодовая база может выйти из-под контроля, как крупные монолитные приложения. Легкий сервер Docker почти не требует управления конфигурацией, а иногда и не требует совсем. Мы управляем приложением, просто развертывая нужные контей­ неры на сервере. Если серверу требуется важное обновление, например для демона Книги для программистов: https://clcks.ru/books-for-it Общие сведения о Docker 1 43 Docker или ядра Linux, мы просто поднимаем новый сервер с изменениями, развер­ тываем на нем контейнер, а старый сервер останавливаем или переустанавливаем. Дистрибутивы Linux на базе контейнеров, например Red Hat Fedora CoreOS (https://getfedora.org/en/coreos), построены как раз на этом принципе, но нам не приходится выводить экземпляр из эксплуатации, потому что Fedora CoreOS пол­ ностью обновляется и переходит на обновленную ОС. Конфигурация и рабочая нагрузка остаются в контейнерах, и нам почти не требуется настраивать ОС. Благодаря четкому разделению между развертыванием и конфигурацией серверов многие используют для рабочей среды такие инструменты, как HashiCorp Packer (https://www.packer.io/intro/index.html), чтобы собирать образы облачных вирту­ альных серверов, а затем при помощи Docker полностью или почти полностью обходятся без систем управления конфигурацией. Приложения без сохранения состояния Веб-приложение, которое храни1:_ состояние в базе данных, - идеальный кандидат на контейнеризацию. Приложения без сохранения состояния должны сразу отве­ чать на отдельный запрос и не отслеживают информацию между запросами от одного или нескольких клиентов. Кроме того, в контейнерах можно запускать эфе­ мерные экземпляры Memcached (https://memcached.org/). Скорее всего, ваше веб­ приложение хотя бы отчасти хранит состояние локально, например файлы конфи­ гурации. Может показаться, что это немного, но если придется встраивать конфи­ гурацию в образы, то мы не сможем без ограничений повторно использовать образ и будет сложнее развертывать его в разных средах, не храня несколько образов для различных целевых платформ развертывания. Во многих случаях в процессе контейнеризации мы переносим состояние конфигу­ рации приложения в переменные среды, которые можно передавать приложению во время выполнения. Таким образом мы не встраиваем конфигурацию в контей­ нер, а применяем ее при развертывании. Благодаря этому можно задействовать один и тот же контейнер в рабочей или промежуточной среде. В большинстве ком­ паний для этих сред потребовалось бы много разных параметров конфигурации, например URL-aдpeca для подключения к внешним службам, используемым при­ ложением. При реализации контейнеров вы заметите, что размер приложения постоянно со­ кращается, потому что вы стараетесь оптимизировать его, убрав все лишнее. Кста­ ти, когда приходится продумывать все, что может потребоваться для распределен­ ного запуска в виде контейнера, можно прийти к очень интересным проектным решениям. Например, если сервис собирает данные, обрабатывает их и возвращает результат, можно настроить контейнеры с этой задачей на множестве серверов, а затем объединять ответ в другом контейнере. Книги для программистов: https://clcks.ru/books-for-it 44 1 Глава 2 Хранение состояния во внешнем хранилище Если Docker лучше всего подходит для приложений без сохранения состояния, как все-таки хранить состояние? Конфигурация обычно передается в переменных сре­ ды. Docker нативно поддерживает переменные среды, и они хранятся в метадан­ ных, которые составляют конфигурацию контейнера. Это значит, что при переза­ пуске контейнера приложению каждый раз будет передаваться одна и та же конфи­ гурация. За конфигурацией контейнера теперь легко наблюдать, пока он запущен, а это заметно упрощает отладку, хотя передавать секреты в переменных среды небезопасно. Мы можем хранить конфигурацию приложения во внешнем храни­ лище, например в Consul (https://www.consul.io/) или PostgreSQL (https://www. postgresql.org/). Масштабируемые приложения часто хранят состояние в базе данных, и ничего в Docker не мешает контейнерным приложениям поступать так же. Правда, если приложение должно хранить файлы, возникнут проблемы. При хранении данных в файловой системе контейнера производительность окажется низкой, пространства будет мало, а при пересоздании контейнера состояние не сохранится. Когда сервис хранит состояние и внешнее хранилище не используется, мы целиком теряем со­ стояние при повторном развертывании этого сервиса. Если приложению нужно хранить состояние файловой системы, следует тщательно все продумать, прежде чем реализовывать его с помощью Docker. Если вы думаете, что контейнеры Linux вам подходят, создайте решение, в которой состояние будет храниться в централи­ зованном расположении вне зависимости от того, на каком хосте запущен контей­ нер. В некоторых случаях для этого потребуется сервис, вроде Amazon Simple Storage Service (Amazon S3), OpenStack Swift или локальное блочное хранилище, или придется даже подключать тома EBS или диски iSCSI внутри контейнера. Так­ же можно использовать плаrины томов Docker (https://docs.docker.com/engine/ extend/plugins_volume), и мы поговорим об этом в главе 11. Хотя мы можем хранить состояние в локальной файловой системе хоста, обычно новичкам это не рекомендуется. Лучше начать с приложений, которым не требует­ ся хранить состояние. Для этого есть много причин, но почти во всех случаях это связано с появлением зависимостей между контейнером и хостом, а это мешает использовать Docker как по-настоящему динамичный сервис поставки приложений с горизонтальным масштабированием. Если контейнер хранит состояние в локаль­ ной файловой системе· хоста, его можно развертывать только там, где есть эта локальная файловая система. Удаленные тома, которые можно подключать дина­ мически, могут исправить ситуацию, но это тоже считается усложненным случаем. Рабочий процесс Docker Как и многие инструменты, Docker требует придерживаться определенного рабоче­ го процесса, который хорошо впишется во многие компании, хотя, скорее всего, сейчас ваша команда работает чуть иначе. Мы адаптировали рабочие процессы на­ шей организации к подходу Docker и можем с уверенностью сказать, что это изме­ нение положительно повлияет на многие наши команды разработчиков. Если проКниги для программистов: https://clcks.ru/books-for-it Общие сведения о Docker 1 45 цесс правильно реализован, он поможет сократить издержки на коммуникацию между специалистами. Контроль изменений Docker «из коробки» предлагает два готовых способа контроля изменений: один отслеживает слои файловой системы, из которых состоит каждый образ Docker, а второй - теги для этих образов. Слои файловой системы Контейнеры Linux состоят из наложенных друг на друга слоев файловой системы, каждый с уникальным хешем для идентификации, и все наборы изменений, вне­ сенных в процессе сборки, накладываются поверх предыдущих изменений. Это удобно, потому что при новой сборке требуется пересобрать только слои, следую­ щие за внесенными изменениями. Таким образом мы экономим время и трафик, поскольку контейнеры поставляются в виде слоев, и уже сохраненные на сервере слои отправлять не нужно. Если раньше вы применяли много классических инст­ рументов развертывания, то знаете, что в итоге вы снова и снова отправляете на сервер сотни мегабайтов одинаковых данных с каждым развертыванием. Это очень неэффективно и, хуже того, невозможно узнать точно, что изменилось между раз­ вертываниями. Благодаря наличию слоев и тому факту, что контейнеры Linux включают все зависимости приложения, Docker позволяет лучше видеть изменения, '-----­ поставляемые в рабочую среду. Если сформулировать проще, образ Docker содержит все необходимое для выпол­ нения приложения. Меняя одну строку кода, мы определенно не хотим тратить время на пересборку каждой необходимой зависимости в новый образ. Вместо этого Docker использует кеш сборки, чтобы пересобирать только слои, затронутые изменением. Теги образов Второй тип контроля изменений в Docker помогает ответить на важный вопрос: какая версия приложения была развернута последней? Ответ найти не всегда легко. Для приложений, не использующих контейнеры, существуют разные решения - от тегов Git для каждого выпуска до журналов развертываний, сборок с тегами и т. д. Если координацию развертывания вы осуществляете, например, с помощью Capistrano (https://capistranorb.com/), этот инструмент будет хранить на сервере заданное число предыдущих выпусков, и с помощью символьных ссылок помечать один из них как текущий. В большой рабочей среде у разных приложений будут различные способы контроля версий развертывания. Более того, в средах с разными языками у приложений мо­ гут быть различные инструменты развертывания. На вопрос о предыдущей развер­ нутой версии может быть много ответов - зависит от того, у кого мы спраш�ваем и какое приложение имеем в виду. У Docker есть встроенный механизм для кон­ троля версий: выбор тега образа в ходе стандартного процесса сборки. На сервере Книги для программистов: https://clcks.ru/books-for-it 46 1 Глава 2 может быть несколько версий приложения, чтобы упростить откат к предьщущей. Это понятный функционал, доступный и в других инструментах развертывания, но, имея образы контейнеров, процесс можно стандартизировать для всех приложений и команд. Такой подход упрощает коммуникации между командами специалистов и подготовку развертывания, потому что у нас будет единый источник истины для выпусков приложений. Во многих примерах в Интернете и в этой книге вы встретите тег latest (послед­ ний). Он удобен, когда вы начинаете работать с Docker или пишете примеры, по­ тому что всегда выбирается самая недавняя сборка образа. Правда latest не при­ вязан к конкретным образам, так что его лучше не использовать в рабочих средах� мы не сможем контролировать обновление зависимостей и откатывать приложение до образа latest, ведь у старых версий этого тега уже не будет. Кроме того, будет сложно проверить, выполняется ли на разных серверах один и тот же образ. Поэтому тег latest не рекомендуется для рабочей среды или для исходных образов. В конвейере CI/CD лучше использовать теги, уникально определяющие конкретный коммит исходного кода. В рабочем процессе git это может быть хеш git, связанный с коммитом. Когда будете готовы выпустить образ, примените семантическое вер­ сионирование (https://semver.org/) и пометьте образ тегами: 1.4.3, 2.0.0 и т. д. Управление тегами требует дополнительной работы, зато избавляет от многих неприятных сюрпризов во время сборки и развертывания. Сборка Во многих организациях сборка приложений считается темным искусством - мало кто знает, что нужно повернуть и куда нажать, чтобы получить жизнеспособный программный пакет. Правильная сборка - это затратная часть процесса разверты­ вания приложения. Docker не решает всех проблем, зато предоставляет стандар­ тизированную конфигурацию и набор инструментов для сборок. С их помощью гораздо проще научиться собирать приложения и делать это правильно. В интерфейсе командной строки Docker есть флаг build, который позволяет на основе Docker.file создать образ Docker. Каждая команда в Dockerfile создает новый слой в образе, так что по содержимому Dockerfile легко понять, что собой пред­ ставляет сборка. Преимущества стандартизации в том, что любой инженер, у кото­ рого есть опыт работы с Dockerfile, может легко изменить сборку любого приложе­ ния. Поскольку образ Docker представляет собой стандартизированный артефакт, подготовка к сборке будет одинаковой независимо от языка программирования, базового образа или количества слоев. Dockerfile обычно отправляется в систему контроля версий, что упрощает отслеживание изменений в сборке. В современных многоэтапных сборках в Docker можно определить среду сборки отдельно от ито­ гового образа программного пакета. В результате мы получаем для среды сборки такие же широкие возможности конфигурации, как для контейнера в рабочей среде. Часто сборка Docker выполняется одним вызовом команды docker irnage build, при котором создается один артефакт - образ контейнера. Поскольку вся логика, отно­ сящаяся к сборке, обычно полностью содержится в Dockerfile, мы можем легко созКниги для программистов: https://clcks.ru/books-for-it Общие сведения о Docker 1 47 давать стандартные задания сборки, которые любая команда разработчиков сможет использовать в системах сборки, вроде Jenkins (https://jenkins-ci.org/). Многие компании, например еВау, применяют стандартизированные контейнеры Linux для сборки образов из Dockerfile. Решения по сборке, предлагаемые по модели "про­ граммное обеспечение как услуга" (Software as а Service, SaaS), например Travis CI (https://travis-ci.com/) или CodeShip (https://codeship.com/), также превосходно поддерживают сборки Docker. Мы можем автоматизировать создание нескольких образов, поддерживающих раз­ ные базовые архитектуры, например х86 или АRМ, благодаря недавно представ­ ленной поддержке BuildKit (https://github.com/moby/buildkit) в Docker. Тестирование В самом Docker нет встроенного фреймворка для тестирования, но способ сборки контейнеров упрощает тестирование в контейнерах Linux. Тестировать приложения можно разными способами - от модульных тестов до полноценного интеграционного тестирования в среде, близкой к рабочей. Docker поддерживает эффективное тестирование, поскольку гарантирует, что программ­ ный пакет, прошедший тестирование, в том же самом виде поступит в рабочую среду. Это возможно благодаря тому, что мы можем использовать Docker SНА для контейнера или пользовательский тег, чтобы поставлять точную версию прило­ жения. Контейнеры по определению включают все нужные зависимости, так что тестиро­ вание дает надежный результат. Если фреймворк модульного тестирования под­ тверждает, что тест образа контейнера выполнен успешно, мы можем быть увере­ ны, что при развертывании у нас не возникнет проблем с имеющейся версией биб­ лиотеки. В случае других технологий достичь этого сложнее, и даже файлы Java WAR (Java Web application ARchive), например, не включают тестирование сервера приложений. Приложение Java, развернутое в контейнере Linux, обычно также включает сервер приложений, например Tomcat, так что для всего стека можно провести "дымовое" (smoke) тестирование перед отправкой в рабочую среду. Дополнительное преимущество поставки приложений в контейнерах Linux заклю­ чается в том, что при наличии нескольких приложений, которые взаимодействуют друг с другом удаленно через API или подобный механизм, разработчики одного приложения могут опираться на ту версию другого приложения, которая в данный момент помечена как соответствующая нужной среде, например рабочей или про­ межуточной (staging). В команде разработчиков одного приложения не нужен спе­ циалист, который разбирался бы в работе и развертывании другого приложения. Если представить, что действие происходит в сервис-ориентированной архитектуре с бессчетным числом микросервисов, станет очевидно, насколько контейнеры Linux упрощают задачи по разработке или контролю качества в условиях огромно­ го количества вызовов АР[ между микросервисами. Обычно в организациях, которые используют контейнеры Linux в рабочей среде, применяются автоматизированные интеграционные тесты для развертывания верКниги для программистов: https://clcks.ru/books-for-it 48 Глава 2 сионированного набора контейнеров Linux для разных сервисов в соответствии с текущими развернутыми версиями. Интеграционные тесты для нового сервиса задействуют именно те версии, которые будуr нужны сервису после развертыва­ ния. Раньше в среде с разными языками для этого потребовалась бы длительная подготовка, но благодаря стандартизации, предоставляемой контейнерами Liлux, это стало гораздо проще. Упаковка Сборки Docker создают образ, который можно считать единым артефактом сборки, хотя технически он может состоять из нескольких слоев файловой системы. Неза­ висимо от языка, на котором написано приложение, или используемого дистрибу­ тива Linux в результате сборки мы получаем многослойный образ Docker. При этом весь процесс сборки выполняется с помощью инструментов Docker. Образ сборки похож на метафорический грузовой контейнер, от которого Docker и получил свое название (docker в переводе с английского- "портовый погрузчик"). Это цельная транспортабельная единица, с которой можно работать независимо от ее содержи­ мого. Docker также имеет дело с одной стандартной «емкостью» - образом Docker. Это очень удобно, потому что одни и те же инструменты подойдуr нам для разных приложений, и готовые инструменты для контейнеров, созданные кем-то другим, будуr хорошо сочетаться с нашими образами сборки. С помощью Docker мы можем легко переносить приложения, которым раньше тра­ диционно требовалась трудоемкая конфигурация перед развертыванием на новом хаете или в новой системе разработки. После сборки контейнер можно без усилий развернуть в любой системе с помощью сервера Docker с той же архитектурой. Развертывание Для развертывания существует столько инструментов, что перечислить их все было бы невозможно. Это могут быть shеll-скрипты, Capistrano (https://capistranorb.com/), Fabric (https://www.fabfile.org/), АлsiЫе (https://www.ansiЫe.com/) или собствен­ ные инструменты компании. В больших организациях в каждой команде обычно есть один или два человека, которые умеют волшебным образом осуществлять раз­ вертывания. Если что-то идет не так, команда ждет, пока они все исправят. Навер­ ное, вы уже догадываетесь, что Docker меняет ситуацию к лучшему. Встроенные инструменты поддерживают простую и линейную стратегию развертывания, с по­ мощью которой можно запустить сборку на хаете. Стандартный клиент Docker раз­ вертывает приложение только на один хост за раз, но есть много инструментов, с помощью которых можно выполнять развертывание в кластере хостов Docker или других систем, совместимых с контейнерами Linux. Благодаря предоставляемой Docker стандартизации сборку можно развернуть в любой из этих систем без лиш­ них усилий со стороны команды разработчиков. Книги для программистов: https://clcks.ru/books-for-it Общие сведения о Docker 1 49 Экосистема Docker За несколько лет развилось обширное сообщество Docker, куда входят разработчи­ ки и системные администраторы. Как и философия DevOps, это движение создает инструменты, позволяющие решать задачи сопровождения с помощью программ­ ного кода. Если сам Docker не предлагает необходимые средства, другие компании и отдельные разработчики заполняют этот пробел. Многие инструменты поставля­ ются с открытым кодом, т. е. любая компания может адаптировать их под свои по­ требности. Docker - это коммерческая организация, которая передала сообществу большую часть базового кода Docker. Другие компании могут присоединиться к сообществу и внести свой вклад в развитие проекта. Если вы ищете поддерживаемые версии базовых инструментов Docker, узнайте больше на сайте Docker (https://www. docker.com/support). Оркестрация Первая важная категория инструментов, которые расширяют функционал базовой версии Docker с контейнерами Linux, используется для оркестрации и массового развертывания. Первые инструменты развертывания, вроде Centurion от New Relic (https://github.com/newrelic/centurion), Helios от Spotify (https://github.com/ spotify/helios) и AnsiЫe Docker (https://oreil.lyN8X_t) 1 , все еще придерживаются традиционного подхода к развертыванию, но в качестве распространяемого про­ граммного пакета используют контейнер. В них применяется простой и понятный подход, и мы получаем множество преимуществ Docker без лишних сложностей. Сейчас эти инструменты уступают более надежным и гибким решениям, например Kubemetes. Полностью автоматический планировщик, например KuЬemetes (https://kuЬernetes.io/) или Apache Mesos с Marathon (https://mesos.apache.org/), предлагают больше воз­ можностей и почти полностью берут на себя контроль над пулом хостов, освобож­ дая вас от этой задачи. Доступно множество коммерческих решений, например Nomad от HashiCorp (https://www.nomadproject.io/), Mesosphere DC/OS (Datacenter Operating System) (https://dcos.io/) и Rancher (https://rancher.com/)2 • Экосистемы с бесплатными и коммерческими вариантами продолжают активно развиваться. Неизменяемые атомарные хосты Еще один вариант использования Docker - неизменяемые атомарные хосты. Обычно серверы и виртуальные машины - это системы, которые организация тщательно собирает, настраивает и обслуживает, чтобы предоставлять широкий набор возможностей для разных вариантов применения. Обновления часто прихо­ дится устанавливать с помощью неатомарных операций, и можно представить раз1 Полный URL: https://docs.aosiЫe.com/ansiЫe/latest/collectioos/commuoity/dockeг/docsite/ sce oaгio_guide.html#aosible-collectioos-community-dockeг-docsite-sceoaгio-guide. 1 У некоторых из этих коммерческих решений есть бесплатные версии. Книги для программистов: https://clcks.ru/books-for-it 50 Глава 2 ные варианты, когда в конфигурациях хоста возникают отклонения, вызывая непредвиденное поведение. Для большинства современных систем обновления и патчи устанавливаются на «коленке», при этом в разработке чаще всего заново раз­ вертывается целая копия нового приложения, а не отдельные обновления. Одно из преимуществ контейнеров состоит в том, что они повышают атомарность прило­ жений по сравнению с традиционными моделями развертывания. Представьте, что контейнерный подход можно было бы применить даже к опера­ ционной системе. Не пришлось бы управлять конфигурациями, пытаясь устанавли­ вать обновления и объединять изменения компонентов операционной системы, зато можно просто развернуть новый тонкий образ ОС и перезагрузить сервер. А если что-то сломается, можно так же легко откатиться до точного образа, который использовался раньше. Эта идея лежит в основе атомарных хостов на базе Linux, например Red Hat Fedora CoreOS, Bottlerocket OS (https://github.com/bottlerocket-os/bottlerocket) и др. Вы сможете легко сносить и повторно развертывать не только приложения - эту фи­ лософию можно применить ко всему программному стеку, поддерживая высокий уровень согласованности и устойчивости. Неизменяемый, или атомарный, хает (https://gist.github.com/jzЬ/0f336c6t23a0ba 145Ь0а) обычно занимает минимум места и спроектирован таким образом, чтобы поддерживать контейнеры Linux и Docker, а также атомарные обновления и откаты операционной системы, которыми можно легко управлять с помощью инструмен­ тов оркестрации для нескольких хостов на "голом железе" (bare metal - без ОС) и распространенных платформах виртуализации. В главе 3 мы рассмотрим, как использовать неизменяемые хосты в процессе развер­ тывания. При этом становится возможным создать удивительно идентичные стеки в среде разработки и рабочей среде. Дополнительные инструменты Docker не представляет собой единое решение - скорее, это большой набор воз­ можностей. Часто возникают ситуации, когда стандартного набора не хватает, и тогда мы можем воспользоваться обширной экосистемой инструментов, улучшаю­ щих или дополняющих функционал Docker. Хорошие инструменты для рабочей среды часто используют Docker API, например Prometheus (https://prometheus.io/) для мониторинга и AnsiЫe (https://www.ansiЫe.com/) для простой оркестрации. Другие опираются на поддержку плагинов - исполняемых программ, которые на основе определенной спецификации получают данные из Docker и отправляют их обратно. Многие плагины Docker считаются устаревшими, им на смену приходят новые ре­ шения. Тщательно все изучите, прежде чем выбрать плагин, - убедитесь, что он подходит для вашего случая и не устареет в ближайшее время. Есть много других хороших инструментов, которые взаимодействуют с API или выполняются как плагины. Многие из них появились, чтобы облегчить работу Книги для программистов: https://clcks.ru/books-for-it Общие сведения о Docker 1 51 с Docker в разных облаках и позволяют бесшовно интегрировать Docker в облако. Сообщество продолжает придумывать инновации, экосистема непрерывно расши­ ряется, появляются все новые решения и инструменты. Если вы не можете решить какую-то задачу в своей среде, попробуйте найти подходящее решение в эко­ системе. Заключение Мы кратко рассмотрели, что собой представляет Docker. Мы вернемся к этому обсуждению позже и подробнее рассмотрим архитектуру Docker, примеры исполь­ зования инструментов сообщества, а также аспекты проектирования надежных контейнерных платформ. Скорее всего, вам не терпится перейти к практике, поэто­ му в следующей главе мы установим и запустим Docker. Книги для программистов: https://clcks.ru/books-for-it ГЛАВА 3 Установка Doker Сейчас вы уже примерно представляете себе, что такое Docker, что он умеет и чего не умеет. Давайте теперь установим его, чтобы перейти к практике. Действия по установке Docker зависят о платформы разработки и дистрибутива Linux, с по­ мощью которого вы размещаете приложения в рабочей среде. В этой главе мы рассмотрим, как создать работоспособную среду разработки Docker на большинстве современных настольных операционных систем. Сначала мы установим клиент Docker на нативной платформе разработки, а затем поднимем сервер Docker на Linux. Наконец, мы протестируем установку и убедимся, что все работает нормально. Клиент Docker для управления сервером Docker может работать на Windows и macOS, но контейнеры Linux можно собирать и запускать только в Linux. В других системах потребуется виртуальная машина или удаленный сервер для размещения сервера Docker на базе Linux. Docker Community Edition, Docker Desktop и Vagrant, о которых мы поговорим чуть позже, предлагают некоторые варианты решения этой проблемы. Мы также можем запускать контейнеры Windows в системах Windows, и об этом мы поговорим в разделе "Контейнеры Windows" главы 5, но в основном в этой книге мы будем рассматривать контейнеры Linux. С развитием технологий экосистема Docker очень быстро меняется, становится более надежной и учится решать все больше задач. Некоторые функции, рассмат­ риваемые в этой книге и в других источниках, могут уже устареть. Узнать, какие � функции устарели и будут удалены, можно из документации на https://docs. docker.com/engine/deprecated. В большинстве примеров кода в этой книге предполагается, что у вас традицион­ ная командная оболочка Unix. Вы можете работать с PowerShell, но в некоторые команды нужно будет внести изменения. Если вам необходимо использовать прокси, убедитесь, что он правильно настроен для Docker. Клиент Docker Клиент Docker нативно поддерживает 64-разрядные версии Linux, Windows и macOS. Большинство популярных дистрибутивов Linux произошли от DeЬian или Red Hat. Системы DeЬian используют формат пакета deb и Advanced Package Tool (apt) Книги для программистов: https://clcks.ru/books-for-it Установка Doker 1 53 (https://wiki.deblan.org/AptCLI) для установки пакетов программного обеспече­ ния. В системах Red Hat для этой цели предусмотрены файлы RPM Package Manager (rpm) и Yellowdog Updater, Modified (yum) (https://en.wikipedia.org/ wiki/Yum_(software)) или Dandified yum (dnf) (https://goo.gl!ГdkGRS). В Alpine Linux, который часто встречается в средах, где операционная система должна занимать очень мало места, для этого применяется Alpine Package Keeper (apk) (https://wiki.alpinelinux.org/wiki/Package_management). В macOS и Microsoft Windows устанавливать пакеты программного обеспечения и управлять ими проще всего с помощью встроенных установщиков с графическим интерфейсом. Homebrew for macOS (https://brew.sh/) и Chocolatey for Windows (https://chocolatey.org/) тоже очень популярны среди технически подкованных пользователей. & В этом разделе мы рассмотрим несколько подходов к установке Docker. Выберите первый, который вам подойдет. Если используется несколько версий, нужно уметь переключаться между ними, иначе могут возникнуть проблемы. Варианты на выбор: Docker Desktop, Docker Community Edition, менеджер пакетов операционной системы и Vagrant. Актуальную документацию по установке см. на сайте Docker (https://docs. docker.com/get-docker). Linux Рекомендуется запускать Docker на свежей версии предпочитаемого дистрибутива Linux. Более старые выпуски тоже подойдут, но могут возникнуть проблемы со стабильностью. Обычно требуется ядро 3.8 или более поздней верСИ'1, и мы реко­ мендуем последнюю стабильную версию выбранного дистрибутива. В следующих инструкциях предполагается, что вы используете последний стабильный выпуск дистрибутива Ubuntu или Fedora Linux. Мы не будем рассматривать его здесь, но можно использовать Docker Desktop for Linux (https://docs.docker.com/desktop/linux/install), если демон Docker должен работать на локальной виртуальной машине, а не напрямую в системе. Ubuntu Linux 22.04 (64-разрядная) Начнем с установки Docker на 64-разрядной версии Ubuntu Linux 22.04. Актуальные инструкции и описание других версий Ubuntu см. на странице Docker � Community Edition for Ubuntu (https://dockr.ly/2NwNbuw). � Первые две команды удаляют старые версии Docker, если те установлены. Пакеты несколько раз переименовывались, так что нужно указать несколько возможньrх вариантов: Книги для программистов: https://clcks.ru/books-for-it 54 Глава 3 $ sudo apt-get remove docker docker.io containerd runc $ sudo apt-get remove docker-engine lJ!N � Можно игнорировать ошибки apt-get UпаЫе to locate package (Не удалось найти пакет) или Package is not installed (Пакет не установлен). Далее мы добавим необходимые зависимости и репозиторий apt для Docker Community Edition. Таким образом мы сможем получить и установить пакеты для Docker и убедиться, что они подписаны: $ sudo apt-get update $ sudo apt-get install \ ca-certificates \ curl \ gnupg \ lsb-release $ sudo mkdir -р /etc/apt/keyrings $ curl -fsSL https://download.docker.com/linux/uЬuntu/gpg 1\ sudo gpg --dearmor -о /etc/apt/keyrings/docker.gpg $ sudo chmod a+r /etc/apt/keyrings/docker.gpg $ echo \ "deb [arch=$(dpkg --print-architecture) \ signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/uЬuntu \ $(lsb_release -cs) staЬle" 1\ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null Создав и настроив репозиторий, выполним следующие команды для установки Docker: $ sudo apt-get update $ sudo apt-get install \ docker-ce \ docker-ce-cli \ containerd.io \ docker-compose-plugin Если никаких ошибок не появилось, Docker установлен. Fedora Linux 36 (64-разрядная) Теперь установим Docker на 64-разрядной версии Fedora Linux 36. lJ!N � Актуальные инструкции и описание других версий Fedora см. на странице Docker Community Edition fог Fedora (https://dockr.ly/2NwNdTa). Первая команда удаляет старые версии Docker, если они имеются. Как и в системах Ubuntu, пакет был несколько раз переименован, так что нужно указать несколько вариантов: Книги для программистов: https://clcks.ru/books-for-it Установка Doker 1 55 $ sudo dnf remove -у\ docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-selinux \ docker-engine-selinux \ docker-engine Далее мы добавим необходимые зависимости и репозиторий dnf для Docker Community Edition: $ sudo dnf -у install dnf-plugins-core $ sudo dnf config-manager \ --add-repo \ https://download.docker.com/linux/fedora/docker-ce.repo Теперь можно установить текущую версию Docker Community Edition: $ sudo dnf install -у\ docker-ce \ docker-ce-cli \ containerd.io \ docker-coпpose-plugin macOS, Мае 05 Х Для установки Docker в macOS используйте официальный установщик Docker DesktQp. Установщик с графическим интерфейсом Загрузите последюою версию установщика Docker Desktop for Мае (https://dockr.ly/ 2wyTpCO) и дважды щелкните по значку загруженной программы. Следуйте инст­ рукциям в установщике. Docker Desktop for macOS использует проект xhyve (https://github.com/machyve/xhyve) и гипервизор {https://developer.apple.com/ documentation/hypervisor) Apple, чтобы предоставлять легкий нативный слой вир­ туализации для компонента сервера Linux. Он необходим для запуска виртуальных машин Linux, на которых будут собираться образы Docker и запускаться контей­ неры. Установка Homebrew Вы также можете установить инструменты Docker CLI с помощью популярной сис­ темы управления пакетами Homebrew (https://docs.brew.sh/lnstallation) для macOS. Если вы выберете этот подход, установите Vagrant для создания и администрироКниги для программистов: https://clcks.ru/books-for-it 56 Глава З вания виртуальной машины Linux. Мы поговорим об этом далее в этой главе, в разделе "Сервер на базе виртуальной машины без Linux". Microsoft Windows 11 Вот как установить Docker Desktop в Windows 11. Рекомендуется настроить подсистему Windows для Linux (WSL2) (https:/ldocs. microsoft.com/en-us/windows/wsl/install) до установки Docker Desktop, а затем выбрать из доступных вариантов в установщике Docker Desktop, чтобы установить WSL2. 1 Docker Desktop for Windows может использовать Hyper-V для предоставления на­ тивноrо слоя виртуализации для серверных компонентов Linux, но WSL2 предлага­ ет самый удобный подход для работы с контейнерами Linux. Загрузите последнюю версию установщика Docker Desktop for Windows (https:// dockr.ly/2C0n7H0) и дважды щелкните по значку загруженной программы. Сле­ дуйте инструкциям в установщике. Включение режима контейнеров Linux для Windows По умолчанию установка Docker Desktop на Windows должна быть настроена для контейнеров Linux, но если вы получите сообщение "по matching manifest for windows/amd64" (нет подходящего манифеста для windows/amd64) или что-то по­ добное, скорее всего, Docker Desktop настроен для контейнеров Windows. Контейнеры Linux остаются самым популярным типом контейнеров, и они потре­ буются вам для выполнения практических упражнений в этой книге. Чтобы изме­ нить настройку Windows, нажмите правой кнопкой мыши на значке Docker на панели задач Windows и выберите пункт Switch to Linux containers (Переключить­ ся на контейнеры Linux), как показано на рис. 3.1 и 3.2. При необходимости мы можем легко переключаться между контейнерами Linux и Windows. Установка Chocolatey Вы также можете установить инструменты Docker CLI с помощью популярной сис­ темы управления пакетами Chocolatey (https://docs.chocolatey.org/en-us/choco/ setup) для Windows. Если вы выберете этот подход, установите Vagrant для созда­ ния и администрирования виртуальной машины Linux. Мы поговорим об этом далее, в разделе "Сервер на базе виртуальной машины без Linux". На сайте Docker есть инструкции по установке дополнительных сред � � (https:/ldocs.docker.com/engine/install). 1 Полный URL: https:/Лeaгn.micгosoft.com/en-us/viгtualization/hypeг-v-on-windows/about. Книги для программистов: https://clcks.ru/books-for-it Установка Doker 8 ii � 57 Docker Deslctop is runnIng Dashboard ► spkane Settings Check for Updates Ct,·t�Comma TrouЫeshoot Switch to Linux containers... About Docker Desktop Documentation Quick Start Gu,de Docker Hub • Extensions 11 Pause t'> (.!) ► Restart Qt1it Docker Desktop � = о тJi 11 л е ОФ>) 12:09 РМ 2/3/2023 Рис. 3.1. Переключение на контейнеры Linux х Switch to Linux containers You are about to switch to Linux containers. Existing containers will continue to run, but you will not Ье аЫе to manage them until you switch back to windows containers. No data will Ье lost otherwise. Do you want to continue? О Don't show this message again Switch 1/ Сапсеl Рис. 3.2. Переключение на контейнеры Linux - подтверждение Сервер Docker Сервер Docker представляет собой отдельный от dосkеr-клиента исполняемый файл. С помощью сервера мы управляем большей частью задач, для которых обыч­ но используется Docker. Далее мы поговорим о самых распространенных способах управления сервером Docker. Книги для программистов: https://clcks.ru/books-for-it 58 1 Глава 3 Docker Desktop и Docker Community Edition уже настроили за нас сервер, так что если вы выбрали этот вариант, не нужно больше ничего делать. Можно только убедиться, что сервер (dockerd) запущен. В Windows и macOS для этого достаточно � запустить приложение Docker. В Linux потребуется выполнить следующие команды systemctl, чтобы запустить сервер. Linux с systemd Текущие выпуски Fedora и Ubuntu управляют процессами в системе с помощью Поскольку мы уже установили Docker, можно выполнить следующую команду, чтобы сервер запус­ кался каждый раз при загрузке системы: systerro (https://www.freedesktop.org/wiki/Software/systemd). $ sudo systemctl enaЬle docker Эта команда указывает systemd добавить сервис docker в автозапуск при старте сис­ темы или при переключении на уровень выполнения по умолчанию (default runlevel). Для запуска сервера Docker введите следующую команду: $ sudo systemctl start docker Сервер на базе виртуальной машины без Linux Если вы работаете с Docker в Microsoft Windows или macOS, вам потребуется вир­ туальная машина, чтобы поднять сервер Docker для тестирования. Docker Desktop сам создает эту виртуальную машину, если вы используете нативную технологию виртуализации на этих платформах. Если у вас старая версия Windows или вы не можете использовать Docker Desktop по другим причинам, возможно, Vagrant (https://www.vagrantup.com/) поможет вам в создании и администрировании вир­ туальной машины Linux для севера Docker. Кроме Vagrant есть и другие инструменты виртуализации, например Lima на macOS (https://github.com/lima-vm/lima), или стандартный гипервизор, которые позволят создать локальный сервер Docker в зависимости от конкретных предпоч­ тений и потребностей. Vagrant Vagrant предоставляет поддержку для нескольких гипервизоров и позволяет ими­ тировать даже самые сложные среды. Vagrant часто используется при разработке с Docker для поддержки тестирования образов, которые соответствуют рабочей среде. Vagrant поддерживает любые дист­ рибутивы - от самых общих, вроде Red Hat Enterprise Linux (https://www. redhat.com/en/technologies/linux-platforms/enterprise-linux) и Ubuntu (https:// ubuntu.com/), до специализированных версий для атомарных хостов, например Fedora CoreOS (https://getfedora.org/en/coreos). Vagrant можно легко установить на большинстве платформ, загрузив автономный (self-contained) пакет (https://www.vagrantup.com/downloads.html). Книги для программистов: https://clcks.ru/books-for-it Установка Doker 1 59 Рассмотренный далее пример с Vagrant не считается безопасны� и не рекоменду­ _ ется. Это просто демонстрация базовых требовании для настроики и использова­ ния виртуальной машины для удаленного сервера Docker. Не забудьте позабо­ титься о безопасности сервера, это очень важно. По возможности используйте для разработки Docker Desktop. Вам потребуется установить в системе один из следующих rипервизоров: ♦ VirtualВox • Предоставляется бесплатно • Поддерживает разные платформы на большинстве архитектур ♦ VMware Workstation Pro/Fusion2 • Коммерческое решение • Поддерживает разные платформы на большинстве архитектур ♦ HyperV3 • Коммерческое решение • Поддерживает Windows на большинстве архитектур ♦ КУМ • Предоставляется бесплатно • Поддерживает Linux на большинстве архитектур. По умолчанию Vagrant предполагает, что мы используем rипервизор VirtualВox, но этот параметр можно поменять с помощью флага --provider (https:/Лearn. hashicorp.com/tutorials/vagrant/getting-started-providers) в команде vagrant. В следующем примере мы создадим хост Docker на базе Ubuntu с демоном Docker. Затем мы создадим на хосте каталог с именем docker-host и перейдем в него: $ mkdir docker-host $ cd docker-host Дr�я использования Vagrant потребуется найти Vagrant Вох (образ виртуальной машины https://app.vagrantup.com/boxes/search), совместимый с вашим решением и архитектурой. В этом примере мы используем Vagrant Вох для rипервизора Virtual Вох. l.JN � Virtual Вох работает только на системах lntel/AMD х86(64), а применяемый нами Vagrant Вох специально создан для систем AMD64. 2 Полный URL: https://,V\V\v.vm,vare.com/products/,vorkstation-pro.html. 3 Полный URL: https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enaЫe-hyper-v. Книги для программистов: https://clcks.ru/books-for-it 60 Глава 3 Создайте новый файл с именем Vagrantfile со следующим содержимым: puts («-ЕОГ) [WARNING] This exposes an unencrypted Docker ТСР port on the VМ! ! This is NОГ secure and may expose your system to significant risk if left running and exposed to the broader network4 • ЕОГ $script = <<-SCRIPТ echo \'{"hosts": ["tcp://0.0.0.0:2375", "unix:///var/run/docker.sock"]}\' sudo tee /etc/docker/daemon.json sudo mkdir -р /etc/systemd/system/docker.service.d echo -е \"[Service]\nExecStart=\nExecStart=/usr/Ьin/dockerd\" 1 \ sudo tee /etc/systemd/system/docker.service.d/docker.conf sudo systemctl daemon-reload sudo systemctl restart docker SCRIPТ Vagrant.configure(2) do lconfigl # Выбираем совместимый Vagrant Вох config.vm.box = 'bento/uЬuntu-20.04' # Устанавливаем Docker, если его нет в образе виртуальной машины config.vm.provision :docker # Docker будет ожидать данных на незашифрованном локальном порту config. vm.provision "shell", inline: $script, run: "always" 1 \ # Проброс порта Docker на # 12375 (или другой открытый порт) на хосте config.vm.network "forwarded_port", guest: 2375, host: 12375, protocol: "tcp", auto correct: true end Полную копию файла можно извлечь следующей командой: $ git clone https://githuЬ.com/Ьluewhalebook/\ docker-up-and-running-3rd-edition.git --config core.autocrlf=input $ cd docker-up-and-running-3rd-edition/chapter_03/vagrant $ 1s Vagrantfile Возможно, потребуется удалить символ "\" в команде git clone и собрать URL в одну строку. Просто команда слишком длинная для стандартной страницы выво­ да. В стандартной командной оболочке Unix эта команда будет работать, если в строках нет начальных и конечных пробелов. Убедитесь, что находитесь в каталоге с файлом Vagrantfile, а затем выполните сле­ дующую команду, чтобы запустить виртуальную машину Vagrant. 4 ПРЕДУПРЕЖДЕНИЕ. На виртуальной машине будет открыт незашифрованный порт ТСР Docker. Это НЕБЕЗОПАСНО, система может поверrатъся значительному риску при связи с сетью. Книги для программистов: https://clcks.ru/books-for-it & Установка Doker 1 61 Эта установка представлена в качестве простого примера. Она небезопасна и ее не следует использовать, не закрыв доступ к серверу из общедоступной сети. У Docker есть документация по обеспечению безопасности эндпоинта Docker с помощью клиентских сертификатов SSH или TLS (https://docs.docker.com/engine/ security/protect-access), а также дополнительная информация о поверхности атаки демона Docker (https://docs.docker.com/engine/security/#docker-daemon-attack­ surface). $ vagrant up Bringing machine 'default' up with 'virtualbox' provider ... => default: Importing base Ьох 'bento/uЬuntu-20.04'.. . ==> default: Matching МАС address for NAT networking... ==> default: Checking if Ьох 'bento/uЬuntu-20.04' version '...' is up to date... => default: А newer version of the Ьох 'bento/uЬuntu-20.04' for provider... ==> default: availaЬle' You currently have version '...'. The latest is version ==> default: '202206.03.0'. Run ·vagrant Ьох update' to update. ==> default: Setting the name of the VМ: vagrant_default_1654970697417_18732 ==> default: Clearing any previously set network interfaces... ==> default: Running provisioner: docker... default: Installing Docker onto machine... ==> default: Running provisioner: shell... default: Running: inline script default: {"hosts": ["tcp://0.0.0.0:2375", "unix:///var/run/docker.sock"]) default: [Service] default: ExecStart= default: ExecStart=/usr/bin/dockerd В macOS может появиться следующая ошибка: VBoxManage: error: Details: code NS_ERROR_FAILURE (Ох80004005), component MachineWrap, interface /Machine. Это связано с функциями безопасности в macOS. В статье по адресу https:// scriptcrunch.com/solved-vboxmanage-error-component-machinewrap описывается, как исправить проблему. После запуска виртуальной машины мы сможем подключиться к серверу Docker, выполнив следующую команду и сообщив клиенту Docker, куда следует подклю­ чаться, с помощью аргумента -н: $ docker -Н 127.0.0.1:12375 version Client: Cloud integration: vl.0.24 Version: 20.10.14 API version: 1.41 Server: Docker Engine - Community Engine: Version: 20.10.17 API version: 1.41 (minimurn version 1.12) Книги для программистов: https://clcks.ru/books-for-it 62 Глава З В выводе мы види,:м информацию о версии различных компонентов, из которых со­ стоят клиент и сервер Docker. Передавать IР-адрес и порт при каждом выполнении команды Docker неудобно, но, к счастью, с помощью команды docker context мы можем настроить Docker таким образом, чтобы он знал о нескольких серверах Docker. Для начала давайте посмот­ рим, какой контекст сейчас используется. Обратите внимание на строчку со звез­ дочкой ( *), которая обозначает текущий контёкст: $ docker context list NАМЕ ТУРЕ ... DOCКER ENDPOINT... default * moby ... unix:///var/run/docker.sock Мы можем создать новый ко'нтекст для виртуальный машины, а затем активировать его с помощью следующей последовательности команд: $ docker context create vagrant --docкer host=tcp://127.0.0.1:12375 vagrant Successfully created context "vagrant" $ docker context use vagrant vagrant Если снова вывести все контексты, мы увидим примерно следующее: $ docker context list NАМЕ ТУРЕ default moby vagrant * moby DOCKER ENDPOINT ... unix:///var/run/docker. sock... tcp://127.0.0.1:12375... Теперь, когда для текущего контекста задано vagrant, при выполнении команды docker version без аргумента -н мы сможем подключиться к нужному серверу Docker и получить ту же информацию, что и раньше. Чтобы подключиться к командцой оболочке виртуальной машины на базе Vagrant, выполните следующую команду: $ vagrant ss\). Welcome to UЬuntu 20.04.3 LTS (GNU/Linux 5.4.0-91-generic х86_64) vaqrant@vaqrant:�$ exit Пока вы не обеспечите безопасность этой системы, лучше остановить виртуальную машину и вернуть исходное значение для контекста: $ vagrant halt => default: Atteпpting graceful shutdown of VМ... $ docker version Cannot connect to ... daemon at tcp://127.0.0.1:12375. Is the ... daemon running? $ docker context use default default Книги для программистов: https://clcks.ru/books-for-it Установка Doker 1 63 Для macOS доступна среда выполнения Colima (https://github.com/ablosoft/ colima), с помощью которой можно легко запустить гибко настраиваемую вирту­ альную машину Docker или Kubernetes и управлять ею. Тестирование системы Итак, у нас есть функционирующий клиент и сервер, и мы готовы протестировать, что все работает. Мы можем выполнить в локальной системе любую из следующих команд, чтобы демон Docker загрузил последний официальный контейнер для этого дистрибутива и запустил его с командной оболочкой Unix. Данный шаг позволяет убедиться, что все компоненты правильно установлены и могут взаимодействовать друг с другом. Это проявление одного из преимуществ Docker: мы можем запускать контейнеры на базе любого дистрибутива Linux. Далее мы запустим контейнеры Linux на Ubuntu, Fedora и Alpine Linux. Не обяза­ тельно пробовать их все, достаточно одного варианта. Если вы используете клиент Docker в системе Linux, каждая команда docker долж­ на начинаться с sudo, потому что по умолчанию только у пользователя root может быть доступ к Docker. Обычно создается группа docker для управления пользователями, у которых есть доступ к сокету Unix dockerd. Мы можем добавить пользователя в эту группу, чтобы не приходилось каждый раз указывать sudo (https://man7.org/linux/man-pages/man8/sudo.8.html). Ubuntu Давайте попробуем запустить контейнер с использованием последнего базового образа Ubuntu Linux: $ docker container run --rm -ti docker.io/uЬuntu:latest /bin/bash root@aa9b72aelfea:/# 1W �] С функциональной точки зрения docker container run работает так же, как docker run. Fedora В этом примере мы запустим контейнер с использованием последнего базового образа Fedora Linux: $ docker container run --rm -ti docker.io/fedora:latest /bin/bash [root@Sc9720le827b /]# exit Книги для программистов: https://clcks.ru/books-for-it 64 Глава 3 Alpine Linux Наконец, запустим контейнер с использованием последнего базового образа Alpine Linux: $ docker container run --rm -ti docker.io/alpine:latest /bin/sh / # exit Объекты docker.io/uЬuntu:latest, docker. io/fedora:latest и docker. io/alpine:latest строятся по одной схеме: репозиторий образов Docker, имя образа и тег образа. Сервер Docker Сервер Docker часто устанавливается, вюпочается и запускается автоматически, но будет полезно увидеть, как можно легко запустить демон Docker вручную на системе Linux (https://docs.docker.com/engine/reference/commandline/dockerd), выполнив простую команду: $ sudo dockerd -Н unix:///var/run/docker.sock \ --config-file /etc/docker/daemon.json В этом разделе предполагается, что вы используете сервер Linux или виртуальную машину с демоном Docker. Если у вас Docker Desktop в Windows или Мае, то вы не сможете напрямую взаимодействовать с исполняемым файлом dockerd, потому что он намеренно скрыт от конечного пользователя, но мы посмотрим, как решить эту проблему. Эта команда запускает демон Docker, создает и начинает прослушивать unix-coкeт (-н unix:///var/run/docker.sock), а затем считывает остальную конфигурацию из /etc/docker/daemon.json. Вряд ли вам придется запускать сервер Docker самостоя­ тельно, но полезно знать, что при этом происходит. Если вы работаете не в Linux, то, скорее всего, будете использовать виртуальную машину на базе Linux, на кото­ рой размещен сервер Docker. Docker Desktop сам создает эту виртуальную машину. J-Ж � Если Docker уже запущен, снова �ыполнить демон не получится, потому что он не может прослушивать один сетевои порт дважды. В большинстве случаев мы можем легко подюпочиться по SSH к серверу Docker и посмотреть, что там происходит, но Docker Desktop так хорошо интегрируется с системами, .отличными от Linux, что мы не замечаем локальную виртуальную машину, на которой выполняется демон Docker. Поскольку виртуальная машина Docker Desktop очень маленькая и стабильная, у нее нет демона SSH и к ней слож­ но подобраться. Если у вас будет необходимость подюпочиться к нижележащей виртуальной маши­ не или вам просто любопытно, это возможно, но потребуются углубленные знания. Книги для программистов: https://clcks.ru/books-for-it Установка Doker 1 65 Мы подробно поговорим о команде nsenter в соответствующем разделе в главе 11. А пока что, если вы хотите посмотреть на виртуальную машину (или базовый хост) сейчас, выполните следующие команды: $ docker container run --rm -it --privileged --pid=host debian \ nsenter -t 1 -m -u -n -i sh / # cat /etc/os-release PRETTY_NAМE="Docker Desktop" / # ps I grep dockerd 1540 root 1:05 /usr/local/bin/dockerd --containerd /var/run/desktop-containerd/containerd.sock --pidfile /run/desktop/docker.pid --swarm-default-advertise-addr=ethO --host-gateway-ip 192.168.65.2 / # exit Эти команды используют привилегированный контейнер DeЬian, который содер­ жит команду nsenter для управления пространствами имен ядра Linux, чтобы можно было перемещаться по файловой системе нижележащих виртуальной машины или хоста. Этот контейнер имеет достаточно прав, чтобы разрешить нам перейти к базовому _ хосту, но не стоит использовать привилегированные контеинеры просто так, если вместо этого можно добавить отдельные привилегии (capability) или возможности системного вызова. Подробнее мы поговорим об этом в разделе "Безопасность• главы 11. Если вы можете задействовать эндпоинт сервера Docker, эта команда даст вам доступ к базовому хосту. Конфигурация демона Docker обычно хранится в файле /etc/docker/daemon.json, но расположение на виртуальной машине Docker Desktop может быть другим, напри­ мер /containers/services/docker/rootfs/etc/docker/daemon.json. Docker задает разумные параметры по умолчанию, поэтому файл может быть очень маленьким, а может и Docker Engine -• General :.!.:; .. IC!J Resources Configure the Docker daemon Ьу typing а jsc Docker Engine €) Kubernetes •••• v20.10.22 0 Software updates Extensions ••• Features in development "builder": { "gc": { "defaultKeepStorage": "20GB", "enaЫed": true }, "experimental": false Рис. 3.3. Конфигурация сервера Docker Desktop Книги для программистов: https://clcks.ru/books-for-it 66 1 Глава 3 вовсе отсутствовать. Если вы работаете с Docker Desktop, файл можно открыть для редактирования - щелкните на значок Docker и выберите пункты Preferences... (Параметры) - Docker Engine, как показано на рис. 3.3. Заключение Итак, мы установили и запустили Docker. Вы можете самостоятельно рассмотреть более сложные варианты установки. В следующей главе мы узнаем, как собирать образы Docker и управлять ими. Это базовые действия для каждого контейнера, который мы запускаем в Docker. Если далее в командной строке вы увидите команду docker, помните, что вам нуж­ на корректная конфигурация в виде контекста Docker, переменных среды или фла­ га командной строки -н, чтобы клиент docker понимал, как подключиться к серверу dockerd. Книги для программистов: https://clcks.ru/books-for-it ГЛАВА4 Работа с образами Docker Любой контейнер Linux основан на образе. Образ содержит определение того, что будет работать в запущенном контейнере, - примерно как виртуальный диск ста­ новится виртуальной машиной при запуске. Образы Docker или Open Container Initiative, OCI предоставляют основу для всего, что мы можем развернуть и запус­ тить в Docker. Для запуска контейнера необходимо загрузить общедоступный образ или создать собственный. Образ можно рассматривать как единый ресурс, пред­ ставляющий файловую систему для контейнера. Каждый образ состоит из одного или нескольких связанных слоев файловой системы, обычно по одному на каждый этап сборки, в ходе которой создавался образ. Поскольку образы создаются из отдельных слоев, они предъявляют особые требо­ вания к ядру Linux, которое должно предоставлять драйверы, необходимые Docker для управления хранилищем на бэкенде. Docker использует это хранилище для управления образами, и оно взаимодействует с базовой файловой системой Linux, чтобы собирать и контролировать многочисленные слои, соединяющиеся в один готовый образ. Поддерживаются следующие хранилища: ♦ Overlay2 (https://oreil.ly/r4JНY)1 • ♦ B-Tree File System (Btrfs) (https://Ьtrfs.wiki.kernel.org/index.php/Мain_Page). ♦ Device Mapper (https://www.sourceware.org/dm). Каждый из этих вариантов предоставляет быструю систему копирования при запи­ си (Copy-on-Write, CoW) для управления образами. Подробнее о различных храни­ лищах мы поговорим в главе 11. А пока используем бэкенд по умолчанию и по­ смотрим, как работают образы, поскольку они составляют основу почти всех задач, которые мы будем выполнять с помощью Docker. Мы рассмотрим следующие темы: ♦ Сборка образов. ♦ Отправка (push) образов в реестр. ♦ Загрузка (pull) образов из реестра. ♦ Создание и запуск контейнеров из образа. 1 Полный URL: https://github.com/torvalds/linux/commit/e9be9d5e76e34872f0c37d72e25bc27fe9e2c54c. Книги для программистов: https://clcks.ru/books-for-it 68 Глава 4 Структура Dockerfile Для создания пользовательского образа Docker с помощью инструментов по умол­ чанию потребуется Dockerfile. В этом файле описаны все шаги, с помощью кото­ рых создается образ, и обычно он хранится в корневом каталоге репозитория исходного кода приложения. Далее показано, как может выглядеть типичный Dockerfile. В этом примере созда­ ется контейнер для приложения на Node.js: FRCМ node:18.13.0 ARG email="anna@example.com" LAВEL "maintainer"=$email LAВEL "rating"="Five Stars" "class"="First Class" USER root ENV АР /data/app ENV SCPATH /etc/supervisor/conf.d RIJN apt-get -у update # Демоны RIJN apt-get -у install supervisor RIJN mkdir -р /var/log/supervisor # Конфигурация супервайзера СОРУ ./supervisord/conf.d/* $SCPATH/ # Код приложения СОРУ * .js* $АР/ 'IIORIФIR $АР RIJN npm install СМ) ["supervisord", "-n"] Если подробно рассмотреть этот Dockerfile, можно получить представление об ин­ струкциях, с помощью которых собирается образ. Каждая строка в Dockerfile соз­ дает новый слой образа, который хранится Docker. Этот сло.й включает все измене­ ния, которые были внесены этой командой. Получается, что при сборке новых об­ разов Docker будет собирать только слои, которые отличаются от предыдущих сборок - слои без изменений можно использовать повторно. Mw можем собрать экземпляр Node из простого базового образа Linux или найти официальные образы для Node в Docker Hub (https://registry.hub.docker.com/). Сообщество Node.js предлагает серию образов Docker (https://registry.hub. docker.com/_/node) и теги, чтобы можно бьшо быстро определить доступные вер­ сии. Если мы хотим привязать образ к определенному релизу Node, можно, напри­ мер, добавить тег node:18.13.0. В следующем примере мы получаем образ Ubuntu Linux для Node 11.11.х: FRCМ docker.io/node:18.13.0 Параметр ARG предоставляет способ задавать переменные и их значения по умолча­ нию, которые доступны только во время сборки образа: ARG email ="anna@exa.mple . сот" Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 1 69 Применение меток к образам и контейнерам позволяет добавлять метаданные с по­ мощью пар "ключ-значение", которые затем можно использовать для поиска и идентификации контейнеров и образов Docker. Просмотреть метки для образа можно с помощью команды docker image inspect. Для метки maintainer (ответствен­ ный по сопровождению) мы задаем значение аргумента сборки email, которое было определено в предыдущей строке Dockerfile. Это значит, что метку можно изме­ нить при сборке образа: LAВEL "maintainer"=$email LAВEL "rating"="Five Stars" "class"="First Class" По умолчанию Docker выполняет все процессы с правами root в контейнере, но мы можем изменить этот параметр с помощью инструкции USER: USER root Хотя контейнеры предоставляют некоторую изоляцию от базовой операционной системы, они все же выполняются на ядре хоста. В связи с потенциальными рис­ ками для безопасности в рабочей среде контейнеры почти всегда должны выпол­ няться в контексте непривилегированного пользователя. В отличие от инструкции ARG, инструкция ENV позволяет задавать переменные ко­ мандной оболочки, которые приложение может использовать для конфигурации во время работы, а также в процессе сборки. Инструкции ENV и ARG упрощают Dockerfile и помогают соблюдать принцип DRY (Don't Repeat Уourself - не повто­ ряйся): ENV АР /data/app ENV SCPATH /etc/supervisor/conf.d В следующем фрагменте кода мы с помощью набора инструкций RUN создаем необ­ ходимую структуру файла и устанавливаем обязательные зависимости: RIJN apt-get -у update # Демоны RIJN apt-get -у install supervisor RIJN mkdir -р /var/log/supervisor &� Для простоты здесь мы включили команды вроде apt-get -у update или dnf -у � 1 update, но обычно их не рекомендуют в Dockerfile приложения, потому что потребу­ ется проходить по индексу репозитория при каждом запуске сборки, т. е. сборка не будет воспроизводимой, ведь версии пакетов могут меняться между сборками. Образ приложения может базироваться на другом образе, в котором уже применены обновления и версии находятся в известном состоянии. Так будет быстрее, и образ можно будет легко воспроизвести. Инструкция СОРУ служит для копирования файлов из локальной файловой системы в наш образ. Чаще всего в них входит код приложения и необходимые дополни­ тельные файлы. Поскольку СОРУ копирует файлы в образ, для их использования нам не придется обращаться к файловой системе после сборки образа. Мы будем при­ менять переменные сборки, определенные в предыдущем разделе, чтобы упростить себе работу и не допустить опечаток: Книги для программистов: https://clcks.ru/books-for-it 70 Глава 4 # Конфигурация супервайзера СОРУ ./supervisord/conf.d/* $SCPATH/ # Код приложения СОРУ * .js* $АР/ Каждая инструкция создает новый слой образа Docker, поэтому разумно объеди­ нить несколько логически связанных команд на одной строке. Мы даже можем совместить инструкции СОРУ и RUN, чтобы скопировать в образ сложный скрипт, а затем выполнять этот скрипт всего двумя командами в Dockerfile. С помощью инструкции WORКDIR мы меняем рабочий каталог в образе для остальных инструкций сборки и процесса по умолчанию, который запускается с получивши­ мися контейнерами: tIORIФIR $АР RIJN npm install Порядок команд в Dockerfile заметно влияет на время сборки. Располагайте ко­ манды в таком порядке, чтобы изменения находились ближе к нижней части. Это значит, что добавлять код и выполнять аналогичные действия следует в конце. При повторной сборке образа будет пересобираться каждый слой, который идет после первого найденного изменения. В конце мы пишем инструкцию смо, чтобы определить команду, запускающую про­ цесс, который будет выполняться в контейнере: а,ю [ "supervisord", "-n"] Это не строгое правило, но обычно рекомендуется запускать в контейнере только один процесс. Идея в том, что контейнер должен выполнять одну функцию, и мы могли бы легко горизонтально масштабировать отдельные функции в архитектуре. В этом примере мы используем supervisord для управления процессами, чтобы повысить отказоустойчивость приложения в контейнере и обеспечить его стабильную работу. Кроме того, таким образом мы сможем искать и устранять неисправ­ ности в приложении во время развертывания, чтобы перезапускать сервис, не пе­ резапуская целый контейнер. Подобного эффекта можно достичь с помощью аргумента командной строки --init для docker container run, о котором мы поговорим в разделе "Контроль процес­ сов" главы 7. Сборка образа Для сборки первого образа склонируйте репозиторий Git, который содержит при­ мер приложения docker-node-hello, как показано здесь2: $ git clone https://githuЬ.com/spkane/docker-node-hello.git \ --config core.autocrlf=input Cloning into 'docker-node-hello' ... 2 Этот код взят с GitНub (https://github.com/enokd/docker-node-hello). Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 1 71 remote: Counting objects: 41, done. remote: Total 41 (delta 0), reused О (delta О), pack-reused 41 Unpacking objects: 100% (41/41), done. $ cd docker-node-hello Git обычно уже установлен в Linux и macOS, но если у вас его нет, загрузите про­ стой установщик с сайта https://git-scm.com/downloads. Опция --config core.autocrlf=input помогает защититься от случайного изменения окончания строк, поскольку ожидается соответствие стандарту Linux. В итоге мы загрузим работающий Dockerfile и соответствующие файлы с исходным кодом в каталог docker-node-hello. Если мы посмотрим на содержимое с игнориро­ ванием каталога в репозитории Git, то увидим следующее: $ tree -а -I .git гГ .dockerignore .gitignore Dockerfile index.js package.json L. supervisord conf.d node.conf L. supervisord.conf Г г г гг Давайте рассмотрим самые важные файлы в этом репозитории. Dockerfile должен быть таким же, как мы только что видели. В файле .dockerignore мы указываем файлы и каталоги, которые не хотим отправ­ лять на хост Docker при сборке образа. В этом примере файл .dockerignore содер­ жит следующую строку: .git Это значит, что команда docker image build будет исключать из сборки каталог .git, который содержит целый репозиторий исходного кода. Остальные файлы отражают текущее состояние исходного кода в выгруженной ветви. Нам не потребуется содержимое каталога .git для сборки образа Docker, и поскольку со временем он может разрастись до огромных размеров, мы не хотим тратить время на то, чтобы копировать его при каждой сборке. В файле package.json определяется приложение Node.js и его зависимости. Файл index.js содержит главный исходный код прило­ жения. Каталог supervisord содержит файлы конфигурации для supervisord, которые пона­ добятся нам для запуска и мониторинга приложения. Использовать supervisord (http://supervisord.org/) в этом примере для мониторинга приложений - это чересчур, но так будет понятнее, какие методы в контейнере позволяют получить больше контроля над приложением и его состоянием. Книги для программистов: https://clcks.ru/books-for-it 72 Глава 4 Как мы узнали в главе 3, мы сможем собрать образ Docker, если у нас есть запу­ щенный сервер Docker и правильно настроенный клиент для взаимодействия с ним. Если все работает, мы сможем запустить новую сборку следующей командой, ко­ торая соберет и пометит тегом образ на основе файлов в текущем каталоге. Каждый шаг, определенный в следующих выходных данных, напрямую соотносит­ ся со строкой в Dockerfile, и на каждом шаге создается новый слой образа на основе предыдущего шага. На первую сборку уйдет несколько минут, потому что нужно будет загрузить базовый образ node. Дальше сборки будут вьmолняться гораздо бы­ стрее, если только не появится новая версия тега базового образа. Вывод, приведенный далее, взят из нового BuildКit, включенного в Docker. Если вы получили совсем другой вывод, скорее всего, у вас более старый код сборки обра­ зов. Включите BuildКit в своей среде, задав для переменной среды DOCKER_BUILDKIT значение 1. Подробнее об этом можно узнать на сайте Docker (https://docs. docker.com/bui ld/bui ldkit). В конце команды build стоит точка. Она относится к контексту сборки и указывает Docker, какие файлы нужно загрузить на сервер, чтобы можно было собрать образ. Чаще всего в конце команды build будет стоять точка, обозначающая текущий ката­ лог. По контексту сборки файл .dockerignore отфильтровывает содержимое, чтобы мы не загрузили больше, чем нужно. "f!'l -- Docker предполагает, что Dockerfile находится в текущем каталоге, но если он хра­ нится где-то в другом месте, мы можем указать его с помощью аргумента -f. Давайте выполним сборку: $ docker image build -t example/docker-node-hello:latest => [internal] load build definition from Dockerfile => => transferring dockerfile: 37В [internal] load .dockerignore 34В => [internal] load metadata for docker.io/library/node:18.13.0 => CACHED (1/8] FROМ docker.io/library/node:18.13.0@19a9713dЬaf3a3899ad... => [internal] load build context => => transferring context: 233В => (2/8) RUN apt-get -у update => (3/8] RUN apt-get -у install supervisor => (4/8] RUN mkdir -р /var/log/supervisor => (5/8] СОРУ ./supervisord/conf.d/* /etc/supervisor/conf.d/ => (6/8] СОРУ *.js* /data/app/ => (7/8] WORКDIR /data/app => (8/8] RUN npm install => exporting to image => => exporting layers => => => transferring context: Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 73 => => writing image sha256:99184427lca5b984939aЬ49d8lb24d4d53137f04alЬd... => => naming to docker.io/exaпple/docker-node-hello:latest Чтобы ускорить сборку, Docker использует локальный кеш, если считает, что это безопасно. Иногда это приводит к неожиданным проблемам, потому что можно не заметить изменения на нижнем слое. В предыдущем выводе есть строки вроде => [2/8] RUN apt-get -у update. Если вместо этого вы увидите => CACHED [2/8] RUN apt-get -у update, это значит, Docker решил задействовать кеш. Чтобы отключить кеш для сборки, укажите аргумент --no-cache с командой docker image build. Если вы собираете образы Docker в системе, в которой одновременно выполняются другие процессы, можно ограничить ресурсы, доступные сборкам, с помощью методов cgroup, которые мы рассмотрим в главе 5. Подробнее об аргументах коман­ ды docker image build можно узнать из официальной документации (bttps://docs. docker .com/engine/reference/commandline/image_build). С функциональной точки зрения команда docker image build работает так же, как � docker build. Если сборка работает некорректно, перейдите к разделу "Многоэтапные сборки" или "Решение проблем со сборками" далее в этой главе. Запуск образа После успешной сборки образа его можно запустить на хосте Docker с помощью следующей команды: $ docker container run --rm -d -р 8080:8080 \ example/docker-node-hello:latest Эта команда указывает Docker, что нужно создать в фоновом режиме запущенный контейнер из образа с тегом exaпple/docker-node-hello: latest, а затем пробросить порт 8080 в контейнере с портом 8080 на хосте Docker. Если все получится, в контейнере на хосте запустится приложение Node.js. Это можно проверить командой docker container ls. Чтобы увидеть запущенное приложение в действии, в браузере перей­ дите к порту 8080 на хосте Docker. Чтобы определить IР-адрес хоста Docker, просмотрите запись docker context list со звездочкой или проверьте значение переменной среды DOCKER_нosт, если оно задано. Если для DOCKER ENDPOINT задан unix-coкeт, скорее всего, IР-адрес будет 127.о.о.1: $ docker context list NАМЕ ТУРЕ ... DOCKER ENDPOINT default * moby ... unix:///var/run/docker.sock Введите IР-адрес и порт (http://127.0.0.l:8080/ или другой) в адресной строке брау­ зера или в инструменте командной строки, например curl. Вы увидите примерно следующий текст: Hello World. Wish you were here. Книги для программистов: https://clcks.ru/books-for-it 74 Глава 4 Аргументы сборки Если мы посмотрим на собранный образ, то увидим, что для метки maintainer было задано anna@example.com: $ docker image inspect \ example/docker-node-hello:latest I grep maintainer "maintainer": "anna@example.com", Чтобы изменить метку maintainer, можно просто снова запустить сборку, предоста­ вив новое значение для email ARG с помощью аргумента командной строки --Ьuild­ arg, например: $ docker image build --build-arg email=me@example.com \ -t example/docker-node-hello:latest . => => naming to docker.io/example/docker-node-hello:latest После выполнения сборки мы можем проверить результаты, посмотрев новый образ: $ docker image inspect \ example/docker-node-hello:latest I grep maintainer "maintainer": "me@exarrple.com", Инструкции ARG и ENV позволяют очень гибко использовать Dockerfile и избегать по­ вторяющихся значений, которые сложно постоянно обновлять. Переменные среды для конфигурации В файле index.js вы заметите, что часть файла относится к переменной $wно, с по­ мощью которой приложение определяет, кому скажет Hello: var DEFAULT_WHO = "World"; var WHO = process.env.WHO 11 DEFAULT_WHO; арр.get ( '/', function ( req, res) { res.send ( 'Hello ' + WHO + ' . Wish you were here.\n') ; }) ; Давайте коротко рассмотрим, как настроить это приложение, передавая перемен­ ные среды при запуске. Сначала нужно остановить существующий контейнер дву­ мя командами. Первая команда выдаст идентификатор контейнера, который пона­ добится для второй команды: $ docker container 1s CONTAINER ID IМAGE STATUS b7145e06083f example/centos-node-hello:latest Up 4 minutes ... Вывод docker container 1s можно отформатировать с помощью шаблона Go (https://developer.hashicorp.com/nomad/tutorials/templates/go-template�syntax), чтобы отображалась только нужная информация. В предыдущем примере можно выполнить команду docker container 1s --format "tаЫе {{.ID}}\t{{.Image}} Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 1 75 \t{ { .Status} }", чтобы просмотреть только три поля. Если мы выполним docker con tainer ls --quiet без опций форматирования, то получим только идентификатор контейнера. Затем, указав идентификатор контейнера из вывода предьщущей команды, можно остановить выполняющийся контейнер командой: $ docker container stop b7145e06083f Ь7145е0608Зf Команда docker container ls - это функциональный эквивалент docker container list, docker container ps И docker ps. А docker container stop - это функциональный эквивалент docker stop. Затем мы можем добавить один экземпляр аргумента --env в предыдущую команду docker container run и перезапустить контейнер: $ docker container run --rrn -d \ --puЫish mode=ingress,puЫished=8080,target=8080 \ --env WHO="Sean and Karl" \ example/docker-node-hello:latest Если обновить страницу браузера, мы увидим новый текст: Hello Sean and Karl . Wish you were here. Предыдущую команду docker при желании можно сократить: $ docker container run --rrn -d -р 8080:8080 \ -е WHO="Sean and Karl" \ � example/docker-node-hello:latest Теперь можно остановить контейнер командой docker container stop, передав иден­ тификатор контейнера. Пользовательские базовые образы Базовые образы - это образы нижнего уровня, на которых строятся остальные об­ разы Docker. Чаще всего они основаны на минимальных установках дистрибутивов Linux, например Ubuntu, Fedora или Alpine Linux, но могут быть значительно меньше и содержать, например, один статически скомпилированный файл. Боль­ шинство людей предпочитают использовать официальные базовые образы для любимого дистрибутива или инструмента. Правда, иногда лучше создать собственный базовый образ. Например, чтобы под­ держивать единый образ операционной системы для всех методов развертывания для физического оборудования, виртуальных машин и контейнеров. Еще одна при­ чина - сокращение размера образа. Не нужно передавать дистрибутив Ubuntu це­ ликом, например, если мы развертываем статически собранное приложение на С или Go. Нам понадобятся только стандартные инструменты для отладки и еще Книги для программистов: https://clcks.ru/books-for-it 76 1 Глава 4 несколько двоичных файлов и shеll-команд. Стоит потратить время на создание такого образа, чтобы ускорить развертывание и упростить распространение прило­ жения. Обычно в обоих случаях собирают образы на основе Alpine Linux, потому что обра­ зы получаются очень маленькими, и это отличный вариант для Docker. Alpine Linux основан на легкой и современной стандартной библиотеке musl (https:// musl.libc.org/), а не традиционной GNU С Library (glibc) (https://www.gnu.org/ softwareЛibc). Обычно в связи с этим не возникает проблем, поскольку многие пакеты поддерживают musl, но об этом лучше помнить. Особенно это влияет на приложения на основе Java и разрешение DNS-имен. Из-за небольшого размера образа такой вариант часто применяется в рабочей среде. Alpine Linux экономит пространство, поэтому по умолчанию поставляется с /Ьin/sh, а не /Ьin/Ьash. При не­ обходимости мы можем установить glibc и bash в Alpine Linux, и многие так делают для контейнеров на виртуальных машинах Java. В официальной документации Docker можно найти полезную информацию о том, как собирать базовые образы на разных дистрибутивах Linux (https://dockr.ly/ 2NlFZcU). Хранение образов Итак, мы создали нужный образ Docker, а теперь хотим сохранить его там, где он будет легкодоступен для любого хоста Docker, на котором мы захотим его развер­ нуть. На этапе сохранения образов для будущего развертывания часто происходит передача ответственности. Обычно мы не собираем образы в рабочей среде, чтобы сразу запустить их там. Мы описывали этот процесс, когда рассматривали взаимо­ действие между командами при развертывании приложения. Как правило, при раз­ вертывании образ извлекается из репозитория и запускается на одном или несколь­ ких серверах Linux. Существуют несколько способов сохранения образов в цен­ тральном репозитории, откуда их будет легко извлекать. Публичные реестры Docker предоставляет реестр образов (https://registry.hub.docker.com/), где хра­ нятся общедостутrnые образы от сообщества, в том числе официальные образы для дистрибутивов Linux, готовые контейнеры WordPress и многое другое. Если мы хотим опубликовать образ в Интернете, лучше всего выбрать публичный реестр, например Docker Hub (https://hub.docker.com/). Правда, есть и другие варианты. Когда основные инструменты Docker только набирали популярность, Docker Hub еще не существовал. Чтобы заполнить этот пробел, был создан Quay.io (https://quay.io/). Quay.io несколько раз менял владельца и сейчас принадлежит Red Hat. Поставщики облачных сервисов, вроде Google, и компании, предоставляющие услуги SaaS, например GitHub, предлагают свои реестры. Здесь мы будем рассмат­ ривать только два варианта. Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 1 77 Docker Hub и Quay.io предлагают централизованные реестры образов Docker, дос­ тупные при подключении к Интернету, и позволяют хранить не только общедос­ тупные, но и частные образы. У обоих реестров есть удобные интерфейсы и воз­ можность разделять разрешения на доступ и управлять пользователями. У обоих реестров есть хорошие платные варианты для частного хостинга образов - похо­ жим образом GitHub продает частные реестры в своих системах. Это неплохой начальный вариант, если вы планируете всерьез работать с Docker, но у вас пока недостаточно кода, чтобы вам понадобилось решение для внутреннего хостинга. Главный минус этих реестров для компаний, которые активно используют Docker, - они не находятся в сети, где развертывается приложение. Это значит, что каждый слой каждого развертывания придется передать через Интернет. За­ держки при передаче замедляют развертывание программного обеспечения, а пере­ бои с доступом к реестрам могут нарушить график развертывания. Огчасти эти проблемы решаются с помощью тщательно продуманной структуры образов, ведь тонкие слои гораздо проще передавать через Интернет. Частные реестры Еще один вариант, который выбирают многие компании, - собственный реестр образов Docker, который взаимодействует с клиентом Docker и поддерживает отправку, извлечение и поиск образов. Проект Distribution (https://github.com/ distribution/distribution) с открытым кодом предоставляет базовый функционал, на котором строится большинство остальных реестров. Также неплохими вариантами частных реестров считаются Harbor (https:// goharbor.io/) и Red Hat Quay (https://www.redhat.com/en/technologies/cloud­ computing/quay). Помимо базового функционала реестров Docker эти решения предлагают удобные графические пользовательские интерфейсы с множеством до­ полнительных функций, таких как верификация образов. Аутентификация для входа в реестр Взаимодействие с реестром, который хранит образы контейнеров, - это часть по­ вседневной работы с Docker. Многие реестры требуют аутентификации для доступа к образам. Docker пытается упростить автоматизацию этих задач - он сохраняет данные о входе и использует их при необходимости, например когда вы хотите извлечь частный образ. Docker считает реестром по умолчанию Docker Hub, пуб­ личный репозиторий от Docker, Inc. Потребуются дополнительные усилия, но вы можете настроить демон Docker 3 таким образом, чтобы использовать пользовательское зеркало реестра или кеш 4 образов в режиме pull-through . 3 4 Полный URL: https://docs.docker.com/registry/recipes/mirror/#configure-the-docker-daemon. Полный URL: https://docs.docker.com/registry/recipes/mirror/#run-a-registry-as-a-pull-through-cache. Книги для программистов: https://clcks.ru/books-for-it 78 1 Глава 4 Создание учетной записи Docker Hub В этих примерах мы создадим учетную запись на Docker Hub. Для загрузки обще­ доступных образов учетная запись не нужна, но нам нужно будет зарегистриро­ ваться, чтобы избежать ограничений по скорости и отправлять в реестр свои кон­ тейнеры. Для создания учетной записи перейдите на сайте Docker Hub (https://hub. docker.com/) в mобом браузере. Войдите в существующую учетную запись или создайте новую, указав адрес элек­ тронной почты. При создании учетной записи Docker Hub отправит электронное письмо с подтверждением на указанный адрес. Откройте письмо и перейдите по ссылке подтверждения, чтобы завершить процесс проверки. Теперь у нас есть общедоступный реестр, куда можно отправлять новые образы. В настройках учетной записи (https://hub.docker.com/settings/default-privacy) под фотографией профиля есть раздел Default Privacy (Параметры конфиденциально­ сти по умолчанию), где можно при необходимости изменить видимость реестра на private (частный). Для дополнительной безопасности при создании учетной записи и входа исполь­ зуйте персональный токен доступа с ограниченными привилегиями (https://docs. docker.com/go/access-tokens). Вход в реестр Давайте войдем в реестр Docker Hub с помощью своей учетной записи: $ docker login Login with your Docker ID to push and pull images from Docker НuЬ. If you don't have а Docker ID, head over to https://huЬ.docker.com to create one. Username: <имя пользователя> Password: <пароль/токен> Logiп Succeeded r.:l � Команда docker login - это функциональный эквивалент команды docker login docker.io. Если сервер отправил подтверждение Login succeeded (Вход выполнен успешно), мы можем извлекать из реестра образы. Как это произошло? Docker записал dotfile в домашнем каталоге, чтобы кешировать эту информацию. Установлены разреше­ ния 0600, чтобы другие пользователи не могли прочитать наши учетные данные. В файле мы увидим примерно такое содержимое: $ 1s -la ${HOME)/.docker/config.json -rw-------@ 1 ... 158 Dec 24 10:37 /Users/someuser/.docker/config.json $ cat ${HOME)/.docker/config.json Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 1 79 В Linux мы увидим следующие строки: "auths": { "https://index.docker.io/vl/ 11 : "auth":"cmVsaEXamPLЗhElRmFCOUE=", "email":"someuser@example.com" Docker непрерывно развивается и добавляет поддержку для нативных систем управления секретами разных операционных систем, например macOS Keychain или Диспетчер учетных данных Windows. Ваш файл config.json может выглядеть совсем не так, как в рассмотренном примере. Существуют различные менеджеры учетных данных (https://github.com/docker/docker-credential-helpers) для разных платформ, которые упростят вам жизнь. Заачение auth , файле ,онфщрации ,nиента Oocker имеет rодиров,у base64. Оно не зашифровано. Обычно это представляет проблему только в системах Linux с несколькими пользователями, поскольку в системе нет менеджера учетных дан­ � ных по умолчанию, и другие привилегированные пользователи смоrут читать файл конфигурации клиента Docker и хранящиеся в нем секреты. Мы можем настроить gpg или pass для шифрования этих файлов на Linux. Здесь мы видим, что файл ${HOМE}/.docker/config.json содержит учетные данные docker.io для пользователя someuser@example.com в JSON. Этот файл конфигура­ ции поддерживает хранение учетных данных для нескольких реестров. Пока у нас будет только одна запись, для Docker Hub, но при необходимости их можно добав­ лять. Если теперь реестр потребует аутентификацию, Docker будет искать учетные данные в файле ${HOМE}/.docker/config.json. Если они там есть для этого имени хоста, он их предоставит. Вы могли заметить, что у нас отсутствует одно значение: метка времени. Учетные данные хранятся неопределенное время или до тех пор, пока вы не удалите их сами. Если мы больше не хотим хранить учетные данные в кеше, то можем выйти из рее­ стра: $ docker logout Removing login credentials for https://index.docker.io/vl/ $ cat ${HOМE}/.docker/config.json { "auths": { Таким образом мы удаляем кешированные учетные данные, и Docker больше не будет их хранить. Некоторые версии Docker даже удаляют этот файл, если в нем ничего нет. Если мы попытаемся войти в другой реестр, кроме Docker Hub, то можем указать имя хоста в командной строке: $ docker login someregistry.example.com Книги для программистов: https://clcks.ru/books-for-it 80 Глава 4 В файл ${HOМE}/.docker/con:fig.json будет добавлена еще одна запись об аутенти­ фикации. Отправка образов в репозиторий На первом этапе отправки образа мы должны убедиться, что вошли в нужный репо­ зиторий Docker. В этом примере мы будем использовать Docker Hub, так что вой­ дите в Docker Hub, указав свои учетные данные: $ docker login Login with your Docker ID to push and pull images from Docker НuЬ. If you don't have а Docker ID, head over to https://huЬ.docker.com to create one. Username: <имя пользователя> Password: <пароль/токен> Login Succeeded Logging in with your password grants your terminal coЩ)lete access to your account. Войдя в реестр, отправьте образ. Раньше мы использовали команду ctocker image build -t example/docker-node-hello: latest . для сборки образа docker-node-hello. В реальности клиент Docker (и по соображениям совместимости многие другие ин­ струменты для работы с контейнерами) интерпретирует example/docker-node­ hello: latest как docker.io/example/docker-node-hello:latest. Здесь docker. io означает имя хоста реестра образов, а example/docker-node-hello - это репозиторий внутри реестра, который содержит нужные образы. При локальной сборке образа можно указать любое имя реестра и репозитория. Если мы будем отправлять образ в реальный реестр, имя должно совпадать с логи­ ном. Если мы хотим изменить теги для уже созданного образа, можно выполнить сле­ дующую команду, заменив $/<myuser>J на свое имя пользователя Docker Hub: $ docker image tag example/docker-node-hello:latest \ docker.io/$(<myuser>}/docker-node-hello:latest Если нужно заново собрать образ с новым соглашением об именовании или мы просто хотим поэкспериментировать, можно выполнить следующую команду в ра­ бочем каталоге docker-no de-hello, созданном при операции git checkout чуть раньше в этой главе: $ docker image build -t docker.io/${<myuser>}/docker-node-hello:latest . � В следующих примерах вместо $(<myuser>} указывайте свое имя пользователя на Docker Hub. Если вы используете другой реестр, заменяйте docker.io на имя хоста вашего реестра. Первая сборка займет некоторое время. Повторная сборка будет выполняться го­ раздо быстрее. Это связано с тем, что почти все или даже все слои уже существуют на сервере Docker после предыдущей сборки. Мы можем быстро проверить, нахоКниги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 81 1 дится ли образ на сервере, выполнив команду docker image 1s $ ( <myuser>}/docker-node­ hello: $ docker image 1s $(<myuser>}/docker-node-hello REPOSITORY TAG IМAGE ID latest f683df27f02d myuser/docker-node-hello CREATED дЬоut an hour ago SIZE 649МВ Мы можем отформатировать вывод команды docker image 1s, чтобы просматри­ вать только нужные данные, с помощью арrумента --format: docker image 1s --format="taЫe ((.ID}}\t((.Repository}}". Сейчас мы можем отправить образ в репозиторий Docker с помощью команды dockerimage push: $ docker image push $(<myuser>}/docker-node-hello:latest Using default tag: latest The push refers to repository [docker.io/myuser/docker-node-hello] 5f3ee7afc69c: Pushed 5bb0785f2eee: Mounted from library/node latest: digest: sha256:f5ceb032aec36fcacaЬ7le468eaf0ba8a832cfc8244fЬc784d0... Если этот образ отправлен в публичный репозиторий, кто угодно может загрузить его с помощью команды docker image pull. $ docker image pull ${<myuser>}/docker-node-hello:latest Using default tag: latest latest: Pulling from myuser/docker-node-hello Digest: sha256:f5ceb032aec36fcacaЬ7le468eaf0ba8a832cfc8244fЬc784d040872be04lcd5 Status: Image is up to date for myuser/docker-node-hello:latest docker.io/myuser/ docker-node-hello:latest Если вы загрузили образ в частный репозиторий, пользователь должен войти в него под своими учетными данными с помощью команды docker login, чтобы скачать образ в локальную систему. Поиск образов в Docker Hub Мы можем искать доступные образы не только на сайте (https://hub.docker.com/), но и с помощью команды docker search. Docker Hub Например, команда docker search node найдет образы со словом node в имени или описании: $ docker search node NАМЕ DESCRIPТION STARS OFFICIAL АUТОМАТЕD node Node.js is а JavaScript-ba... 12267 [ОК] mongo-express Web-based MongoDB admin in... nodered/node-red Low-code prograпming for е... nodered/node-red-docker Deprecated - older Node-RE... circleci/node Node.js is а JavaScript-ba... 1274 544 356 130 [ОК] [ОК] Книги для программистов: https://clcks.ru/books-for-it 82 Глава 4 kindest/node Ьitnami/node cimg/node opendronemap/nodeodm Ьitnami/node-exporter appdynamics/nodejs-agent wallarm/node sigs.k8s.io/kind node imag... Bitnami Node.js Docker Ima... The CircleCI Node.js Docke... Automated build for NodeOD... Bitnami Node Exporter Dock... Agent for monitoring Node.... Wallarm: end-to-end API se... 78 69 14 10 9 5 5 [ОК] [ОК] [ОК] [ОК] Заголовок OFFICIAL указывает, что это официальный рекомендованный образ (https://docs.docker.com/docker-huЬ/official_images) с Docker Hub. Обычно это означает, что образ принадлежит компании или официальному сообществу разра­ ботчиков, которое занимается сопровождением приложения. Заголовок AUTOМATED указывает, что образ собран и отправлен автоматически в ходе процесса CVCD, который запускается через коммиты в базовом репозитории исходного кода. Офи­ циальные образы всегда имеют этот заголовок. Управление частным реестром Docker поддерживает философию программного обеспечения с открытым исход­ ным кодом и рекомендует сообществу делиться образами Docker через Docker Hub по умолчанию. Иногда это невозможно по коммерческим или юридическим причи­ нам, из-за требований к хранению образов или проблем с надежностью. В таких случаях имеет _смысл поднять внутренний частный реестр. Создать базо­ вый реестр несложно, но если вы будете использовать его в рабочей среде, следует изучить все доступные варианты конфигурации для Docker Registry с открытым исходным кодом (https://docs.docker.com/registry). В этом примере мы создадим очень простой защищенный реестр с поддержкой SSL и базовой НТТР-аутентификацией. Сначала создадим на сервере Docker несколько каталогов и файлов. Если вы ис­ пользуете виртуальную машину или облако для сервера Docker, подключитесь к нему через SSH, чтобы выполнить несколько команд. Если вы применяете Docker Desktop или Community Edition, их можно выполнить в локальной системе. Пользователям Windows нужно будет загрузить дополнительные инструменты, наnри�ер htpassw�, или скорректировать команды, чтобы выполнить те же задачи в своеи локальнои системе. Склонируем репозиторий Git, который содержит базовые файлы для создания про­ стого реестра Docker с аутентификацией: $ git clone https://githuЬ.com/spkane/basic-registry \ --config core.autocrlf=input Cloning into 'basic-registry' ... remote: Counting objects: 10, done. remote: Compressing objects: 100% (8/8), done. remote: Total 10 (delta О), reused 10 (delta 0), pack-reused О Unpacking objects: 100% .(10/10), done. Книги для программистов: https://clcks.ru/books-for-it Работа с образами !)ocker 1 83 Загрузив файлы ло�ально, мы можем перейти в соответствующий каталог и про­ смотреть, что мы только что загр_узили: $ ed basie-registry $ 1s Doekerfile eonfig.yaml.sample registry.ert.sample README.md registry.key.sample htpasswd.sample Dockerfile, на основе базового образа с Docker Hub, копирует локальные файлы конфигурации и сопутствующие файлы в новый образ. Файлы с примерами пригодны для тестирования, но не для рабочей среды. Если сервер Docker дОС'I)'ПеН через loealhost (127.0.0.1), просто скопируйте файлы без изменений: $ ер eonfig.yaml.sample eonfig.yaml $ ер registry. key.sample registry.key $ ер registry.ert.sample registry.ert $ ер htpasswd.sample htpasswd Если сервер Docker находится на удаленном IР-адресе, потребуется чуть больше усилий. Сначала скопируйте config.yaml.sample в config.yaml: $ ер eonfig.yaml.sample eonfig.yaml Затем в config.yaml замените 127.о.о.1 на IР-адрес сервера Docker. Вместо: http: host: https://127.0.0.1:5000 у вас получится что-то вроде: http: host: https://172.17.42.10:5000 Мы можем легко создать реестр с помощью полного доменного имени (Fully Qualified Domain Name, FQDN), например my-registry. example. сот, но в этом при­ мере проще работать с IР-адресами, потому что DNS не требуется. Затем нужно будет создать пару ключей SSL для IР-адреса реестра. Это можно сде­ лать с помощью следующей команды OpenSSL: $ openssl req -х509 -nodes -sha256 -newkey rsa:4096 \ -keyout registry.key -out registry.ert \ -days 14 -suЬj '{/CN=l72.17.42.10}' IР-адрес в части /CN=l 72.17.42.10 команды должен соответствовать IР-адресу сервера Docker. Наконец, можно скопировать файл примера htpasswd: $ ер htpasswd.sample htpasswd Книги для программистов: https://clcks.ru/books-for-it 84 Глава 4 Или создайте свое имя пользователя и пароль для аутентификации с помощью сле­ дующей команды, заменив $!<имя_пользователя>} и ${<пароль>}своими значениями: $ docker coпtainer run --rm --entrypoint htpasswd g \ -Bbn ${<имя_пользователя>} ${<пароль>}> htpasswd Снова проверив содержимое каталога, вы увидите примерно следующее: $ ls Dockerfile README.md config.yaml config.yaml.sample htpasswd htpasswd.sample registry.crt registry.crt.sample registry.key registry.key.sample Если какие-то из этих файлов отсутствуют, убедитесь, что вы ничего не пропустили на предыдущих этапах. Если все нормально, вы готовы собрать и запустить реестр: $ docker image build -t my-registry . $ docker container run --rm -d -р 5000:5000 --name registry my-registry $ docker container logs registry Если возникли ошибки, например "docker: Еггог response from daemon: Conflict. The container name "/registry" is already in use" (Сообщение об ошибке от демона: кон­ фликт: имя контейнера /registry уже используется), измените имя предыдущего контейнера или удалите существующий контейнер с этим именем. Удалить контей­ нер можно командой docker container rm registry. Тестирование частного реестра Реестр запущен, можно приступать к тестированию. В первую очередь, нужно пройти аутентификацию и войти в него. Убедитесь, что IР-адрес, указанный в ctocker login, соответствует IР-адресу сервера Docker, где запущен реестр. Имя пользователя по умолчанию - myuser, пароль по умолчанию - myuser-pw ! . Если у вас свой htpasswd, укажите значение из него. � $ docker login 127.0.0.1:5000 Username: <имя_пользователя_реестра> Password: <пароль_реестра> Login Succeeded У этого контейнера реестра есть встроенный ключ SSL, и он не использует внеш­ нее хранилище, а значит, он содержит секрет, и при удалении выполняющегося контейнера будут удалены все образы. Так и задумано. В рабочей среде контейнеры должны получать секреты из системы управления секретами и иметь резервное внешнее хранилище, например хранилище объектов. Если вы хотите сохранять образы реестра в процессе разработки при удалении контейнеров, укажите --mount type=Ьind, source=/tmp/registry- data, target=/var/ liЬ/registry в команде docker container run, чтобы сохранить данные реестра на сервере Docker. Давайте теперь попробуем отправить только что собранный образ в локальный частный реестр. Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 1 85 ; Во всех эп,х командах должен быть у,азан ,оррепны• IР-адрес вашего реестра. $ docker image tag my-registry 127.0.0.1:5000/my-registry $ docker image push 127.0.0.1:5000/my-registry Using default tag: latest The push refers to repository [127.0.0.1:5000/my-registry] f09a0346302c: Pushed 4fc242d58285: Pushed latest: digest: sha256:c374b0a72lal2c4ld5b298930dlle658fЬd37f22dc2a0fac7dбa2... Попробуйте извлечь тот же образ из репозитория: $ docker image pull 127.0.0.1:5000/my-registry Using default tag: latest latest: Pulling from my-registry Digest: sha256:c374b0a72lal2c4ld5b298930dlle658fЬd37f22dc2a0fac7dбa2ecdc0ba5490 Status: Image is up to date for 127.0.0.1:5000/my-registry:latest 127.0.0.1:5000/ my-registry:latest Помните, что Docker Hub и Docker Distribution предоставляют АРl-эндnоинт, к кото­ рому можно отправлять запросы для получения информации. Больше об API читайте в официальной документации (https://github.com/distribution/distribution/ЬloЬ/main/docs/spec/api.md). Если ошибок не возникло, у нас теперь есть рабочий реестр для разработки, и на его основе мы можем создать реестр для рабочей среды. Пока реестр можно оста­ новить, выполнив команду: $ docker container stop registry Познакомившись с Docker Distribution, вы можете изучить оnенсорс-nроект Cloud Native Computing Foundation (CNCF) - НагЬог (https://goharbor.io/), который до­ полняет Docker Distribution множеством функций, связанных с безопасностью и надежностью. Оптимизация образов Начав работать в Docker, вы быстро заметите, что маленькие образы и быстрая сборка помогают ускорить развертывание новых версий программного обеспечения в рабочей среде. В этом разделе мы рассмотрим, о чем нужно помнить при проек­ тировании образов и какими методами достичь этих целей. Контроль размера образов В большинстве современных компаний люди, не задумываясь, загружают через Интернет файлы размером 1 ГБ. Некоторые программы проще скачать, запустить один раз, удалить, а при необходимости скачать снова, чтобы не хранить локаль­ ную копию. Это нормально, если нам нужна одна копия на одном сервере, но предКниги для программистов: https://clcks.ru/books-for-it 86 1 Глава 4 ставьте, что у нас больше ста узлов и мы развертываем новые выпуски несколько раз в день. Большой объем трафика перегружает сеть и замедляет развертывание, и это скоро станет заметно в рабочей среде. Для удобства большое число контейнеров Linux наследуют от базового образа, ко­ торый содержит минимальный дистрибутив Linux. Это удобно для начала работы, но не обязательно. Контейнеры должны содержать только файлы, которые нужны для запуска приложения на ядре хоста, и ничего лишнего. Лучший способ это по­ нять - рассмотреть минимальный контейнер. Go - это компилируемый язык программирования, который позволяет легко соз­ давать статически скомпонованные двоичные файлы. В этом примере мы возьмем очень маленькое веб-приложение на Go с GitHub (https://github.com/spkane/ scratch-helloworld). Давайте посмотрим, что делает· это приложение. Выполним следующую команду, откроем браузер и подключимся к хосту Docker на порте 8080 (например, Ьttp://127.0.0.1:8080 для Docker Desktop и Community Edition): $ docker container run --rm -d -р 8080:8080 spkane/scratch-helloworld Если все получилось, мы увидим в браузере следующее сообщение: Hello World from Go in minimal Linux container (Hello World от Go в минимальном контейнере Linux). Давайте посмотрим, какие файлы содержит контейнер. Можно предполо­ жить, что как минимум там будет среда Linux и все файлы для компиляции про­ грамм Go, но мы увидим, что это не так. Не останавливая контейнер, выполним следующую команду, чтобы узнать его идентификатор. Эта команда возвращает информацию для последнего созданного контейнера: $ docker container 1s -1 CREATED СОММАND CONTAINER ID IМAGE ddc3fбlf311b spkane/scratch-helloworld "/helloworld" 4 minutes ago Узнав идентификатор контейнера, мы можем экспортировать файлы в Т АR-файл, чтобы изучить их: $ docker container export ddc3fбlf311b -о web-app.tar С помощью команды tar мы можем посмотреть содержимое контейнера на момент экспорта: $ tar -tvf web-app.tar о о о о -rwxr-xr-x Jan 7 15:54 .dockerenv drwxr-xr-x Jan 7 15:54 dev/ о о о о Jan 7 15:54 dev/console о о о о -rwxr-xr-x о о о о drwxr-xr-x Jan 7 15:54 dev/pts/ Jan 7 15:54 dev/shm/ о о о о drwxr-xr-x Jan 7 15:54 etc/ drwxr-xr-x о о о о Jan 7 15:54 etc/hostname о о о о -rwxr-xr-x Jan 7 15:54 etc/hosts о о о о -rwxr-xr-x Jan 7 15:54 etc/mtaЬ -> /proc/mounts lrwxrwxrwx о о о о Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker -rwxr-xr-x -rwxr-xr-x drwxr-xr-x drwxr-xr-x о о о о о о ,О о о о о о о 3604416 о о Jan Jul Jan Jan 7 15:54 2 2014 7 15:54 7 15:54 1 87 etc/resolv.conf helloworld proc/ sys/ Первое, что бросается в глаза, - в контейнере очень мало файлов, и почти все имеют размер О байтов. Все файлы нулевой длины обязательно должнь1 находиться в каждом контейнере Linux и автоматически монтируются методом Ьind mount (https://unix.stackexchange.com/questions/198590/what-is-a-Ьind-mount) с хоста в контейнер при его создании. Все эти файлы, кроме .dockerenv, необходимы для корректной работы. Единственный файл с действительным содержимым относится к нашему приложению - это статически скомпилированный двоичный файл helloworld. Как видите, контейнеры обязательно должны содержать только то, что необходимо для запуска на ядре. Все остальное можно не включать. Для диагностики и устра­ нения неполадок полезно иметь доступ к командной оболочке контейнера, поэтому люди часто идут на компромисс и собирают образы на основе легких дистрибути­ вов Linux, вроде Alpine Linux. Если вы часто изучаете файлы образов, попробуйте инструмент dive (https:// github.com/wagoodman/dive), который предоставляет удобный интерфейс CLI для просмотра содержимого образов. Давайте заглянем глубже: вернемся к тому же контейнеру и рассмотрим базовую файловую систему, чтобы сравнить ее с популярным базовым образом alpine. Мы могли бы легко заглянуть в образ alpine с помощью команды docker container run -ti alpine:latest /Ьin/sh, но для образа spkane/scratch-helloworld такой способ не подходит, пот_ому что в -нем нет командной оболочки или SSH. Получается, мы не можем изучить его с помощью ssh, nsenter или docker container ехес, хотя есть один сложный прием, который мы· рассмотрим в разделе "Отладка контейнеров без ко­ мандной-оболочки II главы 11. Ранее мы с помощью команды docker container export создали файл .tar с копией всех файлов в контейнере, �о на этот раз мы изучим файловую систему кшrrейцера, напрямую подключившись к серверу Docker. Для этого нужно найти_ файлы образа на диске сервера. Чтобы определить, где име,нно на сервере хранятся файлы, выполним команду docker image insp.ect для образ.а alpine: latest: $ docker image inspect alpine:latest .[ "Id": "sha25?:3fd...353", "RepoTags": [ ], "alpine:latest" "RepoDigests": [ "alpine@sha256: 7Ь8 ... f8b" ], Книги для программистов: https://clcks.ru/books-for-it 88 Глава 4 11GraphDriver 11 : 11 Data 11 : { ''МergedDir 11 : 11 /var/liЬ/docker/overlay2/ea8...13a/merged11 , 11UpperDir 11 : 11 /var/liЬ/docker/overlay2/ea8... 13a/diff 11 , 11WorkDir 11 : 11 /var/liЬ/docker/overlay2/ea8...13a/work 11 }, 11Name 11 : 1 overlay2 11 1 А затем для образа spkane/scratch-helloworld:latest: $ docker image inspect spkane/scratch-helloworld:latest 11 Id11 : 11 sha256:4fa... Обd11 11 RepoTags 11 : 1 1 ], spkane/scratch-helloworld:latest 11 11 RepoDigests 11 : 11 ], , [ [ spkane/scratch-helloworld@sha256: 46d...ald11 11 Gr aphDriver 11 : 11 Data 11 : { 11LowerDir 11 : /var/liЬ/docker/overlay2/37a...84d/diff: /var/liЬ/docker/overlay2/28d...ef4/diff", 11 ''МergedDir 11 : 1 1 /var/liЬ/docker/overlay2/fc9... c91/merged", 11UpperDir 11 : 11 /var/liЬ/docker/overlay2/fc9.. .c91/diff", 11WorkDir 11 : 11 }, /var/liЬ/docker/overlay2/fc9...c91/work" 11Name 11 : 11 overlaу2" Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker � 1 89 В этом примере мы используем Docker Desktop на macOS, -но такой подход будет работать на большинстве серверов Docker. Подключайтесь к серверу Docker лю­ бым удобным для вас способом. Поскольку мы используем Docker Desktop, нам потребуется nsenter для входа в вир­ туальную машину без SSH: $ docker container run --rm -it --privileged --pid=host debian \ nsenter -t 1 -m -u -n -i sh / # Войдя в виртуальную машину, мы можем изучать различные каталоги из секции GraphDriver в выводе команды docker image inspect. В этом примере мы посмотрим на первую запись для образа alpine. Мы увидим, что она помечена как мergedDir и указывает на папку /var/liЬ/docker/overlay2/ea86408b 2Ьl5d33ee27d78ff44f8210470528622lf055bal33lb58673f4b3lЗa/merged. Если мы попробуем вывести содержимое этого каталога, то получим ошибку, но, заглянув в родительский каталог, мы увидим, что на самом деле нам нужен каталог diff: / # 1s -lFa /var/liЬ/docker/overlay2/ea ...3a/merged 1s: /var/liЬ/docker/overlay2/ea ..Зa/merged: No such file or directory / # 1s -lF /var/liЬ/docker/overlay2/ea...3a/ total В 4096 Mar 15 19:27 diff/ 18 root root drwxr-xr-x 26 Mar 15 19:27 link root 1 root -rw-r--r-/ # 1s -lF /var/liЬ/docker/overlay2/ea...3a/diff total 64 Jan 9 19:37 2 root 4096 root drwxr-xr-x 9 19:37 2 root 4096 Jan root drwxr-xr-x 9 19:37 4096 Jan root root drwxr-xr-x 15 9 19:37 4096 Jan root drwxr-xr-x 2 root 4096 Jan 9 19:37 drwxr-xr-x 5 root root 9 19:37 4096 root root Jan drwxr-xr-x 5 2 root root 9 19:37 4096 Jan drwxr-xr-x 2 4096 root root Jan dr-xr-xr-x 9 19:37 drwx-----2 root root 4096 9 19:37 Jan root drwxr-xr-x 2 root 4096 9 19:37 Jan drwxr-xr-x 2 root root 4096 Jan 9 19:37 2 drwxr-xr-x root 4096 Jan root 9 19:37 2 drwxr-xr-x root 4096 root 9 19:37 Jan 2 drwxrwxrwt root root 4096 9 19:37 Jan 7 drwxr-xr-x root root 4096 Jan 9 19:37 drwxr-xr-x 11 root root 4096 9 19:37 Jan Ьin/ dev/ etc/ home/ liЬ/ media/ mnt/ proc/ root/ run/ sЬin/ srv/ sys/ trnp/ usr/ var/ / # du -sh /var/lib/docker/overlay2/ea ...3a/diff 4.5М /var/liЬ/docker/overlay2/ea ... Зa/diff alpine - очень маленький образ, он весит всего 4,5 МБ и идеально подходит как база под контейнеры. Однако мы видим, что еще до начала сборки в контейнере по-прежнему много всего. Книги для программистов: https://clcks.ru/books-for-it 90 1 Глава 4 Давайте посмотрим 'файлы в образе spkane/scratch-helloworld. В этом случае нас ин­ тересует первый каталог в записи LowerDir в выводе docker image inspect, и в итоге мы снова попадаем в каталог с именем diff: / # ls -lFh /var/liЬ/docker/overlay2/37 ...4d/diff_ total 3520 -rwxr-xr-x root З.4М Jul 2 2014 helloworld* l root / # exit Как видите, в каталоге находится всего один файл размером 3,4 МБ. Двоичный файл helloworld - единственный в этом контейнере, и он меньше, чем начальный размер образа alpine до того, как в него бьmи добавлены другие файлы приложения. Мы можем запустить приложение helloworld из этого каталога прямо на сервере Docker, потому что ему не нужны никакие другие файлы. Но это следует делать только в среде разработки, чтобы лучше понять, в чем могут быть преимущства статически компилируемых приложений. Многоэтапные сборки Во многих случаях мы можем еще больше ограничить размер с помощью много­ этапных сборок (multistage build). Это рекомендованный подход для большинства контейнеров в рабочей среде. В результате нам не приходится волноваться о при­ влечении дополнительных ресурсов для сборки приложения, и контейнер остается очень легким. Контейнеры с многоэтапной сборкой поддерживают сборки в Docker, а это обеспечивает высокую степень воспроизводимости в системе сборки. Как написал автор исходного приложения scratch-helloworld (https://medium.com/ %40adriaandejonge/simplify-the-smallest-possiЬle-docker-image-62c0e0d342ef), под­ держка выпуска многоэтапных сборок в самом Docker значительно упростила цро­ цесс создания небольших контейнеров. Многоэтапные сборки позволяют .легко сделать то, для чего раньше пришлось бы собрать один образ, который компилиру­ ет код, извлечь получившийся двоичный файл, а затем собрать второй образ без зависимостей и вставить в него двоичный файл. Это был сложный процесс, кото­ рый еще нужно бьmо адаптировать к стандартным конвейерам развертывания. Сейчас мы можем достичь подобных результатов с помощью простого Dockerfi\e: # Сборка контейнера FROМ docker.io/golang:alpine as builder RUN apk update && \ apk add git && \ CGO_ENAВLED=0 go install -а -ldflags '-s' \ githuЬ.com/spkane/scratch-helloworld@latest # Рабочий контейнер FROМ scratch СОРУ --from=builder /go/bin/scratch-helloworld /helloworld EXPOSE 8080 CMD [ "/helloworld"] Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 1 91 Первое, что бросается в глаза, - этот файл выглядит так, будто состоит из двух Dockerfile. Это действительно так, но не все так просто. Команда FROM расширена, чтобы можно было присвоить образу имя на этапе сборки. В этом примере первая строка ( FROM docker.io/golang as builder) означает, что сборка должна быть основана на образе golang, и этот образ и шаг сборки мы будем называть builder. В четвертой строке мы снова видим FROM - до введения многоэтапных сборок такой синтаксис был запрещен. В этой строке FROM указано специальное имя образа, scratch, чтобы Docker начал с пустого образа без дополнительных файлов. Следую­ щая строка, СОРУ --from=builder /go/Ьin/scratch-helloworld /helloworld, позволяет ско­ пировать двоичный файл, собранный в образе builder, напрямую в текущий образ. В результате мы получим контейнер минимального размера. Строка EXPOSE 8080 уведомляет пользователей, через какие порты и протоколы (по умолчанию - ТСР) сервис получает данные. Давайте выполним сборку и посмотрим, что получится. Сначала создадим каталог для работы, а затем вставим в любом текстовом редакторе содержимое из преды­ дущего примера в файл с именем Dockerfile: $ mkdir /trnp/multi-build $ cd /tmp/multi-build $ vi Dockerfile ; Мож,о за,рузи;s ,опию этою Dockenile ,а G;tнuь' Мы можем запустить многоэтапную сборку: $ docker image build . [+) Building 9.7s (7/7) FINISHED => [internal) load build definition from Dockerfile => => transferring dockerfile: 37В => [internal) load .dockerignore => => transferring context: 2В => [internal) load metadata for docker.io/library/golang:alpine => CACHED [builder 1/2] FROM docker.io/library/golang:alpine@sha256:7cc6257.. . => [builder 2/2) RUN apk update && apk add git && CGO_ENABLED=0 go install .. . => [stage-1 1/1) СОРУ --from=builder /go/Ьin/scratch-helloworld /helloworld => exporting to image => => exporting layers => => writing image sha256:bb853f23418161927498b9631f54692cflld84dбbde3af2d... Выходные данные будут похожи на большинство остальных сборок. В конце мы получаем сообщение об успешном создании итогового минимального образа. 5 Полный URL: https://githu b.com/Ыuewhalebook/docker-up-and-running-Зrd-edition/ЫoЫmain/chapter_04/multi stage/Dockerfile. Книги для программистов: https://clcks.ru/books-for-it 92 Глава 4 Если вы компилируете двоичные файлы в локальной системе, и они используют общие библиотеки, убедитесь, что процессу в контейнере доступны правильные версии этих библиотек. Эrапов может быть больше двух, и они могут быть никак не связаны друг с другом. Эrапы выполняются по порядку. Например, у нас может быть этап, основанный на общедоступном образе Go, где мы собираем приложение Go в качестве API, и этап на основе контейнера Angular для сборки пользовательского интерфейса веб­ приложения. На последнем этапе можно объединить выходные данные из первых двух. Приступая к сборке более сложных образов, вы заметите, как сложно придержи­ ваться одного контекста сборки. Плагин docker-buildx, о котором мы поговорим ближе к концу этой главы, поддерживает несколько контекстов сборки (https:// www.docker.com/Ыog/dockerfiles-now-support-multiple-build-contexts), поэтому его можно использовать в очень сложных рабочих процессах. Суммирование слоев На первый взгляд это не очевидно, но слои файловой системы, составляющие обра­ зы, всегда только суммируются. Мы можем скрывать или маскировать файлы на предыдущих слоях, но удалить их невозможно. На практике это означает, что мы не можем уменьшить образ, удалив файлы, созданные на предыдущих шагах. lSl Если включить на сервере Docker экспериментальные функции, можно вместить в один слой несколько слоев с помощью команды docker irnage build --squash. При этом все файлы, удаленные на промежуточных слоях, не войдут в итоговый образ и не будут занимать лишнее место. Зато системе придется загружать весь слой целиком, даже если обновлена всего одна строка кода, так что у этого подхода есть свои ощутимые недостатки. Самый простой способ объяснить суммирование слоев - на практических приме­ рах. В новом каталоге загрузите (https://github.com/Ыuewhalebook/docker-up-and­ running-3rd-edition/ЫoЬ/main/chapter_04/additive) или создайте следующий файл, который сформирует образ, запускающий веб-сервер Apache на Fedora Linux: FRCМ docker.io/fedora RUN dnf install -у httpd СИ) ["/usr/sЬin/httpd", "-DFOREGROUND"] Выполните сборку: $ docker irnage build [+] Building 63.5s (6/6) FINISHED => [internal] load build definition from Dockerfile => => transferring dockerfile: 130В => [internal] load .dockerignore => => transferring context: 2В => [internal] load metadata for docker.io/library/fedora:latest => (1/2) FROМ docker.io/library/fedora Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 1 93 => [2/2) RUN dnf install -у httpd => exporting to image => => exporting layers => => writing image sha256:543dбlc956778b8ea3b32fle09a9354a864467772eб... Давайте пометим тегом получившийся образ, чтобы ссылаться на него в после­ дующих командах: $ docker image tag sha256:543dбlc956778b8ea3b32fle09a9354a864467772e6... sizel Теперь посмотрим на наш образ с помощью команды docker image history. Эта команда позволит больше узнать о слоях файловой системы и шагах сборки нашего образа: $ docker image history sizel IМAGE CREATED AЬout а minute ago 543dбlc95677 <missing> AЬout а minute ago 6 weeks ago <missing> <missing> 6 weeks ago <missing> 3 months ago 15 months ago <missing> CREATED ВУ CMD ["/usr/sЫn/httpd" "-DFOREGROU... "] RUN /Ыn/sh -с dnf install -у httpd /Ыn/sh -с #(nop) CMD ["/Ьin/bash"] ... /Ыn/sh -с #(nop) ADD file:58865512с... /Ьin/sh -с #(nop) ENV DISTTAG=f36co... /Ыn/sh -с #(nop) LAВEL maintainer=... SIZE ... ов 273МВ ов lбЗМВ ов ов Как видите, три слоя не повлияли на размер итогового образа, а другие два - уве­ личили его значительно. Слой размером 163 МБ можно легко объяснить - это базовый образ Fedora, который включает минимальный дистрибутив Linux. А что насчет слоя размером 273 МБ? Веб-сервер Apache не должен быть таким большим. Если вы работали с диспетчерами пакетов, например apk, apt, dnf или yum, то знае­ те, что почти все они используют большой кеш, который содержит информацию обо всех пакетах, доступных для установки на той или иной платформе. Кеш зани­ мает много места и не приносит никакой пользы после установки нужных пакетов. Разумеется, на следующем этапе мы хотим удалить этот кеш. В системах Fedora для этого достаточно отредактировать Dockerfile: FRCМ docker.io/fedora RlJN dnf install -у httpd RlJN dnf clean all СМ) ["/usr/sЬin/httpd", "-DFOREGROUND"] Затем мы соберем, пометим тегами и изучим получившийся образ: $ docker image build . [+] Building 0.5s (7/7) FINISHED => => writing image sha256:bбbf99cбe7a69a1229ef63fc086836ada20265a793cb8f2d... $ docker image tag sha256:bбbf99cбe7a69al229ef63fc086836ada20265a793cb8f2dl7... IМAGE CREATED SIZE ... CREATED ВУ Ь6Ьf99сбе7аб AЬout а minute ago CMD ["/usr/sЬin/httpd" "-DFOREGROU..."] ов <missing> AЬout а minute ago RUN /Ьin/sh -с dnf clean all # build... 71.ВkВ <missing> 10 minutes ago RUN /Ьin/sh -с dnf install -у httpd ... 273МВ <missing> 6 weeks ago /Ыn/sh -с #(nop) CMD ["/Ьin/bash"] ... ов <missing> 6 weeks ago /Ыn/sh -с #(nop) ADD file:58865512c... lбЗМВ Книги для программистов: https://clcks.ru/books-for-it 94 1 Глава 4 <rnissing> <rnissing> 3 months ago 15 months ago ов ов /Ьin/sh -с. #(nop) ENV DISTTAG=fЗбco... /Ьin/sh -с #(nop) LAВEL maintainer=.. . В ВЫХОДНЫХ данных команды docker image history МЫ увtщим, ЧТО создали · НОВЫЙ слой, добавивший в образ 71,8 КБ, но при э.том проблемный слой нисколько не уменьшился. Что же произошло? Важно понимать, .-что слои образа всегда_ суммируются. Мы не J\.fожем ничего уда­ лить ИЗ уже СОЗДаННОГО СЛОЯ. Д� если На следующих: СЛОЯХ МЫ Уда.JJИМ файлы, предыдущие слои не станут меньше. Когда мы удащiем или редактируем файлы на следующих слоях, мы просто маскируем более старые, версии. Получается, что мы можем уменьшить слой, только если удалим файлы до сохранения. Самый простой способ это сделать - написать J<Омандьi на одной строке в Docker­ file с помощью оператора &&. Этот оператор высl)'Пае.т как AND и выполняет сле­ дующую �омандУ, только если предыдущая выполнена успешно. Кроме того, с помощью оператора \ можно указать, что команда продолжается после перехода на новую строку. Это упростит чтение длинных команд. Итак, мы можем переписать Dockerfile:' • FIOf docker.io/fedora RUN dnf install �У httpd && \ dnf clean all СИ) ["/usr/sЬin/httpd", "-DFOREGROUND"] 1 . . ' . ' Давайте пересоберем образ и посмотрим, как это изменение повлияло на размер слоя, который вкmочает �емон http: $ docker image build . [+] Building 0.5s (7/7) FINISHED => => writing image sha256:14fe7924bb0b64lddflle08d3dd56f40aff427lcad7a42lfe..• $ docker image tag sha256:14fe7924bb0b64lddflle08d3dd56f40aff427lcad7a42lfe9b... IМAGE CREATED CREATED ВУ ( 14fe7924bb0b AЬout а minute ago CMD ["/usr/sЬin/httpd" "-DFOREGROUN."] ... RUN /Ьin/sh -с drif inst_all --у httpd &••• <rnissing> AЬout а minute ago 6 weeks ago <rnissing> /Ьin/sh -с #(nop) CMD ["/Ьin/bash"] ... 6 weeks ago <rnissing> /Ьin/sh -с #(nop) ADD ·file:58865512са...· <rnissing> 3 months·ago /Ьin/sh -с #(nop) ENV DISTTAG=f36con... <rnissing> 15 months ago /Ьin/sh -с #(nop) LAВEL maintainer=C .. ; SIZE ов 44.ВМВ ов iбЗМВ ов ов В первых двух примерах размер. слоя составлял 273 МБ, но мы удалили лишние файлы и уменьшили его до 44,8 МБ. Это позволит существенно сэкономить трафик, когда много серверов будут извлекать образ во время развертывания. Использование кеша слоев Последний метод сборки, который мы здесь рассмотрим, связан с ускорением сборки. Философия DevOps подразумевает максимально короткий циI<Л обратной Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 1 95 связи. Чем раньше мы найдем проблему, тем быстрее приступим к ее исправлению, пока мы еще помним этот код и не перешли к совершенно не связанным с ним задачам. В стандартном процессе сборки Docker использует кеширование слоев, чтобы не пересобирать слои образа, которые уже были собраны и не содержат заметных из­ менений. Благодаря этому кешу порядок выполнения операций в Dockerfile заметно влияет на время выполнения сборки. Для начала возьмем Dockerfile из предыдущего примера и немного его изменим. �l � Эти файлы, как и остальные, вы найдете на GitHub (https://github.com/ Ыuewhalebook/docker-up-and-running-Зrd-edition/ЫoЬ/main/chapter_04/cache). FRCМ docker.io/fedora RUN dnf install -у httpd && \ dnf clean all RUN mkdir -р /var/www && \ mkdir -р /var/www/html ADD index.html /var/www/html СМ) ["/usr/sЬin/httpd", "-DFOREGROUND"] Теперь в том же каталоге создадим файл index.html: <html> <head> <title>My custom Web Site</title> </head> <Ьоdу> <p>Welcome to my custom Web Site</p> </Ьоdу> </html> В первом тесте измерим время сборки без использования кеша Docker: $ time docker image build --no-cache time docker image build --no-cache [+] Building 48.Зs (9/9) FINISHED => [internal] load build definition from Dockerfile => => transferring dockerfile: 238В => [internal] load .dockerignore => => transferring context: 2В => [internal] load metadata for docker.io/library/fedora:latest => CACHED [1/4] FROM docker.io/library/fedora => [internal] load build context => => transferring context: 32В => [2/4] RUN dnf install -у httpd && dnf clean all => [3/4] RUN mkdir -р /var/www && mkdir -р /var/www/html => [4/4] ADD index.html /var/www/html Книги для программистов: https://clcks.ru/books-for-it 96 Глава 4 => exporting to image => => exporting layers => => writing image sha256:7f94d0d6492f2d2c0b8576f0f492e03334eбa535cac85576c... real lm21.645s user Om0.428s sys Om0.323s Пользователи Windows могут выполнить эту команду в сеансе WSL2 или исполь­ зовать функцию PowerShell Measure-Command6, чтобы заменить команду Unix time в этих примерах. В выводе команды time мы видим, что сборка без кеша заняла 1 мин 21 с, и мы из­ влекли из кеша только базовый образ. Если сразу пересобрать образ и на этот раз задействовать кеш, то получится гораздо быстрее: $ time docker image build . [+] Building 0.1s (9/9) FINISHED => [internal] load build definition from Dockerfile => => transferring dockerfile: 37В => [internal] load .dockerignore => => transferring context: 2В => [internal] load metadata for docker.io/library/fedora:latest => [1/4] FROМ docker.io/library/fedora => [internal] load build context => => transferring context: 32В => CAC1�ED [2/4] RUN dnf install -у httpd && dnf clean all => CACHED [3/4] RUN mkdir -р /var/www && mkdir -р /var/www/html => CACHED [4/4] ADD index.html /var/www/html => exporting to image => => exporting layers => => writing image sha256:0d3aeeeeebd09606d99719e0c5197clf3e59a843c4d7a2laf... real Om0.416s user Om0.120s sys Om0.087s Поскольку ни один из слоев не изменился и все четыре этапа можно взять из кеша, понадобилось меньше секунды. Давайте внесем небольшое изменение в файл index.html: <html> <head> <title>My custom Web Site</title> </head> 6 Полный URL: bttps://learn.microsoft.com/en-us/powersbell/module/microsoft.powershell.utility/ measure-command? view=powershell-7.3. Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 1 97 <Ьоdу> <div align= "center"> <p>Welcome to ту custom Web Site! ! !</р> </div> </Ьоdу> </html> Снова пересоберем образ: $ time docker image build . [+] Building 0.1s (9/9) FINISHED => [internal] load build definition from Dockerfile => => transferring dockerfile: 37В => [internal] load .dockerignore => => transferring context: 2В => [internal] load metadata for docker.io/library/fedora:latest => [internal] load build context => => transferring context: 214В => [1/4] FROМ docker.io/library/fedora dnf clean all => CACHED [2/4] RUN dnf install -у httpd && mkdir -р /var/www/html => CACHED [3/4] RUN mkdir -р /var/www && => [4/4] ADD index.html /var/www/html => ADD index.html /var/www/html => exporting to image => => exporting layers => => writing image sha256:daf792dalbбa0ae7cfЬ2673b29f98ef2123d666b8dl4e0Ь74 ... real 0m0.456s user 0m0.120s sys 0m0.068s В выводе видно, что для большей части сборки использовался кеш. Только на ша­ ге 4/4 Docker извлек index.html, для которого копия в кеше бьша отменена, и слои нужно было создавать заново. Поскольку большую часть сборки мы извлекли из кеша, сборка снова заняла не больше секунды. Давайте посмотрим, что будет, если мы изменим порядок команд в Dockerfile: FRCМ docker.io/fedora RUN mkdir -р /var/www && \ mkdir -р /var/www/html ADD index.html /var/www/html RUN dnf install -у httpd && \ dnf clean all СМ) ["/usr/sЬin/httpd", "-DFOREGROUND"] Измерим время этой сборки без кеша: $ time docker image build --no-cache [+] Building 51.5s (9/9) FINISHED => => writing image sha256:lcc5f2c5e4a4dlcf384fбfb3a34fd4d00e7f5e7a7308d5flf... Книги для программистов: https://clcks.ru/books-for-it 98 Глава 4 real Om51.859s user Om0.237s Om0.159s sys На этот раз сборка заняла 51 с: указав аргумент --no-cache, мы знаем, что кроме базового образа мы ничего не загружали из кеша. Разница во времени по сравне­ нию с первым тестом связана с изменениями скорости сети, а не с изменениями в Dockerfile. Давайте снова внесем изменения в файл index.html: <hЬal> <head> <title>My custom Web Site</title> </head> <Ьоdу> <div align="center" style="font-size:180%"> <p>Welcome to my custom Web Site</p> </div> </Ьоdу> </hЬal> На этот раз пересоберем образ, используя кеш: $ time docker image build . [+] Building 43.4s (9/9) FINISHED => [internal] load build definition from Dockerfile => => transferring dockerfile: 37В => [internal] load .dockerignore => => transferring context: 2В => [internal] load metadata for docker.io/library/fedora:latest => (1/4] FROМ docker.io/library/fedora => [internal] load build context => => transferring context: 233В mkdir -р /var/www/html => CACHED (2/4] RUN mkdir -р /var/www && = > [3/4] ADD index.html /var/www/html => (4/4] RUN dnf install -у httpd && dnf clean all = > exporting to image => => exporting layers => => writing image sha256:9a05b2d0lb5870649e0adld7ad68858e0667f402c8087f0b4... real Om43.695s user OmO.211s sys Om0.133s Когда мы в первый раз собирали образ снова после изменения файла index.html, это заняло всего 0,456 с, а сейчас понадобилось 43,695 с - почти столько же, сколько ушло на сборку всего образа без кеша. Это связано с тем, как мы изменили Dockerfile, - теперь файл index.html копирует­ ся в начале процесса. Проблема этого подхода связана с тем, что файл index.html Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 1 99 часто меняется и отменяет содержимое в кеше. Еще один минус- файл почему-то помещен перед очень долгим шагом в Dockerfile: установкой веб-сервера Apache. Как видите, порядок операций в Dockerfile очень важен, поэтому старайтесь распо­ лагать их таким образом, чтобы самые стабильные и долгие части сборки выполня­ лись в первую очередь, а код добавляйте как можно позже. Если в проекте необходимо устанавливать зависимости на основе кода с помощью таких инструментов, как npm и bundle, тщательно изучите дополнительную информа­ цию, чтобы оптимизировать сборки Docker для этих платформ. Для этого часто необходимо фиксировать версии зависимостей и хранить их вместе с кодом, чтобы не загружать при каждой сборке. Кеширование каталога BuildКit расширяет наши возможности при сборке образов, и одна из удобных функций- кеширование каталога. Этот метод ускоряет сборку, не заставляя нас сохранять в образе лишние файлы для среды выполнения. По сути, мы можем со­ хранить содержимое каталога в образе на специальном слое, который можно мон­ тировать методом Ьind mount во время сборки, а затем удалять до создания момен­ тального снимка образа. Этот метод часто используется для работы с каталогами, в которых установщики Linux (apt, apk, dnf и т. д.) и менеджеры зависимостей для разных языков (npm, bundler, pip и т. д.) загружают свои базы данных и файлы архи­ вов. Если вы не знаете, как работает Ьind mount, изучите этот раздел в документации Docker (https://docs.docker.com/storage/blnd-mounts). Чтобы реализовать кеширование каталога, потребуется включить BuildKit. Обычно он уже включен, но мы можем включить его принудительно со стороны клиента, задав для переменной среды DOCKER_вur1oкrт значение 1: $ export DOCKER_BUILDKIT=l Давайте извлечем следующий репозиторий Git и посмотрим, как кеширование каталога повышает эффективность последовательных сборок и позволяет контро­ лировать размер образов: $ git clone htt ps://githuЬ .com/spkane/open-mastermind .git \ --config core.autocrlf=input $ cd open-mastermind $ cat Dockerfile � python:3.9.15-slim-bullseye RIJN mkdir /арр WORКDIR /арр СОРУ . /арр RIJN pip install -r requirements.txt 'IIORI<DIR /app/mastermind Q,I) [ " python", "mastermind.p y"] Книги для программистов: https://clcks.ru/books-for-it 100 Глава 4 В этом коде мы отправляем в репозиторий обычный Dockerfile. Давайте посмот­ рим, сколько времени уйдет на сборку этого образа с кешем и без кеша и каким в итоге получится образ: $ tirne docker build --no-cache -t docker.io/spkane/open-mastermind:latest [+] Building 67.5s (12/12) FINISHED => => real user sys naming to docker.io/spkane/open-mastermind:latest 0m28.934s 0m0.222s 0m0.248s 0.0s $ docker image 1s --format "{{ .Size }}" spkane/open-mastermind:latest 293МВ $ tirne docker build -t docker.io/spkane/open-mastermind:latest [+] Building 1.5s (12/12} FINISHED => => naming to docker.io/spkane/open-mastermind:latest 0ml.083s real 0m0.098s user 0m0.095s sys 0.0s В выводе мы видим, что сборка образа без кеша слоев занимает меньше 29 с, а с кешем - меньше 2 с. Итоговый образ имеет размер 293 МБ. BuildKit позволяет изменить или полностью отключить цвета для выходных дан­ ных (https:1/github.com/moby/buildkit#color-output-controls). Это особенно удоб­ но при работе в терминале с темным фоном. Цвета можно настроить с помощью команды (например, export BUILDKIT_COLORS=run=green:warning=yellow:error=red: cancel=cyan) или отключить: expcrt NO_COLOR=true. Версии BuildKit в различных компонентах docker и сторонних инструментах обнов­ ляются, так что этот подход может работать не всегда. Для тестирования сборки можно выполнить команду: $ docker container run -ti --rm docker.io/spkane/open-mastermind:latest Запустится опенсорс-версия игры Masteпnind в терминале (https://github.com/ philshem/open-mastermind). На экране появятся инструкции к игре, при необходи­ мости из нее можно выйти, нажав клавиши <Ctrl>+<C>. Если это приложение Python, оно указывает все нужные библиотеки в файле requirements.txt, а затем для установки зависимостей используется приложение pip в Dockerfile. Мы устанавливаем необязательные зависимости, чтобы продемонстрировать пре­ � имущества кеширования каталога. � . . Оrкроем файл requirements.txt и добавим строку log- symЬols: colorama # Необязательно - используется дпя демонстрации pandas flask log-symЬols Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 1 101 Выполним сборку еще раз: $ time docker build -t docker.io/spkane/open-mastermind:latest \ --progress=plain . 11 [internal] load build definition from Dockerfile 19 [5/6] RUN pip install -r requirements.txt 19 sha256:82dЬcl0flbb9fa476d93cc0d8104b76f46afBece7991eb55393d6d72a230919e 19 1.954 Collecting colorama 19 2.058 Downloading colorama-0.4.5-py2.py3-none-any.whl (16 kВ) real user sys Om16.379s Om0.112s Om0.082s В выводе для шага 5/6 видно, что все зависимости загружаются снова, хотя pip кеширует большую часть из них в /root/.cache. builder видит, что мы внесли измене­ ние, влияющее на слой, и воссоздает весь слой заново, поэтому мы теряем кеш, который был сохранен для этого слоя образа. Давайте исправим ситуацию. Для этого используем кеш каталога BuildКit (https://github.com/moby/Ьuildkit/ЫoЬ/master/frontend/dockerflle/docs/reference.m d#run--mounttypecache), но сначала внесем несколько изменений в Dockerfile: # syntax=docker/dockerfile:1 FRCМ python:3.9.15-slim-bullseye RlJN mkdir /арр 'WORКDIR / арр СОРУ . /арр RlJN --mount=type=cache,target=/root/.cache pip install -r requirements.txt 'WORКDIR /app/mastermind (1,f) ["python", "mastermind.py"] Здесь мы видим два важных изменения. Во-первых, мы добавили следующую строку: # syntax=docker/dockerfile:1 Таким образом мы указываем Docker, что будем использовать более новую версию фронтенда Dockerfile (https://hub.docker.com/r/docker/dockerfile), которая предос­ тавляет доступ к новым возможностям BuildКit. Затем мы отредактировали строку RUN: RUN --mount=type=cache,target=/root/.cache pip install \ -r requirements.txt В результате BuildKit монтирует слой кеширования в контейнер по пути /root/.cache для этого этапа сборки. Эго позволит нам достичь двух целей: содержимое каталога не войдет в итоговый образ, но он будет подключен к pip и доступен в последую­ щих сборках. Книги для программистов: https://clcks.ru/books-for-it 102 Глава 4 Давайте полностью пересоберем образ с учетом этих изменений и создадим начальное содержимое кешируемого каталога. В выводе мы увидим, что pip загру­ жает все зависимости, как и раньше: $ time docker build --no-cache -t docker.io/spkane/open-rnastermind:latest [+] Building 15.2s (15/15) FINISHED => => naming to docker.io/spkane/open-rnastermind:latest real user sys 0.0s 0ml5.493s 0m0.137s 0m0.096s Откроем файл requirements.txt и добавим строку py-events: colorarna # Необязательно - используется для демонстрации pandas flask log-symЬols py-events Здесь мы заметим результат внесеннь1х изменений. Если мы пересоберем образ сейчас, то увидим, что загружены только py-events и его зависимости. Все остальное использует существующий кеш из предыдущей сборки, который примонтирован к образу для этого шага сборки: $ time docker build -t docker.io/spkane/open-rnastermind:latest \ --progress=plain . #1 [internal] load build definition from Dockerfile #14 [stage-0 5/6] RUN --mount=type=cache,target=/root/.cache pip install #14 sha256:9bc7244lfdf2ec5f5803d4d5df43dЬe7bc6eeef88ebee98edl8d8dЬb478270ba #14 1.711 Collecting colorarna Using cached colorarna-0.4.5-py2.py3-none-any.whl (16 kB) #14 1.714 #14 2.236 Collecting py-events #14 2.356 Downloading py_events-0.l.2-py3-none-any.whl (5.8 kВ) #16 DONE 1.4s real user sys 0ml2.624s 0m0.180s 0m0.112s $ docker irnage 1s --forrnat "{{ .Size }}" spkane/open-rnastermind:latest 261МВ Время сборки сократилось, потому что мы не загружаем все заново, и образ стал на 32 МБ меньше, хотя мы добавили новые зависимости. Это связано с тем, что ката­ лог кеша больше не хранится напрямую в образе, содержащем приложение. Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 1 103 BuildKit и новые инструменты для работы с Dockerfile дают больше возможностей при сборке образов. Мы советуем прочесть руководство https://github.com/moby/ buildkit/ЫoЬ/master/frontend/dockerfile/docs/reference.md и изучить все доступ­ ные функции. Решение проблем со сборками В идеале проблем возникать не должно, особенно если мы сами написали сценарий сборки, но в реальном мире что угодно может пойти не так. Давайте выясним, как решать проблемы со сборками в Docker. В этом разделе мы рассмотрим два вариан­ та: в одном мы соберем образ так, как это делали до появления BuildKit, а в другом воспользуемся BuildКit. В этом примере мы опять обратимся к репозиторию docker-hello-node. При необхо­ димости склонируйте его снова: $ git clone https://githuЬ.com/spkane/docker-node-hello.git \ --config core.autocrlf=input Cloning into 'docker-node-hello'... remote: Counting objects: 41, done. remote: Total 41 (delta 0), reused О (delta О), pack-reused 41 Unpacking objects: 100% (41/41), done. $ cd docker-node-hello Отладка образов без BuildKit Для следующих упражнений нам нужна проблемная сборка. Давайте создадим ее. Для этого внесем изменения в Dockerfile в строку: RUN apt-get -у update Теперь она будет выглядеть так: RUN apt-get -у update-all _ Если вы используете PowerShell в Windows, скорее всего, потребуется задать переменную среды, которая отключает BuildКit, прежде чем выполнять команду docker image build, а затем сбросить изменения: PS С:\> $env:DOCKER_BUILDKIT = О PS С:\> docker image build' -t example/docker-node-hello:latest --no-cache . PS С:\> $env:DOCKER_BUILDKIT = 1 Если мы попробуем собрать образ сейчас, то увидим следующую ошибку: $ DOCKER_BUILDKIT=O docker image build -t example/docker-node-hello:latest \ --no-cache . Sending build context to Docker daemon 9.21бkВ Step 1/14 ---> 9ff38e3aбd9d FROМ docker.io/node:18.13.0 Книги для программистов: https://clcks.ru/books-for-it 104 Глава 4 Step 6/14 : ENV SCPATH /etc/supervisor/conf.d ---> Running in е903367еаеЬ8 Removing intermediate container е903367еаеЬ8 ---> 2a236efc3f06 Step 7/14 : RUN apt-get -у update-all ---> Running in c7cd72f7d9bf Е: Invalid operation update-all The coпrnand '/bin/sh -с apt-get -у update-all' returned а non-zero code: 100 Как устранить проблему, особенно если мы работаем не в Linux? Помните, что почти все образы Docker являются слоями поверх других образов Docker, и мы мо­ жем запустить контейнер на основе любого из них. Поначалу смысл не очевиден, но давайте посмотрим на вывод на шаге 6: Step 6/14 : ENV SCPATH /etc/supervisor/conf.d ---> Running in е903367еаеЬ8 Removing intermediate container е903367еаеЬ8 ---> 2a236efc3f06 В первой строке Running in е903367еаеЬ8 указано, что процесс сборки запустил новый контейнер на основе образа, созданного на шаге 5. В следующей строке - Removing intermediate container е903367еаеЬ8 - мы видим, что Docker удаляет контейнер после изменения на основе инструкции на шаге 6. На этом этапе добавилась перемен­ ная среды по умолчанию: ENV SCPATH /etc/supervisor/conf.d. Последняя строка (---> 2a236efc3f06) - самая важная, потому что мы получаем идентификатор образа, созданного на шаге б. Он понадобится для устранения неполадок со сборкой, пото­ му что это образ с последнего успешного этапа в сборке. Имея эту информацию, мы сможем запустить интерактивный контейнер, чтобы вы­ яснить, почему сборка не работает. Каждый образ контейнера основан на преды­ дущих слоях. Одно из главных преимуществ такого подхода заключается в том, что мы можем запустить нижний слой как отдельный контейнер и осмотреться там с помощью командной оболочки: $ docker container run --rm -ti 2a236efc3f06 /bin/bash root@Ь83048106ЬOf:/1 Внутри контейнера можно вьmолнять любые команды, чтобы определить причину проблемы в Dockerfile и понять, как исправить ошибку: root@Ь83048106ЬOf:/I apt-get -у update-all Е: Invalid operation update-all root@Ь83048106ЬOf:/I apt-get --help apt 1.4.9 (amd64) Most used commands: update - Retrieve new lists of packages root@Ь83048106ЬOf:/1 apt-get -у update Get:1 http://security.debian.org/debian-security stretch/updates ... [53.0 kB] Reading package lists... Done root@Ь83048106ЬOf:/1 exit exit Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 105 Раз мы нашли причину, то можем исправить Dockerfile, заменив строку RUN apt-get -у update-all на RUN apt-get -у update. После пересборки образа все должно завер­ шиться успешно: $ DOCКER_BUILDKIT=0 docker image build -t exarnple/docker-node-hello:latest Sending build context to Docker daemon 15.87kВ Successfully built 69f5e83bb86e Successfully tagged exarnple/docker-node-hello:latest Отладка образов с BuildKit При использовании BuildKit мы изменим подход к поискам причины сбоя, потому что промежуточные слои сборки не экспортируются из контейнера в демон Docker. Возможности отладки с BuildKit практически наверняка будут развиваться, а пока мы рассмотрим подход, который работает уже сейчас. Давайте вернем Dockerfile в исходное состояние и изменим строку RlJN npm install на: RlJN npm installer Попробуем собрать этот образ. ; Не забудьте включ,ть вu;ldKt. $ docker image build -t exarnple/docker-node-hello:debug --no-cache [+] Building 51.7s (13/13) FINISHED 0.0s => [internal] load build definition from Dockerfile => [7/8] WORКDIR /data/app => ERROR [8/8] RUN npm installer 0.0s 0.4s > [8/8] RUN npm installer: #13 0.399 #13 0.399 Usage: npm <command> #13 0 .402 Did you mean one of these? #13 0.402 install #13 0.402 install-test #13 0.402 uninstall executor failed running [/bin/sh -с npm installer]: exit code: 1 Как и ожидалось, мы видим ошибку. Как получить доступ к слою, чтобы устранить проблему? Книги для программистов: https://clcks.ru/books-for-it 106 Глава 4 Один из способов - использовать многоэтапные сборки и аргумент --target в docker image build. Давайте внесем два изменения в Dockerfile. Изменим строку: FRCМ docker.io/node:18.13.0 на: FRCМ docker.io/node:18.13.0 as deploy И прямо перед проблемной строкой добавим новую строку FRoм: FRCМ deploy RIJN npm installer Таким образом мы создаем многоэтапную сборку: на первом этапе находятся все шаги, которые точно работают, а второй этап начинается с проблемного шага. Если мы попробуем пересобрать образ с помощью той же команды, ничего не по­ лучится: $ docker image build -t example/docker-node-hello:debug [+] Building 51.7s (13/13) FINISHED executor failed running [/bin/sh -с npm installer]: exit code: 1 Вместо этого укажем Docker, что хотим собрать только первый образ в нашем мно­ гоэтапном Dockerfile: $ docker image build -t example/docker-node-hello:debug --target deploy . [+] Building 0.8s (12/12) FINISHED => [internal] load build definition from Dockerfile => => transferring dockerfile: 37В 0.0s 0.0s 0.1s 0.1s => => writing image sha256:a42dfЬcfc7Ь18ee3d30ace944ad4134ea2239a2cO 0.0s 0.0s => => naming to docker.io/example/docker-node-hello:debug => exporting to image => => exporting layers Теперь можно создать из этого образа контейнер и выполнить тесты: $ docker container run --rm -ti docker.io/example/docker-node-hello:debug \ /Ьin/bash root@l780799717бe:/data/appt 1s index.js package.json root@l780799717бe:/data/appt npm install added 18 packages from 16 contributors and audited 18 packages in l.248s root@l780799717бe:/data/appt exit exit Найдя ошибку в Dockerfile, отменим изменения и исправим строку npm, чтобы все работало правильно. Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 107 Мультиархитектурные сборки С момента запуска Docker архитектура АМD64/х86_64 была и остается основной платформой, для которой развертывалось большинство контейнеров. Сейчас мно­ гое изменилось. Все больше разработчиков используют системы на основе АRМ64/AArch64, а облачные компании начинают предоставлять виртуальные машины на базе АRМ, потому что эти платформы обеспечивают более дешевые вычисления. В результате нам приходится собирать и сопровождать образы, предназначенные для нескольких архитектур. Как обслуживать единую оптимизированную кодовую базу и конвейер сборки и при этом поддерживать различные целевые платформы? К счастью, Docker выпустил плагин для Docker CLI - buildx, который помогает упростить этот процесс. Обычно docker-buildx уже установлен в системе. Мы можем это проверить: $ docker buildx version githuЬ.com/docker/buildx v0 .9.1 ed00243a0ce2a0aee75311b06e32d33b44729689 Если нужно установить плагин, следуйте инструкциям из репозитория GitHub � (https://github.com/docker/buildx#installing). - По умолчанию docker-buildx использует виртуализацию на базе QEМU (https:// www.qemu.org/) и Ьinfint_misc (https://docs.kernel.org/admin-guide/Ьinfmt-misc.html) для поддержки архитектур, которые отличаются от базовой системы. Скорее всего, в Linux уже есть все необходимое, но на всякий случай выполним следующую команду при создании сервера Docker, чтобы убедиться, что файлы QEМU зареги­ стрированы корректно и актуальны: $ docker container run --rm --privileged multiarch/qemu-user-static \ --reset -р yes Setting /usr/bin/qemu-alpha-static as binfmt interpreter for alpha Setting /usr/bin/ qemu-arm-static as binfmt interpreter for arm Setting /usr/bin/qemu-armeb-static as binfmt interpreter for armeb Setting /usr/bin/qemu-aarch64-static as binfmt interpreter for aarch64 Setting /usr/bin/qemu-aarch64_be-static as binfmt interpreter for aarch64_be В отличие от встроенного функционала сборок Docker, который выполняется напрямую на сервере, BuildКit использует при сборке образов контейнер сборки, который предоставляет дополнительную гибкость. Далее мы создадим контейнер buildx по умолчанию с именем builder. � ._ Если у вас уже есть контейнер buildx с этим именем, удалите его командой docker buildx rm builder или измените имя в последующей команде docker buildx create. Книги для программистов: https://clcks.ru/books-for-it 108 Глава 4 Следующие две команды создают контейнер сборки, задают его по умолчанию, и запускают: $ docker buildx create --narne builder --driver docker-container --use builder $ docker buildx inspect --bootstrap [+] Building 9.6s (1/1) FINISHED => [internal] booting buildkit 9.6s => => pulling image moby/buildkit:buildx-staЬle-1 8.6s => => creating container buildx_buildkit_builder0 0.9s Narne: builder Driver: docker-container Nodes: builder0 Narne: Endpoint: unix:///var/run/docker.sock running Status: Buildkit: v0.10.5 Platforms: linux/amd64, linux/amd64/v2, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/vб В этом примере давайте загрузим репозиторий Git wordchain, который содержит полезный инструмент для генерации случайных: и детерминированных: последова­ тельностей слов для динамического именования: $ git clone https://githuЬ.com/spkane/wordchain.git \ --config core.autocrlf=input $ cd wordchain Если мы внимательнее посмотрим на Dockerfile, то увидим вполне обычный много­ этапный Dockerfile, в котором нет ничего, что связано с архитектурой платформы: FRQf golang:1.18-alpineЗ.15 AS build RIJN apk --no-cache add \ bash \ gcc \ musl-dev \ openssl � CGO ENAВLED=0 СОРУ . /build 1ЮRIO>IR /build RIJN go install githuЬ.com/markЬates/pkger/cmd/pkger@latest && \ pkger -include /data/words.json && \ go build . FRQf alpine:3.15 AS deploy 1ЮRIO>IR / СОРУ --from=build /build/wordchain / USER 500 EXPOSE 8080 Dl'l'RYIOINТ ["/wordchain"] а,ю ["listen"] Книги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 1 109 На первом шаге мы собираем статически компилируемый двоичный файл Go, а на втором - упаковываем его в небольшой образ развертывания. Инструкция ENTRYPOINT в Dockerfile позволяет отделить процесс по умолчанию, вы­ полняемый контейнером, (ENTRYPOINT) от аргументов командной строки, которые передаются этому процессу (смо). Если ENTRYPOINT отсутствует в Dockerfile, инст­ � рукция смо должна содержать и процесс, и все необходимые аргументы командной строки. Давайте соберем этот образ и загрузим его в локальный сервер Docker с помощью следующей команды: $ docker buildx build --tag wordchain:test --load [+] Building 2.4s (16/16) FINISHED => [internal] load .dockerignore => => transferring context: 93В => [internal] load build definition from Dockerfile => => transferring dockerfile: 461В 0.0s 0.0s 0.0s 0.0s 0.3s => exporting to oci image format 0.0s => => exporting layers => => exporting manifest sha256:4bdl971f2ed820b4f64ffda97707c27aac3e8eb7 0.0s => => exporting config sha256:ce8f8564bf53b283d486bddeb8cbb074ff9a9d4ce9 0.0s 0.2s => => sending tarball 0.0s => importing to docker Протестируем образ с помощью следующих команд: $ docker container run wordchain:test random witty-stack $ docker container run wordchain:test random -1 3 -d . odd.goo $ docker container run wordchain:test --help wordchain is an application that can generate а readaЫe chain of customizaЫe words for naming things like containers, clusters, and other objects. Если вы получили случайные пары слов после выполнения первых двух команд, все работает правильно. Чтобы собрать этот образ для нескольких архитектур, нужно просто добавить к сборке аргумент --platform. В обычной ситуации мы также заменили бы --load на --push, чтобы все получив­ шиеся образы были отправлены в указанный репозиторий, но в данном случае мы просто удалим --load, потому что сервер Docker не может загружать образы для нескольких платформ одновременно и у нас нет репозитория, куда можно было бы Книги для программистов: https://clcks.ru/books-for-it 110 Глава 4 отправлять эти образы. Если бы репозиторий был и мы корректно пометили обра­ зы, то могли бы легко собрать и отправить итоговые образы за один шаг примерно такой командой: docker buildx build --platfoпn linux/amd64,linux/aпn64 --tag docker.io/spkane/ wordchain:latest --push. Этот образ можно собрать для платформ linux/amd64 и linux/arm64: $ docker buildx build --platfoпn linux/amd64,linux/aпn64 \ --tag wordchain:test . [+] Building 114.9s (23/23) FINISHED => [linux/апn64 internal] load metadata for docker.io/library/alpine:3.1 2.7s => [linux/amd64 internal] load metadata for docker.io/library/alpine:3.1 2.7s => [linux/aпn64 internal] load metadata for docker.io/library/golang:1.1 3.0s => [linux/amd64 internal] load metadata for docker.io/library/golang:1.1 2.8s => CACHED [linux/amd64 build 5/5] RUN go install githuЬ.com/markЬates/pk 0.0s => CACHED [linux/amd64 deploy 2/3] СОРУ --from=build /build/wordchain / 0.0s => [linux/aпn64 build 5/5) RUN go install githuЬ.com/markЬates/pkger/c 111.7s => [linux/aпn64 deploy 2/3] СОРУ --from=build /build/wordchain / 0.0s WARNING: No output specified with docker-container driver. Build result will oniy remain in the build cache. То push result image into registry use --push or to load image into docker use --load � При сборке образов для других архитектур требуется эмуляция, поэтому некото­ рые шаги выполняются дольше обычного. Это нормально, учитывая дополнитель­ ные издержки на эмуляцию. Мы можем настроить Docker таким образом, чтобы он собирал каждый образ на рабочем узле с соответствующей архитектурой, - во многих ситуациях это значи­ тельно ускорит процесс. Подробнее об этом читайте в этой статье из блога о Docker (https://www.docker.com/Ыog/speed-up-building-with-docker-buildx-and­ graviton2-ec2). В выводе сборки мы увидим строки, начинающиеся с => [linux/amd64 *J или => [linux/aпn64 *J. Каждая из этих строк представляет процесс сборки на этом шаге для соответствующей платформы. Многие из этих шагов будут выполняться парал­ лельно, и из-за кеширования и других факторов каждая сборка будет выполняться со своей скоростью. Поскольку мы не добавили в сборку --push, в конце мы увидим предупреждение. Дело в том, что драйвер docker-container, который используется в ходе сборки, про­ сто оставил все в кеше сборки - мы не можем запустить получившиеся образы и остается только надеяться, что все работает. Существует несколько аргументов build (https://docs.docker.com/engine/ reference/builder/#automatic-platform-args-in-the-global-scope), которые автома­ тически задаются Docker и могут использоваться в Dockerfile при сборках для разных архитектур. Например, TARGETARCH позволяет убедиться, что определенКниги для программистов: https://clcks.ru/books-for-it Работа с образами Docker 1 111 ный шаг сборки загружает корректный подготовленный двоичный файл для текущей платформы образа. Когда мы отправляем образ в репозиторий, как Docker узнает, какой образ следует использовать для локальной платформы? Эта информация указывается на сервере Docker с помощью манифеста образа. Мы можем изучить манифест для docker.io/spkane/workdchain, выполнив следующую команду: $ docker manifest inspect docker.io/spkane/wordchain:latest { "schemaVersion": 2, "media'l'ype": 1 application/vnd.docker. distribution.manifest.list.v2+json11 "manifests": [ 1 , "media'l'ype": application/vnd.docker.distribution.manifest.v2+json", 739, "diqest": "sha256: 4Ьdl ...ЬfсО", "platform": { "architecture": "amd64", linuxtt "0s 11 11 "size": : 11 ), "platform": "architecture": 11 arm6411 , "os": "linux" ), В выводе мы видим блоки, в которых определен образ, необходимый для каж­ дой поддерживаемой платформы. Для этого используются отдельные записи digest (https://github.com/opencontainers/image-spec/ЫoЬ/main/descriptor.md#digests), объединяемые с блоком platform. Этот файл манифеста загружается сервером, когда ему требуется образ. Сервер ищет в манифесте, какой образ требуется для локаль­ ной платформы, и загружает его. Поэтому наш Dockerfile вообще работает. Каждая строка FROМ соответствует базовому образу, а сервер Docker обращается к файлу ма­ нифеста, чтобы определить нужный образ для каждой платформы, для которой предназначена сборка. Заключение Мы подробно рассмотрели создание образов для Docker и теперь хорошо представ­ ляем себе, какие инструменты и возможности помогут нам оптимизировать конвей­ ер сборки. В следующей главе мы поговорим о том, как с помощью образов созда­ вать контейнеризированные процессы для проектов. Книги для программистов: https://clcks.ru/books-for-it ГЛАВА 5 Работа с контейнерами В предыдущей главе мы узнали, как создать образ Docker и как запустить из него контейнер. В этой главе мы сначала рассмотрим историю появления контейнеров, а затем перейдем к подробностям работы контейнеров и изучим команды Docker для управления конфигурацией, ресурсами и привилегиями контейнера. Что такое контейнер? Скорее всего, вы знакомы с системами виртуализации, вроде VMware или КУМ, с помощью которых мы можем запускать полноценное ядро Linux и операционную систему поверх слоя виртуализации - гипервизора. Такой подход обеспечивает хорошую изоляцию рабочих нагрузок, поскольку у каждой виртуальной машины есть свое ядро операционной системы, которое находится в отдельном пространст­ ве памяти поверх слоя аппаратной виртуализации. Контейнеры основаны на других механизмах, ведь они используют одно общее ядро, в котором и реализуется изоляция между рабочими нагрузками. Это называ­ ется виртуализацией операционной системы. В libcontainer READМE приводится емкое определение контейнера (https:// github.com/opencontainers/runc/ЬloЬ/main/libcontainer/READМE.md). Контей­ нер - это самодостаточная среда выполнения, которая использует ядро хост­ системы и может работать изолированно от других контейнеров в системе. Одно из главных преимуществ контейнеров - эффективность по ресурсам, потому что нам не требуется целый экземпляр операционной системы для каждой изолиро­ ванной рабочей нагрузки. Ядро общее, а значит, между задачей и оборудованием находится меньше слоев. Когда процесс выполняется внутри контейнера, нам нуж­ но совсем немного кода в ядре, чтобы управлять этим контейнером. Сравните этот подход с виртуальной машиной, где требуется дополнительный слой. В виртуаль­ ной машине вызовы от процесса к оборудованию или гипервизору требуют дважды переходить в привилегированный режим процессора, поэтому многие вызовы вы­ полняются значительно медленнее. Libcontainer - это библиотека Go, которая предоставляет стандартный интерфейс для управления контейнерами Linux из приложений (https://github.com/opencontainers/runc/tree/main/libcontainer). При использовании контейнеров мы можем выполнять только процессы, совмести­ мые с ядром. Например, в отличие от аппаратной виртуализации через VMware, Книги для программистов: https://clcks.ru/books-for-it Работа с контейнерами 1 113 КУМ и подобные технологии, приложения Windows не могут нативно вьmолняться в контейнере Linux на хаете Linux. Зато приложения Windows могут выполняться в контейнерах Windows на хаете Windows. Контейнер зависит от операционной системы, и мы можем запускать в нем только приложения или демоны, совмести­ мые с ядром сервера. При работе с контейнерами постарайтесь забыть все, что вы знали о виртуальных машинах. Представьте себе контейнер в виде оболочки вокруг обычного процесса, выполняющегося на сервере. Контейнеры можно выполнять на виртуальных машинах, а виртуальные машины можно легко запускать в контейнерах. При таком подходе мы можем выполнять приложение Windows на виртуальной машине Windows, запущенной внутри кон­ тейнера Linux. История контейнеров Нередко революционная технология на самом деле оказывается не такой уж и но­ вой - просто ее время наконец пришло. Технологии циклично возвращаются, и некоторые идеи из 1960-х годов сегодня снова в моде. Например, Docker появил­ ся недавно и сразу стал хитом благодаря своей простоте, но он возник не в вакууме. Многие механизмы, на которых работает Docker, развивались в течение 30 лет си­ лами специалистов в нескольких областях. Мы легко можем проследить концепту­ альную эволюцию контейнеров от простого системного вызова, добавленного в ядро Unix в конце 1970-х, до современных инструментов работы с контейнерами, применяемых такими технологическими гигантами, как Google, Twitter и Meta. Стоит отдельно изучить развитие технологий, которое привело к созданию Docker, - это поможет нам вписать его в контекст других платформ и инструмен­ тов, с которыми вы уже знакомы. Сама по себе идея контейнеров не нова, ведь это просто способ изолировать и ин­ капсулировать часть работающей системы. Самая старая технология в этой сфере включает первые системы пакетной обработки. В первых компьютерах система вы­ полняла по одной программе за раз и переходила к следующей программе, когда завершалась предыдущая или истекал заданный период времени. Такой подход подразумевал вынужденную изоляцию: программы не делили ресурсы, потому что всегда выполнялись по одной. Современные компьютеры тоже переключаются между задачами, но это происходит очень быстро и совершенно не заметно для большинства пользователей. Можно сказать, что семена современных контейнеров были посажены еще в 1979 году, когда в Unix версии 7 добавили системный вызов chroot, который по­ зволяет ограничить доступную процессу часть дерева базовой файловой системы. Системный вызов chroot защищает операционную систему от недоверенных сер­ верных процессов, таких как FTP, BIND и Sendrnail, которые открыты для общего доступа и уязвимы для компрометации. В 1980-1990-х бьmи созданы различные варианты Unix с мандатным контролем доступа из соображений безопасности 1• Это означало, что на одном ядре Unix вы1 Современная реализация - SELinux. Книги для программистов: https://clcks.ru/books-for-it 114 1 Глава 5 полнялось несколько строго контролируемых доменов. Процессы в каждом домене имели ограниченный доступ к системе и не могли взаимодействовать с процессами в других доменах. В популярном коммерческом дистрибутиве Unix был реализован межсетевой экран Sidewinder поверх BSDI Unix, но большинство распространен­ ных реализаций Unix не поддерживало такой подход. Все изменилось в 2000 году с выпуском FreeBSD 4.0, где была представлена новая команда jail. Она позволяла хостинг-провайдерам безопасно разделять в общей среде свои процессы и процессы каждого клиента. Команда FreeBSD jail дополня­ ла возможности chroot и также ограничивала все, что мог делать процесс с базовой системой и другими изолированными процессами. В 2004 году Sun выпустили раннюю сборку Solaris 1 О, которая включала контейне­ ры Solaris, позже реализованные в виде технологии Solaris Zones. Это была первая крупная коммерческая реализация технологии контейнеризации, и она по­ прежнему используется. В 2005 году компания Virtuozzo представила OpenVZ for Linux, а в 2007 году появилось решение НР Secure Resource Partitions for НР-UХ, позже переименованное в НР-UХ Containers. Компании вроде Google, которые масштабируют приложения для всего Интернета и предоставляют хостинг для недоверенного пользовательского кода, поддержива­ ли развитие технологии контейнеризации с начала 2000-х, чтобы надежно и безо­ пасно распределять приложения по центрам обработки данных по всему миру. Не­ которые компании дорабатывали ядра Linux, реализуя поддержку контейнеров для внутреннего использования, но когда потребность в этих функциях стала очевид­ ной внутри сообщества Linux, Google предложил свои вспомогательные контейне­ ры для мейнлайнового ядра Linux, и в 2008 году были выпущены контейнеры Linux (Linux Containers, LXC) в версии 2.6.24 ядра Linux. Невероятный рост популярно­ сти контейнеров Linux в сообществе начался только в 2013 году, когда в версию 3.8 ядра Linux были включены пользовательские пространства имен, а месяц спустя вышел Docker. Сегодня контейнеры распространены повсеместно. Образы Docker и OCI предос­ тавляют формат упаковки для огромного множества программ, поставляемых в ра­ бочие среды, а также предлагают основу для таких систем, как Kubernetes и бессер­ верные облачные технологии. Так называемые бессерверные технологии на самом деле работают на серве­ рах - просто не на ваших, а на чьих-то других, так что вам, как владельцу прило­ жения, не приходится управлять оборудованием и операционной системой. Создание контейнера До этого мы запускали контейнеры с помощью удобной команды docker container run. На самом деле docker container run объединяет два шага, и на первом мы создаем контейнер из базового образа. Эту задачу можно выполнить отдельно командой docker container create. Вторая задача docker container run - выполнение контейнера, и это тоже можно сделать отдельно командой docker container start. Книги для программистов: https://clcks.ru/books-for-it Работа с контейнерами 1 115 Команды docker container create и docker container start поддерживают любые аргу­ менты, относящиеся к первой настройке контейнера. В главе 4 мы видели, что в команде docker container run можно сопоставлять сетевые порты в контейнере с хостом с помощью аргумента -p/--puЫish, а -e/--env позволяет передавать в кон­ тейнер переменные среды. И это лишь малая часть возможностей для конфигурации контейнера при первом создании. Давайте рассмотрим некоторые возможности, доступные с docker. Базовая конфигурация Давайте узнаем, как сообщить Docker желаемую конфигурацию контейнера при создании. Имя контейнера Контейнер создается из базового образа, но на итоговую конфигурацию можно повлиять с помощью различных аргументов командной строки. Параметры в Dockerfile всегда задаются как значения по умолчанию, но мы можем переопре­ делить их при создании. По умолчанию Docker присваивает контейнеру случайное имя (https://github.com/ moby/moby/ЬloЬ/master/pkg/namesgenerator/names-generator.go), состоящее из прилагательного и имени какого-нибудь известного человека. Например, ecstatic­ babbage (восторженный Бэббидж) или serene-albattani (безмятежный Аль-Батани). Присвоить контейнеру другое имя можно с помощью аргумента --name: $ docker container create --name= "awesome-service" uЬuntu:latest \ sleep 120 После создания контейнер можно запустить командой docker container start awesome­ service. Он автоматически завершится через 120 секунд, но мы можем остановить его принудительно: docker container stop awesome-service. Позже в этой главе мы рас­ смотрим эти команды. В пределах хоста Docker у контейнеров должны быть уникальные имена. Если мы запустим предыдущую команду два раза подряд, получим ошибку. Нужно сначала удалить предыдущий контейнер командой docker container rm или переименовать новый контейнер. Метки Как было сказано в главе 4, метки представляют собой пары "ключ-значение", ко­ торые можно добавлять к образам и контейнерам Docker в качестве метаданных. При создании новые контейнеры Linux автоматически наследуют все метки из родительского образа. Мы также можем добавлять в контейнеры новые метки, чтобы применять метадан­ ные, относящиеся к одному контейнеру: Книги для программистов: https://clcks.ru/books-for-it Глава 5 116 $ docker container run --rm -d --name has-some-labels \ -1 deployer=Ahmed -1 tester=Asako \ uЬuntu:latest sleep 1000 Мы можем искать и фильтровать контейнеры по этим метаданным с помощью таких команд, как docker container 1s: $ docker container 1s -а -f label=deployer=Ahmed CONTAINER ID IМAGE СОММАND NAМES 845731631Ьа4 uЬuntu:latest "sleep 1000" ... has-some-laЬels Команда docker container inspect выводит все метки контейнера: $ docker container inspect has-some-laЬels 11 LaЬels 11 : { "deployer": "Ahmed", "tester": "Asako" ), Этот контейнер выполняет команду sleep 1000, так что через 1 ООО секунд он остано­ вится. Имя хоста По умолчанию при запуске контейнера Docker копирует некоторые системные файлы на хосте, включая /etc/hostname, в каталог конфигурации контейнера на хос­ те2, а затем при помощи binct mount связывает эту копию файла с контейнером. Мы можем запустить контейнер по умолчанию без специальной конфигурации: $ docker container run --rm -ti uЬuntu:latest /bin/bash Здесь команда docker container run запускает docker container create И docker container start в фоновом режиме. Нам нужна возможность взаимодействовать с контейне­ ром, который мы создадим для демонстрации, поэтому мы передадим несколько полезных аргументов. Аргумент --rm указывает Docker удалить контейнер при за­ вершении работы, аргумент -t велит выделить псевдо-ТТУ, а аргумент -i означает, что сеанс будет интерактивным и мы хотим оставить поток STDIN открытым. Если в образе не определена точка входа ENTRYPOINT, то последним аргументом в команде будет исполняемый файл и аргументы командной строки, которые должны выпол­ няться в контейнере. В нашем случае это будет неизменный /Ьin/bash. Если в образе определена ENTRYPOINT, финальный аргумент передается в процесс ENTRYPOINT в виде списка аргументов командной строки для этой команды. Возможно, вы заметили, что мы говорим об аргументах -i и -t, а в команде указан аргумент -ti. Это связано с историей Unix. Если интересно, прочтите короткую справку (https://nullprogram.com/Ыog/2020/08/01 ). 2 Обычно по пуrи /var/liЬ/docker/coпtaiпers. Книги для программистов: https://clcks.ru/books-for-it Работа с контейнерами 117 Если мы сейчас выполним команду mount в итоговом контейнере, то увидим что-то подобное: root@eЬc8cf2d8523:/1 mount overlay оп/ type overlay (rw,relatime,lowerdir= ...,upperdir= ...,workdir ...) proc оп/proc type proc (rw,nosuid,nodev,noexec,relatime) tmpfs оп/dev type tmpfs (rw,nosuid,mode=755) shm оп/dev/shm type tmpfs (rw,nosuid,nodev,noexec,relatime,size=б5536k) mqueue оп/dev/mqueue type mqueue (rw,nosuid,nodev,noexec,relatime) devpts оп/dev/pts type devpts (rw,nosuid,noexec,relatime, ...,ptmxmode=ббб) sysfs оп/sys type sysfs (ro,nosuid,nodev,noexec,relatime) /dev/sda9 оп/etc/resolv.conf type ext4 (rw,relatime,data=ordered) /dev/sda9 оп/etc/hostname type ext4 (rw,relatime,data=ordered) /dev/sda9 оп/etc/hosts type ext4 (rw,relatime,data=ordered) devpts оп/dev/console type devpts (rw,nosuid,noexec,relatime, ...,ptmxmode=OOO) proc оп/proc/sys type proc (ro,nosuid,nodev,noexec,relatime) proc оп /proc/sysrq-trigger type proc (ro,nosuid,nodev,noexec,relatime) proc оп/proc/irq type proc (ro,nosuid,nodev,noexec,relatime) proc оп/proc/bus type proc (ro,nosuid,nodev,noexec,relatime) tmpfs оп/proc/kcore type tmpfs (rw,nosuid,mode=755) root@eЬc8cf2d8523:/I Если в строке с примером вы видите что-то вроде root@hashID, команда выполня­ ется в контейнере, а не на локальном хосте. � Иногда для контейнера настроено другое имя хоста (например, с помощью аргу­ мента --name в CLI), но по умолчанию это хеш идентификатора (ID hash) контей­ нера. Также мы можем изменить пользователя в контейнере с помощью --user, но по умолчанию это будет root. В контейнере несколько привязок Ьind mount, но мы обратим внимание на следую­ щую строку: /dev/sda9 оп/etc/hostname type ext4 (rw,relatime,data=ordered) Номера устройства для контейнеров будут различаться, но нас интересует точка монтирования - /etc/hostname. В контейнере /etc/hostname связывается с файлом имени хоста, который Docker подготовил для этого контейнера. По умолчанию он содержит идентификатор контейнера и не имеет полного доменного имени. Мы можем проверить это в контейнере, выполнив следующую команду: root@eЬc8cf2d8523:/1 hostname -f ebc8cf2d8523 root@eЬc8cf2d8523:/# exit Когда закончите, не забудьте выйти (exit) из командной оболочки контейнера, что­ � бы вернуться на локальный хост. � Книги для программистов: https://clcks.ru/books-for-it 118 Глава 5 Чтобы задать имя хоста, можно указать аргумент --hostname и передать нужное зна­ чение: $ docker container run --rm -ti --hostname= "mycontainer.example.com" \ uЬuntu:latest /Ыn/bash В контейнере мы увидим, что полное имя хоста определено, как мы запросили: root@mycontainer:/# hostname -f mycontainer.example.com root@mycontainer:/# exit DNS Как и /etc/hostname, файл r esolv.conf, настраивающий разрешение доменных имен (Domain Name Syste m, DNS), управляется через Ыnd mount между хостом и контей­ нером: /dev/sda9 on /etc/resolv.conf type ext4 (rw,relatime,data= ordered) � � Подробнее о файле resolv.conf можно почитать здесь: https://sslhow.com/understanding-etc-resolv-conf-file-in-linux. По умолчанию это точная копия файла resolv.conf хоста Docker. Если вас это не устраивает, с помощью аргументов --dns и --dns-search переопределите это поведе­ ние в контейнере: $ docker container run --rm -ti --dns=8.8.8.8 --dns=8.8.4.4 \ --dns-search=exaшplel.com --dns-search=example2.com \ uЬuntu:latest /Ьin/bash � Есл, вы не ,а,,.е нас;раива;s аоис,овый домен, у,аж"е --dns-seшh•. В контейнере останется привязка Ыnd mount, но содержимое файла больше не будет повторять файл resolv.confxocт a, а станет выглядеть следующим образом: root@Of887071000a:/# more /etc/resolv.conf nameserver 8.8.8.8 nameserver 8.8.4.4 search examplel.com example2.com root@Of887071000a:/# exit МАС-адрес Мы можем настроить для контейнера МАС-адрес. Если значение не задано, контейнер получит вычисленный МАС-адрес, который начинается с префикса 02: 42: ас: 11. Книги для программистов: https://clcks.ru/books-for-it Работа с контейнерами 1 119 При желании -мы можем задать это значение: $ docker container run --rm -ti --mac-address="a2:11:aa:22:bb:33" \ uЬuntu:latest /Ыn/bash Как правило, в этом нет необходимости, но иногда мы хотим зарезервировать на­ бор МАС-адресов для контейнеров, чтобы избежать конфликтов с другими слоями виртуализации, которые используют тот же блок частных адресов, что и Docker. Будьте осторожны при настройке МАС-адресов. Если две системы объявят один МАС-адрес, возникнет конфликт, связанный с ARP. Если задать адрес все же не­ обходимо, постарайтесь сделать так, чтобы ваши локальные диапазоны находи­ лись в пределах официальных, например х2-хх-хх-хх-хх-хх, хб-хх-хх-хх-хх- хх, хАхх-хх-хх-хх-хх и хЕ-хх-хх-хх-хх-хх (где х - любой допустимый шестнадцатерич­ ный символ). Тома хранилища Иногда нам недостаточно дискового пространства, выделенного контейнеру по умолчанию, или контейнер не должен быть "эфемерным", и тогда потребуется хра­ нилище, чтобы данные сохранялись между развертываниями. Обычно не рекомендуется подключать хранилище с хоста Docker, потому что кон­ тейнер будет привязан к определенному хосту, на котором хранится его состояние. Но для временных фi:iйлов кеша или полуэфемерных состояний этот вариант мо­ жет подойти. В таких случаях используйте команду --mount/-v для подкmочения каталогов и от­ дельных файлов с сервера хоста к контейнеру. В аргументе --mount/-v необходимо указать полный путь. В следующем примере /mnt/session_data подключается к /data в контейнере: $ docker container run --rm -ti \ --mount type=Ыnd,target=/mnt/session_data,source=/data \ uЬuntu:latest /Ьin/bash root@Of887071000a:/t mount I grep data /dev/sda9 on /data type ext4 (rw,relatime,data=ordered) root@Of887071000a:/t exit Для Ыnd mount можно использовать аргумент -v, чтобы сократить команду. При этом вы заметите, что исходные и целевые файлы и каталоги разделены двоето­ чием(:). Важно ртметить, что тома по умолчанию монтируются в режиме чтения и записи. Мы можем указать doaker, что файл или каталог нужно примотировать только для чтения, добавив readonl у после аргументов --mount или указав : ro после аргумен­ тов -v: $ docker container run --rm -ti \ -v /mnt/session data:/data:ro \ uЬuntu:latest /Ыn/bash Книги для программистов: https://clcks.ru/books-for-it 120 Глава 5 Точки монтирования на хосте и в контейнере не обязательно создавать заранее. Если точка монтирования на хосте еще не существует, она будет создана в виде каталога. Если мы пытаемся указать на файл, а не каталог, могут возникнуть про­ блемы. В параметрах монтирования мы видим, что файловая система успешно подключена для чтения и записи по пути /data, как и ожидалось. SELinux и подключение томов Если на хосте Docker включен SELinux, то при попытке примонтировать том к контейне­ ру может возникнуть ошибка из-за отсутствия прав доступа. Чтобы решить проблему, укажите z в команде Docker для подключения томов: • Опция z (в нижнем регистре) указывает, что содержимое bind mount является общим для нескольких контейнеров. • Опция z (в верхнем регистре) указывает, что содержимое Ьind mount доступно только этому контейнеру. Если том будет общим для контейнеров, укажите z при подключении контей­ нера: $ docker container run --rm -v /app/dhcpd/etc:/etc/dhcpd:z dhcpd т Лучше, конечно, указать в команде подключения тома параметр z, чтобы задать для директории ту же метку MCS (например, chcon ... -1 s0:cl,c2}, которую будет использо­ вать контейнер. Это позволит обеспечить самый высокий уровень безопасности, потому что к тому будет доступ только у одного контейнера: $ docker container run --rm -v /app/dhcpd/etc:/etc/dhcpd:Z dhcpd Будьте крайне осторожны с параметрами z/Z. Если мы попытаемся с помощью Ьind mount примонтировать системный каталог, например /etc или /var, указав па­ раметр z, скорее всего, мы не сможем дальше работать с системой, и придется использовать инструменты SELinux, чтобы вручную сменить метки на хост-машине (https://www.thegeekdiary.com/understanding-selinux-file-labelling-and-selinuxcontext). Если контейнерное приложение будет записывать данные в каталог /data, то эти данные будут видны в файловой системе хоста в /mлt/session_data, и они будут дос­ тупны, даже когда этот контейнер остановится и запустится новый с тем же под­ ключенным томом. Мы можем указать Docker, что корневой том контейнера должен подключаться в режиме "только для чтения", чтобы процессы в контейнере ничего не могли запи­ сывать в корневую файловую систему. В этом случае файлы журнала и другие дан­ ные, о которых разработчик может не знать, не будут заполнять выделенное кон­ тейнеру место на диске в рабочей среде. При таком подходе мы сможем гарантиро­ вать, что данные будут записываться только в ожидаемые расположения. В предыдущем примере для этого достаточно было добавить к команде аргумент --read-only=true: Книги для программистов: https://clcks.ru/books-for-it Работа с контейнерами 121 $ docker container run --rm -ti --read-only=true -v /mnt/session data:/data \ uЬuntu:latest /bin/bash root@df542767Ьc17:/# mount I grep " /" overlay on / type overlay (ro,relatime,lowerdir=...,upperdir=...,workdir=... ) root@df542767Ьc17:/# mount I grep data /dev/sda9 on /data type ext4 (rw,relatime,data=ordered) root@df542767Ьc17:/# exit Если внимательно изучить параметры подключения для корневого каталога, можно заметить параметр ro, который задает доступ только для чтения. Однако папка /session_data все еще подключена с параметром rw, так что приложение будет запи­ сывать данные в тот том, запись в который разрешена. Иногда мы хотим записывать данные в каталог /tmp или подобный, даже если ос­ тальное предоставляется только для чтения. В этом случае можно добавить аргу­ мент --mount type=tmpfs в команду docker container run, чтобы подключить файловую систему tmpfs к контейнеру. Файловые системы tmpfs целиком находятся в памяти. Они очень быстрые, но временные (т. е. содержимое не сохраняется при переза­ грузке), и потребляют дополнительную системную память. При остановке контей­ нера все данные в каталогах tmpfs будут также утеряны. В следующем примере по­ казан контейнер с файловой системой tmpfs на 256 МБ, подключенный к каталогу /tmp: $ docker container run --rm -ti --read-only=true \ --mount type=tmpfs,destination=/tmp,tmpfs-size= 256M \ uЬuntu:latest /bin/bash rооt@25Ь4f3632ЬЬс:/# df -h /tmp Used Avail Use% Mounted оп Filesystem Size 256М 0% 256М О /tmp tmpfs rооt@25Ь4f3632ЬЬс:/# grep /tmp /etc/mtaЬ tmpfs /tmp tmpfs rw,nosuid,nodev,noexec,relatime,size=262144k О О rооt@25Ь4f3632ЬЬс:/# exit � По возможнос,и ,онтейнеры стоит ороекп,роватъ та,им образом, чтобы они не сохраняли состояние. Необходимость в хранилище приводит к нежелательным зависимостям и существенно усложняет сценарии развертывания. Квоты на ресурсы "Шумный сосед" (noisy neighbor) - одна из главных проблем, которые возникают при работе в облаке. "Шумным соседом" называют приложение, которое выполня­ ется в той же физической системе, что и ваше приложение, и мешает его работе, захватывая много ресурсов. При использовании виртуальных машин мы строго контролируем выделение памя­ ти, ЦП и других ресурсов. С Docker приходится задействовать функционал cgroup в ядре Linux, чтобы ограничивать ресурсы, доступные контейнеру Linux. Команды docker container create и docker container run позволяют при создании контейнера наКниги для программистов: https://clcks.ru/books-for-it 122 Глава 5 прямую настраивать лимиты по ЦП, памяти, подкачке (swap) и вводу-выводу хра­ нилища. Если нужно будет изменить эти параметры после создания контейнера, можно вы­ полнить команду docker container update или развернуть новый контейнер с новы­ ми значениями. Правда, нужно помнить, что хотя Docker и поддерживает лимиты на ресурсы, эти возможности должны быть включены в ядре. Возможно, необходимо будет доба­ вить их как параметры командной строки к ядру при запуске. Чтобы узнать, под­ держивает ли ваше ядро эти лимиты, выполним команду docker system info. Если что-то не поддерживается, мы увидим предупреждение: WARNING: No swap limit support (ПРЕДУПРЕЖДЕНИЕ: лимиты на подкачку не поддерживаются) lЖ � Настройка поддержки возможностей в cgroup для ядра зависит от дистрибутива. 3 Подробности см. в документации Docker . Доля процессорного времени В Docker есть несколько способов ограничить потребление процессорных ресурсов для приложений в контейнерах. Давний и все еще популярный метод - доли про­ цессорного времени (CPU shares). Мы рассмотрим и другие варианты. Вычислительная мощность всех процессорных ядер в системе представляет собой весь пул долей. В Docker пул делится на 1024 доли. Настраивая доли процессорно­ го времени контейнера, мы можем указывать, сколько времени следует выделить контейнеру. Если мы хотим отдать контейнеру половину вычислительной мощно­ сти системы, можно выделить 512 долей. Мы не запрещаем другим процессам ис­ пользовать эти доли - если мы назначим все 1024 доли одному контейнеру, это не значит, что остальные контейнеры не смогут выполняться. Мы скорее указываем планировщику, сколько времени каждый контейнер может выполняться при каж­ дом запуске. Если мы назначили одному контейнеру 1024 доли (по умолчанию) и еще двум - по 512, все они будут запланированы одинаковое число раз. Но если обычное процессорное время для каждого процесса составляет 100 мкс, контейне­ ры, которым мы выделили по 512 долей, будут выполняться по 50 мкс, а контейнер с 1024 долями получит стандартные l 00 мкс. Давайте посмотрим, как это будет работать на практике. Для следующих примеров мы создадим новый образ Docker, который содержит команду stress (https:// Iinux.die.net/man/1/stress), чтобы задействовать все ресурсы системы. 3 Полный URL: https://docs.docker.com/engine/instalVlinux-postinstalV#your-kernel-does-not-support­ cgroup-swap-limit-capabllities. Книги для программистов: https://clcks.ru/books-for-it Работа с контейнерами 123 Если выполнить stress без ограничений cgroup, то будет потребляться столько ре­ сурсов, сколько мы укажем. Следующая команда создает среднюю нагрузку (load average) около пяти: два процесса, ограниченных процессорным временем, один процесс, ограниченный вводом-выводом, и два процесса, ограниченных памятью. Во всех следующих примерах мы работаем в системе с двумя ЦП. Здесь весь код после имени образа контейнера относится к команде stress, а не docker: $ docker container run --rm -ti spkane/train-os \ stress -v --cpu 2 --io 1 --vrn 2 --vrn-bytes 128М --timeout 120s Эта команда будет адекватно выполняться на любом современном компьютере, но нагрузка на хост-систему будет очень большой. Не выполняйте команду, если сис­ тема не справится с дополнительной нагрузкой или из-за нехватки ресурсов воз­ никнет сбой. Если выполнить команду top или htop на хосте Docker, то ближе к концу двухми­ нутного выполнения вы заметите, как влияет на систему нагрузка, созданная про­ граммой stress: $ top -bnl I head -n 15 top - 20:56:36 up 3 min, 2 users, load average: 5.03, 2.02, 0.75 Tasks: 88 total, 5 running, 83 sleeping, О stopped, О zomЬie %Cpu(s): 29.8 us, 35.2 sy, О.О ni, 32.0 id, 0.8 wa, 1.6 hi, 0.6 si, О.О st 42716 buffers KiB Mem: 1021856 total, 270148 used, 751708 free, 83764 cached Mem KiB Swap: О total, О used, О free. PID USER 810 root 813 root 812 root 814 root 811 root 1 root 2 root 3 root PR 20 20 20 20 20 20 20 20 NI о о о о о о о о VIRT 7316 7316 138392 138392 7316 110024 о о RES 96 96 46936 22360 96 4916 о о SHR S О R О R 996 R 996 R О D 3632 s оs оs %CPU 44.3 44.3 31.7 31.7 25.3 о.о о.о о.о %МЕМ о.о о.о 4.6 2.2 о.о 0.5 о.о о.о TIME+ COMМAND О:49.63 stress 0:49.18 stress 0:46.42 stress 0:46.89 stress 0:21.34 stress 0:07.32 systemd 0:00.04 kthreadd О:00.11 ksoftir Пользователи Docker Desktop, которые работают не в Linux, заметят, что Docker настраивает доступ к файловой системе только для чтения и не предлагает много полезных инструментов для мониторинга виртуальной системы. Если вы хотите � отслеживать потребление ресурсов различными процессами, попробуйте обойти эту проблему так: $ docker container run --rm -it --pid=host alpine sh / # apk update / # apk add htop / # htop -р $ (pgrep stress I tr '\n' ', ') / # exit Книги для программистов: https://clcks.ru/books-for-it 124 Глава 5 Предыдущая команда htop выдаст ошибку, если при ее запуске не выполнятся stress, потому что команда pgrep не вернет никакие процессы. Необходимо выходить и запускать htop снова каждый раз при запуске нового эк­ земпляра stress. Если вы хотите снова выполнить ту же команду stress, но уменьшив доступное время ЦП вдвое, это возможно: $ docker container run --rm -ti --cpu-shares 512 spkane/train-os \ stress -v --cpu 2 --io 1 --vm 2 --vm-bytes 128М --timeout 120s Флаг --cpu-shares 512 указывает, что этому контейнеру нужно выделить 512 долей процессорного времени. Если система не очень загружена, мы можем не заметить действие этого аргумента - контейнер, выполняющий задачи, будет Шiанировать­ ся на тот же отрезок времени, когда в системе достаточно ресурсов. В нашем слу­ чае результаты команды top в хост-системе будут выглядеть точно так же, если только мы не запустим еще несколько контейнеров, которые тоже будут потреблять процессорное время. В отличие от виртуальных машин, в Docker ограничения по долям процессорного времени на основе cgroup могут иметь неожиданные последствия. Это не жесткие лимиты, а относительные, примерно как команда nice. В рассмотренном примере запускается контейнер, которому выделили только половину процессорного времени, но система не слишком загружена. Поскольку ЦП не занят, лимит по времени будет иметь ограниченный эффект - больше никто не претендует на ресурсы в пуле планировщика. Если развернуть в той же системе второй контейнер, кото­ рому требуется много процессорного времени, мы сразу заметим, как действуют ограничения для первого контейнера. Учитывайте эти факторы при распределении и ограничении ресурсов. Привязка к процессору Мы можем привязать контейнер к одному или нескольким ядрам ЦП (CPU pinning), и тогда он будет Шiанироваться только на этих ядрах. Это удобно, если мы хотим зафиксировать разделение ЦП между приложениями или приложение по каким-то причинам должно выполняться на определенном процессоре, например для эффек­ тивного использования кеша. В следующем примере мы запускаем с утилитой stress контейнер, привязанный к первым двум ЦП, выделив ему 512 долей процессорного времени: $ docker container run --rm -ti \ --cpu-shares 512 --cpuset-cpus= O spkane/train-os \ stress -v --cpu 2 --io 1 --vm 2 --vm-bytes 128М --timeout 120s т Аргумент --cpuset-cpus задает начало с индекса О, так что первым ядром будет ядро О. Если мы укажем Docker использовать ядро ЦП, которого нет в хост­ системе, получим ошибку "Саппоt start container'' (Невозможно запустить контей­ нер). На хосте с двумя ядрами, это можно проверить, используя --cpuset-cpus=0-2. Если снова выполнить top, мы заметим, что процент процессорного времени, по­ траченного в пространстве пользователя (user space, us), ниже, чем раньше, потому Книги для программистов: https://clcks.ru/books-for-it Работа с контейнерами 125 что мы предоставили один ЦП двум процессам, требующим много процессорного времени: %Cpu(s): 18.5 us, 22.0 sy, О.О ni, 57.6 id, 0.5 wa, 1.0 hi, 0.3 si, О.О st Когда мы привязываем контейнер к ЦП, дополнительные ограничения по процес­ сорному времени будут учитываться, только если на том же наборе ядер выпол­ няются другие контейнеры. При использовании планировщика Completely Fair Scheduler (CFS) в ядре Linux можно изменить квоту по процессорному времени для заданного контейнера, уста­ новив для флага --cpu-quota соответствующее значение при запуске контейнера командой docker container run. Упрощение квот на ресурсы процессора Изначально для управления лимитами на ресурсы процессора в Docker использова­ лись доли процессорного времени, но с тех пор многое изменилось, и сейчас суще­ ствует гораздо более удобный способ. Вместо того чтобы самостоятельно задавать доли и квоты, мы можем указать Docker, сколько ресурсов процессора следует пре­ доставить контейнеру, и система сама рассчитает значения, которые нужно задать ДЛЯ cgroup. Для команды -cpus можно задать число с плавающей запятой от 0.01 до количества ядер ЦП на сервере Docker: $ docker container run -rm -ti -cpus= ".25" spkane/train-os \ stress -v -cpu 2 -io 1 -vm 2 -vm-bytes 128М -timeout 60s Если задать слишком большое значение, мы получим сообщение об ошибке от самого Docker (не от утилиты stress), в котором будет указано доступное число ядер процессора: $ docker container run -rm -ti -cpus="40.25" spkane/train-os \ stress -v -cpu 2 -io 1 -vm 2 -vm-bytes 128М -timeout 60s docker: Error response from daemon: Range of CPUs is from 0.01 to 4.00, as there are only 4 CPUs availaЫe. See 'docker container run -help'. Команда docker container update позволяет динамически корректировать ограниче­ ния ресурсов в одном или нескольких контейнерах. Мы можем изменить выделение процессорных ресурсов сразу для двух контейнеров: $ docker container update -cpus= "l.5" 092c5dc85044 92Ь797f12afl Docker воспринимает процессоры так же, как их воспринимает Liпux. Гиперпоточ­ ность и ядра процессора обрабатываются в Liпux, и информация о них экспорти­ руется в специальный файл /proc/cpuinfo. При вызове команды --cpus в Docker мы указываем, к какому числу записей в этом файле у контейнера будет доступ, и это могут быть стандартные или гиперпоточные ядра. Книги для программистов: https://clcks.ru/books-for-it 126 1 Глава 5 Память Мы можем ограничить потребление памяти контейнером так же, как ограничивали ресурсы процессора. Правда, есть важное отличие: ограничение процессорного времени влияет только на приоритет при выполнении приложения, а лимиты памя­ ти задаются жестко. Даже в системе без ограничений, rде свободно 96 ГБ памяти, мы можем выделить контейнеру 24 ГБ, и он не _получит больше, сколько бы сво­ бодной памяти ни было в системе. Система виртуальной памяти в Linux работает таким образом, что мы можем выделить контейнеру· больше памяти, 'чем имеет фактическое ОЗУ системы. В этом случае контейнер будет использовать файл под­ качки, как любой обычный процесс Linux. Давайте запустим контейнер с ограничением памяти, передав параметр --rnemory в команде docker container run: $ docker container run --rrn -ti --rnemory 512m spk�ne/train-os \ stress -v --ери 2 --io 1 --vrn 2 --vrn-bytes 128М --tirneout 10s ' ' Если мы укажем только параметр --rnemory, то зададим для контейнера ограничение по ОЗУ и объему подкачки. Если мы указали --rnernory s12m, контейнер получит 512 МБ ОЗУ и 512 МБ пространства подкачки. Docker· поддерживает обозначения ь, k, rn и g для байтов, килобайтов, мегабайтов и гигабайтов соотвщтвенно. Если · в вашей системе с Linux и Docker память измеряется терабайтами,, к сожалению, вам все равно придется оперировать гигабайтами. Если объем подкачки нужно задать отдельно или совсем отключить, у нас есть параметр --rnemory-swap. Он определяет общий объем памяти и подкачки, доступный контейнеру. Давайте изменим предыдущую команду: $ docker container run --rrn -ti --rnernory 512m --rnernory-swap=768rn ,. spkane/train-os stress -v --cpu 2 --io 1 --vrn 2 --vrn-bytes 128М \ --tirneout 10s Мы указали ядру, что этому контейнеру будет доступно 512 МБ памяти и 256 МБ подкачки. Если задать для параметра --rnemory-swap значение -1, то контейнер сможет использовать неограниченное пространство подкачки в базовой системе, а если для --rnemory-swap и --rnemory задано одинаковое положительное значение, то у контейнера не будет доступа к пространству подкачки. Опять же, в отличие от процессорного времени, ограничения на память задаются жестко. Это хорошо, потому что при развертывании в системе очередного контей­ нера уже развернутые контейнеры не пострадают. С другой стороны, нужно тща­ тельно рассчитывать лимиты в соответствии с потребностями контейнера, потому что пространства для маневра не 'останется. Если у контейнера заканчивается память, ядро ведет себя так, будто память закончилась всi' всей системе, - оно по­ пытается найти процесс, который можно завершить, чтобы освободить место. Это частая причина сбоя в системах, где для контейнеров выделяется слишком мало памяти. На проблему указывает код завершения контейнера 137 и сообщения ядра о нехватке памяти (Out-Of-Memoгy, ООМ) в выводе drnesg сервера Docker. Что будет, если контейнер достигнет лимита памяти? Давайте попробуем изменить одну из предыдущих команд, выделив гораздо меньше памяти: Книги для программистов: https://clcks.ru/books-for-it Работа с контейнерами 127 $ docker container run --rm -ti --memory 100m spkane/train-os \ stress -v --cpu 2 --io 1 --vm 2 --vm-bytes 128М --timeout 10s Предыдущие запуски контейнера stress завершались строками об успешном вы­ полнении: stress: info: [17] successful run completed in 10s Теперь запуск быстро завершается сбоем: stress: FAIL: [1] (451) failed run completed in 0s Это связано с тем, что контейнер пытается выделить больше памяти, чем разреше­ но, и вызывается защитный механизм Linux ООМ Killer, который начинает завер­ шать процессы в cgroup, чтобы освободить память. В этом контейнере выполняется один родительский процесс, породивший несколько дочерних, и когда ООМ Killer завершает один из дочерних процессов, родительский процесс· очищает остальное и завершает работу с ошибкой. В Docker можно настроить и отключить Linux ООМ Killer с помощью аргументов --oom-kill-disaЬle и --oom-score-adj для команды docker container run, но в по­ давляющем большинстве случаев лучше так не делать. Если обратиться к серверу Docker, можно прочесть сообщение от ядра, связанное с этим событием, с помощью ctmesg. Мы получим примерно следующий вывод: 4210.403984] stress invoked oom-killer: gfp_mask=0x24000c0 [ 4210.404899] stress cpuset=5bfa65084931efaЬda59d9a70fa8e88 [ 4210.405951] CPU: 3 PID: 3429 Comm: stress Not tainted 4.9 [ 4210.406624] Hardware name: BHYVE, BIOS 1.00 03/14/2014 4210.408978] Call Trace: [ 4210.409182] [ <ffffffff94438115>] ? dump_stack+0x5a/0x6f [ 4210.414139] [<ffffffff947f9cf8>] ? page_fault+0x28/0x30 [ 4210.414619] Task in /docker-ce/docker/5... 3 killed as а result of limit of /docker-ce/docker/5...3 4210.416640] memory: usage 102380kB, limit 102400kB, failc ... 4210.417236] memory+swap: usage 204800kB, limit 204800kB, ... 4210.417855] kmem: usage 1180kB, limit 9007199254740988kB, ... 4210.418485] Memory cgroup stats for /docker-ce/docker/5...3: cache:0KB rss:101200KB rss_huge:0KB mapped_file:0KB dirty:0KB writeback:11472KB swap:102420KB inactive_anon:50728KB active-anon:50472KB inactive-file:0KB active-file:0KB unevictaЬle:0KB [ 4210.426783] Memory cgroup out of memory: Kill process 3429... [ 4210.427544] Killed process 3429 (stress) total-vm:138388kB, anon-rss:44028kB, file-rss:900kВ, shmem-rss:0kB [ 4210.442492] oom_reaper: reaped process 3429 (stress), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB Книги для программистов: https://clcks.ru/books-for-it 128 1 Глава 5 Это событие ООМ также будет записано в Docker, и его можно посмотреть с по­ мощью команды docker systern events: $ docker systern events 2018-01-28Т15:56:19.972142371-08:ОО container oorn \ d0d803ce32c4e86d0aa6453512a9084a156e96860e916ffc2856fc63ad9cf88b \ (irnage=spkane/train-os, narne=loving_franklin) Блочный ввод-вывод Многие контейнеры представляют собой приложения без сохранения состояния, и им не нужны ограничения на блочный ввод-вывод. Docker все же поддерживает эти ограничения несколькими способами с помощью механизмов cgroup. Первый способ - настройка приоритетов при использовании контейнером ввода­ вывода блочного устройства. Для этого необходимо изменить параметр по умолча­ нию атрибута Ыkio.weight cgroup. У атрибута может быть значение О (отключено) или число от 10 до 1000. По умолчанию - 500. Это ограничение работает пример­ но так же, как доли процессорного времени, потому что система делит весь доступ­ ный ввод-вывод для всех процессов в cgroup на 1 ООО, и выбранное значение указы­ вает, сколько ресурсов ввода-вывода доступно каждому процессу. Чтобы задать это значение для контейнера, мы передаем --Ыkio-weight команде docker container run с допустимым значением. Мы можем указать значение для кон­ кретного устройства с помощью параметра --Ьlkio-weight-device. Как и при настройке долей процессорного времени, здесь сложно подобрать пра­ вильное значение, но можно упростить эту задачу, с помощью cgroup ограничив максимальное число байтов или операций в секунду, доступных контейнеру. Пара­ метры для контроля этих значений: ♦ --device-read-bps - ограничение скорости чтения (байт в секунду) с устройства; ♦ --device-read-iops - ограничение скорости чтения (операций в секунду) с уст­ ройства; ♦ --device-write-bps - ограничение скорости записи (байт в секунду) на устройство; ♦ --device-write-iops - ограничение скорости записи (операций в секунду) на устройство. Мы можем проверить, как эти настройки влияют на производительность контейне­ ра, выполнив следующие команды, использующие утилиту тестирования Linux bonnie (https://www.coker.com.au/bonnie): $ tirne docker container run --rrn -ti spkane/train-os:latest bonnie++ \ -u 500:500 -d /tmp -r 1024 -s 2048 -х 1 real 0rn27.715s user 0rn0.027s sys 0rn0.030s $ tirne docker container run -ti --rrn --device-write-iops /dev/vda:256 \ spkane/train-os:latest bonnie++ -u 500:500 -d /tmp -r 1024 -s 2048 -х 1 Книги для программистов: https://clcks.ru/books-for-it Работа с контейнерами 129 real 0m58.765s user 0m0.028s sys 0m0.029s $ time docker container run -ti --rm --device-write-bps /dev/vda:5mЬ \ spkane/train-os:latest bonnie++ -и 500:500 -d /tmp -r 1024 -s 2048 -х 1 Пользователи PowerShell могут использовать функцию Measure-Command (https:// learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/ measure-command?view=powershell-7.3) вместо команды Unix time. Как показывает опыт, аргументы --device-read-iops и --device-write-iops - это самый эффективный способ задавать ограничения для блочного ввода-вывода. ulimit До Linux cgroup был другой способ ограничивать ресурсы, досrупные процессу: применение пользовательских лимитов с помощью команды ulimit. Этот способ все еще работает для всех случаев, в которых он использовался раньше (https://www. linuxhowtos.orgffips%20and%20Tricks/ulimit.htm). В следующем фрагменте кода перечисляются типы системных ресурсов, которые можно ограничить с помощью мягких и жестких лимитов командой ulimit: $ ulimit -а core file size (Ьlocks, -с) О data seg size (kЬytes, -d) unlimited scheduling priority (-е) О file size (Ьlocks, -f) unlimited pending signals (-i) 5835 max locked memory (kЬytes, -1) 64 max memory size (kЬytes, -m) unlimited open files (-n) 1024 pipe size (512 bytes, -р) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) О stack size (kЬytes, -s) 10240 ери time (seconds, -t) unlimited max user processes (-u) 1024 virtual memory (kЬytes, -v) unlimited file locks (-х) unlimited Мы можем настроить демон Docker с пользовательскими лимитами по умолчанию, которые будут применяться к каждому контейнеру. Следующая команда указывает демону Docker, что нужно запустить все контейнеры с мягким (soft) ограничением в 50 открытых файлов и жестким (hard) ограничением в 150 открытых файлов: $ sudo dockerd --default-ulimit nofile=50:150 Книги для программистов: https://clcks.ru/books-for-it 130 Глава 5 Мы можем переопределить эти ограничения для конкретного контейнера, передав значения с помощью аргумента --ulimit: $ docker container run --rm -d --ulimit nofile=lS0:300 nginx При создании контейнеров можно выполнять и более сложные команды, но приве­ денных здесь будет достаточно для самых распространенных вариантов использо­ вания. В документации по клиенту Docker (https://dockr.ly/2ME0ygi) перечислены все доступные варианты. Изменения вносятся с каждым выпуском Docker. Запуск контейнера Перед тем как углубиться в тему ограничения ресурсов, мы создали контейнер ко­ . мандой docker container create. Пока контейнер ничего не делает. У него есть конфи­ гурация, но никакие процессы не выполняются. Когда мы будем готовы запустить контейнер, мы выполним команду docker container start. Допустим, мы хотим запустить копию Redis, стандартного хранилища ключей и значений. Мы ничего не будем делать с этим контейнером Redis - это просто легкий и долгосрочный процесс, на примере которого мы рассмотрим, какие воз­ можности нам доступны в реальной среде. Сначала можно создать контейнер: $ docker container create -р 6379:6379 redis:2.8 UnaЫe to find image 'redis:7.0' locally 7.0: Pulling from library/redis 3f4ca6laafcd: Pull complete 20bfl5ad3c24: Pull complete Digest: sha256:8184cfe57f205ab34c62bd0e9552dffeb885d2a7f82ce4295c0df344cb6f0007 Status: Downloaded newer image for redis:7.0 092c5dc850446324e4387485df7b7625Bfdf9ed0aedcd53a37299d35fc67a042 В последней строке вывода этой команды приводится полный хеш, сгенерирован­ ный для контейнера. Мы можем использовать эту строку для запуска контейнера, но если мы ее не записали, можно вывести список всех контейнеров в системе, даже не запущенных, следующей командой: $ docker container 1s -а --filter ancestor=redis:2.8 СОММАND CONTAINER ID IМAGE ... NAМES CREATED 092c5dc85044 redis:7.0 "docker-entrypoint.s... " 46 seconds ago elegant_wright Чтобы найти нужный контейнер, отфильтруем результаты по использованному об­ разу и посмотрим на время создания. Запустим контейнер следующей командой: $ docker container start 092c5dc85044 Большинство команд Docker работают с именем контейнера, полным хешем, ко­ ротким хешем или даже частью хеша, если она уникальна. В предыдущем примере полный хеш контейнера - 092c5dc850446324e...a37299d35fc67a042, а короткий, кото­ � рый будет отображаться в выходных данных большинства команд, - 092c5dc85044. Короткий хеш включает первые 12 символов полного. В рассмотренном примере достаточно было указать docker container start 092. Книги для программистов: https://clcks.ru/books-for-it Работа с контейнерами 1 131 Контейнер должен был запуститься, но поскольку он работает на фоне, мы могли бы и не заметить сбой. Мы можем проверить, все ли в порядке: $ docker container ls CONTAINER ID IМAGE СОММАND 092c5dc85 044 redis:7.0 "docker-entrypoint.s ..." STATUS Up 2 minutes Все работает, как и ожидалось: мь; видим статус Up и время работы контейнера. Автоматический перезапуск контейнера Часто мы хотим, чтобы контейнеры перезапускались после завершения работы. Некоторые контейнеры живут очень недолго, постоянно запускаясь и отключаясь. Если мы запустили приложение в рабочей среде, мы ожидаем, что оно будет рабо­ тать непрерывно. В сложной системе нам помогает планировщик, но в этом про­ стом примере мы поручим Docker управлять перезапуском с помощью аргумента --restart для команды docker container run. Он принимает четыре значения: no, always, on-failure и unless-stopped. Если для restart задано no (нет), контейнер никогда не пе­ резапускается после завершения работы. Значение always (всегда) означает, что кон­ тейнер будет запускаться каждый раз, независимо от кода выхода. Когда для restart указано on-failure (при сбое), Docker попробует перезапустить контейнер, если код выхода отличен от О. Если установить on-failure:З, Docker попробует перезапустить контейнер три раза, а затем прекратит попытки. Значение unless-stopped (пока не остановят)- самый популярный вариант, при котором контейнер перезапускается, пока мы его не остановим, например командой docker container stop. Мы можем проверить эти значения на практике, стартовав последний контейнер с ограничением по памяти без аргумента --rm, зато с аргументом --restart: $ docker container run -ti --restart=on-failure:3 --memory 10 0m \ spkane/train-os stress -v --cpu 2 --io 1 --vm 2 --vm-bytes 128М \ --timeout 120s В этом примере мы видим на консоли выходные данные первого выполнения, пре­ жде чем контейнер завершается. Если сразу после этого выполнить docker container ls, скорее всего, мы увидим, что Docker перезапустил контейнер: $ docker container ls ... IМAGE ... STATUS ... spkane/train-os ... Up Less than а second Перезапуск снова завершится сбоем, потому что мы не предоставили контейнеру достаточно памяти для работы. После трех попыток Docker перестанет запускать контейнер, и он исчезнет из выходных данных docker container ls. Остановка контейнера Мы можем запускать и останавливать контейнеры по желанию. Это похоже на приостановку и возобновление обычного процесса, но есть отличия. При остановке процесс не ставится на паузу- он завершается. Если мы остановим контейнер, он Книги для программистов: https://clcks.ru/books-for-it 132 Глава 5 не будет отображаться в выводе docker container 1s. При перезагрузке Docker попы­ тается запустить все контейнеры, которые функционировали при завершении рабо­ ты. Если контейнер не должен выполнять никаких задач, но при этом мы не хотим останавливать процесс, контейнер Linux можно приостановить, выполнив команду docker container pause, а затем запустить снова с помощью unpause. Позже мы погово­ рим об этом подробнее. Давайте остановим контейнер Redis, который запустили чуть раньше: $ docker container stop 092c5dc85044 $ docker container 1s CONTAINER ID IМAGE СОММАND CREATED STATUS PORTS NAМES Список контейнеров пуст. Можем запустить контейнер снова по идентификатору, но запоминать идентификаторы неудобно. К счастью, у команды docker container 1s есть дополнительный параметр (-а), с помощью которого можно просмотреть все контейнеры, а не только запущеннь1е: $ docker container 1s -а CONTAINER ID IМAGE STATUS 092c5dc85044 redis:7.0 Exited (0) 2 minutes ago В поле sтдтus мы видим, что контейнер завершил работу с кодом выхода О, т. е. без ошибок. Мы можем запустить его снова с той же конфигурацией, что и раньше: $ docker container start 092c5dc85044 092c5dc85044 $ docker container 1s -а CONTAINER ID IМAGE STATUS 092c5dc85044 redis:7.0 Up 14 seconds Контейнер снова работает, и с теми же параметрами. Контейнеры существуют в системе Docker в виде ВLОВ-объекта с конфиrурацией, даже если они не запущены. Следовательно, пока мы не удалим контейнер, его можно запускать снова, не создавая повторно. Хотя память и содержимое времен­ � ных файлов (tmpfs) будут утеряны, остальное содержимое файловой системы и метаданные, включая переменные среды и привязки портов, сохранятся при пере­ запуске. Скорее всего, вы уже понимаете, что контейнеры представляют собой дерево про­ цессов, которые взаимодействуют с системой практически так же, как любые дру­ гие процессы на сервере. Эго означает, что мы можем отправлять сигналы Unix процессам в контейнерах, и они будут отвечать на сигналы. В предыдущем приме­ ре с помощью команды docker container stop мы отправляем контейнеру сигнал SIGТERМ и ждем, пока он корректно завершит работу. Контейнеры подчиняются тем же правилам распространения сигналов по группе процессов, что и остальные группы процессов в Linux. Книги для программистов: https://clcks.ru/books-for-it Работа с контейнерами 1 133 Команда docker container stop отправляет процессу сигнал SIGГERМ. Если мы хотим принудительно завершить процесс, который не остановился через определенный период, можно указать аргумент -t: $ docker container stop -t 25 092c5dc85044 Docker сначала отправит сигнал SIGTERМ, как и раньше, и если контейнер не остано­ вится через 25 секунд (по умолчанию- 10), Docker отправит сигнал SIGKILL, чтобы завершить процесс принудительно. Лучший способ останавливать контейнеры- команда stop, но иногда она не сраба­ тывает, и нужно остановить контейнер принудительно, как это бывает с любым другим процессом. Принудительное завершение контейнера Если процесс работает неправильно, команда docker container stop может на него не подействовать. Если мы хотим завершить контейнер немедленно, можно подать команду docker container kill, которая очень похожа на docker container stop: $ docker container start 092c5dc85044 092c5dc85044 $ docker container kill 092c5dc85044 092c5dc85044 Команда docker container ls показывает, что контейнер больше не выполняется: $ docker container ls CONTAINER ID IМAGE СОММАND CREATED STATUS PORTS NAМES Если контейнер завершен принудительно, его все равно можно снова запустить обычной командой docker container start. Кроме stop и kill доступны и другие сиг­ налы. Как и команда Linux kill, docker container kill поддерживает все сигналы Unix. Допустим, мы хотим послать контейнеру сигнал usю, чтобы он повторно под­ ключился к удаленному сеансу журналирования. Это можно сделать следующим образом: $ docker container start 092c5dc85044 092c5dc85044 $ docker container kill --signal=USRl 092c5dc85044 092c5dc85044 Если процесс контейнера знает, как реагировать на сигнал usю, он отреагирует соответствующим образом. Таким способом можно послать контейнеру любой стандартный сигнал Unix. Приостановка и возобновление контейнера У нас могут быть разные причины не останавливать контейнер полностью. Напри­ мер, мы хотим приостановить его, сохранить за ним выделенные ресурсы и оста­ вить записи в таблице процессов. Возможно, мы хотим сделать моментальный сииКниги для программистов: https://clcks.ru/books-for-it 134 Глава 5 мок файловой системы, чтобы создать новый образ, или нам нужно ненадолго получить дополнительное процессорное время на хосте. Если вы умеете работать с обычными процессами Unix, то представляете себе, как это действует, ведь кон­ тейнеризированные процессы, по сути, мало чем отличаются от обычных. Когда мы приостанавливаем процесс, cgroup freezer (https://www.kernel.org/doc/ Documentation/cgroup-vl/freezer-subsystem.txt) запрещает планировать его, пока мы не запустим его заново. Контейнер не делает ничего, только поддерживает свое состояние, включая содержимое памяти. В отличие от остановки контейнера сигна­ лом SIGSTOP, при которой процессы знают, что они остановлены, в случае приоста­ новки контейнер не получает информации об изменении состояния. Это важное различие. Некоторые команды Docker используют приостановку и возобновление. Вот как можно приостановить контейнер: $ docker container start 092c5dc85044 092c5dc85044 $ docker container pause 092c5dc85044 092c5dc85044 l,ж � Для приостановки и возобновления контейнеров в Windows необходимо использо­ вать базовую технологию виртуализации, например Нурег-V или WSL2. В списке запущенных контейнеров мы теперь видим, что контейнер Redis имеет статус (Paused) : $ docker container 1s CONTAINER ID IМAGE 092c5dc85044 redis:7.0 STATUS Up 25 seconds (Paused) Мы не сможем использовать контейнер, поставленный на паузу. Он присутствует, но ничего не делает. Давайте возобновим контейнер командой docker container unpause: $ docker container unpause 092c5dc85044 092c5dc85044 $ docker container 1s CONТAINER ID IМAGE 092c5dc85044 redis:7.0 STATUS Up 55 seconds Контейнер снова выполняется, и это отражено в выводе docker container 1s. Мы видим время выполнения - Up 55 seconds, - потому что Docker считает, что кон­ тейнер выполняется, даже когда он приостановлен. Очистка контейнеров и образов Когда мы выполним все команды, чтобы собрать образы, создать контейнеры и запустить их, в системе накопится много слоев образов и папок контейнеров. Книги для программистов: https://clcks.ru/books-for-it Работа с контейнерами 1 135 Мы можем вывести перечень всех контейнеров в системе с помощью команды docker container 1s -а, а затем удалить контейнеры из списка. Прежде чем удалить сам образ, нужно остановить все контейнеры, которые его используют. Удалить контейнер можно командой docker container rm: $ docker container stop 092c5dc85044 092c5dc850441s $ docker container rm 092c5dc85044 092c5dc85044 Мы мо:кем удалить апущенный контейнер с помощью флага -f или --force с ко­ � � мандои docker contaшer пn. � Затем можно вывести все образы в системе: $ docker image 1s REPOSITORY uЬuntu redis spkane/train-os TAG latest 7.0 latest IМAGE ID 5Ьа9dаЬ47459 0256c63af7dЬ 78fЬ082a4d65 CREATED 3 weeks ago 2 weeks ago 4 months ago SIZE 188,ЗМВ 117МВ 254МВ Теперь можно удалить образ и все слои файловой системы: $ docker image rm 0256c63af7dЬ Если мы попробуем удалить образ, используемый контейнером, то получим ошиб­ ку о невозможности уд�ления: "Conflict, cannot delete". Сначала необходимо оста­ новить и удалить контеинер. Иногда, особенно во время разработки, необходимо удалить из системы все образы или контейнеры. Самый простой способ - выполнить команду docker system prune. При этом мы получим предупреждение, что будут удалены все остановленные кон­ тейнеры, все сети, не используемые ни одним контейнером, все несвязанные обра­ зы без тегов и весь кеш сборки: $ docker system prune WARNING! This will remove: - all stopped containers - all networks not used Ьу at least one container - all dangling images - all build cache Are you sure you want to continue? [y/N] у Deleted Containers: cbbc42acfeбcc7c2d5eбc3361003e077478c58bb062dd57a230d3lbcd01f6190 Deleted Images: deleted: sha256:becбec29elбa409aflc556Ьf9e6Ь2ec584c7fb5ffЬfd7c46ec00b30bf untagged: spkane/squid@sha256:64fbc44666405fdla02f0ec73le35881465fac395e7 Total reclaimed space: 1,385ГБ Книги для программистов: https://clcks.ru/books-for-it 136 Глава 5 Чтобы удалить все неиспользуемые образы, а не только несвязанные (dangling), � выполните команду docker system prune -а. - Мы можем достичь тех же целей, задав более конкретные команды. Например, уда­ ляем все контейнеры на хостах Docker: $ docker container rm $(docker container 1s -а -q) Удаляем все образы на хосте Docker: $ docker image rm $(docker images -q) Команды docker container 1s и docker images поддерживают аргумент filter, который позволяет удалять конкретные контейнеры и образы. Например, с помощью фильтра можно удалить все контейнеры с кодом выхода, отличным от нуля: $ docker container rm $(docker container 1s -а -q --filter 'exited!=O') Также мы можем удалить все образы без тегов: $ docker image rm $(docker images -q -f "dangling=true") � Подробнее о фильтрах см. в официальной документации Docker (https:1/docs. docker.com/engine/reference/commandline/ps/#filtering). Пока фильтров не так много, но со временем их станет больше. Мы можем создавать очень сложные фильтры, объединяя команды с помощью вертикальной черты ( 1) и применяя другие подобные методы. В рабочих системах, где постоянно происходят развертывания, накапливается мно­ го старых и неиспользуемых образов, занимающих место на диске. Напишите скриm, чтобы команда docker system prune выполнялась по расписанию (например, через cron или с помощью таймера systemd). Контейнеры Windows До этого мы рассматривали исключительно команды Docker для контейнеров Linux, потому что это самый распространенный сценарий, который работает на всех платформах Docker. Однако с 2016 года Microsoft Windows поддерживает кон­ тейнеры Windows с нативными приложениями Windows, и ими можно управлять с помощью обычных команд Docker. В этой книге мы не будем подробно останавливаться на контейнерах Windows, по­ скольку они составляют лишь малую часть всех контейнеров в рабочих системах и не полностью интегрируются в экосистему Docker из-за специфических образов. Однако они набирают популярность, так что мы кратко рассмотрим их. По сути, не считая содержимого контейнеров, остальное работает так же, как в контейнерах Linux. В этом разделе мы рассмотрим небольшой пример того, как запустить кон­ тейнер Windows на Windows 1 О и выше с Hyper-V и Docker. Книги для программистов: https://clcks.ru/books-for-it Работа с контейнерами 137 Вам потребуется Docker Desktop на совместимом 64-разрядном выпуске Windows 10 или более поздней версии. � • Сначала нужно переключить Docker с контейнеров Linux на контейнеры Windows. Для этого щелкните правой кнопкой мыши по значку Docker с изображением кита на панели задач и выберите пункт Switch to Windows Containers... (Переключиться на контейнеры Windows), а затем подтвердите переход (рис. 5.1 и 5.2). 8 Docker Desktop is running ii Dashboard ..:. spkane Settings Ctrl+Comma Check for Updates TrouЫeshoot Switch to Windo,vs containers... AЬout Docker Desktop Documentation Quick Start Guide Docker Hub • Extensions 11 Pause ► л е С?Ф>) Рис. 5.1. Переключение на контейнеры Windows х А Switch to Windows containers Switch to Windows containers You are about to switch to Windows containers. Existing containers will continue to run, but you will not Ье аЫе to manage them until you switch back to linux containers. No data will Ье lost otherwise. Do you want to continue? О Don't show this message again Switch J i Cancel J Рис. 5.2. Переключение на контейнеры Windows - подтверждение Книги для программистов: https://clcks.ru/books-for-it 138 Глава 5 Процесс может занять некоторое время, хотя обычно это происходит почти момен­ тально. К сожалеюnо, уведомление о переходе мы не получим. Если мы еще раз щелкнем по значку Docker, то увидим теперь вариант Switch to Linux Containers... (Переключиться на контейнеры Linux). Если в первый раз при нажатии на значок Docker отображалась надпись Switch to Linux Containers... (Переключиться на контейнеры Linux), значит, Docker уже был готов к работе с контейнерами Windows. Мы можем протестировать простой контейнер Windows, открыв PowerShell4 и по­ пытавшись выполнить следующую команду: PS С:\> docker container run --rm -it mcr.microsoft.com/ powershell ' pwsh -command ' 'Write-Host "Hello World from Windows '($IsWindows') "' Hello World from Windows (True) При этом загрузится и запустится базовый контейнер для PowerShell (https://hub.docker.com/_/microsoft-powershell), а затем скрипт выведет на экран Hello World from Windows (True). Если вы видите Hello World from Windows (false), значит, вы не перешли на кон­ � тейнеры Windows или выполняете команду не на платформе Windows. � Чтобы собрать образ контейнера Windows, который будет выполнять примерно ту же задачу, можно создать следующий Dockerfile: # escape= ' FIO-f mcr.microsoft.com/powershell SНELL [ "p wsh", "-command"] RUN Add-Content C:\helloworld.psl ' 'Write-Host "Hello World from Windows"' СИ> ["pwsh", "C:\\helloworld.psl"] При сборке этого Dockerfile образ будет основан на mcr.microsoft.com/powershell. Docker создаст небольшой скрипт PowerShell, а затем настроит образ так, чтобы он выполнял скрипт при запуске контейнера из образа. Возможно, вы заметили, что пришлось экранировать обратную косую черту (\) та­ ким же символом в строке смо в файле Dockerfile. Дело в том, что Docker основан на Unix, а в командных оболочках Unix у этого символа есть собственное значение. И хотя мы изменили символ экранирования (https://docs.docker.com/engine/ reference/builder/#escape) для Dockerfile, чтобы он соответствовал параметрам PowerShell по умолчанию (задается с помощью директивы SHELL - https://docs. docker.com/engine/reference/builder/#shell-form-entrypoint-example), нам все же нужно экранировать обратную косую черту, чтобы Docker правильно восприни­ мал ее. 4 Полный URL: https:/Лeaгn.micгosoft.com/en-us/powersbelVscгipting/oveгview?view=poweгshell7 .З&viewFallback Fгom=poweгshell-6. Книги для программистов: https://clcks.ru/books-for-it Работа с контейнерами 139 Если мы соберем этот Dockerfile, то получим примерно следующее: PS С:\> docker image build -t windows-helloworld:latest Sending build context to Docker daemon 2.04ВkВ Step 1/4 : FROМ mcr.microsoft.com/powershell ---> 7d8f82lc04eb Step 2/4 : SHELL ["pwsh", "-coпroand"] ---> Using cache ---> 1987fЬ489a3d Step 3/4 : RUN Add-Content C:\helloworld.psl 'Write-Host "Hello World from Windows"' ---> Using cache ---> 37df47d57bfl Step 4/4 : CMD ["pwsh", "C:\\helloworld.psl"] ---> Using cache ---> 03046ff62Be4 Successfully built 03046ff62Be4 Successfully tagged windows-helloworld:latest Если мы выполним получившийся образ, то увидим следующее: PS С:\> docker container run --rm -ti windows-helloworld:latest Hello World from Windows Microsoft предлагает хорошую документацию по контейнерам Windows5, где можно найти пример сборки контейнера, выполняющего приложение .NЕГ. На платформе Windows мы можем достичь улучшенной изоляции для контейнера, если запустим его внутри специальной и очень легкой виртуальной машины Нурег-V. Это легко можно сделать, добавив параметр --isolation=hyperv к командам docker container create и docker container run. В результате немного изменится произво­ дительность и потребление ресурсов, зато изоляция контейнеров серьезно улучшится. Подробнее читайте в документации (https://learn.microsoft.com/en-us/ virtualization/windowscontainers/manage-containers/hyperv-container). Даже если вы планируете в основном работать с контейнерами Windows, для изу­ чения примеров в оставшейся части книги переключитесь обратно на контейнеры Linнx, чтобы все работало правильно. Когда закончите читать книгу и будете гото­ вы к сборке собственных контейнеров, вы всегда сможете переключиться обратно. Чтобы перейти на контейнеры Linux, щелкните правой кнопкой мыши на значке Docker и выберите пункт Switch to Linux Containers... (Переключиться на ·контей­ неры Linux). 5 Полнъ1й URL: https:/Лearn.microsoft.com/en-us/virtualization/windowscontainers/about. 6 Полный URL: https:/Лearn.microsoft.com/en-us/virtualization/windowscontainers/quick-start/Ьuilding­ sample-app. Книги для программистов: https://clcks.ru/books-for-it 140 1 Глава 5 Заключение В следующей главе мы продолжим рассматривать возможности Docker. Рекомен­ дуем вам поэкспериментировать самостоятельно - попробуйте выполнить коман­ ды, которые мы рассмотрели, чтобы изучить параметры командной строки и при­ выкнуть к синтаксису. Попробуйте создать и собрать небольшой образ, а потом запустить его в виде контейнера. Когда будете готовы к дальнейшему изучению, переходите к главе 6. Книги для программистов: https://clcks.ru/books-for-it ГЛАВА& Сбор информации в Docker У вас уже есть опыт работы с контейнерами и образами. Давайте теперь познако­ мимся с другими возможностями Docker. В этой главе мы снова будем с помощью инструмента командной строки Docker отправлять команды активному серверу dockerd, а также рассмотрим другие базовые команды. Docker предлагает команды для упрощения разных задач: ♦ просмотр версии Docker; ♦ просмотр информации о сервере; ♦ загрузка обновлений образов; ♦ изучение контейнеров; ♦ вход в работающий контейнер; ♦ возврат результата; ♦ просмотр журналов; ♦ мониторинг статистики и т. д. Давайте выясним, как выполнять эти задачи, а также использовать другие инстру­ менты от сообщества, которые расширяют встроенные возможности Docker. Просмотр версии Docker Если вы выполняли практическую часть в предыдущей главе, значит, уже работали с демоном Docker на сервере Linux или виртуальной машине и запускали базовый контейнер, чтобы убедиться, что все работает. Если вы пока не настроили систему и хотите работать с примерами в оставшейся части книги, выполните действия по установке из главы 3. Самое простое, что можно сделать в Docker, - посмотреть версии различных ком­ понентов. Может показаться, что это не слишком важная информация, но Docker состоит из множества компонентов, версии которых определяют доступные нам возможности. Кроме того, нам нужно знать точную версию, чтобы решать некото­ рые проблемы с подключением между клиентом и сервером. Например, если кли­ ент Docker выдает непонятное сообщение о несоответствии версий API, мы сможем сопоставить их с версиями Docker и определить, какой компонент нуждается в об­ новлении. Эта команда обращается к удаленному серверу Docker, так что если кли­ ент по какой-то причине не может подключиться к серверу, он сообщит об ошибке Книги для программистов: https://clcks.ru/books-for-it 142 Глава б и выдаст только информацию о версии клиента. Если возникли проблемы с под­ ключением, изучите инструкции, приведенные в предыдущей главе. Мы можем напрямую войти на сервер Docker и выполнять команды docker из обо­ лочки на сервере, если пытаемся устранить неисправность или просто не хотим использовать клиент Docker для подключения к удаленной системе. На большин­ стве серверов Docker для этого потребуются привилегии root или членство в груп­ пе docker, чтобы можно было подключиться к uпiх-сокету, на котором Docker ожидает данные. Поскольку мы установили все компоненты Docker одновременно, команда docker version покажет, что все версии совпадают: $ docker version Client: Cloud integration: vl.0.24 20.10.17 Version: 1.41 АР! version: gol.17.11 Go version: 100с701 Git corrnni t: Mon Jun 6 23:04:45 2022 Built: darwin/amd64 OS/Arch: default Context: Experimental: true Server: Docker Desktop 4.10.1 (82475) Engine: 20.10.17 Version: 1.41 (minimum version 1.12) АР! version: gol.17.11 Go version: а89Ь842 Git corrnnit: Mon Jun 6 23:01:23 2022 Built: linux/amd64 OS/Arch: false containerd: Experimental: Version: 1.6.6 10c12954828e7c7c9b6e0ea9b0c02b01407d3ael GitCorrnnit: runc: 1.1.2 Version: vl.1.2 0-ga916309 GitCorrnnit: docker-init: Version: 0.19.0 GitCorrnnit: de40ad0 Обратите внимание, что клиент и сервер находятся в разных разделах. Здесь версии клиента и сервера сочетаются, потому что мы установили их вместе. Но так бывает не всегда. В рабочих системах придется внимательно следить за тем, чтобы везде была установлена одна версия. При этом в средах разработки и системах сборки версии часто немного различаются. Книги для программистов: https://clcks.ru/books-for-it Сбор информации в Docker 143 Библиотеки и клиенты API обычно поддерживают разные версии Docker, но это зависит от версии API. В разделе server указано, что текущая версия API 1.41, а минимальная - 1.12. Это полезная информация при работе со сторонними клиен­ тами, и теперь вы знаете, как получить эти сведения. Информация о сервере Через клиент Docker мы можем многое узнать о сервере Docker. Например, бэкенд файловой системы сервера Docker, версию ядра, операционную систему, установ­ ленные плагины, среду выполнения, количество сохраненных контейнеров и обра­ зов. Позже мы поговорим о том, что все это значит. Команда docker system info выведет примерно следующие данные, которые здесь приводятся в сокращенном виде: $ docker system info Client: Plugins: buildx: Docker Buildx (Docker Inc., v0.8.2) compose: Docker Compose (Docker Inc., v2.6.1) extension: Manages Docker extensions (Docker Inc., v0.2.7) sbom: View the packaged-based Software Bill Of Materials (SВОМ) scan: Docker Scan (Docker Inc., v0.17.0) Server: Containers: 11 Images: 6 Server Version: 20.10.17 Storage Driver: overlay2 Plugins: Volume: local Network: bridge host ipvlan macvlan null overlay Log: awslogs fluentd gcplogs gelf journald json-file local logentries Runtimes: io.containerd.runc.v2 io.containerd.runtime.vl.linux runc Default Runtime: runc Kernel Version: 5.10.104-linuxkit Operating System: Docker Desktop OSType: linux Architecture: хВб 64 У вас вывод может быть другим - все зависит от вашего демона Docker. Это нор­ мально, данные приводятся здесь только для примера. Мы видим, что сервер - это выпуск Docker Desktop с ядром Linux 5.10.10 4 и драйвером файловой системы overlay2. На сервере есть несколько образов и контейнеров. Если Docker только что установлен, здесь будет число О. Книги для программистов: https://clcks.ru/books-for-it 144 1 Глава б Обратите внимание на информацию о плагинах. Мы видим все, что поддерживает эта установка Docker. Примерно так будет выглядеть этот раздел в только что уста­ новленном Docker, в зависимости от того, какие новые плагины сейчас распростра­ няются с дистрибутивом. По сути, Docker представляет собой множество плагинов, работающих вместе. Это удобно, потому что мы можем устанавливать другие плагины, созданные сообществом. Нам нужна возможность просматривать, какие плагины установлены хотя бы для того, чтобы убедиться, что Docker распознает недавно добавленные плагины. Обычно образы и контейнеры сохраняются в корневом каталоге по умолчанию /varЛiЬ/docker. Если вы хотите это изменить, в скриптах запуска демона укажите на новое расположение с помощью аргумента --data-root. Для тестирования выполните следующую команду: $ sudo dockerd \ -Н unix:///var/run/docker.sock \ --data-root="/data/docker" 1 По умолчанию файл конфиrурации для сервера Docker находится в /etc/docker/ daemon.json. Для большинства арrументов, которые мы передаем dockerd, в этом файле можно задать постоянные значения. Если вы работаете с Docker Desktop, редактируйте файл в пользовательском интерфейсе. Мы видим, что установлены три среды выполнения (мы еще поговорим о них поз­ же). runc- среда выполнения Docker по умолчанию. Говоря о контейнерах Linux, обычно мы представляем контейнеры, собираемые в runc. На этом сервере также установлены среды выполнения io. containerd . runc.v2 и io. containerd . runtime. vl.linux. В главе 11 мы рассмотрим и другие варианты. Загрузка обновлений образов В следующих примерах мы будем использовать базовый образ UЬuntu. Если вы уже загружали базовый образ uЬuntu: latest, то можете снова выполнить команду pull, чтобы автоматически получить обновления, которые были опубликованы с момен­ та последней загрузки. Это возможно благодаря тому, что тег latest по соглашению представляет послед­ нюю сборку контейнера. Правда, у такого подхода есть недостаток, поскольку тег не привязан к конкретному образу и может обозначать разные образы в разных час­ тях проекта. Одни программисты помечают им последние стабильные выпуски, другие - последнюю сборку, выданную системой CI/CD, третьи совсем не исполь­ зуют тег latest. Однако он популярен и особенно полезен в подготовительных (preproduction) средах, где удобство перевешивает невозможность узнать, какая действительно версия актуальна. 1 Полный URL: https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file. Книги для программистов: https://clcks.ru/books-for-it Сбор информации в Docker 145 Вызов docker image pull вьщает примерно следующий результат: $ docker image pull uЬuntu:latest latest: Pulling from library/uЬuntu 405f018f9dld: Pull coпplete Digest: sha256:bбb83d3c331794420340093eb706aбf152d9clfa51b262d9bf34594887c2c7ac Status: Downloaded newer image for uЬuntu:latest docker.io/library/uЬuntu:latest Здесь показаны только слои, которые изменились с последнего выполнения коман­ ды. Список может быть длиннее или короче или вовсе пустым - зависит от того, когда вы в последний раз скачивали образ, какие изменения с тех пор бьши отправ­ лены в реестр и сколько слоев содержит целевой образ. Даже если вы скачали образ с тегом latest, Docker не будет автоматически обнов­ лять локальный образ. Это придется делать вручную. Правда, если мы разверты­ ваем образ на основе более новой копии uЬuntu:latest, клиент Docker загрузит недостающие слои при развертывании, как и можно было ожидать. Так ведет себя клиент Docker - другие библиотеки или API могут функционировать иначе. Рекомендуется всегда развертывать в рабочей среде образы, помеченные тегом с ука­ занием фиксированной версии, а не тегом latest. В результате мы сможем гаран­ тированно получать ожидаемую версию, без сюрпризов. К элементам реестра можно обращаться не только по тегу с обозначением послед­ ней версии (latest) или конкретного номера версии, но и по содержимому, напри­ мер: sha256:bбb83d3c331794420340093eb706aбf152d9clfa51b262d9bf34594887c2c7ac Данный тег содержит хешированную сумму содержимого образа, и это самый точ­ ный идентификатор для образов Docker, применение которого гарантирует, что мы получаем ожидаемую версию, - в отличие от тега с версией, этот тег нельзя пере­ местить. Синтаксис извлечения образов по такому тегу почти такой же, не считая символа@ : $ docker image pull uЬuntu@sha256:b6Ь83d3c331794420340093eb706aбf152d ... В отличие от большинства команд Docker, в которых хеш можно сократить, хеши SНА-256 остаются без изменений - мы всегда используем полный вариант. Изучение контейнера С помощью docker мы можем посмотреть конфигурацию контейнера, даже если он еще не запущен. Это полезно для отладки и для других ситуаций, когда нам нужно больше информации. Давайте запустим контейнер: $ docker container run --rm -d -t uЬuntu /bin/bash Зc4f916619a5dfc420396d823b42e8bd30a2f94aЬ5bOf42f052357a68a67309b Посмотреть все выполняющиеся контейнеры можно с помощью команды docker container 1s. Так мы убедимся, что все работает, как ожидалось, и сможем скопиро­ вать идентификатор контейнера: Книги для программистов: https://clcks.ru/books-for-it 146 Глава б $ docker container 1s CONTAINER ID IМAGE СОММАND Зс4f916619а5 uЬuntu:latest "/Ьin/bash" STATUS Up 31 seconds NAМES angry_mestorf В этом случае идентификатор контейнера - Зс4f916619а5. Мы также можем обра­ щаться к контейнеру по динамически назначенному имени angry_mestorf, но многим инструментам требуется именно идентификатор, так что следует всегда сначала смотреть на него. Как мы уже говорили, здесь приводится короткая версия иденти­ фикатора, но для Docker это не имеет значения. Как и во многих системах контроля версий, этот хеш представляет собой префикс гораздо более длинного хеша. Ядро идентифицирует контейнер по 64-байтовому хешу, но людям это неудобно, так что Docker поддерживает укороченный хеш. Команда docker container inspect выдает очень подробный вывод. В следующем блоке кода мы сократим его до нескольких важных значений. Изучите полный вы­ вод и найдите то, что вас интересует: $ docker container inspect Зс4f916619а5 [{ "Id": "3c4f916619a5dfc420396d823b42e8Ьd30a2f94ab5b0f42f052357а68а67309Ь", "Created": "2022-07-17Tl 7:26:53.611762541Z", "Args": [], "Image": "sha256:27941809078cc9b2802deb2b0bbбfeedбc ... 7f200e24653533701ee", "Config": { "Hostname": "Зс4f916619а5", "Env": [ "PAТH;/usr/local/sЬin:/usr/local/Ьin:/usr/sЬin:/usr/Ьin:/sЬin:/Ьin" ] 1 "Cmd": [ ], "/Ьin/bash" "Image": "uЬuntu", ), }] Обратите внимание на длинную строку "Id". Это полный уникальный идентифика­ тор контейнера. ·к счастью, нам доступна короткая версия, хотя и ее не назовешь удобной. Можно заметить, что время создания контейнера здесь указано гораздо точнее, чем в выводе команды docker container 1s. Мы видим еще несколько интересных моментов: команду верхнего уровня в кон­ тейнере; окружение, переданное при его создании; образ, на котором он основан; Книги для программистов: https://clcks.ru/books-for-it Сбор информации в Docker 1 147 и имя хоста внутри контейнера. Все эти параметры можно при необходимости на­ страивать при создании контейнера. Например, обычно мы передаем конфигура­ цию через переменные среды, и в выводе команды docker container inspect можно найти много полезной информации при отладке. Вы можете теперь остановить выполняющийся контейнер командой docker container stop Зс4f916619а5. Изучение командной �болочки Давайте запустим контейнер с помощью интерактивной командной оболочки Ьash, чтоб�• заглянуrь внуrрь·. Для этого, как и раньше, выполним команду: $ docker container run --rm -it uЬuntu:22.04 /bin/bash Запустится контейнер uьuntu 22. 04 1тs с командной оболочкой ьash в качестве про­ цесса верхнего уровня. Указав тег 22. 04, мы гарантированно получаем конкретную версию образа. Какие процессы будут выпоJ?IЯться при запуске контейнера? root@35fdlad27228:/И ps -ef UID PID PPID С STIME ТТУ ТIМЕ CMD О 17:45 pts/0 root 1 о 00:00:00 /bin/bash О 17:47 pts/0 9 1 00:00:00 ps -ef root Не так уж много? Оказывается, когда мы велели docker запустить bash, мы только bash и получили. Мы находимся внутри целого образа с дистрибутивом Linux, но никакие процессы не запускаются автоматически. Мы получили только то, о чем попросили. Всегда помните об этом. По умолчанию контейнеры Linux ничего не запускают в фоновом режиме, как это делают виртуальные машины. Контейнеры гораздо легче и не запускают систему init. Конечно, при желании мы можем запустить целую систему init или систему tini init (https://github.com/krallin/tini), встроенную в Docker, но это необходимо сделать вручную. Мы подробнее поговорим об этом в главе 7. Итак, мы запустили в контейнере командную оболочку. Попробуйте выполнить несколько команд и посмотреть, какую еще интересную информацию можно найти. Набор команд ограничен, но мы находимся в базовом образе UЬuntu, так что можем это исправить, выполнив команды apt-get update, а затем apt-get install для загрузки пакетов. Правда, эти приложения будут работать, только пока контейнер существу­ ет, - мы меняем верхний слой контейнера, а не базовый образ; Контейнеры "эфе­ мерны" по своей природе - они существуют некоторое время, а после завершения работы их содержимое не сохраняется. Закончив изучение контейнера, не забудьте выйти (exit) из командной оболочки, чтобы завершить работу контейнера: root@35fdlad27228:/И exit Книги для программистов: https://clcks.ru/books-for-it 148 Глава 6 Возврат результата Представьте, насколько неэффективно запускать целую виртуальную машину, что­ бы выполнить одну команду и получить результат. Обычно мы так не делаем, по­ тому что потребуется много времени и придется запускать операционную систему ради одной команды. Контейнеры Docker и Linux работают не так, как виртуальные машины: контейнеры очень легкие и запускаются гораздо быстрее, чем операцион­ ная система. Контейнеры Linux идеально подходят для того, чтобы выполнить не­ большое фоновое задание и дождаться кода завершения. Представьте, что это спо­ соб получить удаленный доступ к контейнеризированной системе и к отдельным командам внутри контейнера с возможностью передавать данные в обе стороны и возвращать коды выхода. Это удобно в самых разных сценариях. Например, мы можем удаленно проверять работоспособность системы или запускать процессы на нескольких компьютерах через Docker для обработки рабочей нагрузки и получения результата. Инструмен­ ты командной строки Docker отправляют результаты на локальный компьютер. Если выполнить удаленную команду на переднем плане и не указать иное, docker перенаправит поток stdin в удаленный процесс, а потоки stdout и stderr - в терми­ нал. Единственное, что мы должны сделать для этого, - запустить команду на переднем плане и не выделять ТТУ на удаленном компьютере. Кроме того, это конфигурация по умолчанию: никаких параметров командной строки указывать не требуется. При выполнении этих команд Docker создает новый контейнер, выполняет коман­ ду, запрошенную в пространствах имен и cgroup контейнера, удаляет контейнер, а затем завершает работу, чтобы ничего больше не выполнялось и не занимало лиш­ нее место на диске между вызовами. Следующий код должен дать представление о доступных возможностях: $ docker container run --rm uЬuntu:22.04 /bin/false $ echo $? 1 $ docker container run --rm uЬuntu:22.04 /bin/true $ echo $? о $ docker container run --rm uЬuntu:22.04 /bin/cat /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin $ docker container run --rm uЬuntu:22.04 /bin/cat /etc/passwd I wc -1 19 Здесь мы выполнили /bin/false на удаленном сервере, и эта команда всегда завер­ шается с кодом 1. Обратите внимание, как ctocker отправил результат на локальный терминал. Чтобы убедиться, что другие результаты тоже возвращаются, мы выпол­ нили /Ьin/true, который всегда возвращает о, и мы этот ноль получили. Книги для программистов: https://clcks.ru/books-for-it т Сбор информации в Docker 1 149 Затем мы попросили docker выполнить cat /etc/passwd на удаленном контейнере. Мы получили содержимое файла /etc/passwd, который находится в файловой системе контейнера. Поскольку это обычный вывод в stdout, мы можем передавать его в ло­ кальные команды. Предыдущий код передает вывод в локальную команду wc, а не в команду wc в кон­ тейнере. Сам символ вертикальной черты не передается. Если вы хотите передать на сервер всю команду вместе с этими символами, нужно вызвать полную команд­ ную оболочку на удаленной стороне и передать команду в кавычках, например bash -с "<команда> 1 <что-то еще>". В предыдущем фрагменте кода получилось бы docker container run uЬuntu:22.04 /Ьin/ bash -с " /Ьin/cat /etc/passwd I wc -1". Что происходит в контейнере Мы можем легко запустить командную оболочку в новом контейнере на базе почти любого образа, как мы сделали ранее командой docker container run. Но это не то же самое, что запустить новую командную оболочку внутри существующего контей­ нера, в котором активно выполняется приложение. Каждая команда docker container run запускает новый контейнер, так что если в существующем контейнере выполня­ ется приложение и нам нужно отладить его внутри контейнера, потребуется другая команда. Команда docker container ехес запускает новый интерактивный процесс в контейнере и относится к Docker. Есть похожая команда, но со стороны Linux, - nsenter. В следующем разделе мы рассмотрим docker container ехес, а об nsenter поговорим позже, в разделе "nsenter" главы 11. Этот подход пригодится при разработке, пока мы активно создаем и тестируем приложение. Механизм "контейнеры в среде развертывания" (https:// containers.dev/) используют в интегрированных средах разработки, вроде Visual Studio Code (https://code.visualstudio.com/docs/devcontainers/containers). В рабочей среде не рекомендуется подключаться через SSH к рабочим серверам, а этот метод действует почти так же, но иногда нам необходимо посмотреть, что происходит в среде, и в таких случаях эта команда очень пригодится. docker container ехес Давайте рассмотрим самый простой и эффективный способ заглянуть в работаю­ щий контейнер. Сервер dockerd и инструмент командной строки docker поддержива­ ют удаленное выполнение нового процесса в работающем контейнере с помощью команды docker container ехес. Давайте запустим контейнер в фоновом режиме, а затем войдем в него командой docker container ехес и вызовем командную оболочку. Мы не обязаны вызывать именно оболочку - можно выполнять в контейнере от­ дельные команды и просматривать результаты снаружи с помощью docker container ехес. Но если мы хотим заглянуть внутрь контейнера, проще всего это сделать с по­ мощью командной оболочки. Книги для программистов: https://clcks.ru/books-for-it Глава 6 150 Для выполнения docker container ехес нам потребуется идентификатор контейнера. В этом примере давайте создадим контейнер, который выполняет команду sleep в течение 600 с: $ docker container run -d --rm uЬuntu:22.04 sleep 600 9f09ac4bcaa0f201e31895Ы5b479d2c82c30387cf2c8a46e487908d9c285eff Сокращенный идентификатор этого контейнера - 9f09ac4bcaao. Этот идентифика­ тор можно указать в команде docker container ехес. Командная строка будет очень похожа на docker container run, и это неудивительно. Мы запрашиваем интерактив­ ный сеанс и псевдо-ТТУ с помощью флагов -i и -t: $ docker container ехес -it 9f09ac4bcaa0 /bin/bash root@9f09ac4bcaa0:/# Итак, у нас есть командная строка с идентификатором контейнера, внутри которого мы работаем. Это удобно, потому что мы видим, где находимся. Теперь можно выполнить стандартную команду Linux ps, чтобы посмотреть, что еще выполняется в контейнере. Мы видим, что при первом запуске контейнера создается процесс sleep: root@9f09ac4bcaa0:/# ps -ef PID UID 1 7 15 root root root PPID О о 7 С STIME ТТУ О 20:22? О 20:23 pts/0 О 20:23 pts/0 TIME CMD 00:00:00 sleep 600 00:00:00 /bin/bash 00:00:00 ps -ef Введите exit, чтобы выйти из контейнера, когда закончите. Мы можем запустить дополнительные процессы на фоне с помощью docker container ехес. Можно указать параметр -d, как в docker container run. Если вам - важна воспроизводимость при развертывании образа, дважды подумайте, прежде чем применять такой подход не для отладки. Иначе другие люди должны будут знать, что следует передать в docker container ехес, чтобы получить желаемый результат. Если вам все же необходимо запускать дополнительный процесс, луч­ ше пересоберите образ контейнера с этим процессом. Если вы хотите отправлять сигнал другим программам за пределами контейнера, например, для ротации жур­ налов или перезагрузки конфигурации, лучше использовать команду docker container kill -s <СИГНАЛ>, указав стандартный сигнал Unix, чтобы передать ин­ формацию процессу внутри контейнера. docker volume Docker поддерживает вспомогательную команду volшne, которая позволяет вывести все тома, сохраненные в корневом каталоге, а затем получить дополнительную информацию о них, в том числе о месте их физического хранения на сервере. Такие тома монтируются не с помощью Ьind mount. Это специальные контейнеры данных, которые предоставляют удобный метод сохранения данных. Если мы выполним обычную команду docker, которая монтирует каталог методом Ьind mount, то заметим, что она не создает тома Docker: Книги для программистов: https://clcks.ru/books-for-it Сбор информации в Docker 1 151 $ docker volume ls DRIVER VOLUME NАМЕ $ docker container run --rm -d -v /trnp:/tпp uЬuntu:latest sleep 120 бfc97c50fb888054e2d0lfOa93aЬЗb3dЬ172b2cd402fclcd616858b2b5138857 $ docker volume ls DRIVER VOLUME NАМЕ Мы можем создать том сами командой: $ docker volume create my-data Если вывести все тома, мы увидим примерно следующее: $ docker volume ls DRIVER VOLUME NАМЕ local ·my-data $ docker volume inspect my-data· "CreatedAt": "2022-07-ЗlТlб: 19:42Z", "Driver": · "local", 11 LaЬels 11 : {}, "!oюuntpoint": . "/var/liЬ/docker/volumes/my-data/_data", "Name 11 : "my-data", 110ptions" : {}, "Scope": 11 local 11 Мы можем запустить контейнер с прикрепленным томом данных следующей командой: $ docker container run --rm \ --mount source=my-data,target=/app \ uЬuntu:latest touch /app/my-persistent-data Контейнер создал в томе данных файл и сразу завершил работу. Если теперь мы при�юнтируем этот том к другому контейнеру, то наши данные ни­ куда не пропадут: $ docker container run --rm \ --mount source=my-data,target=/app \ fedora: latest ls -lFa ~/app/my-persistent-d.ata -rw-r--r-- 1 root root О Jul'Зl 16:24 /app/my-persistent-data Наконец, мы можем удалить том, если он больще не нужен: $ docker volume rm my-data my-data .Если мы попытаемся удалить том, используемый контейнером, .даже если этот контейнер не выполняется, то получим ошибку: Error response from daemon: unaЬle to remove volume: remove my�data: volume is in use - [ d0763e6e8d79e55850ald3aЬ2le9d..., 4b40d52978ea5e784eббddca8bc22... ] Книги для программистов: https://clcks.ru/books-for-it 152 1 Глава б Рассмотренные команды позволяют изучить контейнеры во всех деталях. В гла­ ве 11 мы подробно обсудим пространства имен, и вы лучше поймете, как именно эти компоненты сочетаются и взаимодействуют, образуя контейнер. Журналирование Журналирование - важная часть любого приложения в рабочей среде. Когда что­ то идет не так, именно журналы способны помочь нам восстановить работу, поэто­ му важно продумать их ведение. В системах Linux есть несколько способов рабо­ тать с журналами приложений, и некоторые из них эффективнее, чем другие. Если процесс приложения выполняется на нашем сервере, вывод будет поступать в ло­ кальный файл журнала, к которому у нас есть доступ. Кроме того, вывод может записываться в буфер ядра, откуда его можно считать с помощью dmesg. Во многих современных дистрибутивах Llnux с systernd журналы доступны с помощью утили­ ты journalctl. Из-за ограничений контейнеров и структуры Docker при использова­ нии этих способов требуется дополнительная конфигурация. Но это нормально, потому что журналирование прекрасно подцерживается в Docker. В некоторых аспектах Docker упрощает журналирование. Во-первых, он записыва­ ет весь обычный текстовый вывод от приложений в управляемых им контейifерах. Все, что отправляется в потоки stdout и stderr в контейнере, записывается демоном Docker и передается в настраиваемый бэкенд журналирования. Во-вторых, как и многие другие компоненты Docker, это модульная система, к которой можно под­ ключать разные плагины. Давайте рассмотрим основные принципы. docker container logs Начнем с простейшего варианта: механизм журналирования по умолчанию в Docker. У этого механизма есть ограничения, о которых мы поговорим чуть поз­ же, но в большинстве типичных случаев применения это удобный рабочий вариант. Если вы работаете с Docker в среде разработки, скорее всего, другая стратегия журналирования вам не понадобится. Этот метод существовал с самого начала, он всем понятен и хорошо подцерживается. Речь о плагине json-file. Для работы с ним мы задаем команду docker container logs. Как видно по названию, при использовании стандартного плагина json-file демон Docker передает журналы приложений в файл JSON для каждого контейнера. Мы можем в любое время просматривать журналы для любого контейнера. Отобразить записи в журналах можно путем запуска контейнера nginx: $ docker container run --rrn -d --narne nginx-test --rrn nginx:latest А затем: $ docker container logs nginx-test 2022/07/31 16:36:05 [notice] 1#1: using the "epoll" event rnethod 2022/07/31 16:36:05 [notice] 1#1: nginx/1.23.1 Книги для программистов: https://clcks.ru/books-for-it Сбор информации в Docker 153 2022/07/31 16:36:05 [notice] 1#1: built Ьу gcc 10.2.1 20210110 (Debian 10.2.1-6) 2022/07/31 16:36:05 [notice] 1#1: OS: Linux 5.10.104-linuxkit Такой способ удобен, потому что Docker позволяет просматривать журналы уда­ ленно, прямо из командной строки, и это удобно, если в журнал заносится немного данных. Чтобы просмотреть только недавние записи, укажите параметр --since и дату в формате RFC 3339 (например 2002-10-02Т10:ОО:ОО-05:ОО), метку времени Unix (1450071961), стандартную метку времени (20220731) или строку длительности Go (5m45s). Также можно указать --tail и число строк с конца, которое нужно отобразить. Файлы этого журнала хранятся на сервере Docker, по умолчанию в папке /var/liЬ/docker/containers/<id_кoнтeйнepa>/. В файле с именем <id_кoнтeйнepa>­ json.log каждая строка представляет собой JSОN-объект. Мы увидим примерно следующее: {"log":"2022/07/31 16:36:05 [notice] 1#1: using the \"epoll\" event method\n", "stream":"stdout","time":"2022-07-31T16:36:05.189234362Z"} Именно поле log было отправлено в поток stdout. В поле stream мы видим, что это был stdout, а не stderr. В поле time мы видим точное время, когда демон Docker по­ лучил данные. Это нестандартный формат журналирования, но он структурирован, а это удобно, если мы захотим обрабатывать эти записи в дальнейшем. Как и в файле журнала, в журналах Docker мы можем просматривать последние строки по мере их появления с помощью команды docker container logs -f: $ docker container logs -f nginx-test 2022/07/31 16:36:05 [notice] 1#1: start worker process 35 2022/07/31 16:36:05 [notice] 1#1: start worker process 36 2022/07/31 16:36:05 [notice] 1#1: start worker process 37 2022/07/31 16:36:05 [notice] 1#1: start worker process 38 Это похоже на стандартную команду docker container logs, но клиент продолжит ждать новые сообщения и будет отображать их по мере поступления с сервера, как это делает команда Linux tail -f. Нажмите клавиши <Ctrl>+<C>, чтобы в любое время выйти из потока записей журнала: $ docker container stop nginx-test · Указав дополнительные параметры в формате --log-opt tag=" { { . ImageName}} / { {. ID}} ", мы можем изменить тег журнала по умолчанию, с которого будет начи­ наться каждая строка в журнале. По умолчанию записи журнала в Docker помеча­ ются первыми 12 символами идентификатора контейнера. Описанный способ удобен для журналирования на одном хосте. Недостатки такого подхода связаны с ротацией журналов, удаленным доступом к журналам после Книги для программистов: https://clcks.ru/books-for-it 154 Глава б ротации, а также местом на диске, которое занимают большие журналы. Несмотря на то, что он основан на JSОN-файле, этот механизм настолько эффективен, что при желании его можно использовать даже в большинстве приложений в рабочей среде. В сложных средах, правда, потребуется более продуманный подход и централизованное журналирование. Параметры по умолчанию в dockerd в настоящее время не пода.ерживают ротацию журналов. Для рабочей среды укажите параметры --log-opt max-size и --log-opt max-file в командной строке или файле конфиrурации daemon.json. Эти параметры ограничивают максимальный размер файла до ротации и максимальное число хранящихся файлов журнала соответственно, причем max-file не будет работать, если мы не зададим max-size, чтобы указать Docker, когда выполнять ротацию. Если этот параметр включен, механизм docker container logs вернет данные толь­ ко из текущего файла журнала. Расширенные возможности журналирования / Если механизма по умолчанию недостаточно (а в больших системах так и будет), Docker поддерживает настраиваемые бэкенды журналирования. Список доступных плагинов постоянно растет. На момент написания поддерживаются json-file (мы уже упомянули его), а также syslog, fluentd, journald, gelf, awslogs, splunk, gcplogs, local и logentries, которые служат для отправки журналов в популярные фреймворки и сервисы журналирования. Список большой, и самый простой способ для масштабных систем. Docker - на­ прямую отправлять журналы контейнеров в syslog из Docker. Выбрать этот вариант можно с помощью командной строки Docker, указав --log-driver=syslog, или задав его по умолчанию в файле daemon.json для всех контейнеров. Файл daemon.json содержит конфиrурацию для сервера dockerd. Обычно он хра­ нится на сервере в каталоге /etc/docker/. Для Docker Desktop этот файл можно ре­ дактировать в пользовательском интерфейсе, в разделе Preferences (Настрой­ ки)-+ Docker Engine. После внесения изменений в файл перезапустите Docker Desktop или демон dockerd. Кроме того, доступно несколько сторонних плагинов. У них есть свои недостатки, и главный из них - усложнение установки и обслуживания Docker.. Однако, воз­ можно, вы найдете стороннюю реализацию, которая идеально вписывается в вашу систему, а ее достоинства перекрывают неудобства. Некоторые сложности относятся ко всем драйверам журналирования. Например, Docker не позволяет задействовать несколько вариантов одновременно. Вы може­ те применить драйвер syslog или gelf, но не вместе с json-file. Только при ис­ пользовании json-file или journald нам доступна команда docker container logs. Это неожиданный недостаток, о котором обязательно нужно помнить при смене драйвера. Некоторые плагины отправляют журналы на удаленный эндпоинт и хранят локаль­ ную копию JSON для команды docker container logs, но не все плагины пода.ержи­ вают этот подход. У каждого драйвера свои подводные камни, так что приходится Книги для программистов: https://clcks.ru/books-for-it Сбор информации в Docker 1 155 искать компромисс между гарантированной доставкой журналов и риском нару­ шить нормальную работу развертывания Docker. Мы рекомендуем решения на базе UDP и другие неблокирующие варианты. Традиционно в большинстве систем Linux есть получатель системных журналов, например syslog, rsyslog и другие. Этот протокол в разных формах существует уже давно и неплохо поддерживается большинством развертываний. Если компания переходит на Docker с традиционной среды Linux или Unix, скорее всего, у нее уже есть инфраструктура системных журналов, так что обычно это самый простой вариант миграции. В более новых дистрибутивах Linux часто предусмотрена система инициализации Этот systemd и journald в качестве механизма журналирования по умолчанию. драйвер отличается от syslog. И хотя syslog считается традиционным решением, у него есть свои проблемы. Драйвер Docker syslog поддерживает протоколы TLS, ТСР и UDP, и это удобно, но следует проявлять осторожность при потоковой передаче журналов с Docker на удаленный сервер журналов по ТСР или TLS. Проблема в том, что они используют ориентированные на установку соединения ТСР-сессии, и Docker пытается под­ ключиться к удаленному серверу журналирования при запуске контейнера. Если установить соединение не удастся, Docker не сможет запустить контейнер. Если это ваш механизм журналирования по умолчанию, то проблема может возникнуть в любое время в любом развертывании. Это неудобно для рабочих систем, поэтому для журналирования с драйвером syslog рекомендуется использовать UDP. В таком случае, разумеется, журналы не будут зашифрованы, и их доставка не гарантируется. Существуют разные стратегии жур­ налирования, и каждый раз мы ищем компромисс между сохранностью журналов и надежностью системы. Обычно рекомендуется склоняться в сторону надежности, но если вы работаете в защищенной среде аудита, у вас могут быть другие приори­ теты. Записи журнала можно напрямую отправлять из одного контейнера на удаленный сервер, совместимый с syslog, задав параметр журнала syslog-address: --log-opt syslog-address=udp://192.168.42.42:123. Еще одна сложность с плагинами журналирования - большинство из них являют­ ся блокирующими по умолчанию, т. е. обратное давление (back-pressure) может привести к проблемам в приложении. Это поведение можно изменить, задав пара­ метр --log-opt mode=non-Ыocking, а затем установив максимальный размер буфера для журналов: --log-opt max-buffer-size=4m. С такими параметрами заполнение буфера больше не будет блокировать приложение, потому что самые старые записи в жур­ нале будут удаляться. Необходимо найти баланс между надежностью приложения и необходимостью собирать все записи журналов. Книги для программистов: https://clcks.ru/books-for-it 156 Глава 6 Некоторые сторонние библиотеки и программы записывают данные в файловые системы по разным, иногда совершенно неожиданным причинам. Если вы хотите создавать контейнеры, которые не будут напрямую записывать данные в файло­ вую систему, задайте параметры --read-only и --mount type=tnq:>fs у команды docker container run, как мы обсуждали в главе 4. Не рекомендуется записывать журналы в сам контейнер - до них будет сложно добраться, они не сохранятся после за­ вершения работы контейнера и могут нарушить стабильную работу файловой сис­ темы Docker. Мониторинг в Docker Одно из самых важных требований к рабочим системам - наблюдаемость (obser­ vaЬility) и измеряемость. Если мы не видим, как ведет себя рабочая система, мы вряд ли достигнем желаемого результата с ее помощью. В современных средах не­ обходимо отслеживать все значимые показатели и собирать как можно больш� по­ лезной статистики. Docker поддерживает проверку работоспособности контейнеров и базовые функции отчетности с помощью docker container stats и docker system events. Сначала мы рассмотрим их, затем решение, предложенное Google, которое представляет выходные данные в наглядном виде, и, наконец, экспериментальную функцию Docker, которая экспортирует метрики контейнера в систему мониторин­ га Prometheus. Статистика контейнера Начнем с инструментов CLI, которые поставляются с Docker. У Docker CLI есть эндпоинт для просмотра важной статистики по выполняющимся контейнерам. Через этот эндпоинт инструмент командной строки может каждые несколько секунд передавать базовую статистическую информацию о том, что происходит в контейнерах. Команда docker container stats, как и команда Linux top, подключает­ ся к терминалу и обновляет строки на экране, отображая текущую информацию. Сложно показать этот процесс в печатном виде, но мы приведем пример. По умол­ чанию сведения обновляются каждые несколько секунд. Статистика в командной строке Запускаем контейнер: $ docker container run --rm -d --name stress \ docker.io/spkane/train-os:latest \ stress -v --cpu 2 --io 1 --vm 2 --vm-bytes 128М --timeout 60s Затем выполняем команду stats, чтобы начать следить за новым контейнером: $ docker container stats stress CONTAINER ID NАМЕ CPU % МЕМ USAGE/LIMIT МЕМ % NET I/O la9f52f0855f stress 476.50% 36.09MiB/7.773GiB 0 .45% 1.0SkВ/0B PIDS BLOCK I/O 0В/0В 6 Нажмите клавиши <Ctrl>+<C>, чтобы в любое время выйти из потока stats. Книги для программистов: https://clcks.ru/books-for-it Сбор информации в Docker 1 157 Укажите параметр --no-stream, чтобы получить статистику на момент времени, без обновления, и вернуться в командную строку после выполнения команды. Давайте посмотрим, что содержит вывод. У нас есть следующая информация: ♦ идентификатор контейнера без имени; ♦ потребляемый процент ресурсов ЦП, где 100% - это одно целое ядро ЦП; ♦ используемый объем памяти и максимально доступный контейнеру объем памяти; ♦ статистика по сети и блочному вводу-выводу; ♦ число активных процессов в контейнере. Некоторые данные особенно важны для отладки. Давайте посмотрим, как их можно использовать. Обратите внимание на задействованный процент лимита памяти. Слишком строгое ограничение памяти - одна из самых распространенных проблем с контейнерами в рабочей среде, потому что из-за него ООМ Killer в ядре Linux снова и снова оста­ навливает контейнер. Команда stats позволяет обнаруживать и решать такие про­ блемы. Что касается статистики по вводу-выводу, если все наши приложения работают в контейнерах, эти сведения покажут, как движутся данные по системе. До контей­ неров это было очень сложно узнать. При отладке также полезно знать число активных процессов в контейнере. Если приложение порождает дочерние процессы, но не завершает их, они могут бескон­ трольно накапливаться. Одно из преимуществ docker container stats - возможность просматривать стати­ стику по всем контейнерам в единой сводке. Мы увидим много полезной информа­ ции, даже если думаем, что и так все знаем. Более того, данные представлены в удобном для человека формате и доступны в командной строке. Docker API предоставляет еще один эндпоинт, который дает гораздо больше информации, чем отображается в клиенте. В предыдущих главах мы пока не применяли API, но в этом случае через API можно получить настолько больше данных, чем через клиент, что мы посредством curl выполняем запрос к API и смотрим, что делает наш контейнер. Читать эти данные гораздо сложнее, зато там больше деталей. Почти все, что мы делаем через клиент docker, можно сделать через Docker API. � При необходимости мы можем выполнять все эти задачи программным способом. � Ниже в разделе "Эндпоинт API stats" приводится базовая информация о прямом вызове API. Книги для программистов: https://clcks.ru/books-for-it 158 1 Глава б Эндпоинт API, ffats ' ' . \ Эндпоинт /stats/, к которому .мы обратимся в :API, будет передавать статистику, пока мы не закроем подключение. Поскольку, файлы JSON читать неудобно, мы запросим только одну строку и приведем ее в читаемый вид с ·пqм0щью инструмен­ та jq. У нас должен быть установлен jq версии 2.6 и выше. Если он не.установлен, пропустите пересьшку в jq, но тогда вы получите данные в необработанном и не­ удобном формате JSON. Если вы предпочитаете другой инструмент структуриро­ ванного вывода JSON, используйте его.. Обычно демоны Docker устанавливаются с предоставлением API только через unix­ coкeт, а не через. ТСР. Мы будем вызывать\ API. �ерез cu'rl с сервера Docker. Если вы планируете отслеживать этот эндпоинт в рабочей среде; Dock.er API необходимо предоставлять через ТСР-порт. Мы не рекомендуем такой родход; Н() в докумецта/ ции Docker (https://dockr.ly/2Lzuox2) вь1 найдете подробные инструкции. Если вы не находитесь. на сервере Docker иnи используете Oocker л�_каr�ьно, про­ верьте содержимое переменной среды DOCKER_нosт с помощью echo $DOCKER_нosт, чтобы узнать имя хоста или IР-адрес вашего сервера Docker. . . . 1 Сначала запустите контейнер, о котором будете собирать статистику: $ docker container run --rm -d --name stress \ docker.io/spkane/train-os:latest \ stress -v --ери 2 --io 1 --vm 2 --vm-bytes 128М --timeout 60s Контейнер работает, и теперь мы можем получать непрерывный пщ·ок статистики-о нем в формате JSON, используя curl с указанием имени или хеша контейнера.· В след�ющих примера� мы выполняем команду curl к сокету Oock�r, но Ml;,1 можем � обращаться и к riopтy Docker, если он доступен. � $ curl --no-buffer -XGET --unix-socket /var/run/docker.sock \ '. http: //docker/containers/stress/stats Этот поток статистики в формате JSON не остановится �в�оматически: Нажмите · · , . клавиши <Ctrl>+<C>, чтобы остановить его. ' ' . ' \ Мы можем получить статистику на определенный момент: $ curl -s -XGET --unix-socket /varl,run/docker.sock \ http://docker/containers/stress/stats I head -n 1 1 jq . . Наконец, используем утилиту jq (https:fstedolan.github.io/jq) или другой инструмент для преобразования JSON в удобочитаемый вид: $ curl -s -ХGЕТ --unix-socket /yar/run/docker.sock \ http://docker/containers/stress/stats head -n 1 jq I 1 Книги для программистов: https://clcks.ru/books-for-it Сбор информации в Docker 11 read 11 : 159 "2022-07-31Tl7:41:59.10594836Z", "0001-01-0lT00:00:00Z", 11 preread 11 : { 11 pids_stats11 : 11 cu rrent": 6, 18446744073709552000 11 l:imi.t 1 1: }, 1 { _stats "io_service_Ьytes_recursive 11 : 11 : 11 Ыkio 11 major": 254, minor": О, "ор": 11 read 11 , 11 11 value 11 : О }, }, 11 num_procs 11 : О, 11 storage_stats 11 : 11 11 cpu_stats { 11 : {}, cpu_usage 11 : { 11 total_usage 11 : 101883204000, 43818021000, 11 usage_in_usermode 11 : 58065183000 11 usage_in_kerne lmode 1 1: }, }, memory_stats 1 1: 11 usage 11 : 183717888, 11 { 11 stats 11 : 11 active anon 11 : О, 11 active file 11 : О, }, "limit": 8346021888 }, 11 name 11 : "/stress", "id": 11 "9be7c9de26864ac97e07 fc3d8e3f fЬ5bb52cc2ba4 9f569d4ba8d407f87 47851f", networks 11 : { "ethO": { 11 x_Ьytes11 : r 1046, "rx_packets1 1 : 9, Книги для программистов: https://clcks.ru/books-for-it 160 1 Глава б Информации получилось с избытком. Мы сократили вывод, чтобы не занимать слишком много места, но он все равно немалый. Теперь вы видите, сколько данных о каждом контейнере можно получить через API. Мы не будем вдаваться в подроб­ ности, но здесь мы видим очень подробные сведения об использовании памяти, а также блочного ввода-вывода и процессорных ресурсов. Если вы сами занимаетесь мониторингом, изучите возможности этого эндпоинта. Минус в том, что мы видим статистику только по одному контейнеру, и не можем собрать данные обо всех контейнерах с помощью одного вызова. Проверки работоспособности контейнеров Как и остальные приложения, контейнер после запуска может не достичь работо­ способного состояния, в котором он сможет принимать трафик. Иногда в рабочих системах случаются сбои, и тогда приложение теряет работоспособность. Во многих рабочих средах существуют стандартизированные способы провер�ь работоспособность приложений. К сожалению, у нас нет четкого стандарта для всех организаций, и каждая компания делает это по-своему. Зато есть системы мониторинга, которые могут работать в разных производственных системах и упрощают контроль работоспособности. Единый стандарт в этой сфере действи­ тельно не помешал бы. В Docker для упрощения и стандартизации есть свой механизм проверки работо­ способности. Если мы продолжим метафору с контейнерами для морских грузопе­ ревозок, контейнеры Linux должны выглядеть совершенно одинаково независимо от содержимого, и механизм проверок работоспособности Docker не только стан­ дартизирует эти проверки для контейнеров, но и позволяет отделить содержимое контейнера от его "внешнего вида". Это значит, что контейнеры из Docker Hub или других общих репозиториев могут реализовывать стандартизированный механизм проверки работоспособности, и он будет работать в любой среде Docker, предна­ значенной для выполнения контейнеров в рабочей среде. Проверки работоспособности настраиваются во время сборки и создаются с помо­ щью определения НЕАLТНСНЕСК в Dockerfile. Эта директива указывает демону Docker, какую команду он может выполнять в контейнере, чтобы убедиться в его работо­ способном состоянии. Если код выхода команды О, Docker считает контейнер рабо­ тоспособным. Другие коды выхода указывают, что контейнер неработоспособен, и тогда планировщик или система мониторинга могут принять меры. В дальнейших главах мы будем использовать следующий проект для изучения Docker Compose, а сейчас обратим внимание на пример проверок работоспособно­ сти в Docker. Скачайте копию кода и перейдите в каталог rocketchat-hubot­ demo/mongodЬ/docker/: $ git clone https://githuЬ.com/spkane/rocketchat-huЬot-derno.git \ --config core.autocrlf=input $ cd rocketchat-huЬot-derno/mongoc!Ь/docker В этом каталоге находится Dockerfile и скрипт docker-healthcheck. В Dockerfile мы увидим следующее содержимое: Книги для программистов: https://clcks.ru/books-for-it Сбор информации в Docker 1 161 FRCМ docker.io/bitnami/mongodЬ:4.4 # Более новый базовый Dockerfile: # https://githuЬ.com/bitnami/containers/ЬloЬ/ # f9fЬЗf8a6323fЬ768fd488c77d4flllЫ330bdOe/bitnami/mongodЬ # /5.0/debian-11/Dockerfile СОРУ docker-healthcheck /usr/local/bin/ # Полезная информация: # https://docs.docker.com/engine/reference/builder/#healthcheck # https://docs.docker.com/compose/compose-file/#healthcheck НЕАLТНСНЕСК СИ> ["docker-healthcheck"] Файл короткий, потому что основан на базовом образе Mongo2, от которого наш образ наследует много компонентов, включая точку входа, команду по умолчанию и порт для взаимодействия. В начале 2023 года в Bitnami провели рефакторинг репозиториев контейнеров, так что ссылка указывает на более новую версию Dockerfile, предназначенную для MongoDB 5.0. В этом примере используется MongoDB 4.4, но мы все равно сможем понять суть. EXIOSE 27017 EN'l'RYIOINТ [ "/opt/Ьitnami/scripts/mongodЬ/entrypoint.sh" СИ> [ "/opt/Ьitnami/scripts/mongodЬ/run.sh"] Учтите, что Docker перенаправляет трафик на порты контейнера, даже пока кон­ тейнер и базовые процессы все еще запускаются. В наш Dockerfile мы добавляем один скрипт, который проверяет работоспо­ собность контейнера, и определяем команду проверки работоспособности, которая запускает этот скрипт. Соберем контейнер: $ docker image build -t mongo-with-check:4.4 => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 37В 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2В 0.0s => [internal] load metadata for docker.io/Ьitnami/mongodЬ:4.4 0.5s 0.0s 0.0s => CACHED [1/2] FROМ docker.io/Ьitnami/mongodЬ:4.4@sha256:9162... ae209 => [2/2] СОРУ docker-healthcheck /usr/local/Ьin/ 0.0s => exporting to image 0.0s => [internal] load build context => => transferring context: 40В 0.0s 2 Полный URL: https://gitbub.com/Ьitnami/containers/ЫoЬ/f9tЪ3f8a6323fЬ768fd488c77d4fl 11 Ы330Ьd0е/ Ьit nami/mongodЬ/5.0/deblan-11/Dockerf"de. Книги для программистов: https://clcks.ru/books-for-it Глава б 162 => => exporting layers 0.0s => => writing image sha256:aбef ...da808 0.0s => => naming to docker.io/library/mongo-with-check:4.4 0.0s Запустим контейнер и изучим вывод docker container 1s: $ docker container run -d --rm --name mongo-hc mongo-with-check:4.4 5a807c892428ab0641232c82bd477fc8dll42c9el5c27d5946b8bfe7056e2695 $ docker container 1s IМAGE mongo-with-check:4.4 PORTS STATUS Up 1 second (health: starting) 27017/tcp Обратите внимание, что в столбце sтдтus теперь есть раздел health в скобках. При запуске контейнера будет отображаться health: starting (работоспособность: запус­ кается). Мы можем изменить время, в течение которого Docker ждет инициализа­ ции контейнера, с помощью аргумента --health-start-period для команды docker container run. Статус изменится на healthy (работоспособный), когда контейнер окончательно запустится и проверка работоспособности завершится успешно. Для перехода в работоспособное состояние контейнеру может понадобиться более 40 с: $ docker container 1s IМAGE ... mongo-with-check:4.4 STATUS PORTS Up 32 seconds (healthy) 27017/tcp Статус можно запросить напрямую командой docker container inspect: $ docker container inspect --format=' {{.State.Health.Status))' mongo-hc healthy $ docker container inspect --format=' ((json .State.Health)}' mongo-hc I jq 11 Status 11 : 11 "healthy", FailingStreak11 : О, "Log": Если контейнер не пройдет проверку работоспособности, статус изменится на unhealthy (неработоспособный), и нам нужно будет решить эту проблему: $ docker container 1s IМAGE ... mongo-with-check:4.4 STATUS PORTS Up 9 minutes (unhealthy) 27017/tcp Сейчас мы можем остановить контейнер с помощью команды docker container stop mongo-hc. Как и в большинстве систем, мы можем настроить детали проверок работоспособ­ ности, в том числе их частоту (--health-interval), количество повторных попыток до того, как контейнер будет признан неработоспособным (--health-retries) и дру­ гие параметры. Проверку работоспособности можно отключить совсем (--no­ healthcheck). Книги для программистов: https://clcks.ru/books-for-it Сбор информации в Docker 1 163 Это полезная функция, и мы рекомендуем предусмотреть ее во всех контейнерах, чтобы повысить надежность среды и видимость происходящего в ней. Данную возможность легко реализовать, потому что она поддерживается многими плани­ ровщиками и системами мониторинга для рабочей среды. � Полезность проверки работоспособности, конечно, во многом определяется тем, насколько хорошо она написана и насколько точно определяет состояние сервиса. � docker system events Демон dockerd генерирует поток событий, связанных с жизненным циклом контей­ нера. С его помощью различные части системы узнают, что происходит в других частях. Мы можем отслеживать в этом потоке события жизненного цикла, которые происходят с контейнерами на сервере Docker. Для этого, как вы уже могли дога­ даться, есть аргумент в Docker CLI. При выполнении этой команды мы начнем по­ лучать непрерывный поток сообщений. За него будет отвечать долгосрочный НТТР-запрос к Docker API, который возвращает сообщения в виде JSОN-блобов по мере поступления. Инструмент Docker CLI расшифровывает их и выводит данные на терминал. Поток событий позволяет осуществлять мониторинг или запускать другие дейст­ вия, например оповещения о завершении задания. При отладке мы увидим, что контейнер завершился сбоем, даже если Docker потом перезапустил его. Позже мы посмотрим, как напрямую реализовывать инструменты для работы с этим API. В одном терминале выполним команду events: $ docker system events Ничего не произойдет. В другом терминале запустим следующий контейнер с коротким сроком жизни: $ docker container run --rm --name sleeper debian:latest sleep 5 В первом терминале, где выполняется команда events, мы увидим примерно сле­ дующее: ...09:59.606... container create d6... (image=debian:latest, name=sleeper) ...09:59.610... container attach d6... (image= debian:latest, name=sleeper) ...09:59.631 ... network connect еа ... (container= d60b..., name=bridge, type= bridge) ...09:59.827 ... container start d6... (image= deЬian:latest, name= sleeper) ...10:04.854... container die d6... (exitCode= 0, image= deЬian:latest, name=sleeper) ...10:04.907 ... network disconnect еа ... (container=d60b..., name=bridge, type= bridge) ...10:04.922... container destroy d6... (image= deЬian:latest, name=sleeper) Нажмите <Ctrl>+<C>, чтобы в любое время выйти из потока событий. Как и при просмотре статистики в Docker, мы можем изучить системные собы­ тия Docker через curl с помощью команды, вроде curl --no-buffer -XGET --unix­ socket /var/run/docker.sock http://docker/events. Книги для программистов: https://clcks.ru/books-for-it 164 Глава б В этом примере мы запустили временный контейнер, который завершает работу через 5 с. События container create, container attach, network connect и container start - это обя­ зательные этапы запуска контейнера. При завершении работы контейнера в журнал потока событий вносятся записи container die, network disconnect и cbntainer destroy. Каждое событие обозначает этапы завершения контейнера. Docker сообщает иден­ тификатор образа, на котором основан контейнер. Эти данные могут понадобиться, например, для привязки развертываний к событиям, потому что развертывание обычно связано с новым образом. Если на каком-то сервере контейнеры все время отключаются, поток ctocker system events позволит узнать, что и когда происходит. Нам не обязательно следить за со­ бытиями в реальном времени - Docker кеширует их, и мы можем посмотреть их чуть позже. Параметр --since позволяет вывести события, произошедшие после определенного момента, а параметр --until выдает события, произошедшие до определенного момента. Мы можем указать оба параметра, чтобы выбрать период, когда возникла интересующая нас проблема. Параметры используют формат вре­ мени ISO, как в предыдущем примере (например, 2018-02-18Т14:03:31-08:00). Некоторые события следует отслеживать во что бы то ни стало: container oom (в контейнере закончилась память) container ехес create container ехес start container ехес die (кто-то попытался войти в контейнер с помощью docker container ехес, и это может указывать на инцидент безопасности). cAdvisor Команды docker container stats и docker system events выдают данные в текстовом формате, а нам нужны графики, чтобы наглядно отслеживать тренды, и для этого существуют специальные инструменты. Когда вы будете изучать решения для мо­ ниторинга в Docker, вы заметите, что многие популярные инструменты мониторин­ га предоставляют функционал для отслеживания производительности и состояния контейнеров. Помимо коммерческих решений от Datadog, GroundWork, New Relic и других вен­ доров, есть бесплатные варианты с открытым исходным кодом, например Prometheus и даже Nagios. Мы рассмотрим Prometheus далее в этой главе в разделе "Мониторинг с помощью Prometheus". Вскоре после появления Docker Google вы­ пустил собственный инструмент мониторинга контейнеров в виде хорошо поддер­ живаемого опенсорс-проекта на GitHub - cAdvisor (https://github.com/google/ cadvisor). Инструмент cAdvisor можно использовать вне Docker, но сейчас вы уже Книги для программистов: https://clcks.ru/books-for-it Сбор информации в Docker 1 165 вряд ли удивитесь, узнав, что самый простой способ реализовать cAdvisor - запус­ тить его как контейнер Linux. Для установки cAdvisor на большинстве систем Linux достаточно выполнить при­ веденный далее код. & Выполните ЭТ'{ комао,ду напрямую на сервере Linux Docker. В Windows или macDS она работать не будет. $ docker container run \ --volume=/:/rootfs:ro \ --volume=/var/run:/var/run:ro \ --volume=/sys:/sys:ro \ --volume=/var/liЬ/docker/:/var/liЬ/docker:ro \ --volume=/dev/disk/:/dev/disk:ro \ --puЫish=BOB0:8080 \ --detach=true \ --name=cadvisor \ --privileged \ --rm \ --device=/dev/kmsg \ gcr.io/cadvisor/cadvisor:latest UnaЬle to find image 'cadvisor/cadvisor:latest' locally Pulling repository cadvisor/cadvisor f0643dafd7f5: Download complete Ьа9Ьб6За8908: Download complete Status: Downloaded newer image for cadvisor/cadvisor:latest f54eбbc0469f60fd74ddf30770039fla7aa36a5edaбef5100cddd9ad5fda350b В системах на базе Red Hat Enterprise Linux (RHEL) может потребоваться добавить � следующую строку в команду docker container run: --volume=/cgroup:/cgroup \. � Теперь мы сможем перейти к хосту Docker через порт 8080, чтобы открыть веб­ интерфейс cAdvisor (например, http:l/172.17.42.10:8080/), и посмотреть различные графики с информацией о хосте и отдельных контейнерах (рис. 6.1). cAdvisor предоставляет REST АРI-эндпоинт, через который система мониторинга легко может запросить подробную информацию: $ curl http://172.l7.42.l0:8080/api/v2.l/machine/ Подробности о cAdvisor API можно найти в официальной документации (https://github.com/google/cadvisor/ЫoЬ/master/docs/api_v2.md). Подробных сведений, предоставляемых cAdvisor, будет достаточно для получения почти любых необходимых данных мониторинга и составления большинства нуж­ ных графиков. Книги для программистов: https://clcks.ru/books-for-it Глава б 166 Total Uuge j 0.1& f-----+н--,.. 0.12 t---++----t---\-+--.,�•-0.0& f------\---t+--t------+-----t--t--t--\ 5:15:ЭОРМ 5:15:40РМ 5:15:50 РМ ■ Tolal 5:16:ООРМ 5:16:10РМ \ Uuge per Core 5:15:30 РМ ■ Corw О ■ Corw 1 5:15:40 РМ ■ Corw 2 5:15:50РМ ■ Corw 3 ■ Corw 4 5:16:00 РМ ■ Corw 5 5:16:10РМ ■ Corw 6 ■ Corw 7 Uuge Вreakdown Рис. 6.1. Примеры графиков ЦП в c:Advisor Мониторинг с помощью Prometheus Prometheus (https://prometheus.io/) - это популярное решение для мониторинга распределенных систем. В основном он активно опрашивает источники и собирает статистику с эндпоинтов по заданному графику. У Docker есть эндпоинт специаль­ но для Prometheus, через который мы легко можем передавать статистику контей­ нера в систему мониторинга Prometheus. На момент написания книги этот эндпоинт предоставлялся как экспериментальная функция и по умолчанию был отключен на сервере dockerd. Как показывает небольшой опыт, это вполне эффективное и акку­ ратное решение, и мы рассмотрим его чуть позже. Решение предназначено для моКниги для программистов: https://clcks.ru/books-for-it Сбор информации в Docker 1 167 ниторинга сервера dockerd, в отличие от других вариантов, которые предоставляют информацию о контейнерах. Для экспорта метрик в Prometheus мы изменим конфигурацию сервера dockerd, что­ бы включить экспериментальные функции и предоставить слушатель (\istener) мет­ рик через выбранный порт. Это удобно, потому что не приходится открывать весь Docker API через слушатель ТСР - потребуется чуть больше конфигурирования, зато так гораздо безопаснее. Мы можем указать параметры --experimental и --metrics-addr= в командной строке или внести изменения в файл daemon.json, где хранится конфигурация демона. Многие текушие дистрибутивы используют systemd, а изменение конфигурации зависит от особенностей конкретной установки. Мы будем работать с daemon.json, потому что его проще переносить. Мы проде­ монстрируем этот подход на дистрибутиве UЪuntu Linux 22.04 LTS, в котором этот файл обычно отсутствует по умолчанию. Давайте добавим файл с помощью любого редактора. Кроме того, файл daemon.json для Docker Desktop можно редактировать в пользо­ вательском интерфейсе, в разделе Preferences (Настройки) ..... Docker Engine . . После внесения изменений в файл перезапустите Docker Desktop или демон dockerd. Отредактируйте или добавьте следующие строки в файле daemon.json: "experimental": true, "metrics-addr": "О.О.О.О:9323" Итак, у нас есть файл, который содержит только то, что мы вставили, и ничего больше. Когда мы предоставляем доступ к сервису в сети, мы должны учитывать риски для безопасности. Мы считаем, что метрики достаточно важны, чтобы ради них пойти на компромисс, но советуем вам обдумать последствия в вашем конкретном сце­ нарии. Например, в большинстве случаев не стоит открывать доступ к метрикам из Интернета. Когда мы перезапустим Docker, порт 9323 будет прослушиваться на всех адресах. К этому порту подключится Prometheus для получения данных. Сначала нужно пе­ резапустить сервер dockerd. Docker Desktop автоматически выполняет перезапуск, но для сервера Linux Docker выполните команду sudo systemctl restart docker, чтобы перезапустить демон. После перезапуска никаких ошибок быть не должно. Если они появились, скорее всего, что-то не так с файлом daemon.json. Давайте протестируем эндпоинт для получения метрик с помощью curl: $ curl -s http://localhost:9323/metrics I head -15 # HELP builder_builds_failed_total NumЬer of failed image builds # ТУРЕ builder-builds-failed-total counter builder_builds_failed_total{reason="build_canceled"} О builder_builds_failed_total{reason="build_target_not_reachaЬle_error"} О Книги для программистов: https://clcks.ru/books-for-it 168 Глава б builder_builds_failed_total{reason="command_not_supported_error"} О builder_builds_failed_total{reason="dockerfile_empty_error"} О builder_builds_failed_total{reason="dockerfile_syntax_error"} О builder_builds_failed_total{reason="error_processing_commands_error"} О builder_builds_failed_total{reason="missing_onЬuild_arguments_error"} О builder_builds_failed_total{reason="unknown_instruction_error"} О # HELP builder_builds_triggered_total NurnЬer of triggered image builds # ТУРЕ builder_builds_triggered_total counter builder_builds_triggered_total О # HELP engine_daemon_container_actions_seconds The numЬer of seconds it # takes to process each container action # ТУРЕ engine_daemon_container_actions_seconds histogram Если вы выполните эту команду локально, то получите примерно такой же вывод. Он может немного отличаться, это нормально. Главное, чтобы вы не получили со­ общение об ошибке. Итак, теперь у Prometheus есть точка для извлечения статистики, но нам где-то нужно запустить сам Prometheus. Давайте сделаем это в контейнере. Сначала на­ пишем простую конфигурацию и поместим ее в /tmp/prometheus/prometheus.yaml. Создайте файл в любом редакторе и напишите в нем следующий код: # Сбор метрик каждые 5 секунд, присвоение монитору имени stats-monitor' gloЬal: scrape_interval: 5s external laЬels: monitor: 'stats-monitor' # Задание будет называться DockerStats, мы подключимся к docker0 # для получения статистики. Если у docker0 другой IР-адрес, укажите его. # 127.0.0.1 и localhost не сработают. scrape_configs: - joЬ_name: 'DockerStats' static_configs: - targets: [ '172.17.0.1:9323'] Для Docker Desktop также можно указать host.docker.internal:9323 или gateway. docker.internal:9323 вместо 172 .17. О .1:9323. Оба имени хоста будут указывать на IР-адрес контейнера. Как указано в файле, используйте IР-адрес моста dockero или IР-адрес интерфейса ens3 или eth0, поскольку localhost и 127. о. о.1 не маршрутизируются из контейнера. Здесь мы указали обычный адрес по умолчанию для docker0. Скорее всего, вам этот вариант подойдет. Написав конфигурацию, давайте запустим контейнер: $ docker container run --rm -d -р 9090 :9090 \ -v /tmp/prometheus/prometheus.yaml:/etc/prometheus.yaml \ prom/prometheus --config.file=/etc/prometheus.yaml Книги для программистов: https://clcks.ru/books-for-it Сбор информации в Docker 1 169 Таким образом мы запускаем контейнер и монтируем наш файл конфигурации в контейнер, чтобы он нашел параметры для мониторинга эндпоинта Docker. Если все запустилось нормально, мы сможем открыть браузер и перейти к порту 9090 на хосте. Мы увидим окно Prometheus, как на рис. 6.2. На рис. 6.2 вы видите, что мы выбрали одну из метрик- engine_daemon_events_total (общее число событий в демоне)- и составили для нее график за короткий пери­ од. Мы можем запросить любую другую метрику в выпадающем меню. С помощью Prometheus мы можем определить оповещения и политики оповещений на основе метрик. Этот инструмент позволяет отслеживать гораздо больше информации, чем сервер dockerd. Мы также можем предоставлять метрики для Prometheus из прило­ жений. Если вам интересно попробовать расширенные возможности, обратите внимание на dockprom (https://github.com/stefanprodan/dockprom), который ис­ пользует Grafana для создания наглядных панелей мониторинга и запрашивает метрики контейнера, вроде тех, которые предоставляются эндпоинтом Docker API /stats. О PrometheL.s D Use 1оса1 tine Q. �э, ■ Enal>le query hisrory D Enable 1Utocon!l>lete D Enal>le Nghligl!tirQ D - lnler tngllle_dllfllQft_events_tot1\ Т•Ые �t -��- -■ . L..-liм:1,- �1• ...,_..,_1 ■- ----"'•-•_m•--- --��-<•!./ - : ,._ ! ! ! .... i . .. ! ,о.оо 2000 10.00 --- -- ___/ � / .... А Чi г / 1 / / 1 ' . � ,. ,,...., Рис. 6.2. Пример графика событий в Prometheus Книги для программистов: https://clcks.ru/books-for-it 170 1 Глава б Самостоятельная работа Теперь вы знаете, как получать информацию о выполняющихся контейнерах. За­ грузите пару контейнеров из реестра Docker Hub и поизучайте их самостоятельно, чтобы запомнить команды, которые мы рассмотрели. В Docker у вас есть гораздо больше возможностей, в том числе: ♦ Копирование файлов из контейнера и в контейнер - docker container ер. ♦ Сохранение образа в ТАR-файл - docker image save. ♦ Загрузка образа из ТАR-файла - docker image iпport. У Docker много функций, и вы будете знакомиться с ними по ходу работы. С каж­ дым новым выпуском функций становится больше. Мы подробно рассмотрим некоторые из них позже. Пока просто знайте, что их действительно много. Заключение В следующей главе мы подробно рассмотрим технические принципы работы Docker и узнаем, как отлаживать контейнеризированные приложения. Книги для программистов: https://clcks.ru/books-for-it ГЛАВА 7 Отладка контейнеров После развертывания приложения в рабочей среде однажды что-то непременно пойдет не так, и нам лучше подготовиться к этому моменту. Прежде чем перехо­ дить к более сложным развертываниям, полезно научиться отлаживать контейнеры, иначе невозможно будет понять, какие проблемы возникли в системе оркестрации. Давайте рассмотрим принципы отладки контейнеров, тем более что отладка контейнеризированного приложения мало чем отличается от отладки обычного процесса в системе. Разве что нам понадобятся другие инструменты, и Docker их предоставляет. Некоторые из них можно сравнить с обычными системными инст­ рументами, а другие дают гораздо больше. Важно понимать, что приложение не выполняется в отдельной от других процессов Docker системе. Они задействуют одно ядро и, в зависимости от конфигурации контейнера, могут также совместно использовать подсистему хранения и сетевые интерфейсы. Это значит, что система может предоставить много информации о ра­ боте контейнеров. Если вы привыкли отлаживать приложения в среде с виртуальными машинами, ва­ шим первым побуждением будет войти в контейнер, чтобы проверить потребление памяти и ЦП или отладить системные вызовы. Однако это необязательно. Контей­ неры могут ощушаться как слой виртуализации, но на самом деле процессы в кон­ тейнерах - это просто процессы на самом хосте Linux. Чтобы просмотреть список процессов во всех контейнерах Linux на компьютере, войдите на сервер и введите ps в любой командной строке. Также можно выполнить команду docker container top где угодно, чтобы посмотреть список процессов в контейнере с точки зрения базо­ вого ядра Linux. Давайте посмотрим, что можно сделать при отладке контейнери­ зированноrо приложения, не прибегая к помощи docker container ехес или nsenter. Вывод процесса Первое, что мы хотим узнать при отладке контейнера, - его содержимое. Мы уже знаем, что в Docker для этого есть встроенная команда: docker container top. Это не единственный способ "заглянуть" в контейнер, но определенно самый простой. Давайте посмотрим, как это работает: $ docker container run --rm -d --name nginx-debug --rm nginx:latest 796b282bfed33a4ec864a32804ccf5cbbee688b5305f094cбfЬaf20009ac2364 Книги для программистов: https://clcks.ru/books-for-it 172 Глава 7 $ docker container top nginx-debug UID root uuidd uuidd uuidd uuidd uuidd uuidd uuidd uuidd PID 2027 2085 2086 2087 2088 2089 2090 2091 2092 PPID 2002 2027 2027 2027 2027 2027 2027 2027 2027 с STIME о 12:35 о 12:35 о 12:35 о 12:35 о 12:35 о 12:35 о 12:35 о 12:35 о 12:35 ТТУ TIME ? ? ? ? ? ? ? ? ? 00:00 00:00 00:00 00:00 00:00 00:00 00:00 00:00 00:00 CMD nginx: master process nginx -g daemon off; nginx: worker process nginx: worker process nginx: worker process nginx: worker process nginx: worker process nginx: worker process nginx: worker process nginx: worker process $ docker container stop nginx-debug С командой docker container top мы передаем имя или идентификатор контейнера, а в ответ получаем список процессов внутри контейнера, упорядоченный по PID, как в обычных выходных данных Linux команды ps. Правда, здесь есть несколько особенностей. Главная из них - пространство имен идентификаторов пользователей и файловых систем. Следует понимать, что имя пользователя для определенного идентификатора поль­ зователя (User ID, UID) может различаться в контейнерах и в хостовой системе. У некоторых UID и вовсе может не быть именованного пользователя в файле /etc/passwd контейнера или хоста. Дело в том, что Unix не требует привязывать к UID именованного пользователя, и пространства имен Linux, о которых мы пого­ ворим подробнее в разделе "Пространства имен" главы 11, обеспечивают некото­ рую изоляцию между пользователями в контейнере и на базовом хосте. Давайте рассмотрим конкретный пример. Допустим, у нас есть рабочий сервер Docker на Ubuntu 22.04, а на нем контейнер с дистрибутивом Ubuntu. Если мы вы­ полним следующие команды на хосте Ubuntu, то увидим, что UID 7 имеет имя lp: $ id 7 uid=7(lp) gid=7(lp) groups=7(lp) Номер UID здесь не имеет особого значения, вам не нужно его записывать. Он был выбран, потому что принят по умолчанию на обеих платформах, но представляет разные имена пользователей. Если затем мы войдем в стандартный контейнер Fedora на этом хосте Docker, то увидим, что для UID задано halt в /etc/passwd. Выполнив следующие команды, мы увидим, что у контейнера совершенно другие представления о том, с каким пользо­ вателем связан UID 7: $ docker container run --rm -it fedora:latest /bin/bash rооt@с399сЬ807еЬ7:/1 id 7 uid=7(halt) gid=0(root) groups=0(root) rооt@с399сЬ807еЬ7:/1 grep х:7: /etc/passwd halt:x:7:0:halt:/sbin:/sbin/halt root@409c2a8216Ьl:/1 exit Книги для программистов: https://clcks.ru/books-for-it Отладка контейнеров 173 Если выполнить ps aux на теоретическом сервере Ubuntu Docker, пока контейнер выполняется под UID 7 (-u 7), то мы увидим, что хает Docker считает, будто про­ цесс в контейнере работает от имени lp, а не halt: $ docker container run --rm -d -и 7 fedora:latest sleep 120 55...сб $ ps aux grep sleep lp 2388 0.2 О.О 2204 ... 0:00 sleep 120 784? vagrant 2419 о.о о.о 5892 1980 pts/0 ... 0:00 grep --color=auto sleep Это особенно сбивало бы с толку, если бы какой-нибудь известный пользователь, например nagios или postgres, был настроен в хает-системе, но не в контейнере, при этом контейнер выполнял бы этот процесс с тем же идентификатором. Из-за раз­ ных пространств имен вывод команды ps выглядит странно. Например, если не вглядываться, может показаться, что пользователь nagios на хаете Docker выполняет демон postgresql, запущенный в контейнере. Одно из решений этой проблемы - назначить ненулевой UID контейнерам. На серверах Docker мы можем создать пользователя container с UID 5000, а затем создать того же пользователя в базовых образах контейнера. Если затем запускать все контейнеры под UID 5000 (-u 5000), мы повысим безопасность системы, по­ скольку процессы в контейнере не будут иметь UID О. Кроме того, будет проще читать вывод команды ps на хосте Docker, т. к. для всех выполняющихся в контей­ нере процессов будет отображаться пользователь container. В некоторых систе­ мах для этой цели применяется пользователь nobody или daemon, но мы предпочи­ таем указывать container для ясности. В разделе "Пространства имен" главы 11 мы подробнее рассмотрим, как это работает. Поскольку процесс видит файловую систему по-другому, то пути, которые приво­ дятся в выводе ps, показаны относительно контейнера, а не хоста. В таких случаях очень полезно знать, что процесс находится в контейнере. Таким образом мы можем посмотреть на содержимое контейнера с помощью инст­ рументов Docker. Но это не единственный способ, и для отладки не самый удоб­ ный. Если мы зайдем на сервер Docker и выполним обычную команду Linux ps, то получим полный список всех процессов в контейнерах и за их пределами, и эти процессы будут выглядеть одинаково. Есть несколько способов просмотреть вывод процесса, чтобы лучше понять происходящее. Например, мы можем упростить от­ ладку, изучая вывод команды Linux ps в виде дерева, чтобы увидеть все процессы, происходящие от Docker. Виртуальная машина Docker Desktop содержит минимальные версии большинства инструментов Linux, и некоторые из этих команд могут не давать тот же вывод, который мы получили бы при использовании стандартного сервера Linux в качест­ ве хоста демона Docker. Вот как может выглядеть вывод при использовании флагов командной строки BSD для изучения системы, в которой выполняются два контейнера (здесь приводится только самая интересная часть): $ ps axlfww /usr/Ыn/containerd Книги для программистов: https://clcks.ru/books-for-it Глава 7 174 ... /usr/bin/dockerd -Н fd:// --containerd=/run/containerd/containerd.sock ... \_ /usr/bin/docker-proxy -proto tcp -host-ip О.О.О.О -host-port 8080 \ -container-ip 172.17.0.2 -container-port 8080 ... \_ /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 8080 \ -container-ip 172.17.0.2 -container-port 8080 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 97 ...Зd -address /run/ ... \_ sleep 120 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 69 ...7с -address /run/ ... Многие команды ps в этом примере работают только на дистрибутивах Linux с пол­ ной поддержкой команды ps. Упрощенные версии Linux, например Alpine, исполь­ зуют командную оболочку BusyBox, которая не полностью поддерживает ps и вы­ дает не все данные. Рекомендуется установить полноценный дистрибутив, например Ubuntu или Fedora CoreOS. Здесь мы видим, что выполняется один экземпляр containerd. Это главная среда вы­ полнения контейнеров, используемая демоном Docker. У dockerd есть два выпол­ няющихся подпроцесса docker-proxy, о которых мы подробнее поговорим далее в этой главе в разделе "Изучение сети". Каждый процесс, использующий containerd-shim-runc-v2, представляет один контей­ нер и все процессы, вьшолняющиеся внутри этого контейнера. В этом примере у нас два контейнера. Они отображаются как containerd-shim-runc-v2 с дополнитель­ ной информацией о процессе, включая идентификатор контейнера. В этом случае выполняется один экземпляр Google cadvisor и один экземпляр sleep в другом кон­ тейнере. У каждого контейнера с сопоставленными портами есть как минимум один процесс docker-proxy, который служит для проброса необходимых сетевых портов между контейнером и сервером Docker. В этом примере оба процесса docker­ proxy связаны с cadvisor. Один пробрасывает порты для адресов 1Pv4, а другой­ для IPvб. Поскольку вывод команды ps представлен в виде дерева, мы видим, какие процессы в каких контейнерах выполняются. Если вы предпочитаете использовать флаги командной строки Unix SysV, то можете получить похожий, хотя и не такой на­ глядный, результат с помощью ps -еjн: $ ps -ejH containerd dockerd docker-proxy docker-proxy containerd-shim cadvisor Книги для программистов: https://clcks.ru/books-for-it Отладка контейнеров 1 175 containerd-shim sleep Более лаконичный вид дерева процессов docker можно получить с помощью коман­ ды pstree. Здесь мы указали pidof, чтобы посмотреть дерево, относящееся к docker: $ pstree 'pidof dockerd' dockerd--т-<Jocker-proxy-7*[{docker-proxy}] �ocker-proxy-6*[{docker-proxy}] L10* [ {dockerd}] Мы не видим PID, но можем получить представление, как все связано друг с дру­ гом. Это очень наглядный вид, если на хаете выполняется много процессов. Тут нет ничего лишнего, просто общая схема взаимосвязей. Мы видим те же контейнеры, что и в предыдущем выводе команды ps, но дерево свернуто, и отображается только число, например, 7*, если у нас семь одинаковых процессов. Мы можем просмотреть полное дерево с идентификаторами PID с помощью ко­ манды pstree: $ pstree -р 'pidof dockerd' dockerd (866)--т-<Jocker-proxy {3050) -т-{docker-proxy} (3051) f-{docker-proxy} (3052) 1 f-{docker-proxy} (3053) 1 f-{docker-proxy) {3054) 1 f-{docker-proxy} (3056) 1 f-{docker-proxy} (3057) 1 L( docker-proxy} (3058) 1 �ocker-proxy (3055) -т- {docker-proxy} (3059) f-{docker-proxy} (3060) 1 f-{docker-proxy} (3061) 1 f-{docker-proxy} (3062) 1 1 f-{docker-proxy} (3063) 1 L(ctocker-proxy} (3064} f-{dockerd} (904) f-{ctockerd} (912) f-{dockerd} (913} f-{dockerd} (914) f-{dockerd} (990) f-{dockerd} (1014) f-{dockerd} (1066) dockerd} (1605) f-{ctockerd} (1611) L(ctockerd} (2228) Н В этом выводе мы видим все процессы, выполняющиеся в Docker. Можно заглянуть в один контейнер и посмотреть его процессы, узнав идентифика­ тор главного процесса контейнера и указав его в команде pstree, чтобы вывести все связанные подпроцессы: Книги для программистов: https://clcks.ru/books-for-it 176 Глава 7 $ ps aux I grep containerd-shim-runc-v2 root 3072 ... /usr/bin/containerd-shim-runc-v2 -namespace moby -id 69...7с root 4489 ... /usr/bin/containerd-shim-runc-v2 -namespace moby -id fl ...46 vagrant 4651 ... grep --color=auto shim $ pstree -р 3072 containerd-shim(3072)--т-<:advisor(3092) -т- {cadvisor) (3123) f-{cadvisor} (3124) 1 f-{cadvisor} (3125) 1 f-{cadvisor) (3126) 1 f-{cadvisor) (3127) 1 f-{cadvisor) (3128) 1 f-{cadvisor} (3180) 1 f-{cadvisor} (3181) 1 L{cadvisor} (3182) 1 f-{containerd-shim} (3073) f-{containerd-shim} (3074) f-{containerd-shim} (3075) f-{ containerd-shim} (3076) f-{ containerd-shim} (3077) f-{ containerd-shim} (3078) f-{containerd-shim} (3079) f-{ containerd-shim} (3080) f-{ containerd-shim) (3121) L{containerd-shim} (3267) Изучение процесса Войдя на сервер Docker, мы можем проверять выполняемые процессы с помощью всех стандартных инструментов отладки. Привычные инструменты, вроде strace, будут работать обычным образом. В следующем коде мы проверим процесс nginx, выполняющийся внутри контейнера: $ docker container run --rm -d --name nginx-debug --rm nginx:latest $ docker container top nginx-debug UID PID PPID CMD 22983 22954 root nginx: master process nginx -g daemon off; systemd+ 23032 22983 nginx: worker process systemd+ 23033 22983 nginx: worker process $ sudo strace -р 23032 strace: Process 23032 attached epoll_pwait(10, � � Когда закончите, нажмите <Ctrl>+<C>, чтобы выйти из процесса strace, если вы использовали именно его. Книги для программистов: https://clcks.ru/books-for-it Отладка контейнеров 177 Мы получаем такой же вывод, как и от процессов на хосте, которые выполняются не в контейнерах. Подобным образом lsof показьmает, что файлы и сокеты, откры­ тые в процессе, работают так, как ожидалось: $ sudo lsof -р 22983 СОММАND PID USER ...NАМЕ nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx nginx 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 22983 root root root root root root root root root root root root root root root root root root root root root root root root root root root .../ .../ ... /usr/sbin/nginx .../usr/sbin/nginx (stat: No such file or directory) .../liЬ/aarch64-linux-gnu/libnss_files-2.31.so (stat: .../liЬ/aarch64-linux-gnu/libc-2.31.so (stat: ... .../liЬ/aarch64-linux-gnu/libz.so.l.2.ll (path inode=...) .../usr/liЬ/aarch64-linux-gnu/libcrypto.so.l.l (stat: ... .../usr/liЬ/aarch64-linux-gnu/libssi.so.l.l (stat: .../usr/liЬ/aarch64-linux-gnu/libpcre2-8.so.0.10.l (stat: .../liЬ/aarch64-linux-gnu/libcrypt.so.l.l.O (path ... .../liЬ/aarch64-linux-gnu/libpthread-2.31.so (stat: .../liЬ/aarch64-linux-gnu/liЬdl-2.31.so (stat: ... .../liЬ/aarch64-linux-gnu/ld-2.31.so (stat: ... .../dev/zero ... /dev/null ...pipe ...pipe ...pipe ...protocol: UNIX-STREAМ ...pipe ...pipe ...protocol: ТСР ...protocol: ТСРvб ...protocol: UNIX-STREAМ ...protocol: UNIX-STREAМ ...protocol: UNIX-STREAМ Все пуrи к файлам указаны относительно вида на файловую систему изнутри кон­ тейнера, а не с хоста. По этой причине на уровне хост-системы не так-то просто найти конкретный файл из выполняющегося контейнера. В большинстве случаев лучше войти в контейнер командой docker container ехес, чтобы смотреть на файлы с той же точки, с которой их видят процессы внутри контейнера. Мы можем точно так же запустить отладчик GNU (gdЬ) и другие инструменты про­ верки процессов, если мы работаем с правами root и у нас есть необходимые раз­ решения. Стоит отметить, что мы можем запустить новый отладочный контейнер, который будет иметь доступ к процессам в существующем контейнере и предоставит до­ полнительные инструменты для отладки (мы подробно обсудим эту команду позже, в главе 11 в разделах "Пространства имен" и "Безопасность''): Книги для программистов: https://clcks.ru/books-for-it 178 Глава 7 $ docker container run -ti --rm --cap-add=SYS_PТRACE \ --pid=container:nginx-debug spkane/train-os:latest bash ° [root@e4Ь5d2f3a3a7 /J il ps aux USER PID %CPU %МЕМ TIME COMМAND root 1 О.О О.2 0�00 nginx: master process nginx -g daemon off; 101 30 О.О О.1 0:00 nginx: worker process 101 31 о.о 0.1 0:00 nginx: worker process 0:00 bash root 136 о.о 0.1 root 152 о.о 0.2 0:00 ps aux [root@e4b5d2f3a3a7 /]# strace -р 1 strace: Process 1 attached rt_sigsuspend([], 8 [Control-C] strace: Process 1 detached <detached ...> [root@e4b5d2f3a3a7 /]# exit $ docker container stop nginx-debug � Нажмите <Ctrl>+<C>, чтобы выйти из процесса зtще. Контроль процессов Если у нас есть командная оболочка, напрямую подключенная к серверу Docker, мы можем во многих смыслах работать с контейнеризированными процессами так же, как с любыми другими процессами в системе. Если мы работаем удаленно, то можем отправлять сигналы с помощью docker container kill, потому что это будет просто. Но если мы уже вошли на сервер Docker - для отладки или потому что демон Docker не отвечает, - мы можем с помощью kill принудительно завершить процесс, как завершили бы любой другой. Если только мы не завершаем в контейнере процесс верхнего уровня (с идентифи­ катором PID 1 ), после завершения процесса контейнер продолжит работать. Воз­ можно, этого мы и хотели, если нам нужно было завершить нестабильный процесс, но иногда в результате контейнер переходит в непредвиденное состояние. Разра­ ботчики ожидают, что все процессы выполняются, если контейнер отображается в выводе команды docker container 1s. Кроме того, могут возникнугь проблемы с планировщиком, например Mesos, или Kubernetes и другими системами, которые проверяют работоспособность приложения. Помните, что для "внешнего мира" все контейнеры выглядят одинаково. Если мы завершаем какой-то процесс внутри кон­ тейнера, лучше заменить контейнер целиком. Контейнеры - это абстракция, с ко­ торой взаимодействуют разные инструменты. Ожидается, что внутри контейнеры остаются предсказуемыми и согласованными. Завершение процессов - не единственная причина отправлять сигналы. Поскольку контейнеризированные процессы во многих отношениях похожи на обычные, им Книги для программистов: https://clcks.ru/books-for-it Отладка контейнеров 1 179 можно передавать все сигналы Unix, которые приводятся в руководстве для коман­ ды Linux kill. Многие программы Unix выполняют специальные действия при по­ лучении определенных сигналов. Например, nginx повторно открывает журналы при получении сигнала SIGUSRl. Вместе с командой Linux kill можно оmравлять любой сигнал Unix в процесс контейнера на локальном сервере. Управление процессами в контейнерах Если вы не используете оркестратор, например Kubernetes, который управляет несколь­ кими контейнерами в более крупной абстракции, вроде пода (pod), рекомендуется при­ менять механизмы управления процессами в контейнерах в рабочей среде. Это �ожет быть tini (https://github.com/krallin/tini), upstart (https://upstart.ubuntu.com/), runit (http://smarden.org/runit), s6 (https://skarnet.org/software/s6) или что-то подобное. Та­ кой подход позволяет работать· с контейнером как с неделимым целым, даже если он содержит несколько процессов. Все же постарайтесь приложить все усилия, чтобы в контейнере выполнялся только один процесс. Пусть он отвечает за одну хорошо оп­ ределенную задачу и не превращается в монолит. В любом случае команда docker container 1s должна показывать присутствие целого контейнера, чтобы мы не задумывались о работоспособности отдельного процесса внутри него. Если предположить, что наличие контейнера и отсутствие записи об ошиб­ ках в журнале означает, что все работает, можно считать, что вывод docker container 1s действительно отражает то, что происходит в системах Docker. Кроме того, так может считать и применяемая нами система оркестрации. Мы должны хорошо понимать поведение выбранного сервиса управления процессами, включая потребление памяти или пространства на диске, обработку сигналов Unix и т. д., потому что это может повлиять на поведение и производительность контейнера. Как правило, лучше выбирать самые легкие системы. Поскольку контейнеры работают так же, как любые другие процессы, важно пони­ мать, каким образом они могут негативно повлиять на приложение. Некоторые процессы в контейнере пороЖдают дочерние фоновые процессы, которые ответв­ ляются и выполняются в виде демонов, так что родительский процесс больше не управляет их жизненным циклом. Например, в Jenkins эти процессы часто выходят из-под контроля: демоны ветвятся на фоне и становятся дочерними процессами процесса PID 1 в системах Unix, а процесс 1 имеет особый статус и обычно являет­ ся процессом init. PID 1 отвечает за завершение работы дочерних процессов. По умолчанию в кон­ тейнере главным процессом будет PID 1. Поскольку мы, скорее всего, не будем управлять завершением работы дочерних процессов в приложении, рано или позд­ но в контейнере накопятся процессы-зомби. Решить эту проблему можно несколь­ кими способами. Например, запустить в контейнере систему инициализации init, которая сможет выполнять обязанности PID 1. Для этого подойдут sб, runit и дру­ гие механизмы, которые мы упомянули ранее. Сам Docker предлагает еще более простой вариант, который решает только эту проблему, не выполняя все задачи системы init. Если указать флаг --init в команде docker container run, Docker запустит очень маленький процесс init на основе проек­ та tini, который будет выступать в качестве процесса PID 1 в контейнере при Книги для программистов: https://clcks.ru/books-for-it 180 Глава 7 запуске. То, что мы укажем в Dockerfile в качестве смо, передается в tini, и в ос­ тальном все будет работать вполне ожидаемо. Правда, будет заменено все содер­ жимое раздела ENTRYPOINT в Dockerfile. Если мы запустим контейнер Linux без флага --init, то в списке процессов увидим что-то подобное: $ docker container run --rm -it alpine:3.16 sh / # ps -ef TIME USER СОММАND PID 1 root 0:00 sh 5 root 0:00 ps -ef / # exit Обратите внимание, что в этом случае смо запускается с PID 1, а значит, отвечает за завершение работы дочерних процессов. Если необходимо, при запуске контейнера мы можем передать параметр --init, чтобы точно завершить дочерние процессы при завершении родительского: $ docker container run --rm -it --init alpine:3.16 sh / # ps -ef PID USER TIME СОММАND /sbin/docker-init -- sh 1 root 0:00 root 0:00 sh 5 root 0:00 ps -ef 6 / # exit Здесь процесс с PID 1 - /sЫn/docker-init. При этом запускается код оболочки, как указано в командной строке. Поскольку теперь в контейнере есть система инициа­ лизации, обязанности процесса PID 1 будет выполнять она, а не команда, которой мы вызвали контейнер. В большинстве случаев это как раз то, что нам нужно. Вам может не понадобиться система инициализации, но она занимает очень мало места, так что в контейнерах в рабочей среде можно запускать хотя бы tini. Как правило, процесс init в контейнере понадобится, только если мы запускаем несколько родительских процессов или некоторые процессы не отвечают на сигна­ лы Unix должным образом. Изучение сети Отлаживать контейнеризированные приложения на уровне сети сложнее, чем на уровне процессов. В отличие от обычных процессов, выполняющихся на хаете, контейнеры Linux могут подключаться к сети разными способами. Если мы остави­ ли настройки по умолчанию, как это обычно бывает, то все контейнеры будут под­ ключень1 к сети через сеть bridge, по умолчанию создаваемую Docker. Это вирту­ альная сеть, в которой хает служит шлюзом для связи с внешним миром. Эти вир­ туальные сети можно проверять с помощью инструментов, поставляемых с Docker. Мы можем посмотреть существующие сети командой docker network 1s: Книги для программистов: https://clcks.ru/books-for-it Отладка контейнеров $ docker network ls NEТWORК ID f9685b50d57c Bacae 1680cbd fЬ70d67499d3 NАМЕ bridge host попе 181 SCOPE DRIVER bridge host null local local local Здесь мы видим сеть bridge, сеть host, которая доступна для всех контейнеров, рабо­ тающих в режиме host (см.раздел "Сеть хоста" главы 11), и сеть попе, которая пол­ ностью отключает доступ к сети для контейнера. Если вы используете docker compose или друтие инструменты оркестрации, они могуr создавать дополнительные сети с друтими именами. Правда, мы видим только названия сетей и не знаем, что в них происходит. Чтобы посмотреть, какие контейнеры подключены к определенной сети, можно подать команду docker network inspect. Мы получим довольно много вывода, в котором будут видны контейнеры в конкретных сетях, а также информация о самой сети. Давайте посмотрим на сеть bridge по умолчанию: $ docker network inspect bridge "Name": "bridge", "Driver": "bridge", "EnaЬleIPvб": false, "Contai.ners": "69е9 ...с87с": "Name": "cadvisor", }, "IPv4Address": "172.17.0.2/16", "IPvбAddress": "а2а8 ...е163": "Name": "nginx-debug", "IPv4Address": "172.17.0.3/16", "IPvбAddress" : "" }, "Options": "com.docker.network.Ьridge.default_bridge": "true", "com.docker.network.bridge.host_Ьinding_ipv4": "О.О.О.О", "com.docker.network.bridge.name": "docker0", }, "LaЬels": {} Книги для программистов: https://clcks.ru/books-for-it 182 Глава 7 Для краткости здесь приводится не вся информация, но мы видим два контейнера в сети bridge, подключенные к мосту dockerO на хосте. Кроме того, мы видим IР-ад­ реса всех контейнеров (IPv4Address и IPv6Address) и адрес сети host, к которой они подключены (host_Ьinding_ipv4). Эта информация помогает понять внутреннюю структуру мостовой сети. Если контейнеры подключены к разным сетям, они могут не взаимодействовать друг с другом, но это зависит от конфигурации сетей. Как правило, рекомендуется оставить подключение контейнеров к сети bridge по умолчанию, если только у вас нет веских причин подключить их к другой сети или вы не используете docker compose или планировщик, который сам управляет сетями контейнеров. Кроме того, можно присвоить контейнерам понятные имена, потому что здесь мы не видим информацию об образах. В этом выводе указаны только имя и идентификатор, по которым мы можем найти контейнер в листинге docker container 1s. Некоторые планировщики не присваивают контейнерам понятные имена, и это затрудняет отладку. Мы уже видели, что у контейнеров обычно есть свой сетевой стек и свой IР-адрес, если они выполняются не в режиме host, который мы обсудим в разделе "Сети" главы 11. А как это будет выглядеть с самого хает-компьютера? У контейнеров есть своя сеть и адреса, так что они не будут отображаться во всем выводе netstat на хосте. Мы знаем, что порты, проброшенные на контейнеры, привязаны к хосту. Команда netstat -an на сервере Docker дает ожидаемый результат: $ sudo netstat -an Active Internet connections (servers and estaЫished) Foreign Address Proto Recv-Q Send-Q Local Address tcp О О 0.0.0.0:8080 О.О.О.О:* О.О.О.О:* о о 127.0.0.53:53 tcp О.О.О.О:* 0.0.0.0:22 о о tcp 192.168.15.120:63920 о 192.168.15.158:22 о tcp . . .* :::8080 о tcp6 о . . .* о :::22 tcp6 о О.О.О.О:* о 127.0.0.53:53 о udp О.О.О.О:* о о 192.168.15.158:68 udp . . .* о :::58 raw6 о State LISTEN LISTEN LISTEN ESTABLISHED LISTEN LISTEN 7 Здесь мы видим все интерфейсы, которые прослушиваем. Наш контейнер привязан к порту 8080 по IР-адресу о.о.о.о, и это отображается в выводе. Что будет, если мы захотим, чтобы команда netstat показала имя процесса, привязанного к порту? $ sudo netstat -anp Active Internet connections (servers and estaЫished) Local Address Foreign Address Proto О.О.О.О:* 0.0.0.0:8080 tcp О.О.О.О:* 127.0.0.53:53 tcp О.О.О.О:* 0.0.0.0:22 tcp 192.168.15.120:63920 192.168.15.158:22 tcp . . .* : ::8080 tcp6 PID/Program name 1516/docker-proxy 692/systemd-resolve 780/sshd: /usr/sЬin 1348/sshd: vagrant 1522/docker-proxy Книги для программистов: https://clcks.ru/books-for-it Отладка контейнеров tcp6 udp udp raw6 :::22 127.0.0.53:53 192.168.15.158:68 :::58 : : :* О.О.О.О:* О.О.О.О:* : : :* 183 780/sshd: /usr/sЬin 692/system:i-resolve 690/system:i-network 690/system:i-network Мы видим тот же вывод, но обратите внимание, что к порту привязан docker-proxy. Дело в том, что в конфигурации по умолчанию у Docker есть прокси, написанный на Go, который находится между всеми контейнерами и внешним миром. Получа­ ется, что в этом выводе все контейнеры, выполняющиеся через Docker, будут при­ вязаны к docker-proxy. Обратите внимание, что здесь мы не видим связь между docker-proxy и конкретным контейнером. Это не страшно, потому что команда docker container ls показывает привязки между контейнерами и портами. Правда, это не­ очевидно, и об этом стоит знать, прежде чем приступать к отладке сбоя в рабочей среде. Мы можем передать флаг р с командой netstat, чтобы посмотреть, какие пор­ ты привязаны к контейнерам. Если контейнер использует сеть host, этот уровень пропускается. Здесь нет docker­ proxy, и процесс в контейнере может напрямую привязываться к порту. Он также отображается как обычный процесс в выходных данных netstat -anp. Другие команды для проверки сети работают очевидным образом, вкmочая tcpdwrp, но нужно помнить о docker-proxy между сетевым интерфейсом хоста и контейнером, а также о том, что у контейнеров есть свои сетевые интерфейсы в виртуальной сети. История образа При сборке и развертывании одного контейнера несложно отследить его происхо­ ждение и базовый образ. Когда мы развертываем больше контейнеров и над при­ ложением работает несколько команд, задача усложняется в разы. Как узнать, на каких слоях основан определенный контейнер? Тег образа контейнера показывает, какая сборка приложения используется, но по тегу невозможно ничего узнать о слоях, на которых строится приложение. К счастью, у нас есть команда docker image history. Она показывает все слои в рассматриваемом образе, их размеры и команды, с помощью которых образ собирался: $ docker image history redis:latest ...CREATED ВУ IМAGE e800a8da9469 .../Ьin/sh -с #(nop) CMD ["redis-server"] <missing> .../Ьin/sh -с #(nop) EXPOSE 6379 .../Ьin/sh -с #(nop) ENTRYPOINT ["docker-entry.. . <missing> .../Ьin/sh -с #(nop) СОРУ file:e873a0e3cl300lb5 ... <missing> .../Ьin/sh -с #(nop) WORКDIR /data <missing> .../Ьin/sh -с #(nop) VOLUME [/data] <missing> .../Ьin/sh -с mkdir /data && chown redis:redis <missing> .../Ьin/sh -с set -eux; savedAptMark="$ (apt-m ... <missing> ... /Ьin/sh -с #(nop) ENV REDIS_DOWNLOAD_SНA=fO ... <missing> <missing> .../Ьin/sh -с #(nop) ENV REDIS_DOWNLOAD_URL=ht .. . SIZE ов ов ов СОММЕNТ 661В ов ов ов 32,4МВ ов ов Книги для программистов: https://clcks.ru/books-for-it 184 Глава 7 <missing> <missing> <missing> <missing> <missing> <missing> .../bin/sh -с #(nop) ENV REDIS VERSION=7.0.4 .../Ьin/sh -с set -eux; savedAptMark="$ (apt-ma... .../Ьin/sh -с #(nop) ENV GOSU_VERSION=l.14 .../Ьin/sh -с groupadd -r -g 999 redis && usera... ... /Ьin/sh -с #(nop) CMD [ "bash"] .../Ьin/sh -с #(nop) ADD file:6039adfЬca55ed34a ... ов 4,ОбМВ 0В ЗЗlkВ 0В 74,ЗМВ Команда docker image history пригодится в ситуациях, когда мы пытаемся опреде­ лить, почему итоговый образ получился гораздо больше, чем мы ожидали. Слои перечисляют по порядку, первый находится снизу, а последний - сверху. Здесь мы видим, что вывод команды обрезан в некоторых местах. Дпя длинных ко­ манд мы можем добавить параметр --no-trunc к команде docker image history, чтобы увидеть целую команду, с помощью которой собирался каждый уровень. Конечно, при наличии --no-trunc вывод будет объемнее, а читать его станет сложнее. Изучение контейнера В главе 4 мы узнали, как читать вывод команды docker container inspect, чтобы уз­ нать конфигурацию контейнера. На диске хоста есть каталог, выделенный для кон­ тейнера. Обычно это /var/liЬ/docker/containers. Если заглянуть в каталог, мы увидим очень длинные хеши SНА: $ sudo 1s /var/lib/docker/containers 106ead0d55af55bd803334090664e4bc821c76dadf231elaab7798dlbaa19121 28970c706dЬOf69716af43527ed926acbd82581elcef5e4eбffl52fcelb79972 Зc4f916619a5dfc420396d823b42e8bd30a2f94ab5b0f42f052357a68a67309b 589f2ad30138lb7704c9cade7daбb34046ef69ebe3d6929b9bc24785d7488287 959dЬlбlld632dc27a86efcbббflc6268d948dбf22e81e2a22a57610b5070b4d alel5f197ea0996d3lf69c332f2Ы4el8b727e53735133a230d54657acбaa5dd bad35aac3f503121abf0e543e697fcade78f0d30124778915764d85fЫ0303a7 bc8c72c965ebca7dЬ9a2b816188773a5864aa38lb8lc3073b9d3e52e977c55ba daa75fЫ08a33793a3f8fcef7ba65589el24af66bc52c4a070f645fffЬbc498e e2ac800b58c4c72e240b90068402b7d4734a7dd03402ee2bce3248ccбf44d676 e8085ebcl02b5f51c13cc5c257acb2274e7f8dl645af7baad0cbбfe8eef36e24 f8e46faa3303d93fc424e289d09b4ffЬalfc7782b9878456e0fellflf6814e4b Выглядит непонятно, но это просто длинная форма идентификаторов контейнеров. Если мы хотим посмотреть конфигурацию для конкретного контейнера, можно уз­ нать его короткий идентификатор с помощью команды docker container 1s, а затем найти соответствующий каталог: $ docker container 1s CONTAINER ID IМAGE с58ЬfеffЬ9еб gcr.io/cadvisor/cadvisor:v0.44.1-test СОММАND "/usr/Ьin/cadvisor..." Мы можем взять короткий идентификатор из docker container 1s и сопоставить его с выводом 1s /var/liЬ/docker/containers, чтобы узнать, что нам нужен каталог, начи­ нающийся с с58ЫеffЬ9еб. Здесь удобно использовать функцию автозавершения ко­ манды с помощью клавиши <ТАВ>. Если требуется точное совпадение, выполните Книги для программистов: https://clcks.ru/books-for-it Отладка контейнеров 185 docker container inspect c58ЬfeffЬ9e6 и найдите длинный идентификатор в ее выводе. Этот каталог содержит интересные файлы, связанные с контейнером: $ cd /var/liЬ/docker/containers/\ c58ЬfeffЬ9e6e607f3aacb4a06ca473535Ьf9588450f08be46baa230aЬ43fld6 $ 1s -la total 48 drwx--x--- 4 root root 4096 Aug 20 10:38 drwx--x--- 30 root root 4096 Aug 20 10:25 . . -rw-r----- 1 root root 635 Aug 20 10:34 с58Ьf... fld6-json.log drwx------ 2 root root 4096 Aug 20 10:24 checkpoints -rw------- 1 root root 4897 Aug 20 10:38 config.v2.json -rw-r--r-- 1 root root 1498 Aug 20 10:38 hostconfig.json -rw-r--r-- 1 root root 13 Aug 20 10:24 hostnarne -rw-r--r-- 1 root root 174 Aug 20 10:24 hosts drwx--x--- 2 root root 4096 Aug 20 10:24 mounts -rw-r--r-- 1 root root 882 Aug 20 10:24 resolv.conf -rw-r--r-- 1 root root 71 Aug 20 10:24 resolv.conf.hash Как мы видели в главе 5, этот каталог содержит несколько файлов, примонтирован­ ных прямо к контейнеру с помощью Ьind mount, например hosts, resolv.conf и hostname. Если вы используете механизм журналирования по умолчанию, то в этом каталоге Docker будет хранить файл JSON, содержащий записи, которые отобра­ жаются по команде docker container logs, конфигурацию JSON для вывода docker container inspect (config.v2.json) и сетевую конфигурацию для контейнера (hostconfig.json). С помощью файла resolv.conf.hash Docker определяет, что файл контейнера отклонился от текущего файла на хосте, чтобы его можно бьmо обно­ вить. Этот каталог содержит важную информацию, которая поможет решить проблему в случае серьезных сбоев. Даже если мы не можем войти в контейнер или docker не отвечает, мы можем изучить конфигурацию контейнера. Кроме того, можно по­ смотреть, как эти файлы монтируются изнутри контейнера. Старайтесь не редакти­ ровать эти файлы. Docker ожидает, что они содержат достоверные данные, и если вы их измените, может произойти что-нибудь неприятное. Рассматривайте их как еще один источник информации о том, что происходит в контейнере. Изучение файловой системы Независимо от выбранного бэкенда, Docker использует многослойную файловую систему, с помощью которой можно отслеживать изменения в любом контейнере. Она помогает не только при сборке образов, но и при попытке понять, изменил ли контейнер Linux что-нибудь, и если да, то что. Распространенная проблема с кон­ тейнеризированными приложениями - они могут записывать данные в файловую систему контейнера. Обычно это нежелательно, и при отладке следует проверять, записывают ли процессы что-то в контейнер. Иногда полезно изучить файлы жур­ налов в контейнере. Этот функционал также встроен в инструмент командной строки docker и предоставляется через API. Давайте посмотрим, что мы получим. Запустим контейнер и изучим его по имени: Книги для программистов: https://clcks.ru/books-for-it 186 Глава 7 $ docker container run --rm -d --name nginx-fs nginx:latest 1272b950202dЬ25ee030703515f482e9ed576f8e64c926e4e535ballf7536cc4 $ docker container diff nginx-fs С /run А /run/nginx.pid С /var С /var/cache С /var/cache/nginx А /var/cache/nginx/scgi_terrq:,A /var/cache/nginx/uwsgi_terrq:, А /var/cache/nginx/client_terrq:, А /var/cache/nginx/fastcgi_terrq:, А /var/cache/nginx/proxy_terrq:, С /etc С /etc/nginx С /etc/nginx/conf.d С /etc/nginx/conf.d/default.conf $ docker container stop nginx-fs nginx-fs Каждая строка начинается с А или с, по первой букве от added (добавлено) или changed (изменено) соответственно. Мы видим, что контейнер выполняет nginx, что данные записывались в файл конфигурации nginx и что в новом каталоге /var/cache/nginx созданы временные файлы. В результате можно понять, как ис­ пользуется файловая система контейнера, оптимизировать и контролировать ее применение. Дальнейшее детальное изучение требует применять команды docker container export, docker container ехес, nsenter и т. д., чтобы посмотреть, что именно находится в фай­ ловой системе. Для начала работы будет достаточно команды docker container diff. Заключение Вы уже хорошо представляете себе, как развертывать и отлаживать отдельные кон­ тейнеры в среде разработки и рабочей среде, но как масштабировать эту систему для крупных экосистем приложений? В следующей главе мы рассмотрим простой инструмент оркестрации Docker - Docker Compose. С его помощью можно перей­ ти от одиночного контейнера Linux к системе оркестрации производственного класса. Это хороший вариант для сред разработки и конвейеров DevOps. Книги для программистов: https://clcks.ru/books-for-it ГЛАВА8 Принципы работы Docker Compose Мы подробно рассмотрели, как с помощью команд docker собирать, запускать, от­ слеживать и отлаживать приложения. Научившись работать с отдельными контей­ нерами, вы захотите делиться своими проектами и создавать более сложные приложения, работающие на нескольких контейнерах. Особенно это касается сред разработки, в которых мы можем запустить целый стек контейнеров, чтобы имити­ ровать несколько рабочих сред на локальном компьютере. В стеке каждому контейнеру потребуется соответствующая конфигурация, чтобы приложение правильно функщюнировало. Настраивать каждый отдельный контей­ нер сложно, особенно если приложение написано кем-то другим. Во время разра­ ботки для выполнения этой задачи обычно используют shеll-скрипты (скрипты командной оболочки), которые собирают и запускают множество контейнеров по одной схеме. Это работает, но новым сотрудникам сложно разобраться в системе, и эти скрипты непросто обслуживать, когда со временем в проект добавляется все больше изменений. Не всегда одни и те же скрипты подойдут для разных проектов. Поэтому Docker, Inc. предлагает инструмент Docker Compose, ориентированный в основном на разработчиков. Этот инструмент поставляется с Docker Desktop, но его можно установить, выполнив эти инструкции (https://docs.docker.com/ соm pose/install). Изначально Docker Compose был отдельным приложением, написанным на Python, которое запускалось командой docker-compose. Эта команда считается первой вер­ сией Docker Compose, и недавно ее заменил Docker Compose версии 2. Этот � Docker Compose полностью переписан на Go и поставляется как nлагин для клиен­ та Docker. Если docker compose version возвращает результат, плагин установлен. Если не установлен, установите его сейчас. Docker Compose - полезный инструмент, оптимизирующий разные задачи по раз­ работке, которые раньше считались очень трудоемкими и вызывали много ошибок. С его помощью разработчики быстро запускают сложные приложения, компили­ руют приложения, не создавая громоздкие локальные среды разработки и т. д. В этой главе мы рассмотрим, как эффективно применять возможности Docker Compose. Во всех следующих примерах нам понадобится репозиторий GitHub. Если вы будете выполнять примеры, но еще не загрузили код, приведенный в гла­ ве 6, запустите следующую команду: $ git clone https://githuЬ.com/spkane/rocketchat-huЬot-demo.git \ --config core.autocrlf=input Книги для программистов: https://clcks.ru/books-for-it 188 Глава 8 В этом примере файлы shеll-скриптов docker-compose.yaml были обрезаны для краткости. Обязательно используйте файлы из этого репозитория Git для выпол­ нения примеров. Этот репозиторий содержит конфигурацию, которая понадобится нам для запуска полноценного веб-сервиса с хранилищем MongoDB, сервер для коммуникаций Rocket.Chat с открытым исходным кодом, бот Hubot ChatOps (https://goo.gl/ hKT3QW) и инстанс zmachine-api для развлекательных целей. Настройка Docker Compose Прежде чем мы начнем выполнять команду docker compose, давайте посмотрим, ка­ кой специальный инструмент она заменяет, - изучим shеll-скрипты, с помощью которых мы могли бы собрать и развернуть локальную копию сервиса для разра­ ботки и локального тестирования в Docker. Вывод будет очень длинным и подроб­ ным, зато вы увидите, почему Docker Compose гораздо удобнее shеll-скриптов. � � Не рекомендуем выполнять этот скрипт. Это просто пример, и в вашей среде он может не сработать или что-то испортить. Jl!/Ьin/bash jl Не запускайте этот скриnт. exit 1 JI Сам скриnт JI JI Примечание. Скриnт не обновляется в соответствии с файлом docker-coпpose JI и nриводится только для информации. set -е set -u if [ $JI -ne О ] && [ ${1} = "down" ] ; then docker rm -f huЬot 11 true docker rm -f zmachine 11 true docker rm -f rocketchat 11 true docker rm -f mongo-init-replica 11 true docker rm -f mongo 11 true docker network rm botnet 11 true echo "Environment torn down ..." exit О fi JI Глобальные настройки export РОRТ="ЗООО" export ROOТ_URL="http://127.0.0.l:3000" export MONGO_URL="mongodЬ://mongo:27017/rocketchat" export MONGO_OPLOG_URL="mongodЬ://mongo:27017/local" export МAIL_URL="smtp://smtp.email" export RESPOND_TO_DM="true" export HUBOТ_ALIAS=". " export LISTEN_ON_ALL_PUBLIC="true" export ROCКEТCНAT_AUTH="password" Книги для программистов: https://clcks.ru/books-for-it Принципы работы Docker Compose 189 export RОСКЕТСНАТ URL ="rocketchat:3000" export RОСКЕТСНАТ ROOM="" export RОСКЕТСНАТ USER="huЬot" export ROCКETCНAT_PASSWORD="bot-pw 1 " export ВСУГ_NАМЕ="Ьоt" export EXTERNAL_SCRIPTS="huЬot-help,huЬot-diagnostics,huЬot-zmachine" export HUBCYГ_ZМACHINE_SERVER="http://zmachine:80" export HUBCYГ_ZМACHINE_ROOMS="zmachine" export HUBCYГ_ZМACHINE_CYГ_PREFIX="ot" docker build -t spkane/mongo:4.4 ./mongodЬ/docker docker push spkane/mongo:4.4 docker pull spkane/zmachine-api:latest docker pull rocketchat/rocket.chat:5.0.4 docker pull rocketchat/huЬot-rocketchat:latest docker rm -f huЬot 11 true docker rm -f zmachine 11 true docker rm -f rocketchat 11 true docker rm -f mongo-init-replica 11 true docker rm -f mongo 11 true docker network rm botnet 11 true docker network create -d bridge botnet docker container run-d \ --name=mongo \ --network=botnet \ --restart unless-stopped \ -v $(pwd)/mongodЬ/data/dЬ:/data/dЬ \ spkane/mongo:4.4 \ mongod --oplogSize 128 --replSet rs0 sleep 5 docker container run-d \ --name=mongo-init-replica \ --network=botnet \ spkane/mongo:4.4 \ 'mongo mongo/rocketchat --eval "rs.initiate ( { id: "rsO", memЬers: [ { ... ' sleep 5 docker container run-d \ --name=rocketchat \ --network=botnet \ --restart unless-stopped \ -v ${pwd)/rocketchat/data/uploads:/app/uploads \ -р 3000:3000 \ -е PORT=${PORT} \ -е ROCYГ_URL=${ROCYГ_URL} \ -е MONGO_URL=${MONGO_URL} \ -е MONGO_OPLOG_URL=${MONGO_OPLOG_URL} \ -е МAIL_URL=${МAIL_URL} \ rocketchat/rocket.chat:5.0.4 Книги для программистов: https://clcks.ru/books-for-it 190 1 Глава 8 docker container run-d\ --name=zmachine\ --network=botnet\ --restart unless-stopped \ -v $(pwd)/zmachine/saves:/root/saves\ -v $(pwd)/zmachine/zcode:/root/zcode \ -р 3002:80\ spkane/zmachine-api:latest docker container run-d\ --name=huЬot\ --network=botnet\ --restart unless-stopped\ -v $(pwd)/huЬot/scripts:/horne/huЬot/scripts\ -р 3001:8080\ -е RESPOND ТО DM="true"\ -е НUВОТ ALIAS=". "\ -е LISTEN ON ALL PUBLIC="true"\ -е ROCKETCНAT_AUTH="password"\ -е RОСКЕТСНАТ URL="rocketchat:3000"\ -е RОСКЕТСНАТ RООМ=""\ -е RОСКЕТСНАТ USER="huЬot"\ -е ROCKEТCНAT_PASSWORD="bot-pw!"\ -е ВОТ NAМE="bot"\ -е EXTERNAL_SCRIPТS="huЬot-help,huЬot-diagnostics,huЬot-zmachine"\ -е HUBOТ_ZМACHINE_SERVER="http://zmachine:80"\ -е НUВОТ-ZМACHINE-ROOМS="zmachine"\ -е НUВОТ-ZМACHINE-ОТ-PREFIX="ot"\ rocketchat/huЬot-rocketchat:latest echo "Environrnent setup ..." exit О Скорее всего, вы понимаете большую часть скрипта, и успели заметить, что его сложно читать, ему не хватает гибкости, его будет прqбл_ематично редактировать и что-то легко может пойти не так. Если бы мы попытались здесь обработать все возможные ошибки, чтобы сделать скрипт воспроизводимым, он стал бы в два-три раза длиннее. Если мы не проделаем большую работу по обработке ошибок для основного функционала, нам придется каждый раз переписывать большую часть логики для нового проекта. Это не лучший подход к созданию стабильного процес­ са. К счастью, теперь у нас есть Docker Compose, с которым процесс гораздо проще воспроизводить, читать, понимать и обслуживать. По сравнению с этим неудобным shеll-скриптом, в котором много лишнего кода и слабых мест,. Docker Compose обычно· использует один декларативный файл УАМL (https://yaml.org/) для каждого проекта - docker-compose.yaml. Этот файл конфи­ гурации легко читается и воспроизводится - он будет работать одинаково у каж­ дого пользователя. Здесь приводится пример файла docker-compose.yaml, который может заменить предыдущий нестабильный shеll-скрипт: Книги для программистов: https://clcks.ru/books-for-it Принципы работы Docker Compose 191 version: '3' services: mongo: build: context: ../mongodЬ/docker image: spkane/mongo:4.4 restart: unless-stopped environment: м::NGODB REPLICA SET К)[)Е: primary м::NGODB-REPLICA-SЕТ-NАМЕ: rs0 м::NGODB PORT NUМВER: 27017 м::NGODB_INITIAL_PRIМARY_HOST: mongodЬ м::NGODB INITIAL PRIМARY PORT NUМВER: 27017 м::NGODB-ADVERTISED_ноs�: mongo м::NGODB ENAВLE JOURNAL: "true" ALLOW ЕМРТУ PASSWORD: "yes" # Порт 27017 уже открыт в базовом файле # См. более новый базовый Dockerfile: # https://githuЬ.com/bitnami/containers/ЫoЬ/ # f9fb3f8a6323fb768fd488c77d4f111Ы330bd0e/bitnami/ # mongodЬ/5.0/debian-11/Dockerfile#L52 networks: - botnet rocketchat: image: rocketchat/rocket.chat:5.0.4 restart: unless-stopped laЬels: traefik.enaЫe: "true" traefik.http.routers.rocketchat.rule: Host('127.0.0.1 ') traefik.http.routers.rocketchat.tls: "false" traefik.http.routers.rocketchat.entrypoints: http volumes: - "../rocketchat/data/uploads:/app/uploads" environment: ROOТ_URL: http://127.0.0.1:3000 PORT: 3000 м::NGO_URL: "mongodЬ://mongo:27017/rocketchat?replicaSet=rs0" м::NGO_OPLOG_URL: "mongodЬ://mongo:27017/local?replicaSet=rs0" DEPLOY МЕТНОD: docker depends_on: mongo: condition: service_healthy ports: - 3000:3000 networks: - botnet Книги для программистов: https://clcks.ru/books-for-it Глава 8 192 zmachine: image: spkane/zmachine-api:latest restart: unless-stopped volU1118s: - "../zmachine/saves:/root/saves" - "../zmachine/zcode:/root/zcode" depencls_on: - rocketchat expose: - "ВО" networks: - botnet huЬot: image: rocketchat/huЬot-rocketchat:latest restart: unless-stopped volU1118s: - ".. /huЬot/scripts:/home/huЬot/scripts" environment: RESIOm то IМ: "true" НUВОТ ALIAS: ". " LISТEN 00 ALL PUВLIC: "true" RОСКЕТСВАТ_АUТН: "password" RОСКЕТСВАТ URL: "rocketchat:3000" RОСКЕТСВАТ RОСМ: "" RОСКЕТСВАТ USER: "huЬot" ROCКEТCНAT_PASSIORD: "bot-pw!" ВОТ NАМЕ: "bot" EXТERNAL_SCRIPТS: "huЬot-help,huЬot-diagnostics,huЬot-zmachine" НUВОТ_ZМACНINE_SERVER: "http://zmachine:80" НUВОТ ZМACНINE R()(],IS: "zmachine" НUВОТ ZМACНINE CJr РRЕПХ: "ot" depends_on: - zmachine ports: - 3001:8080 networks: - botnet networks: Ьotnet: driver: bridge В файле docker-compose.yaml можно легко поискать все важные требования для каждого сервиса, а также их взаимодействия друг с другом. Вдобавок мы получаем валидацmо и проверку логики, которые мы даже не писали в shell-cкpиme и в кото­ рых мы, скорее всего, временами ошибались бы, несмотря на все старания. Что мы указали для Compose в этом файле формата УАМL? Первая строка указы­ вает Docker Compose, для какой версии языка конфигурации Compose (https:// docs.docker.com/compose/compose-file) предназначен этот файл: version: ' 3 ' Книги для программистов: https://clcks.ru/books-for-it Принципы работы Docker Compose 193 Оставшийся код разделен на две части: services (сервисы) и networks (сети). Начнем с раздела networks. В этом файле docker-compose.yaml мы определяем одну именованную сеть Docker: networks: Ьotnet: driver: bridge Это очень простая конфигурация, которая указывает Docker Compose, что нужно создать одну сеть с именем Ьotnet с помощью драйвера bridge по умолчанию, кото­ рый будет соединять сеть Docker с сетевым стеком хоста. Самая важная часть конфигурации находится в разделе services - здесь мы указы­ ваем, какие приложения хотим запускать. В этом примере в разделе services опре­ делены пять сервисов: mongo, mongo-init-replica, rocketchat, zmachine и huЬot. У каждого сервиса есть свои разделы, в которых указано, как его собирать, настраивать и за­ пускать. Например, у сервиса mongo первый подраздел называется build и содержит мюч context. Таким образом, Docker Compose понимает, что может собрать этот образ, а файлы, необходимые для сборки, находятся в каталоге ../../ mongodЬ/docker, кото­ рый расположен на два уровня выше каталога, содержащего файл docker­ compose.yaml: Ьuild: context: ../ .. /mongodЬ/docker В Dockerfile в каталоге mongodЬ/docker мы увидим следующее: FRCМ mongo:4.4 СОРУ docker-healthcheck /usr/local/bin/ # Полезная информация: # https://docs.docker.com/engine/reference/builder/#healthcheck # https://docs.docker.com/compose/compose-file/#healthcheck НЕАLТНСНЕСК СМ> ["docker-healthcheck"] Обратите внимание на строку HEALTHCHECK (проверка работоспособности). В ней ука­ зано, какую команду Docker должен запустить, чтобы проверить работоспособ­ ность контейнера. Docker ничего не будет делать по результатам проверки, но сообщит о работоспособности, чтобы другие компоненты могли использовать эту информацию. Если интересно, изучите скрипт docker-healthcheck в каталоге mongodЬ/docker. Следующий параметр, image, определяет тег образа, который мы хотим применить к сборке или загрузить (если не собираем образ), а затем запустить: image: spkane/mongo:4.4 С помощью параметра restart мы указываем Docker, когда нужно перезапускать контейнеры. В большинстве случаев контейнеры следует перезапускать всегда, если только мы явно не остановили их: restart: unless-stopped Книги для программистов: https://clcks.ru/books-for-it 194 Глава 8 Далее идет раздел environment. Здесь мы можем определить переменные среды, которые хотим передать контейнеру: environment: �GOOB_REPLICA_SEТ_КX>E: primary �GOOB REPLICA-SЕТ-Nl\МE: rs0 �GOOB PORT NUМВER: 27017 �GOOB_INITIAL_PRIМARY_HOST: mongodЬ �GOOB INITIAL PRIМARY PORT NUМВER: 27017 �GOOB_ADVERTISED_НОSТNАМЕ: mongo �GOOB ENAВLE JOORNAL: "true" ALLOW_ЕМРТУ_PASSWORD: "yes" - В последнем подразделе для сервиса mongo, networks, мы указываем Docker Compose, к какой сети должен быть подкmочен контейнер: networks: - botnet Теперь давайте перейдем к сервису rocketchat. У этого сервиса нет подраздела build, но есть тег образа, который запрещает Docker Compose собирать образ и указывает, что необходимо извлечь и запустить готовый образ Docker с указанным тегом. У этого сервиса есть подраздел volumes (тома), который мы не видели раньше. У многих сервисов есть данные, которые необходимо хранить на протяжении раз­ работки, несмотря на эфемерность контейнеров. Самый простой способ - монти­ ровать внутрь контейнеров локальный каталог. В разделе volumes мы можем указать все локальные каталоги, которые нужно примонтировать к контейнеру, и опре­ делить их путь. Следующая команда с помощью Ьind mount примонтирует ../rocketchat/data/uploads к /app/uploads внутри контейнера: volumes: - " ../rocketchat/data/uploads:/app/uploads" Возможно, вы заметили, что мы не определили volume для MongoDB, и это может показаться странным. В подключенном томе было бы удобно хранить файлы базы данных, но MongoDB не сможет записывать данные в нативную файловую систему Windows, поэтому для максимальной совместимости мы будем записывать данные из базы данных в контейнер в этом проекте. В результате при удалении контейнера командой вроде docker compose down все данные в экземпляре MongoDB будут утеряны. Мы могли бы решить проблему с хранением в MongoDB с помощью контейнера с томом данных (https://docs.docker.com/storage/volumes/#create-and-manage­ volumes), но в этом примере мы используем Ьind mount для томов. Локальное хранилище на хаете для контейнеров в рабочей среде почти никогда не рекомендуется. Это очень удобно при разработке, поскольку мы используем один хост, но в рабочей среде контейнеры часто развертываются на любом узле, где есть свободное место и ресурсы, и если файлы хранятся в файловой системе од­ ного хоста, мы потеряем к ним доступ. Если нам понадобится хранилище с отелеКниги для программистов: https://clcks.ru/books-for-it Принципы работы Docker Compose 195 живанием состояния в рабочей среде, мы применяем сетевое хранилище, Kuber­ netes Persistent Volumes и т. д. В разделе environment у сервиса rocketchat у значения MONGO_URL не указан IР-адрес или полное доменное имя. Дело в том, что все сервисы выполняются в одной сети Docker, и Docker Compose настраивает каждый контейнер, чтобы он искал осталь­ ные по имени сервиса. Получается, что мы можем легко настраивать URL таким образом, чтобы указывать на имя сервиса и внутренний порт контейнера, к которо­ му нужно подключиться. Если мы что-то поменяем, эти имена будут указывать на нужный контейнер в стеке. Это удобно, потому что любой читающий сразу увидит зависимость для контейнера: environment: MONGO URL: "mongodЬ://mongo:27017/rocketchat?replicaSet=rs0" Файл docker-compose.yaml также может ссылаться на переменные среды в форма­ те ${ИМЯ_ПЕРЕМЕННОЙ), чтобы извлекать секреты, не сохраняя их в файле. Docker Compose также поддерживает файл .env (https://docs.docker.com/compose/env­ file), в котором удобно хранить секреты и переменные среды. Например, если они отличаются у разных разработчиков. В разделе ctepends_on (зависит от) определяется контейнер, который необходимо за­ пустить до эtого контейнера. По умолчанию ctocker сощ:юsе отвечает только за вы­ полнение, а не за работоспособность контейнера, но мы можем задействовать функционал HEALTHCHECK в Docker и условный оператор в Docker Compose, чтобы сервис, от которого зависит наш контейнер, был работоспособен, прежде чем Docker Compose запустит новый сервис. Стоит отметить, что это важно только при запуске. Docker сообщит о сервисах, которые позже станут неработоспособными, но ничего не сделает, чтобы исправить ситуацию, если контейнер существует. Если контейнер не существует, Docker перезапустит его, если мы указали это поведение в настройках: dependз_on: mongo: condition: service_healthy Функционал проверок работоспособности Docker подробно описан в разделе "Про­ верки работоспособности контейнеров" главы 6. Также можно изучить докумен­ тацию по Docker и Docker Compose (https://dockr.ly/2wt366J). В подразделе ports мы можем определить все порты, которые хотим пробросить из контейнера на хост: ports: - 3000:3000 В сервисе zmachine есть только один пока незнакомый нам подраздел - expose. В этом разделе мы указываем Docker, что хотим открыть порт для других контейКниги для программистов: https://clcks.ru/books-for-it 196 1 Глава 8 неров в сети Docker, но не для базового хоста. Поэтому мы не указываем порт на хосте для проброса этого порта: expose: - "80" Возможно, вы заметили, что мы открываем порт для zmachine, но не открыли порт в сервисе mongo. Мы могли бы открыть порт mongo, но в этом нет необходимости, пото­ му что он уже открыт в базовом mongo Dockerfile (https://github.com/docker-library/ mongo/ЫoЬ/58bdba62b65bldlelea5cbde54c1682f120e0676/3.2/Dockerfile#L95). Ино­ гда это не очевидно, и стоит изучить docker image history для собранного образа. Здесь мы рассмотрели довольно сложный пример, так что вы уже получили неко­ торое представление о том, как работает Docker Compose, но его возможности гораздо шире. В файле docker-compose.yaml можно настроить очень много, вклю­ чая настройки безопасности, квоты на ресурсы и др. Подробную информацию о конфигурации читайте в официальной документации по Docker Compose (https:// docs.docker.com/compose/compose-file ). Запуск сервисов В файле УАМL мы настроили набор сервисов для приложения. Таким образом мы указываем Compose, что хотим запустить и какая конфигурация потребуется. Давайте запустим наши сервисы. Прежде чем вьmолнить первую команду Docker Compose, нужно убедиться, что мы находимся в том же каталоге, что и файл docker-compose.yaml: $ cd rocketchat-huЬot-demo/compose Перейдя в нужный каталог, проверим правильность конфигурации: $ docker compose config Если все нормально, эта команда выведет наш файл конфигурации. Если есть про­ блемы, команда выдаст ошибку с дополнительными сведениями: services.mongo Additional property builder is not allowed Мы можем собрать любые нужные контейнеры с помощью параметра build. Серви­ сы, использующие образы, будут пропущены: $ docker compose build => [internal] load build definition from Dockerfile 0.0s 0.0s => => transferring dockerfile: 32В => [internal] load .dockerignore 0.0s => => transferring context: 2В 0.0s => [internal] load metadata for docker.io/bitnami/mongodЬ:4.4 1.2s => [auth] bitnami/mongodЬ:pull token for registry-1.docker.io 0.0s 0.0s => [internal] load build context 0.0s => => transferring context: 40В 0.0s => [1/2] FROМ docker.io/bitnami/mongodЬ:4.4@sha256:9162...ae209 Книги для программистов: https://clcks.ru/books-for-it Принципы работы Docker Compose ;> CACHED (2/2] СОРУ docker-healthcheck /usr/local/bin/ ;> exporting to image ;> ;> exporting layers ;> ;> writing image sha256: a6ef ...da808 ;> ;> naming to docker.io/spkane/mongo:4.4 197 O.Os O.Os O.Os O.Os O.Os Давайте запустим.-"веб-сервис в фоновом режиме, выполнив следующую команду: $ docker СОЩ)ОSе up -d [+] Running 5/5 .. Network COЩJOSe_botnet .. Container coЩJose-mongo-1 .. Container coЩJose-rocketchat-1 ... Container COЩ)ose-zmachine-1 .. Container coЩJose-huЬot-1 Created Healthy Started Started Started O.Os 62.0s 62.Зs 62.5s 62.6s Docker Compose перед именами сетей и контейнеров указывает имя контейнера. По умолчанию это имя каталога, в котором находится файл docker-compose.yaml. Поскольку эта команда выполнялась в каталоге compose, все имена начинаются С СОЩ)ОSе. & 1 Для пользователей Windows: при первом запуске сервисов Windows может запро­ сить разрешение для vpnkit, и Docker Desktop for Windows может запросить доступ к диску. Разрешите vpnkit и предоставьте доступ к сетевым папкам и томам, чтобы все работало правильно. Когда все будет готово, мы можем изучить записи журналов для всех сервисов (рис. 8.1): $ docker coЩJose logs Commit Hash: 29fb34babc ompose-rocketchat-1 1 1 Connit Branch: НЕАО ompose-rocketchat-1 1 1 ompose-rocketchat-1 1 1 ompose-rocketchat-1 1 +-----------------+ ompose-nюngo-1 1 {"t":{"$date":"2823-83-31T28:48:84.893+88:88"},•s•:•r•, •c•:"REPL", "ctx":"Rep\Coord-8","msg•:•starting rep\ication app\ier thread'} ompose-mongo-1 1 {'t':{'$date':'2823-83-31T28:48:84.893+88:88'},"s':'I", "c':"REPL", "ctx":'Rep\Coord-8',"msg":'Starting rep\ication reporter thread"} ompose-hubot-1 1 hubot-zmachineQ)ll.1.8 node_nюdu\es/hubot-zniachine ompose-hubot-1 1 npm info ok 'id':21388 "id':21381 Рис. 8.1. Вывод команды docker COЩJOse logs Вывод не очень наглядный, но обратите внимание, что записи выделены разными цветами в зависимости от сервиса, и указано время, когда Docker получил записи журнала. Благодаря этому гораздо проще следить за происходящим, даже когда несколько сервисов записывают события в журнал одновременно. Возможно, потребуется некоторое время, чтобы Rocket.Chat настроил базу данных и подготовился принимать подключения. Когда в журналах Rocket.Chat появится строка, содержащая SERVER RUNNING, все готово к работе: Книги для программистов: https://clcks.ru/books-for-it 198 Глава В $ docker compose logs rocketchat I grep "SERVER RUNNING" compose-rocketchat-1 1 1 SERVER RUNNING Итак, мы успешно запустили относительно сложное приложение, состоящее из не­ скольких контейнеров. Давайте внимательно изучим детали приложения и поймем , как работает Compose. Следующий раздел не связан напрямую с самим Docker, но мы увидим, как просто можно с помощью Docker Compose запустить полноценные веб-сервисы. Принципы работы Rocket.Chat В этом разделе мы ненадолго отвлечемся от Docker и рассмотрим Rocket.Chat. Мы вынесли его в отдельный раздел, чтобы вы лучше поняли, как он работает, и уви­ дели, насколько он упрощает создание сложной среды с помощью Docker Compose. Можете пропустить этот раздел и перейти к разделу "Применение Docker Compose" далее в этой главе. Позже мы изучим, что происходит за кулисами, но сначала нужно кратко рассмот­ реть созданный нами стек приложения. Rocket.Chat (https://rocket.chat/), основное приложение, которое мы запустили с помощью Docker Compose, представляет собой клиент-серверное приложение-чат с открытым исходным кодом. Чтобы посмотреть, как оно работает, давайте в браузере перейдем по адресу http:// 127.0.0.1:3000. Далее откроем экран Admin Info (Информация об администраторе) для Rocket.Chat (рис. 8.2). Step 1 of 3 Admin lnfo We need this to create an admin profile inside your worl<space Full name 1 1 1 student Username student Email student@example.com - Password student-pw! ---- - -- --- - - - --- --- - <§> 1 1 1 1 Рис. 8.2. Экран с информацией об администраторе в Rocket.Chat Книги для программистов: https://clcks.ru/books-for-it Принципы работы Docker Compose 1 199 Заполните форму, как на рис. 8.2: ♦ Full name: student ♦ Username: student ♦ Email: student@example.com ♦ Password: student-pw! Нажимаем кнопку Nf�xt (Далее) и переходим на экран информации об организации Organization Info (рис. 8.3). Step 2 of 3 Organization lnfo Please, Ьеаг with us. This info will help us personalize уоuг workspace Organlzatlon name 1 1 1 1 training Organization type Community V Organizatlon industry Education V Organization slze 1-10 people V Country - United States I ... J. 1 1 1 1 --------V -- -- - --- ------- - - - Рис. 8.3. Экран с информацией об организации в Rocket.Chat Здесь можно указать что уrодно, например: ♦ Organization name: training ♦ Organization type: Community ♦ Organization industry: Education ♦ Organization size: 1-1О people ♦ Country: United States Нажимаем кнопку Next (Далее) и переходим на экран регистрации сервера Register Your Server (рис. 8.4). Здесь мы удалим все данные и флажки и нажмем на ссылку Continue as standalone (Продолжить на автономном сервере), чтобы перейти на экран конфигурации авто­ номного сервера Standalone Server Configuration (рис. 8.5). Нажимаем синюю кнопку Confirm (Подтвердить). Книги для программистов: https://clcks.ru/books-for-it 200 Глава В Step Э of З Register Your Server ✓ MoЬile push notifications ✓ lntegration with extemal providers (WhatsApp, FaceЬook, Telegram, Twitter) ✓ Access to marketplace apps Cloud account ernall 0 1 Please enter your Email This field is required О Кеер те informed aЬout news and events О I agree with Terms and Conditions and Privacy Policy Ву registering I agree to receive relevant product and security updates Continue а s standalone Рис. 8.4. Экран регистрации сервера в Rocket.Chat Step З of З Standalone Server Confirmation & Some of the servlces wlll Ье uмvailaЫe or wlll requlre manual setup 0 ln order to send push notitications you need to compile and puЬlish your own арр to Google Play and Арр Store 0 Need to manually integrate with external services Рис. 8.5. Экран подтверждения автономного сервера в Rocket.Chat Если вы используете localhost или открываете Rocket.Chat в браузере не по адре­ су 127. о. о .1, то может открыться всплывающее окно с вопросом о том, хотите ли вы обновить SIТE_URL. В большинстве случаев вы можете изменить это значение на свое собственное. Итак, мы вошли в полноценный чат-клиент, но это еще не все. Конфигурация Docker Compose запустила экземпляр ассистента Hubot (https://hubot.github.com/) и непонятный zmachine. Давайте посмотрим, что это. Наш сервер Rocket.Chat совсем новый, так что у нас нет пользователя для бота. Да­ вайте это исправим. Нажмем на фиолетовый квадрат с буквой S в левом верхнем углу. В открывшемся меюо выбираем пункт Administration (Администрирование) (рис. 8.6). На панели администрирования выбираем Users (Пользователи) (рис. 8.7). Нажмем кнопку New (Создать) в правом верхнем углу, чтобы перейти на экран добавления пользователя Add User (рис. 8.8). Книги для программистов: https://clcks.ru/books-for-it Принципы работы Docker Compose Е1 1 201 estudent online STATUS 8 online О away О busy О offline @ Custom Status... � Administration 1) Omnichannel & Му Account 0 Logout Рис. 8.6. Панель администрирования в Rocket.Chat AdrrwlistJatiul1 х Users о. Search Users Email .� Name ..., � 2: lrwites Rocket.Cat @rocket.cat 11!11 student 1;1 @sutdent Status: Online student@example.... Online Рис. 8.7. Экран пользователей Rocket.Chat Заполните форму: ♦ Name: hubot ♦ Usemame: hubot ♦ Email: hubot@example.com ♦ Отметьте: Verified (Подтверждено, отмечено синим) ♦ Password: bot-pw! ♦ Roles: bot ♦ Отключите переключатель: Send welcome email (Отправить приветственное сообщение, отмечено серым). Нажмем кнопку Save (Сохранить), чтобы создать пользователя. Чтобы бот мог войти в систему, отключим двухфакторную аутентификацию, кото­ рая включена по умолчанию: выбираем пункт Settings (Настройки) в самом низу панели администрирования в левой части браузера (рис. 8.9). Книги для программистов: https://clcks.ru/books-for-it Глава 8 202 Name 1 huЬot (D lnfo Usemame 1 huЬot @ w lrnport 19 # Rooms Email 1 huЬot@example.com Verified е users � lnvites & Conn8c:tМty Services в Status Мess.ge Viewlogl О1 Custom Sounds • Feder8tlon DнhЬoan:I Bio ф Apps в Emlil lnЬous (а) Custom Emoji Nickname ф 1nt8C,"81io1IS • OAulhApps вм.. Password Custom u- St8tus 1 ........ . Require password change Set random password and send Ьу email (]1 (]1 Roles Select а Role Join default channels Рис. 8.9. Настройки администрирования в Rocket.Chat Settings Send welcome email Accounta Рис. 8.8. Экран добавления пользователя Rocket.Chat Modify workspace memЬer account settings. Рис. 8.10. Настройки аккаунта в Rocket.Chat Оrкроется экран настроек Settings (рис. 8.1 О). В строке поиска вводим totp и нажимаем кнопку Open (Оrкрыть) в окне Accounts (Аккаунты). Оrкроется длинный список настроек (рис. 8.11). Книги для программистов: https://clcks.ru/books-for-it Принципы работы Docker Compose +-- 203 Accounts л Two Factor Authentication о ЕnаЫе Two Factor Authentication lf deactivated, this setting will deactivate all Two Factor Authentication.<br>То force users to use Two Factor Authentication, the admin has to configure the 'user' role to Рис. 8.11. Настройки ТОТР в Rocket.Chat Прокрутим список до раздела Two Factor Authentication (Двухфакторная аутенти­ фикация), откроем его и уберем флажок ЕnаЫе Two Factor Authentication (Вклю­ чить двухфакторную аутентификацию). Нажимаем кнопку Save changes (Сохранить изменения). В правом верхнем углу панели администрирования нажимаем на значок х, чтобы закрыть панель (рис. 8.12). Administration 0 lnfo (!] lmport Рис. 8.12. Закрываем панель администрирования Rocket.Chat На левой панели в разделе Channels (Каналы) выбираем general (общий) (рис. 8.13). * :j:j: general IJ student HasJoined the channel. 1:14 РМ 111 huЬot Has Jomed the channel, 1 :21 РМ Рис. 8.13. Общий канал в Rocket.Chat Если в канале не появилось сообщение Hubot has joined the channel (Hubot присое­ динился к каналу), укажите Docker Compose, что нужно перезапустить контейнер Hubot. В результате Hubot попробует снова войти на сервер чата, ведь теперь у нас есть пользователь для сервиса: $ docker compose restart huЬot Restarting unix_huЬot_l ... done Книги для программистов: https://clcks.ru/books-for-it 204 Глава В Если все получилось, мы сможем вернуться в браузер, чтобы отправлять Hubot команды в окне чата. � Hubot должен автоматически присоединиться к каналу General при входе на сер­ вер, но при необходимости мы можем отправить в канал следующее сообщение, чтобы явно пригласить Hubot: / invite @huЬot Мы можем получить сообщение от внутреннего администратора rocket.cat, в кото­ ром будет сказано, что сервис уже присоединился (@hubot is already in here). Это нормально. Переменные среды, которые использовались для конфигурации Hubot, определяют для него псевдоним в виде точки. Давайте введем . help, чтобы убедиться, что бот отвечает. Если все работает, мы получим список команд, которые бот готов прини­ мать: > help adapter - Reply with the adapter echo <text> - Reply back with <text> help - Displays all of the help commands that this bot knows aЬout. help <query> - Displays all help commands that match <query>. ping - Reply with pong time - Reply with current time Введем команду из списка, чтобы получить ответный сигнал: . ping Hubot должен в ответ прислать PONG. Если мы введем: . time Hubot в ответ пришлет время, заданное на сервере. Давайте теперь попробуем создать новый канал, введя /create zmachine в окне чата. Мы можем нажать на новый канал zmachine на левой панели и пригласить Hubot в чат командой /invite @huЬot. В ответ мы можем получить от Hubot сообщение: There's по game for zmachine! � (для zmachine нет игр). Это нормально. � Теперь введем в окне чата следующие команды, чтобы поиграть в чат-версию зна­ менитой игры Colossal Cave Adventure: . z start adventure more look go east examine keys get keys z save firstgame z stop z start adventure z restore firstgame inventory Книги для программистов: https://clcks.ru/books-for-it Принципы работы Docker Compose 1 205 Осторожно! Игра затягивает. Если вы в нее еще не играли и хотите узнать больше, изучите следующие ресурсы: 1) Интерактивная литература (interactive fiction); 1 2) Симулятор; 3) Разработка; 4) Игры ; 5) Соревнования. Итак, вы увидели, как с помощью Docker Compose можно легко настраивать, за­ пускать и администрировать сложные веб-сервисы, которым требуется несколько компонентов для выполнения задачи. В следующем разделе мы подробнее рас­ смотрим возможности Docker Compose. Мы можем упростить подготовку Rocket.Chat, используя MongoDB с предваритель­ но настроенной базой данных Rocket.Chat, но мы рассмотрели подробный пр�мер, чтобы вы понимали, как это работает. Применение Docker Compose Итак, у нас есть полный стек Rocket.Chat, и мы понимаем, что делает приложение. Теперь можно подробнее рассмотреть, как работают сервисы. Некоторые стандарт­ ные команды Docker также могут использоваться как команды Compose, но для определенного стека, а не одного контейнера или всех контейнеров на хосте. С по­ мощью команды docker compose top мы можем посмотреть контейнеры и выполняю­ щиеся в них процессы: $ docker compose top compose-huЬot-1 UID PID CMD /usr/Ьin/qemu-x86_64 /Ьin/sh /Ьin/sh -с node -е "console.l... " 1001 73342 /usr/Ьin/qemu-x86_64 /usr/local/Ьin/node node node modules/... 1001 73459 compose-mongo-1 CMD UID PID 1001 71243 ... /usr/Ьin/qemu-x86_64 /opt/Ьitnami/mongodЬ/Ьin/mongod /opt/... compose-rocketchat-1 CMD UID PID 65533 71903 ... /usr/Ьin/qemu-x86_64 /usr/local/Ьin/node node main.js compose-zmachine-1 CMD UID PID root 71999 /usr/Ьin/qemu-x86_64 /usr/local/Ьin/node node /root/src/server.js root 75078 /usr/Ьin/qemu-x86_64 /root/src/.. /frotz/dfrotz /root/src/ ... Так же, как можем войти в выполняющийся контейнер Linux с помощью команды docker container ехес, мы можем выполнять команды в контейнерах через Docker 1 Полный URL: https://ifaгcЬive.oгg/indexes/if-aгcЬiveXgamesXzcode.html. Книги для программистов: https://clcks.ru/books-for-it 206 Глава В Compose с помощью docker compose ехес. Поскольку инструмент docker compose новее, у него есть удобные сокращения по сравнению со стандартными командами docker. При использовании docker compose ехес не нужно передавать параметры -i -t, и мы можем указать имя сервиса Docker Compose, чтобы не запоминать имя или иденти­ фикатор контейнера: $ docker compose ехес mongo bash I have no name!@0078134f9370:/$ mongo MongoDB shell version v4.4.15 connecting to: mongodЬ://127.0.0.1:27017/?compressors;disaЫed&.. . Implicit session: session { "id" : UUID("daec9543-ЬЬ9c-4e8c-baбb... ") MongoDB server version: 4.4.15 rs0:PRIМARY> exit Ьуе I have no name!@0078134f9370:/$ exit exit Команды docker compose logs и docker compose ехес, пожалуй, самые полезные для решения проблем. Если Docker Compose совсем не может собрать образ или за­ пустить контейнер, используйте стандартные команды docker, чтобы отладить об­ раз и контейнер, как обсуждалось в разделах "Решение проблем со сборками" гла­ вы 4 и "Что происходит в контейнере" главы 6. С помощью Docker Compose мы можем запустить (start), остановить (stop) и в большинстве сред приостановить (pause) и возобновить (unpause) один контейнер или все контейнеры: $ docker compose stop zmachine [+] Running 1/1 :: Container compose-zmachine-1 $ docker compose start zmachine [+] Running 2/2 :: Container compose-mongo-1 :: Container compose-zmachine-1 $ docker compose pause [+] Running 4/0 "Container compose-mongo-1 "Container compose-zmachine-1 .. Container compose-rocketchat-1 .. Container compose-huЬot-1 $ docker compose unpause [+] Running 4/0 "Container compose-zmachine-1 "Container compose-huЬot-1 " Container compose-rocketchat-1 "Container compose-mongo-1 Stopped 0.3s Healthy Started 0.5s 0.4s Paused Paused Paused Paused 0.0s 0.0s 0.0s 0.0s Unpaused Unpaused Unpaused Unpaused 0.0s 0.0s 0.0s 0.0s Наконец, когда мы захотим завершить работу и удалить все контейнеры, созданные Docker Compose, мы можем выполнить следующую команду: Книги для программистов: https://clcks.ru/books-for-it Принципы работы Docker Compose $ docker compose down [+] Running 5/5 "Container compose-huЬot-1 "Container compose-zmachine-1 "Container compose-rocketchat-1 "Container compose-mongo-1 :: Network compose _botnet � � Removed Removed Removed Removed Removed 207 10.4s 0.ls О.бs 0.95 0.ls Если удалить контейнер MongoDB командой docker compose down, то все данные в экземпляре MongoDB будут потеряны. Управление конфигурацией У Docker Compose есть возможности, которые заметно повышают гибкость файлов docker-compose.yaml. В этом разделе мы посмотрим, как не писать фиксированные значения конфигурации в файлах docker-compose.yaml, но при этом легко исполь­ зовать их по умолчанию. Значения по умолчанию Если мы посмотрим на раздел services:rocketchat: environment в файле docker­ compose.yaml, то увидим что-то вроде этого: environment: RESPCND то I:М: true 11 11 НUВОТ ALIAS: 11 11 LISТEN 00 ALL PUВLIC: true 11 RОСКЕТСНАТ_АUТН: password" RОСКЕТСНАТ URL: "rocketchat:3000" RОСКЕТСНАТ RОСМ: " 1 RОСКЕТСНАТ USER: "huЬot" RОСКЕТСНАТ_PASSIORD: "bot-pw ! " ВОТ NАМЕ: 11 bot" EXТERNAL_SCRIPТS: "huЬot-help,huЬot-diagnostics,huЬot-zmachine 11 НUВOТ_ZМACHINE_SERVER: "http://zmachine:80" НUВОТ ZМACHINE ROCМS: "zmachine" НUВОТ ZМACHINE (Л PREFIX: "ot" • 11 11 1 Если мы заглянем в docker-compose-defaults.yaml в том же каталоге, тот же раздел будет выглядеть следующим образом: environment: RESPCND_ТO_Ш: ${HUBar_RESPOND_TO_DM:-true} НUВOТ_ALIAS: ${HUBOT_ALIAS:-. ) LISТEN_CN_ALL_PUВLIC: ${HUBm'_LISTEN_ON_ALL_PUBLIC:-true) RОСКЕТСНАТ_АUТН: ${HUBOT_ROCKETCНAT_AUTH:-password) RОСКЕТСНАТ URL: ${HUBm'_ROCKETCНAT_URL:-rocketchat:3000) Книги для программистов: https://clcks.ru/books-for-it 208 Глава 8 RОСКЕТСНАТ RОСМ: ${НUВОГ_RОСКЕТСНАТ_RООМ:-} RОСКЕТСНАТ USER: ${HUBOГ_ROCKEТCНAT_USER:-huЬot} RОСКЕТСНАТ_PASSIORD: ${нuваг_RОСКЕТСНАТ-PASSWORD:-bot-pw ! } ЮТ_NАМЕ: ${НUВОГ_ВОГ_NАМЕ:-Ьоt} Пl'ERNAL_SCRIPТS: ${HUBOГ_EXTERNAL_SCRIPТS:-huЬot-help, huЬot-diagnostics,huЬot-zmachine} НUВOТ_ZМIICIIINE_SERVER: ${HUBOГ_ZМACHINE_SERVER:-http://zmachine:80} НUВOТ_ZМ11CHINE_ROCМS: ${HUBOГ_ZМACHINE_ROOМS:-zmachine} НUВOТ_ZМIICIIINE_OТ_PREFIX: ${HUBOГ_ZМACHINE_OГ_PREFIX:-ot} Здесь используется интерполяция переменных (https://docs.docker.com/compose/ compose-flle/#interpolation), которую Docker Compose позаимствовал напрямую из распространенных командных оболочек Unix вроде bash. В исходном файле для переменной среды ROCKETCНAT_PASSWORD указано фиксированное значение "bot-pw! ": RCX:КEТCНAT_PASSIORD: "bot-pw!" Применяя новый подход, мы указываем, что для ROCКETCНAT_PASSWORD нужно задать значение переменной нuваг_RОСКЕТСНАТ_PASSWORD, если оно задано в среде пользователя, а если нет, то для RОСКЕТСНАТ_PASSWORD нужно задать значение по умолчанию bot-pw ! : RCX:КE'l'CНAT_PASSIORD: ${НUВОГ_RОСКЕТСНАТ_PASSWORD:-bot-pw ! } Такой подход дает больше гибкости, потому что мы можем настраивать почти что угодно, при этом указывая значения по умолчанию для большинства распростра­ ненных ситуаций. Давайте протестируем это, выполнив команду docker сопроsе up с новым файлом: $ docker сопроsе -f docker-corrpose-defaults.yaml up -d Running 5/5 :: Network corrpose_botnet .. Container corrpose-mongo-1 "Container corrpose-rocketchat-1 .. Container coщ:юse-zmachine-1 .. Container corrpose-huЬot-1 [+] Created Healthy Started Started Started 0.0s 31.0s 31.2s 31.5s 31.8s По умолчанию мы получим тот же стек, который запустили раньше. Однако мы можем вносить изменения, просто задав одну или несколько переменных среды в терминале, прежде чем выполнять команды docker сопроsе: $ docker сопроsе -f docker-corrpose-defaults.yaml down $ docker сопроsе -f docker-corrpose-defaults.yaml config 1 \ grep ROCКETCНAT_PASSWORD ROCKETCНAT_PASSWORD: bot-pw 1 $ HUBOГ_ROCКETCНAT_PASSWORD="my-unique-pw" docker сопроsе \ -f docker-corrpose-defaults.yaml config 1 \ grep ROCKETCНAT_PASSWORD RОСКЕТСНАТ PASSWORD: my-unique-pw Книги для программистов: https://clcks.ru/books-for-it Принципы работы Docker Compose 209 В этих примерах Docker Compose обрабатывает пустую переменную среды так же, как если бы ей была назначена пустая строка. Если пустая строка является допус­ тимым значением в конкретном случае, следует изменить формат строки подста­ новки переменной: $ {имя_ПЕРЕМЕННОЙ-значение-по-умолчанию J. Прочтите документацию по этой функции (https://docs.docker.com/compose/compose-file/#interpolation), чтобы изучить все ее возможности. Что если мы не хотим предоставлять значение по умолчанию и требуем, чтобы пользователь сам задал значение? Эго легко. Вам может не понравиться, что мы передаем пароль в командной строке, ведь его будет видно в списке системных процессов, но не волнуйтесь, с этой проблемой мы разберемся чуть позже. Обязательные значения Чтобы задать обязательное значение, мы немного меняем строку подстановки переменной. Передавать пароль по умолчанию - плохая идея, так что давайте сде­ лаем его обязательным значением. В файле docker-compose-defaults.yaml переменная RОСКЕТСНАТ _PASSWORD определена следующим образом: ROCКE'l'CНAT_PASSWORD: ${HUBOТ_ROCKETCНAT_PASSWORD:-bot-pw!} В более новом файле docker-compose-env.yaml мы видим следующее определение: ROCКEТCНAT_PASSWORD: ${HUBOТ_ROCKEТCНAT_PASSWORD:?HUBOТ_ROCKETCНAT_PASSWORD must Ье set!) Вместо значения по умолчанию здесь определяется строка ошибки, которая возни­ кает, если в среде для переменной не указана непустая строка. Если мы запустим сервис, получим сообщение об ошибке: $ docker compose -f docker-compose-env.yaml up -d invalid interpolation format for services.huЬot.eпvironment.ROCKETCНAT PASSWORD. You may need to escape any $ with another $. required variaЫe HUBOТ_ROCKEТCНAT_PASSWORD is missing а value: НUВОТ-RОСКЕТСНАТ-PASSWORD must Ье set! По выводу можно понять, что не так, и в последних двух строках это очевидно. В последней строке мы видим конкретное сообщение об ошибке, которое мы опре­ делили ранее, - для него можно указать текст в зависимости от ситуации. Если мы передадим свой пароль, все заработает: $ HUBOТ_ROCKETCНAT_PASSWORD="a-Ь3tt3r-pw" docker compose \ -f docker-compose-env.yaml up -d [+] Running 5/5 .. Network compose_botпet Created Healthy .. Container compose-mongo-1 .. Container compose-rocketchat-1 Started 0.0s 31.0s 31.Зs Книги для программистов: https://clcks.ru/books-for-it 210 Глава В :: Container compose-zmachine-1 Started :: Container compose-huЬot-1 Started $ docker compose -f docker-compose-env.yaml down 31.5s 31.Bs Файл dotenv Передать одну переменную среды не так сложно, но если мы хотим передать много пользовательских значений или один секрет, вряд ли стоит использовать для этого локальный терминал. Дпя этого и пригодится файл .env (dotenv). .env - это стандарт специального файла (https://www.dotenv.org/docs/security/env) для программ, которым требуется дополнительная информация о конфигурации, относящейся к локальной среде. В предыдущем примере мы должны были указать пароль, чтобы запустить среду Docker Compose. Мы можем каждый раз передавать переменные в среду, но по не­ скольким причинам это не лучшее решение. Было бы удобно иметь возможность безопасно задавать переменные для среды с одним пользователем и снизить веро­ ятность ошибок. По сути, файл .env представляет собой список пар ключей и значений. Поскольку файл должен быть уникальным для локальной среды и обычно содержит хотя бы один секрет, мы должны убедиться, что случайно не отправим его в систему кон­ троля версий. Если мы используем git, нужно просто внести файл .env в .gitignore, и в нашем случае он уже указан там: $ grep .env ../.gitignore .env Если мы работаем в системе с одним пользователем, теперь можно безопасно соз­ дать файл .env в том же каталоге, где находится файл docker-compose.yaml. В этом примере в файле .env может быть примерно следующее содержимое: нuваг-RОСКЕТСНАТ- PASSWORD=th2l@stPW! Можно добавлять много пар ключей и значений, но мы не будем усложнять и огра­ ничимся паролем. Если выполнить git status после создания этого файла, можно заметить, что git полностью игнорирует новый файл, как мы и хотели: $ git status On branch main Your branch is up to date with 'origin/main'. nothing to cormtit, working tree clean Файл .env не является shеll-скриптом Unix. Между этим форматом и подходом к определению переменных в стандартном shell-cкpиnтe есть небольшие, но важ­ ные различия. Самое важное из них: в большинстве случаев не следует заключать значения в кавычки. В предыдущем разделе, когда мы выполнили docker compose -f docker-compose­ env. yaml up -d без указания HUBOГ_ROCKETCНAT_PASSWORD, мы получили ошибку, но если мы повторим попытку после создания файла .env, все получится: Книги для программистов: https://clcks.ru/books-for-it Принципы работы Docker Compose $ docker сощ:,оsе -f docker-coщ:,ose-env.yaml up -d [+] Running 5/5 .. Network coщ:,ose_botnet Created Healthy Container coщ:,ose-mongo-1 Started Container coщ:,ose-rocketchat-1 .. Container coщ:,ose-zmachine-1 Started .. Container coщ:,ose-huЬot-1 Started .. .. 211 0.0s 31.ls 31.Зs 31.5s 31.8s Давайте убедимся, что значение, указанное для переменной ROCKETCНAT_PASSWORD, сов­ падает с тем, что мы задали в файле .env: $ docker сощ:,оsе \ -f docker-coщ:,ose-env.yaml config 1 grep ROCKETCНAT_PASSWORD ROCKETCНAT_PASSWORD: th2l@stPW! \ Как видите, это именно то значение, которое мы указали в файле .env. Дело в том, что Docker Compose всегда считывает пары "ключ-значение", определенные в фай­ ле .env, который находится в том же каталоге, что и наш файл docker-compose.yaml. Тут важно понимать приоритетность. Сначала Docker Compose считывает все зна­ чения по умолчанию, заданные в файле docker-compose.yaml. Затем он читает файл .env и меняет значения по умолчанию на значения, указанные здесь. Наконец, он переходит к переменным среды, заданным в локальной среде, и переопределяет ранее заданные значения. Это значит, что значения по умолчанию должны быть самыми общими, а затем каждый пользователь определяет собственные значения в файле .env, и, наконец, в редких случаях можно использовать локальные переменные среды, чтобы внести нестандартные изменения для конкретного случая. С помощью этих возможностей Docker Compose помогает собирать воспроизводимые процессы, которые будут достаточно гибкими, чтобы их можно было адаптировать к большинству стандарт­ ных рабочих процессов. В Docker Compose есть и другие возможности, которые мы здесь не рассматрива­ ем, например переопределение файлов (https://docs.docker.com/compose/ extends). Когда вы начнете регулярно использовать Docker Compose, изучите документацию (https://docs.docker.com/compose), чтобы найти другие функции, которые могут пригодиться для ваших проектов. Заключение Мы рассмотрели, какие задачи можно выполнять с помощью Docker Compose и как применять этот инструмент, чтобы упростить себе работу и повысить воспроизво­ димость окружений разработки. В следующей главе мы рассмотрим инструменты для масштабирования Docker в центре обработки данных и в облаке. Книги для программистов: https://clcks.ru/books-for-it ГЛАВА 9 Переход на контейнеры в рабочей среде Мы рассмотрели инструменты для запуска стека контейнеров на одном хосте, а теперь выясним, как проделать это в реальных условиях. В этой главе мы будем разворачивать контейнеры в большой рабочей среде. Вам придется внести очень много корректировок, чтобы адаптировать примеры к своим приложениям и сре­ дам, но вы получите хорошее общее представление о том, как философия Docker работает на практике. Подготовка к развертыванию Перевод собранного и настроенного приложения в рабочие системы - один из самых СЛОЖI;!ЫХ этапов всего цикла разработки. К счастью, контейнеризация упро­ щает эту задачу. Представляете, как сложно и неудобно было загружать товары на корабли до изобретения контейнеров? Ящики, тюки и бочки всех форм и размеров приходилось заносить на суда вручную, а потом так же вручную выгружать, при этом думая, что вынести сначала, чтобы вся эта куча не развалилась, как башня Дженrа (https://en.wikipedia.org/wiki/Jenga). Грузовые контейнеры стандартных размеров произвели настоящий переворот. Эти контейнеры можно упаковывать и сгружать в логическом порядке, и все содержи­ мое прибывает вместе, как положено. В современных портах для погрузки и вы­ грузки используется специальное оборудование. Примерно так же работает Docker. Все контейнеры Linux подцерживают одинаковый внешний интерфейс, а инстру­ менты просто помещают их на нужные серверы, независимо от того, что находится внутри. Если у нас есть рабочая сборка приложения, нам не требуется детально настраивать инструменты для его развертывания. При желании развернуть приложение только на одном сервере достаточно будет интерфейса командной строки docker. Если сер­ веров несколько, то нам потребуется более сложный инструмент для работы с кон­ тейнерами. В любом случае нам придется передать приложению некоторые данные и учесть разные факторы, прежде чем развертывать контейнеризированное прило­ жение в рабочей среде. При развертывании приложения с Docker мы следуем определенной процедуре: 1. Локально собираем и тестируем образ Docker на машине разработки. 2. Собираем официальный образ для тестирования и развертывания, обычно в кон­ вейере непрерывной интеграции или системе сборки. Книги для программистов: https://clcks.ru/books-for-it Переход на контейнеры в рабочей среде 1 213 3. Отправляем образ в реестр. 4. Развертываем образ Docker на сервере, а затем настраиваем и запускаем контей­ нер. Со временем все эти этапы сольются в единый рабочий процесс: ♦ Оркестрация сборки, тестирования и сохранения образов и развертывание контейнеров на серверах в рабочей среде. На деле все несколько сложнее. В двух словах, развертывание в рабочей среде должно отвечать трем критериям: ♦ Процесс должен быть воспроизводимым. При каждом запуске должно происхо­ дить одно и то же, причем в идеале - для всех приложений. ♦ Конфигурация должна обрабатываться автоматически. Мы определяем конфи­ гурацию приложения в определенной среде, а затем точно знаем, что эта конфи­ гурация будет соблюдаться при каждом развертывании. ♦ Должен поставляться исполняемый программный пакет, который можно запустить. Чтобы создать такой процесс, нужно учесть много факторов. Здесь мы рассмотрим общую схему, на которую вы сможете опираться при разработке этого процесса для своего приложения. Docker в рабочих средах Мы рассмотрели разные возможности Docker и обсудили общие стратегии развер­ тывания в рабочей среде. Прежде чем мы перейдем к детальному изучению рабо­ чих контейнеров, давайте посмотрим, как Docker вписывается в традиционную и более современную рабочую среду. При переходе на Docker с традиционной систе­ мы вы выбираете, какие задачи будете делегировать Docker, инструменту развер­ тывания, более комплексной платформе, вроде Kubemetes или облачной системы оркестрации контейнеров. Некоторые задачи вы, возможно, предпочтете оставить традиционной инфраструктуре. Мы успешно переносили множество систем с тра­ диционного подхода на контейнеры, и для этого есть немало эффективных реше­ ний. Давайте рассмотрим обязательные компоненты и структуру современных и традиционных вариантов, чтобы вам проще было сделать выбор. На рис. 9.1 приведено несколько аспектов, которые необходимо учитывать в рабо­ чей среде, современные инструменты для этих аспектов, а также более традицион­ ные системы, которые эти инструменты могут заменить. Мы разделим аспекты на две категории - с одними работает сам Docker, а для других понадобится так на­ зываемая платформа. Платформа - это система, которая обычно охватывает кла� стер серверов· и предоставляет единый интерфейс для управления контейнерами Linux. Это может быть единая система, вроде Kubemetes или Docker Swann, или несколько взаимодействующих компонентов. При переходе на полностью контей­ неризированную систему с планировщиком платформа может дублировать некото­ рые задачи. Давайте рассмотрим каждый из этих аспектов. Книги для программистов: https://clcks.ru/books-for-it 214 Глава 9 Системный компонент Аспект Устаревший компонент Приложение Платформа Kubernetes, Docker Swarm, Mesos (с Marathon или Aurora) Обнаружение сервисов Планирование Мониторинг Статические балансировщики нагрузки Скрипты развертывания, оркестрация Nagios/Sensu, дежурные инженеры Syslog, rsyslog, logfiles Docker Engine Скрипты развертывания, оркестрация, FAT jar, ZIР-файлы, ТАR-файлы, git clone, scp, rsync Виртуальные машины Puppet, Chef Система init Скрипты развертывания Рис. 9.1. Роль Docker в рабочей системе На рис. 9.1 приложение находится на верхушке стека. Оно полагается на остальные аспекты рабочей среды. В некоторых случаях мы целенаправленно работаем с эти­ ми аспектами, а в других нам на помощь приходят неожиданные инструменты. В любом случае в рабочей среде приложению так или иначе требуются почти все эти аспекты, и мы должны это учитывать. Если вы хотите перейти на контейнеры Linux, подумайте, как вы решаете эти задачи сейчас и как будете решать их в новой системе. Мы начнем с того, что нам хорошо известно, а потом будем двигаться снизу вверх. Мы хорошо знаем наше приложение, и оно находится на вершине стека. Все остальное помогает обеспечивать функционал, но именно приложение имеет цен­ ность для бизнеса, а другие аспекты просто поддерживают его, позволяют надежно работать в большом масштабе и стандартизируют его работу. Порядок элементов под приложением важен, но отдельно каждый верхний слой не зависит от ниж­ них - все они нужны для самого приложения. Поскольку контейнеры Linux и Docker обеспечивают большую часть этого функ­ ционала, контейнеризация системы упростит выбор в этих аспектах. Когда мы дой­ дем до той части стека, которая обеспечивается платформой, будет сложнее, и сна­ чала нужно разобраться в том, что находится ниже. Начнем с управления заданиями. Управление заданиями Управление заданиями Uob control) - важное требование в современных разверты­ ваниях. На нашей схеме (см. рис. 9.1) оно входит в нижний блок. Ни одна система не будет работать без управления заданиями. Обычно эту задачу выполняет опера­ ционная система или одна из систем инициализации Linux (systemd, System v init, runit, вsо rc и т. д.). Мы указываем для операционной системы информацию о про­ цессе, который хотим запустить, а затем настраиваем поведение для его перезапус­ ка, перезагрузки конфигурации и управления жизненным циклом приложения. Книги для программистов: https://clcks.ru/books-for-it Переход на контейнеры в рабочей среде 1 215 За остановку и запуск приложения отвечают эти системы. Кроме того, в некоторых случаях они позволяют приложению работать более стабильно. Например, переза­ пускают его при сбое. Разным приложениям требуются разные подходы к управле­ нию заданиями. В традиционной системе Linux можно запускать и останавливать задания по времени с помощью cron. Инструмент systemd может перезапускать при­ ложение при сбое. Подход к выполнению этих задач зависит от особенностей сис­ темы. Увы, различных реализаций существует очень много. Если мы переходим на контейнеры, нам нужен способ снаружи управлять всеми заданиями более или менее одинаково. Нам потребуется чуть больше метаданных о них, чтобы получить желаемый результат, но мы не хотим заглядывать в контей­ нер. Docker Engine предоставляет целый набор команд для управления заданиями, например docker container start, docker container stop, docker container run И docker container kill, которые, по сути, соответствуют основным этапам жизненного цикла приложения. Все платформы, которые строятся на контейнерах Docker, включая Kubemetes, также используют этот жизненный цикл. Мы поместили этот аспект в нижнюю часть стека, потому что это самая низкоуровневая абстракция, которую Docker предоставляет для приложения. Даже если бы мы не задействовали никакие другие возможности Docker, это все равно был бы отличный ·инструмент, ведь он предоставляет одинаковый подход для всех приложений и платформ, на которых выполняются контейнеры Docker. Ограничения ресурсов Над управлением заданиями находятся ограничения ресурсов (resource limits). В системах Linux мы можем использовать контрольные группы Linux (control groups, cgroups, https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html) напрямую, чтобы управлять ограничениями ресурсов. Это довольно распростра­ ненный подход, но обычно применяются другие инструменты, вроде команды ulimit (https:/Лinuxconfig.orgЛimit-user-environment-with-ulimit-Iinux-command), и разные настройки среды выполнения приложения, например виртуальных машин Java, Ruby или Python. В облачных системах у нас появилась возможность запус­ кать отдельные виртуальные серверы, чтобы ограничивать ресурсы для одного бизнес-приложения. Такой подход позволял защититься от " шумных соседей", но не давал такого детального контроля, как контейнеры. Используя контейнеры Linux, мы можем ограничивать потребление ресурсов кон­ тейнерами с помощью cgroups. Мы сами решаем, хотим ли ограничивать в рабочей среде доступ приложения к памяти, месту на диске или вводу-выводу. Обычно ре­ комендуется установить ограничения, чтобы контролировать потребности прило­ жения. В противном случае мы не сможем воспользоваться одним из главных пре­ имуществ контейнерных приложений и выполнять несколько приложений на одной машине так, чтобы они не мешали друг другу. Мы уже видели, что Docker дает нам такую возможность, и это одна из причин, почему стоит использовать контейнеры. В главе 5 мы рассматривали, с помощью каких аргументов мы управляем ресурса­ ми в Docker. Книги для программистов: https://clcks.ru/books-for-it 216 Глава 9 Сети Подробно о сетях в Docker мы поговорим в главе 11, а здесь мы кратко рассмотрим, как контейнеризированная система может управлять подключением приложений к сети. Docker предоставляет широкий набор вариантов конфигурации для сетей. Выберите для рабочей среды один механизм и используйте его для всех контейне­ ров - разные подходы лучше не комбинировать. Если вы применяете платформу, вроде Kubemetes, некоторые решения будут приняты за вас. К счастью, большая часть сложностей, связанных с сетями, не относится к приложению в контейнере. Docker или платформа сами позаботятся о сетях, а приложение будет одинаково работать в контейнере на локальном компьютере и в рабочей среде, если мы будем следовать нескольким правилам: 1. Docker или платформа сами будут динамически пробрасывать порты и сообщать об этом приложению. Обычно это делается с помощью переменной среды. 2. Избегайте протоколов FTP или RTSP, которые направляют обратный трафик через случайные порты. На контейнеризированной платформе это будет очень неудобно поддерживать. 3. Воспользуйтесь службой DNS, которую предоставляет контейнеру Docker или среда выполнения. Если следовать этим правилам, приложение даже не поймет, где его развернули. Большинство рабочих сред позволяют определять фактическую конфигурацию и применять ее в среде выполнения. Docker Compose, режим Docker Swarm, Kubemetes и среды выполнения облачных провайдеров, вроде ECS, делают все за нас. Конфигурация Всем приложениям нужен доступ к своей конфигурации. Конфигурацию можно разделить на два уровня. На нижнем уровне определяется, как должна быть на­ строена среда Linux вокруг приложения. У контейнеров для этого есть Dockerfile, с помощью которого мы можем всегда создавать одинаковую среду. В более тра­ диционной системе мы могли бы управлять конфигурацией, например, с помощью Chef, Puppet или AnsiЫe. С контейнерами эти системы тоже сочетаются, но обычно мы не применяем их для предоставления зависимостей приложениям. За это отве­ чает Docker и Dockerfile. Даже если содержимое Dockerfile меняется для разных приложений, механизм и инструменты будут одинаковыми, и это очень удобно. Следующий уровень - конфигурация, которая применяется к самому приложе­ нию. Эту тему мы ранее подробно обсуждали. По умолчанию в Docker для этого предназначены переменные среды, и такой подход работает на всех современных платформах. В некоторых системах проще оказываются более традиционные фай­ лы конфигурации. В Kubernetes, например, это относительно просто, но мы не советуем этот подход, если вы стремитесь создать по-настоящему переносимое контейнеризированное приложение. Мы считаем, что это ухудшает наблюдаемость приложения, и не рекомендуем задействовать файлы конфигурации. Подробнее о переменных среды мы поговорим в главе 13. Книги для программистов: https://clcks.ru/books-for-it Переход на контейнеры в рабочей среде 1 217 Упаковка и поставка Упаковку и поставку мы объединим в один раздел. Это область, в которой контей­ неризированная система заметно опережает традиционную. Если вернуться к ана­ логии с грузовыми контейнерами, у нас есть единая стандартная упаковка, образ контейнера, и стандартный способ его перемещения - реестр Docker и инструмен­ ты image pull и image push. В традиционных системах пришлось бы создавать отдель­ ные инструменты развертывания и надеяться, что их можно стандартизировать, чтобы применять для нескольких приложений. При работе в многоязычной среде это привело бы к проблемам. В контейнеризированной среде нужно продумать, как мы будем упаковывать приложения в образы и как эти образы хранить. Самый простой способ хранения - платная подписка в коммерческом реестре образов. Если это допустимо для вашей компании, рассмотрите такой вариант. Несколько облачных провайдеров, включая Amazon, предлагают сервисы для раз­ мещения образов, которые можно развернуть в вашей среде, и это тоже неплохой метод. Конечно, мы можем создать частный реестр, как обсуждалось в разделе "Управление частным реестром" главы 4. Нам доступно немало решений, и следу­ ет рассмотреть разные варианты. Журналирование Журналирование - это аспект, который находится на границе между задачами, которые мы можем поручить Docker, и задачами, которыми будет управлять плат­ форма. Как мы уже видели в главе 6, Docker может собирать все журналы из кон­ тейнеров и передавать их в другое место. По умолчанию журналы остаются в ло­ кальной системе. Это очень удобно для сред ограниченного размера, и вы можете выбрать такой вариант, если вам подходит хранилище на локальном хосте. Ваша платформа будет обрабатывать журналы от множества приложений на различных системах, так что, возможно, стоит централизовать журналирование, чтобы улуч­ шить видимость и упростить устранение неисправностей. При проектировании такой системы опирайтесь на главу 6, где мы подробно рассматриваем журналиро­ вание. Некоторые системы, например Kubemetes, используют свои подходы к сбо­ ру журналов. Со стороны приложения нам достаточно указать, что оно отправляет записи в stdout или stderr, а Docker или платформа позаботятся об остальном. Мониторинг Это первый аспект системы, который Docker или контейнеры Linux не берут на се­ бя полностью, но он все же получает преимущества от стандартизации, обеспечи­ ваемой Docker. Возможность проводить стандартные проверки работоспособности, которые мы обсуждали в главе 6, упрощает мониторинг работоспособности прило­ жений. Во многих системах за мониторинг отвечает сама платформа, а планиров­ щик динамически завершает работу неработоспособных контейнеров и даже может перенести рабочую нагрузку на другой сервер или перезапустить ее в той же сис­ теме. В более старых системах мониторинг контейнеров реализован с помощью Книги для программистов: https://clcks.ru/books-for-it 218 Глава 9 существующих решений, таких как Nagios, Zabbix или другие традиционные сис­ темы мониторинга. Как мы видели в главе 6, существуют и более современные ва­ рианты, например Prometheus. Коммерческие решения для мониторинга произво­ дительности приложений (Application Performance Monitoring, АРМ), например New Relic, Datadog или Honeycomb, предлагают первоклассную поддержку для контейнеров Linux и приложений внутри них. Если вы уже используете подобное решение для мониторинга своего приложения, скорее всего, вам почти не придется ничего менять. В более старых системах при возникновении проблем приходилось привлекать ин­ женеров, которые принимали решения о необходимых мерах. В динамических сис­ темах эти процессы во многом автоматизированы и выполняются в рамках плат­ формы. В переходный период система будет основана на комбинации подходов, и инженеры будут получать оповещения, только если платформа не может решить проблему. В любом случае окончательное решение всегда за человеком, но в кон­ тейнеризированных системах решать проблемы гораздо проще, потому что у нас есть стандартизированные механизмы, единые для нескольких приложений. Планирование Как распределить сервисы по серверам? Контейнеры можно легко перемещать, по­ тому что Docker предоставляет для этого удобные механизмы. Мы можем оптими­ зировать потребление ресурсов, повысить надежность приложения, обеспечить са­ мовосстановление сервисов и добиться динамического масштабирования. Какой инструмент должен принимать эти решения? В более старых системах мы выделяли сервису несколько серверов. В скриптах развертывания мы прописывали список серверов, и при каждом развертывании но­ вое приложение работало только на этом наборе серверов. Модель с одним серви­ сом на сервер была широко распространена при первых попытках виртуализации в частных центрах обработки данных. Облачные системы поддерживали эту мо­ дель, предоставляя удобные виртуальные серверы с нужными характеристиками. Отчасти это динамическое поведение в системах, вроде АWS, обрабатывалось с помощью автоматического масштабирования. Но если мы переходим на контей­ неры и начинаем запускать много сервисов на одном виртуальном сервере, мас­ штабирование и динамические изменения на уровне сервера нам не помогут. Распределенные планировщики Распределенные планировщики используют Docker, чтобы мы могли смотреть на целую сеть серверов так, будто это один компьютер. Мы назначаем политики, ука­ зывая, как должно выполняться приложение, и система сама определяет, где и сколько экземпляров запускать. Если что-то идет не так на сервере или в приложе­ нии, планировщик запускает его снова на доступном работоспособном ресурсе, ко­ торый отвечает требованиям приложения. Этот подход соответствует изначальной задумке основателя Docker, lnc. Соломона Хайкса (https://www.linkedin.com/in/ solomonhykes)- мы хотим запускать приложения где угодно, не беспокоясь о том, Книги для программистов: https://clcks.ru/books-for-it Переход на контейнеры в рабочей среде 1 219 как они будут развертываться. Как правило, нулевой простой при развертывании в этой модели достигается с помощью сине-зеленого развертывания (https:// martinfowler.com/Ыiki/ВlueGreenDeployment.html) - это подход, при котором мы запускаем новое поколение приложения наряду со старым, а затем медленно переносим трафик со старого стека на новый. Если использовать знаменитую метафору Келси Хайтауэра (Kelsey Hightower) (https://youtu.be/НIAXp0-M6SY?t=10m23s), планировщик будто играет в "Тетрис", на лету размещая сервисы по серверам, подбирая лучшее соответствие. Kubernetes (https://kubernetes.io/), представленный Google в 2014 году, не быллер­ вым в своем роде - сначала появились Mesos и Cloud Foundry, - но сейчас он бесспорный лидер среди планировщиков в контейнерных системах. В ранних вы­ пусках Kubernetes учитывался опыт Google с собственной внутренней системой Borg (https://kubernetes.io/Ыog/2015/04/borg-predecessor-to-kubernetes), который стал доступен сообществу в виде открытого исходного кода. Kubernetes изначально строился на Docker и контейнерах Linux и поддерживает не только Docker containerd, но и несколько других сред выполнения контейнеров, которые также ис­ пользуют контейнеры Docker. Kubernetes - комплексная система со множеством компонентов. У Kubernetes есть много коммерческих и облачных разновидностей. Cloud Native Computing Foundation (https://landscape.cncf.io/members?category= certifie� -ku bernetes-distribu tio n %2С certified-kubernetes-hosted %2 Ccertified­ kubernetes-installer&grouping=category) предлагает сертификацию, чтобы каждый дистрибутив отвечал определенным стандартам, принятым в сообществе Kuber­ netes. Kubernetes - очень эффективный инструмент, но эта сфера постоянно разви­ вается, и за нововведениями довольно сложно уследить. Если вы разрабатываете новую систему с нуля, рассмотрите Kubernetes. Если у вас нет другого опыта и вы используете облако, то самым простым вариантом будет реализация, предлагаемая вашим провайдером. Kubernetes - это популярная платформа, но далеко не един­ ственный вариант. В 2015 году компания Docker, Inc. представила режим Docker Swarm, изначально разработанный под Docker. Это неплохой вариант, если вам требуется очень про­ стой инструмент оркестрации, который полностью находится в пределах платфор­ мы Docker и поддерживается одним вендором. Режим Docker Swarm так и не стал популярным, и поскольку Docker активно интегрирует Kubernetes, это не самый очевидный вариант. Оркестрация Когда мы говорим о планировщиках, то часто имеем в виду не только распределе­ ние заданий по ресурсам, но и возможности оркестрации, т. е. администрирования и организации приложений и развертываний по всей системе. Планировщик может автоматически перемещать задания на лету или разрешать вам запускать задачи на отдельных серверах. В более старых системах для этого были предусмотрены спе­ циальные инструменты оркестрации. В большинстве современных контейнерных систем все задачи по оркестрации, включая планирование, обрабатываются основным программным обеспечением Книги для программистов: https://clcks.ru/books-for-it 220 1 Глава 9 для управления кластером, например Kubemetes, Swaпn, системой управления кон­ тейнерами конкретного провайдера и т. д. Планирование, несомненно, лучшая среди всех возможностей платформы. Хотя именно из-за нее приходится больше всего переделывать приложения при переносе в контейнер. Многие традиционные приложения разрабатывались без поддержки обнаружения сервисов и изменения выделенных ресурсов, так что приходится про­ делать большую работу, чтобы адаптировать их к действительно динамичной сре­ де. Поэтому при переносе приложения в контейнеризированную систему поначалу вы можете не применять планировщик. Часто лучше начать с контейнеризации приложений в традиционной системе, и только затем переходить к более динамич­ ной системе планирования. Иначе говоря, сначала приложение будет работать в контейнерах на тех же серверах, где было развернуто изначально, а когда вы убе­ дитесь, что все работает, добавляйте планировщик. Обнаружение сервисов Обнаружение сервисов - это механизм, с помощью которого приложение находит все сервисы и ресурсы, необходимые для работы. Редкое приложение обходится без зависимостей от внешних компонентов, так что обнаружение сервисов не пона­ добится разве что статическим сайтам, не имеющим состояния. Все остальные должны каким-то образом получать информацию об окружающей системе. Обычно в этом задействовано несколько систем, но они тесно связаны друг с другом. Возможно, вы удивитесь, но в традиционных системах обнаружением сервисов в основном занимаются балансировщики нагрузки, которые отвечают за надеж­ ность и масштабирование, а еще они следят за всеми эндпоинтами, связанными с определенным сервисом. Иногда они настраиваются вручную, иногда работают динамически, но другие системы находят конечные точки сервиса с помощью из­ вестного адреса или имени через балансировщик нагрузки. Это в своем роде обна­ ружение сервисов, и в более старых системах для этого широко использовались балансировщики нагрузки. В современных средах они тоже часто применяются в этих целях, даже если мало напоминают традиционные балансировщики нагруз­ ки. Другие варианты обнаружения сервисов в старых системах - статические кон­ фигурации базы данных или файлы конфигурации приложения. Как вы видели на рис. 9.1, Docker не выполняет обнаружение сервисов, разве что при использовании режима Docker Swarm. В подавляющем большинстве систем за обнаружение сервисов отвечает платформа. Получается, что это первый аспект, которым вам нужно будет заняться в более динамичной системе. Контейнеры легко перемещаются, и традиционная система, построенная для статически развертывае­ мых приложений, их не поддержит. Каждая платформа по-разному реализует обна­ ружение сервисов, и вам следует выбрать вариант, подходящий для вашего случая. Docker Swarm (классический Swarm) (https://github.com/docker-archive/classicswarm) и режим Docker Swarm - это не одно и то же. Подробнее о режиме Docker Swarm мы поговорим в главе 10. Книги для программистов: https://clcks.ru/books-for-it Переход на контейнеры в рабочей среде 1 221 Примеры механизмов обнаружения сервисов, с которыми вы, возможно, работали: ♦ балансировщики нагрузки со стандартными адресами; ♦ Round-roЬin DNS; ♦ SRV-записи DNS; ♦ динамический DNS; ♦ многоадресный DNS; ♦ оверлейные сети со стандартными адресами; ♦ протоколы Gossip; ♦ протокол Apple Bonjour (https://en.wikipedia.org/wiki/Вonjour_(software)); ♦ Apache ZooKeeper (https://zookeeper.apache.org/); ♦ HashiCorp's Consul (https://www.consul.io/); ♦ etcd (https://etcd.io/). Как видите, список длинный, но вариантов гораздо больше. Некоторые из этих сис­ тем также выполняют и другие задачи, помимо обнаружения сервисов, и это может еще больше запутать. Пример обнаружения сервисов, на котором удобно изучать эту концепцию, - механизм связывания, используемый Docker Compose в главе 8. Этот механизм задействует систему DNS, поставляемую ctockerd, которая позволяет одному сервису в Docker Compose указывать имя другого сервиса и возвращать IР-адрес нужного контейнера. Kubemetes в простейшем виде тоже предлагает по­ добную систему с внедренными переменными среды. Но это самые простые формы обнаружения в современных системах. Часто интерфейс для этих систем предусматривает стандартные имена и/или порты для сервиса. Мы можем указать http://service-a.example.com, чтобы обратиться к сервису А по стандартному имени. Или же по http://services.example.com:service­ a-port можно обратиться к этому же сервису по имени и порту. В современных средах часто принят другой подход. В новой системе этот процесс обычно управля­ ем и довольно органичен. С платформы обращаться к более традиционным систе­ мам проще, чем в обратном направлении. Обычно лучше начать с системы, где мы представляем динамически настраиваемые балансировщики нагрузки, к которым легко обращаться из старой среды. Хотя в долгосрочной перспективе это не иде­ альное решение. Kubernetes для этого предоставляет маршруты Ingress, и это не­ плохой вариант для пользователей этой платформы. Примеры реализации такого подхода: ♦ в Kubemetes - контроллеры Ingress 1, включая Traefik2 или Contour (https://projectcontour.io/) и др.; ♦ Linkerd service mesh (https://linkerd.io/); 1 Полный URL-aдpec: https://kubernetes.io/docs/concepts/services-netw orking/ingress. 2 Полный URL-aдpec: https://doc.traefik.io/traefik/providers/kubernetes-ingress. Книги для программистов: https://clcks.ru/books-for-it 222 1 Глава 9 ♦ отдельный Sidecar (https://github.com/NinesStack/sidecar) для обнаружения сервисов с прокси Lyft Envoy (https://github.com/envoyproxy/envoy); ♦ Istio service mesh (https://istio.io/) и Lyft Envoy. Если в вашей системе сочетаются современные и традиционные подходы и техно­ логии, направить трафик в новую контейнеризированную систему обычно сложнее, и следует обдумать все заранее. Заключение по рабочей среде Многие люди начинают с простых инструментов оркестрации Docker. Однако когда контейнеров станет больше и вы будете развертывать их чаще, без распреде­ ленных планировщиков не обойтись. Такие инструменты, как Kubernetes, позволя­ ют абстрагировать отдельные серверы и целые центры обработки данных в огром­ ные пулы ресурсов для выполнения контейнеров. Конечно, сушествуют и другие решения, но эти самые популярные и часто упоми­ наемые. В этой сфере постоянно пщ1вляются новы,е технологии, так что изучите тему, прежде чем приступать к делу. В любом случае сначала создайrе и запустите инфраструктуру контейнеров Linux, а затем присматривайтесь к сторонним инструментам. Встроенные инструменты Docker вполне могут подойти для вашей ситуации. Начните с самого простого ин­ струмента для определенной задачи, но помните, что у вас очень широкий выбор, и для контейнеров Linux появляется все больше эффективных решений. Docker в конвейере DevOps Изучив и реализовав весь этот функционал, мы получим надежную рабочую среду. Как узнать, что все работает? Одно из главных преимуществ Docker - возмож­ ность тестировать приложения и все его зависимости в точности в той среде, в ко­ торой оно в итоге будет выполняться. Docker не гарантирует тестирование внешних зависимостей, вроде баз данных, и мы не получаем волшебную платформу тести­ рования, но зато можем протестировать вместе зависимости от библиотек и другого кода. При изменении базовых зависимостей проблемы могут возникнуть даже у организаций с продуманными стратегиями тестирования. С помощью Docker мы можем собрать образ, запустить его на компьютере разработки, а потом протести­ ровать тот же образ в конвейере непрерывной интеграции, прежде чем отправить на рабочие сервер1;,1. Тестировать приложение в контейнерах не сложнее, чем 'тестировать само при­ ложение, если тестовая среда поддерживает рабочие нагрузки контейнеров Linux. Давайте рассмотрим пример. Краткий обзор Давайте создадим пример рабочей среды для вымышленной компании. Это будет типичная среда, в которую мы затем добавим Docker. Книги для программистов: https://clcks.ru/books-for-it Переход на контейнеры в рабочей среде 1 223 У нашей вымышленной компании есть пул рабочих серверов, на которых выпол­ няются демоны Docker и где развернуты разные приложения. У сервера координа­ ции конвейера есть несколько рабочих узлов сборки и тестирования. Пока мы опус­ тим развертывание, и вернемся к нему, когда вымышленное приложение будет про­ тестировано и готово к поставке. На рис. 9.2 изображен типичный рабочий процесс для тестирования контейнеризи­ рованноrо приложения, который включает следующие шаги: 1. Сборка запускается извне - например, с помощью вызова вебхука из репозитория исходного кода или вручную разработчиком. 2. Сервер сборки запускает сборку образа контейнера. 3. На локальном сервере создается образ. 4. Образ помечается тегом с номером сборки или версии или хешем коммита. 5. Для запуска тестового набора настраивается новый контейнер, основанный на собранном образе. 6. Для контейнера выполняются тесты, результат передается серверу сборки. 7. Отмечается, прошла сборка тесты или нет. 8. Если прошла, она передается в реестр образов или другой механизм хранения. Это похоже на обычный процесс тестирования приложений. Как минимум нам тре­ буется задание, которое запускает тестовый набор. С помощью этих действий мы сначала создаем образ контейнера и вызываем в контейнере тестовый набор. Запуск сборки Уведомление о неудаче Рис. 9.2. Схема процесса тестирования в Docker Давайте посмотрим, как этот процесс будет работать в нашем вымышленном при­ ложении. Мы только что обновили приложение и отправили последний код в репо­ зиторий Git. У нас есть хук, который на каждый коммит запускает сборку, так что Книги для программистов: https://clcks.ru/books-for-it 224 Глава 9 на сервере сборки выполняется задание, которое также активирует демон dockerd. Задание на сервере сборки назначает задачу тестовому узлу. На рабочем узле не запущен dockerd, но установлен инструмент командной строки Docker. Мы выпол­ няем команду docker irnage build для удаленного демона dockerd, чтобы создать новый образ на удаленном сервере Docker. Образ контейнера нужно собрать точно таким, каким мы отправим его в рабочую среду. Если для тестирования нужно временно что-то изменить, используем пре­ доставленные извне параметры, либо через переменные среды, либо через аргу­ менты командной строки. Мы хотим протестировать именно ту сборку, которую будем поставлять, так что это очень важно. После сборки образа задание тестирования создаст и выполнит новый контейнер на основе нового образа. Образ настроен для выполнения приложения в рабочей сре­ де, но для тестирования нужно выполнить другую команду. Эго нормально. Docker позволяет просто указать эту команду в конце команды docker container run. В рабо­ чей среде воображаемый контейнер запустит supervisor, который, в свою очередь, запустит экземпляр nginx и экземпляры веб-сервера Ruby Unicom. Для тестирования нам не потребуется nginx и не нужно запускать веб-приложение. Вместо этого зада­ ние сборки вызывает контейнер: $ docker container run -е ENVIRONMENT=testing -е АРI_КЕУ=12345 \ -it awesome_app:versionl /opt/awesome_app/test.sh Мы выполнили docker container run, а еще передали в контейнер пару переменных среды: ENVIRONMENТ и API_REY. Эго могут быть новы� значения или замена для значе­ ний, которые Docker уже экспортировал. Мы также указали определенный тег versio1,l. Благодаря этому тегу мы сможем использовать нужный образ, даже если одновременно будет запущена еще одна сборка. Затем мы переопределяем коман­ ду, которую контейнер должен выполнять, в строке смо в Dockerfile - мы будем вызывать тестовый скрипт /opt/awesome_app/test.sh. В нашем примере это необяза­ тельно, но иногда необходимо переопределить в Dockerfile ENTRYPOINT (--entrypoint), чтобы можно было выполнить в контейнере что-то, кроме команды по умолчанию. Всегда указывайте точный тег Docker (обычно это версия или хеш коммита) для образа в тестовом задании. Если вы всегда указываете просто latest, невозможно гарантировать, что другое задание не заменит этот тег сразу после начала сборки. Если мы указываем точный тег, то можем быть уверены, что тестируем нужную сборку. Обратите внимание, что docker container run завершится с кодом возврата команды, которая вызывалась в контейнере. Эго значит, что по коду выхода мы увидим, успешно ли выполнены тесты. Если тестовый набор хорошо продуман, больше нам ничего не понадобится. Если нужно выполнить несколько шагов или на код выхода невозможно положиться, можно записать все выходные данные тестового прогона в файл, а затем поискать в них сообщения о статусе. Так происходит с нашей вы­ мышленной сборкой: мы выводим выходные данные тестового набора и в послед­ ней строке в test.sh увидим Result: succESS! (Результат: ПРОЙДЕНО) или Result: FAILURE! (Результат: НЕ ПРОЙДЕНО). Если вы применяете этот механизм, убедиКниги для программистов: https://clcks.ru/books-for-it Переход на контейнеры в рабочей среде 1 225 тесь, что проверяете не случайные строки. Например, слово success должно стоять в последней строке файла, и строка должна точно соответствовать стандартному выводу. В этом случае мы смотрим на последюою строку файла, видим success и помечаем сборку как прошедшую тест. Нужно выполнить еще один шаг, связанный с контейнером. Мы хотим отправить образ сборки, которая прошла тест, в реестр. Реестр хранит образы для сборок и развертываний, а также позволяет делиться ими с коллегами и использовать для других сборок. Это место, куда мы складываем успешные сборки с тегами. Скрипт сборки выполнит команду docker irnage tag, чтобы присвоить образу подходящие теги, возможно, включая latest, а затем запустит docker irnage push, чтобы отправить образ в реестр. Вот и все. Как видите, процесс мало чем отличается от тестирования обычного приложения. Мы воспользовались клиент-серверной архитектурой Docker, чтобы вызвать тест не на основном сервере тестирования, а также заключили тесты в кон­ солидированный shеll-скрипт, чтобы получить выходной статус. В целом это очень похоже на большинство современных подходов к системе сборки. Система нашей вымышленной компании позволяет убедиться, что в рабочую среду будут поставляться только приложения, которые прошли тесты на том же дистри­ бутиве Linux, с теми же библиотеками и теми же параметрами сборки. Контейнер можно протестировать с внешними зависимостями, например с базами данных и кешами, без имитаций. Конечно, гарантировать успех на 100% невозможно, но это гораздо ближе к истине, чем рулетка, в которую мы играем, когда создаем системы развертывания без контейнеров. Если вы используете Jenkins для непрерывной интеграции или ищете способ тес­ тировать Docker с масштабированием, существует множество неплохих плагинов (https://plugins.jenkins.io/) для Docker, Mesos и Kubemetes. Многие управляемые � коммерческие платформы предоставляют контейнеризированные среды непре­ рывной интеграции, включая CircleCI (https://circleci.com/) и GitHub Actions (https://github.Ыog/2022-02-02-build-ci-cd-pipeline-github-actions-four-steps). Внешние зависимости Как насчет внешних зависимостей, которые мы пока пропустили? Например, базы данных или экземпляры Memcached или Redis, с которыми нужно протестировать контейнер? Если нашему вымышленному приложению требуется база данных или экземпляр Memcached или Redis, мы должны протестировать эту внешюою зависи­ мость в надежной тестовой среде. Для этого бьmо бы удобно использовать контей­ нер. Приложив некоторые усилия, мы можем сделать это с помощью таких инстру­ ментов, как Docker Compose (https://github.com/docker/compose), который мы подробно обсуждали в главе 8. В Docker Compose задание сборки может выражать некоторые зависимости между контейнерами, а Compose будет органично их под­ ключать. Нам пригодится возможность тестировать приложение в среде, которая очень похожа на реальные условия, и Compose упрощает эту задачу. Нам по-прежнему Книги для программистов: https://clcks.ru/books-for-it 226 1 Глава 9 потребуются тестовые фреймворки для конкретных языков, но эту среду действи­ тельно легко оркестрировать. Заключение Мы увидели, как контейнеризированное приложение взаимодействует с внешней средой и как разграничиваются разные аспекты, а далее рассмотрим, как собирать кластеры Docker для поддержки современных глобальных приложений, доступных непрерывно и по требованию. Книги для программистов: https://clcks.ru/books-for-it ГЛАВА 10 Масштабирование контейнеров Одно из главных преимуществ контейнеров заключается в том, что они не зависят от базового оборудования и операционной системы, поэтому приложение не огра­ ничено определенным хостом или средой. Такой подход позволяет легко масшта­ бировать приложения без сохранения состояния не только в пределах центра обра­ ботки данных, но и на разных облачных платформах, избегая при этом типичных трудностей, возникающих в традиционных системах. Контейнер в одном облаке не отличить от контейнера в другом, - как грузовые контейнеры на разных судах. Многие организации выбирают готовое облачное развертывание контейнеров Linux, чтобы сразу получить все преимущества масштабируемой платформы и не тратить ресурсы на создание собственной системы. Так действительно проще, но создать платформу в облаке или своем центре обработки данных не так уж сложно, и мы рассмотрим некоторые варианты. Все крупные облачные провайдеры предлагают нативную поддержку контейнеров Linux. Лучшие публичные облака для контейнеров Linux: ♦ Amazon Elastic Container Service (https://aws.amazon.com/ecs); ♦ Google Cloud Run (https://cloud.google.com/run); ♦ Azure Container Apps (https://azure.microsoft.com/en-us/services/container-apps). Те же компании предлагают эффективные решения с Kubernetes: ♦ Amazon Elastic Kubernetes Service (https://aws.amazon.com/eks); ♦ Google Kubernetes Engine (https://cloud.google.com/kubernetes-engine); ♦ Azure Kubernetes Service (https://azure.microsoft.com/en-us/services/kubernetes- service). Установить Docker на экземпляре Linux в публичном облаке несложно, но это лишь первый шаг к созданию полноценной рабочей среды. Вы можете сделать это само­ стоятельно или выбрать один из множества инструментов от облачных провайде­ ров, Docker, Inc. и обширного сообщества. Большинство инструментов будут рабо­ тать одинаково хорошо в публичном облаке и вашем центре обработки данных. Среди планировщиков и более сложных систем есть достаточно вариантов, кото­ рые повторяют большую часть функционала, предлагаемого провайдерами публич­ ных облаков. Даже если вы используете публичное облако, у вас могут быть при­ чины выбрать другую среду для контейнеров Linux вместо готового решения. В этой главе мы рассмотрим варианты запуска контейнеров Linux в большом мас­ штабе. Начнем с простого - режима Docker Swarm, - а затем перейдем к более Книги для программистов: https://clcks.ru/books-for-it 228 1 Глава 10 сложным инструментам: Kubemetes и облачным решениям. В этих примерах мы увидим, как с помощью Docker создать очень гибкую платформу для рабочих нагрузок приложения. Режим Docker Swarm Создав среду выполнения контейнеров в виде Docker Engine, инженеры Docker принялись разрабатывать решения для оркестрации отдельных хостов Docker и эф­ фективного размещения на них контейнеров. Первый подобный инструмент назы­ вался Docker Swarm. Мы уже говорили, что есть два инструмента с названием Swarm, и оба от компании Docker, Inc. Изначальный отдельный Docker Swarm теперь называется классическим Docker Swarm (https://github.com/docker-archive/classicswarm), а вторая реализация из­ вестна как режим Swarm (https://docs.docker.com/engine/swarm). Это не отдельный продукт, он встроен в клиент Docker. Встроенный режим Swarm предлагает гораздо больше возможностей, чем отдельный инструмент, и в итоге полностью его заме­ нит. Режим Swarm не нужно устанавливать отдельно, он уже доступен во всех сис­ темах с Docker, и в этом его большое преимущество. Здесь мы будем говорить именно об этой реализации Docker Swarm. Теперь вы понимаете, что у Docker Swarm есть две реализации, и это поможет вам не запутаться, когда будете искать информацию в Интернете. Docker Swarm предоставляет для инструмента командной строки ctocker единый ин­ терфейс, но работать через него можно с целым кластером, а не с одним демоном Docker. Главная задача Swarm - управлять кластерными вычислительными ресур­ сами с помощью инструментов Docker. Он постоянно развивается и теперь содер­ жит несколько плагинов планировщиков с разными стратегиями для назначения контейнеров хостам, а также обеспечивает базовые возможности обнаружения сер­ висов. Однако это по-прежнему всего один компонент более сложного решения. Кластеры Swarm могут содержать один или несколько менеджеров, которые вы­ ступают как центральный хаб управления для кластера Docker. Лучше создавать нечетное число менеджеров. В каждый момент времени только один менеджер ру­ ководит кластером. Добавляя в Swarm новые узлы, мы объединяем их в кластер, которым можно легко управлять с помощью инструментов Docker. Давайте запустим кластер Swaпn. Нам понадобится три или больше серверов Linux, связанных друг с другом по сети. На каждом сервере должен быть установлен по­ следний выпуск Docker Community Edition из официальных репозиториев Docker. ; В главе 3 приводятся инструкции по установке пакетов dock«-ce на Linux. В этом примере у нас будет три сервера UЬuntu с docker-ce. Подключимся по ssh к серверу, который будет менеджером Swarm, а затем вьшолним команду swarrn init, указав IР-адрес для менеджера Swarm: Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 229 $ ssh 172.17.4.1 uЬuntu@172.17.4.1:$ sudo docker swarm init --advertise-addr 172.17.4.1 Swarm initialized: current node (hypysglii5syybd2zewбovuwq) is now а manager. То add а worker to this swarm, run the following command: docker swarm join --token SWМТКN-1-14......a4o55z0lzq 172.17.4.1:2377 То add а manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. � � 1 Вы должны обеспечить безопасность кластера с режимом Docker Swarm, но здесь мы не будем обсуждать этот вопрос. Прежде чем запустить режим Docker Swarm на долговременных системах, изучите все параметры и примите меры для защиты среды. Во многих примерах в этой главе необходимо указывать корректный IР-адрес для � управляющего и рабочих узлов. - Это действие инициализирует менеджер Swaлn, и мы получим токен для узлов, которым нужно присоединиться к кластеру. Сохраните этот токен в безопасном месте, например в менеджере паролей. Не бойтесь его потерять - мы всегда мо­ жем получить его, выполнив в менеджере следующую команду: sudo docker swarm join-token --quiet worker Вы можете проверить свой текущий прогресс, запустив локальный клиент docker с IР-адресом нового менеджера: $ docker -Н 172.17.4.1 system info Swarm: active NodeID: 19gfcj7xwii5deveu3raf4782 Is Manager: true ClusterID: mvdaf2xsqwjwrb94kgtn2mzsm Managers: 1 Nodes: 1 Default Address Pool: 10.0.0.0/8 SuЬnetSize: 24 Data Path Port: 4789 Orchestration: Task History Retention Limi.t: 5 Raft: Snapshot Interval: 10000 NumЬer of Old Snapshots to Retain: О HeartЬeat Tick: 1 Election Tick: 10 Dispatcher: HeartЬeat Period: 5 seconds Книги для программистов: https://clcks.ru/books-for-it 230 Глава 10 СА Configuration: Expiry Duration: 3 months Force Rotate: О Autolock М&nagers: false Root Rotation In Proqress: false Node Address: 172.17.4.1 мanager Addresses: 172.17.4.1:2377 Мы можем просмотреть все узлы, которые сейчас находятся в кластере, следующей командой: $ docker -Н 172.17.4.1 node 1s STATUS НОSТNАМЕ ID 19...82 * ip-172-17-4-1 Ready AVAILAВILITY МANAGER STATUS ENGINE VERSION Active Leader 20.10.7 Теперь можно добавить в кластер Swarm еще два сервера в качестве рабочих узлов. Вот что мы сделали бы в рабочей среде при желании увеличить масштаб, и при ис­ пользовании Swaпn это довольно легко: $ ssh 172.17.4.2 \ "sudo docker swarm join --token SWМТКN-1-14...... a4o55z01zq 172.17.4.1:2377" This node joined а swarm as а worker. $ ssh 172.17.4.3 \ "sudo docker swarm join --token SWМТКN-1-14...... a4o55z01zq 172.17.4.1:2377" This node joined а swarm as а worker. Иногда необходимо добавлять новые управляющие узлы, и это так же просто, как ; добавлять рабочие, - достаточно передать токен присоединения менеджера, а не рабочего узла. Выполните команду docker swarm join-token manager на любом ак­ тивном узле, чтобы узнать токен. Если мы снова выполним команду docker node 1s, то увидим, что в кластере уже три узла, и только один из них помечен как Leader: $ docker -Н 172.17.4.1 node 1s ID HOSTNAМE STATUS AVAILAВILITY МANAGER STATUS ENGINE VERSION 19...82 * ip-172-17-4-1 Ready Active Leader 20.10.7 3d... 7Ь ip-172-17-4-2 Ready Active 20.10.7 ip...qe ip-172-17-4-3 Ready Active 20.10.7 Это все, что нужно сделать для запуска кластера Swarm в режиме Swarm (рис. 10.1). Теперь нужно создать сеть для взаимодействия сервисов. В Swaпn есть сеть по умолчанию - ingress, но мы можем легко создать дополнительные сети для улуч­ шенной изоляции: $ docker -Н 172.17.4.1 network create --driver=overlay default-net ckwh5ph4ksthvx6843ytrl5ik Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров $ docker -Н 172.17.4.1 network 1s NEТWORК ID NАМЕ bridge 494ela1Ьf8f3 default-net xqgshgOnurzu 2e7d2d7aaf0f docker_gwbridge df0376841891 host n8kjdбoa44fr ingress Ь4720еа13Зdб none DRIVER bridge overlay bridge host overlay null 231 SCOPE local swarm local local swarm local Кластер в режиме Docker Swarm Рис. 10.1. Простой кластер в режиме Docker Swarm До сих пор мы занимались только инфраструктурой и не развернули никакой биз­ нес-логики. Давайте запустим в кластере первый сервис с помощью команды: $ docker -Н 172.17.4.1 service create --detach=true --name quantum \ --replicas 2 --puЬlish puЬlished=80,target=8080 --network default-net \ spkane/quantum-game:latest tiwtsЬf270mh83032kuhwv07c Этот сервис запускает контейнеры с игрой Quantum Game - это браузерная голо­ воломка с квантовой механикой, и она поинтереснее, чем очередной Hello World (https://github.com/stared/quantum-game). � Во многих примерах мы указываем тег latest, но не забывайте, что он не подходит для рабочей среды. Здесь нам удобно отправлять обновления с помощью этого тега, но он непостоянный, поэтому его нельзя прикреплять к конкретной долго­ срочной версии. При использовании тега latest развертывания теряют воспроизводимость. В итоге на разных серверах будут развернуты разные версии приложе­ ния. Давайте выполним команду docker service ps с именем созданного сервиса, чтобы посмотреть наши контейнеры: Книги для программистов: https://clcks.ru/books-for-it 232 Глава 10 $ docker -Н 172.17.4.1 service ps quanturn NАМЕ ID IМAGE NODE DESIRED... CURRENT ... ERROR PORTS rk...13 quanturn.1 spkane/qua ... ip-172-17-4-1 Running Running... lz ...tЗ quanturn.2 spkane/ qua ... ip-172-17-4-2 Running Running... Режим Swann использует маршрутизирующую mesh-ceть, чтобы автоматически перенаправлять трафик в контейнер, который может обслужить запрос. Когда мы указываем опубликованный порт в команде docker service create, mesh-ceть позволя­ ет подключиться к этому порту на любом из трех узлов и направляет нас к веб­ приложению. Мы упомянули три узла, но у нас запущено всего два экземпляра. В традиционной системе для этого пришлось бы создать отдельный слой обратного прокси, но режим Swann делает все за нас. Давайте протестируем сервис, введя в браузере IР-адрес любого из узлов: http://172.17.4.l/ Если все работает нормально, отобразится игровая доска Quantum Game (https:// quantumgame.io/): То get а list of all the services, we can use +service ls+: $ docker -Н 172.17.4.1 service ls PORTS ID NАМЕ MODE REPLICAS IМAGE spkane/quanturn-garne:latest *:80-> 8080/tcp iu... 9f quanturn replicated 2/2 Мы видим общую информацию, но иногда этого недостаточно. При необходимости Docker предоставляет подробные метаданные не только о контейнерах, но и о сер­ висах. Для этого выполним команду service inspect: $ docker -Н 172.17.4.1 service inspect --pretty quanturn iuohбoxrec9fk67ybwuikutqa m: Nаше: quanturn Servioe Моdе: Replicated Replicas: 2 Plaoement: Updateeonfig: Parallelism: 1 0n failure: pause Мonitoring Period: Ss Мах failure ratio: О Update order: stop-first RollЬackConfig: Parallelism: 1 0n failure: pause Мonitoring Period: Ss Мах failure ratio: О RollЬack order: stop-first ContainerSpec: Image: spkane/quanturn-garne:latest@sha256:lf57...4a8c Init: false Resouroes: Networks: default-net Endpoint Моdе: vip Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 233 Ports: PuЬlishedPort = 80 Protocol = tcp TargetPort = 8080 PuЫishМode = ingress Здесь много информации, давайте отметим самое важное. Это реплицированный сервис с двумя репликами, как мы видели в выводе команды service 1s. Кроме того, Docker проверяет работоспособность сервиса каждые 5 с. Обновление будет вы­ полняться методом stop-first (сначала остановить), т. е. сначала экземпляров станет N - 1, и только потом запустится новый экземпляр, чтобы их снова стало N. Мы можем всегда запускать N + 1 экземпляров, чтобы во время обновлений не испыты­ вать недостатка в узлах. Для этого нужно указать параметр --update-order=start­ first (сначала запустить) в команде service update. Аналогичное поведение настрое­ но и для отката, и мы также можем изменить его: --rollback-order=start-first. В реальном мире нужно будет не просто запустить сервис, но и менять его масштаб в обе стороны. Было бы очень неудобно, каждый раз развертывать его заново, рис­ куя при этом столкнуться с дополнительными проблемами. К счастью, режим Swarm позволяет масштабировать сервисы одной командой. Чтобы увеличить чис­ ло запущенных экземпляров с двух до четырех, мы можем выполнить следующую команду: $ docker -Н 172.17.4. 1 service scale --detach=false quantUПF4 quanturn scaled to 4 overall progress: 4 out of 4 tasks 1/4: running >] [ 2/4: running =============>] [===== 3/4: running ==================>] 4/4: running ================>] verify: Service converged В предыдущей команде мы указали --detach=false, чтобы было проще увидеть, что � происходит. � Теперь можно задать service ps, что�ы убедиться, что мы добились желаемых результатов. Эту команду мы уже выполняли ранее, но теперь у нас должно быть больше копий. Получается, мы запросили больше копий, чем узлов? $ docker -Н 172.17.4.1 service ps quanturn NАМЕ IМAGE ID NODE DESIRED ... CURRENT ... ERROR PORTS rk ...13 quanturn.1 spkane/quan ... ip-172-17-4-1 Running Running ... lz ...t3 quanturn.2 spkane/quan ... ip-172-17-4-2 Running Running ... mh ...g8 quanturn.3 spkane/quan ... ip-172-17-4-3 Running Running ... сп ...хЬ quanturn.4 spkane/quan... ip-172-17-4-1 Running Running ... Обратите внимание: два сервиса выполняются на одном хаете. Вас это удивило? Возможно, это снижает отказоустойчивость, но по умолчанию приоритет для Книги для программистов: https://clcks.ru/books-for-it 234 Глава 10 Swarm - обеспечить запрошенное число экземпляров, а не распределить отдель­ ные контейнеры по хостам. Если узлов недостаточно, на некоторых будет по не­ сколько копий. В реальном мире стоит тщательно обдумывать вопросы размещения и масштабирования. Что будет, если на одном хосте запущено несколько копий, а мы теряем целый узел? Будет ли приложение с этими ограниченными ресурсами все еще обслуживать пользователей? Развернуть новый выпуск приложения можно с помощью команды docker service update. У этой команды много параметров, но вот один пример: $ docker -Н 172.17.4.1 service update --update-delay 10s \ --update-failure-action rollback --update-monitor 5s \ --update-order start-first --update-parallelism 1 \ --detach=false \ --image spkane/quantum-game:latest-plus quantum quantum overall progress: 4 out of 4 tasks 1/4: running ===>] [ 2/4: running >] 3/4: running >] 4/4: running =�======>] verify: Service converged При выполнении этой команды Swarm обновит сервис, по одному контейнеру за раз, делая паузы между обновлениями. После этого мы сможем перейти по URL­ aдpecy сервиса в режиме инкогнито (чтобы не мешал локальный кеш браузера) и убедиться, что теперь фон зеленый, а не голубой. Итак, мы применили обновление, но как быть, если возникли проблемы? Нам нуж­ на возможность развернуть предыдущий выпуск, чтобы все снова заработало. Мы можем откатиться до предыдущей версии с голубым фоном с помощью команды service rollback, которую мы уже упоминали: $ docker -Н 172.17.4.1 service rollback quantum quantum rollback: manually requested rollback overall progress: rolling back update: 4 out of 4 tasks 1/4: running [> 2/4: running [> 3/4: running [> 4/4: running [> verify: Service converged Это лучший механизм отката для сервиса, не отслеживающего состояние. Нам не приходится следить за предыдущей версией- Docker делает это за нас. Мы просто указываем, что хотим откатить версию, и он извлекает предыдущие метаданные из внутреннего хранилища и выполняет откат. Как и во время развертывания, Docker проверяет работоспособность контейнеров, чтобы убедиться, что откат работает правильно. Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров � 235 Приложение всегда откатывается до последней развернутой версии, так что если мы выполним его несколько раз подряд, лишь две версии будут сменять друг друга. На основе docker service существует команда docker stack, которая позволяет развер­ тывать специальный файл docker-compose.yaml в режиме Docker Swarm или в кла­ стере Kubemetes. Если мы вернемся к репозиторию Git из главы 8, то сможем раз­ вернуть измененную версию этого стека контейнеров в кластере в режиме Swarm: $ git clone https://githuЬ.com/spkane/rocketchat-huЬot-demo.git \ --config core.autocrlf=input В этом репозитории есть каталог stack с измененной версией файла docker­ compose.yaml, который мы использовали раньше: $ cd rocketchat-huЬot-demo/stack При желании запустить его в кластере в режиме Swarm можно было бы вьшолнить следующую команду: $ docker -Н 172.17.4.1 stack deploy --compose-file docker-compose-stack.yaml \ rocketchat Creating network rocketchat_default Creating service rocketchat_huЬot Creating service rocketchat_mongo Creating service rocketchat_rocketchat Creating service rocketchat_zmachine Теперь можно посмотреть, какие стеки находятся в кластере и какие сервисы были добавлены: $ docker -Н 172.17.4.1 stack 1s SERVICES ORCHESTRATOR NАМЕ rocketchat 4 Swarm $ docker -Н 172.17.4.1 service 1s NАМЕ IМAGE ID 2/2 spkane/quantum-game:latest iu... 9f quantum 1/1 rocketchat/huЬot-rocketchat:latest nh...jd ..._huЬot 1/1 spkane/mongo:4.4 gw...qv ..._mongo 1/1 rocketchat/rocket.chat:5.0.4 mЗ ... vd... rocketchat lb... 91 ... zmachine 1/1 spkane/zmachine-api:latest PORTS *:80->8080/tcp *:3001->8080/tcp *:3000->3000/tcp Этот стек предназначен только для демонстрации. Он не был тщательно протес­ тирован для такого применения, но позволяет получить представление о том, как создать аналогичный стек. Возможно, вы заметили, что для запуска всех контейнеров потребуется время, и Hubot продолжит перезапускаться. Это нормально, поскольку Rocket.Chat еще не настроен. Подробнее о настройке Rocket.Chat см. в главе 8. Сейчас мы можем в браузере перейти через порт 3000 к одному из узлов Swarm (например, http://172.17.4.1:ЗOOO/ в этом примере) и увидим страницу начальной настройки Rocket.Chat. Книги для программистов: https://clcks.ru/books-for-it 236 Глава 10 С помощью команды docker stack ps можно посмотреть все контейнеры, управляе­ мые стеком: $ docker -Н 172.17.4.1 stack ps -f "desired-state=running" rocketchat NАМЕ ID IМAGE ...CURRENT SТАТЕ... NODE Running 14 seconds ago huЬot.1 rocketchat/huЬot-rocket... -1 bS...lh... Running 11 minutes ago spkane/mongo:4.4... eq...88... _mongo.1 -2 Running 11 minutes ago rocketchat.1 rocketchat/rocket.chat:... -3 Sx...8u... Running 12 minutes ago zmachine.1 spkane/zmachine-api:lat... -4 rS...х4... Затем мы можем снести стек: $ docker -Н 172.17.4.1 stack rm rocketchat Removing service rocketchat huЬot Removing service rocketchat_mongo Removing service rocketchat rocketchat Removing service rocketchat_zmachine Removing network rocketchat_default Если мы попробуем сразу запустить все заново, могут возникнуть непредвиденные ошибки. Нужно подождать некоторое время, пока кластер не закончит удалять ста­ рую сеть для стека и т. д. Что будет, если на одном из серверов возникла ошибка и нужно его отключить? В этом случае можно вывести все сервисы с одного узла с помощью параметра --availaЬility в команде docker node update. Давайте еще раз посмотрим, какие узлы остались в кластере: docker -Н 172.17.4.1 node 1s HOSTNAМE STATUS ID Ready 19...82 * ip-172-17-4-1 ip-172-17-4-2 Ready 3d...7Ь ip-172-17-4-3 Ready ip... qe AVAILAВILITY Active Active Active МANAGER Leader STATUS ENGINE VERSION 20.10.7 20.10.7 20.10.7 Проверим, где сейчас выполняются контейнеры: $ docker -Н 172.17.4.1 service ps -f "desired-state=running" quantum ID NАМЕ IМAGE NODE DESIRED... CURRENT... spkane/qua... ip-172-17-4-1 sc... lh quantum.1 Running... Running Running spkane/qua... ip-172-17-4-2 ах.. . от quantum.2 Running... р4...8h quantum.3 Running... spkane/qua... ip-172-17-4-3 Running g8...tw quantum. 4 spkane/qua... ip-172-17-4-1 Running Running... ERROR PORTS В предыдущей команде мы использовали фильтр, чтобы в выводе отображались только текущие процессы. По умолчанию Docker также показывает предыдущие контейнеры в виде дерева, чтобы мы видели данные об обновлениях и откатах. Если мы решим, что сервер по адресу 172.17.4. з нужно остановить, мы можем пере­ вести задачи с этого узла на другой хост, изменив состояние availaЬility на drain в Swann: Книги для программистов: https://clcks.ru/books-for-it 237 Масштабирование контейнеров $ docker -Н 172.17.4.1 node update --availaЬility drain ip-172-17-4-3 ip-172-17-4-3 Если мы сейчас проверим узел, то увидим, что для параметра AvailaЬility (доступ­ ность) установлено ctrain: $ docker -Н 172.17.4.1 node inspect --pretty ip-172-17-4-3 m: ipohyw73hvf70td9licnls9qe Hostname: ip-172-17-4-3 Joined at: 2022-09-04 16:59:52.922451346 +0000 utc Status: Ready State: AvailaЬility: Drain Address: 172.17.4.3 Platform: Operating System: linux х86 64 Architecture: Resources: CPUs: Мemory: Plugins: 2 7.795GiB Log: awslogs, fluentd, gcplogs, gelf, journald, json-file, local, logentries, splunk, syslog bridge, host, ipvlan, macvlan, null, overlay Network: Volume: local 20.10.7 Engine Version: ТLS Info: TrustRoot: Issuer SuЬject: Issuer PuЬlic Кеу: ... Как это отразится на сервисе? Мы указали одному узлу прекратить выполнять копии сервиса, и они должны исчезнуть с него или перенестись на другие узлы. Чего мы добились? Давайте снова проверим сервис и убедимся, что все контейнеры с этого хоста перенесены на другой: $ docker -Н 172.17.4.1 service ps -f "desired-state=running" quantum ID NАМЕ IМAGE NODE DESIRED... CURRENT... sc... lhquantum.1 spkane/qua... ip-172-17-4-1 Running Running... ах...omquantum.2 spkane/qua... ip-172-17-4-2 Running Runnirig.. . р4...8hquantum.3 spkane/qua... ip-172-17-4-2 Running... Running g8...twquantum.4 spkane/qua... ip-172-17-4-1 Running Running... ERROR PORTS Сейчас мы можем спокойно остановить узел и сделать все необходимое, чтобы вернуть его в работоспособное состояние. Когда мы будем готовы добавить узел обратно в кластер Swarm, мы выполним следующую команду: $ docker -Н 172.17.4.1 node update --availaЬility active ip-172-17-4-3 ip-172-17-4-3 Книги для программистов: https://clcks.ru/books-for-it 238 Глава 10 · Мы не будем проверять узел снова, но вы всегда можете выполнить команду node inspect, чтобы посмотреть, что там происходит. При возвращении узла в кластер контейнеры не будут автоматически переносить­ ся обратно, но при новом развертывании или обновлении контейнеры равномерно распределятся по узлам. Когда мы закончим, можно будет удалить сервис и сеть следующими командами: $ docker -Н 172.17.4.1 service rm quantшn quantшn $ docker -Н 172.17.4.1 network rm default-net default-net Убедимся, что ни сервиса, ни сети больше нет: $ docker -Н 172.17.4.1 service ps quantшn no such service: quantшn $ docker -Н 172.17.4.1 network 1s NETWORК ID 494elalbf8f3 2e7d2d7aaf0f df0376841891 n8kjdбoa44fr Ь4720еа13Зdб NАМЕ bridge docker_gwbridge host ingress none DRIVER bridge bridge host overlay null SCOPE local local local swarm local Пока это все. Сейчас мы можем остановить все серверы в кластере Swarm, если они нам больше не нужны. Мы проделали большую работу, и вы получили представление о том, как использо­ вать режим Swarm в Docker Engine. Теперь вы можете приступить к созданию кла­ стеров Docker для своих нужд. Kubernetes В этом разделе мы посмотрим, как работает Kubernetes. Впервые Kubernetes был представлен публике на конференции DockerCon (https://events.docker.com/events/ dockercon) в 2014 году. С тех пор эта платформа для контейнеров активно развива­ ется и, пожалуй, является самой популярной. Это не самый старый и не самый зре­ лый проект - Mesos был запущен в 2009, еще до повсеместного распространения контейнеров, - но Kubernetes был разработан специально для контейнерных рабо­ чих нагрузок, предлагает целый ряд возможностей, которых становится все больше, и поддерживается большим сообществом, куда входят давние пользователи Docker и контейнеров Linux. Благодаря этим преимуществам популярность Kubernetes с каждым годом растет. На DockerCon EU в 2017 году компания Docker, Inc. объ­ явила, что поддержка Kubernetes будет встроена в сам Docker Engine. Docker Desktop может запустить кластер Kubernetes с одним узлом, и клиент может развертывать стеки контейнеров в целях разработки. Это удобный переход для разработчиков, которые используют Docker локально и развертывают контейнеры в Kubernetes. Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 1 239 Как и сам Linux, Kubernetes поставляется в разных платных и бесплатных дистри­ бутивах. Нам доступно много вариантов с различной степенью поддержки возмож­ ностей. Kubernetes популярен, так что в нашем распоряжении есть удобные инст­ рументы для локальной разработки. В этой книге мы рассматриваем основы работы с Kubernetes и его интеграцию с контейнерами Linux, но не будем углубляться в экосистему Kubernetes. Настоя­ тельно рекомендуем прочитать книгу Kubernetes: Up & Running Брендана Бернса (O'Reilly) (https://www.oreilly.com/library/view/kubernetes-up-and/9781098110192) или что-то подобное, чтобы больше узнать о главных принципах и терминах. Инструмент Minikube Minikube - один из базовых инструментов для управления локальной установкой Kubernetes, и мы начнем с него, потому что большинство концепций, с которыми вы познакомитесь при работе с Minikube, можно применить к реализации Kubernetes, включая варианты, которые мы рассмотрим после Minikube. Нам доступно много других вариантов для запуска локального кластера Kuber­ netes. Мы начинаем с minikuЬe, потому что контейнер или виртуальная машина, которые он запускает, представляют собой стандартную установку Kubernetes с одним узлом. Кроме инструментов, которые мы обсудим в этом разделе, рекомен­ дуем изучить kЗs (https://kЗs.io/), kЗd (https://kЗd.io/), k0s (https://k0sprojectio/) и microk8s (https://microk8s.io/). Общие сведения о Minikube Minikube - это полноценный дистрибутив Kubernetes для одного экземпляра. Он управляет контейнером или виртуальной машиной на компьютере, причем мы ис­ пользуем такие же инструменты Kubernetes, как и в рабочей среде. В чем-то это похоже на Docker Compose - мы получаем полноценный стек в локальной среде, но у Minikube, в отличие от Compose, есть все API для рабочей среды. Если мы планируем применять Kubernetes в рабочей среде, мы можем создать на своем ком­ пьютере среду, которая пусть не по масштабу, но по функциям будет очень близка к рабочей. Уникальность Minikube заключается в том, что целым дистрибутивом можно управлять из одного двоичного файла, который мы загружаем и запускаем локаль­ но. Он автоматически определяет, какой менеджер контейнеров или виртуальных машин установлен на локальном компьютере, и запускает контейнер или виртуаль­ ную машину со всеми необходимыми сервисами Kubernetes, а значит, начать рабо­ ту очень просто. Давайте установим Minikube. Установка Minikube Большая часть установки будет одинаковой для всех платформ, потому что после установки инструментов именно через них мы будем взаимодействовать с вирту­ альной машиной, где запущен Kubernetes. Инструкции по установке ищите в раздеКниги для программистов: https://clcks.ru/books-for-it 240 Глава 10 ле для вашей операционной системы. Когда установите и запустите инструмент, следуйте общей документации. Для Minikube нам потребуются два инструмента: minikuЬe и kuЬectl. Обе утилиты представляют собой статические двоичные файлы без внешних зависимостей, по­ этому их будет легко установить в нашем простом примере. Существуют и другие способы установки Minikube. Здесь приводится самый про­ стой (по нашему мнению) метод для каждой платформы. Если у вас свои предпоч­ тения, смело используйте другой подход. В Windows, например, можно задейство­ вать менеджер пакетов Chocolatey (https://chocolatey.org/), а в Linux - систему упа­ ковки Sпар (https://snapcraft.io/). macOS Как мы уже упоминали, в системе должен быть установлен Homebrew. Если его еще нет, следуйте инструкциям из главы 3. После установки Homebrew установить клиент minikuЬe будет очень просто: $ brew install minikuЬe Homebrew загрузит и установит Minikube. В зависимости от конфигурации вы уви­ дите что-то подобное: => Downloading https://ghcr.io/v2/homebrew/core/kuЬernetes-cli/.../l.25.0 Already downloaded: ... /Homebrew/downloads/...kuЬernetes-cli...manifest.json => Downloading https://ghcr.io/v2/homebrew/core/kuЬernetes-cli/Ьlobs/sha256... Already downloaded: .../Homebrew/downloads/...kuЬernetes-cli--1.25...bottle.tar.gz => Downloadinghttps://ghcr.io/v2/homebrew/core/minikuЬe/manifests/l.26.1 Already downloaded: .../Homebrew/downloads/...minikuЬe-1.26.1...._manifest.json => Downloading https://ghcr.io/v2/homebrew/core/minikuЬe/Ыobs/sha256:... Already downloaded: .../Homebrew/downloads/ ...minikuЬe--1.26.1... bottle.tar.gz ==> Installing dependencies for minikuЬe: kuЬernetes-cli ==> Installing minikuЬe dependency: kuЬernetes-cli => Pouring kuЬernetes-cli--l.25.0.arm64_monterey.bottle.tar.gz DJ /opt/homebrew/Cellar/kuЬernetes-cli/1.25.0: 228 files, 52.ВМВ => Installing minikuЬe ==> Pouring minikuЬe--l.26.1.arm64_monterey.bottle.tar.gz => Caveats Bash completion has been installed to: /opt/homebrew/etc/bash_completion.d => Surmiary /opt/homebrew/Cellar/minikuЬe/1.26.1: 9 files, 70.бМВ ==> Running 'brew cleanup minikuЬe' ... DisaЬle this behavior Ьу setting HOМEBREW_NO_INSTALL_CLEANUP. Hide these hints with HOМEBREW NO ENV HINTS (see 'man brew'). ==> Caveats => minikuЬe Bash completion has been installed to: /opt/homebrew/etc/bash_completion.d Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 241 Вот и все. Давайте убедимся, что все установлено: $ which minikuЬe /opt/homebrew/bin/minikuЬe lJ.N � Homebrew в системах arm64 устанавливает Minikube в папке /opt/homebrew/Ьin, а не /usr/local/bin. Если вы не получили ответа, убедитесь, что в переменной среды РАТН есть /usr/local/Ьin и /opt/homebrew/Ьin. Если все в порядке, minikuЬe будет установлен. Инструмент kuЬectl установится автоматически, потому что это зависимость minikuЬe, но мы можем установить его явно с помощью brew. Как правило, версия kuЬectl в Homebrew совпадает с текущим выпуском minikuЬe, так что выполним ко­ манду brew install, чтобы избежать несоответствий: $ brew install kuЬernetes-cli Проверим установку так же, как для minikuЬe: $ which kuЬectl /opt/homebrew/Ьin/kuЬectl Все получилось. Windows Как и при установке Docker Desktop в Windows, установите Hyper-V или другую платформу виртуализации для запуска виртуальной машины Kubernetes. Для уста­ новки minikuЬe просто загрузите двоичный файл и поместите его в папку, которая указана в переменной РАТН, чтобы его можно было выполнять в командной строке. Загрузите последний выпуск minikuЬe на GitHub (https://github.com/kubernetes/ minikube/releases/latest). Переименуйте загруженный исполняемый файл Windows в minikube.exe, иначе придется вводить слишком много букв. Больше о процессе установки Windows и исполняемом двоичном файле см. в до­ � кументации по установке Minikube (https://minikuЬe.sigs.k8s.io/docs/start). ._ Теперь нужно загрузить последний выпуск Kubernetes CLI, kuЬectl, для выполнения команд. К сожалению, это нельзя сделать с помощью пути /latest, так что полу­ чите послед}Щ)ю версию (11ttps://storage.googleapis.com/kubernetes-release/release/ staЫe.txt) с сайта, а затем вставьте в URL: https://storage.googleapis.com/ kubernetes-release/release/<BEPCИЯ>/bin/windows/amd64/kubectl.exe. После загрузки убедитесь, что инструмент доступен в переменной РАТН. Linux В Linux вам понадобится установить Docker, а также КУМ (Linux Kernel-based Virtual Machine) или VirtualВox, чтобы minikuЬe мог создать виртуальную машину Книги для программистов: https://clcks.ru/books-for-it 242 Глава 10 Kubernetes и управлять ею. Поскольку minikuЬe умещается в одном двоичном файле, после его установки нам не нужно устанавливать дополнительные пакеты. Благо­ даря статической компоновке minikuЬe будет работать на любом дистрибутиве. Мы могли бы уложить всю установку в одну строку, но разделим процесс на несколько этапов, чтобы его было проще понять и исправить, если что-то пойдет не так. На момент написания двоичный файл размещается на googleapis, где обычно бывают очень стабильные URL. Итак: # Загружаем файл и сохраняем как 'minikuЬe' $ curl -Lo minikuЬe \ https://storage.googleapis.com/minikuЬe/releases/latest/minikuЬe-linux-amd64 # Делаем его исполняемым $ chmod +х minikuЬe # Переносим файл в /usr/local/Ыn $ sudo mv miпikuЬe /usr/local/Ыn/ Убедитесь, что у вас есть /usr/loca\/Ьin. Теперь у нас есть minikuЬe, и нам нужно получить еще kuЬectl: # Получаем последнюю версию $ KUBE_VERSION=$(curl -s \ https://storage.googleapis.com/kuЬernetes-release/release/staЫe.txt) # Получаем исполняемый файл $ curl -10 \ https://storage.googleapis.com/kuЬernetes-release/\ release/$(KUBE_VERSION)/Ьin/liпux/amd64/kuЬectl # Делаем его исполняемым $ chmod +х kuЬectl # Переносим файл в /usr/local/Ыn $ sudo mv kuЬectl /usr/local/Ып/ Один из URL в этом примере продолжается на следующей строке, чтобы вписаться в ширину. Возможно, потребуется пересобрать его и убрать обратные косые чер­ ты, чтобы команда работала в вашей среде. Итак, мы все установили, давайте приступать к работе. Запуск Kubernetes Мы можем использовать установленный minikuЬe, чтобы запустить кластер Kubernetes. Обычно это довольно просто, никакой предварительной конфигурации не требуется. В этом примере мы увидим, что miпikuЬe использует драйвер docker, хотя можно было выбрать и другие. Запустить minikuЬe можно следующей командой: $ minikuЬe start � minikuЬe vl.26.1 оп Darwiп 12.5.1 (arm64) • Automatically selected the docker driver. Other choices: parallels, ssh, ... :1' Usiпg Docker Desktop driver with root privileges Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 243 .J# Starting control plane node minikuЬe in cluster minikuЬe � Pulling base image ... F1 Downloading KuЬernetes vl.24.3 preload ... > preloaded-images-k8s-v18-vl...: 342.82 MiB / 342.82 MiB 100.00% 28.22 М > gcr.io/k8s-minikuЬe/kicbase: 348.00 MiB / 348.00 MiB 100.00% 18.13 MiB > gcr.io/kBs-minikuЬe/kicbase: О В [ __________] ?% ? p/s 16s 11\ Creating docker container (CPUs=2, Memory=4000MB) ... 81 Preparing KuЬernetes vl.24.3 on Docker 20.10.17 ... • Generating certificates and keys • Booting up control plane ... • Configuring RВАС rules ... Р Verifying KuЬernetes components... • Using image gcr.io/k8s-minikuЬe/storage-provisioner:v5 .... EnaЫed addons: storage-provisioner, default-storageclass ;$, Done! kuЬectl is now configured to use "minikuЬe" cluster and "default" namespace Ьу default Что мы сейчас сделали? Одна команда Minikube выполняет много действий. В этом примере мы запустили один контейнер Linux, который предоставляет работающую установку Kubernetes в локальной системе. Если использовать с minikuЬe один из драйверов виртуализации, то можно создать полноценную виртуальную машину с Kubernetes, а не один контейнер. Затем в контейнерах Linux на хосте запускаются все необходимые компоненты Kubemetes. Мы можем изучить контейнер или виртуальную машину minikuЬe: $ minikuЬe ssh docker@minikuЬe:~$ В кластере Kubemetes, скорее всего, вы нечасто будете использовать SSH с ко­ мандной строкой. Мы хотим посмотреть, что установили, и, к счастью, при нали­ чии minikuЬe в среде можно запускать сразу много процессов. Давайте посмотрим, что выполняется на экземпляре Docker в кластере Kubernetes: docker@minikuЬe:~$ docker container 1s ... ID 48...cf 4е...Bd ld...3d 82...d3 27...10 15...се ff...3d 33... с5 30...97 f5...41 5Ь...08 87...се 5а...14 бf... Ос IМAGE Ьа...57 ed...е8 pause:3.6 7а...dc pause:3.6 pause:3.6 f9...55 pause:3.6 а9 ...df 53...аб Bf...73 pause:3.6 pause:3.6 pause:3.6 СОММАND ... NAМES "/storage-provisioner" ...k8s_storage-provisioner_storage-... "/coredns -conf /etc..." k8s POD coredns-6d4b75cbбd-... "/pause" k8s POD coredns-6d4b75cbбd-.. . "/usr/local/Ьin/kuЬe..." k8s_kuЬe-proxy_kuЬe-proxy-... "/pause" k8s_POD_kuЬe-proxy-zbбw2_kuЬe-... "/pause" ...kBs_POD_storage-provisioner_kuЬe-... "kuЬe-controller-man... " kBs_kuЬe-controller-manager_kuЬe-... "/pause" k8s_POD_kuЬe-controller-manager-... "etcd --advertise-cl..." k8s-etcd-etcd-minikuЬe-kuЬe-... "kuЬe-apiserver --ad..." k8s-etcd-etcd-minikuЬe-kuЬe-... "kuЬe-scheduler --аи . . . " k8s-kuЬe-scheduler-kuЬe-scheduler-... "/pause" k8s_POD_kuЬe-apiserver-... "/pause" k8s-POD-etcd-minikuЬe-kuЬe-... "/pause" k8s POD kuЬe-scheduler-... Книги для программистов: https://clcks.ru/books-for-it 244 Глава 10 Мы не будем подробно рассматривать каждый компонент, просто посмотрим, как работает механизм. Компоненты легко обновлять, ведь это просто контейнеры, их версии отслеживаются, и мы можем извлекать их из базового репозитория контей­ неров. Давайте выйдем из командной оболочки в системе Minikube: docker@minikuЬe:~$ exit Команды minikube Для экономии места и времени мы не будем рассматривать все команды minikuЬe. Попробуйте выполнить их сами без параметров, изучите вывод и поэксперименти­ руйте. Мы остановимся на самых интересных командах, а по ходу установки стека приложений изучим еще несколько. Ранее мы вызывали команду minikuЬe ssh, чтобы посмотреть, что происходит в сис­ теме. Это удобный способ отлаживать или изучать систему напрямую. Не обраща­ ясь прямо к системе Minikube, мы можем проверить состояние кластера еще одной командой minikuЬe: $ minikuЬe status minikuЬe type: Control Plane host: Running kuЬelet: Running apiserver: Running kuЬeconfig: Configured Мы видим, что все в порядке. Еще две полезные команды: Команда Действие minikuЬe ip minikuЬe update-check Получает IР-адрес виртуальной машины Minikube Проверяет версию Minikube относительно последнего выпуска Чтобы обновить версию, используйте тот же механизм, с помощью которого изна­ чально установили инструмент. Команда minikuЬe status также показывает, что kuЬeconfig правильно настроен. Это необходимо, чтобы kuЬectl знал, как подключиться к кластеру. Мы запустили кластер Kubemetes командой minikuЬe start. Будет логично предпо­ ложить, что minikuЬe stop остановит все компоненты Kubemetes и виртуальную машину или контейнер Linux. Чтобы очистить среду, можно удалить кластер командой minikuЬe ctelete. Kubernetes Dashboard После запуска Minikube мы можем работать не только с инструментами командной строки, но и с графическим интерфейсом Kubemetes Dashboard. Для этого у нас есть команда minikuЬe ctashЬoarct, которая открывает в браузере Kubemetes Dashboard с правильным IР-адресом и портом. На панели много компонентов, и в этой книге мы не сможем рассмотреть их все, но вы можете изучить их самостоятельно. Если Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 245 у вас уже есть опыт работы с Kubemetes, некоторые термины в боковом меню панели будут вам знакомы, но во многих придется разбираться. Если под рукой нет компьютера, на рис. 10.2 приводится скриншот - как выглядит пустая установка Minikube, если нажать кнопку Service в боковом меню. Рис. 10.2. Kubernetes Dashboard (nример) Если нажать ссылку Nodes (Узлы) в разделе Cluster (Кластер) в левом меню, мы увидим один узел в кластере с именем minikube. Это запущенный нами контейнер или виртуальная машина, и панель, как и остальные компоненты, размещена в од­ ном из контейнеров, которые мы видели, когда подключались к системе Minikube раньше. Давайте что-нибудь развернем в кластере, а потом снова посмотрим на панель. Почти все, что мы видим на панели, можно посмотреть с помощью команды kuЬectl, так что мы можем легко писать разные shеll-скрипты. Например, если мы выполним kuЬectl get services или kuЬectl get nodes, то уви­ дим ту же информацию, что и на панели. Изучая панель, вы заметите, что сам Kubemetes отображается как компонент сис­ темы - так же будет отображаться и приложение. �lЖl Нажмите комбинацию клавиш <Ctrl>+<C>, чтобы выйти из процесса minikuЬe dashЬoard и вернуться в терминал. Контейнеры и поды Kubernetes Итак, мы легко запустили кластер Kubemetes в локальной среде. Давайте вернемся к теории и узнаем, что Kubemetes добавляет поверх абстракции контейнеров. Книги для программистов: https://clcks.ru/books-for-it 246 1 Глава 10 Kubemetes появился как результат работы Google с очень большой платформой. У Google возникали проблемы, типичные для рабочей среды, и компания искала способы упростить их понимание и решение. В процессе разработки инженеры Google создали сложную систему новых абстракций, и для Kubemetes выработался свой набор терминов. Мы не будем рассматривать их все, но остановимся на глав­ ной из новых абстракций, которая находится на уровень ниже контейнера и назы­ вается pod (под). l,ж � Символ Docker - кит Моби, а слово pod в английском языке обозначает стаю ки­ тов. В терминологии Kubemetes под - это один или несколько контейнеров, у которых общие cgroups и пространства имен. С помощью cgroups и пространств имен мы так­ же можем изолировать контейнеры друг от друта в пределах одного пода. Под ин­ капсулирует все процессы или приложения, которые нужно развернуть вместе как функциональную единицу, которой будет управлять планировщик. Все контейнеры в поде могут взаимодействовать друт с другом на localhost, так что налаживать об­ наружение не требуется. Так почему бы просто не развернуть один большой кон­ тейнер сразу со всеми приложениями? Потому что в поде, в отличие от большого контейнера, мы можем ограничивать ресурсы для отдельных приложений и состав­ лять приложение из обширного ряда публичных контейнеров Linux. Кроме того, администраторы Kubemetes часто используют поды, например, чтобы контейнер запускался при запуске пода с правильной конфигурацией для всех, что­ бы задействовать общий ресурс или чтобы объявлять приложение для других сер­ висов. Поды дают более детализированный контроль, поскольку нам не приходится группировать все в один контейнер. Еще одно преимущество пода - возможность монтировать общие тома. Время жизни подов схоже с контейнерами Linux. Они эфемерны, и их можно по­ вторно развертывать на новых хостах на протяжении жизненного цикла приложе­ ния или хоста. У контейнеров в поде один IР-адрес для общения с внешним миром, так что на уровне сети они выглядят как единый объект. В одном контейнере мы запускаем только один экземпляр приложения, а в одном поде - один экземпляр контейнера. Считайте, что под - это группа контейнеров Linux, которые во многих смыслах работают как один контейнер. Если нам нужен только один контейнер, Kubernetes все равно развертывает под, но внутри будет один контейнер. К сча­ стью, планировщик Kubemetes имеет дело только с одной абстракцией - с подом. Контейнеры управляются компонентами среды выполнения, которые составляют под, а также выбранной нами конфигурацией. Важное различие между подом и контейнером - мы не создаем поды на этапе сборки. Поды - абстракция среды выполнения, которая определяется в манифесте JSON или УАМL и существует только в Kubernetes. Мы собираем контейнеры Linux и отправляем их в реестр, а затем определяем и развертываем поды с помо­ щью Kubemetes. В реальности мы не описываем под напрямую - за нас его создаКниги для программистов: https://clcks.ru/books-for-it 247 Масштабирование контейнеров ют инструменты в ходе развертывания. В кластере Kubemetes под - это единица выполнения и планирования. Все гораздо сложнее, но это базовая концепция. Про­ ще всего понять ее на примере. Система, состоящая из подов, выглядит сложнее, чем система из контейнеров, но с ними работать гораздо эффективнее. Практический пример При работе с подами в Kubernetes мы обычно управляем ими через такую абстрак­ цию, как развертывание (deployment). Развертывание - это определение пода с дополнительной информацией, включая конфигурацию репликации и монитор.дн­ га работоспособности. В двух словах, это определение пода и немного метаданных о нем. Давайте рассмотрим базовое развертывание. Самое простое, что можно развернуть в Kubernetes, - под с одним контейнером. Давайте на примере приложения httpbin (https://httpbln.org/) рассмотрим основы развертывания в Kubernetes. Назовем наше развертывание hello-minikuЬe. До этого мы пользовались командой minikuЬe, но для выполнения операций в самом Kubernetes нам понадобится команда kuЬectl, которую мы установили чуть раньше: $ kuЬectl create deployment hello-minikuЬe \ --image=kennethreitz/httpbin:latest --port= B0 deployment.apps/hello-minikuЬe created Чтобы посмотреть, что произошло, выполним команду kuЬectl get all и получим самые важные объекты в кластере: $ kuЬectl get all NАМЕ READY AGE STAТUS RESTARTS 2m39s 1/1 Running pod/hello-minikuЬe-ff49df9b8-svl68 о NАМЕ service/kuЬernetes ТУРЕ ClusterIP NАМЕ deployment.apps/hello-minikuЬe CLUSTER-IP 10.96.0.1 READY 1/1 NАМЕ replicaset.apps/hello-minikuЬe-ff49df9b8 EXTERNAL-IP <none> PORT (S) 443/ТСР UP-TO-DATE 1 AVAILABLE 1 AGE 2m39s DESIRED 1 CURRENT 1 READY 1 AGE 98m AGE 2m39s При выполнении этой команды Kubernetes создал развертывание, ReplicaSet для масштабирования и под. Давайте убедимся, что у пода для параметра sтюus (ста­ тус) установлено значение Running (выполняется). Если у вашего пода другой статус, выполните команду еще пару раз, пока статус не изменится. Сервис service/ kuЬernetes представляет сам Kubernetes. А где наш сервис? Пока мы его не видим. Он находится в том же состоянии, в каком пребывал бы контейнер Linux, если бы мы не указали ему открыть порт. Давайте укажем Kubemetes открыть сервис: $ kuЬectl expose deployment hello-minikuЬe --type=NodePort service/hello-minikuЬe exposed Книги для программистов: https://clcks.ru/books-for-it Глава 10 248 Теперь у нас есть сервис, с которым можно взаимодействовать. Сервис является оберткой над одним или несколькими развернутыми приложениями и указывает, как обращаться к приложению. В этом случае у нас есть NodePort, который предос­ тавляет на каждом узле в кластере порт, через который можно связаться с подами. Давайте узнаем у Kubernetes, как связаться с подом: $ kuЬectl get services AGE EXTERNAL-IP PORT (S) ТУРЕ CLUSTER-IP NАМЕ 80:32557/ТСР 8s NodePort hello-minikuЬe 10.105.184.177 <none> 107m <none> 10.96.0.1 443/ТСР ClusterIP kuЬernetes Можно подумать, что теперь к сервису можно подключиться по адресу http://l 0.105.184.177:8080, но эти адреса недоступны с нашей хает-системы, потому что Minikube работает в контейнере или виртуальной машине. Нам понадобится команда minikuЬe, чтобы узнать, где найти сервис: $ minikuЬe service hello-minikuЬe --url http:l/192.168.99.100:30616 В некоторых конфигурациях может отобразиться сообщение: 1 Because you are using а Docker driver on darwin, the terminal needs to Ье open to run it. (Вы используете драйвер Docker на darwin, поэтому для запуска нужно открыть терминал.) Это значит, что пока мы не можем установить соединение между хос­ том и сервисами Kubernetes, и при работе с приложением команда должна про­ должать выполняться. Мы можем использовать локальный браузер или открыть еще один терминал, чтобы запустить команду, вроде curl. Когда закончите, нажмите <Ctrl>+<C> в исходном сеансе, чтобы завершить коман­ ду minikuЬe service. Эту команду, как и многие другие команды Kubernetes, можно вставлять в скрипты, и ее удобно вводить в командной строке. Если мы хотим открывать ее с помощью curl, можно просто включить вызов minikuЬe в запросе в командной строке: $ curl -Н foo:bar $(minikuЬe service hello-minikuЬe --url)/get 11 args 11 : {}, "headers": { "Accept": 11 */* 11, "Foo": 11bar", "Host": "127.О.О .1:56695", "User-Agent": "curl/7.85.О" }, "origin": "172.17.0.1", "url": "http:l/127.0.0.1:56695/get" Инструмент httpЬin - это простой API НТТР-запросов и ответов, с помощью кото­ рого можно тестировать и проверять НТТР-сервисы. Не самое интересное прило­ жение, но мы видим, что можем связаться с сервисом и получить от него ответ через curl. Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 1 249 Это простейший вариант применения. Мы почти ничего не настраивали, Kubemetes сделал все необходимое с настройками по умолчанию. Далее мы рассмотрим более сложный пример, но сначала давайте завершим работу сервиса и развертывания. Для этого выполним две команды: одна удалит сервис, а другая удалит разверты­ вание: $ kuЬectl delete service hello-minikuЬe service "hello-minikuЬe" deleted $ kuЬectl delete deployrnent hello-minikuЬe deployrnent.apps "hello-rninikuЬe" deleted $ kuЬectl get all NАМЕ service/kuЬernetes ТУРЕ ClusterIP CLUSTER-IP 10.96.0.1 EXTERNAL-IP <none> PORT(S) AGE 443/ТСР 138m Развертывание реалистичного стека Давайте развернем что-нибудь более похожее на настоящий стек. Это будет при­ ложение, которое извлекает документы PDF из бакета SЗ, кеширует их на локаль­ ном диске и по запросу преобразует отдельные страницы в изображения PNG, ис­ пользуя кешированный документ. Файлы кеша мы будем записывать за пределами контейнера, в более постоянном и стабильном хранилище. На этот раз мы хотим создать воспроизводимое развертывание, поэтому не будем развертывать приложе­ ние с помощью серии команд CLI, которые приходится сначала запоминать, а по­ том аккуратно вводить, стараясь избежать ошибок. Kubernetes, как и Docker Compose, позволяет определить стек в одном или нескольких файлах УАМL, кото­ рые содержат все нужные определения. Другие инструменты в рабочей среде реа­ лизуют очень похожий подход. Сейчас мы создадим сервис lazyraster, который будет по запросу преобразовывать изображение в растровый формат. Под этим именем приложение будет обозначено в определении УАМL. Постоянный том (persistent volume) будет называться cache­ data. У Kubernetes своя терминология, и мы не сможем рассмотреть все понятия, но сейчас нужно разъяснить еще два: PersistentVolume и PersistentVolumeClaim. PersistentVolume - это физический ресурс, который мы выделяем в кластере. Kubemetes поддерживает разные типы томов, от локального хранилища на узле до томов Amazon Elastic Block Store (Amazon EBS) (https://docs.aws.amazon.com/ АWSEC2/latest/UserGuide/AmazonEBS.html) в А WS и аналогичных решений в других облаках. Он также поддерживает Network File System (NFS) (https:// en.wikipedia.org/wiki/Network_File_System) и другие, более современные сетевые файловые системы. Жизненный цикл данных в PersistentVolume не зависит от при­ ложения или развертываний - данные сохраняются между развертываниями. Мы используем его для кеша. PersistentVolumeClaim - это связь между физическим ре­ сурсом PersistentVolume и приложением, которому он требуется. Мы можем создать политику, которая разрешит один запрос на чтение-запись или много запросов на чтение. Для этого приложения мы выбираем один запрос на чтение и запись в cache­ data Persistent Volume. Книги для программистов: https://clcks.ru/books-for-it 250 Глава 10 Если хотите узнать больше об этих концепциях, изучите официальный глоссарий (https://kubernetes.io/docs/reference/glossary/?fundamental=true) с терминами Kubernetes. Это может очень пригодиться. В статьях глоссария есть ссылки на страницы с более подробной информацией. Получим файл, который мы будем использовать в этом разделе: $ git clone \ https://githuЬ.com/Ыuewhalebook/\ docker-up-and-running-Зrd-edition.git --config core.autocrlf=input Cloning into 'docker-up-and-running-Зrd-edition' ... $ cd docker-up-and-running-Зrd-edition/chapter_l0/kuЬernetes URL в этом примере продолжается на следующей строке, чтобы вписаться в ши­ рину. Возможно, потребуется пересобрать его и убрать обратные косые черты, чтобы команда работала. Для начала изучим манифест в файле lazyraster-service.yaml. Полный манифест со­ стоит из нескольких файлов УАМL, разделенных символами ---. Мы рассмотрим каждый раздел. Определение сервиса apiVersion: vl kind: Service metadata: name: lazyraster laЬels: арр: lazyraster spec: type: NodePort ports: - port: 8000 targetFort: 8000 protocol: ТСР selector: арр: lazyraster В первом разделе определяется объект service (сервис). Во втором и третьем разде­ лах определяются объекты PersistentVolumeClaim (запрос на постоянный том) и Deployment (фактическое развертывание). Мы сообщили Kubernetes, что сервис будет вызывать lazyraster, и он будет предоставляться через порт 8000, который соответ­ ствует фактическому порту 8000 в нашем контейнере. Мы предоставили его через механизм NodePort, с помощью которого приложение предоставляется через один и тот же номер порта на каждом хосте. Это похоже на флаг --puЬlish в команде docker container run. В случае применения minikuЬe это удобно, потому что мы запускаем только один экземпляр, а NodePort упрощает доступ к нему с компьютера. Как это Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 251 часто бывает в Kubemetes, кроме NodePort есть и другие варианты. Возможно, вы найдете механизм, который лучше подходит для вашей рабочей среды. NodePort хорошо работает с minikuЬe, но подходит и для других балансировщиков нагрузки с более статической конфигурацией. Итак, возвращаемся к определению Service. Объект Service будет подключаться к Deployment через selector, который присутствует в разделе spec. Метки в Kubemetes часто служат для обозначения схожих компонентов, чтобы их можно было связать друг с другом. Метки представляют собой пары произвольно определяемых ключей и значений, к которым мы можем обращаться для определения компонентов систJ;:­ мы. Здесь selector указывает Kubemetes, что нужно искать объекты Deployments с меткой арр: lazyraster. Обратите внимание, что та же метка указана и у самого объекта service. Это удобно, если мы хотим позже определить все компоненты вме­ сте, но именно в разделе selector объект Deployment привязывается к Service. Итак, у нас есть service, но пока он ничего не делает. Нам нужны дополнительные опре­ деления, чтобы Kubernetes делал то, что мы хотим. Определение PersistentVolumeC/aim apiVersion: vl kind: PersistentVolumeClaim metadata: name: cache-data-claim laЬels: арр: lazyraster spec: accessМodes: - ReadWriteOnce resources: requests: storage: l00Mi В следующем разделе определяются объекты PersistentVolumeClaim и Persistent Volume. PersistentVolumeClaim позволяет указать том и сообщить, что у нас есть токен для доступа к этому тому определенным образом. Обратите внимание, что здесь мы не определили PersistentVolume, потому что Kubemetes делает это сам с помощью так называемого динамического выделения томов. В нашем случае все просто: нам нужен запрос на чтение и запись к тому, и мы по­ зволяем Kubemetes поместить его в контейнер тома. Представьте, что приложение будет развертываться в облаке, где и так доступно динамическое выделение. В та­ ком сценарии нам не нужны отдельные вызовы для создания тома в облаке. Мы хотим, чтобы за это отвечал Kubemetes. Это и есть динамическое выделение томов. Здесь подобная процедура создаст контейнер для хранения постоянных данных, а затем примонтирует его к поду при запросе. В этом разделе мы просто присваиваем имя, запрашиваем 100 МБ данных и указываем Kubemetes, что это единичный запрос на чтение-запись к тому. Книги для программистов: https://clcks.ru/books-for-it 252 Глава 10 В этой сфере для Kubernetes существует множество предложений - доступные варианты томов ОТ'-lасти зависят от вашего облака или провайдера. Изучите их и выберите наиболее подходящее решение для вашей рабочей среды. Определение Deployment apiVersion: apps/vl kind: Deployment metadata: name: lazyraster laЬels: арр: lazyraster spec: selector: matchLaЬels: арр: lazyraster s trategy: type: RollingUpdate template: metadata: laЬels: spec: арр: lazyraster containers: - image: relistan/lazyraster:demo name: lazyraster env: - name: RASTER RING ТУРЕ value: memЬerlist - name: RASTER BASE DIR value: /data ports: - containerPort: 8000 name: lazyraster volumeМounts: - name: cache-data mountpath: /data volumes: - name: cache-data persistentVolumeClaim: claimName: cache-data-claim Объект Deployment создает поды и использует контейнер Linux для приложения. Мы задаем некоторые метаданные приложения, включая его имя и метку, как в других определениях. Мы также применяем еще один selector, чтобы найти остальные не­ обходимые ресурсы. В разделе strategy мы выбрали плавающие обновления RollingUpdate, чтобы поды обновлялись по одному во время развертывания. Книги для программистов: https://clcks.ru/books-for-it 253 Масштабирование контейнеров В разделе template мы указали, как делать копии развертывания. Определение кон­ тейнера включает имя образа Docker, порты для подключения, тома для монти­ рования, а также переменные среды, которые нужны приложению lazyraster. В последней части раздела spec мы указываем PersistentVolumeClaim с именем cache­ data-claim. Итак, мы определили приложение. Давайте теперь развернем его. Мы можем написать здесь много разных параметров и директив, чтобы указать Kubernetes, что делать с приложением. Здесь мы рассматриваем несколько простых вариантов. Изучите документацию по Kubernetes, чтобы узнать больше. Развертывание приложения Прежде чем продолжить, давайте заглянем в кластер Kubemetes с помощью коман­ ды kuЬectl: $ kuЬectl get all PORT (S) EXTERNAL-IP AGE CLUSTER-IP ТУРЕ NАМЕ <none> 10.96.0.1 443/ТСР ClusterIP 160m service/kuЬernetes Пока мы только определили сервис с именем service/kuЬernetes. В Kubemetes приня­ то перед именем объекта указывать его тип (кind), который иногда сокращается до двух или трех букв. Например, service может обозначаться как svc. Если интересно, посмотрите все ресурсы и их сокращенное название с помощью команды kuЬectl api-resources. Давайте запустим сервис, развертывание и том в кластере: $ kuЬectl apply -f ./lazyraster-service.yaml service/lazyraster created persistentvolumeclaim/cache-data-claim created deployment.apps/lazyraster created Вывод выглядит ожидаемо: у нас есть сервис, запрос на постоянный том и развер­ тывание. Вот что находится в кластере: $ kuЬectl get all READY AGE RESTARTS STAТUS NАМЕ 1/1 Running 17s pod/lazyraster-644cb5c66c-zsjxd о NАМЕ service/kuЬernetes service/lazyraster ТУРЕ ClusterIP NodePort NАМЕ deployment.apps/lazyraster CLUSTER-IP 10.96.0.1 10.109.116.225 READY 1/1 NАМЕ replicaset.apps/lazyraster-644cb5c66c EXTERNAL-IP <none> <none> PORT (S) AGE 443/ТСР lбlm 8000:32544/TCP17s UP-TO-DATE 1 AVAILABLE 1 AGE 17s DESIRED 1 CURRENT 1 READY 1 AGE 17s Мы видим незнакомый компонент, но не видим том или запрос на него. Информа­ цию о томах нужно запрашивать отдельно: Книги для программистов: https://clcks.ru/books-for-it 254 1 Глава 10 $ kuЬectl get pvc NАМЕ STATUS cache-data-claim Bound VOLUME CAPACITY pvc-la... 41 lOOMi ACCESS MODES RWO STORAGECLASS standard AGE 65s Несмотря на то что get all означает "получить все", kuЬectl get all эти данные не показывает. Отображаются самые распространенные ресурсы. Изучите шпаргалку с командами kuЬectl (https ://kubernetes. io/docs/reference/kubectl/cheatsheet). Что такое replicaset.apps в выходных данных команды get all? Это ReplicaSet­ pecypc Kubemetes, который следит за наличием достаточного количества экземпля­ ров приложения и их работоспособностью. Обычно мы не настраиваем ReplicaSet сами, потому что им управляет созданное нами развертывание. При необходимости мы можем контролировать ReplicaSet, но обычно такая необходимость не возни­ кает. Мы не указали в kuЬectl, сколько экземпляров нам нужно, поэтому он всего один. Мы видим, что желаемое и текущее состояния совпадают. Вернемся к этому чуть позже, а сначала давайте подключимся к приложению и посмотрим, что у нас есть: $ minikuЬe service --url lazyraster http://192.168.99.100:32185 У вас IР-адрес и порт могут отличаться. Это нормально, они предоставляются динамически. Поэтому мы используем команду minikuЬe, чтобы она управляла за нас этими данными. Кстати, minikuЬe предупредит, если необходимо непрерывно выполнять команду service, пока вы изучаете сервис lazyraster. Скопируйте полученный адрес и вставьте его в адресной строке браузера следующим образом: bttp://<192.168.99.100:32185>/ documents/docker-up-and-running-puЫic/sample.pdf?page=l. Замените адрес и порт на свои значения. Потребуется подключение к Интернету, потому что приложению lazyraster нужно будет извлечь PDF из бакета SЗ, а затем отрисовать первую страницу в формате PNG, т. е. выполнить растеризацию. Если все получилось, вы увидите копию обложки предыдущего издания книги. Этот PDF включает две страницы, так что измените аргумент на ?page=2. Вы заметите, что вторая страница будет готова гораз­ до быстрее, чем первая, потому что приложение кеширует данные в постоянном томе. Вы можете указать ширину, например width=204B, или запросить JPEG вместо PNG - imageType=image/jpeg. Первую страницу можно преобразовать в очень боль­ шой JPEG: bttp://<192.168.99.100:32185>/documents/docker-up-and-running-puЫic/ sam-ple.pdf?page= l&imageType=image/jpeg&width=2048. Если у вас есть публичный бакет SЗ с другими PDF, просто замените имя бакета docker-up-and-running-puЬlic в URL на ваш вариант. Если хотите поэкспериментиро­ вать с приложением, загляните в репозиторий Nitro/lazyraster на GitHub (https:// github.com/NitroЛazyraster). Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 255 Увеличение масштаба В реальном мире мы не только развертываем, но и обслуживаем приложения. Одно из главных преимуществ запланированных рабочих нагрузок - возможность ме­ нять их масштаб по желанию в рамках ресурсов, доступных в системе. У нас есть только один узел Minikube, но мы все же можем увеличить масштаб сервиса, чтобы лучше распределить нагрузку и повысить надежность во время развертываний. Kubemetes позволяет легко увеличивать и уменьшать масштаб. Для нашего сервиса понадобится всего одна команда. Затем мы изучим выходные данные kuЬectl и по­ смотрим на Kubernetes Dashboard, чтобы убедиться, что масштаб увеличен. В Kubemetes мы увеличиваем масштаб не для сервиса, а для развертывания. Вот как это будет выглядеть: $ kuЬectl scale --replicas=2 deploy/lazyraster deployment.apps/lazyraster scaled Что-то произошло, но что именно? $ kuЬectl get deployment/lazyraster UP-TO-DATE NАМЕ READY 2 2/2 lazyraster AVAILABLE 2 AGE lбm Теперь запущены два экземпляра приложения. Давайте заглянем в журнал: $ kuЬectl logs deployment/lazyraster Found 2 pods, using pod/lazyraster-644cb5c66c-zsjxd Trying to clear existing Lazyraster cached files (if any) in the background ... Launching Lazyraster service ... time="2022-09-10T21:14:16Z" level=info msg="Settings time="2022-09-10T21:14:16Z" level=info msg=" * BaseDir: /data" time="2022-09-10T21:14:16Z" level=info msg=" * HttpPort: 8000" time="2022-09-10T21:14:16Z" level=info msg=" * LoggingLevel: info" time="2022-09-10T21:14:16Z" level=info msg=" time="2022-09-10T21:14:16Z" level=info msg="Listening on tcp://:6379" Мы запросили журналы развертывания, но Kubemetes показывает, что запущены два пода, и отображает журналы только для одного из них. Мы видим, что реплика запускается. Если мы хотим посмотреть конкретный экземпляр, можно запросить журналы для определенного пода - kuЬectl logs pod/lazyraster-644cb5c66c-zsjxd, но сначала надо найти нужный под с помощью команды kuЬectl get pods. Теперь запущены две копии приложения. Давайте посмотрим, как это выглядит на Kubernetes Dashboard, выполнив команду minikuЬe dashЬoard. Из левого меню перей­ дем в раздел Workloads (Рабочие нагрузки) ---t Deployments (Развертывания) и на­ жмем на развертывание lazyraster. Откроется экран, как на рис. 10.3. Книги для программистов: https://clcks.ru/books-for-it 256 1 Глава 10 Рис. 10.3. Сервис lazyraster на панели (пример) Изучите разные разделы на Kubernetes Dashboard, чтобы увидеть, что там еще есть. Теперь вы знаете гораздо больше и можете в чем-то разобраться самостоятельно. У kuЬectl есть много разных параметров, и многие из них понадобятся вам в рабо­ чей среде. Настоятельно рекомендуем ознакомиться со шпаргалкой по командам (https://kubernetes.io/docs/reference/kubectl/cheatsheet). Когда закончите, нажмите комбинацию клавиш <Ctrl>+<C>, чтобы завершить команду minikuЬe dashЬoard. kubectl API Пока мы не рассматривали API, но вы уже знаете, что в Docker очень удобен про­ стой API для написания скриптов, программирования и других общих задач. Вы можете написать программу, которая будет напрямую взаимодействовать с Kubernetes API, но для локального развертывания и других простых сценариев используйте kuЬectl как удобный прокси для Kubernetes. Он предоставляет мини­ малистичный API, доступный через curl, и инструменты командной строки для работы с JSON. Пример того, что мы можем сделать: $ kuЬectl proxy Starting to serve on 127.0.0.1:8001 Теперь сам kuЬectl предоставляет нам веб-АРI в локальной системе. Рекомендуем изучить другие доступные возможности, но сейчас давайте рассмотрим отдельные экземпляры приложения lazyraster. Для этого перейдем по следующему URL­ aдpecy в браузере или через curl в другом окне терминала: http:/Лocalhost:8001/ api/vl/ namespaces/default/endpointsЛazyraster. Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 257 Выходных данных много, но нас интересует раздел suЬsets: "suЬsets": "addresses": "ip": "172.17.0.5", "nocleName": "minikuЬe", "targetRef": { "kind": "Pod", "namespace": "default", "name": "lazyraster-644cb5c66c-zsjxd", "uid": "9631395d-7e68-47fa- bb9f-964ld724d8f7" }, "ip": "172.17.0.6", "nocleName": "minikuЬe", "targetRef": { "kind": "Pod", "namespace": "default", "паше": "lazyraster-644cb5c66c-pvcmj", "uid": "e909d424-7a91-4a74-aed3-69562Ь74b422" ], "ports": } "port": 8000, "protocol": "ТСР" ] } ] Что интересно, оба экземпляра выполняются на одном хосте Minikube, но у них разные IР-адреса. При создании облачного приложения, которому требуется знать, где выполняются другие его экземпляры, это удобный подход. Нажмите <Ctrl>+<C>, чтобы выйти из процессов kuЬectl proxy, а затем удалите раз­ вертывание и все его компоненты следующей командой. Kubernetes потребуется около минуты, чтобы все удалить и вернуть нас в терминал: $ kuЬectl delete -f ./lazyraster-service.yaml service "lazyraster" deleted persistentvolwneclaim "cache-data-claim" deleted deployment.apps "lazyraster" deleted Книги для программистов: https://clcks.ru/books-for-it 258 Глава 10 Наконец, мы можем удалить кластер Minikube, если закончили работу с ним: $ minikuЬe delete � Deleting "minikuЬe" in docker ... � Deleting container "minikuЬe" ... (), Removing /Users/spkane/.minikuЬe/machines/minikuЬe - Removed all traces of the "minikuЬe" cluster. Kubernetes - это развитая система с активным сообществом. Minikube - лишь верхушка айсберга. Если интересно, изучите другие дистрибутивы и инструменты Kubernetes. Kubernetes, интегрированный в Docker Desktop Docker Desktop поддерживает встроенный кластер Kubemetes с одним узлом, кото­ рый можно включить в настройках приложения. Встроенный кластер Kubemetes не так легко настроить, но это удобный вариант, если мы хотим изучить базовый функционал Kubemetes. Чтобы включить в Docker Desktop встроенный функционал Kubemetes, запустите Docker Desktop, а затем нажмите на значок Docker с китом на панели задач и выбе­ рите пункт Preferences (Настройки). На вкладке Kubernetes выберите флажок ЕпаЫе Kubernetes (Включить), а затем нажмите кнопку Apply & Restart (Приме­ нить и перезапустить), чтобы внести необходимые изменения в виртуальной маши­ не. В первый раз Docker выполнит команду kuЬeadm для создания кластера Kuber­ netes (https://kubernetes.io/docs/reference/setup-tools/kubeadm). Если вы хотите узнать больше о включении встроенного Kubernetes в Docker Desktop, прочтите эту статью (https://www.docker.com/Ыog/how-kubernetes­ works-under-the-hood-with-docker-desktop). При этом будет создан новый контекст kuЬectl с именем docker-desktop, и мы автома­ тически перейдем в этот контекст. Проверить текуший контекст можно следующей командой: $ kuЬectl config current-context docker-desktop При необходимости мы можем перейти в нужный контекст: $ kuЬectl config use-context docker-desktop --namespace;default Switched to context "docker-desktop". Наконец, если мы хотим убрать текущий контекст, можно задать команду: $ kuЬectl config unset current-context Property "current-context" unset. Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 1 259 Запустив кластер, мы можем взаимодействовать с ним так же, как с любым другим кластером Kubemetes, - с помощью команды kuЬectl. При завершении работы Docker Desktop кластер Kubemetes тоже останавливается. Если хотите полностью отключить кластер Kubemetes, вернитесь на панель Preferences, перейдите на вкладку Kubernetes и уберите флажок ЕпаЫе Kuber­ netes. Kind Последний инструмент для Kubemetes, который мы рассмотрим, - kind. Это про­ стое и удобное средство позволяет управлять кластером Kubemetes, который со­ стоит из одного или нескольких контейнеров Linux, запущенных в Docker. Назва­ ние kind - это акроним, обозначающий Kubemetes in Docker (Kubernetes в Docker). Кроме того, тип объекта в Kubemetes определяется в API по полю Kind. Искать информацию об этом инструменте в Интернете будет непросто, так что ориентируйте�ь на главный сайт (https://kind.sigs.k8s.io/) с самим инструментом и документациеи к нему. Инструмент kind - это что-то среднее между упрощенным кластером Kubemetes, встроенным в виртуальную машину Docker, и виртуальной машиной minikuЬe, кото­ рая иногда чересчур усложняется. kind распространяется в одном двоичном файле, и его можно установить с помощью любого менеджера пакетов или на странице выпусков kind (https://github.com/kubernetes-sigs/kind/releases), где можно загру­ зить актуальный выпуск для вашей системы. Если вы загружаете файл вручную, переименуйте его в kind, скопируйте в каталог по соответствующему пути и убеди­ тесь, что у него есть необходимые разрешения для запуска. После установки kind попробуем создать первый кластер: $ kind create cluster --name test Creating cluster "test" ... ✓ Ensuring node image (kindest/node:vl.25.3) ✓ Preparing nodes ✓ Writing configuration ✓ Starting control-plane • ✓ Installing CNI � ✓ Installing StorageClass М Set kuЬectl context to "kind-test" You can now use your cluster with: kuЬectl cluster-info --context kind-test Thanks for using kind 1 � По умолчанию эта команда запускает один контейнер Docker, который представля­ ет кластер Kubemetes с одним узлом, используя последний стабильный релиз Kubemetes, поддерживаемый инструментом kind. Книги для программистов: https://clcks.ru/books-for-it 260 1 Глава 10 kind задает для текущего контекста кластер Kubernetes, поэтому мы сразу можем выполнять команды kuЬectl: $ kuЬectl cluster-info KuЬernetes control plane is running at https://127.0.0.1:56499 CoreDNS is running at https://127.0.0.1:56499/api/vl/namespaces/kuЬe-system/services/kuЬe-dns:dns/proxy То further debug and diagnose cluster proЫems, use _'kuЬectl cluster-info dump'. Мы можем посмотреть отредактированную версию информации, с помощью кото­ рой kuЬectl подключается к серверу Kubernetes: $ kuЬectl config view --minify apiVersion: vl clusters: - cluster: certificate-authority-data: DATA+OМITTED server: https://127.0.0.1:56499 name: kind-test contexts: - context: cluster: kind-test user: kind-test name: kind-test current-context: kind-test kind: Config preferences: {} users: - name: kind-test user: client-oertificate-data: REDACTED client-key-data: REDACTED У kind есть расширенные функции (https://kind.sigs.k8s.io/docs/user/quick-start/ #advanced), и их можно использовать через файл конфигурации, который мы пере­ даем с аргументом --config при запуске кластера. Вот несколько полезных функций: ♦ Изменение текущей версии Kubernetes. ♦ Запуск нескольких рабочих узлов. ♦ Запуск нескольких узлов плоскости управления для тестирования высокой дос­ тупности. ♦ Проброс портов между Docker и локальным хостом. ♦ Включение и отключение функций Kubernetes с помощью feature gate (https:// kubernetes.io/docs/reference/command-line-tools-reference/feature-gates). Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 1 261 ♦ Экспорт журналов компонентов ruюскости управления· с помощью kind export logs и многое другое. Применяя kind, помните, что Kubemetes выполняется в одном или нескольких кон­ тейнерах, которые, в свою очередь, могут находиться в виртуальной машине Linux, если вы работаете с Docker Desktop или подобным инструментом. Это значит, что при запуске кластера необходимо будет дополнительно настроить проброс портов. Для этого можно использовать extraPortMappings в конфигурации kind. Давайте удалим кластер: $ kind delete cluster --name test Deleting cluster "test" ... Amazon ECS и Fargate Одна из самых популярных облачных платформ Amazon AWS в середине 2014 года предложила АWS Elastic Beanstalk (https://amzn.to/2wNalrL) для нативной под­ держки контейнеров. Правда, этот сервис назначает экземпляру Amazon только один контейнер, а значит, это не идеальное решение для краткосрочных или легких контейнеров. Amazon Elastic Compute Cloud (Amazon ЕС2) - это отличная плат­ форма для размещения среды Docker, и поскольку у самого Docker достаточно воз­ можностей, нам не нужно добавлять много инструментов для эффективной работы. Однако у Amazon есть решение, созданное специально для контейнеров, - Amazon Elastic Container Service (Amazon ECS). В последние несколько лет Amazon разви­ вает новые продукты, например Elastic Kubemetes Services (EKS) и АWS Fargate. Fargate - это просто маркетинговое название для обозначения функции ECS, ко­ торая позволяет AWS автоматически управлять всеми узлами в кластере контей­ неров, чтобы мы могли сосредоточиться на развертывании сервиса, а не на ин­ фраструктуре. ECS - это набор инструментов, который координирует несколько компонентов АWS. При использовании ECS мы можем добавить Fargate. Если добавляем, нам почти ничего не приходится делать самим. Если мы обходимся без Fargate, то в до­ полнение к узлам кластера, которые возьмут на себя рабочую нагрузку, потребует­ ся добавить в кластер один или несколько экземтшяров ЕС2 с Docker и специаль­ ным агентом Amazon ECS. Если мы запустим Fargate, нам не придется самим управлять кластером. В любом случае мы запускаем кластер и отправляем в него контейнеры. Агент Amazon ECS (https://github.com/aws/amazon-ecs-agent), который мы упомя­ нули, координирует работу кластера и планирует контейнеры на хостах. Мы взаи­ модействуем с ним, только когда управляем традиционным кластером ECS без Fargate. Книги для программистов: https://clcks.ru/books-for-it 262 1 Глава 10 Базовая настройка AWS Предполагается, что у вас есть доступ к аккаунту А WS и вы знакомы с сервисом. Посетите страницу https://aws.amazon.com/free, чтобы узнать о ценах и создать аккаунт. Amazon предлагает бесплатный доступ, и вам хватит его для эксперимен­ тов в этой книге, если у вас нет платного аккаунта. После создания аккаунта А WS вам потребуется хотя бы один административный пользователь, пара ключей, Amazon Virtual Private Cloud (Amazon VPC) и группа безопасности по умолчанию в вашей среде. Если у вас чего-то не хватает, следуйте инструкциям в документа­ ции Amazon (https://amzn.to/2FcPDSL). Настройка ролей IAM Роли Amazon Identity and Access Management (Amazon IАМ) позволяют разрешать пользователям разный набор действий в облачной среде. Нам нужна возможность предоставлять необходимые разрешения. Для работы с ECS необходимо создать роль ecsinstanceRole, к которой привязана управляемая роль Amэ.zonEC2ContainerServiceRole. Самый постой способ это сделать - войти на консоль AWS (https://console.aws. amazon.com/) и перейти в раздел ldentity and Access Management (https://console. aws.amazon.com/iam/home). Проверьте, установлена ли необходимая роль. Если она уже существует, убеди­ тесь, что она правильно настроена, потому что за последние годы правила немно­ го изменились. 1. На левой панели выбираем раздел Roles (Роли). 2. Нажимаем кнопку Create role (Создать роль). 3. В разделе АWS Service (Сервис А WS) выбираем Elastic Container Service. 4. В разделе Select your use case (Выберите вариант применения) выбираем Elastic Container Service. 5. Нажимаем кнопку Next: Permissions (Далее: Разрешения). 6. Нажимаем Next: Review (Далее: Проверка). 7. В поле Role Name (Имя роли) вводим ecslnstanceRole. 8. Нажимаем кнопку Create role (Создать роль). Если вы хотите хранить конфигурацию контейнеров в бакете объектного хранили­ ща SЗ, изучите документацию (https://amzn.to/2PNapOL) по конфигурации агента контейнеров Amazon ECS. Настройка AWS CLI Amazon предлагает инструменты командной строки для работы с инфраструктурой, использующей API. Установите последнюю версию AWS CLI. У Amazon есть подКниги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 1 263 робная документация (https://amzn.to/lPCpPNA) по установке инструментов, но мы вкратце приведем эти инструкции здесь. Установка Здесь мы рассмотрим установку на нескольких операционных системах, но не за­ бывайте, что всегда можно запустить А WS CLI через контейнер Docker. Вы можете сразу перейти к разделу с нужной операционной системой или прочитать все раз­ делы, если вам нравятся инструкции по установке или просто любопытно. macOS В главе 3 мы устанавливали Homebrew. Если вы выполнили те инструкции, устано­ вите А WS CLI следующей командой: $ brew update $ brew install awscli Windows Amazon предоставляет стандартный установщик MSI для Windows, который можно загрузить из Amazon SЗ для вашей архитектуры: ♦ 32-разрядная Windows (https://s3.amazonaws.com/aws-cli/AWSCLI32.msi); ♦ 64-разрядная Windows (https://s3.amazonaws.com/aws-cli/АWSCLI64.msi). Другие ОС Инструменты Amazon CLI написаны на Python, так что на большинстве платформ их можно устанавливать с помощью менеджера пакетов Python pip следующей командой в командной оболочке: $ pip install awscli --upgrade --user На некоторых платформах pip не установлен по умолчанию и нужно использовать менеджер пакетов easy_install: $ easy_install awscli Конфигурация Убедитесь, что установлен AWS CLI версии не ниже 1.7.0: $ aws --version aws-cli/1.14.50 Python/3.6.4 Darwin/17.3.0 botocore/1.9.3 Для настройки А WS CLI нужен идентификатор ключа доступа А WS и секретный ключ доступа А WS. Мы выполним команду configure и должны будем ввести дан­ ные для аутентификации и настройки по умолчанию: $ aws configure AWS Access Кеу ID (None]: ЕХАМРLЕЕХАМРLЕЕХАМРLЕ AWS Secret Access Кеу (None]: ExaMPleKEy/7EXAМPL3/EXaMPLeEXAМPLEKEY Default region name (None]: us-east-1 Default output format (None]: json Книги для программистов: https://clcks.ru/books-for-it 264 Глава 10 Сейчас подходящий момент, чтобы убедиться, что инструменты CLI работают нормально. Для этого вьmолним следующую команду, чтобы вывести пользовате­ лей IАМ в аккаунте: $ aws iam list-users Если все пошло по плану и вы выбрали JSON в качестве выходного формата по умолчанию, то увидите примерно следующее: "Users": [ "Path": "/", "administrator", "ExmaPLЗExmaPLЗExmaPLЗEx", "Arn": "arn:aws:iam::936262807352:user/myuser", "CreateDate": "2021-04-08Т17:22:23+00:ОО", "PasnordLastUsed": "2022-09-0STlS:56:21+00:00" "UserNallle": "Userid": Экземпляры контейнеров После установки необходимых инструментов в первую очередь следует создать хотя бы один кластер, в котором будут регистрироваться новые хосты Docker. 1-Ж � Кластер по умолчанию так и называется - default. Если вы решите оставить это имя, вам не придется указывать --cluster-name в последующих командах. Для начала нужно создать в сервисе контейнеров кластер, а затем мы сможем за­ пускать в нем задачи. В следующих примерах создадим кластер с именем fargate­ testing: $ aws ecs create-cluster --cluster-name fargate-testing "cluster": ( "arn:aws:ecs:us-east-1:1 ... 2:cluster/fargate-testing", "fargate-testing", "clusterArn": "cluste:Name": "status": "ACTIVE", "reqisteredContainerinstanoesCount": О, "pendi.ngТasksCount": О, О, "runningТasksCount": "activeServioesCount": О, "statistics": [], "taqs": [], "settinqs": "name": "containerinsights", Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 265 "value": "disaЫed" ), "capacityProviders": [], "defaul tcapaci tyProviderStrategy" : [] До выхода А WS Fargate нужно было создавать экземпляры А WS ЕС2 с помощью docker и ecs-agent, а затем добавлять их в кластер. При желании мы и сейчас можем применить этот подход (Ес2 launch type), но Fargate упрощает запуск динамического кластера, который будет менять масштаб в зависимости от рабочей нагрузки. Задачи Мы создали кластер контейнеров, и теперь можем использовать его для работы. Давайте создадим хотя бы одно определение задачи. В Amazon ECS определением задачи называется список контейнеров, объединенных в одну группу. Чтобы создать первое определение задачи, откройте любой редактор, скопируйте следующий код JSON и сохраните его как файл webgame-task.json в текущем ката­ логе: "containerDefinitions": "name": 11 web-game 11 , "image": 11 spkane/quantum-game", ''cpu": о, "portмappings": ( 11 containerPort 11 : "hostPort": 8080, "protocol": 11 tcp 11 8080, ), essential 11 : true, "environment": [), 11mountpoints 11 : [), 11 volumesFrom11 : [) 11 ], 11 family 11 : 1 f g 11 1 ar ate-game , orkМode 11 : 11awsvpc 11, "volumes": [], 11 placementconstraints 11 : [], 11 requiresCompatiЬilities 11 : 11 netw 11 FARGATE 11 ], Книги для программистов: https://clcks.ru/books-for-it Глава 10 266 "cpu": "256", "memory": "512" Мы можем выгрузить эти и некоторые другие файлы: git clone \ https://githuЬ.com/Ыuewhalebook/\ docker-up-and-running-Зrd-edition.git \ --config core.autocrlf=input URL продолжается на следующей строке, чтобы вписаться в ширину страницы. Возможно, потребуется пересобрать его и убрать обратные косые черты, чтобы команда работала. В определении задачи мы указываем, что хотим создать группу задач fargate-game с одним контейнером web-game с игрой Quantum (https://github.com/stared/quantum­ game). В одной из предыдущих глав мы видели, как образ Docker запускает брау­ зерную игру на основе квантовой механики. Fargate ограничивает параметры, которые мы можем задать в этой конфигурации, включая networkМode (сетевой режим), cpu (процессорные ресурсы) и memory (па­ мять). В документации AWS (https://amzn.to/2PkliGR) подробно описаны парамет­ ры в определении задач. В этом определении задачи ограничиваем использование памяти и процессора кон­ тейнером, а также указываем для Amazon, критичен ли этот контейнер для нашей задачи. Флаг essential (критически важен) удобен, когда в задаче определено не­ сколько контейнеров, но не все они необходимы для успешного выполнения зада­ чи. Если для essential задано true и контейнер не запускается, все контейнеры, определенные в задаче, будут остановлены, а задача помечена как завершившаяся сбоем. Кроме того, с помощью определения задачи мы можем задать почти все типичные переменные и настройки, которые мы указали бы в Dockerfile или в ко­ мандной строке docker container run. Для отправки этого определения задачи на Amazon выполним следующую команду: $ aws ecs register-task-definition --cli-input-json file://./webgame-task.json "taskDefinition": { "taskDefinitionArn": "arn:aws:ecs:... : task-definition/fargate-game:1", "containerDefinitions": "name": "web-game", "image": "spkane/quantum-game", "cpu": О, "portмappings": [ "containerPort": 8080, "hostFort": 8080, "protocol": "tcp" ], Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 267 essential 11 true, environment11 111110untpoints 11 11 : 11 : : 11 volumesFrom11 : ], 11 family 11 : 11 11 volumes 11 : 11 status 11 : [] , [] f argate-game11 awsvpc11 11 networkМod 11 e : 11 rev ision 11 : [], 11 , , 1, [] , 1 ACTIVE 1 1, 1 11 requiresAttriЬutes 11 : 11 name 11 : 11 name11 : 1 1 com.amazonaws.ecs.capaЬility .docker-remote-api.1.18 11 11 ecs.capaЬility.task-eni11 }, ], 11 placementConstraints": [] , 11 compatiЬilities 11 : 11 ЕС2" , "FARGATE 11 ], 11 requiresCompatiЬilities": 11 FA RG ATE 11 ], 11 11 cpu 11 11 256", memory11 н512 11 : : 11 registeredAt 11 : 11 registeredВy11 : ' 2022-09-05Т09:10:18.184000-07:0011 "arn:aws: iam::...:user/me11 11 , Теперь можно посмотреть все определения задач: $ aws ecs list-task-definitions 11 taskDefinitionArns 11 : [ "arn:aws:ecs:us-east-1: ...:task-definition/fargate-game:l", Мы готовы создать первую задачу в кластере, выполнив команду, приведенную далее. Аргумент count позволяет указать, сколько копий задачи необходимо развер­ нуть в кластере. Для этого задания будет достаточно одной. Измените команду, чтобы указать идентификатор подсети и группы безопасности для вашего А WS УРС. Эти данные можно найти на консоли А WS (https://console. Книги для программистов: https://clcks.ru/books-for-it Глава 10 268 aws.amazon.com/vpc/home) или с помощью команд АWS CLI aws ес2 describe­ suЬnets и aws ес2 describe-security-groups. Вы также можете указать АWS, что нужно назначить задачам публичный IР-адрес, задав примерно следующую сетевую кон­ фигурацию: awsvpcConfiguration= {suЬnets= [suЬnet-aЬcdl234], securityGroups= [sg-aЬcdl234], assignPuЬlicip=ENABLED} Публичный IР-адрес может понадобиться, если мы используем публичные подсети: $ aws ecs create-service --cluster fargate-testing --service-name \ fargate-game-service --task-definition fargate-game:l --desired-count 1 \ --launch-type "FARGATE" --network-configuration \ "awsvpcConfiguration= {suЬnets= [suЬnet-abcdl234],\ securityGroups= [sg-aЬcdl234]}" "service": { "arn:aws:ecs:...:service/fargate-game-service", "fargate-game-service", "cluзterArn": "arn:aws:ecs: ...: cluster/fargate-testing", "loaclВalancers" : [], "serviceRagistries": [], "status": "ACTIVE", "serviceArn": "serviceNaшe": "desiredCount": 1, "runningCount": О, О, "FARGATE", "platformVersion": "LATEST", "platformFami.ly": "Linux", "taskDefinition": "arn:aws:ecs: ...:task-definition/fargate-game:l", "deployшentconfiguration": { "pendingCount": "launchТype": "deployшentcircui tвreaker": false, false "enaЬle": "rollЬack": }, "JDIOvjпnпnP-trcent": 200, "lllinilllualНealthyPercent": 100 }, "deployшents": [ "id": "ecs-svc/ ...", "statuз": "PRIМARY", "taskDefinition": "arn:aws:ecs:... definition/fargate-game:l", "desiredCount": 1, "pendingCount": О, "runningCount": О, "failedтasks": о, Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 269 2022-09-05Т09:14:51.653000-07:00", "2022-09-05Т09: 14:51.653000-07:00", 11 launchТype 11 : "FARGATE 11 , 11 platformVersion 11 : "1.4. О", 11 platformFami.ly 11 : 11 Linux", 11 createdAt 11 : 11 upd 11 11 11 atedAt : networkConfiguration 11 : ), 11 11 rolloutstate 11 : ], 11 "IN_PROGRESS", "ECS deployment ecs-svc/... in progress." rolloutstateReason 11 : roleArn": "...aws-service-role/ecs.arnazonaws.com/AWSServiceRoleForECS", "events 11 : [], "createdAt": "2022-09-05Т09: 14:51.653000-07:00", [ "placementconstraints 11 : ], [] , "placementstrategy11 : "networkConfiguration 11 : ), "REPLICA", "arn:aws:iam::... :user/me", "schedulingStrategy 11 : 11 createdВy": "enaЫeECSМanagedTags 11 : false, 11 propagateTags 11 : "NONE", 11 enaЬleExecuteCOIIIIDal1d. 11 : false Для Fargate и сети awsvpc требуется роль для ECS, связанная с сервисом. В пре­ дыдущих выходных данных есть строка, которая заканчивается следующим обра­ зом: "role/aws-service-role/ecs.arnazonaws.com/ AWSServiceRoleForECS" Обычно она создается автоматически, но мы можем сделать это вручную: $ aws iam create-service-linked-role \ --aws-service-name ecs.amazonaws.com Мы можем просмотреть все сервисы в кластере следующей командой: $ aws ecs list-services --cluster fargate-testing 11 serviceArns 11 : [ "arn:aws:ecs:us-west-2:...:service/fargate-testing/fargate-game-service" Следующая команда предоставляет все детали о сервисе: $ aws ecs describe-services --cluster fargate-testing \ --services fargate-game-service Книги для программистов: https://clcks.ru/books-for-it Глава 10 270 "services": [ 11 dep loyments 11 : ecs-svc/...11 11 s tat us 11 : PRIМARY", 11 taskDefinition 11 : "arn: ...: task-definition/fargate-game: 1", 11 desiredCount 11 : 1, 11 dingCount 11 : 1, 11 id": 11 1 11 pen 11 runningCount 11 : 11 c 1 1 reatedAt 11 : updatedAt 11 : О, 11 2022-09-05Т09:14: 51.653000-07:00 11 , "2022-09-05Т09:14:51.653000-07:00 11 1 11 11 1 launch'l'ype ; "FARGATE / 11 1p s e latformV r ion : "1.4.0", 1 1 1 1 1p 1 latformFamily 11 : inux 11 , networkConfiguration 11 : ), 11 rolloutstate 11 : 11 1L 1 1 1 IN_PROGRESS", 1 ECS deployment ecs-svc/... progress. 1 rolloutstateReason 11 : 1 1 ], roleArn 11 : 11 •••role/ecs.amazonaws.com/AWSServiceRoleForECS 11 , 11 events 11 : [ 11 11 id": 11 83Ьd5с2ееd5d4866ЬЬ7ес8с3с938666с", 2022-09-05Т09:14:54.950000-07:00 11 11 createdAt 11 : 11mess age 11 : 11 11 ( ••• , game-service) has started 1 tasks: ( ...) . 1 1 ], ], 11failures 11 : [] Их этих выходных данных можно многое узнать обо всех задачах в сервисе. Сейчас у нас запущена одна задача. Значение task-definition содержит имя, за которым следует число (fargate­ game: 1), указывающее на версию. Если мы внесем изменения и снова зарегистри­ руем задачу командой aws ecs register-task- definition, версия изменится, т. е. мы будем ссылаться на новую версию в командах, например aws ecs updateservice. Если мы не изменим число, то продолжим запускать контейнеры с помо­ щью старого JSON. Номера версий позволяют легко откатывать изменения и тес­ тировать новые версии без последствий для будущих экземпляров. Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 271 Мы можем просмотреть отдельные задачи в кластере: $ aws ecs list-tasks --cluster fargate-testing [ "arn:aws:ecs:...:task/fargate-testing/83bd5c2eed5d4866bЬ7ec8c3c938666c" "taskArns": Сейчас в кластере только одна задача, поэтому список будет очень маленьким. Чтобы узнать больше об отдельной задаче, можно выполнить следующую команду, указав соответствующий идентификатор задачи: $ aws ecs describe-tasks --cluster fargate-testing \ --task 83bd5c2eed5d4866bЬ7ec8c3c938666c "tasks": [ "attachments": "details": "name": "networkinterfaceid", "eni-00a40225208c9411a" "value": ), "name": "privateIPv4Address", "172.31.42.184" "value": ], "attriЬutes": ], "us-west-2b", "arn:aws:ecs:us-west-2: ...:cluster/fargate-testing", "connectivity": "CONNECTED", "connectivityAt": "2022-09-05Т09:23:46.929000-07:00", "containers": [ "availaЬilityZone": "clusterArn": "arn: ... :container/fargate-testing/ ... ", "arn:...:task/fargate-testing/...", "containerArn": "taskArn": "name": 11 web-g ame 11 , "image": "spkane/quantum-game", Книги для программистов: https://clcks.ru/books-for-it Глава 10 272 11 runtimeid 11 : 11 laststatus 11 : "83bd...998", "RUNNING", 11 networkinterfaces 11 : [ ntid 11 : "ddaЬ...37За 11 , 11 attachme 11 priv ateipv4Address 11 : ], 11 healthStatus11 : "cpu": "О" ], 11 cpu 11 : "172.31.42.184" "UNКNOWN", "256", "createdAt 11 : "2022-09-05ТО9:23:42.700000-07:00", "desirec\Status": "RUNNING", "enaЬleExecuteeoaaand": false, "qroup 11 : "service:fargate-game-service", 11 healt h Status 11 : "UNКNO�IN", "laststatus 11 : "RUNNING", 11 launchТype 11 : "FARGATE", 1111181110ry11 : "512", 11 overrides 11 : { 11 contai.ner0verrides 11 : 11 name 11 : "web-game" ], inferenceAcceleratorOverrides" : [] }, 11 platfot111Version 11 : "1.4.0", 11 platforшFamily 11 : "Linux", 11pullStartedAt 11 : "2022-09-05Т09:59:36.554000-07:00", 11 pullStoppedAt": "2022-09-05Т09:59:46.361000-07:00", 11 startedAt 11 : "2022-09-05Т09:59:48.546000-07:00", " startec1Вy11 : "ecs-svc/ ...", 11 tags 11 : [ , ] 11 taskArn 11 : "arn:aws:...:task/fargate-testing/83bd... бббс", 11 taskDefinitionArn 11 : "arn:aws:...:task-definition/fargate-game:l", 11 version 11 : 4, "epheшeralStorage 11 : 11 sizeinGiB 11 : 20 11 ], 11 failures": [] Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 273 Если ключ laststatus имеет значение PENDING (В ожидании), скорее всего, сервис по­ ка запускается. Выведите информацию о задаче снова и убедитесь, что состояние изменилось на RUNNING (Выполняется). Когда ключ lastStatus будет иметь значение RUNNING, мы сможем протестировать контейнер. В некоторых сетях задача не сможет загрузить образ. Если возникнет ошибка: "stoppedReason": "CannotPullContainerError: inspect image has Ьееп retried 5 time(s): failed to resolve ref \"docker.io/spkane/quantum-game:latest\": failed to do request: Head https://registry-1.docker.io/v2/spkane/quantum-game/manifests/latest: dial tcp 54.83.42. 1 45:443: i/o timeout", изучите руководство по устранению неполадок . Тестирование задачи Вам понадобится современный браузер для подключения к контейнеру и тестиро­ вания игры. В предыдущих выходных данных для частного IPv4-aдpeca (privateIPv4Address) ука­ зано значение 172.31.42.184. У вас будут другие цифры. Если вам нужно больше информации о настройках сети для вашей задачи и экземпляре ЕС2, на котором она запущена, скопируйте идентификатор сетевого интерфейса networkinterfaceid в выходных данных aws ecs describe-tasks и до­ бавьте его в команду aws ес2 describe-network-interfaces --network-interface-ids, чтобы узнать всю необходимую информацию, включая значение PuЫicip, если вы настроили в сервисе публичный IР-адрес. Убедитесь, что подключены к сети, которая имеет доступ к публичному или част­ ному IР-адресу хоста, а затем откройте браузер и перейдите к порту 8080 по этому IР-адресу. В этом примере частный URL-aдpec будет выглядеть следующим образом: http:l/172.31.42.184:8080/ Если все работает, как ожидалось, откроется игра Quantum Game. Официальную версию игры вы найдете на сайте https://quantumgame.io. Мы поймем, если вы отложите книгу и на несколько часов увлечетесь игрой и изу­ � чением квантовой механики. Книга подождет. Продолжите, когда будете готовы. � Остановка задачи Итак, у нас выполняется задача. Давайте узнаем, как ее остановить. Нам пона­ добится ее идентификатор, и мы можем его получить, снова выведя все задачи, выполняющиеся в кластере: 1 Полный URL: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_cannot_pull_image.html. Книги для программистов: https://clcks.ru/books-for-it 274 Глава 10 $ aws ecs list-tasks --cluster fargate-testing 11 taskArns 11 : 1 1 [ arn:aws:ecs: ...:task/fargate-testing/83bd5c2eed5d4866bЬ7ec8c3c938666c 11 Мы также можем узнать идентификатор из информации о сервисе: $ aws ecs describe-services --cluster fargate-testing \ --services fargate-game-service id11 : "6Ь7f...0384", "2022-09-05ТО9:59:23.917000-07:00", 11message 11 : " • • • : (task 83bd5c2eed5d4866bЬ7ec8c3c938666c) . " 11 11 createdAt 11 : Наконец, мы можем остановить задачу, выполнив следующую команду с указанием идентификатора: $ aws ecs stop-task --cluster fargate-testing \ --task 83bd5c2eed5d4866bb7ec8c3c938666c "STOPPED", 11 desiredStatus11 : 11 lastStatus 11 : "RUNNING", 11 stopCode 11 : "Userinitiated", "stoppedReason 11 : 11Task stopped Ьу user 11 , "stoppingAt 11 : 11 2022-09-0STl0:29:05.110000-07:00", Если мы снова посмотрим на задачу по тому же идентификатору, то увидим, что ключ laststatus имеет значение STOPPED (Остановлено): $ aws ecs describe-tasks --cluster fargate-testing \ --task 83bd5c2eed5d4866bb7ec8c3c938666c 11 desiredStatus11 : 11 STOPPED", 11 lastStatus": 11 STOPPED 11 , Книги для программистов: https://clcks.ru/books-for-it Масштабирование контейнеров 275 Если мы посмотрим задачи в кластере, то получим пустой набор: $ aws ecs list-tasks --cluster fargate-testing "taskArns" : [ ] Теперь мы можем создавать более сложные задачи, которые связывают несколько контейнеров, и использовать инструменты ECS и Fargate для запуска хостов и раз­ вертывания задач в кластере. Очистить среду ECS можно следующими командами: $ aws ecs delete-service --cluster fargate-testing \ --service fargate-game-service --force $ aws ecs delete-cluster --cluster fargate-testing Заключение В этой главе мы рассмотрели много инструментов. Вряд ли вы будете использовать их все, потому что их функции во многом совпадают, однако каждый из них под­ держивает свой подход к рабочей среде и решает разные проблемы. Изучив эти ин­ струменты, вы получите хорошее представление о том, какие варианты позволят вам создать среду для контейнеров Linux. В основе всех этих инструментов лежит легко переносимый формат образов Docker для контейнеров Linux, а также возможность абстрагировать большую часть систе­ мы Linux, поэтому мы можем легко переносить приложения между центрами обра­ ботки данных и облачными платформами. Осталось выбрать идеальный подход для вашей организации, а затем реализовать его. В следующей главе мы будем рассматривать технические тонкости экосистемы Docker, включая безопасность, сети и хранение. Книги для программистов: https://clcks.ru/books-for-it ГЛАВА 11 Расширенные концепции В этой главе мы рассмотрим расширенные концепции. Предполагается, что вы уже довольно хорошо знакомы с Docker, используете его в рабочей среде или как минимум часто с ним работаете. Мы подробно обсудим, как работают контейнеры, а также поговорим о безопасности, сетях и плагинах Docker, альтернативных сре­ дах выполнения и других расширенных конфигурациях. Также мы рассмотрим изменения, которые можно внести в установку Docker. Ино­ гда это удобно, но в Docker, как и в большинстве других платформ, неплохие настройки по умолчанию, и нам не следует их менять без особой причины и без четкого понимания того, что мы делаем. Вам потребуется время, чтобы методом проб и ошибок подстроить систему под вашу среду, но мы не рекомендуем менять настройки по умолчанию, если вы не до конца понимаете последствия этих изме­ нений. Контейнеры в деталях Обычно мы говорим о контейнерах Linux как о едином объекте, хотя на самом деле они реализуются с помощью нескольких отдельных механизмов, встроенных в ядро Linux и работающих вместе: контрольные группы (control groups, cgroups), про­ странства имен, безопасный режим вычислений (Secure Computing Mode, seccomp) и SELinux или AppArmor. Вместе они инкапсулируют процесс. Контрольные группы отвечают за ограничения ресурсов, пространства имен позволяют процессам ис­ пользовать ресурсы с одинаковыми именами и изолируют их от остальных объек­ тов, Secure Computing Mode ограничивает доступные системные вызовы, а SELinux и AppArmor обеспечивают дополнительную изоляцию процессов в целях безопас­ ности. Итак, зачем нужны cgroups и пространства имен? Прежде чем мы углубимся в детали, давайте рассмотрим аналогию, которая помо­ жет вам понять, как эти компоненты взаимодействуют при работе контейнера. Представьте, что компьютер - это большой открытый склад, полный работников (процессов). На складе много пространства и ресурсов, но работники мешают друг другу, а ресурсы получает тот, кто успеет схватить их первым. Когда мы используем Docker и контейнеры Linux для рабочих нагрузок, склад пре­ вращается в офисное помещение, в котором у каждого работника есть свой каби­ нет. В каждом кабинете есть все необходимое для работы, и, как правило, теперь сотрудники могут заниматься своим делом, не заботясь о том, что делают осталь­ ные. Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 1 277 Пространства имен - это стены кабинета, благодаря которым процессы не могут бесконтрольно взаимодействовать с соседями. Контрольные группы - это аренд­ ная плата за ресурсы. Когда процесс запускается впервые, ему выделяется процес­ сорное время и хранилище на каждый цикл, а также память, которая будет доступ­ на ему в каждый момент. Таким образом работники (процессы) получают необхо­ димые ресурсы без ущерба для коллег. Представьте себе худший пример шумного соседа, и вы поймете, почему так важно окружить кабинеты толстыми стенами. На­ конец, Secure Computing Mode, SELinux и AppAлnor - это служба безопасности, благодаря которой даже в случае инцидента, скорее всего, придется просто запол­ нить чуть больше документов и отправить отчет. cgroups В традиционных распределенных системах нам приходится выполнять каждую ин­ тенсивную задачу на отдельном виртуальном сервере. Например, не следует запус­ кать приложения на севере базы данных, потому что они захватят все ресурсы, а производительность баз данных упадет. Использовать для этого реальное оборудование было бы слишком дорого, и на вы­ ручку приходят виртуальные серверы, которые позволяют размещать на дорогом оборудовании несколько конкурирующих приложений, а слой виртуализации отве­ чает за распределение ресурсов. Так дешевле, но все же недостаточно экономно, если нам не нужны остальные возможности виртуализации, потому что использо­ вание нескольких ядер приводит к дополнительным издержкам для приложений. Обслуживать виртуальные машины тоже недешево, зато облачные вычисления до­ казали свою полезность и высокую эффективность при применении правильных инструментов. Если нам требуется только разделение ресурсов, было бы гораздо удобнее работать на одном ядре, не запуская еще один экземпляр операционной системы. В течение многих лет мы могли выражать только свои "пожелания" о приоритетности (niceness) выполнения процессов и не могли задавать жесткие ограничения, как в виртуальных машинах. Эти пожелания нельзя было детально настраивать, можно было только указать, что этому процессу следует выделить больше операций ввода­ вывода или меньше ЦП, чем остальным. Контейнеры Linux обеспечивают деталь­ ный контроль с помощью cgroups, которые были разработаны специально для реше­ ния этих проблем еще до Docker. Контрольные группы позволяют ограничивать ресурсы, предоставляемые роди­ тельским и дочерним процессам. Это механизм, с помощью которого ядро Linux контролирует ограничения памяти, подкачки, процессора, хранилища и опера­ ций ввода-вывода, встроенный в ядро и впервые появившийся в 2007 году в Linux 2.6.24. В официалъной документации по ядру (https://www.kernel.org/doc/ Documentation/cgroup-v2.txt) они определены как "механизм иерархической орга­ низации процессов и контролируемого распределения ресурсов системы в иерар­ хии". Важно отметить, что эти настройки применяются к процессу и всем его дочерним процессам, и такая организация отражает структуру контейнеров. Книги для программистов: https://clcks.ru/books-for-it 278 Глава 11 У контрольных групп Linux было два выпуска: версия 1 (https://www.kernel.org/doc/ Documentation/cgroup-v1/cgroups.txt) и версия 2 (https://www.kernel.org/doc/ Documentation/cgroup-v2.txt). Проверьте, какая версия используется в рабочей сре­ де, чтобы задействовать все доступные возможности. Каждому контейнеру Linux назначается уникальная группа cgroup. Все процессы в контейнере входят в одну группу, т. е. мы можем контролировать ресурсы для каждого контейнера, не беспокоясь о том, какие процессы в нем выполняются. Если мы добавляем в контейнер процессы и развертываем его снова, Docker может назначить ту же политику, и она будет применяться ко всему контейнеру и ко всем процессам в нем. Мы уже говорили, что можем настраивать cgroups в Docker через API. Этот интер­ фейс позволяет управлять памятью, подкачкой и использованием диска, но cgroups дает гораздо больше возможностей, например мы можем помечать сетевые пакеты от контейнера тегами, чтобы приоритизировать трафик. Эти функции могут пона­ добиться для контроля контейнеров. Группы cgroups обрабатывают много данных о ресурсах, необходимых каждой группе, а значит, при их применении в ядре мож­ но найти много интересной статистики о потребляемых процессами ресурсах про­ цессора, ОЗУ, операциях ввода-вывода и т. д. Docker с помощью cgroups не только ограничивает ресурсы, но и предоставляет данные по ним. Мы можем посмотреть много разных метрик, например с помощью команды docker container stats. Файловая система /sys Даже если мы настроили cgroups в Docker, для тонкого управления ими потребуется наше вмешательство. Это самый удобный способ контролировать параметры, по­ тому что изменения можно вносить не только при создании контейнера, но и на лету. В системах с systemd для этого предусмотрены инструменты командной строки, вроде systemctl. Поскольку cgroups встроены в ядро, самый универсальный способ взаимодействовать с ядром напрямую через файловую систему /sys. Файловая сис­ тема /sys предоставляет несколько параметров и выходных данных ядра. Мы можем использовать ее с простыми инструментами командной строки, чтобы опре­ делять поведение ядра. Этот метод настройки cgroups для контейнеров работает только напрямую на серве­ ре Docker, не через API. При реализации этого метода мы должны продумать, как написать скрипт с нужными параметрами для нашей среды. . Изменение значений cgroups отдельно, вне конфигурации Docker, может ломать воспроизводимость развертывания. Если не применить изменения в процессе �аз­ вертывания, параметры вернутся к значениям по умолчанию при замене контеине­ ров. Некоторые планировщики обрабатывают эти изменения. Если в вашей рабочей среде есть планировщик, изучите документацию, чтобы узнать, как вносить воспроизводимые изменения. Давайте рассмотрим пример, в котором изменим параметры процессорного време­ ни в cgroups для только что запущенного контейнера. Нам потребуется длинный Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 279 идентификатор контейнера, который нужно найти в файловой системе /sys. Вот как это будет выглядеть: $ docker container run -d spkane/train-os \ stress -v --cpu 2 --io 1 --vm 2 --vm-bytes 128М --timeout 360s dcbb...8e86fldc0a9le7675d3c93895cbбaбd8337le25b7f0bd62803ed8e86 С помощью docker container run мы узнали длинный идентификатор контейнера: dcbb...Be86fldc0a9le7675d3c93895cbбaбd8337le25Ь7f0bd62803ed8e86. Понятно, почему обыч­ но Docker обрезает его. В этих примерах может потребоваться обрезать идентификатор, чтобы он помес­ тился н� стандартной странице, но не забывайте, что мы должны использовать длинныи вариант. Итак, у нас есть идентификатор, и мы можем найти cgroup контейнера в файловой системе /sys. Эта файловая система организована таким образом, что каждый тип параметров сгруппирован в модуль, который можно предоставлять в разных местах файловой системы. Следовательно, изучая параметры процессорного времени, мы не увидим, например, параметры блочного ввода-вывода. Исследуйте /sys само­ стоятельно, чтобы узнать, что еще в ней есть. Сейчас нас интересует контроллер процессорных ресурсов. Нам потребуются права root в системе, потому что мы будем менять параметры ядра. Не забывайте про использование nsenter, как мы обсуждали в главе 3. Выполним команду docker container run --rm -it -privileged --pid=host deЬian nsenter -t 1 -m -u -n -i sh, чтобы получить доступ к хосту Docker, даже если мы не можем под­ ключиться к серверу через SSH. $ ls /sys/fs/cgroup/docker/dcbb ...8е86 cgroup.controllers cpuset.cpus.partition cgroup.events cpuset.mems cgroup.freeze cpuset.mems.effective cgroup.max.depth hugetlb.2MB.current cgroup.max.descendants hugetlb.2MB.events cgroup.procs hugetlb.2MB.events.local cgroup.stat hugetlb.2MB.max cgroup.suЬtree_control hugetlb.2MB.rsvd.current cgroup.threads hugetlb.2МВ.rsvd.max cgroup.type io.Ьfq.weight cpu.max io.latency cpu.stat io.max cpu.weight io.stat cpu.weight.nice memory.current cpuset.cpus memory.events cpuset.cpus.effective memory.events.local memory.high memory.low memory.max memory.min memory.oom.group memory.stat memory.swap.current memory.swap.events memory.swap.high memory.swap.max pids.current pids.events pids.max rdma.current rdma.max Книги для программистов: https://clcks.ru/books-for-it 280 Глава 11 Точнь�й путь зависит от дистрибутива Linux на сервере Docker, а также от хеша � контеинера. � Под cgroups мы видим каталог docker, который содержит все контейнеры Linux, за­ пущенные на этом хосте. Мы не можем задать cgroups для того, чего нет на хосте, потому что они применяются только к выполняющимся процессам. Это важно помнить. Docker повторно применяет параметры cgroups при запуске и остановке контейнеров. Без этого механизма приходилось бы все делать вручную. Давайте проверим долю процессорного времени (CPU weight) для этого контейнера (https://docs.kernel.org/admin-guide/cgroup-v2.html#cpu-interface--files). В главе 5 мы рассматривали, как устанавливать эти значения с помощью аргумента команд­ ной строки --cpus в команде docker container run. Если ничего не задавать, мы уви­ дим значение по умолчанию: $ cat /sys/fs/cgroup/docker/dcbb...Be86/cpu.weight 100 Здесь 100 означает отсутствие ограничений на процессорное время. Давайте ука­ жем ядру, что нужно ограничить ресурсы наполовину: $ echo 50 > /sys/fs/cgroup/docker/dcbb...Be86/cpu.weight $ cat /sys/fs/cgroup/docker/dcbb...Be86/cpu.weight 50 В рабочей среде не следует с помощью этого метода менять настройки cgroups на лету, но мы рассматриваем этот пример, чтобы лучше понять механику. Для вы­ полняющегося контейнера можно задать команду docker container update (https://dockr.ly/2PPC4P1). Также рассмотрите параметр --cgroup-parent для docker container run (https://dockr.ly/2PTLaKK). Итак, мы изменили настройки контейнера на лету. Это эффективный метод, с по­ мощью которого можно задавать любые настройки cgroups для контейнера. Но не стоит забывать, что контейнеры не существуют долго, и при перезапуске вернутся значения по умолчанию: $ docker container stop dcbb...ВеВб dсЬЬ ...ВеВб $ cat /sys/fs/cgroup/docker/dcbb...Be86/cpu.weight cat: /sys/fs/.../cpu.weight: No such file or directory Мы видим, что после остановки контейнера этот путь больше не существует. Если мы запустим его снова, увидим значение 100: $ docker container start dcbb...ВеВб dcbb...ВеВб $ cat /sys/fs/cgroup/docker/dcbb...Be86/cpu.weight 100 Если бы мы хотели изменить эту настройку в рабочей среде через файловую систе­ му /sys, это нужно было бы сделать напрямую. Например, с помощью демона, который наблюдает за потоком docker system events и меняет настройки при запуске контейнера. Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 281 Мы можем создавать индивидуальные cgroups за пределами Docker, а затем при­ креплять к ним новые контейнеры с помощью аргумента --cgroup-parent для ко­ манды docker container create. С помощью этого механизма планировщики запус­ � кают несколько контейнеров в одной группе cgroup (например, поды Kubernetes). Пространства имен Внутри каждого контейнера есть файловая система, сетевые интерфейсы, диски и другие ресурсы, которые кажутся уникальными для контейнера, хотя ядро предос­ тавляется для всех процессов в системе. Основной сетевой интерфейс на фактиче­ ском компьютере, например, всего один, но в контейнере это будет выглядеть так, будто весь сетевой интерфейс находится в его распоряжении. Это полезная абст­ ракция, благодаря которой контейнер работает как полноценный компьютер. Такая возможность реализуется в ядре с помощью пространств имен Linux. Пространства имен предоставляют контейнеру отдельную, уникальную и принадлежащую только ему версию традиционно глобального ресурса. Пространства имен не так легко изучить в файловой системе, как cgroups, но большую часть информации можно найти в. иерархиях /prod*/ns/* и /prod*/task/ */ns/*. В более новых выпусках Linux также можно использовать команду lsns. Вместо единого пространства имен, однако, по умолчанию контейнеры используют пространство имен для каждого ресурса в ядре: rrюunt (файловая система), uтs, IPC, PID, network (сеть) и user (пользователь), а также частично реализованное простран­ ство имен time (время). По сути, контейнер представляет собой несколько про­ странств имен, координируемых Docker. Зачем они нужны? ♦ Пространства имен файловой системы (mount namespaces) - с их помощью Linux создает видимость того, что у контейнера есть собственная полноценная файловая система. Это более надежная альтернатива chroot jail, которая работа­ ет на самых глубоких уровнях ядра, так что в пространство имен входят даже системные вызовы rrюunt и unmount. Если для входа в контейнер вы используете docker container ехес или nsenter, о которых мы поговорим позже в этой главе, то увидите корень файловой системы /. Мы знаем, что это не настоящий корневой раздел, и это обозначение возможно благодаря пространству имен файловой системы. ♦ Пространства имен uтs (Unix Time Sharing) - предоставляют контейнеру от­ дельное имя хоста и доменное имя. Они также используются более старыми сис­ темами, вроде NIS, для определения принадлежности хоста к домену. Когда мы входим в контейнер и видим имя хоста, которое не совпадает с именем компью­ тера, - это работа пространства имен. Чтобы контейнер использовал пространство имен UTS хоста, можно указать пара­ метр --uts=host при запуске контейнера с помощью docker container run. Для дру­ гих пространств имен есть похожие команды. Книги для программистов: https://clcks.ru/books-for-it 282 1 Глава 11 ♦ Пространства имен IPC - изолируют системы очередей сообщений System V IPC и POSIX контейнера от аналогичных систем хоста. Некоторые механизмы IPC задействуют ресурсы файловой системы, например именованные каналы, и за них отвечают пространства имен файловой системы. Пространство имен IPC руководит общей памятью и семафорами, которые не являются ресурсами фай­ ловой системы, но не должны выходить за пределы контейнера. ♦ Пространства имен PID - мы уже видели, что можем просматривать все процес­ сы в контейнерах в выводе Linux ps на сервере Linux. Внутри контейнера у про­ цессов другие идентификаторы PID, потому что используется пространство имен PID. У процесса есть уникальный PID в каждом пространстве имен, куда он вхо­ дит. Если мы заглянем в /proc или выполним команду ps, то увидим только про­ цессы внутри пространства имен PID контейнера. ♦ Пространства имен сети - позволяют контейнеру работать с отдельными сете­ выми устройствами, портами и т. д. Если мы выполним команду docker container 1s и увидим привязанные порты, это будут порты из обоих пространств имен. Внутри контейнера nginx может быть привязан к порту 80, но сетевой интерфейс ограничен пространством имен. Благодаря этому пространству имен у контей­ нера есть сетевой стек, который кажется принадлежащим только ему. ♦ Пространства имен пользователей - обеспечивают изоляцию между идентифи­ каторами пользователей и групп в контейнере и на хосте Linux. Ранее мы изуча­ ли вывод ps снаружи и внутри контейнера и видели разные идентификаторы пользователей. Причина как раз в пространствах имен пользователей. Новый пользователь в контейнере не становится новым пользователей в главном про­ странстве имен хоста Linux и наоборот. Правда, здесь есть несколько особенно­ стей. Например, uro о (root) в пространстве имен пользователей означает не то же самое, что UID о на хаете, хотя работа с правами root в контейнере действи­ тельно повышает риски, и именно поэтому набирают популярность такие меха­ низмы, как контейнеры без root (rootless). Мы поговорим о брешах в системе безопасности чуть позже. ♦ Пространства имен cgroup - это пространство имен появилось в ядре Linux 4.6 в 2016 году, чтобы скрыть принадлежность cgroup, в которую входит процесс. Если какой-то процесс попытается проверить, в какую cgroup входит любой дру­ гой процесс, он получит путь относительно cgroup, заданный в момент создания, но не истинное расположение и идентификацию cgroup. ♦ Пространства имен времени - изначально для времени не существовало про­ странства имен, поскольку этого ресурса нет в ядре Linux, и полноценно ограни­ чить его пространствами имен было бы очень сложно. Однако в ядре Linux 5.6 в 2020 году была добавлена поддержка пространства имен времени (https:// man7.org/linux/man-pages/man7/time_namespaces.7.html), которое позволяет контейнерам использовать собственное системное время. На момент написания в Docker напрямую не подцерживается отдельное время для контейнеров, но при необходимости можно использовать дополнительные меха­ низмы. Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 283 Сочетая все пространства имен, Linux может предоставить визуальную и в некото­ рых случаях функциональную изоляцию, благодаря которой контейнер похож на виртуальную машину, даже если выполняется на том же ядре. Давайте подробнее рассмотрим некоторые пространства имен. Сейчас сообщество активно работает над повышением безопасности контейнеров. Рассматриваются способы улучшить поддержку гооtlеss-контейнеров (https:// rootlesscontaine.rs/), чтобы обычные пользователи могли создавать, запускать и � администрировать контейнеры локально без специальных привилегий. В Docker для этого можно использовать режим rootless (https://docs.docker.com/engine/ security/rootless). Новые среды выполнения контейнеров, вроде Google gVisor (https://github.com/google/gvisor), также пробуют разные способы работать в бо­ лее защищенных изолированных средах, при этом используя преимущества кон­ тейнеризированных рабочих процессов. Изучение пространств имен Пространство имен uтs проще всего продемонстрировать, так что давайте с помо­ щью команды docker container ехес войдем в контейнер. На сервере Docker введите следующую команду: $ hostname docker-desktop Не забывайте, что можно выполнить команду docker container run --rm -it -­ privileged --pid=host deЬian nsenter -t 1 -m -u -n -i sh, которую мы рассматри­ вали в главе 3, чтобы получить доступ к хосту Docker, даже если мы не можем подключиться к серверу по SSH. В локальной системе выполним следующую команду: $ docker container run -ti --rm uЬuntu \ bash -с 'echo "Container hostname: $(hostname)"' Container hostname: 4сdЬббd4495Ь Команда docker container run начинает интерактивный сеанс ( -ti), а затем выполняет команду hostname через /Ьin/bash в контейнере. Поскольку команда hostname выполня­ ется в пространстве имен контейнера, мы получаем сокращенный идентификатор контейнера, который используется как имя хоста по умолчанию. Это очень простой пример, но он показывает, что мы находимся не в пространстве имен хоста. Еще один простой для понимания пример - пространства имен PID. Давайте созда­ дим новый контейнер: $ docker container run -d --rm --name pstest spkane/train-os sleep 240 6e005f895e259ed03c4386b5aeb03e0a50368ccl73078007bбdlbeaa8cd7dded $ docker container ехес -ti pstest ps -ef UID root root PID 1 13 PPID С О STIME О 15:33 О 15:33 о ТТУ ? pts/0 TIME CMD 00:00:00 00:00:00 sleep 240 ps -ef Docker может показать нам идентификаторы процесса с точки зрения хоста: $ docker container top pstest UID root PID 31396 PPID 31370 С О STIME 15:33 ТТУ ? TIME 00:00:00 CMD sleep 240 Книги для программистов: https://clcks.ru/books-for-it 284 Глава 11 Мы видим, что изнутри контейнера исходная команда от Docker выглядит как sleep 240 и ей назначен PID 1 в контейнере. Возможно, вы помните, что этот PID обычно используется процессом init в системах Unix. В этом случае команда sleep 240, с которой мы запустили контейнер, является первым процессом, поэтому получает PID 1. Но в главном пространстве имен сервера Docker мы видим, что процессу на­ значен PID 31396, и это дочерний процесс для процесса PID 31370. Если любопытно, выполните команду, чтобы узнать, у какого процесса PID 31370: $ docker container run --pid=host uЬuntu ps -р 31370 PID 31370 ТТУ ? TIME 00:00:00 CMD containerd-shim Теперь можно удалить контейнер, запущенный в последнем примере: $ docker container rm -f pstest Остальные пространства имен работают примерно так же, и вы уже получили неко­ торое представление об этом. Стоит отметить, что при первом использовании nsenter в главе 3 мы передали довольно сложные аргументы, выполняя команду для входа в контейнер с сервера Docker. Давайте рассмотрим часть, связанную с nsenter в команде docker container run --rm -it --privileged --pid=host deЬian nsenter -t 1 -m -u -n -i sh. Оказывается, что nsenter -t 1 -m -u -n -i sh- то же самое, что nsenter --target 1 --mount --uts --net -ipc sh. Эта команда указывает, что нужно ориентироваться на PID 1 и открыть командную оболочку в тех же пространствах имен mount, uts, net и ipc, в которых находится этот процесс. Вероятно, теперь вы лучше понимаете, как работают пространства имен. Для даль­ нейшего изучения попробуйте с помощью nsenter входить в разные пространства имен в одноразовом контейнере, чтобы лучше понять происходящее. Именно благодаря пространствам имен контейнер выглядит как контейнер. Если добавить cgroups, получим надежную изоляцию между процессами на одном яд­ ре ОС.. Безопасность Мы много говорили о том, как Docker обеспечивает изоляцию для приложений, позволяет ограничивать потребление ресурсов и использует пространства имен, чтобы обеспечить уникальность контейнера. Мы также упомянули такие техноло­ гии, как Secure Computing Mode, SELinux и AppArmor. Одно из преимуществ кон­ тейнеров - возможность заменить виртуальные машины в некоторых вариантах применения. Давайте посмотрим, что по умолчанию будет изолировано, а что­ нет. Сейчас вы уже понимаете, что контейнер не так хорошо изолирован, как виртуаль­ ная машина. На протяжении всей книги мы говорили о том, что контейнеры - это просто процессы, выполняющиеся на сервере Linux. Пространства имен обеспечиКниги для программистов: https://clcks.ru/books-for-it Расширенные концепции 285 вают изоляцию, но контейнеры не так хорошо защищены, как нам хотелось бы, особенно по сравнению с легковесными виртуальными машинами. У контейнеров высокая производительность, и они занимают мало места, потому что используют общее ядро сервера Linux. Этот факт и является главной проблемой безопасности контейнеров Linux. Дело в том, что в ядре не все ограничено про­ странствами имен. Мы рассмотрели все пространства имен, которые обособляют работу контейнера, но в ядре еще остаются аспекты, которые совершенно не изоли­ рованы, а пространства имен ограничивают контейнер, только если у него нет пол­ номочий запросить у ядра доступ к другому пространству имен. Контейнеризированные предложения лучше защищены, чем неконтейнеризирован­ ные, потому что cgroups и стандартные пространства имен предоставляют изоляцию от базовых ресурсов хоста, но не надейтесь, что контейнеры смогут заменить адек­ ватные меры безопасности. В рабочей среде запускайте контейнеры с теми права­ ми, которые нужны приложению. Если приложение должно выполняться на серве­ ре от имени непривилегированного пользователя, таким же образом его следует запускать и в контейнере. В Docker мы легко можем запускать процессы контейне­ ров от имени непривилегированных пользователей, и почти всегда следует именно так и поступать. Аргумент --userns-remap для команды dockerd и rootless-peжим позволяют запус­ кать все контейнеры в контексте непривилегированного пользователя и группы в хост-системе. Эти подходы помогают защитить хост от множества типов атак. Подробнее о userns-remap см. в официальной документации по функции (https:// dockr.ly/2BYfWze) и демону Docker (https://dockr.ly/2LE9gG2). Режим без root подробно рассматривается в разделе "Режим rootless" этой главы. Давайте поговорим о типичных рисках и мерах контроля безопасности. UJDO Главный риск, связанный с контейнерами, заключается в том, что если мы не ис­ пользуем режим rootless или функционал userns-remap в демоне Docker, пользова­ тель root в контейнере будет являться пользователем root в системе. Существуют дополнительные ограничения для root в контейнере, и пространства имен эффек­ тивно изолируют root в контейнере от самых опасных частей файловых систем /proc и /sys. Но если мы работаем от имени uro о, у нас есть rооt-доступ, и если мы каким-то образом получим доступ к защищенным ресурсам в файловой системе или за пределами пространства имен, ядро будет считать нас пользователем root, а значит, предоставит доступ к ресурсу. Если мы не настроили другие параметры, Docker запускает все сервисы в контейнере как root, т. е. мы должны управлять привилегиями в приложениях так же, как в любой стандартной системе Linux. Да­ вайте рассмотрим некоторые ограничения для доступа root и обсудим очевидные бреши. Мы не сможем подробно рассмотреть все вопросы безопасности в контей­ нере, но вы получите некоторое представление о типах рисков. Книги для программистов: https://clcks.ru/books-for-it 286 Глава 11 Для начала давайте запустим контейнер и перейдем в bash, используя публичный образ UЬuntu, как в следующем коде. Установив нужные инструменты, мы обсу­ дим, какой доступ нам нужен: $ docker container run --rm -ti uЬuntu /bin/bash root@808a2Ь8426dl:/# apt-get update root@808a2Ь8426dl:/# apt-get install -у kmod root@808a2Ь8426dl:/# lsmod Module xfrm user xfrm_algo shiftfs grpcfuse vmw_vsock_virtio_transport vmw_vsock_virtio_transport_common vsock Size 36864 16384 28672 16384 16384 28672 36864 Used Ьу 1 1 xfrm user о о 2 1 vmw_vsock_virtio_transport 9 vmw_vsock_virtio_transport common... В Docker Desktop мы видим в списке только несколько модулей, но в обычной сис­ теме Linux список может быть очень длинным. С помощью команды lsmod мы по­ просили ядро показать загруженные модули. Неудивительно, что мы можем полу­ чить список из контейнера, потому что у пользователя с обычными правами всегда есть такая возможность. Если мы выполним этот код на сервере Docker, получим такой же результат, и это еще раз подтверждает, что контейнер обращается к тому же ядру Linux, которое запущено на сервере. Если мы видим модули ядра, что будет, когда мы попробуем удалить модуль floppy? root@808a2Ь8426dl:/# rmmod shiftfs rmmod: ERROR: ../libkmod/libkmod-module.c:799 kmod_module_remove_module() rmmod: ERROR: could not remove module shiftfs: Operation not permitted root@808a2Ь8426dl:/# exit Такое же сообщение об ошибке было бы выдано, если бы мы от имени непривиле­ гированного пользователя попросили ядро удалить модуль. Как видите, ядро стара­ ется запретить нам выполнять опасные операции. Поскольку мы находимся в огра­ ниченном пространстве имен, ядро не дает нам доступ к пространству имен верхне­ го уровня. По сути, мы просто надеемся, что в ядре нет ошибок, из-за которых мы смогли бы повысить привилегии в контейнере. Если бы у нас это получилось, мы получили бы права root и смогли бы вносить изменения, разрешенные ядром. Давайте рассмотрим простой пример того, что может пойти не так. Запустим обо­ лочку bash в контейнере, где /etc сервера Docker примонтирован к пространству имен контейнера. Не забывайте, что любой пользователь, который может запустить контейнер на сервере Docker, способен выполнить те же действия, потому что мы не можем настроить это запрет в Docker. Для этого требуются сторонние инстру­ менты вроде SELinux. Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 287 В этом примере предполагается, что вы используете Docker CLI в системе Linux, где есть файл /etc/shadow. Этого файла не будет на хостах Windows или macOS с Docker Desktop. $ docker container run --rm -it -v /etc:/host_etc uЬuntu /bin/bash rооt@еб74еЬ96ЬЬ74:/# more /host_etc/shadow root: 1 :16230:0:99999:7::: daemon:*:16230:0:99999:7::: bin:*:16230:0:99999:7::: sys:*:16230:0:99999:7::: irc:*:16230:0:99999:7::: nobody:*:16230:0:99999:7::: libuuid: !:16230:0:99999:7::: syslog:*:16230:0:99999:7::: messagebus:*:16230:0:99999:7::: kmatthias:$1$aTAYQТ.j$3xamPL3dHGow4ITBdRhl:16230:0:99999:7::: sshd:*:16230:0:99999:7::: lxc-dnsmasq: !:16458:0:99999:7::: rооt@еб74еЬ96ЬЬ74:/# exit Здесь мы указали параметр -v, чтобы Docker подключил путь хоста к контейнеру. Мы выбрали /etc, и это небезопасно, зато мы видим, что получили права root в кон­ тейнере, и у root есть разрешения на доступ к файлам по этому пути. Итак, у нас есть доступ к файлу /etc/shadow на сервере Linux, где содержатся зашифрованные пароли всех пользователей. Мы можем выполнять много друтих операций, и по умолчанию наши возможности только частично ограничены. Не следует запускать процессы контейнера с UID о, потому что любой эксплойт, из-за которого процесс выйдет за пределы своих пространств имен, откроет при­ вилегирован�ый доступ к хосту. У стандартных контейнеров должен быть неприви­ легированныи UID. Самый простой способ решить потенциальные проблемы с использованием uro о в контейнерах - указать, что Docker должен всегда назначать для контейнеров другой uro. Для этого можно передать аргумент -u команды docker container run. В следующем примере мы выполняем команду whoami, чтобы показать, что у нас есть права root по умолчанию, и мы можем читать файл /etc/shadow внутри этого контейнера: $ docker contai�er run --rm spkane/train-os:latest whoami root $ docker container run --rm spkane/train-os:latest cat /etc/shadow root: !locked::0:99999:7::: bin:*:18656:0:99999:7::: daemon:*:18656:0:99999:7::: adm:*:18656:0:99999:7::: lp:*:18656:0:99999:7::: Книги для программистов: https://clcks.ru/books-for-it 288 Глава 11 В этом примере при добавлении -и 500 мы становимся новым непривилегирован­ ным пользователем и больше не можем читать файл /etc/shadow: $ docker container run --rm -и 500 spkane/train-os:latest whoarni user500 $ docker container run --rm -и 500 spkane/train-os:latest cat /etc/shadow cat: /etc/shadow: Permission denied Кроме того, рекомендуется добавить директиву USER в Dockerfile, чтобы контейне­ ры, созданные на основе этих файлов, по умолчанию запускались как непривилеги­ рованные пользователи: FRCМ fedora:34 RUN useradd -u 500 -m myuser USER 500:500 СМD [ "whoarni"] Если мы создадим этот Dockerfile, а затем соберем и запустим его, мы увидим, что whoarni возвращает myuser вместо root: $ docker image build -t user-test . [+] Building 0.5s (6/6) FINISHED => [internal] load build definition from Dockerfile => => transferring dockerfile: 36В => [internal] load .dockerignore => => transferring context: 2В => [internal] load metadata for docker.io/library/fedora:34 => (1/2] FROM docker.io/library/fedora:34@sha256:321d... 2697 => CACHED (2/2] RUN useradd -u 500 -m myuser => exporting to image => => exporting layers => => writing image sha256:4727... 30d5 => => naming to docker.io/library/user-test $ docker container run --rm user-test myuser 0.0s 0.0s 0.0s 0.0s 0.4s 0.0s 0.0s 0.0s 0.0s 0.0s 0.0s Режим rootless Привилегированные процессы с правами root требуются контейнерам для запуска и управления, но это серьезная угроза для безопасности. Если мы используем функ­ цию --userns-remap демона Docker, сам демон выполняется как привилегированный процесс, даже если у запускаемых им контейнеров не будет привилегий. В режиме rootless (https://docs.docker.com/engine/security/rootless) мы можем за­ пускать демон и все контейнеры без прав root, и это позволяет заметно повысить уровень безопасности системы. Режим rootless работает только в системе Linux, и Docker рекомендует UЬuntu, по­ этому давайте рассмотрим пример с новой системой Ubuntu 22.04. Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции � 289 В этом примере предполагается, что мы входим в систему как обычный непривиле­ гированный пользователь и у нас уже установлен Docker Engine (https:1/docs. docker.com/engine/install/ubuntu). Сначала нам нужно убедиться, что dЬus-user-session и uidrnap установлены. Если dЬus­ user-session не установлен, нужно выйти из системы, выполнить следующую команду и снова войти: $ sudo apt-get install -у dЬus-user-session uidrnap dЬus-user-session is already the newest version (l.12.20-2uЬuntu4). Setting up uidrnap (l:4.8.l-2uЬuntu2) ... Это не строгое требование, но если в системе запущен демон Docker, следует его откmочить и перезагрузиться: $ sudo systemctl disaЫe --now docker.service docker.socket Synchronizing state of docker.service with SysV service script with /liЬ/systemd/systemd-sysv-install. Executing: /liЬ/systemd/systemd-sysv-install disaЬle docker Removed /etc/systemd/system/sockets.target.wants/docker.socket. Removed /etc/systemd/system/multi-user.target.wants/docker.service. $ sudo shutdown -r now Когда система запустится, снова подключитесь через SSH к серверу как обычный пользователь и убедитесь, что больше не видите /var/run/docker.sock: $ 1s /var/run/docker.sock 1s: cannot access '/var/run/docker.sock': No such file or directory Следующий шаг - запустить скрипт установки режима rootless, который установ­ щик Docker загружает в /usr/Ьin: $ dockerd-rootless-setuptool.sh install [INFO] Creating /home/me/.config/systemd/user/docker.service [INFO] starting systemd service docker.service + systemctl --user start docker.service + sleep 3 + systemctl --user --no-pager --full status docker.service • docker.service - Docker Application Container Engine (Rootless) Loaded: loaded (/home/me/.config/systemd/user/docker.service; ...) + DOCKER HOST=unix:///run/user/1000/docker.sock /usr/bin/docker version Client: Docker Engine - Community Version: 20.10.18 Server: Docker Engine - Community Engine: Version: 20.10.18 Книги для программистов: https://clcks.ru/books-for-it 290 Глава 11 + systemctl --user еnаЫе docker.service Created symlink /home/me/.config/systemd/user/default.target.wants/ docker.service � /home/me/.config/systemd/user/docker.service. [INFO] Installed docker.service successfully. [INFO] То control docker.service, run: 'systemctl --user (startlstoplrestart) docker.service' [INFO] То run docker.service оп system startup, run: 'sudo loginctl enaЫe-linger me' [INFO] Creating CLI context "rootless" Successfully created context "rootless" [INFO] Make sure the following environment variaЬles are set (or add them to ~/.bashrc): export PATH=/usr/bin:$PATH export DOCKER_HOST=unix:///run/user/1000/docker.sock uro в переменной DOCKER_нosт эдесь должен совпадать с UID пользователя, эапус­ � тившего скрипт. В данном случае это UID 1000. � Этот скрипт выполнил несколько проверок, чтобы убедиться, что наша система готова, а затем установил и запустил файл сервиса systemd для пользователя в $ { НОМЕ) /.config/systemd/user/docker.service. При желании каждый пользователь в системе может сделать то же самое. Демон Docker пользователя можно контролировать так же, как большинство серви­ сов systemd. Несколько простых примеров: $ systemctl --user restart docker.service $ systemctl --user stop docker.service $ systemctl --user start docker.service Чтобы демон Docker пользователя выполнялся, когда пользователь не в системе, нужно с помощью sudo включить параметр systemd linger, а затем разрешить демону Docker выполняться при запуске системы: $ sudo loginctl enaЬle-linger $(whoami) $ systemctl --user еnаЫе docker Сейчас можно было бы добавить эти переменные среды в наши файлы запуска командной оболочки, но нужно убедиться, что эти переменные заданы в текущем терминале: $ export PATH=/usr/bin:$PATH $ export DOCKER_HOST=unix:///run/user/1000/docker.sock Мы можем запустить стандартный контейнер: $ docker container run --rm hello-world Hello from Docker! This message shows that your installation appears to Ье working correctly. Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 291 For more examples and ideas, visit: https://docs.docker.com/get-started/ Вы заметите, что некоторые привилегированные контейнеры, которые мы рассмат­ ривали в предыдущих разделах, не будут работать в этой среде: $ docker container run --rm -it --privileged --pid=host debian nsenter \ -t 1 -m -u -n -i sh docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unaЬle to start container process: error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/7), flags: Охе: operation not permitted: unknown. Дело в том, что в режиме rootless у контейнера не может быть больше привилегий, чем у пользователя, который его запускает, хотя и кажется, что у контейнера есть все права root: $ docker container run --rm spkane/train-os:latest whoami root Давайте подробнее рассмотрим этот вопрос, запустив небольшой контейнер с ко­ мандой sleep 480s: $ docker container run -d --rm --name sleep spkane/train-os:latest sleep 480s lf8ccec0a834537da20c6e07423f9217efe34c0eac94f0b0el78fb9761234lef Если мы посмотрим на процесс внутри контейнера, станет ясно: нам только кажет­ ся, что он выполняется с правами root: $ docker container ехес sleep ps auxwww USER PID %CPU %МЕМ VSZ root root 1 0.1 О.О 7 о.о о.о 2400 7780 RSS ТТУ 824? 3316? STAT START Ss Rs 17:51 17:51 ТIМЕ СОММАND 0:00 sleep 480s 0:00 ps auxwww Если мы посмотрим на процессы в системе Linux, то увидим, что команда sleep вы­ полняется локальным пользователем с именем те, а не пользователем root: $ ps auxwww I grep sleep 3509 О.О О.О 2400 824? Ss 10:51 0:00 sleep 480s me 3569 О.О О.О 17732 2360 pts/0 S+ 10:51 0:00 grep --color=auto sleep те root в rооtlеss-контейнере соответствует самому пользователю. Процессы в контей­ нерах не могут использовать разрешения, которых нет у пользователя, запустивше­ го демон, и поэтому мы можем разрешать пользователям в многопользовательской системе запускать контейнеры, не рискуя дать им слишком много привилегий. � - На сайте Docker есть инструкции по удалению режима rootless {https://docs.docker.com/engine/securitylrootless/#uninstall). Книги для программистов: https://clcks.ru/books-for-it 292 Глава 11 Привилегированные контейнеры Иногда у контейнера должны быть специальные привилегии (capaЬilities) в ядре (https://man7.org/linux/man-pages/man7/capabllities.7.html), которые обычно мы не предоставляем контейнерам. Например, для подкточения USВ-накопителя, изменения сетевых конфигураций или для создания нового устройства Unix. В следующем коде мы пьrгаемся изменить МАС-адрес контейнера: $ docker container run --пn -ti spkane/train-os /bin/bash [root@280d4dclб407 /]1 ip link ls 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNКNOWN mode link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN mode DEFAULT ... link/ipip О.О.О.О brd О.О.О.О 3: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop �tate DOWN mode DEFAULT ... link/tunnel6 :: brd :: permaddr 12b5:6flb:a7e9:: 22: eth0@if23: <BROADCAST,МULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue link/ether 02:42:ас:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid О [root@fc4589fЬ8778 /Jf ip link set eth0 address 02:Оа:03:ОЬ:04:Ос RТNEТLINK answers: Operation not permitted [root@280d4dclб407 /]1 exit Как видите, не работает. Дело в том, что ядро Linux не дает непривилегированному контейнеру выполнить операцию. Именно этого мы обычно и добиваемся, но если в данном случае мы хотим, чтобы контейнер сделал то, что задумано, самый про­ стой способ повысить его привилегии - запустить его с аргументом --privileged= true. Мы не рекомендуем выполнять команду ip link set eth0 address в следующем примере, поскольку она изменит МАС-адрес сетевого интерфейса контейнера. Мы показываем это� пример, чтобы объяснить механизм. Выполняйте команду исклю­ чительно на свои страх и риск. $ docker container run -ti --пn --privileged=true spkane/train-os /bin/bash [root@BSЗeOefSddбЗ /Jf ip link ls 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNКNOWN mode link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN mode DEFAULT ... link/ipip О.О.О.О brd О.О.О.О 3: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN mode DEFAULT ... link/tunnel6 :: brd :: permaddr 12b5:6flb:a7e9:: 22: eth0@if23: <BROADCAST,МULTICAST,UP,LOWER UP> mtu 1500 qdisc noqueue link/ether 02:42:ас:11:00:02 brd ff:ff:ff�ff:ff:ff link-netnsid О [root@BSЗeOefSddбЗ /]1 ip link set eth0 address 02:Оа:03:ОЬ:04:Ос [root@BSЗeOefSddбЗ /JI ip link show eth0 26: eth0@if27: <BROADCAST,МULTICAST,UP,LOWER UP> mtu 1500 qdisc noqueue link/ether 02:Оа:03:ОЬ:04:Ос brd ff:ff:ff�ff:ff:ff link-netnsid О [root@BSЗeOefSddбЗ /Jf exit В этом выводе мы уже не получаем ошибку, а запись link/ether для eth0 изменилась. Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 1 293 Проблема с аргументом --privileged=true заключается в том, что мы даем контей­ неру очень широкие привилегии, и в большинстве случаев нам достаточно будет одной или двух привилегий в ядре. Если мы изучим наш привилегированный контейнер, то увидим, что предоставлен­ ные ему привилегии никак не связаны с изменением МАС-адреса. Мы можем делать даже то, что приведет к проблемам в Docker и на хаете. В следующем коде мы примонтируем раздел диска с базовой хает-системы, выведем все базовые кон­ тейнеры Linux с Docker в системе и изучим некоторые важные файлы: $ docker container run -ti --rm --privileged=true spkane/train-os /bin/bash [root@664a896983d7 /]1 пюunt /dev/vdal /mnt && \ ls -F /mnt/docker/containers 1 \ head -n 10 047df420fбdlf227a26667f83e477f608298c25b0cdad2e149a781587aae5ell/ 0888b9f97Ыecc4261f637404e0adcc8ef0c8df291Ь87c9160426e42dc9b5dea/ 174ea3ec35cd3a576bedбf475b477Ьla474d897ece15acfc46e61685aЬb3101d/ leddad26ee64c4b29eЫ64b71d56d680739922b3538dc8aaбc6966fce61125b0/ 22b2aa38a687f423522ddl74fdd85d578eb21c9c8ecl54a0f9b841ld08fбfd4Ь/ 23879e3b9cdбa42ale09dc8e96912ad66e80ec09949c744dl177a911322e7462/ 266fe7da627d2e8ec5429140487e984c8d5d36a26bb3cc36a88295e38216e8a7/ 2cb6223e115c12ae729d968dЬOd2f29a934b4724f0c9536e377e0dЬd566fll02/ 306f00e86122b69eeba9323415532a12f88360alббlf445fc7d64c07249eb0ce/ 333b85236409f873d07cd47f62ecla987df59f688a20ldf744f40f98b7e4ef2c/ [root@664a896983d7 /]i ls -F /mnt/docker/containers/047d... Sell/ 047df420fбdlf227a26667f83e477f608298c25b0cdad2e149a781587aae5ell-json.log checkpoints/ config.v2. j son hostconfig.j son hostname hosts mounts/ resolv.conf resolv.conf.hash [root@664a896983d7 /]1 cat /mnt/docker/containers/047d...5ell/047...ell-json.log {"log":"047df420fбdl\r\n","stream":"stdout","time":"2022-09-14T15:18:29.... "} [root@664a896983d7 /]i exit � Не меняйте и не удаляйте эти файлы - это может привести к непредсказуемым .,.. последствиям для контейнеров или базовой системы Linux. � Как видите, мы можем выполнять команды и получать доступ к данным, которые должны быть нам недоступны. Для изменения МАС-адреса нам достаточно привилегии CAP_NEТ_ADМIN. Вместо того чтобы предоставлять контейнеру все привилегии, мы можем дать только одну, запустив контейнер Linux с аргументом --cap-add: Книги для программистов: https://clcks.ru/books-for-it 294 Глава 11 . $ docker container run -ti --пn --cap-add=NET_ADMIN spkane/train-os /bin/bash [root@087c02a3cбe7 /]И ip link show eth0 36: eth0@if37: <BROADCAST,МULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue link/ether 02:42:ас:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid О [root@087c02a3cбe7 /]И ip link set eth0 address 02:Оа:03:ОЬ:04:Ос [root@087c02a3cбe7 /]И ip link show eth0 36: eth0@if37: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue link/ether 02:Оа:03:ОЬ:04:Ос brd ff:ff:ff:ff:ff:ff link-netnsid О [root@087c02a3cбe7 /]И exit Обратите внимание: теперь мы можем изменить МАС-адрес, но не можем выпол­ нять команду mount в контейнере: $ docker container run -ti --пn --cap-add=NET_ADMIN spkane/train-os /bin/bash [root@b84a06ddaa0d /]И mount /dev/vdal /mnt mount: /mnt: permission denied. [root@b84a06ddaa0d /]И exit Мы также можем отнять у контейнера определенные привилегии. Допустим, по соображениям безопасности мы хотим отключить tcpduпp во всех контейнерах, но при тестировании контейнеров мы обнаруживаем, что команда tcpduпp установлена и ее легко можно выполнять: $ docker container run -ti --пn spkane/train-os:latest tcpduпp -i eth0 dropped privs to tcpduпp tcpduпp: verbose output suppressed, use -v[v] ... for full protocol decode listening on eth0, link-type ENl0MB (Ethernet), snapshot length 262144 bytes 15:40:49.847446 IP6 fe80::23:6cff:fed6:424f > ff02::16: НВН ICMP6, ... 15:40:49.913977 ARP, Request who-has _gateway tel1 5614703ffee2, length 28 15:40:49.914048 ARP, Request who-has _gateway tel1 5614703ffee2, length 28 15:40:49.914051 ARP, Reply _gateway is-at 02:49:9b:d9:49:4e (oui Unknown), 15:40:49.914053 IP 5642703bbff2.45432 > 192.168.75.8.domain: 44649+ PTR? ... Мы можем удалить tcpduпp из образов, но не можем помешать кому-то установить утилиту обратно. Самый эффективный способ решить эту проблему - определить, какая привилегия нужна для выполнения tcpduпp, и удалить эту привилегию для контейнера. Для этого можно добавить аргумент --cap-drop=NET_RAW в команду docker container run: $ docker container run -ti --пn --cap-drop=NET_RAW spkane/train-os:latest \ tcpduпp -i eth0 tcpduпp: eth0: You don't have permission to capture on that device (socket: Operation not permitted) Задавая аргументы --cap-add и --cap-drop для docker container run, мы можем управ­ лять отдельными привилегиями в ядре Linux (https://man7.orgЛinux/man-pages/ man7/capabllities.7.html). Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 295 Имейте в виду, что отдельные привилегии Linux не только разрешают определен­ ные системные вызовы, но и могут предоставлять другие возможности. Например, просмотр всех устройств в системе или возможность менять системное время. Secure Computing Mode В версии ядра Linux 2.6.12, выпущенной в 2005 году, появилась новая функция безопасности - Secure Computing Mode, или sессощ:,. Этот механизм позволяет односторонне перевести процесс в состояние, в котором ему будет разрешено делать только системные вызовы exit(), sigreturn() и read() или write() по дескрип­ торам уже открытых файлов. Расширение sессощ:,, называемое sессощ:,-Ьрf, использует правила Berkeley Packet Filter (BPF) (https://www.kernel.org/doc/Documentation/networking/filter.txt) для Linux, позволяя нам создать политику с явным списком системных вызовов, кото­ рые процесс может выполнять в режиме Secure Computing Mode. С помощью sессощ:,-Ьрf в Docker мы можем создавать профили с детализированным контролем системных вызовов для процессов в контейнерах. По умолчанию все контейнеры используют Secure Computing Mode, и к ним привя­ заны стандартные профили. Вы можете прочитать документацию о Secure Computing Mode (https://docs.docker.com/engine/security/seccomp) и системных вызовах в профиле по умолчанию. Также изучите JSON с политикой по умолчанию (https://github.com/moby/moby/ЫoЬ/master/profiles/seccomp/default.json). Давайте рассмотрим пример. Используем программу strace для трассировки сис­ темных вызовов, которые делает процесс при попытке размонтировать файловую систему командой umount. � � Эти примеры нужны для демонстрации - нам не следует размонтировать файло­ вые системы, если мы не уверены, к чему это приведет. $ docker container run -ti --rm spkane/train-os:latest umount /sys/fs/cgroup umount: /sys/fs/cgroup: must Ье superuser to Шll!\ount. $ docker container run -ti --rm spkane/train-os:latest \ strace umount /sys/fs/cgroup execve("/usr/bin/umount", [ "umount", "/sys/fs/cgroup"], Ox7fff902ddЬe8 = -1 ЕРЕRМ (Operation not permitted) umount2("/sys/fs/cgroup", О) =8 write(2, "umount: ", Bumount: write(2, "/sys/fs/cgroup: must Ье superuse" ..., 45/sys/fs/cgroup: must Ье superuser to unmount.) = 45 write(2, "\n", 1 dup(l) close(З) dup(2) = 1 = 3 = о = 3 Книги для программистов: https://clcks.ru/books-for-it 296 Глава 11 close(3) exit_group(32) +++ exited with 32 +++ = О ? Мы уже знаем, что команды монтирования не работают в контейнере со стандарт­ ными разрешениями, и из-за strace мы получим ошибку "операция не разрешена", когда команда umount попытается использовать системный вызов umount2. Эrо можно исправить, дав контейнеру привилегию SYS_домш: $ docker container run -ti --rm --cap-add=SYS_ADMIN spkane/train-os:latest \ strace umount /sys/fs/cgroup execve("/usr/Ьin/umount", ["umount", "/sys/fs/cgroup"], 0x7ffd3e4452b8 umount2("/sys/fs/cgroup", О) dup(l) close(3) dup(2) close(3) exit_group(0) +++ exited with О+++ = о = 3 = о = 3 = о ? Не забывайте, что параметр --cap-add=SYS_ADMIN позволяет выполнять разные дейст­ вия, в том числе монтировать системные разделы следующим образом: $ docker container run -ti --rm --cap-add=SYS_ADMIN spkane/train-os:latest \ mount /dev/vdal /mnt Эгу проблему можно решить более эффективно с помощью профиля sессопр. В от­ личие от sессопр, аргумент --cap-add разрешает много системных вызовов и дает лишние привилегии. Привилегия САР_SYS_ADMIN дает особенно много нежелательных разрешений. Профиль sессопр позволяет точно указать, какие системные вызовы следует включить или отключить. В профиле sессопр содержится примерно следующее: "defaultAction": "SCMP_ACT_ERRNO", 1, "defaultErrnoRet": "archМap": [ "architecture": "SCMP_ARCH_Х86_64", "suЬArchitectures": "SCMP_ARCH_X86", "SCMP ARCH ХЗ2" ), ], "syscalls": "names": "accept", Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 1 297 "accept4", 11 access 11 , "adjtimex", ], "waitid", "waitpid", "write", "writev" "action": "SCMP АСТ ALLOW" }, "names": "bpf", "clone", ], "umount2", "unshare" "action": "SCMP_АСТ _ALLOW", "includes": { "caps": [ "САР SYS ADMIN" }, В файле JSON мы видим список поддерживаемых архитектур, набор правил по умолчанию и группу системных вызовов, которые входят в каждую привилегию. В нашем случае задано действие по умолчанию sсмР_дет_ERRNO, которое генерирует ошибку при попытке сделать не указанный в списке вызов. Если вы подробно изучите профиль по умолчанию, то заметите, что привилегия CAP_SYS_ADMIN управляет доступом к 37 системным вызовам, и это очень много, го­ раздо больше, чем стандартные 4-6 системных вызовов, входящих в большинство остальных привилегий. В нашем случае нам требуется особый функционал, предоставляемый CAP_SYS_ADMIN, но мы хотим запретить остальные системные вызовы. Чтобы добавить только один нужный системный вызов, мы можем создать политику Secure Computing Mode на основе предоставляемой в Docker политики по умолчанию. Для начала извлекаем политику по умолчанию и делаем ее копию: $ wget https://raw.githuЬusercontent.com/moby/moby/master/profiles/seccomp/default.json $ ер default.json umount2.json Книги для программистов: https://clcks.ru/books-for-it 298 Глава 11 URL продолжается на следующей строке, чтобы вписаться в ширину. Возможно, потребуется пересобрать его и убрать обратные косые черты, чтобы команда работала в вашей среде. Редактируем файл, удаляя системные вызовы, которые по умолчанию предоставля­ ет САР_SYS_ADMIN. В этом случае мы оставляем всего два системных вызова, чтобы команды strace и umount работали корректно. Нужный раздел файла заканчивается следующим блоком JSON: "inclucles": { "caps": [ "САР SYS ADМIN" Следующий diff показывает точные изменения, которые нужно внести в нашем случае: $ diff -u -US default.json umount2.json diff -u -U5 default.json umount2.json 2022-09-25 13:23:57.000000000 -0700 -- default.json +++ шnount2.json 2022-09-25 13:38:31.000000000 -0700 @@ -575,34 +575,12 @@ }, "names": "bpf 11 , 1 "clone 1, "clone3", == 11 � fanotify_init :======== fsconfig 11 :========:::::;;:::::: ::::,. fsmount", 11 fsopen", := =========: fspick", p_dcookie", "looku :========::::::= :::: .. mount", "mount_setattr", ;::::::::::::;====== ::::;,,move_mount", "name_to_handle_at", ::=========== =;; open_tree 1p erf_ event_open :============ ::::;,,quotactl "quotactl_fd 11 :==========: .. setdomainname", "sethostname", :========= =: setns", 11, 11 , 11 11 11 , 11, 1 11, , 11 Книги для программистов: https://clcks.ru/books-for-it ===========:.:: Расширенные концепции 1 299 "syslog", "umount", ===============::::,,urnount2", _________"unshare" + "umount2" ], "action": "SCMP_ACT_ALLOW", "includes": "caps": [ "САР SYS ADMIN" Теперь можно протестировать обновленный профиль seccomp и убедиться, что он может выполнять umount, но не mount: $ docker container run -ti --rm --security-opt seccomp=umount2.json \ --cap-add=SYS_ADMIN spkane/train-os:latest /bin/bash [root@15b8a26Ьбcfe /]# strace umount /sys/fs/cgroup execve("/usr/Ьin/umount", ["umount", "/sys/fs/cgroup"], Ox7ffece9ebc38 ... close(З) О = ? exit_group(O) +++ exited with О+++ [root@15b8a26Ьбcfe /]# mount /dev/vdal /mnt mount: /mnt: permission denied. [root@15b8a26Ьбcfe /]# exit Если все пошло по плану, команды strace и umount должны были сработать, а mount нет. В реальных условиях было бы гораздо безопаснее переделать приложение таким образом, чтобы ему не требовались особые привилегии, но если этого не избежать, используйте эти инструменты, чтобы защитить контейнеры, не мешая их работе. � Вы можете полностью отключить профиль Secure Computing Mode по умолчанию, задав --security-opt seccomp=unconfined, однако запускать контейнер без ограни­ чений - плохая идея. Такой подход возможен разве что при попытке понять, какие именно системные вызовы необходимо определить в профиле. Преимущество Secure Computing Mode в том, что он позволяет нам выбирать воз­ можности контейнера в нижележащ!;)м ядре Linux. Для большинства контейнеров не нужно настраивать индивидуальные профили, но это очень удобный инстру­ мент, когда мы создаем контейнер с дополнительными полномочиями и хотим из­ бежать лишних рисков для безопасности всей системы. SELinux и AppArmor В предыдущих разделах мы видели, как контейнеры используют cgroups и про­ странства имен. SELinux (https://www.redhat.com/en/topics/linux/what-is-selinux) и AppArmor (https://apparmor.net/) в экосистеме Linux позволяют усилить безо­ пасность контейнеров. В данном разделе мы кратко рассмотрим эти две системы. SELinux и AppArmor расширяют возможности безопасности в системах Unix. Книги для программистов: https://clcks.ru/books-for-it 300 1 Глава 11 SELinux разработан Агентством национальной безопасности США, широко ис­ пользуется Red Hat и подцерживает детализированный контроль. AppArmor выпол­ няет почти те же функции, но работать с ним немного удобнее, чем с SELinux. По умолчанию Docker поставляется с адекватными профилями, подцерживающими одну из этих систем. Мы можем настраивать эти профили, разрешая или запрещая те или иные возможности. Если вы используете Docker в рабочей среде, проанали­ зируйте риски, чтобы понять, какие дополнительные факторы нужно учитывать. Мы кратко рассмотрим преимущества этих систем. Обе системы обеспечивают ма1:1датный кон_троль доступа - механизм безопасно­ сти, который предоставляет пользователям (инициаторам) в системе права на дос­ туп к ресурсу (целевому объекту). С его помощью можно запретить любым пользо­ вателям, в том числе с правами root, доступ к части системы. Политику можно применить ко всему контейнеру, чтобы ограничить все процессы. Здесь мы не будем подробно расписывать конфигурацию этих систем, потому что на это пона­ добилось бы несколько глав. Профили по умолчанию могут, например, блокиро­ вать доступ к частям файловых систем /proc и /sys, которые было бы опасно пре­ доставлять контейнеру, хотя они и находятся в пространстве имен контейнера. С помощью профилей по умолчанию также можно ограничить область файловой системы, чтобы контейнеры не могли использовать точки подключения, которые им нельзя видеть. Если вы планируете применять контейнеры Linux в рабочей среде, обязательно присмотритесь к AppArmor или SELinux. В целом обе системы почти одинаковые, но в контексте Docker у SELinux есть одно неудобное ограничение - он полноцен­ но работает только в системах, которые подцерживают метаданные файловых сис­ тем, а значит, будет совместим не со всеми драйверами хранилища Docker. Для AppArmor, с другой стороны, не нужны метаданные файловой системы, а значит, он работает со всеми бэкендами Docker. Выбор между ними зависит от дистрибу­ тива. Возможно, вам придется выбрать бэкенд файловой системы, который под­ держивает используемую систему безопасности. Демон Docker С точки зрения безопасности демон Docker и его компоненты - это единственный новый риск для инфраструктуры. Приложения в контейнерах не представляют бо­ лее серьезных рисков и даже лучше защищены, чем если бы мы развернули их вне контейнеров. С другой стороны, без контейнеров мы не стали бы запускать dockerd, демон Docker. Мы можем запустить Docker таким образом, чтобы не открывать в сеть никакие порты. Это рекомендованный подход, который по умолчанию при­ нят в большинстве установок Docker. В большинстве дистрибутивов конфигурация по умолчанию изолирует Docker от сети, предоставляя только локальный сокет Docker. Поскольку мы не можем уда­ ленно администрировать Docker с такими настройками, многие просто добавляют в конфигурацию нешифрованный порт 2375. Это удобно для начала работы с Docker, но небезопасно. Не открывайте Docker для внешнего мира без убедительКниги для программистов: https://clcks.ru/books-for-it Расширенные концепции 1 301 ной причины. А если открываете, то обеспечьте надежную защиту. Обычно плани­ ровщики работают на каждом узле и взаимодействуют с Docker через сокет домена Unix, а не через сетевой порт. Если мы хотим предоставить доступ к демону из сети, то должны принять меры, чтобы защитить Docker в рабочей среде. В любом случае мы ожидаем, что сам демон Docker будет устойчив к таким угрозам, как переполнение буфера или состояние гонки - к двум самым распространенным классам уязвимостей. Это от­ носится к любому сетевому сервису. В демоне Docker риск гораздо выше, посколь­ ку обычно он выполняется от имени root, может запускать в системе что угодно и у него нет интегрированного управления доступом на основе ролей. Дriя защиты Docker можно применять те же принципы, что и для других сетевых демонов, - шифрование трафика и аутентификация пользователей. Зашифровать в Docker трафик довольно просто, а вот с аутентификацией все сложнее. Если у вас есть SSL-сертификаты для защиты НТТР-трафика к хостам, например wildсаrd­ сертификаты для домена, используйте TLS для шифрования всего трафика к серве­ рам Docker через порт 2376. Это неплохое начало. Инструкции приведены в доку­ ментации Docker (https://docs.docker.com/engine/security/protect-access). Настройка аутентификации потребует больше усилий. В Docker нет детализиро­ ванной авторизации - доступ либо предоставляется, либо не предоставляется. Зато контроль аутентификации с помощью подписанных сертификатов работает до­ вольно эффективно. К сожалению, это означает, что мы не можем просто включить аутентификацию, не настроив центр сертификации. Если у вашей организации он уже есть, вам повезло. Управление сертификатами, включая их защиту и распреде­ ление, требует продуманной стратегии. Вьmолните следующие действия: 1. Настройте метод генерации и подписания сертификатов. 2. Создайте сертификаты для сервера и клиентов. 3. Настройте Docker таким образом, чтобы он требовал сертификаты с параметром --tlsverify. Подробные инструкции по настройке сервера и клиента, а также простого центра сертификации см. в документации Docker. Поскольку это демон, который почти всегда выполняется с привилегиями, и по­ скольку у него есть прямой контроль над приложениями, не стоит открывать Docker напрямую для Интернета. Если мы хотим взаимодействовать с хостами Docker за пределами нашей сети, можно использовать VPN или SSН-туннель для доступа к защищенному инсталляционному серверу uump host). Расширенная конфигурация Снаружи Docker выглядит как монолит, но за внешней поверхностью скрывается много настраиваемых компонентов. Например, бэкенды журналирования, которые мы рассматривали в разделе "Журншzирование" главы 6. Мы также можем изменить бэкенд хранилища образов для всего демона, выбрать другую среду выполнения Книги для программистов: https://clcks.ru/books-for-it 302 1 Глава 11 или настроить отдельные контейнеры таким образом, чтобы они использовали дру­ гую сетевую конфигурацию. Это сложные сценарии, которые нужно очень хорошо понимать, прежде чем реализовывать. Сначала мы рассмотрим сетевую конфигура­ цию, затем бэкенды хранилища и, наконец, попробуем другую среду выполнения контейнеров вместо runc, которая поставляется с Docker по умолчанию. Сети Мы уже рассматривали сетевые уровни между контейнерами Linux и реальной се­ тью. Давайте подробнее обсудим, как это работает. Docker поддерживает широкий набор сетевых конфигураций, но мы начнем с настроек по умолчанию. На рис. 11.1 мы видим схему типичного сервера Docker с тремя контейнерами в частной сети справа. У одного из них есть общедоступный порт (порт ТСР 10520), предостав­ ляемый на сервере Docker. Мы проследим, как входящий запрос попадает в кон­ тейнер Linux и как контейнер Linux может устанавливать исходящее подключение к внешней сети. Виртуальная сеть Сервер Docker Входящий запрос к открытому порту ТСР 10520 NAT dockerO 172.16.23.7 • • •• 'ii 172.16.23.1 1 1 Исходящий запрос 1 от контейнера Рис. 11.1. Сеть на типичном сервере Docker Если в сети у нас есть клиент, который хочет обратиться к серверу nginx, доступно­ му через порт ТСР 80 в контейнере 1, запрос пройдет через интерфейс ethO на сер­ вере Docker. Поскольку Docker знает, что это общедоступный порт, он запустил экземпляр docker-proxy, чтобы прослушивать порт 10520. Запрос передается в про­ цесс docker-proxy, который перенаправляет его в нужный контейнер по указанному адресу и порту в частной сети. В обратную сторону трафик по запросу проходит по тому же маршруту. Исходящий трафик от контейнера идет по другому пути, в обход docker-proxy. На этой схеме контейнер 3 хочет обратиться к серверу в Интернете. У него есть адрес в частной сети 172.16.23.1, а его маршрут по умолчанию - интерфейс dockero 172.16.23.7. Он отправляет трафик по этому адресу. Сервер Docker видит, что это Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 303 исходящий трафик и перенаправление трафика включено. Поскольку виртуальная сеть является частной, трафик отправляется с общедоступного адреса. Запрос про­ ходит через уровень преобразования сетевых адресов (Network Address Translation, NAТ) ядра и передается во внешнюю сеть через интерфейс eth0 на сервере. Обрат­ ный трафик проходит по тому же маршруту. NAT работает в одну сторону, так что контейнеры в виртуальной сети в ответных пакетах будут видеть реальные сетевые адреса. Вы могли заметить, что это непростая конфигурация, но с ней Docker выглядит довольно прозрачным. Кроме того, он предоставляет дополнительную защиту для стека Docker, поскольку контейнеры используют отдельные пространства имен се­ ти, находятся в отдельных частных сетях и не имеют доступа к таким компонентам, как шина DBus (Desktop Bus) или iptaЬles в основной системе. Давайте рассмотрим, что именно происходит. Интерфейсы, которые отображаются в ifconfig или ip addr show в контейнере Linux, на самом деле представляют собой виртуальные интерфейсы Ethemet в ядре сервера Docker. Они сопоставляются с пространством имен сети контейнера и получают имена, которые мы видим внут­ ри контейнера. Давайте посмотрим, что мы получим при выполнении ip addr show на сервере Docker. Мы сократим выходные данные для ясности и экономии места: $ ip addr show 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNКNOWN group link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 brd 127.255.255.255 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state link/ether 02:50:00:00:00:01 brd ff:ff:ff:ff:ff:ff inet 172.16.168.178/24 brd 192.168 .65.255 scope global dynamic ... valid_lft 4908sec preferred_lft 3468sec inet6 fe80::50:ff:fe00:l/64 scope link valid_lft forever preferred_lft forever 7: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue link/ether 02:42:9c:d2:89:4f brd ff:ff:ff:ff:ff:ff inet 172.17.42 .1/16brd 172.17.255 .255 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::42:9cff:fed2:894f/64 scope link valid_lft forever preferred_lft forever 185: veth772de2a@if184: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc link/ether 9а:а9:24:Ь7:5а:31 brd ff:ff:ff:ff:ff:ff link-netnsid 1 inet6 fe80::98a9:24ff:feb7:5a31/64 scope link valid_lft forever preferred_lft forever Мы видим, что у нас есть нормальный lоорЬасk-интерфейс, настоящий интерфейс Ethemet eth0, а затем интерфейс моста Docker, docker0, который мы рассматривали Книги для программистов: https://clcks.ru/books-for-it 304 1 Глава 11 ранее. Оrсюда весь трафик от контейнеров Linux перенаправляется за пределы вир­ rуальной сети. В этих выходных данных обратите внимание на интерфейс veth772de2a. При создании контейнера Docker создает два вирrуальных интерфейса: �дин на стороне сервера, прикреплен к мосrу docker0, а другой - прикреплен к про­ странству имен контейнера. Здесь мы видим интерфейс на стороне сервера. Вы за­ метили, что мы не видим IР-адрес? Это потому, что интерфейс привязан к мосrу. В пространстве имен контейнера у интерфейса будет другое имя. Как и для многих других компонентов Docker, мы можем выбрать другую реализа­ цию прокси. Можно задать настройку --userland-proxy-path=<path>, но для этого вряд ли найдутся серьезные причины, если только вы не используете очень специализи­ рованную сеть. Флаг --userland-proxy=false для dockerd полностью отключает userland­ proxy, и трафик между локальными контейнерами будет перенаправляться с помо­ щью функционала Hairpin NAТ (https://www.geeksforgeeks.org/network-address­ translation-nat). Если вам нужны службы с высокой пропускной способностью, рассмотрите этот подход. Hairpin NAТ описывает взаимодействие сервисов в сети с NAТ через общедоступ­ ные IР-адреса. Трафик при этом проходит из исходного сервиса в Интернет, до внешнего интерфейса для маршрутизатора NAT, а затем возвращается в исход­ ную сеть к сервису назначения. Этот U-образный маршрут по форме напоминает шпильку для волос - hairpin. Сеть хоста Реализация по умолчанию выглядит довольно сложно, но мы можем запускать кон­ тейнер без полноценной сетевой конфигурации, создаваемой Docker. docker-proxy может ограничивать пропускную способность для сервисов, обрабатывающих большие объемы данных, - весь сетевой трафик может проходить через процесс docker-proxy до получения контейнером. Что будет, если мы отключим сетевой уро­ вень Docker? В Docker это можно сделать отдельно для каждого контейнера с по­ мощью параметра --net=host в командной строке. Иногда, например, когда мы запускаем приложения с высокой пропускной способностью, нам это необходимо, но при этом мы теряем гибкие возможности, предлагаемые Docker. Давайте по­ смотрим, как работает этот механизм. К этой конфигурации тоже нужно отнестись очень серьезно. Она влияет на работу и на систему безопасности и, возможно, не подойдет для вашей ситуации. Внима­ тельно изучите все факторы и последствия, прежде чем использовать ее. Давайте запустим контейнер с параметром --net=host и посмотрим, что получится: $ docker container run --rm -it --net=host spkane/train-os bash [root@docker-desktop /]1 docker container run --rm -it --net=host \ spkane/train-os ip addr show 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNКNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 305 inet 127.0.0.1/8 brd 127.255.255.255 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: eth0: <BROAOCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 02:50:00:00:00:01 brd ff:ff:ff:ff:ff:ff inet 192.168.65.3/24 brd 192.168.65.255 scope global dynamic noprefixroute eth0 valid_lft 4282sec preferred_lft 2842sec inet6 fe80::50:ff:fe00:l/64 scope link valid_lft forever preferred_lft forever 7: docker0: <NO-CARRIER,BROAOCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state OOWN group default link/ether 02:42:9c:d2:89:4f brd ff:ff:ff:ff:ff:ff inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::42:9cff:fed2:894f/64 scope link valid_lft forever preferred_lft forever 8: br-340323d07310: <NO-CARRIER,BROADCAST,МULTICAST,UP> mtu 1500 qdisc noqueue state OOWN group default link/ether 02:42:56:24:42:Ь8 brd ff:ff:ff:ff:ff:ff inet 172.22.0.1/16 brd 172.22.255.255 scope global br-340323d07310 valid_lft forever preferred_lft forever 11: br-0lf7537b9475: <NO-CARRIER,BROAOCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state OOWN group default link/ether 02:42:ed:14:67:61 brd ff:ff:ff:ff:ff:ff inet 172.18.0.1/16 brd 172.18.255.255 scope global br-0lf7537b9475 valid_lft forever preferred_lft forever inet6 fc00:f853:ccd:e793::l/64 scope global valid_lft forever preferred_lft forever inet6 fe80::42:edff:fel4:6761/64 scope link valid_lft forever preferred_lft forever inet6 fe80::l/64 scope link valid_lft forever preferred_lft forever Выглядит знакомо? Дело в том, что когда мы запускаем контейнер с сетью Host, контейнер выполняется пространствах имен сети и UTS, в которых находится сам хост. Имя хоста сервера docker-desktop, и в командной оболочке мы видим, что у контейнера то же имя хоста: [root@docker-desktop /]# hostname docker-desktop Если мы выполним команду mount и посмотрим, что подключено, то увидим, что в Docker по-прежнему присутствуют каталоги /etc/resolv.conf, /etc/hosts и /etc/hostname. Как и ожидалось, каталог /etc/hostname содержит только имя хоста сервера: [root@docker-desktop /]# mount overlay оп/ type overlay (rw,relatime,lowerdir;/var/liЬ/docker/overlay2/...) /dev/vdal оп/etc/resolv.conf type ext4 (rw,relatime) /dev/vdal оп/etc/hostname type ext4 (rw,relatime) /dev/vdal оп/etc/hosts type ext4 (rw,relatime) [root@docker-desktop /]# cat /etc/hostname docker-desktop Чтобы убедиться, что мы видим все нормальные сети на сервере Docker, давайте изучим выходные данные ss и посмотрим, доступны ли нам сокеты, используемые Docker: Книги для программистов: https://clcks.ru/books-for-it 306 Глава 11 root@852dl8f5c38d:/# ss I grep docker и str ЕSТАВ О О /run/guest-services/docker.sock 18086 * 16860 и str ЕSТАВ О О /var/run/docker.sock � � 21430 * 21942 Если бы демон Docker прослушивал ТСР-пот, например 2375, его тоже можно было бы увидеть. Найдите другой ТСР-порт, если известно, что он используется. Если мы поищем docker в выходных данных обычного контейнера в своем про­ странстве имен, то не получим никаких результатов: $ docker container run --rm -it spkane/train-os bash -с "ss I grep docker" Итак, мы действительно находимся в пространстве имен сети сервера. Это значит, что если мы собираемся запускать сетевой сервис с высокой пропускной способно­ стью, мы можем ожидать приличную производительность сети по умолчанию. С другой стороны, мы можем попытаться подключиться к портам, которые кон­ фликтовали бы с портами на сервере, поэтому нужно проявлять особую осторож­ ность при назначении портов. Конфигурирование сетей Мы можем использовать не только сеть по умолчанию или сеть хоста. Команда docker network позволяет создавать несколько сетей на разных драйверах. Кроме того, с ее помощью мы можем просматривать и контролировать сетевые уровни Docker, а также их подключение к контейнерам, запущенным в системе. Мы можем просмотреть все сети, доступные в Docker, следующей командой: $ docker network 1s SCOPE DRIVER NETWORК ID NАМЕ local bridge bridge 5840а6с23373 local lc22b4582189 host host с128ЬfdЬеООЗ none null local Мы также можем посмотреть детали отдельных сетей командой docker network inspect, указав идентификатор сети: $ docker network inspect 5840а6с23373 "Name": "bridge", "Id": "5840... fc94", "Created": "2022-09-2ЗТО1: 21: 55.6979079582", "Scope": "local", "Driver": "bridge", "EnaЬleIPvб": false, "!РАМ": { "Driver": "default", Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 307 "Options": null, "Config": [ "SuЬnet": "172.17.0.0/16", "Gateway": "172.17.0.1" }, "Internal": false, "AttachaЬle": false, "Ingress": false, "ConfigFrom": { "Network" : 1111 }, "ConfigOnly": false, "Containers": {}, "Options": { "com.docker.network.bridge.default_bridge": "true", "com.docker.network.bridge.enaЫe_icc": "true", "com.docker.network.bridge.enaЫe_ip_masquerade": "true", "com.docker.network.bridge.host_Ьinding_ipv4": "О.О.О.О", "com.docker.network.bric\ge.name": "dockerO", "com.docker.network.driver.mtu": "1500" }, "LaЬels": {) Сети Docker можно создавать и удалять, а также подкточать и откточать от от­ дельных контейнеров с помощью подкоманды network. Итак, у нас есть сеть в режиме Host, сеть в режиме None и сеть в режиме Bridge с Hairpin NAТ. Нам доступны другие режимы, с помощью которых можно созда­ вать различные топологии в Docker. Самые распространенные из них - overlay и macvlan. Давайте рассмотрим их возможности: ♦ overlay - этот драйвер используется в режиме Swarm для создания оверлейной сети между хостами Docker, чтобы установить частную сеть между всеми кон­ тейнерами поверх реальной сети. Это удобно для Swaпn, но не применяется в других случаях. ♦ macvlan - этот драйвер создает реальный МАС-адрес для каждого контейнера и предоставляет его по сети с помощью выбранного интерфейса. Для этого ком­ мутатор должен поддерживать более одного МАС-адреса на физический порт. В результате все контейнеры будут видимы напрямую в базовой сети. При пере­ ходе с традиционной системы на контейнеры это может быть очень удобно. У такого подхода есть свои недостатки, например при отладке сложнее опреде­ лить, с какого хоста поступает трафик, есть риск переполнения таблиц МАС­ адресов на коммутаторах, чрезмерного использования ARP хостами и других Книги для программистов: https://clcks.ru/books-for-it 308 Глава 11 проблем в базовой сети. Поэтому драйвер macvlan целесообразен только в тех случаях, когда вы хорошо понимаете, как работает нижележащая сеть и как ею управлять. Здесь возможно несколько конфигураций, но базовый вариант настроить довольно просто: $ docker network create -d macvlan \ --suЬnet= 172.16.16.0/24 \ --gateway=172.16.16.1 \ -о parent=eth0 ourvlan $ docker network 1s NETWORК ID NАМЕ DRIVER SCOPE 5840абс23373 bridge local bridge lc22b4582189 host host local с128ЬfdЬеООЗ none null local 8218с0есс9е2 ourvlan macvlan local $ docker network rm 8218с0есс9е2 Мы можем з�претить Docker назначать конкретные адреса, указывая их как имено­ � ванные дополнительные адреса: --aux-address= "my- router= 172.16.16.129". - На сетевом уровне Docker можно многое настроить под конкретный вариа·нт при­ менения, но обычно все оставляют конфигурацию по умолчанию с сетью хоста и пользовательским режимом без прокси. Мы можем настроить для контейнеров имена серверов DNS, параметры преобразователя, шлюзы по умолчанию и др. В разделе о сетях в документации Docker (https://docs.docker.com/network) под­ робно описаны некоторые конфигурации. Для расширенной сетевой конфиrурации в Docker изучите Weave - хорошо под­ держиваемый инструмент оверлейных сетей для запуска контейнеров на несколь­ ких хостах Docker (https://github.com/weaveworks/weave). Он похож на драйвер overlay, но мы можем настроить гораздо больше параметров и обойтись без Swarm. Еще одно решение - Project Calico (https://www.tigera.io/project-calico). Если вы используете Kubernetes, у которого своя конфиrурация сети, изучите Container Network lnterface (CNI) (https://www.cni.dev/), а также Cilium (https:// cilium.io/), которые предоставляет надежные сети на базе eBPF для контейнеров. Хранилище За чтение и запись всех данных для образов и контейнеров на сервере Docker отве­ чает бэкенд хранилища. Docker предъявляет к нему довольно строгие требования: хранилище должно поддерживать слои, с помощью которых Docker отслеживает изменения и контролирует занимаемое пространство и объем данных, передавае­ мых при развертывании новых образов. Благодаря механизму копирования при за­ писи Docker может запустить новый контейнер из имеющегося образа, не копируя образ целиком, потому что хранилище поддерживает эту возможность. Благодаря Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 1 309 бэкенду хранилища мы можем экспортировать образы в виде групп изменений в слоях, а также сохранять состояние работающих контейнеров. В большинстве случаев нам потребуется помощь ядра, чтобы эффективно выполнять эти задачи, потому что контейнер видит объединение всех слоев под ним, которые не копиру­ ются в сам контейнер, а просто предоставляются ему. Только когда мы вносим из­ менения, в файловую систему контейнера что-то записывается. Мы видим эти слои, когда загружаем или выгружаем новый образ из реестра, вроде Docker Hub. Демон Docker извлекает и отправляет каждый слой по отдельности, а если некоторые слои совпадают с уже сохраненными, он задействует кешированный слой. При отправке в реестр мы иногда даже видим, к какому образу относится слой. Docker работает со слоями с помощью нескольких драйверов ядра. Кодовая база Docker содержит код, который реализует взаимодействие со многими бэкендами, и мы можем выбрать, какой из них будет использоваться при перезапуске демона. Давайте рассмотрим преимущества и недостатки доступных вариантов. У решений есть свои ограничения, и вам следует изучить их, чтобы подобрать под­ ходящий вариант. В некоторых случаях выбор зависит от установленного дистри­ бутива Linux. Проще всего использовать драйверы, встроенные в ядро вашего дистрибутива, - всегда лучше придерживаться хорошо протестированных реше­ ний. Обычно ,следует выбирать варианты, которые лучше всего поддерживаются, чтобы проблем возникало как можно меньше. Кроме того, разные бэкенды показы­ вают разную статистику через Docker Remote API (конечная точка /info) для отсле­ живания систем Docker. Давайте сравним доступные варианты файловых систем: ♦ Overlay Overlay (ранее OverlayFS) - это объединенная файловая система, в которой несколько слоев группируются таким образом, что выглядят как единая файло­ вая система (https://www.kerneJ.org/doc/htmlЛatest/filesystems/overlayfs.html). Overlay, пожалуй, лучший выбор для Docker, совместимый с большинством ос­ новных дистрибутивов. Если вы используете ядро Linux старше версии 4.0 (или 3.10.0-693 для RНEL), это решение вам не подойдет. Эта файловая система на­ столько надежная и производительная, что ради нее стоит обновить операцион­ ную систему для хостов Docker, даже если компания использует более старый дистрибутив. Файловая система Overlay входит в мейнлайновое ядро Linux, а значит, это очень стабильное решение с гарантированной долгосрочной под­ держкой. Docker поддерживает две версии Overlay: overlay и overlay2. Как можно догадаться, лучше использовать overlay2, потому что это более быстрая, надеж­ ная и эффективная версия с поддержкой inode. Сообщество Docker часто совершенствует подцержку различных файловых сис­ тем. Дополнительную информацию о подцерживаемых файловых системах см. в официальной документации (https://docs.docker.com/storage/storagedriver). ♦ AuFS На момент написания этой книги использовать aufs уже не рекомендуется, но все-таки это исходный бэкенд для Docker. AuFS (Advanced multilayered unification filesystem, https://aufs.sourceforge.net/) - это объединенная файлаКниги для программистов: https://clcks.ru/books-for-it 310 Глава 11 вая система, поддерживающаяся многими популярными дистрибутивами Linux. Она никогда не входила в основную ветвь ядра, а потому доступна не во всех дистрибутивах. Например, она не поддерживается в новых версиях Red Hat и Fedora. Она не поставляется в стандартном дистрибутиве Ubuntu, но входит в пакет Ubuntu linux-image-extra. Из-за отсутствия популярности этого бэкенда были разработаны другие вариан­ ты. Если у вас старые дистрибутивы, которые поддерживают AuFS, то вы може­ те рассмотреть этот вариант, но лучше все же обновить ядро до версии, которая поддерживает Overlay или Btrfs. ♦ btrfs B-Tree File System (Btrfs, https://Ьtrfs.wiki.kernel.org/index.php/Мain_Page)­ этo файловая система с копированием при записи, а значит, она хорошо подхо­ дит для образов Docker. Как и aufs, но, в отличие от devicemapper, Docker широко использует этот бэкенд, так что он отличается стабильностью и производитель­ ностью в рабочей среде. Он поддерживает до нескольких тысяч контейнеров в одной системе. Недостаток для систем Red Hat - Btrfs не поддерживает SELinux. Система btrfs - это второй оптимальный вариант после драйвера overlay2. Популярный способ использовать бэкенды btrfs для контейнеров Linux, при этом не выделяя файловой системе целый том, - упаковать Btrfs в файл и примонтировать файл как блочное устройство: mount -о loop file.Ьtrs /mnt. С по­ мощью этого метода можно создать файловую систему для контейнеров Linux на 50 ГБ, не отдавая все ценное локальное хранилище драйверу Btrfs. ♦ Device Mapper Изначально Device Mapper был создан Red Hat для их дистрибутивов, которые не поддерживали AuFS в первое время существования Docker, а сейчас это бэкенд по умолчанию для всех дистрибутивов Linux на базе Red Hat. В некото­ рых версиях Red Hat Linux это может быть единственным доступным вариан­ том. Device Mapper уже давно встроен в ядро Linux и работает очень стабильно. Демон Docker использует его немного нетрадиционно, и раньше этот вариант не отличался стабильностью. Из-за сомнительного прошлого мы рекомендуем по возможности выбирать другие бэкенды. Если ваш дистрибутив поддерживает только драйвер devicemapper, ничего страшного, но в других ситуациях стоит предпочесть overlay2 или Ьtrfs. По умолчанию devicemapper задействует режим loop-lvm без конфигурации. Он работает медленно и обычно подходит только для разработки. Если вы выберете драйвер devicemapper, убедитесь, что он использует · режим direct-1 vm за пределами среды разработки. Больше о режимах devicemapper в Docker см. в официальной документации (https:// docs.docker.com/storage/storagedriver/device-mapper-driver). В статье блога, опубликованной в 2014 году, описана история разных бэкендов хранилища Docker (https://developers.redhat.com/Ыog/2014/09/30/overview-storage-scalabllity-docker). ♦ VFS Среди поддерживаемых драйверов Virtual File System {vfs) самый простой (и медленный) для запуска. Он не поддерживает копирование при записи, а вмеКниги для программистов: https://clcks.ru/books-for-it Расширенные концепции З11 сто этого создает новый каталог и копирует все имеющиеся данные. Изначально он предназначался для тестирования и монтирования томов хоста. Драйвер vfs заметно замедляет создание новых контейнеров, зато работает быстро в рантай­ ме. Это простой механизм, в котором мало что может пойти не так. Компания Docker, Inc. не рекомендует использовать его в рабочей среде, так что будьте осторожны, если выберете его для реальных условий. ♦ ZFS Файловая система ZFS, созданная Sun Microsystems, - это самая продвинутая файловая система с открытым исходным кодом для Linux. Из-за ограничений лицензирования она не поставляется в основной ветке Linux, зато благодаря проекту ZFS on Linux (https://zfsonlinux.org/) довольно легко устанавливается. Docker может работать поверх файловой системы ZFS и использовать расши­ ренные возможности копирования при записи для реализации работы со слоями. Учитывая, что ZFS не находится в мейнлайновом ядре и не поставляется с большинством крупных коммерческих дистрибутивов, при выборе этого вари­ анта потребуются дополнительные усилия. Если вы уже применяете ZFS в рабо­ чей среде, для вас это может стать лучшим выбором. От бэкенда хранилища зависит производительность контейнеров. Если мы поме­ няем бэкенд на сервере Docker, все имеющиеся образы хотя и не пропадут, но станут невидимыми, пока мы не включим этот бэкенд снова. Будьте осторожны. Команда docker system info показывает, какой бэкенд хранилища сейчас использует­ ся в системе: $ docker system info Storage Driver: overlay2 Backing Filesystem: extfs Supports d_type: true Native Overlay Diff: true userxattr: false Docker показывает базовую файловую систему, если она есть. Здесь мы видим драйвер overlay2, и он поддерживается файловой системой ext. В некоторых случа­ ях, например при использовании devicemapper на разделах raw или с btrfs, отдельной базовой файловой системы не будет. Бэкенды хранилища можно менять с помощью файла конфигурации daemon-json или аргументов командной строки с командой dockerd при запуске. Если мы хотим в системе Ubuntu перейти с aufs на devicemapper, то можем выполнить следующую команду: $ dockerd --storage-driver=devicemapper Это сработает почти в любой системе Linux, которая поддерживает Docker, потому что devicemapper есть почти везде. То же самое относится к overlay2 в современных Книги для программистов: https://clcks.ru/books-for-it 312 Глава 11 ядрах Linux, но для вкточения других драйверов понадобятся дополнительные ме­ ханизмы. Например, без aufs в ядре (обычно в виде модуля) Docker не запустится, если для драйвера хранилища выбран aufs. Это относится также к Btrfs и ZFS. Выбрать подходящий драйвер хранилища для конкретной системы и развертыва­ ния - это одно из самых важных решений, которые необходимо принять при пла­ нировании Docker для рабочей среды. Придерживайтесь консервативного подхода: выберите решение, которое хорошо поддерживается вашим ядром и дистрибути­ вом. Раньше это было довольно серьезной проблемой, но сейчас большинство драйверов достигли неплохой зрелости. Проявляйте осторожность, эксперименти­ руя с новыми бэкендами. Как показывает опыт, драйверам хранилища требуется некоторое время, прежде чем их можно будет спокойно использовать в рабочих системах. nsenter Утилита nsenter (namespace enter - вход в пространство имен) позволяет входить в любое пространство имен Linux. Эта утилита включена в пакет uf.il-linux с kernel.org (bttps://mirrors.edge.kernel.org/pub/linux/utils/util-linux). С помощью nsenter мы можем войти в контейнер Linux с самого сервера, даже если сервер dockerd не отвечает и нам не доступна команда docker container ехес. Мы также можем использовать его, чтобы выполнять задачи в контейнере с правами root на сервере, даже если docker container ехес нам этого не позволяет. Это бывает очень полезно при отладке. Обычно нам вполне достаточно docker container ехес, но nsenter все равно должен быть под рукой. Большинство дистрибутивов Linux поставляются с достаточно новым пакетом util­ linux, который содержит nsenter. Если в вашем дистрибутиве его нет, самый простой способ получить nsenter - установить его через сторонний контейнер Linux (https://github.com/jpetazzo/nsenter). Контейнер извлекает образ Docker из реестра Docker Hub, а затем выполняет контейнер Linux, который установит инструмент командной строки nsenter в /usr/locaVЬin. Поначалу это может показаться странным, но это удобный способ удаленно устанавливать nsenter на любом сервере Docker с помощью одной коман­ ды docker. В отличие от docker container ехес, которую можно выполнять удаленно, nsenter ра­ ботает только на самом сервере, напрямую или через контейнер. В нашем примере мы предусмотрели для nsenter специальный контейнер. Как и в примере с docker container ехес, нужно запустить контейнер: $ docker container run -d --пn uЬuntu:22.04 sleep 600 fd521174d66dc32650dl65e0ce7dd97255c7b3624c34cЫdll9d955284382ddf docker container ехес работает просто, но nsenter не так удобен, потому что ему ну­ жен PID фактического верхнеуровневого процесса в контейнере, а его еще нужно найти. Давайте запустим nsenter вручную и посмотрим, что происходит. Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 1 313 Сначала нужно найти ID выполняющегося контейнера, потому что иначе nsenter не сможет к нему обратиться. Для этого выполним команду docker container ls: $ docker container ls CONTAINER ID fd521174d66d СОММАND IМAGE uЬuntu:22.04 "sleep 1000" NAМES angry_albattani Нужный ID находится в первом поле - fd521174d66d. Теперь найдем PID: $ docker container inspect --format \{{.State.Pid\}} fd521174d66d 2721 Мы также можем получить реальный PID процессов в контейнере, выполнив ко­ манду docker container top с указанием ID контейнера. В нашем примере он будет выглядеть следующим образом: $ docker container top fd521174d66d UID PID PPID С STIME ТТУ TIME CMD root 2721 2696 О 20:37? 00:00:00 sleep 600 Измените аргумент --target в следующей команде, указав идентификатор процесса, полученный в предыдущей команде, и вызовите nsenter: $ docker container run --rm -it --privileged --pid=host debian \ nsenter --target 2721 --all # ps -ef ТIМЕ CMD PPID С STIME ТТУ PID UID root root root # exit 1 11 15 о о 11 О 20:37? О 20:51? О 20:51? 00:00:00 sleep 600 00:00:00 -sh 00:00:00 ps -ef Результат очень похож на то, что мы получили бы с помощью docker container ехес, потому что, по сути, он делает то же самое. Аргумент командной строки --all указывает nsenter, что мы хотим войти во все пространства имен, используемые процессом, который указан в --target. Отладка контейнеров без командной оболочки Если нам нужно провести диагностику в контейнере, у которого нет командной оболочки Unix, придется приложить дополнительные усилия. Например, мы можем запустить контейнер, в котором только один исполняемый файл: $ docker container run --rm -d --name outyet-small \ --puЬlish mode=ingress,puЬlished=8090,target=8080 \ spkane/outyet:1.9.4-small 4f6de24d4c9c794c884afa758ef5b33ea38c0lf8ec9314dcddd9fadc25cla443 Давайте посмотрим на процессы, которые выполняются в этом контейнере: $ docker container top outyet-small UID PID PPID С STIME ТТУ root 61033 61008 О 22:43 ? TIME 00:00:00 CMD /outyet -version 1.9.4 -poll 600s ... Книги для программистов: https://clcks.ru/books-for-it 314 Глава 11 Если мы попробуем запустить командную оболочку Unix в контейнере, то получим ошибку: $ docker container ехес -it outyet-small /bin/sh OCI runtime ехес failed: ехес failed: unaЬle to start container process: ехес: "/Ьin/sh": stat /Ьin/sh: no such file or directory: unknown Затем мы можем запустить второй контейнер, с командной оболочкой, и другие полезные инструменты, чтобы новый контейнер видел процессы в первом кон­ тейнере, использовал тот же сетевой стек, что и первый контейнер, а также полу­ чил несколько дополнительных привилегий, которые будут необходимы при от­ ладке: $ docker container run --rm -it --pid=container:outyet-small \ --net=container:outyet-small --cap-add sys_ptrace \ --cap-add sys_admin spkane/train-os /Ьin/sh sh-5.1# Если мы введем 1s в этом контейнере, то увидим в файловой системе образ spkane/train-os, который содержит /Ьin/sh и все отладочные инструменты, но не включает никакие файлы из контейнера outyet-small: sh-5.1* 1s Ьin dev home lib64 media opt root sЬin sys usr boot etc lib lost+found mnt proc run srv tmp var Если мы введем ps -ef, то увидим все процессы исходного контейнера, потому что мы указали Docker использовать пространство имен контейнера outyet-small, пере­ дав аргумент --pid=container:outyet-small: sh-5.1# ps -ef UID root root root PID 1 29 36 PPID о о 29 с о о о STIME 22:43 22:47 22:49 ТТУ ? pts/0 pts/0 TIME 00:00:00 00:00:00 00:00:00 CMD /outyet -version 1.9.4 -poll 600s . . . /Ьin/sh ps -ef Поскольку мы используем тот же сетевой стек, можно даже указать в curl порт, к которому привязан сервис outyet первого контейнера: sh-5.1# curl localhost:8080 <!DOCTYPE html><html><Ьody><center> <h2>Is Go 1.9.4 out yet?</h2> <hl> <а href="https://go.googlesource.com/go/&#43;/gol.9.4">YES!</a> </hl> <p>Hostname: 155914f7cбcd</p> </center></Ьody></html> Сейчас можно использовать strace или другие инструменты для отладки приложе­ ния, а затем выйти (exit) из нового контейнера отладки, при этом исходный кон­ тейнер продолжит выполняться. Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции � � З 15 Когда закончите, нажмите <Ctrl>+<C>, чтобы выйти из процесса strace, если вы использовали именно его. sh-5.1# strace -р 1 strace: Process 1 attached futex(0x963698, FUTEX_WAIT, О, NULL л Cstrace: Process 1 detached <detached ...> sh-5.1# exit exit В этом примере у нас не было доступа к файловой системе. Если мы хотим посмот­ реть или скопировать файлы из контейнера, можно выполнить команду docker container export, чтобы экспортировтаь ТАR-файл с файловой системой контейнера: $ docker container export outyet-small -о export.tar Затем можно с помощью tar посмотреть или извлечь файлы: $ tar -tvf export.tar -rwxr-xr-x drwxr-xr-x -rwxr-xr-x drwxr-xr-x drwxr-xr-x drwxr-xr-x -rwxr-xr-x -rwxr-xr-x lrwxrwxrwx -rwxr-xr-x _drwxr-xr-x drwxr-xr-x -rw-r--r-- -rwxr-xr-x drwxr-xr-x drwxr-xr-x о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о о Jul Jul Jul Jul Jul Jul Jul Jul Jul Jul Apr 17 17 17 17 17 17 17 17 17 17 24 Apr 24 261407 Mar 13 5640640 Apr 24 Jul 17 о Jul 17 о 16:04 16:04 16:04 16:04 16:04 16:04 16:04 16:04 16:04 16:04 2021 2021 2018 2021 16:04 16:04 .dockerenv dev/ dev/console dev/pts/ dev/shm/ etc/ etc/hostname etc/hosts etc/mtab -> /proc/mounts etc/resolv.conf etc/ssl/ etc/ssl/certs/ etc/ssl/certs/ca-certificates.crt outyet proc/ sys/ Когда закончите, удалите export.tar и остановите контейнер outyet-small командой docker container stop outyet-small. Мы можем исследовать файловую систему контейнера с сервера Docker, напря­ мую перейдя к ее расположению в системе хранения сервера. Обычно это будет путь /var/liЫ docker/overlay/fd5... но все зависит от установки Docker, бэкенда хра­ � нилища и хеша контейнера. Мы можем определить корневой каталог Docker с по­ мощью команды docker system info. Структура Docker Docker состоит из пяти компонентов на стороне сервера, которые предоставляют единую точку взаимодействия через API. Эти компоненты: dockerd, containerd, runc, containerd-shim-runc-v2 и docker-proxy, который мы рассматривали в разделе "Сети" Книги для программистов: https://clcks.ru/books-for-it 316 Глава 11 данной главы. Мы рассмотрели много примеров работы с dockerd и его API. Он от­ вечает за оркестрацию всех компонентов, составляющих Docker. При запуске кон­ тейнера Docker использует containerd для работы с экземплярами контейнеров. Раньше все эти задачи выполнял сам процесс dockerd, но у такого подхода было несколько недостатков: ♦ У dockerd было слишком много заданий. ♦ Монолитная среда исполнения не позволяла легко менять компоненты. ♦ dockerd приходилось контролировать жизненный цикл самих контейнеров, и его нельзя было перезапустить или обновить без потери всех запущенных контей­ неров. Еще одна причина, по которой пришлось реализовать containerd, заключается в том, что контейнеры не представляют собой единую абстракцию. На платформах Linux это процессы, которые включают пространства имен, cgroups и правила безопасно­ сти в AppArmor или SELinux. Кроме того, Docker может работать в Windows и, возможно, на других платформах в будущем. Компонент containerd представляет внешнему миру стандартный уровень, не зависящий от реализации, так что разра­ ботчики имеют дело с концепциями высокого уровня, вроде контейнеров, задач и снапшотов, а не с отдельными системными вызовами Linux. Этот подход заметно упрощает демон Docker и позволяет таким платформам, как Kubemetes, напрямую интегрироваться в containerd, без необходимости в Docker API. Kubemetes много лет использовал оболочку (shim) Docker, но сейчас задействует containerd напрямую. На рис. 11.2 представлены все компоненты Docker, и мы рассмотрим их подробнее: ♦ dockerd - один на сервер. Предоставляет API, собирает образы контейнеров, обеспечивает высокоуровневое сетевое управление, включая тома, журналиро­ вание, статистические отчеты и т. д. ♦ docker-proxy - один на правило проброса портов. Каждый экземпляр отвечает за перенаправление трафика по определенному протоколу (TCP/UDP) с указанного IР-адреса и порта хоста на указанный IР-адрес и порт контейнера. dockerd-proxy dockerd dockerd-proxy ,,------ '1 1 1 1 containerd-shim-runc-v2 containerd-shim-runc-v2 containerd-shim-runc-v2 контейнер 1 контейнер 2 контейнер З Рис. 11.2. Структура Docker Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 317 ♦ containerd - один на сервер. Управляет жизненным циклом, исполнением, фай­ ловой системой с копированием при записи, а также низкоуровневыми сетевыми драйверами. ♦ containerd-shim-runc-v2 - один на контейнер. Отвечает за дескрипторы файлов, переданные контейнеру (например, stdin/out), и сообщает статус выхода. ♦ runc - создает контейнер и выполняет его, собирает статистику и докладывает о событиях жизненного цикла. Компоненты dockerd и containerd взаимодействуют друг с другом через сокет, обыч­ но сокет Unix, используя gRPC API (https://grpc.io/). В этой паре dockerd будет кли­ ентом, а containerd- сервером. runc- это инструмент CLI, который считывает конфигурацию из файла JSON на диске и выполняется с помощью containerd. При запуске нового контейнера dockerd проверяет, присутствует ли образ, или извлекает его из репозитория, указанного в имени образа (в будущем эту задачу может взять на себя containerct, который уже поддерживает извлечение образов). Демон Docker отвечает за большую часть остальных задач по созданию контейнера, например запуск docker-proxy для настройки проброса портов. Затем он отправляет containerd запрос на запуск контейнера. Containerd на основе образа применяет кон­ фигурацию, переданную от dockerd, чтобы создать комплект (bundle) OCI (https:// www.opencontainers.org/), который можно выполнить с помощью runc. Цитата с сайта OCI: "Open Container lnitiative (OCI) - это легкая и открытая систе­ ма управления (проект), образованная под эгидой Linux Foundation с целью созда­ ния открытых отраслевых стандартов для форматов и сред выполнения контейне­ � ров. Проект OCI запущен 22 июня 2015 года компаниями Docker, CpreOS и другими лидерами в отрасли контейнеров". Затем выполняется процесс containerd-shim-runc-v2 для запуска контейнера, для чего запускается runc. Однако после запуска контейнера runc не будет выполняться, и containerd-shim-runc-v2 будет фактическим родительским процессом нового кон­ тейнерного процесса. Если мы запустим контейнер и посмотрим на вывод ps axlf на сервере Docker, то увидим родительские и дочерние процессы и отношения между ними. Процесс PID 1 - /sЬin/init, и это родительский процесс для containerd, dockerd и containerd­ shim-runc-v2. Виртуальная машина Docker Desktop содержит минимальные версии большинства инструментов Linux, и некоторые из этих команд моrут не давать тот же вывод, который мы получили бы при использовании стандартного сервера Linux в качест­ ве хоста демона Docker. $ docker container run --rm -d \ --puЫish mode=ingress,puЫished=8080,target=80 \ --name nginx-test --rm nginx:latest 08b5cffed7baaf32b3af50498f7e5c5fa7ed35e094fa6045c205a88746fe53dd $ ps axlf Книги для программистов: https://clcks.ru/books-for-it 318 Глава 11 ...PID PPID СОММАND ...5171 1 ...5288 1 ...5784 5288 ...5791 5288 ...5807 1 ...5829 5807 ...5880 5829 ...5881 5829 ...5882 5829 ...5883 5829 /usr/bin/containerd /usr/bin/dockerd -Н fd:// --containerd=/run/cont .../containerd .... \_ /usr/bin/docker-proxy -proto tcp -host-ip ... -host-port 8080 \_ /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port /usr/bin/containerd-shim-runc-v2 -namespace moby -id \_ nginx: master process nginx -g daemon off; \_ nginx: worker process \_ nginx: worker process \_ nginx: worker process \_ nginx: worker process Что делает runc? Он собирает и запускает контейнер, а затем завершает работу, а его дочерние процессы переходят к его родительскому процессу, containerd-shim­ runc- v2. В памяти остается минимум кода, чтобы управлять дескрипторами файлов и кодами выхода для containerd. Чтобы лучше понять, что происходит, давайте посмотрим, как запускается контей­ нер. Мы вернемся к нашему контейнеру nginx, поскольку он очень легкий{ и кон­ тейнер продолжает выполняться в фоновом режиме: $ docker container 1s CONTAINER ID IМAGE. 08b5cffed7ba nginx:latest СОММАND "/docker-ent ..." PORTS 0.0.0.0:8080->80/tcp NAМES nginx-test Давайте посмотрим на систему под этим углом с помощью инструмента CLI среды выполнения runc. Мы видим почти то же, что и с точки зрения ctr, клиента CLI для containerd, но с runc удобнее работать, и он находится на нижнем уровне: $ sudo runc --root /run/docker/runtime-runc/moby list BUNDLE ID PID ... OWNER 08Ь5...53dd 5829 ... .../io.containerd.runtime.v2.task/moby/08b5...53dd ... root Обычно для выполнения этой команды нужны права root. В отличие от Docker CLI, мы не можем полагаться на разрешения демона Docker, чтобы получить доступ к функционалу на нижнем уровне. При использовании runc нам нужен прямой дос­ туп к этим привилегиям. В выводе runc мы видим наш контейнер. Это бандл (bundle) среды выполнения по стандартам OCI, который представляет наш контей­ нер с тем же идентификатором. Мы также получаем PID контейнера, и это PID на хосте для приложения, выполняющегося в контейнере: $ ps -edaf root systemd+ systemd+ systemd+ systemd+ grep 5829 5829 5807 5880 5829 5829 5881 5882 5829 5883 5829 nginx: master process nginx -g daemon off; nginx: worker process nginx: worker process nginx: worker process nginx: worker process Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции З 19 В нутри бандла мы увидим набор именованных каналов для нашего контейнера: $ sudo 1s -la /run/docker/containerd/08b5 ...53dd total О drwxr-xr-x 2 root root 80 Oct 1 08:49 . drwxr-xr-x 3 root root 60 Oct 1 08:49 .. prwx------ 1 root root О Oct 1 08:49 init-stderr prwx------ 1 root root О Oct 1 08:49 init-stdout В каталоге /run/containerd/io.containerd.runtime.v2.task/moby находится много до­ полнительных файлов, связанных с контейнером: $ sudo 1s -la /run/containerd/io.containerd.runtime.v2.task/moby/08b5 ...53dd/ total 32 drwx-----root 1 08:49 240 3 root Oct drwx--x--x 3 root root 60 Oct 1 08:49 1 08:49 root -rw-r--r-1 root 89 address Oct 1 08:49 root Oct -rw-r--r-1 root 9198 config.json root 4 init.pid -rw-r--r-Oct 1 08:49 1 root root prwx-----о 1 root Oct 1 08:49 log 1 08:49 root log.json -rw-r--r-1 root Oct о 1 08:49 root options.json 82 Oct -rw------1 root 1 rootfs 08:49 root 40 Oct drwx--x--x 2 root 1 08:49 runtime 4 root Oct -rw------1 root 32 Oct 1 08:49 shim-Ьinary-path -rw------- 1 root root lrwxrwxrwx 1 root root 119 Oct 1 08:49 work -> /var/liЬ/containerd/io ... Файл config.json - это очень подробный эквивалент того, что выводит Docker по команде docker container inspect. Он очень большой, поэтому здесь мы его не приво­ дим, но вы можете сами заглянуть в него и изучить конфигурацию. Например, обратите внимание на записи для режима Secure Computing Mode, который мы об­ суждали ранее в этой главе. Если хотите подробнее изучить runc, поэкспериментируйте с инструментом CLI. Большая часть уже доступна в Docker, и обычно это более общая и полезная ин­ формация, чем та, которую предоставляет runc. Рекомендуем изучить подробности, чтобы лучше понять работу контейнеров и стека Docker. Кроме того, будет инте­ ресно понаблюдать за событиями с выполняющимися контейнерами, о которых сообщает runc. Для просмотра событий у нас есть команда runc events. В ходе обыч­ ной работы контейнера в потоке событий мало что происходит, но runc регулярно передает статистику среды исполнения, которую мы можем просмотреть в формате JSON: $ sudo runc --root /run/docker/runtime-runc/moby events 08Ь5 ...53dd { "type":"stats", "id": "08Ь5 ... 53dd", "data": { "cpu": { "usage": { " ..." }}}} Для экономии места мы удалили большую часть вывода предыдущей команды, но это должно выглядеть знакомо, ведь мы уже выполняли команду docker container stats. Угадайте, откуда Docker по умолчанию берет эту статистику. Все верно, от runc. Остановите пример контейнера с помощью docker container stop nginx-test. Книги для программистов: https://clcks.ru/books-for-it 320 1 Глава 11 Альтернативные среды выполнения Как мы уже упоминали в главе 2, runc можно заменить другими нативными средами выполнения, подцерживающими стандарт OCI. Например, crun (https://github.com/ containers/crun), который авторы описывают как "быстрая и потребляющая мало памяти среда выполнения контейнеров по стандарту OCI, полностью написанная на С". Другие альтернативы, например railcar и rkt, уже устарели и практически не встречаются. В следующем разделе мы рассмотрим изолированную среду выпол­ нения от Google - gVisor (https://gvisor.dev/), которая предназначена для недове­ ренного кода. Kata Containers (https://github.com/kata-containers) - интересный опенсорс­ nроект, который предоставляет среду выполнения, использующую виртуальные машины как уровень изоляции для контейнеров. На момент написания Kata версии 3 работает с Kubernetes, но не с Docker. Разработчики Kata вместе с разработчиками Docker (https://github.com/kata-containers/kata-containers/issues/5321) сейчас работают над совместимостью и документацией. Возможно, проблема будет ре­ шена в публичном выпуске Docker 22.06. gVisor В середине 2018 года Google представил новый взгляд на среды выполнения, вы­ пустив инструмент gVisor. Он подцерживает стандарты OCI и может использовать­ ся с Docker. Однако gVisor также выполняется в пользовательском пространстве и изолирует приложения с помощью реализации системных вызовов там же, а не механизмов изоляции ядра. Он не перенаправляет вызовы в ядро, а реализует их сам с помощью вызовов ядра. Самое очевидное преимущество такого подхода улучшенная безопасность, поскольку сам gVisor выполняется в пользовательском пространстве, а значит, изолирован от ядра. Если возникнут проблемы с безопасно­ стью, они останутся в пользовательском пространстве, и все механизмы контроля безопасности в ядре, о которых мы говорили, продолжат действовать. Из недостат­ ков - производительность ниже, чем у решений, использующих ядро или вирту­ альные машины. Если вашим процессам не требуется большой масштаб, но нужна более безопасная изоляция, присмотритесь к gVisor. Обычно gVisor целесообразен, когда в контей­ нерах должен выполняться код, поступающий от конечных пользователей, и вы не можете быть уверены, что этот код безвреден. Давайте на примере посмотрим, как работает gVisor. Инструкции по установке см. в документации по gVisor (https://gvisor.dev/docs/ user_guide/quick_startfdocker). Он написан на Go и поставляется как один испол­ няемый файл, без пакетов. После установки контейнеры можно запустить в среде выполнения runsc. Чтобы продемонстрировать разные уровни изоляции в gVisor, мы запустим командную оболочку и сравним возможности со стандартным кон­ тейнером. Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 321 Для начала запустим командную оболочку для gVisor и осмотримся: $ docker container run --rm --runtirne=runsc -it alpine /bin/sh Мы получим командную оболочку в контейнере Alpine Linux. Самое заметное раз­ личие мы увидим в выводе команды mount: $ docker container run --rm --runtime=runsc -it alpine /Ьin/sh -с "mount" попе оп/type 9р (rw,trans=fd,rfdno=4,wfdno=4,aname=/,...) попеon /dev type tпpfs (rw,mode=0755) попе оп/sys type sysfs (ro,noexec,dentry_cache_limit= l000) попе оп/proc type proc (rw,noexec,dentry_cache_lirnit= l000) попе оп /dev/pts type devpts (rw,noexec) попеon /dev/shm type tпpfs (rw,noexec,mode= 1777,size=67108864) попе оп /etc/hosts type 9р (rw,trans=fd,rfdno=7,wfdno=7,...) попеon /etc/hostname type 9р (rw,trans=fd,rfdno=б,wfdno=б,...) попеon /etc/resolv.conf type 9р (rw,trans=fd,rfdno=5,wfdno=5,...) попon е /tпр type tпpfs (rw,mode=01777) Здесь мы не видим ничего особенно интересного. Давайте сравним этот вывод с тем, что мы получаем от традиционного контейнера в runc: $ docker container run --rm -it alpine /Ьin/sh -с "mount" overlay оп/type overlay (rw,relatime,...) proc оп /proc type proc (rw,nosuid,nodev,noexec,relatime) tпpfs оп/dev type tпpfs (rw,nosuid,size=65536k,mode=755,inode64) devpts оп/dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,...) sysfs оп/sys type sysfs (ro,nosuid,nodev,noexec,relatime) ·cgroup п о /sys/fs/cgroup type cgroup2 (ro,nosuid,nodev,noexec,relatirne) mqueue п о /dev/mqueue type mqueue (rw,nosuid,nodev,noexec,relatirne) shm оп/dev/shm type tпpfs (rw,nosuid,nodev,noexec,relatime,...) /dev/sdaЗ оп/etc/resolv.conf type ext4 (rw,relatime,errors=remount-ro) deyPts п о /dev/console type devpts (rw,nosuid,noexec,relatime,gid=5,...) proc оп /proc/bus type proc (ro,nosuid,nodev,noexec,relatime) tпpfs on /proc/asound type tпpfs (ro,relatime,inode64) В выводе было 24 строки, но здесь мы приводим не все. Как видите, данных очень много, и они показывают взгляд на ядро из контейнера. -Если сравнить это с не­ большим количеством данных от gVisor, разница в степени изоляции будет оче­ видна. Мы не будем вдаваться в подробности, но стоит изучить вьшод и для ip addr show. В gVisor: $ docker container run --rm --runtime=runsc alpine ip addr show 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65522 link/loopback 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff inet 127.0.0.1/8 scope global dynamic Книги для программистов: https://clcks.ru/books-for-it 322 Глава 11 2: eth0: <UP,LOWER_UP> mtu 1500 link/ether 02:42:ас:11:00:02 brd ff:ff:ff:ff:ff:ff inet 172.17.0.2/16 scope global dynamic В обычном контейнере Linux: $ docker container run --rm alpine ip addr show 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNКNOWN qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:0D:DD:00:D0:DD inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 44: ethD@if45: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP link/ether D2:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever Даже файловая система Linux /proc открывает гораздо меньше в контейнере gVisor: $ docker container run --rm --runtime=runsc alpine ls -С /proc 1 cgroups andline cpuinfo net self sentry-meminfo stat filesystems loadavg meminfo mounts sys thread-self uptime version Сравним с обычным контейнером Linux: $ docker container run --rm alpine ls -С /proc 1 acpi asound bootconfig buddyinfo bus cgroups andline consoles cpuinfo crypto devices diskstats dma driver dynamic_debug execdomains fЬ filesystems fs interrupts iomem ioports irq kallsyms kcore key-users keys kmsg kpagecgroup kpagecount kpageflags loadavg locks mdstat meminfo misc modules mounts щ:,t mtd mtrr net pagetypeinfo partitions pressure schedstat scsi self slaЬinfo softirqs stat swaps sys sysrq-trigger sysvipc thread-self timer list tty uptime version version signature vmallocinfo vmstat zoneinfo Книги для программистов: https://clcks.ru/books-for-it Расширенные концепции 1 323 gVisor и подобные изолированные среды позволяют более безопасно выполнять ненадежные рабочие нагрузки, обеспечивая барьер между приложением и нижеле­ жащим ядром. Заключение Мы кратко рассмотрели расширенные концепции Docker. Теперь вы лучше пони­ маете, как все работает и в какую сторону можно продолжать самостоятельное изу­ чение. Этих знаний будет достаточно, чтобы понимать, с чего начать реализацию Docker в своих системах. Книги для программистов: https://clcks.ru/books-for-it ГЛАВА 12 Развитие экосистемы Набор инструментов для работы с контейнерами Linux постоянно растет и развива­ ется, тем более что Kubemetes уже много лет продолжает набирать популярность. В этой главе мы кратко рассмотрим ряд инструментов, появившихся благодаря Docker и обычно предназначенных для специфических сценариев. Это не полный обзор, а несколько примеров, чтобы вы получили представление о некоторых дос­ тупных вариантах. Клиентские инструменты В этом разделе мы рассмотрим три инструмента командной строки: nerdctl, podman и buildah. Эти инструменты могут быть полезны тем, кто знаком с Docker и его стан­ дартными рабочими процессами. nerdctl Хотя по умолчанию во многих средах на базе containerd устанавливается crictl 1, стоит обратить внимание на nerdctl, простой в использовании CLI, совместимый с Docker, для containerd. nerdctl предлагает очень простой путь миграции для поль­ зователей и скриптов, которые применяют Docker, но поддерживают системы containerd без демона Docker. Если мы запустим небольшой кластер Kubemetes с kind, который мы рассматривали в разделе 11Kind11 главы 1 О, то получим кластер Kubemetes на базе containerd, с кото­ рым мы не сможем напрямую взаимодействовать через Docker CLI: $ kind create cluster --name nerdctl Creating cluster "nerdctl" ... $ docker container ехес -ti nerdctl-control-plane /bin/bash Мы вошли в контейнер kind/KuЬernetes. . В следующей команде curl мы должны загрузить подходящую версию для нашей архитектуры. Замените $ {ARCHJ на amd64 или arrn64, в зависимости от используемой системы. Попробуйте загрузить последнюю версию nerdctl (https://github.com/ containerd/nerdctl/releases). 1 Полный URL: https://github.com/kubernetes-sigs/cri-tools/ЫoЬ/master/docs/crictl.md. Книги для программистов: https://clcks.ru/books-for-it Развитие экосистемы 325 Оrредактировав следующую команду curl и собрав ее в одну строку, вы сможете загрузить и извлечь клиент nerdctl, а затем выполнить несколько команд: root@nerdctl-control-plane:/t curl -s -1 \ "https://githuЬ.com/containerd/nerdctl/releases/download/v0.23.0/\ nerdctl-0.23.0-linux-${ARCH).tar.gz" -о /trrp/nerdctl.tar.gz root@nerdctl-control-plane:/1 tar -С /u_sr/local/Ьin -xzf /trrp/nerdctl.tar.gz root@nerdctl-control-plane:/t nerdctl namespace list NАМЕ k8s.io CONTAINERS 18 IМAGES VOLUMES LAВELS 24 О root@nerdctl-control-plane:/1 nerdctl --namespace k8s.io container list CONTAINER ID IМAGE NAМES 07ae69902dll registry.k8s.io/pause:3.7 Ob24ldЬ0485f registry.k8s.io/coredns/coredns:vl.9.3 k8s://kuЬe-system/core .. . k8s://kuЬe-system/core... root@nerdctl-control-plane:/1 nerdctl --namespace k8s.io container run --rm \ --net;host deЬian sleep 5 docker.io/library/debian:latest: index-sha256:e538 ...4bff: manifest-sha256:9b0e...2f7d: config-sha256:d917...d33c: layer-sha256:f606...5ddf: elapsed: 6.4 s root@nerdctl-control-plane:/1 exit resolved done done done done total: 1+++++++++++++++++++++++++++1 1+++++++++++++++++++++++++++1 1+++++++++++++++++++++++++++1 1+++++++++++++++++++++++++++1 1+++++++++++++++++++++++++++1 52.5 М (8.2 MiB/s) В большинстве случаев команды docker можно использовать в nerdctl почти без изменений. Единственное заметное отличие - необходимость часто указывать значение пространства имен. Это связано с тем, что containerd предоставляет API в пространстве имен (https:// github.com/containerd/containerd/ЫoЬ/main/docs/namespaces.md), и нужно указы­ вать, с каким именно API мы собираемся взаимодействовать. Выйдя из контейнера kind, мы можем его удалить. $ kind delete cluster --name nerdctl Deleting cluster "nerdctl" ... podman и buildah Утилиты podman (https://podman.io/) и Ьuildah (https://buildah.io/) - это инструмен­ ты от Red Hat, которые были созданы на ранних этапах использования контейне­ ров, чтобы обходиться без демона вроде Docker. Они широко применяются сооб­ ществом Red Hat и предоставляют иной подход к сборке образов и выполнению контейнеров. Читайте подробнее о podman и buildah для пользователей Docker в блоге Red Hat (https://developers.redhat.com/blog/2019/02/21/podman-and-buildah-for-docker­ ; users). Книги для программистов: https://clcks.ru/books-for-it 326 Глава 12 $ kind create cluster --name podman Creating cluster "podman" ... $ docker container ехес -ti podman-control-plane /bin/bash � Инс,рукции по установке и исполозоваsию kind см. в paзiJene "Кind" гпавы 10. Мы находимся в контейнере kind/KuЬernetes: root@podman-control-plane:/# apt update Get:l http://security.uЬuntu.com/uЬuntu jammy-security InRelease (110 kВ] root@podman-control-plane:/# apt install -у podman Reading package lists... Done root@podman-control-plane:/# podman container run -d --rm --name test debian sleep 120 9bбb333313c0d54e2daбcda49f2787bc521368ld90dacl45a9f64128f3el8631 root@podman-control-plane:/# podman container list CONTAINER ID IМAGE СОММАND NAМES test 548a2f709785 docker.io/library/debian:latest sleep 120 root@podman-control-plane:/# podman container stop test test В отличие от docker (который работает с демоном Docker) и nerdctl (который рабо­ тает с containerct), podman обходится без контейнерного движка и напрямую взаимо­ действует со средой выполнения контейнеров, например runc. Хотя мы можем собирать контейнеры с помощью podman build, buildah предоставляет расширенный интерфейс для сборки образов, так что можно написать весь процесс в скрипте и не использовать формат Dockerfile (или Containerfile в терминологии podman). Мы не будем подробно рассматривать buildah, но вы можете попробовать простой пример с контейнером kind, и если вас интересуют альтернативы традиционному подходу с Dockerfile или новые подходы, предоставляемые интерфейсом LLB BuildКit (https://github.com/moby/Ьuildkit#exploring-llb), читайте о buildah на GitHub (https://github.com/containers/Ьuildah) и в блоге Red Hat (https://www.redhat.com/ sysadmin/building-buildah). Попробуйте запустить скрипт buildah в контейнере kind, выполнив следующие команды: root@podman-control-plane:/# cat > apache.sh «"EOF" #!/usr/bin/env bash set -х ctrl=$(buildah from "${1:-fedora}") ## Получить все обновления и установить сервер apache buildah run "$ctrl" -- dnf update -у buildah run "$ctrl" -- dnf install -у httpd Книги для программистов: https://clcks.ru/books-for-it Развитие экосистемы 327 ## Включить аннотации на этапе сборки buildah config --anпotatioп "com.example.build.host;$(uпame -n)" "$ctrl" ## Запустить сервер и открыть порт buildah config --cmd "/usr/sЬin/httpd -D FOREGROUND" "$ctrl" buildah config --port 80 "$ctrl" ## Закоммитить контейнер с именем образа buildah commit "$ctrl" "${2:-myrepo/apache)" EOF root@podman-control-plane:/# chmod +х apache.sh root@podman-control-plane:/# ./apache.sh ++ buildah from fedora + ctrl;fedora-working-container-1 + buildah run fedora-working-container-1 -- dnf update -у Writing manifest to image destination Storing signatures 037c7a7c532a47be67f389d7fd3e4bbba64670e080Ы20d93744el47df5adf26 root@podman-control-plane:/# exit Выйдя из контейнера kind, мы можем его удалить. $ kind delete cluster --name podman Deleting cluster "podman" Универсальные инструменты разработчика Несмотря на все преимущества Docker Desktop, изменения в лицензировании Docker, а также развитие технологий приводят к тому, что некоторые люди и орга­ низации ищут альтернативные инструменты. В этом разделе мы рассмотрим Rancher Desktop и Podman Desktop, которые предоставляют часть функционала Docker Desktop, а также собственные интересные функции. Rancher Desktop Rancher Desktop (https://rancherdesktop.io/) похож на Docker Desktop и предназна­ чен специально для интеграции с Kubemetes. Он использует kЗs (https://k3s.io/) как сертифицированный легкий бэкенд Kubemetes и containerd или dockerd ( moby) в каче­ стве среды выполнения контейнеров. Выйдите из Docker (и/или Podman) Desktop, если один из них запущен, прежде чем экспериментировать с Rancher Desktop, поскольку все они работают на виртуаль­ ной машине, которая потребляет системные ресурсы. После загрузки, установки и запуска Rancher Desktop мы получим локальный кла­ стер Kubemetes, который по умолчанию использует containerd и управляется через nerdctl. Книги для программистов: https://clcks.ru/books-for-it 328 1 Глава 12 Папка, в которую Rancher Desktop устанавливает файл nerdctl, зависит от опера­ ционной системы. Убедитесь, что используете версию, которая упакована с Rancher Desktop. $ ${HOМE)/.rd/bin/nerdctl --namespace k8s.io irnage list REPOSITORY TAG IМAGE ID ... PLATFORМ moby/buildkit v0.8.3 171689е43026 ... linux/amd64 moby/buildkit <none> 171689е43026 ... linux/amd64 SIZE 119.2 MiB 119.2 MiB В10В SIZE 53.9 MiB 53.9 MiB Не забудьте выйти из Rancher Desktop, когда закончите, иначе виртуальная машина продолжит потреблять ресурсы. Podman Desktop Podman Desktop (https://podman-desktop.io/) не использует демон, но предоставля­ ет удобный инструмент, привычный разработчикам во всех популярных операци­ онных системах. Выйдите из Docker (и/или Rancher) Desktop, если один из них запущен, прежде чем экспериментировать с Podman Desktop, поскольку все они работают на виртуаль­ ной машине, которая потребляет системные ресурсы. После загрузки, установки и запуска Podman Desktop откроется окно приложения на вкладке Ноше. Если Podman Desktop не найдет Podman CLI в системе, он пред­ ложит его установить. Нажмите на кнопку Install (Установить), чтобы установить клиент podrnan. Когда виртуальная машина Podman Desktop, которой можно управ­ лять в командной строке через podrnan rnachine, не запущена, щелкните кнопку Run Podman (Запустить) и подождите. Появится сообщение Podman is running (Podman запущен). Папка, в которую Podman Desktop устанавливает файл podrnan, зависит от операци­ онной системы. Убедитесь, что используете версию, которая устанавливается че­ рез Podman Desktop. Давайте посмотрим, что у нас получилось: $ podrnan run quay.io/podrnan/hello ! ... Hello Podrnan World ... ! --"-/ - ~~~ 1 / (О) ~/ о о\ 1 =(Х)= 1~ -\ (О) \ -= ( , У, ) =- 1 / \ 1~~ ~1 ~~~1 / (О (О) \ =(У_)=u 1 Книги для программистов: https://clcks.ru/books-for-it Развитие экосистемы 1 329 Project: https://githuЬ.com/containers/podman Website: https://podman.io Documents: https://docs.podman.io Twitter: @Poclman io Когда закончите экспериментировать с Podman Desktop, завершите работу вирту­ альной машины: на вкладке Preferences (Настройки) выберите разделы Resources (Ресурсы)- Podman - Podman Machine (Машина Podman) и нажмите кнопку Stop. Теперь можно выйти из приложения Podman Desktop. � � Запускать и останавливать виртуальную машину Podman можно с помощью команд podman machine start и podman machine stop. Заключение Docker уже занимает прочное место в истории технологий, ведь он дополнил суще­ ствующие контейнеры Linux концепцией образов, и с его появлением инженеры по всему миру получили удобный доступ к этим технологиям. Мы можем спорить о том, стало ли сейчас лучше, чем было до контейнеров Linux и Docker, а также о том, какие инструменты и рабочие процессы лучше, но в итоге все зависит от того, как применять эти инструменты и организовьmать эти про­ цессы. Ни один инструмент не сможет волшебным образом решить все проблемы, и если применять его неправильно, станет только хуже. Поэтому так важно тщательно об­ думать рабочие процессы как минимум с трех точек зрения. Во-первых, что должен поддерживать рабочий процесс на входе и на выходе. Во-вторых, насколько легко будет людям, которым придется использовать этот рабочий процесс каждый день или раз в год. В-третьих, насколько легко запускать и поддерживать этот процесс, чтобы обеспечить беспроблемную и безопасную работу сис:гемы. Как только вы хорошо себе представите, чего пытаетесь достичь, вы сможете подобрать инструменты, подходящие для этих целей. Книги для программистов: https://clcks.ru/books-for-it ГЛАВА 13 Устройство контейнерной платформы Реализуя любую технологию в рабочей среде, всегда учитывайте возможность сис­ темы противостоять неожиданным проблемам, которые неизбежно возникнут. Docker предлагает широкие возможности, но требует внимания к деталям при создании полноценной платформы. Как любая другая технология, которая быстро развивается, он не обходится без неприятных ошибок, проявляющихся в разных компонентах контейнерной платформы. Если мы не будет торопиться развертывать Docker в имеющейся среде, а тщательно выстроим контейнерную платформу вокруг него, то сможем использовать много­ численные преимущества контейнеров, при этом защитившись от некоторых рис­ ков, присущих быстро развивающимся проектам. Ни одна технология не решит все ваши проблемы как по волшебству, и Docker не исключение. Чтобы реализовать его потенциал, необходимо тщательно продумать, как и почему следует его применять. В маленьких проектах Docker можно вопло­ тить без · лишних сложностей, но если вы планируете поддерживать крупную сис­ тему с масштабированием по требованию, необходимо осознанно подходить к про­ ектированию приложений и платформы, чтобы максимально эффективно вложить инвестиции в технологии. Если изначально спроектировать платформу правильно, впоследствии будет гораздо проще адаптировать рабочий процесс. Хорошо проду­ манная контейнерная платформа и процесс развертывания отличаются легкостью и лаконичностью, но при этом соответствуют всем техническим и регулятивным тре­ бованиям. Приложение должно выполняться на динамическом фундаменте, кото­ рый легко можно модернизировать вместе с развитием технологий и процессов в организации. В этой главе мы рассмотрим два документа: The Twelve-Factor Арр (Приложение двенадцати факторов, https://12factor.net/) и The Reactive Manifesto (Манифест ре­ активных систем, https://www.reactivemanifesto.org/), приложение к The Reactive Principles (Принципы реактивных систем, https://www.reactiveprinciples.org/), и обсудим, как они применяются к Docker и созданию надежных контейнерных платформ. Оба документа содержат интересные идеи, которые помогут вам в про­ ектировании и реализации отказоустойчивой и удобной в сопровождении контей­ нерной платформы. Книги для программистов: https://clcks.ru/books-for-it Устройство контейнерной платформы 331 Приложение двенадцати факторов В ноябре 2011 года, задолго до выпуска Docker, сооснователь Heroku Адам Циггинс с коллегами выпустил статью под названием The Twelve-Factor Арр. В этом доку­ менте описываются 12 методов, сформулированных на основе опыта инженеров Heroku (https://www.heroku.com/) и применимых к проектированию успешного приложения для современной среды SaaS на основе контейнеров. Мы не обязаны учитывать все 12 факторов при создании приложения, но такие приложения будут идеально сочетаться с Docker. В этой главе мы рассмотрим каж­ дый фактор и узнаем, как они поддерживают цикл разработки: ♦ Кодовая база. ♦ Зависимости. ♦ Конфигурация. ♦ Сторонние службы. ♦ Сборка, выпуск, выполнение. ♦ Процессы. ♦ Привязка портов. ♦ Параллелизм. ♦ Утилизируемость. ♦ Паритет среды разработки и рабочей среды. ♦ Журналы. ♦ Администрирование. Кодовая база В системе контроля версий должна быть одна кодовая база. У одного приложения может одновременно выполняться несколько экземпляров, но все они должны происходить из одного репозитория. Каждый образ Docker для приложения должен собираться из единого репозитория исходного кода, в котором содержится весь код, необходимый для сборки контейнера Linux. При таком под­ ходе код легко можно будет собирать повторно, а все сторонние зависимости будут определяться в репозитории и автоматически подгружаться при сборке. Это означает, что для сборки приложения нам не придется совмещать код из раз­ ных источников. У нас могут быть зависимости от программных пакетов из друго­ го репозитория, просто нам нужен четкий механизм определения того, из какого кода мы собрали приложения. Docker упрощает управление зависимостями, но это не поможет, если при сборке приложения мы извлекаем и сшиваем код из многих репозиториев. Такой процесс гораздо хуже воспроизводится, ведь мы должны знать конкретное "заклинание", чтобы заставить систему работать. Для проверки дайте новому разработчику в компании чистый ноутбук и документ с указаниями, а затем посмотрите, сможет ли он собрать ваше приложение меньше Книги для программистов: https://clcks.ru/books-for-it 332 1 Глава 13 чем за час. Если не сможет, скорее всего, процесс нужно оптимизировать и упро­ стить. Зависимости Зависимости должны быть явно объявленными и изолированными. Не стоит надеяться, что зависимость будет предоставляться каким-то иным спосо­ бом, например через установку в операционной системе. Все зависимости, которые нужны вашему приложению, должны быть хорошо определены в базе кода, чтобы их можно было получать в процессе сборки. При таком подходе приложение смо­ жет выполняться после развертывания, не ожидая, пока другие люди или процессы установят нужные библиотеки. Это особенно важно при использовании контейне­ ров, ведь процессы в контейнерах изолированы от операционной системы хоста, и обычно у них нет доступа к компонентам за пределами ядра хоста и файловой системы образа контейнера. Dockerfile и файлы конфигурации для определенного языка, вроде Node package.json или Ruby Gemfile, должны определять все внутренние зависимости, необходимые приложению, чтобы образ выполнялся корректно в любой системе, в которой будет развернут. Больше не нужно переживать, что после развертывания приложения в рабочей среде вдруг обнаружится, что важные библиотеки отсутствуют или уста­ новлены не в тех версиях. Такой подход гарантирует высокую степень надежности и воспроизводимости, а также безопасности системы. Если для исправления про­ блемы с безопасностью мы обновляем OpenSSL или библиотеки libyaml, исполь­ зуемые контейнеризированнь1м приложением, то можем быть уверены, что прило­ жение всегда и в любой среде будет развертываться с нужной версией. Важно отметить, что многие базовые образы Docker весят больше, чем нужно. Процесс приложения будет выполняться на разделяемом ядре, и в образе должны быть только те файлы, которые требуются процессу. Удобно, когда базовые образы легкодоступны, но иногда они могут маскировать зависимости. Разработчики часто начинают с минимальной установки Alpine, Ubuntu или Fedora, но эти образы все равно содержат много файлов и приложений операционной системы, которые не нужны процессу. О некоторых файлах, используемых приложением, вы можете даже не знать, например приложение компилируется с помощью системной биб­ лиотеки musl в Alpine и glibc во многих других базовых образах. Мы должны по­ нимать все зависимости, даже при контейнеризации приложений. Кроме того, нуж­ но помнить о вспомогательных инструментах, которые могут входить в образы. Иногда трудно найти баланс между простотой отладки и сокращением поверхности атаки для приложений и окружений. Чтобы узнать, какие файлы требуются в образе, можно сравнить небольшой базо­ вый образ с образом для статически скомпонованной программы, написанной на Go или С. Эти приложения можно спроектировать таким образом, чтобы запускать напрямую в ядре Linux без дополнительных библиотек или файлов. Чтобы лучше понять этот принцип, вернитесь к упражнениям в разделе "Контроль размера образов" главы 4, где мы рассмотрели сверхлегкий контейнер spkane/ Книги для программистов: https://clcks.ru/books-for-it Устройство контейнерной платформы 1 333 scratch-helloworld, а затем изучили базовую файловую систему и сравнили его с по­ пулярным базовым образом alpine. Мы должны не только продумать управление слоями файловой системы в образах, но и удалить из образа все, кроме самого необходимого, чтобы система работала эффективнее, а команды docker image pull выполнялись быстро. Для приложений, написанных на интерпретируемых языках, потребуется больше файлов, потому что нужно будет устанавливать большие среды выполнения и графы зависимостей, но старайтесь поддерживать минимальный базовый слой, чтобы контролировать зави­ симости. Docker помогает упаковывать их, но продумывать подход все равно при­ дется вам. Конфигурация Храните конфигурацию в переменных среды, а не в файлах в репозитории. При таком подходе будет проще развертывать одну и ту же кодовую базу в разных окружениях, например в промежуточной или в рабочей, не поддерживая сложную конфигурацию в коде и не пересобирая контейнер для каждой среды. Кодовая база будет выглядеть гораздо аккуратнее, если в ней не будут храниться имена баз дан­ ных или пароли. Что еще важнее, нам не придется хранить в репозитории предпо­ лагаемые настройки среды развертывания, поэтому приложения будет очень просто развертывать где угодно. Кроме того, у нас будет возможность протестировать именно тот образ, который мы будем поставлять в рабочую среду. Это бьmо бы не­ возможно, если бы пришлось собирать отдельный образ для каждого окружения со всеми соответствующими конфигурациями. Как мы уже видели в главе 4, для этого можно вьmолнить команду docker container run с аргументом командной строки -е. Строка -е АРР_ENV=production указывает Docker задавать для переменной окружения АРР_ENV значение production в каждом новом контейнере. В реальном мире мы могли бы извлечь образ для чат-бота Hubot с установленным Rocket.Chat (https://www.rocket.chat/). Запустить контейнер можно следующей командой: $ docker container run \ --rm --пате huЬot -d \ •-е ENVIRONМENT="development" \ -е RОСКЕТСНАТ URL='rocketchat:3000' \ -е ROCKETCНAT_ROOM=' general' \ -е RESPOND ТО DM=true \ -е RОСКЕТСНАТ USER=bot \ -е RОСКЕТСНАТ PASSWORD=bot \ -е ROCKETCНAT_AUTH=password \ -е вот NAME=bot \ -е EXTERNAL_SCRIPTS=huЬot-pugme,huЬot-help \ docker.io/rocketchat/huЬot-rocketchat:latest Книги для программистов: https://clcks.ru/books-for-it 334 Глава 13 Здесь мы передаем целый набор переменных среды в контейнер при создании. Когда процесс в контейнере запускается, он получает доступ к переменным среды для правильной настройки в среде вьщолнения. Эти компоненты конфигурации теперь представляют собой внешнюю зависимость, которую можно внедрить во время выполнения. Есть много других способов передать эти данные контейнеру, включая хранилище ключей и значений, вроде etcd и consul. Переменные среды - это универсальный вариант и удобная отправная точка для большинства проектов. С их помощью можно легко настраивать контейнер, потому что они хорошо поддерживаются платформой и всеми популярными языками программирования. Кроме того, они улучшают наблюдаемость приложений, потому что конфигурацию можно легко проверить, выполнив команду docker container inspect. Если это приложение Node.js, вроде huЬot, затем можно написать следующий код, чтобы принимать решения на основе этих переменных среды: switch(process.env.ENVIRONMENT) { case 'development': console.log(' [INFO) Running in development'); case 'staging' : console.log(' [INFO] Running in staging'); case 'production': console.log(' [INFO) Running in production'); default: console.log(' [WARN) Environment value is unknown'); Точный метод передачи этих данных конфигурации в контейнер зависит от инст­ рументов, выбранных для проекта, но почти все они позволяют гарантировать, что каждое развертывание будет содержать необходимые настройки для среды. Когда конфигурация хранится не в исходном коде, мы легко можем развертывать совершенно одинаковые контейнеры в разных окружениях, ничего не меняя и не храня конфиденциальную информацию в репозитории. Кроме того, мы можем тща­ тельно тестировать образы контейнеров, прежде чем развертывать их в рабочей среде, используя один и тот же образ во всех средах: $ docker container stop huЬot Если вам нужен процесс для управления секретами в контейнерах, изучите доку­ ментацию (https:1/docs.docker.com/engine/swann/secrets) по команде docker ; secret, которая работает с режимом Docker Swarm и HashiCorp Vault (https:l/www. vaultproject.io/). Сторонние службы Сторонние службы следует считать подкточаемыми ресурсами. Локальные базы данных не считаются более надежными, чем сторонние сервисы, и их тоже следует отнести к этой категории. Приложения должны корректно обКниги для программистов: https://clcks.ru/books-for-it Устройство контейнерной платформы 1 335 рабатывать потерю подключенного ресурса. Когда мы реализуем «изящную» деградацию (graceful degradation) в приложении и не надеемся на постоянную дос­ тупность всех ресурсов, включая файловую систему, наше приложение будет выполнять столько функций, сколько сможет, даже если внешние ресурсы будут недоступны. Docker не решает эти задачи напрямую, и хотя мы всегда должны продумывать на­ дежность сервисов, при использовании контейнеров это особенно важно. В контей­ нерах мы чаще всего достигаем высокой доступности с помощью горизонтального масштабирования и последовательного развертывания, не применяя динамическую миграцию долгосрочных процессов, как в традиционных виртуальных машинах. Это значит, что конкретные экземпляры сервиса будут появляться и исчезать, и приложение должно корректно обрабатывать эту смену. Кроме того, поскольку у контейнеров Linux ограничены ресурсы файловой систе­ мы, мы не можем надеяться, что нам будет доступно локальное хранилище, и должны явно реализовать эту потребность в зависимостях приложения. Сборка, выпуск, выполнение Этапы сборки, выпуска и выполнения должны быть строго разделены. Соберите код, выпустите его с соответствующей конфигурацией, а затем разверни­ те. Это позволяет контролировать процесс и выполнять каждый этап, не запуская весь рабочий процесс. Когда каждый шаг выполняется отдельно, мы быстрее полу­ чаем обратную связь и реагируем на потенциальные проблемы. В рабочем процессе Docker мы должны четко разделять каждый этап развертыва­ ния. Да, мы можем одним щелчком запускать процесс сборки, тестирования и раз­ вертывания, если полностью доверяем тестам, но не должно быть необходимости пересобирать контейнер только для того, чтобы развернуть его в другой среде. Docker поддерживает этот фактор, потому что реестр образов позволяет удобно передать образ с этапа сборки на этап поставки в рабочую среду. Если процесс сборки создает образы и отправляет их в реестр, при развертывании можно просто извлекать эти образы на серверы и запускать. Процессы Запускайте приложение в виде одного или нескольких процессов без сохранения состояния. Все данные следует хранить в стороннем хранилище, чтобы экземпляры приложе­ ния можно было легко развертывать повторно, не теряя важные данные сеанса. Не стоит хранить критичные состояния на диске в эфемерном контейнере или в памяти одного из его процессов, а контейнеризированные приложения всегда следует счи­ тать эфемерными. В по-настоящему динамической контейнерной среде контейнеры можно уничтожать и создавать повторно по желанию. Такая гибкость поддержива­ ет быстрое развертывание и восстановление после сбоев, как того требуют совре­ менные рабочие процессы Agile. Книги для программистов: https://clcks.ru/books-for-it 336 1 Глава 13 Насколько это возможно, создавайте приложения, которые хранят состояние столь­ ко времени, сколько необходимо для обработки и ответа на один запрос. В таком случае при остановке одного контейнера неприятные последствия будут мини­ мальными. Если хранить состояние необходимо, используйте удаленное хранили­ ще, вроде Redis, PostgreSQL, Memcache или даже Amazon SЗ, в зависимости от тре­ бований к отказоустойчивости. Привязка портов Предоставляйте сервисы через привязку портов. У приложения должен быть отдельный порт, по которому к нему можно обращать­ ся. Приложения должны быть привязаны к порту напрямую, не полагаясь на внеш­ ний демон наподобие inetd. Мы должны быть уверены в том, что через этот порт обращаемся к самому приложению. Большинство современных веб-платформ могут напрямую привязываться к порту и обслуживать собственные запросы. Чтобы открыть порт из контейнера, как описано в главе 4, можно выполнить ко­ манду docker container run с аргументом командной строки --puЬlish. Если, напри­ мер, указать --puЬlish mode=ingress, puЫished=BO, target=BOBO, Docker будет знать, что порт контейнера 8080 нужно проксировать на порт хоста 80. Статически скомпонованный контейнер Go Hello World из раздела "Контроль раз­ мера образов" главы 4 - отличный пример такого подхода, потому что в контей­ нере нет ничего, кроме приложения, которое поставляет контент в браузер. Нам не пришлось включать в сборку другие веб-серверы, которые потребовали бы допол­ нительной конфигурации, усложнили систему и увеличили число потенциальных точек отказа. Параллелизм Масштабирование должно осуществляться через процессы. Приложения должны поддерживать параллелизм и горизонтальное масштабирова­ ние. Изменять количество ресурсов для существующего экземпляра сложно. Гораз­ до проще будет добавлять и удалять экземпляры, поддерживая гибкость инфра­ структуры. Чтобы запустить еще один контейнер на новом сервере, потребуется несравнимо меньше усилий и затрат по сравнению с добавлением ресурсов для ба­ зовой виртуальной или физической системы. Возможности горизонтального мас­ штабирования позволяют платформе гораздо быстрее реагировать на меняющиеся потребности в ресурсах. В главе 1 О мы видели пример того, как легко можно масштабировать сервис с по­ мощью режима Docker Swarm, просто выполнив команду: $ docker service scale myservice=B Тут очень кстати будут такие инструменты, как режим Docker Swarm, Mesos и Kubernetes. Реализовав кластер Docker с динамическим планировщиком, мы можем легко добавить еще три экземпляра контейнера в кластер при увеличении нагрузки, Книги для программистов: https://clcks.ru/books-for-it Устройство контейнерной платформы 1 337 а затем так же легко удалить два экземпляра приложения, когда нагрузка снова спадет. Утил изируемость Возможность быстрого запуска и корректного завершения работы повышает устой­ чивость приложения. Сервисы должны быть задуманы как эфемерные. Мы уже говорили об этом, обсуж­ дая хранение состояния за пределами контейнера. Приложение должно уметь быст­ ро запускаться и завершаться, чтобы поддерживать динамическое горизонтальное масштабирование, последовательные развертывания и реагирование на неожидан­ ные проблемы. Сервисы должны корректно реагировать на сигнал SIGTERМ от опера­ ционной системы и уверенно обрабатывать аппаратные отказы. Что еще важнее, мы не должны волноваться о работоспособности отдельных контейнеров. Пока прило­ жение обслуживает запросы, разработчики не должны думать о состоянии каждого компонента системы. Нам нужна возможность легко отключать или повторно раз­ вертывать неисправные узлы, не тратя время на планирование этих операций и не следя за работоспособностью остальной части кластера. В главе 7 мы видели, что Docker отправляет стандартные сигналы Unix контейне­ рам, когда останавливает или завершает их. Получается, любое контейнеризиро­ ванное приложение может определять эти сигналы и принимать необходимые меры для их корректного завершения. Паритет среды разработки и рабочей среды Среда разработки, промежуточная и рабочая среда должны быть максимально по­ хожи друг на друга. Используйте одинаковые процессы и программные пакеты для сборки, тестирова­ ния и развертывания сервисов во всех средах. Одни и те же люди должны работать над всеми средами, и с физической точки зрения все среды должны как можно больше походить друг на друга. Очень важно обеспечить воспроизводимость. Поч­ ти любая проблема, которую мы обнаруживаем в рабочей среде, указывает на сбой в процессе. Если в каком-то аспекте рабочая среда отличается от промежуточной, этот аспект становится источником дополнительных рисков. Из-за подобных рас­ хождений проблемы в рабочей среде станут заметны, когда предотвращать их уже будет слишком поздно. Эта рекомендация перекликается с некоторыми из предыдущих, но здесь особое внимание уделяется рискам, возникающим из-за различий между средами, и хотя такие различия характерны для многих организаций, в контейнеризированной ин­ фраструктуре их проще избежать. Серверы Docker можно создавать идентичными для всех сред, а изменения конфигурации в разных средах обьi4но затрагивают только конечные точки, к которым подключается сервис, а не поведение прило­ жения. Книги для программистов: https://clcks.ru/books-for-it 338 Глава 13 Журналы Рассматривайте журналы как потоки событий. Сервисы не должны отвечать за пересылку или хранение журналов. Пусть события поступают без буферизации в потоки sтооuт и STDERR и обрабатываются за пределами приложения. В среде разработки потоки sтооuт и STDERR можно легко просмотреть, а в промежуточной и рабочей среде их можно перенаправлять куда угодно, в том числе в центральный сервис журналирования. У разных сред свои особенности об­ работки журналов, и эту логику не следует записывать в приложение. Когда все события поступают в потоки sтооuт и STDERR, менеджер процессов верхнего уровня может обрабатывать журналы тем методом, который лучше подходит для той или иной среды, чтобы разработчики могли сосредоточиться на основном функционале приложения. В главе 6 мы рассматривали команду ctocker container logs, которая собирает выход­ ные данные из потоков sтооuт и STDERR контейнера и записывает их в журналы. Если хранить журналы в виде случайных файлов в файловой системе контейнера, полу­ чить к ним доступ будет трудно. Мы можем настроить Docker так, чтобы отправ­ лять журналы в локальную или удаленную систему журналирования с помощью таких инструментов, как rsyslog, journald или fluentd. Если мы используем на серверах менеджер процессов или систему инициализации, вроде systemd или upstart, то можем легко направить вывод процесса в sтооuт и STDERR, откуда монитор процессов будет записывать их и отправлять на удаленный хост журналирования. Администрирование Задачи администрирования/управления должны выполняться как однократные процессы. Однократные задачи администрирования должны выполняться из той же кодовой базы и конфигурации, которые использует приложение. Это позволяет избежать проблем с синхронизацией и расхождениями в коде и схеме. Инструменты управ­ ления часто существуют в виде одноразовых скриптов или находятся в другой ко­ довой базе, хотя гораздо безопаснее создавать инструменты управления в кодовой базе приложения и задействовать одинаковые библиотеки и функции для выполне­ ния нужных задач. При таком походе надежность возрастает, потому что инстру­ менты ориентированы на те же пути выполнения кода, что и приложение. На практике это означает, что нам не следует выполнять задачи по администриро­ ванию и обслуживанию с помощью случайных скриптов в стиле cron. Гораздо эф­ фективнее будет включить все эти скрипты и функции в кодовую базу приложения. Если они не должны выполняться в каждом экземпляре приложения, мы можем запускать специальный краткосрочный контейнер или команду docker container ехес в работающем контейнере, чтобы выполнить задачу по обслуживанию. Эта коман­ да активирует задание, сообщает о его статусе и завершает работу. Книги для программистов: https://clcks.ru/books-for-it Устройство контейнерной платформы 339 Заключение по 12 факторам Манифест The Twelve-Factor Арр написан не конкретно про Docker, но все эти ре­ комендации можно применить к разработке и развертыванию приложений на плат­ форме Docker. Отчасти это связано с тем, что статья определила развитие самого Docker, а также с тем, что манифест повлиял на многие подходы, которых придер­ живаются современные архитекторы программного обеспечения. Манифест реактивных систем В июле 2013 года сооснователь и технический директор Typesafe Джонас Бонер (Jonas Boner) выпустил документ под названием The Reactive Manifesto (https:// www.reactivemanifesto.org/). В сотрудничестве с коллективом авторов он сформу­ лировал манифест о том, как в последние годы менялись ожидания относительно устойчивости приложений и как приложения должны предсказуемо реагировать на разные формы взаимодействия, включая события, пользователей, нагрузку и сбои (https://www.lightbend.com/Ыog/why-do-we-need-a-reactive-manifesto). В The Reactive Manifesto реактивными системами называются отказоустойчивые, масштабируемые и событийно-ориентированные системы с быстрым откликом. Быстрый отклик Система отвечает сразу, если вообще может ответить. Это значит, что приложение должно очень быстро реагировать на запросы. Пользо­ ватели не хотят ждать, и у нас редко бывают обоснованные причины медлить. Если мы создаем контейнеризированный сервис для рендеринrа больших РDF-файлов, он должен сразу отвечать пользователю, что получил задание, чтобы пользователь мог заниматься своими делами, пока не получит еще одно сообщение о том, что задача выполнена, а итоговый PDF можно загрузить по такому-то адресу. Отказоустойчивость Сис�:ема должна реагировать на запросы даже в случае сбоя. Когда по какой-то причине произойдет сбой, будет хуже, если сервис перестанет отвечать. Лучше корректно обработать сбой и динамически сократить функционал приложения или хотя бы показать пользователю простое и понятное сообщение о проблеме, а также сообщить о сбое команде сопровождения. Масштабируемость Система должна оставаться отзычивой при изменчивых рабочих нагрузках. В Docker мы для этого динамически развертываем и завершаем контейнеры по мере изменения спроса, так что приложение всегда быстро обрабатывает запросы сервера, но при этом не расходует ресурсы зря. Книги для программистов: https://clcks.ru/books-for-it 340 Глава 13 Ориентация на события Реактивные системы используют асинхронную передачу сообщений, чтобы уста­ навливать границы между компонентами, обеспечивая слабую связанность, изоля­ цию и прозрачное расположение. Хотя Docker не выполняет эти задачи напрямую, этот принцип подчеркивает, что иногда приложение перегружено запросами или недоступно. Если мы передаем со­ общения между сервисами асинхронно, мы повышаем вероятность того, что запро­ сы не потеряются, и сервис обработает их, как только сможет. Заключение Все четыре принципа Манифеста реактивных систем требуют предусмотреть «изящную» деградацию и определить четкое разгр·аничение обязанностей в прило­ жениях. В динамических контейнерных средах все зависимости обрабатываются как хорошо спроектированные подключаемые ресурсы, поэтому мы можем под­ держивать число экземпляров N + 2 в стеке приложения, масштабировать отдель­ ные сервисы и быстро заменять неработоспособные узлы. Надежность сервиса определяется самой ненадежной его зависимостью, поэтому так важно учитывать эти принципы в каждом компоненте платформы. Принципы The Reactive Manifesto хорошо сочетаются с принципами The Twelve­ Factor Арр и рабочим процессом Docker. Эти документы описывают подходы, ко­ торых следует придерживаться для успешной разработки приложений в соответст­ вии с требованиями отрасли. Многие из этих идей можно без особых сложностей реализовать в любой организации с помощью Docker. Книги для программистов: https://clcks.ru/books-for-it ГЛАВА 14 Заключение Вы уже получили неплохое представление об экосистеме Docker и видели много примеров того, какие преимущества Docker и контейнеры Linux могут дать вашей организации. Мы рассмотрели распространенные проблемы и рекомендации, осно­ ванные на многолетнем опыте использования контейнеров Linux в рабочей среде. Как показывает опыт, преимущества Docker не так сложно реализовать, и они дей­ ствительно заметно повышают результативность. Как и другие эффективные тех­ нологии, Docker требует некоторых компромиссов, но в итоге приносит пользу и команде разработчиков, и организации. Если вы реализуете рабочий процесс Docker и интегрируете его в уже имеющиеся процессы, то заметите ощутимые пре­ имущества. В этой главе мы рассмотрим место Docker среди других технологий, а также пого­ ворим о том, какие задачи решает Docker и какую пользу он принесет вашей орга­ низации. Дальнейшее развитие Очевидно, что контейнеры с нами надолго, хотя некоторые уже давно говорят, буд­ то скоро Docker потеряет свою популярность. В основном потому, что само слово Docker вызывает много ассоциаций 1 • Мы говорим о компании с годовой выручкой в 50 млн долларов, которая в 2019 году была продана Mirantis через два года после реструктуризации? Или мы говорим о клиентском инструменте docker, исходный код которого кто угодно может загрузить (https://github.com/docker/cli), изменить и собрать? Людям нравится предсказывать будущее, а реальность обычно лежит где-то по­ середине, скрытая под неочевидными деталями. В 2020 году Kubernetes объявил о прекращении поддержки dockershim с выпуска Kubernetes v 1.24 (https://kubernetes.io/Ыog/2022/02/17/dockershim-faq). На тот мо­ мент многие подумали, что пора прощаться с Docker, совершенно забывая о том, что Docker - это, скорее, инструмент разработки, а не компонент рабочей среды. Да, его можно применять в рабочей среде по разным причинам, но он предназначен для того, чтобы оптимизировать упаковку и тестирование приложения. Kubernetes задействует Container Runtime lnterface (CRI), который не был реализован Docker, а 1 Полный URL: https://www.tutorialworks.com/differenr.e-docker-containerd-runc-crio-oci. Книги для программистов: https://clcks.ru/books-for-it 342 Глава 14 значит, требовалась дополнительная оболочка, dockershim, чтобы можно было ис­ пользовать Docker Engine через CRI. Это объявление не касается позиции Docker в экосистеме, и такой шаг был сделан, скорее, чтобы упростить сопровождение крупного опенсорс-проекта, развиваемого добровольцами. Может, Docker и не вы­ полняется на ваших серверах Kubernetes, но в большинстве случаев это никак не повлияет на цикл разработки и выпуска программного обеспечения. Если вы не используете Kubernetes с Docker CLI, чтобы напрямую отправлять запросы к кон­ тейнерам на узле Kubernetes, то вряд ли заметите какие-то изменения после этого перехода. Разработчики Docker создали и продолжают подцерживать новую оболочку shim, называемую cri-dockerd (https://github.com/Мirantis/cri-dockerd), с помощью кото­ рой Kubernetes будет и дальше работать с Docker, если нам требуется такой рабо­ чий процесс. Что любопытно, Docker также развивается в сторону неконтейнерных технологий, вроде WebAssemЬ\y (Wasm, https://docs.docker.com/desktop/wasm), чтобы допол­ нить контейнеры и повысить простоту и скорость разработки. Получается, что Docker как удобный набор инструментов останется с нами надол­ го, но при этом нам доступны и другие инструменты в экосистеме, чтобы при необ­ ходимости или при желании дополнить или заменить его. Благодаря различным стандартам, вроде OCI, и их широкому применению, многие инструменты работа­ ют с образами и контейнерами, создаваемыми и администрируемыми другими ин­ струментами. Зачем нужен Docker Традиционное развертывание часто состоит из множества этапов, каждый из кото­ рых не только усложняет процесс для разработчиков, но и повышает риски, связан­ ные с поставкой приложения в рабочую среду. Docker сочетает рабочий процесс и простой набор инструментов специально для упрощения этих задач. Он помогает оптимизировать процессы разработки по стандартам отрасли, и его продуманный подход часто повышает эффективность коммуникаций и надежность приложений. Благодаря Docker и контейнерам Linux разработчикам не приходится: ♦ Решать проблемы с существенными различиями между средами развертывания. ♦ Каждый раз переписывать логику конфигурации и журналирования в приложе­ ниях. ♦ Использовать устаревшие процессы сборки и выпуска, которые требуют не­ сколько итераций взаимодействия между командами разработки и сопровож­ дения. ♦ Придерживаться сложных и ненадежных процессов развертывания. ♦ Управлять разными версиями зависимостей для приложений, которые размеще­ ны на одном оборудовании. Книги для программистов: https://clcks.ru/books-for-it Заключение 343 ♦ Управлять несколькими дистрибутивами Linux в одной организации. ♦ Выстраивать одноразовые процессы развертывания для каждого приложения в рабочей среде. ♦ Работать с уникальной базой кода каждого приложения при установке патчей и проверке уязвимостей и т. д. Поскольку для передачи ответственности используется репозиторий, Docker упро­ щает коммуникации между командами сопровождения и разработчиками или меж­ ду несколькими командами разработчиков в одном проекте. Docker позволяет объединить все зависимости приложения в один программный пакет, чтобы разра­ ботчики не задумывались о вариантах дистрибутивов Linux, версиях библиотек и методах компиляции ресурсов или упаковки приложений. Команды сопровождения не занимаются процессом сборки, и только разработчики отвечают за зависимости. Рабочий процесс Docker Рабочий процесс Docker помогает организациям решать по-настоящему сложные проблемы - отчасти те же проблемы, для которых используются процессы DevOps. Главное препятствие для успешного внедрения практик DevOps в компа­ нии - многие люди даже не представляют, с чего начать. Они считают, что с по­ мощью инструментов можно решить проблемы, которые на самом деле связаны с непродуманными процессами. Когда мы добавляем инструменты виртуализации, автоматизированного тестирования, разработки или управления конфигурациями, мы просто меняем характер проблемы, не решая ее саму. Docker можно было бы принять за очередной инструмент, который якобы исправит ваши бизнес-процессы, но попробуйте посмотреть на него под другим углом. При использовании Docker весь свой жизненный цикл, от изначальной задумки до пре­ кращения поддержки, приложение проводит в одной экосистеме. В отличие от дру­ гих инструментов, нацеленных на одиночный аспект конвейера DevOps, Docker помогает заметно улучшить почти каждый шаг процесса. Этот рабочий процесс часто предъявляет строгие требования, зато упрощает внедрение базовых принци­ пов DevOps. В результате разработчики начинают лучше понимать весь жизненный цикл приложения, а специалисты по сопровождению могут поддерживать гораздо более широкий ряд приложений в одной среде выполнения. В выигрыше остаются все . Меньше программных пакетов для развертывания Docker позволяет сократить количество программных пакетов, которые приходится развертывать. Результат сборки умещается в одном образе Docker, который содер­ жит все, что требуется приложению в Linux для запуска, и все это работает в одной Книги для программистов: https://clcks.ru/books-for-it 344 1 Глава 14 защищенной среде выполнения. Контейнеры легко развертываются в современных дистрибугивах Linux, но благодаря четкому разделению между клиентом и серве­ ром Docker разработчики могуг собирать приложения и в другой операционной системе, при этом удаленно используя среду контейнеров Linux. Благодаря Docker разработчики создают образы Docker, которые с первой же про­ верки концепции можно запускать локально, тестировать с помощью автоматизи­ рованных инструментов, а затем развертывать в среде интеграции или рабочей сре­ де без пересборки. Мы можем быть уверены, что приложение, запущенное в рабо­ чей среде, в точности совпадает с приложением, которое мы тестировали. Нам не приходится ничего заново компилировать или упаковывать при развертывании, и это позволяет сократить риски, присущие большинству подходов к развертыва­ нию. Кроме того, мы получаем один этап сборки вместо трудоемкого и рискован­ ного процесса, состоящего из компилирования и упаковки нескольких сложных распространяемых компонентов. Образы Docker упрощают установку и конфигурацию приложения - все про­ граммные компоненты, которые нужны приложению в современном ядре Linux, умещаются в образе, поэтому мы избегаем конфликтов зависимостей, часто возни­ кающих в традиционной среде. В результате мы можем запускать на одном сервере несколько приложений, использующих разные версии системных компонентов. Оптимизация хранения и получения Docker использует слои файловой системы, чтобы можно было собирать контейне­ ры из нескольких образов. Мы экономим много времени и сил при развертывании, потому что отправляем только важные изменения. Кроме того, контейнеры зани­ мают гораздо меньше места на диске, потому что задействуют один базовый образ на нижнем слое, а также процесс копирования при записи для записи новых или измененных файлов на верхний слой. Благодаря этому приложения проще масшта­ бировать, ведь можно запускать несколько копий на одних и тех же серверах, не передавая по сети файлы для каждого нового экземпляра. Для размещения и получения образов Docker применяет реестр, который помогает четко разделить обязанности команд в соответствии с принципами DevOps. Разра­ ботчики собирают приложение, тестируют его, отправляют итоговый образ в реестр, а затем развертывают образ в рабочей среде. Специалисты по сопровож­ дению, со своей стороны, создают инструменты для развертывания и управления кластерами, чтобы извлекать образы из реестров, стабильно запускать их и гаран­ тировать их работоспособность. Команды сопровождения могуг отправлять разра­ ботчикам обратную связь и просматривать все результаты тестов на этапе сборки, не дожидаясь, пока проблемы возникнуг в рабочей среде. Такой подход позволяет обеим сторонам сосредоточиться на своих задачах, а не на пересылке друг другу пакетов. Книги для программистов: https://clcks.ru/books-for-it Заключение 1 345 Результат Вместе с опытом работы в Docker приходит осознание того, что контейнеры созда­ ют эффективный уровень абстракции между всеми программными компонентами и базовой операционной системой. Организации могут постепенно отказываться от создания индивидуальных физических серверов или виртуальных машин для большинства приложений и вместо них развертывать идентичные хосты Docker, которые представляют собой единый пул ресурсов для динамического развертыва­ ния приложений, тратя при этом гораздо меньше усилий. Изменения коснутся не только процессов, но и общей культуры разработки в орга­ низации. Разработчики берут на себя больше ответственности за весь стек прило­ жения, включая мельчайшие детали, к_оторыми обычно занимались другие специа­ листы. Команды сопровождения больше не пытаются упаковывать и развертывать сложные деревья зависимости, мало что понимая о самом приложении. В хорошо организованном рабочем процессе Docker разработчики сами компили­ руют и упаковывают приложение, так что им проще обеспечить его корректную работу в любых средах. При этом можно не беспокоиться о том, что специалисты по сопровождению внесут заметные изменения в среду приложения. Команды сопровождения, с другой стороны, занимаются поддержкой приложений, а также созданием надежной и стабильной платформы для выполнения приложений. При таком подходе формируется здоровая обстановка с четким разграничением обязан­ ностей по поставке приложения, и сложности между командами возникают гораздо реже. Хорошо продуманный рабочий процесс принесет преимущества не только компа­ нии, но и ее клиентам. Сотрудничество между командами приведет к повышению качества программного обеспечения, оптимизации процессов и ускоренной постав­ ке кода в рабочую среду. Продуманный рабочий процесс Docker помогает органи­ зациям достигать стратегических целей - повышать качество обслуживания кли­ ентов и удовлетворять потребности бизнеса. Итоги Теперь у вас достаточно знаний для того, чтобы реализовать современный процесс сборки и развертывания на основе контейнеров. Поэкспериментируйте с Docker в небольшом масштабе, на ноутбуке или виртуальной машине, чтобы лучше по­ нять, как все эти компоненты взаимодействуют друг с другом, а потом подумайте о том, как постепенно внедрять этот подход в своей организации. У каждой компа­ нии или отдельного разработчика будет свой путь в зависимости от уникальных потребностей и навыков. Если не знаете, с чего начать, попробуйте улучшить раз­ вертывание с помощью простых инструментов, а затем перейдите к таким задачам/ как обнаружение сервисов или распределенное планирование. На основе Docker можно создать систему любой сложности, но начать всегда можно с простого. Надеемся, у вас получится применить новые знания и воспользоваться преимуще­ ствами Docker и контейнеров Linux. Книги для программистов: https://clcks.ru/books-for-it Предметный указатель А Amazon Elastic Container Service, Amazon ECS 261 Amazon Identity and Access Management, Amazon IАМ 262 AnsiЫe 48 AnsiЫe Docker 49 Application Performance Monitoring, АРМ 218 в BuildKit 47, 99, 103, 105 с Capistrano 45 Centurion 49 Chocolatey 56 Chocolatey for Windows 53 Cloud Native Computing Foundation, CNCF 33 CodeShip 47 Colima 63 Command Line Interface, CLI 28 Completely Fair Scheduler, CFS 125 Consul 44 D DevOps 23, 30, 94, 222, 343 Docker ◊ CLI 37 ◊ демон 36, 300 ◊ клиент 37 ◊ клиент установка 52 ◊ компоненты 315 ◊ образ 67 ◊ преимущества 25 ◊ пространства имен 281 ◊ сервер установка 64 ◊ структура 316 ◊ топологии 307 Docker CLI 37, 156 Docker Compose 187 Docker Registry 82 Docker Swarm 228 Dockerfile 46, 68 dockprom 169 Domain Name Service, DNS 118 Е Elastic Kubernetes Services, EKS 261 F Fabric 48 G gRPC API 317 gVisor 320 н Harbor 77 HashiCorp Packer 43 HashiCorp Vault 334 Helios 49 Heroku 31 Homebrew 55, 240 Homebrew for macOS 53 J Jenkins 47 к Kind 259 Kubernetes 49, 238 Книги для программистов: https://clcks.ru/books-for-it Предметный указатель L Linux Containers, LXC 114 Linux Kernel-based Virtual Machine,КУМ 241 м МАС-адрес 118,293 Minikube 239 ◊ установка 240 N Network Address Translation,NAТ 303 Nomad 49 о Open Container Initiative (OCI) 67,317 Open Container Initiative,OCI 34 р Podman Desktop 328 PostgreSQL 44 Prometheus 50,164,166 R Rancher 49 Rancher Desktop 327 Red Hat Quay 77 ReplicaSet 254 Rocket.Chat 198,333 5 Secure Computing Mode 295 Security-Enhanced Linux,SELinux 42 SELinux 120,286,299 т The Reactive Manifesto 339 The Twelve-Factor Арр 330 Travis CI 47 V Vagrant 28,58 347 Б Балансировщик нагрузки 220 Библиотека ◊ musl 76 ◊ vpnkit 39 г Гипервизор 59,112 д Динамическое выделение томов 251 ж Журналирование 152,217 и Инструкция ◊ ARG 74 ◊ ENTRYPOINT 109 ◊ ENV 69,74 ◊ RUN 69 ◊ WORКDIR 70 Инструмент ◊ gVisor 320 ◊ kind 259 ◊ podman 325 ◊ Podman Desktop 328 к Клиент Docker 28 Команда ◊ build 72 ◊ docker compose top 205 ◊ docker container create 114 ◊ docker container diff 186,188 ◊ docker container ехес 149 ◊ docker container kill 133 ◊ docker container logs 152 ◊ docker container 1s 73 ◊ docker container pause 132 ◊ docker container stats 156 ◊ docker container stop 75 ◊ docker container top 171 ◊ docker container update 125 Книги для программистов: https://clcks.ru/books-for-it 348 Предметный указатель ◊ docker image history 93, 183 ◊ docker image inspect 69 ◊ docker network inspect 181, 306 ◊ docker network 1s 180 ◊ docker search 81 ◊ docker secret 334 ◊ docker stack 235 ◊ docker system info 143 ◊ docker system prune 135 ◊ docker version 142 ◊ dockerimage push 81 ◊ kubectl get pods 255 ◊ mount 117 ◊ ps 174 ◊ ulimit 129 ◊ volume 150 Команды для управления заданиями 215 Конвейер непрерывной интеграции 222 Контейнер 112 ◊ остановка 131 ◊ перезапуск 131 ◊ приостановка 134 Контейнеризация 23 Контроль доступа 300 м Мост 39 о Обнаружение сервисов 221 Образ ◊ Docker 29 ◊ базовый 75 ◊ загрузка обновлений 144 ◊ запуск 73 ◊ история 183 ◊ манифест 111 ◊ многоэтапная сборка 90 ◊ оптимизация 85 ◊ отладка 105 ◊ отправка в репозиторий 80 ◊ сборка 70 Оркестрация 219 п Планировщик 21 8 Платформа 213 Под 246 Проблема "шумный сосед" 121 Процесс тестирования приложения 223 р Развертывание 247 Реестр 35 ◊ образов а публичный 76 частный 77 Режим моста 39 Ресурсы ◊ ввода-вывода 128 ◊ ограничение 215 ◊ памяти 126 ◊ процессора 122 0 с Сервер Docker 28 Среда выполнения 34 ◊ низкоуровневая 34 ф Файл ◊ config.json 319 ◊ daemon.json 154, 167 ◊ docker-compose.yaml 190, 235 ◊ docker-compose-env.yaml 209 ◊ dotenv 210 ◊ lazyraster-service.yaml 250 ◊ Vagrantfile 60 ◊ webgame-task.json 265 Файловые системы 309 х Хранилище 67 ◊ внешнее 44 Книги для программистов: https://clcks.ru/books-for-it 06 авторах Шон Кейн - основатель компании techlabs.sh (https://techlabs.sh/) и главный про­ изводственный инженер в SuperOrЬital (https://superorbltal.io/). Шан специализи­ руется на процессах DevOps, включая Kubernetes, Docker, Terraform и т. д., не толь­ ко реализуя их в работе, но и обучая этому других. У него большой опыт в сопро­ вождении программного обеспечения в разных отраслях. Шан - изобретатель и автор патентов, связанных с контейнерами, который любит писать и говорить об этой технологии. Он обожает путешествовать и ходить в походы. Шан живет на северо-западном побережье США с женой, детьми и собаками. Карл Маттиас - вице-президент по архитектуре в Community.com (https://www. community.com/). Более 25 лет он работает главным инженером и руководителем в известных технических компаниях. Ему нравятся сложные задачи, распределен­ ные системы, Go, Ruby, Elixir, масштабируемые хранилища данных, автоматизиро­ ванные инфраструктуры и повторяемые системы. Книги для программистов: https://clcks.ru/books-for-it 06 изображении на обложке На обложке данной книги изображен синий кит (Balaenoptera musculus). Синие киты достигают 33 метров в длину и могут весить 200 тонн. Это самое крупное современное животное и самое крупное из когда-либо существовавших на Земле. Детеныш синего кита рождается размером с бегемота и прибавляет по 90 кг в день. Взрослые киты имеют длинное и узкое тело, небольшой спинной плавник, два плавника по бокам и горизонтальный хвост. Свое название киты получили за сине­ серый окрас. Синие киты мигрируют и обитают во всех океанах. Обычно они кормятся в холод­ ных полярных регионах, а затем отправляются в более теплые тропические воды для размножения. Синие киты перемещаются поодиночке или парами и общаются с помощью сложных звуковых сигналов. Синие киты входят в семейство полосати­ ковых (balaenopteridae), а значит, питаются, пропуская добычу через костные пла­ стины, называемые китовым усом. Питаются почти исключительно планктонными ракообразными. В день они расходуют 1,5 млн ккал и съедают до 3500 кг планкто­ на. Из-за своих размеров и способности быстро перемещаться практически не име­ ют естественных хищников. Раньше этот вид был широко распространен и насчитывал сотни тысяч особей. Киты были слишком быстрыми и большими животными для охотников, но с изо­ бретением гарпунного ружья в конце XVIII века китобои научились успешно охо­ титься на синего кита. За долгие десятилетия китовой охоты популяция значитель­ но сократилась. В 1966 году охота на синего кита была запрещена по всему миру. Это по-прежнему вымирающий вид, хотя теперь его численность растет. O'Reilly часто размещает на обложках виды, которым угрожает вымирание, и все они очень важны для мира. Если вы хотите поддержать китовую популяцию, то можете стать волонтером или внести пожертвование в организацию по охране китов, например в Общество по охране китов и дельфинов (https://whales.org/). Иллюстрация на обложке выполнена Карен Монтгомери (Karen Montgomery) на основе черно-белой гравюры из книги "А History of British Quadrupeds, Including the Cetacea". Книги для программистов: https://clcks.ru/books-for-it Шон П. Кейн, Карл Мапиас Docker. Вводный курс 3-е издание Перевод с английского М. Попович ТОО"АЛИСТ' 010000, Республика Казахстан, г. Астана, пр. Сарыарка, д. 17, ВП 30 Подписано в печать 23.07.24. Формат 70х100 1/16 . Печать офсетная. Усл. печ. л. 28,38. Тираж 1500 экз. Заказ № 10116. Отпечатано с готового оригинал-макета ООО "Принт-М", 142300, РФ, М.О., г. Чехов, ул. Полиграфистов, д. Книги для программистов: https://clcks.ru/books-for-it Книги для программистов: https://clcks.ru/books-for-it