Возможно, вы уже активно используете CRI-O и Podman, а может только смотрите на альтернативы Docker с осторожностью. Но, как бы там ни было, альтернативные решения создают конкуренцию монополисту Docker и предлагают новые и востребованные улучшения. Одна из таких особенностей, это исключение Docker Hub как корневого и основного источника образов контейнеров. Таким образом, снимается привязка к поставщику и появляются новые возможности, а одной из таких мы и поговорим.
Это инструкция по выбору решения и настройке прозрачного кеширования множества реестров контейнеров для CRI-O, Podman, Buildah, Skopeo и прочих инструментов, работающих с образами контейнеров OCI и использующих общую конфигурацию containers/common.

Для чего нужен прокси?
Если у вас есть несколько экземпляров CRI-O или Podman, работающих в вашей инфраструктуре, например: кластер Kubernetes, сборочные агенты, физические или виртуальные машины на которых работают контейнеризированные приложения. Каждый такой узел при запуске контейнера обращается к реестру контейнеров и пытается скачать образ контейнера, если он остутствует локально в кеше или найдена новая версия. Мы развернем и настроим кеширующий прокси сервер, для получения образов контейнеров из быстрого локального источника, тем самым:
Обойдем ограничения на скачивание;
Увеличим скорость запуска контейнеров, единожды запрошеный контейнер осядет в кеш и все последующие запросы будут отработаны гораздо быстрее, особенно актуально для узких интернет каналов;
Увеличим доступность и стабильность. Аптайма 100% не существует, и в самый ответственный момент публичный реестр контейнеров может оказаться недоступным, по разным причинам, имя кеш мы с большей вероятностью, по-прежнему сможем запускать контейнеры, а в случае недоступности кеша контейнер будет получен на прямую;
Сэкономим трафик (если это еще кому-то актуально);
Сможем реализовать удобный доступ к образам контейнеров в закрытом контуре, доступ в интернет понадобится только для одного узла, что также упростит контроль и анализ активностей.
А самое главное, это будет прозрачный кеш, т.е. людям работающим в этих окружениях не нужно будет даже знать или как-то дополнительно заботится о том, что есть прокси сервер и его нужно использовать. Это просто будет работать и даже в закрытом контуре.

Проксирование будем настраивать для семи популярных реестров контейнеров:
docker.io
quay.io
gcr.io
k8s.gcr.io
ghcr.io
mcr.microsoft.com
registry.gitlab.com
Этот список может быть расширен любыми другими реестрами контейнеров, к примеру вы можете добавить ваш личный реестр или запроксировать только определенную группу (namespace, организацию).

