С 20 ноября 2020 года Docker начал ограничивать по количеству передач запросы к его популярному реестру Docker Hub. Это изменение затронуло всех пользователей, анонимных и бесплатных. После внедрения изменения процесс работы разработчиков по всему миру резко затормозил. Для решения проблемы многим просто было достаточно залогиниться (для залогиненных аккаунтов уровень ограничения передачи выше), однако другим потребовалось платить за сервисный аккаунт. При высоких нагрузках сервисные аккаунты могут быть дорогими.
Нет ничего плохого в том, чтобы решать проблемы деньгами. Возможно, в вашей ситуации это даже будет правильным решением. Однако для других реальность может быть не такой приятной.
Работая в Earthly, я столкнулся с этими ограничениями передачи. Для создания контейнированной сборки приходится подтягивать кучу контейнеров, и делать это часто. За пару часов мы 2-3 раза запускали наш тестовый набор, что приводило к активации ограничения передачи… и с каждым новым тестом ситуация становилась всё хуже. Возможно, это вам знакомо?
Поэтому вместо того, чтобы платить за сервисный аккаунт я настроил Pull-Through Cache, служащий посредником для всех запросов к Docker Hub. После его создания все отказы, вызванные ограничениями передачи, исчезли. Кроме того, это дешевле, чем платить за сервисный аккаунт! Чтобы сэкономить вам время, я задокументировал то, что сделал.
Что такое Pull-Through Cache?
Прежде чем вдаваться в подробности нашей системы в Earthly, давайте разберёмся, чем является и чем не является pull-through cache.
С точки зрения клиента, pull-through cache — это просто обычный реестр. Ну или почти. Например, в него нельзя пушить образы. Но из него можно пуллить. Когда из кэша впервые запрашивается образ (или его метаданные), он прозрачно получается из репозитория вверх по потоку. При последующих запросах будет использоваться кэшированная версия.
Подобная схема работает особенно хорошо, когда извлекаемые образы меняются нечасто. Хотя для обеспечения повторяемости обычно рекомендуют использовать определённые тэги, применение этой практики при работе с кэшем приведёт и к снижению количества циклов прохождения пути туда и обратно. Разве нам нужна ещё одна причина не пользоваться
:latest
?Разумеется, существуют дополнительные методы и инструменты, которые можно использовать для кэширования образов. В частности, есть удобный
rpardini/docker-registry-proxy
, использующий прокси nginx
и кэширующий запросы; принцип работы похож на MITM Proxy. В других реестрах есть режимы кэширования, например Artifactory и GCP.В этой статье я рассмотрю стандартный реестр Docker, находящийся в (
distribution/distribution
), потому что он прост и хорошо задокументирован. Если вы хотите сразу перейти к делу, то вся наша работа выложена на GitHub.Получение реестра
Каноническим реестром является
registry:2
. Можно получить его, просто выполнив docker pull registry:2
. Однако есть некоторые тонкости, изложенные ниже в разделе «HTTPS».Конфигурирование реестра
В процессе разбора опций, которые вам может понадобиться настроить для вашего pull-through cache, я буду делиться фрагментами из готового примера файла конфигурации. Если какая-то часть информации подходит вам не полностью, то существует достаточно исчерпывающая документация по конфигурированию реестра.
Режим прокси
Чтобы использовать реестр Distribution как pull-through cache, вам нужно сообщить ему о необходимости работать в качестве кэша, что неудивительно. Это можно сделать при помощи ключа верхнего уровня
proxy
.proxy:
remoteurl: https://registry-1.docker.io
username: my_dockerhub_user
password: my_dockerhub_password
Стоит заметить, что
username
и password
в этом разделе не являются идентификационными данными, которые вы будете использовать для логина в кэш; это идентификационные данные, которые будет использовать кэш, чтобы пуллить сверху по потоку в remoteurl
.По умолчанию кэш не будет аутентифицировать пользователей. Это означает, что без настройки аутентификации для зеркала (см. ниже), любые частные репозитории, доступные в
my_dockerhub_user
, по сути станут публичными. Убедитесь, что вы делаете всё правильно, чтобы избежать утечки уязвимой информации!Аутентификация в кэше
Чтобы сторонние люди не пуллили ваши частные образы или не тратили драгоценный трафик, зеркало должно быть защищено какой-нибудь аутентификацией. Её можно реализовать при помощи ключа верхнего уровня
auth
:auth:
htpasswd:
realm: basic-realm
path: /auth/htpasswd
Так как я работаю с относительно небольшой командой, достаточно использовать статичные имя пользователя/пароль в стандартном файле
htpasswd
. Если вам нужна помощь в генерировании файла htpasswd
, то прочитайте документацию по Apache.Не используйте тип аутентификации
silly
, так как он предназначен только для разработки. Это должно быть понятно из названия (ну, мы надеемся).Система
token
должна позволить вам подключить её к имеющейся в компании структуре аутентификации. Обычно она присутствует в крупных организациях и для подобного окружения подойдёт больше.HTTPS
Инфраструктура нашей компании находится в домене
.dev
. Весь .dev
использует HSTS. Это означает, что я не могу просто оставить наш кэш на HTTP. Кроме того, в современную эпоху Let’s Encrypt всё это довольно просто настроить, не так ли?Ну, типа того. На момент написания статьи существует проблема с образом по умолчанию и похожая проблема вверх по потоку для самой программы реестра. Так как Let’s Encrypt отключил соответствующие API, а образ по умолчанию очень стар, вам придётся использовать одно из трёх решений:
Скомпилировать реестр
Это решение использовал я. Можно просто обернуть существующий образ
registry
в ещё один Dockerfile при помощи FROM registry:2
(или использовать Earthfile) и заменить двоичный файл другим, собранным из исходников distribution/registry
.После этого достаточно будет настроить его следующим образом при помощи ключа
http.letsencrypt
:tls:
letsencrypt:
cachefile: /certs/cachefile
email: me@my_domain.dev
hosts: [mirror.my_domain.dev]
Благодаря этому Let’s Encrypt выпустит сертификат для доменов в ключе
hosts
и будет автоматически поддерживать его актуальность.Загрузка сертификатов вручную
Вы можете загрузить собственные сертификаты при помощи ключа
https.tls
. При этом нельзя использовать старые или поломанные библиотеки Let’s Encrypt в образе по умолчанию. При необходимости вы можете настроить certbot
для обработки их вручную.Реверсный прокси
Это был наш второй вариант после компилироваться собственной версии. Использование чего-то наподобие Traefik со встроенной поддержкой для Let’s Encrypt — стандартная практика, и она упомянута в указанных выше описаниях проблем как возможное решение.
Хранилище
Так как реестр — это просто кэш, и он не критичен, я решил сохранять кэш просто в локальное дисковое пространство на VPS, в котором я развернул систему. К тому же, метаданные для образов тоже не так критичны, поэтому я решил разместить их в памяти. Подробнее об этих опциях можно прочитать в ключах
storage.filesystem
и storage.cache
.storage:
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
Существуют и другие драйверы хранилищ. В корневом ключе
storage
подробно описаны доступные драйверы.Прочие мелкие улучшения
Я уже добавил множество опций настройки… так почему бы не добавить ещё немного? Вот какие улучшения я добавил ещё:
Проверка состояния хранилища (на случай, если в кэше начнёт заканчиваться место или произойдут другие странности с VPC):
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
Настройка порта, который будет слушать реестр. Закомментированная часть настраивает порт отладки; если её пропустить, то порт отладки будет отключен. Он полезен как свидетельство того, что попадания в кэш происходят.
Можно получить эту информацию через порт отладки (если он включен), перейдя в
/debug/vars
.http:
addr: :5000
# Uncomment to add debug to the mirror.
# debug:
# addr: 0.0.0.0:6000
headers:
X-Content-Type-Options: [nosniff]
Включение логгинга на уровне info, что упрощает отладку и тестирование:
log:
level: info
fields:
service: registry
Хостинг своего кэша
После всего этого вы сможете запустить реестр локально и даже успешно к нему подключиться! Команда для этого может выглядеть так:
docker run -d -p 443:5000 --restart=always --name=through-cache -v ./auth:/auth -v ./certs:/certs -v ./config.yaml:/etc/docker/registry/config.yml registry:2
Но кэш, который находится только на вашей машине, не так полезен, как общий или как находящийся между Docker Hub и вашей CI. К счастью, его не так сложно настроить и запустить на VPS.
Выбрать VPS для своего кэша легко — вероятно, стоит просто использовать тот, которым пользуется ваша компания. Однако я решил выбрать Digital Ocean, потому что у его разумные цены, простая настройка и щедрые лимиты.
Большую часть времени наш кэш достаточно хорошо работал на единственном дроплете за 5 долларов, однако дополнительная нагрузка из-за CI заставила нас подняться на один уровень выше. Если ваши потребности выше, чем возможности одного узла, то всегда есть возможность запустить несколько инстансов с балансировщиком нагрузки или воспользоваться CDN.
Хотя вполне можно создать инстанс VPS вручную, давайте сделаем ещё один шаг и полностью автоматизируем этот процесс при помощи Terraform и clout-init. Если вы сразу хотите перейти к делу, то изучите полный пример.
Давайте начнём с создания инстанса VPS. В отличие от показанных выше примеров с реестром я оставлю переменные, которые использовал в нашем модуле Terraform.
resource "digitalocean_droplet" "docker_cache" {
image = "ubuntu-20-04-x64"
name = "docker-cache-${var.repository_to_mirror}"
region = "sfo3"
size = "s-1cpu-1gb"
monitoring = true
ssh_keys = [var.ssh_key.fingerprint]
user_data = data.template_file.cloud-init.rendered
}
Это довольно стандартная, простая конфигурация для запуска дроплета. Но как запустить наш кэш на свежем дроплете? С помощью ключа
user_data
cloud-init. Мы используем Terraform для создания шаблона с теми же переменными, которые указаны в нашем модуле, и помещаем результат в этот раздел HCL. Вот урезанная версия нашего шаблона cloud-init:#cloud-config
package_update: true
package_upgrade: true
package_reboot_if_required: true
groups:
- docker
users:
- name: griswoldthecat
lock_passwd: true
shell: /bin/bash
ssh_authorized_keys:
- ${init_ssh_public_key}
groups: docker
sudo: ALL=(ALL) NOPASSWD:ALL
packages:
- apt-transport-https
- ca-certificates
- curl
- gnupg-agent
- software-properties-common
- unattended-upgrades
write_files:
- path: /auth/htpasswd
owner: root:root
permissions: 0644
content: ${init_htpasswd}
- path: /config.yaml
owner: root:root
permissions: 0644
content: |
# A parameterized version of our registry config...
runcmd:
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
- add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
- apt-get update -y
- apt-get install -y docker-ce docker-ce-cli containerd.io
- systemctl start docker
- systemctl enable docker
- docker run -d -p 443:5000 --restart=always --name=through-cache -v /auth:/auth -v /certs:/certs -v /config.yaml:/etc/docker/registry/config.yml registry:2
Этот шаблон cloud-init настраивает Docker, конфигурирует наш реестр и запускает контейнер.
Используем наш кэш
Пользоваться зеркалом очень просто. Достаточно добавить зеркало в свой список, и если настроена аутентификация, выполнить
docker login
с соответствующими идентификационными данными (указанными выше данными htpasswd
). docker
должен запуститься автоматически и уже использовать зеркало.Если ваше зеркало оказывается недоступным,
docker
будет подниматься вверх по потоку без дополнительной настройки.Заключение
Настройка и поддержка собственного pull-through cache избавили нас от головной боли и сэкономили деньги. Наша CI больше не подвержена ограничениям передачи. Это означает, что наши сборки стали более целостными, надёжными и быстрыми.
Комментарии (16)
osipov_dv
18.10.2021 10:31А чего бы не поднять локальный registry?
vesper-bot
18.10.2021 10:50А что мешало развернуть то же самое на мощностях, где собираете (если они не в облаке, естественно)? Если у вас и так есть какая-то машина с докером, можно контейнер просто прямо туда развернуть, с выделенным каталогом под кэш образов, и в таком виде и оставить. Мы так сделали на наших мощностях, теперь не нарадуемся.
amarkevich
18.10.2021 16:20меня и смутило отсутствие упоминание Cloud build: там уже будет Container Registry из коробки. Образы из докер хаба обычно используются достаточно редко для построения собственных, в любом случае можно залить себе "копию" и пользоваться ей.
vesper-bot
18.10.2021 16:36Ну, у нас вполне хватают dotnet/aspnet и пихают туда дотнет-приложения. Имхо даже логично. А если "докер-прокси" не использовать, то лимит на скачивание образов они выжирают с большим таким запасом.
Carburn
18.10.2021 21:24а для чего образы из докер хаба используются?
amarkevich
23.10.2021 11:15maven - для сборки, jre/tomcat для запуска. До кучи - есть уже готовые тулзни, например Protobuf кодогенераторы.
LostAlly
18.10.2021 11:05+1>(для залогиненных аккаунтов уровень ограничения передачи выше)
Ниже?
vesper-bot
18.10.2021 16:37Выше — ограничивается число скачиваний с каждого IP, если не авторизован, при этом авторизованные имеют собственный лимит на скачивание отдельно от лимита по IP, который ЕМНИП даже на бесплатном аккаунте выше него.
LostAlly
22.10.2021 22:53Т.е. залогиненых ограничивают больше? Вот это поворот.
vesper-bot
23.10.2021 08:00Ограничение выше не равно "ограничивают больше". Например "два кг в руки" и "пять кг в руки" — второе ограничение выше, но тех, кому можно до 5 кг, ограничивают меньше.
Carburn
Разве контейнеры не кэшируются?
censor2005
Возможно, у них распределённая система, и они скачивают образы на несколько машин