null

Введение в Docker

Docker'у исполнилось уже 8 лет, за это время про него написано много различных статей, но тем не менее всё ещё есть достаточно большое количество людей, которые не знают что это такое и как оно работает. Тем не менее docker в ряде случаев упрощает повседневные задачи взамен на немного больший расход ресурсов.

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

Статьи цикла:
1. Введение в Docker
2. Продолжаем изучать Docker

Что такое docker?

Docker это один из инструментов, которые позволяют создавать изолированные окружения, содержащие приложения и необходимые им зависимости. Идея собственно совсем не новая: виртуальные машины появились давно, linux-контейнеры в лице LXC появились ещё в 2008, а во FreeBSD jail'ы появились в ~2000.

Чем же отличаются контейнеры от виртуальных машин?

 

Как видно из незамысловатой иллюстрации:
- Приложения в виртуальных машинах отделены от реального железа бОльшим кол-вом слоёв логики, что влияет на расход ресурсов и может влиять на производительность.
- Код ОС дублируется в хостовой и виртуальной системах, из-за этого ВМ относительно долго загружаются, а также могут повторно применяться заплатки безопасности, которые в отдельных случаях понижают производительность.
- ВМ должны соревноваться за ресурсы с приложениями/ОС на хостовой системе (хотя этого и можно избежать настройкой планировщиков).
- ОС внутри ВМ тоже нужно адиминистрировать!

Также стоит отметить, что в случае виртуальных машин ресурсы (например, диск и память) расходуются нерационально:
- Выделен диск размером 100Гб, но фактически используется ~40Гб
- Выделено 16Гб памяти, но большую часть времени используется ~8Гб
Обе эти проблемы тоже можно исправить, но возникает вопрос о необходимости и об удобстве использования такого решения. Несмотря на то, что виртуальные машины предоставляют большую гибкость и из-за этого в ряде случаев их выбор вполне обоснован, использовать их в повседневной работе не очень удобно.

Какие бывают контейнеры?

На иллюстрации выше изображена примерная модель для Docker контейнеров, следующая принципу один контейнер - одно приложение. Но это не является ограничением самих контейнеров, в них также можно запустить "init" процесс, который в свою очередь запустит множество приложений. Таким путём пошли LXC-контейнеры и в итоге они скорей являются легковесными аналогами виртуальных машин, со всеми недостатками такого решения:
- Их тоже нужно администрировать.
- Конфигурация задана жёстко и может варьироваться на разных инсталляциях.
- Сложно тиражировать/пересоздавать.

Docker же предлагает иной подход к контейнерам:
- Образ контейнера предоставляет минимально необходимое окружение с приложением.
- Основные параметры (сетевые интерфейсы, DNS, IP-адреса и т.д.) конфигурируются извне, без необходимости производить настройки изнутри контейнера.
- Сборка контейнера воспроизводима.
- Изоляция средствами ОС - приложения в контейнере отделены от остальных (других контейнеров, хоста).
- Эфемерность - контейнеры временны, незначительные изменения не сохраняются.
- Значимые данные отделены от приложения и его окружения.

В итоге Docker предоставляет следующие возможности:
- Единообразие управления
- Автоматическая конфигурации сети и т.д.
- Создание окружения с помощью образов
- Вынесение изменяющихся данных во вне
- Изоляция отдельных приложений

Механизмы работы контейнеров

Linux-контейнеры (и docker в их числе) используют механизмы cgroups и namespaces. Первый позволяет ограничивать использование ресурсов, а второй - создавать отдельные пространства имён для различных аспектов процессов внутри ОС, среди которых есть:
- Пространство процессов (pid)
- Пространство сетевых интерфейсов (net)
- Пространство ФС (mnt)
- Пространство пользователей (user)

По-разному комбинируя эти пространства можно получать интересные результаты, но одним из наиболее полезных вариантов является случай когда все пространства - уникальны. В таком случае создаётся видимость обособленной системы, но при этом со стороны ОС сам процесс выполняется как и все остальные.

Отправляемся в плавание