Отличие использования зеркал
Docker - позволяет указать список registry-mirrors только для основного зеркала (library), это означает, что только Docker Hub будет проксироваться прозрачно, для прочих реестров контейнеров будет необходимо использовать измененный адрес контейнера proxy.host.tld/registry.host.tld/namespace/image:tag
. Есть конечно обходной путь с использованием HTTPS_PROXY
, но этот метод также не лишен недостатков.
CRI-O и Podman - предоставляет возможность настроить перечень реестров контейнеров и зеркал для каждого реестра персонально, чем мы непременно воспользуемся.
Конфигурация registries.conf
Все настройки мы будем выполнять в конфигурационных файлах из проекта containers/image. Здесь вы можете найти документацию к файлу registries.conf.
А еще, для большего удобства, вы можете хранить конфигурацию в отдельных файлах в каталоге /etc/containers/registries.conf.d/
или использовать персональные настройки в пользовательском каталоге $HOME/.config/containers/registries.conf
А это пример конфигурации реестров и их зеркал:
unqualified-search-registries = ["my-registry.tld"]
[[registry]]
prefix = "my-registry.tld/namespace"
location = "internal-registry.tld/project-one"
[[registry.mirror]]
location = "container-mirror.local/namespace"
[[registry.mirror]]
location = "container-mirror-2.local/mirrors/namespace"
insecure = true
Пример того как будет обработан запрос скачивания образа namespace/image:tag
Сначала будет попытка найти алиас
[aliases]
из registries.conf для образаnamespace/image
, пример имеющихся алиасов вы найдете в файле/etc/containers/registries.conf.d/000-shortnames.conf
Далее будет попытка найти образ на предложенном из списка
unqualified-search-registries
узле, так как указан всего один узел, поиск произойдет автоматически наmy-registry.tld
Первая попытка получения образа произойдет по адресу
container-mirror.local/namespace/image:tag
Иначе, будет запрошен адрес
container-mirror-2.local/mirrors/namespace/image:tag
игнорируя проверку сертификатаИначе, обратимся в оригинальный источник
internal-registry.tld/project-one/image:tag
Выбор решения
Существует несколько решений, позволяющих развернуть локально кеширующий прокси сервер для образов контейнеров, рассмотрим некоторые популярные решения:
Собственная реализация - можно реализовать что-то своё взяв библиотеку distribution или сконфигурировать Nginx, Squid или другой подходящий прокси. Но мы не первопроходцы в этом деле, и можно обратить внимание на проект Docker Registry Proxy реализованный на Nginx или Docker Registry Cache основанный на Squid. Это самое гибкое и легковесное решение позволяющее проксировать любое количество репозиториев, а при помощи Lua или njs возможно реализовать почти любые идеи, но это потребует дополнительных компетенций и добавит сложности в обслуживании. Врят ли это то, что вы искали.
Distribution - стандартное решение для запуска реестра контейнеров с открытым исходным кодом, совместимо со спецификацией OCI, библиотека используется в реализации таких проектов как: Docker Hub, GitHub Container Registry, GitLab Container Registry, VMware Harbor Registry и прочих. Помимо хранения ваших контейнеров также поддерживает проксирование одного репозитория, для проксирования нескольких репозиториев, придется запустить по экземпляру приложения на каждый репозиторий.
Harbor - зрелое решение для запуска реестра контейнеров от VMware с открытым исходным кодом. Имеет web-интерфейс, ролевой контроль доступа, интеграцию c AD и OIDC, аудит, сканирование уязвимостей и прочие возможности. Позволяет создать прокси-репозиторий на каждый реестр контейнеров. GitHub.
Quay и Project Quay - коммерческий и свободно распространяемый реестр контейнеров от Red Hat. По функциональности схож с Harbor но имеет более гибкие возможности по разграничению прав доступа, основным отличием для нас является зеркалирование удаленного реестра контейнеров. Здесь это именно зеркалирование в отличии от прозрачного кеширования, что может оказаться избыточным и не подходящим при решении нашей задачи. ProjectQuay GitHub.
Sonatype Nexus - коммерческий и свободно распространяемый реестр артефактов от Sonatype. Это настоящий комбайн для хранения и проксирования артефактов для огромного множества поддерживаемых репозиториев, таких как: Nuget, NPM, Maven, APT, YUM, Conan, Docker и прочих. Как рестр образов контейнеров, Nexus уступает Quay и Harbor по обилию возможностей, но вполне успешно справляется с проксированием нескольких реестров и позволяет объединить их в группы. Прекрасно подойдет вам, если в вашем окружении выполняются сборочные пайплайны, вы сомжете собрать весь необходимый кеш артефактов через единый сервис. Nexus OSS GitHub.
JFrog Artifactory - коммерческое решение схожее по функциональности с Sonatype Nexus. Решения довольно похожи, можно посмотреть сравнение решений от Sonatype, и от JFrog. Также имеется бесплатная версия предназначеная только для хранения образов контейнеров и helm чартов.

