registry

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

Реестр с точки зрения пользователя

Все эти образы откуда-то берутся, а именно из докер-реестра. Реестром по-умолчанию является DockerHub. Когда мы делаем docker pull redis, мы на самом деле скачиваем образ из index.docker.io у специального пользователя по-умолчанию library. Убедиться в этом можно попробовав скачать образ по короткому и полному имени:

$ docker pull redis
Using default tag: latest
latest: Pulling from library/redis
d13d02fa248d: Pull complete
039f8341839e: Pull complete
21b9cdda7eb9: Pull complete
c3eba3e5fbc2: Pull complete
7778a0753f87: Pull complete
b052cf77de81: Pull complete
Digest: sha256:cd277716dbff2c0211c8366687d275d2b53112fecbf9d6c86e9853edb0900956
Status: Downloaded newer image for redis:latest

$ docker pull index.docker.io/library/redis
Using default tag: latest
latest: Pulling from library/redis
Digest: sha256:cd277716dbff2c0211c8366687d275d2b53112fecbf9d6c86e9853edb0900956
Status: Image is up to date for redis:latest

Как видно - этот ровно тот же образ с тем же хешем.

Так же на ДокерХабе можно зарегистрироваться и загружать свои образы, доступные для других пользователей. Не для всего софта на свете есть официальные образы от команды Докера, разработчики и сообщество заполняют пустоты. Пользовательские образы именуются в виде user/image, например vfarcic/jenkins-swarm-agent.

ДокерХаб - дефолтный, но не единственный реестр. Если мы хотим работать с другим реестром - мы должны указывать его доменное имя как первую часть имени образа, например quay.io/prometheus/haproxy-exporter.

Свой уютный приватный реестр

Предпосылки

Не всегда уместно пользоваться публичным реестром, причины могут быть различными, но почти всегда они укладываются в две категории: конфиденциальность и производительность.

Если компания разрабатывает ПО для внутреннего пользования и оно является коммерческой тайной, то загружать его в публичный реестр неразумно. Если оно никак не может пригодиться сообществу, но может помочь конкурентам или злоумышленникам - лучше его никому лишнему не показывать.

Да, ДокерХаб имеет возможность размещения приватных образов и создания команд с моделью прав доступа, но это платная фича и не каждый согласится вручать сохранность коммерческой тайны третьим лицам.

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

Постановка задачи

В первую очередь, что мы хотим от своего реестра?

Он должен находиться на “подконтрольной территории”, а именно хоститься на наших серверах. Таким образом мы избавимся как от вопросов конфиденциальности и доверия сторонним сервисам, так и от различных ограничений, которые они могут на нас накладывать. Также это ускорит наши CI/CD пайплайны: жонглировать образами внутри датацентра быстрее, чем обмениваться со сторонним сервисом.

Доступ к нему должен быть ограничен… странно, что этот пункт не первый, ведь ради этого все и затевалось. В докере имеется команда docker login, позволяющая авторизоваться в определенном реестре. Вроде бы на этом месте можно было бы и закончить, но есть один важный момент, который стоит учесть. Доступ должен быть ограничен только внешним пользователям, обращающимся к реестру через публичную сеть. Вся наша автоматика внутри датацентра должна иметь беспрепятственный доступ к содержимому реестра, чуть ниже, в практической части, я объясню, чем это полезно.

Реализация

Данные

Главная задача реестра - принимать, сохранять и выдавать по запросу образы контейнеров, а значит все функции завязаны на сохранность данных контейнера реестра. В этом плане он похож на типичную СУБД, поэтому нам необходим вольюм для сохранения.

Далее я буду расписывать вариант “серьезного” Сворм-кластера. Для отдельного докер-сервера все детали и трудности будут либо не актуальны вовсе, либо тривиальны.

Каким-бы вольюм драйвером мы ни пользовались, облачным стораджем, или хостовыми маунтами, нам нужно сохранить содержимое /var/lib/registry.

Авторизация

Несмотря на то, что в докер-клиенте есть команды docker login и docker logout - у реестра нет ни базы пользователей, ни жестко заданного набора способов авторизации. Вместо этого реестр умеет перенаправлять пользователей на заданный сервис авторизации. Такая декомпозиция позволит нам провернуть трюк, целиком убирающий процесс авторизации с нашего реестра.

Итак, представим, что мы хотим, чтобы наш реестр был доступен по адресу docker.acme.com. Как порядочные люди, мы не будем выставлять его напрямую голым задом на мороз, а добавим это в конфиг нашего реверс-прокси. На примере Docker Flow Proxy это выглядит следующим образом:

