Возможно, вы уже активно используете 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

  1. Сначала будет попытка найти алиас [aliases] из registries.conf для образа namespace/image, пример имеющихся алиасов вы найдете в файле
    /etc/containers/registries.conf.d/000-shortnames.conf

  2. Далее будет попытка найти образ на предложенном из списка unqualified-search-registries узле, так как указан всего один узел, поиск произойдет автоматически на my-registry.tld

  3. Первая попытка получения образа произойдет по адресу
    container-mirror.local/namespace/image:tag

  4. Иначе, будет запрошен адрес
    container-mirror-2.local/mirrors/namespace/image:tag
    игнорируя проверку сертификата

  5. Иначе, обратимся в оригинальный источник
    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 (proxy) репозитория в Nexus для ghcr.io
Пример создания docker (proxy) репозитория в Nexus для ghcr.io

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

Пример групировки docker (proxy) репозиториев в общую групповой репозиторий docker (group)
Пример групировки 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 и архитектурных решений.