Реализация класса "Стяжки и синяя изолента"
Изобретать свой велосипед мы с вами не будем, а пожалуй воспользуемся уже готовым Docker Registry Proxy, для демонстрации возможностей этого решения будет достаточно. Тем не менее, изучив образ ко��тейнера Docker Registry Proxy, вы сможете реализовать своё решение под ваши нужды.
Запустим контейнер:
mkdir -p ./containers-registry-proxy/cache
mkdir -p ./containers-registry-proxy/certs
podman run --rm --detach --name containers-registry-proxy \
--publish 0.0.0.0:3128:3128 \
--env ENABLE_MANIFEST_CACHE=true \
--env REGISTRIES="quay.io gcr.io k8s.gcr.io ghcr.io mcr.microsoft.com registry.gitlab.com" \
--volume "$(pwd)/containers-registry-proxy/cache":/docker_mirror_cache \
--volume "$(pwd)/containers-registry-proxy/certs":/ca \
rpardini/docker-registry-proxy:0.6.4
Вот и всё, прокси запущен, в отличии от прочих решений, данный пример работает как HTTP прокси, и требует указания адреса прокси сервера в переменных HTTP_PROXY
и HTTPS_PROXY
. Для более тонкой настройки обратитесь к документации проекта.
⚠️ Будьте готовы встретить проблемы и необходимость искать ошибки и дорабатывать решение под свои потребности.
Оценка решения:
? Минимальное потребление ресурсов;
? Решение подходит для прозрачного проксирования образов для Docker;
? Это только кешируйщий прокси, вы не можете публиковать здесь свои образы контейнеров;
? Возможность кешировать другие типы репозиториев, но потребует доработки, дополнительных знаний и времени;
? Отсутствует веб-интерфейс для администрирования;
? Работает как HTTP прокси, при других реализациях требует подмены DNS или публикации нескольких экземпляров на разных портах;
? Сложная отладка и кастомизация.
Реализация класса "Нативный инструмент"
Для реализации будем использовать немного сложный и при этом гибкий вариант, мы запустим несколько экземпляров Distribution. Воспользуемся официальным образом контейнера.
Давайте запустим наш первый прокси для Docker Hub:
mkdir -p ./containers-registry-proxy/docker.io
podman run --rm --detach --name registry \
--publish 5000:5000 \
--env REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/cache \
--env REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \
--volume "$(pwd)/containers-registry-proxy/docker.io":/cache \
registry:2
Добавим наше зеркало Docker Hub в конфигурационный файл registries.conf
[[registry]]
prefix = "docker.io"
[[registry.mirror]]
prefix = "docker.io"
location = "0.0.0.0:5000"
insecure = true
И проверим, что все работает:
podman --log-level debug pull docker.io/alpine
В отладочных сообщениях мы увидим, что все запросы ушли на http://0.0.0.0:5000
, а в ниже приведенных каталогах, появились данные:
./containers-registry-proxy/docker.io/docker/registry/v2/repositories/library/
./containers-registry-proxy/docker.io/docker/registry/v2/blobs/sha256/
Подробности по конфигурированию Distribution можно найти на странице документации. При развертывании продуктового решения не забудьте про TLS, а также обратите внимание на наличие Promethues метрик и поддерживаемые бэкенды хранения, к примеру S3.
Для реализации нашей задачи по прозрачному проксированию нескольких реестров контейнеров, всю рутину по созданию и запуску множества экземпляров Distribution и созданию конфигурационных файлов, мы можем автоматизировать, к примеру при помощи bash скрипта.
Скрипт для запуска нескольких копий Distribution
В скрипте также используется запуск Redis для кеширования метаданных и более быстрого доступа к ним.
По завершению работы скрипта, на экран будет напечатана конфигурация необходимая для размещения в файлеregistries.conf
, где для каждого репозитория как зеркало будет указан персональный проксирующий контейнер.
Пример конфигурации registries.conf полученный из скрипта
unqualified-search-registries = [
"docker.io",
"quay.io",
"gcr.io",
"k8s.gcr.io",
"ghcr.io",
"mcr.microsoft.com",
"registry.gitlab.com",
]
[[registry]]
prefix = "docker.io"
[[registry.mirror]]
prefix = "docker.io"
location = "0.0.0.0:5000"
insecure = true
[[registry]]
prefix = "quay.io"
[[registry.mirror]]
prefix = "quay.io"
location = "0.0.0.0:5001"
insecure = true
[[registry]]
prefix = "gcr.io"
[[registry.mirror]]
prefix = "gcr.io"
location = "0.0.0.0:5002"
insecure = true
[[registry]]
prefix = "k8s.gcr.io"
[[registry.mirror]]
prefix = "k8s.gcr.io"
location = "0.0.0.0:5003"
insecure = true
[[registry]]
prefix = "ghcr.io"
[[registry.mirror]]
prefix = "ghcr.io"
location = "0.0.0.0:5004"
insecure = true
[[registry]]
prefix = "mcr.microsoft.com"
[[registry.mirror]]
prefix = "mcr.microsoft.com"
location = "0.0.0.0:5005"
insecure = true
[[registry]]
prefix = "registry.gitlab.com"
[[registry.mirror]]
prefix = "registry.gitlab.com"
location = "0.0.0.0:5006"
insecure = true
Оценка решения:
? Нативное решение;
? Минимальное потребление ресурсов;
? Вы можете публиковать сюда свои образы контейнеров запустив еще один экземпляр Distribution;
? Горизонтально и вертикально масштабируется;
? Сравнительно более сложное в обслуживании решение;
? Отсутствует веб-интерфейс для администрирования (возможное решение).
Реализация класса "Коробочное решение"
Для реализации будем использовать Harbor. Минимально для запуска потребуется выделить 2 CPU и 4 Gb ОЗУ, для нормальной работы необходимо вдвое больше ресурсов. Установку выполним при помощи официального инсталятора и выполним настройку согласно документации.
Выполнив базовую настройку, перейдем в интерфейс и на каждый проксируемй репозиторий создадим по одноименному проекту в соответствии с примером из документации.
Теперь можем добавить конфигурацию в файл registries.conf
, где каждому репозиторию указан в качестве зеркала отдельный проект в рамках Harbor.
Пример конфигурации registries.conf
[[registry]]
prefix = "docker.io"
[[registry.mirror]]
prefix = "docker.io"
location = "harbor.host.tld/docker"
[[registry]]
prefix = "quay.io"
[[registry.mirror]]
prefix = "quay.io"
location = "harbor.host.tld/quay"
[[registry]]
prefix = "gcr.io"
[[registry.mirror]]
prefix = "gcr.io"
location = "harbor.host.tld/gcr"
[[registry]]
prefix = "k8s.gcr.io"
[[registry.mirror]]
prefix = "k8s.gcr.io"
location = "harbor.host.tld/k8s-gcr"
[[registry]]
prefix = "ghcr.io"
[[registry.mirror]]
prefix = "ghcr.io"
location = "harbor.host.tld/ghcr"
[[registry]]
prefix = "mcr.microsoft.com"
[[registry.mirror]]
prefix = "mcr.microsoft.com"
location = "harbor.host.tld/mcr"
[[registry]]
prefix = "registry.gitlab.com"
[[registry.mirror]]
prefix = "registry.gitlab.com"
location = "harbor.host.tld/gitlab"
Можем проверить получение образа, после чего в интерфейсе Harbor увидим осевшие в кеше образы и слои.
Оценка примера:
? Вы можете публиковать сюда свои образы контейнеров;
? Наличие веб-интерфейса для администрирования и управления;
? Имеет сканирование безопасности и ролевые политики;
? Умеренное потребление ресурсов;
? Избыточность для решения задачи кеширующего прокси.
Реализация класса "Звёздный разрушитель типа Венатор"
Рассмотрим пример запуска Sonatype Nexus OSS, бесплатной версии решения. Для коммерческой версии настройки будут такими же, а для JFrog Artifactory настройка будет похожей и описана в официальной документации.
Установим Nexus согласно официальной документации, для запуска нам понадобится выделить 4 CPU и 8 Gb ОЗУ, но при активном использовании готовтесь отдать около 32 Gb ОЗУ.
Завершив установку, перейдем в интерфейс администратора и выполним настройку проксирования репозиториев, в отличии от Harbor, где мы публиковали зеркала на разных маршрутах, здесь как в Distribution, каждый репозиторий может быть опубликован на собственном порту. Но для удобства существует возможность объединения нескольких репозиториев в группы.
Для каждого реестра контейнеров создадим по новому репозиторию формата docker (proxy)
в соответствии с документацией.