- com.df.port=5000
- com.df.users=admin:admin
- com.df.serviceDomain=docker.acme.com

Дефолтный порт докер-реестра - 5000, именно на него будут проксироваться запросы, а этот поддомен будет закрыт Basic Auth и требовать от пользователя логин и пароль admin:admin. Самое интересное, что сам реестр об этом ничего не знает, а вот docker login -u admin -p admin docker.acme.com сработает и сохранит данные для авторизации.

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

Внешнее и внутреннее представление

Тем не менее остался один важный момент. Мы обустроили обстановку с точки зрения людей, пользующихся реестром в более-менее интерактивном режиме. А что с точки зрения машин и автоматики?

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

По своей природе сугубо внутренний трафик будет ходить через реверс-прокси. С одной стороны это проблема производительности - мы почем зря напрягаем реверс-прокси и замусориваем мониторинг. С другой - мы забиваем гвоздями во множество мест доменное имя нашего реестра. Стоит нам сделать ребрендинг и переехать на другие домены - и придется играться в “50 оттенков grep’а”, заменяя старый домен на новый.

Более того, такой маршрут будет требовать от каждого докер-хоста авторизации в реестре. А это значит, что мы либо должны будем добавлять docker login в провижен-скрипты докер-хостов, либо как-то по-другому распространять файл с кредами. Плюс у нас могут быть какие-то свои приложения, которые будут работать напрямую с реестром - и тут тоже придется озабоиться вопросами авторизации.

Как избежать этих проблем? Сделать два различных пути к реестру из внешнего мира и из внутренней сети. Если мы вспомним, что реестр на текущий момент ничего не знает об авторизации, а все эти дела за него делает реверс-прокси, то надо всего-лишь зайти в реестр напрямую, а не через реверс-прокси.

Можно было бы провернуть какой-нибудь классический фокус с локальным DNS, но это опять-таки дополнительные кастомные сущности, которые надо привнести в систему. Лучше вместо этого воспользоваться сетевыми возможностями докера. Если у нас работающий Сворм-кластер, то у нас есть и Ingress-сеть, ключевое свойство которой в том, что публикуемые порты сервиса публикуются на всех хостах кластера, вне зависимости от того, есть ли на данном конкретном хосте запущенные контейнеры сервиса или нет. Если мы опубликуем дефолтный порт реестра через -p 5000:5000, то находясь на любом хосте кластера сможем обратиться к реестру через localhost:5000.

registry

Важный момент - 5000 порт должен быть заблокирован для внешнего мира либо на облачном фаерволе докер-кластера, либо на локальном фаерволе отдельной ноды, чтобы к реестру нельзя было подключиться через docker.acme.com:5000

После такого фокуса мы разделим внешнее и внутренне представление нашего реестра:

  • для разработчиков, в компоуз-файлах для локальной разработки местные образы будут выглядеть как docker.acme.com/foo/bar. Они один раз сделают docker login на своем ноутбуке и забудут про это.
  • для администраторов, в стек-файлах, разворачиваемых на продакшене, и в пайплайнах местные образы будут выглядеть как localhost:5000/foo/bar. Вопрос авторизации в реестре отпадает сам собой.

Таким образом мы решаем сразу все озвученные проблемы:

  • никакой нагрузки на реверс-прокси и мониторинг, трафик ходит как можно “ближе к телу”
  • в стек-файлах и пайплайнах перестает светиться текущий домен докер-реестра, можем делать ребрендинги хоть каждый день
  • для этого не требуется никакой кастомный DNS-конфиг, да и вообще не нужен DNS, если вдруг он сломается, то наши пайплайны не остановятся
  • вопрос авторизации решается сам собой

Тем не менее это не серебряная пуля, это довольно примитивный с точки зрения безопасности сетап. Если по каким-то соображением подход “один общий пароль на весь реестр” не выдерживает критики безопасников, то стоит посмотреть на более сложные решения, из них можно отметить:

  • JWT Token Auth позволяет реализовать сложные матрицы доступа к реестру. Одну реализацию предоставляет GitLab, где каждому репозиторию ставится в соответствие свой образ в реестре и права на этот образ дублируют права на репозиторий: чтобы запушить образ надо иметь право на пуш в репу.
  • Docker Trusted Registy Энтерпрайсное решение от Docker Inc., соответствующее многим формальным требованиям безопасности.