С 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)


  1. Carburn
    18.10.2021 09:48

    Разве контейнеры не кэшируются?


    1. censor2005
      18.10.2021 10:15

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


  1. osipov_dv
    18.10.2021 10:31

    А чего бы не поднять локальный registry?


    1. zartdinov
      18.10.2021 10:57

      Еще у GitHub и GitLab тоже есть свои бесплатные и приватные регистры


      1. osipov_dv
        18.10.2021 11:03

        бесплатный гитхаб слабо применим для большинства, ибо только 1 приватный проект.


        1. p-oleg
          18.10.2021 11:43
          +3

          Я было подумал, что опять ввели ограничения, но нет.

          Free $0per user/month - Private repositories Unlimited

          https://github.com/pricing#compare-features


          1. osipov_dv
            18.10.2021 11:46

            хм... когда мне нужно было, уперся в этот лимит. ну ок, значит все не так плохо.

            Спасибо


  1. vesper-bot
    18.10.2021 10:50

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


    1. amarkevich
      18.10.2021 16:20

      меня и смутило отсутствие упоминание Cloud build: там уже будет Container Registry из коробки. Образы из докер хаба обычно используются достаточно редко для построения собственных, в любом случае можно залить себе "копию" и пользоваться ей.


      1. vesper-bot
        18.10.2021 16:36

        Ну, у нас вполне хватают dotnet/aspnet и пихают туда дотнет-приложения. Имхо даже логично. А если "докер-прокси" не использовать, то лимит на скачивание образов они выжирают с большим таким запасом.


      1. Carburn
        18.10.2021 21:24

        а для чего образы из докер хаба используются?


        1. amarkevich
          23.10.2021 11:15

          maven - для сборки, jre/tomcat для запуска. До кучи - есть уже готовые тулзни, например Protobuf кодогенераторы.


  1. LostAlly
    18.10.2021 11:05
    +1

    >(для залогиненных аккаунтов уровень ограничения передачи выше)

    Ниже?


    1. vesper-bot
      18.10.2021 16:37

      Выше — ограничивается число скачиваний с каждого IP, если не авторизован, при этом авторизованные имеют собственный лимит на скачивание отдельно от лимита по IP, который ЕМНИП даже на бесплатном аккаунте выше него.


      1. LostAlly
        22.10.2021 22:53

        Т.е. залогиненых ограничивают больше? Вот это поворот.


        1. vesper-bot
          23.10.2021 08:00

          Ограничение выше не равно "ограничивают больше". Например "два кг в руки" и "пять кг в руки" — второе ограничение выше, но тех, кому можно до 5 кг, ограничивают меньше.