Условно Docker состоит из следующих частей:
- Сервер (dockerd) + runtime контейнеров (containerd)
- Клиент (docker CLI)

По-умолчанию клиент и сервер связываются через unix-сокет `/var/run/docker.sock`, но возможен вариант взаимодействия через HTTPS.

Для начала по бородатой традиции познакомимся с миром:

$ docker run hello-world
​​​​​​​Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:f2266cbfc127c960fd30e76b7c792dc23b588c0db76233517e1891a4e357d519
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/

Что произошло выше:
1. Docker не смог найти образ hello-world:latest локально и поэтому загрузил его из Docker Hub
2. Создал окружение контейнера из образа
3. Запустил в нём приложение, помещённое туда при создании образа

latest в данном случае это один из тегов образа. Обычно теги используют для обозначения версии приложения или "варианта поставки".
Если тег не указан, то docker использует тег latest.

А что же стало с контейнером после того как приложение завершило работу? Чтобы узнать это запустим docker ps (или docker container ls):

$ docker ps
CONTAINER ID   IMAGE         COMMAND   CREATED           STATUS     PORTS     NAMES

Пусто... Но docker по-умолчанию показывает только работающие в данный момент контейнеры, а наш уже завершил своё исполнение, тогда добавим флаг `-a`:

$ docker ps -a
CONTAINER ID   IMAGE         COMMAND    CREATED          STATUS                      PORTS   NAMES
10eb73a10fb6   hello-world   "/hello"   25 minutes ago   Exited (0) 25 minutes ago           inspiring_joliot

В выводе команды можно увидеть CONTAINER ID, используемый образ, запущенную команду, время с момента создания контейнера, его текущий статус, маппинг портов и имя контейнера.

- CONTAINER ID - это уникальный идентификатор контейнера, по которому его можно найти однозначно, docker генерирует его автоматически.
- Имя контейнера - произвольная уникальная строка текста, позволяющая дать "человеческое имя" контейнеру, её можно задать вручную, либо она будет сгенерирована.

Как правило, CONTAINER ID и имя контейнера взаимозаменяемы при использовании в docker CLI.

Может возникнуть вопрос: "Зачем нужен этот контейнер, если приложение завершило своё выполенние?".
Тому существует несколько причин:

1) Мы не сказали что хотим, чтобы docker удалил контейнер при завершении исполнения
2) Нужно сохранить информацию, полученную в ходе выполнения приложения (статус, логи, изменения ФС внутри контейнера)

Мы можем запустить контейнер из того состояния, в котором он оказался на момент завершения работы с помощью команды:

$ docker start inspiring_joliot
inspiring_joliot

Почему в этом случае docker вывел только имя контейнера? - Потому что в этом случае мы к нему не подключили терминал и из-за этого docker отрапортовал об удачном выполнении команды для этого контейнера (в аргументах можно указать несколько контейнеров).
Если после этого написать docker logs inspiring_joliot, то docker выведет  приветствие дважды, т.к. hello-world запускался дважды.

Если мы хотим удалить контейнер совсем, то нужно вызвать docker rm.

Полезные команды для управления контейнерами

Предлагаю самостоятельно поэкспериментировать с контейнерами и поизучать доступные команды (для этого, например, можно использовать образы ubuntu, traefik/whoami). Я не буду приводить подробную справку по командам, т.к. её можно получить добавив --help.

docker run # - создать новый контейнер и запустить в нём команду
docker exec # - выполнить команду внутри работающего контейнера
docker restart # - перезапустить контейнер
docker stop # - остановить контейнер
docker logs # - вывести логи контейнера (stdout и stderr), флаги похожи на tail
docker ps # - показать существующие контейнеры, флаги похожи на ps
docker cp # - копировать файлы между хостом и контейнером
docker images # - показать локальные образы
docker pull # - загрузить образ
docker rm # - удалить контейнер
docker rmi # - удалить образ

Где найти образы?

Основным местом, откуда загружаются образы является Docker Hub (hub.docker.com), там можно найти готовые образы и чаще всего там же находится документация к ним.

В следующей статье я подробнее остановлюсь на концепциях самого docker'а и приведу пример его полезного использования.