Вместо вступления
Хочу рассказать про несколько новых возможностей AWS Elastic Container Registry (ECR), о которых, как мне кажеться знают немного.
Когда мы только начали использовать ECR, это был совсем простой сервис: настроил репозиторий, положил туда образ — и забыл. Но каждый раз, возвращаясь к документации по очередному поводу, я обнаруживал, что функциональности там стало заметно больше, чем в прошлый раз. В какой-то момент этих «незамеченных» фич накопилось достаточно, чтобы пересобрать всю нашу схему хранения образов. Об этом и статья.
Инцидент
Наши сервисы спокойно жили в 20 регионах AWS — ровно до очередного инцидента в us-east-1. Подробную хронологию(да это было аж в 2021 году, как летит время!) можно почитать в официальном разборе AWS: https://aws.amazon.com/message/12721/. Если коротко: из-за сетевых проблем в ключевом регионе посыпалось много чего. Нас это почти не задело(мы же распределенные!) — кроме одного маленького нюанса: примерно на час ECR API перестал авторизовать запросы из всех регионов кроме us-east-1. А наши образы в тот момент хранились только в us-east-1.
Получилась обидная картина: вся инфраструктура в остальных регионах жива и работает, но скейлиться вверх мы не можем — новые контейнеры не запускаются, потому что им неоткуда скачать образ. В тот раз обошлось без последствий, но зависимость от одного региона нам резко разонравилась — даже с поправкой на то, что такой инцидент может больше и не повториться.
Полистав документацию, мы включили Cross-Region Replication во всех регионах. Сработало? Да, надёжность выросла. Но вместе с ней мы получили три новые проблемы.
Гидра
У такого решения есть своя цена. В нашем случае она сложилась из трёх сложностей.
1. Синхронизация
Репозиторий-реплика появляется в регионе только после первого успешного пуша (replication event). То есть старые образы в новых регионах-репликах попросту недоступны, пока нет события типа push в главном репозитории.
Звучит безобидно, но по факту:
-
случайно удалили не тот образ — сам он не восстановится, и это простой в регионе(outage);
-
завели новый регион — он стоит пустой, пока мы не сделаем пуш свежего (или старого) образа в вышестоящий регион, и если нужен ролбек- откатываться некуда, ведь старых версий не было, и это тоже простой в регионе(outage);
Да, привыкнуть можно, но не хочется изобретать костыли, да ещё и всей команде надо напоминать, что вот так вот оно работает.
2. Деньги
Репликация в 20 регионов — это стоимость хранения, умноженная на 20. А количество образов растёт практически экспоненциально.
В качестве лекарства Amazon предлагает lifecycle (cleanup) policy, но правила там довольно грубые: можно «хранить последние N образов» или «удалять всё старше X дней / без тегов» — и почти ничего сверх этого. Представьте: разработчики гоняют хотфикс, проблема решается не сразу — и вот в репозитории уже два десятка новых образов, причём с релизными тегами(проблема у них, понимаешь, воспроизводиться только на проде!). А дальше такая политика спокойно удаляет образ, который крутится в проде — просто потому, что ему не повезло оказаться 21-м по счёту и старше 14 дней.
Неудивительно, что соответствующий запрос на фичу собрал в роадмапе AWS Containers больше 600 голосов. Правда, на реализацию у AWS ушло около пяти лет — почему так долго и почему именно в таком виде, судить не берусь. Нам же оставалось только упражняться в написании достаточно хитрых политик, а для критичных сервисов — и вовсе отключать их.
3. Автоматизация
И, наконец, ложка дёгтя для тех, кто живёт в IaC:
-
Terraform не может применить
aws_ecr_lifecycle_policyк репозиторию, которого ещё нет в регионе-реплике. -
А репозиторий-реплика, как мы помним, появляется только после первого пуша.
В итоге деплой нового сервиса распадается на зависимые шаги: terraform apply для репозитория и его реплик → запуск пайплайна и сборка+пуш билда → ожидание репликации → terraform apply для политик очистки. Костыль, конечно, всегда можно прикрутить — но мы любим избегать нестандартных практик там, где это возможно.
Решение: Декларативный принцип
И вот однажды, обсуждая совсем другую тему — задержки и лимиты при стягивании образов с Docker Hub, — мы снова вернулись к теме репликации и решили присмотреться к Pull-Through Cache.
Ранее эта фича работала только для публичных апстримов (Docker Hub, ECR Public и подобных), и для нашей задачи была бесполезна. Но в 2025 году появилась возможность использовать в качестве апстрима приватный ECR. И тут до нас дошло: вместо того чтобы заранее реплицировать образы во все 20 регионов, надо просто стягивать их по требованию. Это же классическая дилемма проектирования — Push model против Pull model, — правда, раньше нам нечем было эту дилему решить.
Идея была использовать сразу две новые фичи: Pull-Through Cache + Repository Creation Templates.
Пример реализации
1. Pull-Through Cache
Декларативный принцип в чистом виде: больше не нужно пушить всё и везде заранее. Если ноде в каком-то регионе понадобился образ — она его запрашивает, а ECR сам сходит в апстрим, заберёт образ и закэширует его локально. Никто ничего не реплицирует «на всякий случай».
Нам надо просто объяснить ECR в новом регионе (например, eu-central-1): что если кто-то просит образ из нашего приватного us-east-1 реестра — не надо отдавать 404, надо сходить в апстрим и забрать его.
resource "aws_ecr_pull_through_cache_rule" "main" { ecr_repository_prefix = "ROOT" upstream_registry_url = "111111111111.dkr.ecr.us-east-1.amazonaws.com"}
После этого команда с другим регионом внутри адреса магически работает, даже если репозитория upstream-prod/my-service в eu-central-1 никогда не существовало.
docker pull 111111111111.dkr.ecr.eu-central-1.amazonaws.com/upstream-prod/my-service:v1
Под капотом вместо того чтобы сразу выдать вам ошибку, ECR проверяет свои настройки кэширования и видит правило которое говорит, что за любыми неизвестными образами надо идти в «111111111111.dkr.ecr.us-east-1.amazonaws.com«. Причем это работает и с другими аккаунтами(но там понадобиться создать IAM-роль).
Например, если за билды отвечает отдельная команда и она все образы складывает в центральный регион своего аккаунта, то ваш сервис может с помощью этой фичи стянуть этот образ в свой аккаунт и регион.
Интересно, что указывая несуществующий адрес образа с eu-central-1 внутри мы просто говорим ECR, в каком регионе он должен обработать запрос, а не указываем конкретный адрес.
Есть и более продвинутые возможности — например, если один и тот же образ лежит в разных регистри (dockerhub, quay.io) и вы хотите на стороне деплоймента регулировать, откуда загрузить образ в регион, можно использовать ecr_repository_prefix, отличный от ROOT.
Получается, наш кубернетес-деплоймент декларативно говорит, что и как делать с образами ещё до загрузки.
2. Repository Creation Templates
Но это еще не все!
У ленивого создания репозиториев есть свои недостатки: созданный Амазоном «на лету» репозиторий пустой — без шифрования, без тегов, без lifecycle-политик.
Repository Creation Templates закрывают этот пробел. Мы один раз описываем шаблон для префикса (например, upstream-prod/*) — и при первом же обращении по этому пути AWS не только создаёт репозиторий автоматически, но и сразу накатывает на него весь нужный конфиг. Настроил один раз — и забыл.
Теперь говорим AWS: «Каждый раз, когда Pull-Through Cache создаёт репозиторий с префиксом upstream-prod, примени к нему вот этот конфиг» — зашифруй ключом KMS, повесь тег ManagedBy и удаляй всё старше 14 дней.
resource "aws_ecr_repository_creation_template" "template" { prefix = "upstream-prod" description = "Auto-config for pull-through cached repos" applied_for = ["PULL_THROUGH_CACHE"] # Обязательный аргумент: к каким действиям применяется шаблон custom_role_arn = "arn:aws:iam::111111111111:role/ecr-template" encryption_configuration { encryption_type = "KMS" kms_key = "arn:aws:kms:eu-central-1:111111111111:key/some-key-123" } resource_tags = { ManagedBy = "Me" } lifecycle_policy = jsonencode({ rules = [{ rulePriority = 1 description = "Dumb cleanup" selection = { tagStatus = "any" countType = "sinceImagePushed" countUnit = "days" countNumber = 14 } action = { type = "expire" } }] })}
Внимательный читатель дочитавший до сюда скажет:
— Падажите! А как это решает изначально описанную проблемму с одним центральным регионом? Да, теперь у вас есть образы и локально, но если в каком‑то регионе сервиса не было, а потом его запустили, то контейнеры уже и не стартанут, ведь регион с апстрим репозиторием отвалился!
Да, это правда, но во‑первых у нас теперь есть требуемые образы во всех регионах, и контейнеры могут скейлиться, а во вторых мы можем оперативно поменять апстрим в `aws_ecr_pull_through_cache_rule.main.upstream_registry_url` если регион прилег надолго, что кстати вот недавно случилось с регионом в ОАЭ(а у нас такой риск был помечен как очень маловероятный!).
Итог
Мы получили систему, которая обслуживает себя сама:
-
Нужен образ в регионе Сан-Паулу? Просто
docker pull. Disaster recovery из коробки: образы доступны везде, где они реально нужны, а не там, куда мы заранее догадались их положить. -
Образ устарел? Политика тихо его удалит. И можно не бояться что продакшен сломаеться!
-
Образ снова понадобился через год? Снова
docker pull— и он опять здесь. -
Zero-touch management. Настроил шаблоны — и забыл; новые сервисы заводятся сами.
-
Платим только за то, что действительно используется в регионе (кэш), а не за копию всего архива × 20.
Больше никаких многократных terraform apply для каждого нового микросервиса в каждом из 20 регионов!
И, пожалуй, главный урок: иногда лучшее решение проблемы синхронизации — это избавиться от синхронизации!
Бонус для тех, кто дочитал:
Автор ужасный зануда, и ему нравится AWS ECR, но ещё больше ему нравится производительность команды AWS ECR. За год, который прошёл с момента написания черновика этой статьи, они выпустили ещё больше крутых фич, которые сделают многим жизнь с образами легче:
-
Archive storage class + архивирование по last-pull-time. Фича которую мы ждали очень долго, я упоминал по ссылке про ишью на GitHub, но реализовали они это через двойное правило сначала правило
sinceImagePulledи архивация, потомsinceImageTransitionedи там уже можно указать expired. Они утверждали что метрикаsinceImagePulledне надежна, потому они перестраховываються. Ещё нюанс: в архиве образ обязан пролежать минимум 90 дней, прежде чем его можно удалить, что тоже стоит денег. -
Cross-repository layer sharing (blob mounting) — шаринг слоев между репозиториями, экономия денег.
-
CREATE_ON_PUSH — Автосоздание репозитория на push. Тоже может быть полезно, шаблоны в том числе применяются и на репозиторий созданный так.
-
Pull-through cache: синк referrers — теперь вместе с образом автоматически подтягиваются подписи, SBOM и attestations из апстрима
-
Managed image signing — управляемая подпись образов без своей инфраструктуры подписи
-
Pull-through для Chainguard — Chainguard-реестр как upstream
-
И всякие мелочи типа поддержки IPv6 и новые метрики для подсчета репозиториев и образов
Посему всем, кто пользуется, категорически рекомендую регулярно наведываться в блог AWS или в документацию. Удачи вам и вашим контейнерам!
ссылка на оригинал статьи https://habr.com/ru/articles/1049920/