Для разнообразия конфигурирования, создадим новый репозиторий с типом docker (group)
и объеденим все наши прокси в общую группу:

Теперь можем добавить конфигурацию в файл registries.conf
, где каждому репозиторию указан в качестве зеркала один и тот же адрес группового репозитория.
Пример конфигурации registries.conf
[[registry]]
prefix = "docker.io"
[[registry.mirror]]
prefix = "docker.io"
location = "nexus.host.tld:8082"
[[registry]]
prefix = "quay.io"
[[registry.mirror]]
prefix = "quay.io"
location = "nexus.host.tld:8082"
[[registry]]
prefix = "gcr.io"
[[registry.mirror]]
prefix = "gcr.io"
location = "nexus.host.tld:8082"
[[registry]]
prefix = "k8s.gcr.io"
[[registry.mirror]]
prefix = "k8s.gcr.io"
location = "nexus.host.tld:8082"
[[registry]]
prefix = "ghcr.io"
[[registry.mirror]]
prefix = "ghcr.io"
location = "nexus.host.tld:8082"
[[registry]]
prefix = "mcr.microsoft.com"
[[registry.mirror]]
prefix = "mcr.microsoft.com"
location = "nexus.host.tld:8082"
[[registry]]
prefix = "registry.gitlab.com"
[[registry.mirror]]
prefix = "registry.gitlab.com"
location = "nexus.host.tld:8082"
Оценка примера:
? Вы можете публиковать сюда свои образы контейнеров
? Наличие веб-интерфейса для администрирования и управления
? Возможность проксировать другие типы репозиториев
? Имеет сканирование безопасности и ролевые политики
? Избыточность для решения задачи кеширующего прокси образов контейнеров
? Возможно потребует денег
? Сравнительно большое потребление ресурсов
? По неопытности можно вызвать коллизи с контейнерами для групповых репозиториев.
Отладка
Для отладки на стороне клиента запросим образ с уровнем логирования debug
:
podman --log-level debug pull docker.io/alpine:3.12
А вот и пример проблемы отсутсвующего TLS:
DEBU[0000] Trying to access "0.0.0.0:3128/library/alpine:3.12"
DEBU[0000] No credentials for 0.0.0.0:3128 found
DEBU[0000] Using registries.d directory /etc/containers/registries.d for sigstore configuration
DEBU[0000] Using "default-docker" configuration
DEBU[0000] No signature storage configuration found for 0.0.0.0:3128/library/alpine:3.12, using built-in default file:///home/woozymasta/.local/share/containers/sigstore
DEBU[0000] Looking for TLS certificates and private keys in /etc/docker/certs.d/0.0.0.0:3128
DEBU[0000] GET https://0.0.0.0:3128/v2/
DEBU[0000] Ping https://0.0.0.0:3128/v2/ err Get "https://0.0.0.0:3128/v2/": http: server gave HTTP response to HTTPS client (&url.Error{Op:"Get", URL:"https://0.0.0.0:3128/v2/", Err:(*errors.errorString)(0xc000422e60)})
DEBU[0000] GET https://0.0.0.0:3128/v1/_ping
DEBU[0000] Ping https://0.0.0.0:3128/v1/_ping err Get "https://0.0.0.0:3128/v1/_ping": http: server gave HTTP response to HTTPS client (&url.Error{Op:"Get", URL:"https://0.0.0.0:3128/v1/_ping", Err:(*errors.errorString)(0xc000423050)})
DEBU[0000] Accessing "0.0.0.0:3128/library/alpine:3.12" failed: error pinging docker registry 0.0.0.0:3128: Get "https://0.0.0.0:3128/v2/": http: server gave HTTP response to HTTPS client
DEBU[0000] Trying to access "docker.io/library/alpine:3.12"
Рекомендации
Лучше всего, всегда использовать полностью определенные имена образов, включая имя сервера реестра контейнеров, пространство имен, имя образа и тег, например:
ghcr.io/woozymasta/archimate-ci:4.9.1-1.0.2
quay.io/woozymasta/archimate-ci:4.9.1-1.0.2
docker.io/woozymasta/archimate-ci:4.9.1-1.0.2
DockerHub это не единственный источник правды, а в конфигурации у пользователся может быть настроено что-то не стандартное, могут быть заданы какие-то персональные алиасы, по этому даже для DockerHub лучше указывать абсолютный адрес начинающийся с docker.io
При использовании коротких имен всегда существует риск того, что извлекаемое изображение может быть подделано. Если нужный реестр контейнеров не стоит первым в списке поиска, злоумышленник может разместить другое одноименное изображение в публичном реестре, находящемся раньше в списке поиска. Пользователь случайно получит и запустит образ и код злоумышленника, а не ожидаемый контент.
Старайтесь выполнять анализ уязвимостей для хранимых у вас образов в ваших реестрах контейнеров, будь это прокси или собственные репозитории. Рекомендую обратить внимание на утилиту Clair которая без проблем интегрируется в Harbor и Quay, и при небольшом усили в Distribution.
Выводы
Какое из решений использовать?
На практике для проксирования образов контейнеров, я использую все из перечисленных решений кроме самоделок на nginx, всё зависит от контекста. К примеру:
При развертывании On-Premise серверов предназначенных для запуска контейнеризированных приложений и кластеров Kubernetes, я использую Distribution размещенный на специально отведенный под это узел или в кластере Kubernetes, за частую используя Minio как хранилище.
Harbor или Project Quay подходит для небольших команд, где планируется разработка и построение CI пайплайнов. Также это может быть встроенный реестр контейнеров GitLab который также позволяет проксировать образы. Тут всё зависит от конкретной ситуации.
Для больших команд разработки при построении процессов CI и CD, зачастую использую Sonatype Nexus OSS как прокси для всего, что только можно кешировать. Но стараюсь не использовать как целевое хранилище контейнеров, вопреки: довольно слабой ролевой модели, отсутсвии OIDC в OSS версии и уступающему удобству и функцоналу в сравнении с Harbor или Project Quay.
На этом всё
Благодарю за ваше время и внимание! Стабильных и высокодоступных окружений вам.
Присоединяйтесь в телеграмм канал, где я периодически публикую заметки на тему DevOps, SRE и архитектурных решений.