Docker: по ту сторону Hello World
В мире программирования существуют технологии, must have для каждого разработчика, к числу которых относится и Docker. Подразумевается, что это просто, как таблица умножения, и известно всем. О том, зачем в 2021 году в 100500й раз заводить разговор про докер — статья Сергея Кушнарева, руководителя отдела разработки ZeBrains.
С одной стороны — про него все знают. С другой — если тебя устраивают небольшие веб-проекты, особенно на какой-то конкретной CMS, то докер очень часто оказывается тем самым «первым лишним», и все сводится к инструкции «возьми готовый докер-файл, запусти в терминале docker run и будет тебе счастье». А когда понимаешь, что этого уже недостаточно — натыкаешься на статьи, написанные по тому же принципу «скачайте-запустите-получите». Кому этого мало — читайте дальше.
Зачем программисту холодильник
Кто сказал: «Не люблю пить теплое»? Убираем емкость подальше и смотрим на принципиальную схему девайса:
Холодильник — это блок питания, компрессор, хладагент, конденсатор и камера, в одну из стенок которой спрятаны капиллярные трубки. Все это хозяйство утрамбовано в большой, в меру красивый шкаф с дверцей наружу, за которой и прячется то, что требуется пить охлажденным.
А теперь представим, что в доме программиста появилось существо противоположного пола, считающее, что в холодильнике можно хранить мясо. Что характерно — замороженное. Но при этом, как несложно догадаться, замерзнут до непотребного состояния и напитки, что нас в некотором смысле не совсем устраивает.
У нас два пути: купить второй холодильник или разделить тот, что у нас есть, на две камеры с датчиками температуры на входе, чтобы управлять клапаном подачи хладагента внутрь. Ура, мы только что изобрели двухкамерный холодильник. А заодно — разобрались с тем, для чего программистам понадобились виртуальные машины и докер.
Проекты, которые мы делаем, не сильно отличаются от продуктов в холодильнике — каждому нужны свои условия. Для одного — PHP 7.4, база MySQL 7.6, Sphinx и мейлер на Golang. Для другого — нода 12 версии, Angular 7 и база MySQL 8.0. Проектов может быть не один десяток. Установить это все на одну машину — все равно, что запихнуть все продукты в одну камеру холодильника.
Нужно как-то изолировать один проект (продукт) от другого. На помощь приходит или виртуальная машина (еще один холодильник), или докер (вторая камера со своими настройками). Давайте немного изменим схему нашего устройства:
Включаем воображение и смекалку, поехали!
Итак, у нас есть квартира (компьютер) со своей инфраструктурой, от которой нам требуется электричество (жесткий диск, сетевая плата, процессор, etc). Для установки второго холодильника (виртуальной машины) нам нужен разветвитель розеток (hypervisor). Довольно просто, но мы видим, что для изоляции мяса от напитков нам потребовалось два комплекта оборудования (Guest OS), хотя по факту условия хранения определяет только датчик, управляющий клапаном к капиллярной системе (bins/lib).
В случае, когда мы физически разделяем холодильную и морозильную камеры (container engine), нам не нужна вторая розетка (hypervisor) и место для второго холодильника (полноценная Guest OS). Мы получили два независимых контейнера — каждый со своими условиями (bins/lib), которые подходят нужному продукту (app).
Что ты, черт возьми, такое, Докер?
Разделим вопрос на две части: «как с этим работать» и «как это работает». Эта статья посвящена ответу на первую часть. Если интересно углубиться в детали, пишите в комментариях — поговорим про изоляцию процессов, пространства имен и прочее. А может быть, напишу еще одну статью.
Из схемы выше очевидно, что ответ на вопрос «что такое докер» спрятан в блоке «Container/Docker engine», иначе говоря — движок контейнеризации. Давайте посмотрим на него внимательней:
Первым делом нам потребуется некий сервис (Docker daemon), который будет управлять всем процессом — набор инструкций, как создавать изолированные пространства. Поселим его на своем компьютере!
Сам процесс установки прост и у вас не должно возникнуть трудностей.
Неважно, какую операционную систему вы выберете — все равно выполнение произойдет на Linux. В Windows и Mac будет запускаться виртуализированное ядро Линукса для докера.
Для того, чтобы мы могли как-то управлять этим сервисом, воспользуемся REST API, а команды будем выдавать посредством CLI, назовем его для удобства клиентом (Docker client).
Сами приложения и нужные для их работы библиотеки мы будем хранить в виде файлов-образов (Docker-images). Можете воспринимать их по аналогии с ISO-образами DVD-дисков или как специфический вид архива с данными.
Чтобы все заработало, мы должны с помощью клиента попросить Docker daemon взять конкретный образ и развернуть его в работающий контейнер. Но откуда он его возьмет? Добавим немного логики и инфраструктуры — пусть Docker daemon создаст на нашем компьютере реестр образов и при запросе находит нужный. А если не нашел — отправляется в сеть, находит Docker Hub (сетевой реестр), находит там нужный образ и копирует к нам на локальный компьютер.
Указать, какой конкретно образ нам нужен, мы можем или при запуске контейнера из готового образа (docker run), или при создании нового образа (docker build), или просто запросив скачивание (docker pull).
Получив нужный образ, докер-демон запустит на его основе контейнер, и мы получим работающее приложение. Заодно докер пробросит внутрь изолированного контейнера сеть (network), чтобы мы смогли увидеть результаты работы приложения, и при необходимости «прикрутит» к нему хранилище для сохранения данных (data volumes).
Повторение — мать учения
Теперь все вышеизложенное — но на примере демонстрационного приложения Hello World. Считаем, что демона мы на своем компе уже поселили, ссылка на установку — чуть выше по тексту.
Выполняем команду docker run hello-world и видим следующий результат:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:308866a43596e83578c7dfa15e27a73011bdd402185a84c5cd7f32a88b501a24
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working
correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker
Hub.
(amd64)
3. The Docker daemon created a new container from that image
which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which
sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
Давайте разбираться.
Первое что произошло после команды «докер, запусти образ с именем hello-world», это попытка найти его локально и запустить.
Unable to find image 'hello-world:latest' locally
Попытка не увенчалась успехом (у меня была чистая установка докера), причем, обратите внимание, искался образ не hello-world, а hello-world:latest.
Через двоеточие указывается тег — что-то вроде версии или модификации образа. Если его не указать, будет искаться самая свежая версия с общепринятым тегом latest.
latest: Pulling from library/hello-world
Тогда докер решает поискать этот образ на docker hub`е и скачать его оттуда.
https://hub.docker.com/_/hello-world
b8dfde127a29: Pulling fs layer
b8dfde127a29: Downloading 488B/977B
b8dfde127a29: Extracting 977B/977B
b8dfde127a29: Pull complete
Примерно такие строки будут друг друга заменять, но это может произойти очень быстро, так как образ очень мал. Вы будете их чаще видеть при загрузке больших и многослойных (про слои чуть позже) образов.
А далее видим следующие строки:
b8dfde127a29: Pull complete
Digest: sha256:308866a43596e83578c7dfa15e27a73011bdd402185a84c5cd7f32a88b501a24
Status: Downloaded newer image for hello-world:latest
Тут нам сообщают хэш образа и статус, говорящий, что загружен новый образ. Также в этот момент выводится и приветствие с сообщением. Но перед этим происходит еще кое-что.
А именно — из только что скачанного образа был создан контейнер. Если перевести на язык ООП, создался объект sleepy_antonelli (контейнер) экземпляр класса hello-world (образа). Sleepy_antonelli — это рандомно сгенерированное имя контейнера, поскольку мы не указали его явно.
Ну и, наконец, сам текст появляется на экране. Он, кстати, и есть результат работы приложения в контейнере.
Hello from Docker!
This message shows that your installation appears to be working
correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker
Hub.
(amd64)
3. The Docker daemon created a new container from that image which
runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which
sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
Собственно, это тут и написано (вместе с призывом не останавливаться на достигнутом и ссылкой на документацию). Но давайте проверим сами.
Если выполнить команду docker images, мы увидим скачанный образ.
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest d1165f221234 3 weeks ago 13.3kB
А если выполнить команду docker ps -a — флаг «a» показывает все образы, даже те, что остановлены в данный момент — то увидим и сам контейнер, как раз с именем sleepy_antonelli.
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
01d1bc18db71 hello-world "/hello" 18 seconds ago Exited (0) 17 seconds ago sleepy_antonelli
Имена можно задавать и самим, но нам пока это не нужно, поэтому докер сам генерирует для имени два рандомных слова. Также тут видны его ID, образ, с которого он был сделан, команду, ради которой был запущен (в данном случае — написать приветствие), когда был создан, код завершения работы (0 — это штатное завершение) со временем, порты (в данном случае они никак не пробрасывались, потому и пусто) и, наконец, имя.
Из одного образа можно создать много контейнеров. Также их можно останавливать, запускать и удалять. Но важно помнить, что образ — это шаблон. Он создается один раз и не меняется, а контейнеры можно модифицировать индивидуально. Это то же самое, что получить с завода (образа) 10 одинаковых контейнеров для перевозок грузов, но перекрасить каждый в разный цвет, повесить разные замки, и по-разному назвать.
«Людоеды — как лук, многослойные!»
Осталась еще одна тонкость, которую следует рассказать. Если образ неизменен, а контейнеры смертны, то тогда придется под каждую задачу создавать свой образ?
Нет, для этого придуманы слои. Каждое изменение образа можно выносить в отдельный слой. Это позволяет комбинировать их в разные итоговые образы. И вдобавок, уже готовый образ можно взять за основу и «наслоить» что-то свое.
Это можно представить в виде стопки блинов — тех, что железные. Мы надеваем на гриф блины, чтобы получить нужную нам конфигурацию (вес). Но при этом мы не можем вынуть или заменить какой-то блин в середине — придется пересобрать конфигурацию или довесить новые поверх имеющихся.
Если выполнить предложенную команду из hello world:
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
То мы увидим, что там несколько слоев:
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
04a5f4cda3ee: Pull complete
ff496a88c8ed: Pull complete
0ce83f459fe7: Pull complete
Digest: sha256:a15789d24a386e7487a407274b80095c329f89b1f830e8ac6a9323aa61803964
Status: Downloaded newer image for ubuntu:latest
root@61f60707bdbe:/#
Подведем итоги
Что же дает нам докер? Во-первых, это изоляция — мы можем запускать что угодно на своем компьютере, не опасаясь за целостность как системы, так и приложения. Они просто не пересекаются.
Во-вторых — чистота. Образ на диске — это самые обычные «инертные» файлы, которые «оживают» только при создании контейнера. Контейнеры же изолированы от внешнего мира.
В-третьих — скорость. Благодаря тому, что мы пользуемся операционной системой хоста, не требуется тратить время на полноценную инициализацию гостевой OS, как это происходит в виртуальной машине. Кроме того, можно удалить из сборки образа весь функционал, не относящийся к конкретному приложению, что тоже уменьшает и размер сборки, и время запуска.
Наконец, это предсказуемость. Контейнеру все равно, что находится снаружи. Поэтому если ваше приложение работало в контейнере на вашем локальном компьютере, оно заработает на любом другом. Забудьте про dependencies hell — эта фраза дорогого стоит!
Можно, конечно, вспомнить еще и о том, что тот же Kubernetes является развитием идеи контейнеров, перенесенной на уровень DevOps, но достаточно и того, что изложено выше.
Вопросы, уточнения и комментарии можно оставлять прямо под текстом — буду рад пообщаться.