Мультирегиональный ACR для AKS: уроки и ошибки

Когда наша команда начала расширять инфраструктуру — с одного кластера AKS в регионе East US до кластеров в West Europe и Southeast Asia — мы наивно решили, что управление образами контейнеров окажется самой простой частью. Мы ошиблись.

В этой статье я расскажу, как мы выстроили архитектуру единого приватного реестра контейнеров (container registry), доступного всем шести нашим кластерам AKS в трёх регионах Azure. Разберу, что сработало, что незаметно ломалось неделями, и что я сделал бы иначе сегодня.

С чего мы начинали

Изначально у нас был один кластер AKS и один Azure Container Registry (ACR) в том же регионе. Конвейер CI/CD собирал образ, отправлял его в ACR, и AKS его забирал. Всё просто.

Затем мы добавили второй кластер в West Europe — ради снижения задержек. Потом третий в Southeast Asia — по требованиям compliance. И внезапно перед нами встала задача, которая на первый взгляд кажется тривиальной, но скрывает множество подводных камней: как организовать безопасную загрузку образов для каждого кластера, не управляя кучей учётных данных и не переплачивая за трансграничный исходящий трафик (cross-region egress).

Что мы рассматривали

Прежде чем остановиться на финальной архитектуре, мы изучили три подхода.

Вариант 1: один центральный ACR, все кластеры тянут из него

Самый простой вариант, который мы и попробовали в первую очередь. Все кластеры просто указывали на один и тот же эндпоинт ACR, независимо от региона. На тестах всё работало нормально. В продакшне обнаружились два серьёзных недостатка. Во-первых, задержки при загрузке образов во время обновления кластеров стали заметны: каждый узел тянул большие образы через длинный сетевой путь. Во-вторых, однажды ночью в East US случился кратковременный сбой связи — кластеры в других регионах не смогли загрузить образы, и перезапуски подов начали падать. Реестр в одном регионе превратился в единую точку отказа (single point of failure) для всей платформы.

Вариант 2: отдельный ACR в каждом регионе

Мы рассматривали вариант с независимым ACR в каждом регионе и отправкой образов во все три из конвейера. Это решало проблему задержек и доступности, но порождало худшую: теперь конвейер при каждой сборке должен был пушить в три реестра. Продвижение образов между окружениями превратилось в хаос. А поддерживать согласованность дайджестов (digests) между реестрами оказалось труднее, чем ожидалось: разные операции push под нагрузкой иногда давали немного разные метаданные в зависимости от тайминга.

Вариант 3: гео-репликация ACR (ACR Geo-Replication)

Именно на этом мы и остановились. Premium-SKU в ACR поддерживает гео-репликацию: вы сохраняете логически единый реестр, а Azure автоматически реплицирует образы в выбранные вами регионы. Отправляете один раз — каждая региональная реплика получает образ. Кластеры в каждом регионе автоматически тянут с ближайшей реплики, без каких-либо изменений в ссылках на образы в манифестах.

Архитектура, которую мы используем сегодня

Вот общая картина.

Наш конвейер CI/CD в Azure DevOps собирает образ и отправляет его в единый эндпоинт ACR в East US. ACR в фоне берёт на себя репликацию в реплики в West Europe и Southeast Asia. Каждый кластер AKS аутентифицируется через управляемое удостоверение (Managed Identity), поэтому никаких секретов для загрузки образов (image pull secrets) не требуется — ротировать и управлять ими не нужно. Azure нативно обеспечивает аутентификацию между AKS и ACR.

Команда для привязки кластера к ACR выглядит просто:

az aks update \
--name my-cluster \
--resource-group my-rg \
--attach-acr my-registry

Одна эта команда выдаёт управляемому удостоверению кластера роль AcrPull на реестр. Выполните её для каждого кластера — и с учётными данными покончено.

Для гео-репликации добавляете реплики через портал или CLI:

az acr replication create \
--registry my-registry \
--location westeurope
az acr replication create \
--registry my-registry \
--location southeastasia

После этого любой образ, который вы пушите в реестр, реплицируется в оба региона в течение нескольких минут — в зависимости от размера образа.

Что ломалось и когда

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

Задержка репликации при быстрых выкатках

Первый инцидент с этой схемой был незаметным. Мы запушили новый образ и запустили выкатку буквально через секунды после завершения пуша. Кластер в East US подхватил новый образ без проблем — он тянул из локальной реплики. Но кластер в West Europe попытался загрузить образ до завершения репликации и получил ошибку «образ не найден». Поды ушли в ImagePullBackoff, и мы 20 минут недоумевали, почему одна и та же выкатка в одном регионе проходит успешно, а в другом — нет.

Исправление: добавили в конвейер проверку готовности репликации перед тем, как запускать выкатки во всех регионах.

az acr replication show \
--name westeurope \
--registry my-registry \
--query "status.displayStatus" \
--output tsv

Ждём, пока команда не вернёт Synced, и только потом разрешаем конвейеру переходить к стадии выкатки. Просто — но до этого не додумываешься, пока не обожжёшься.

Закрепление дайджестов (digest pinning) между репликами

В продакшне мы используем закрепление дайджестов, чтобы деплоилось именно то, что было проверено и одобрено. Мы предполагали, что один и тот же образ, запушенный в ACR, будет иметь одинаковый дайджест везде. В нашем случае это предположение оправдалось, но стоит явно проверить его в своей конфигурации — некоторые инструменты репликации и настройки реестров могут перезаписывать метаданные манифеста таким образом, что дайджест изменится. Если дайджесты расходятся между регионами, ваш GitOps-инструмент будет считать их разными образами — и отладка превратится в настоящий квест.

Масштабирование пула узлов с одновременными загрузками

Когда автоскейлер кластера (cluster autoscaler) добавляет несколько узлов одновременно, все они тянут один и тот же большой образ в параллель. Во время всплеска трафика мы добавили 12 узлов в Southeast Asia за две минуты. Каждый узел начал независимо загружать образ размером 2,4 ГБ. Реплика реестра справилась, но в метриках ACR мы увидели предупреждения о троттлинге, которых раньше не замечали. Мы решили эту проблему, предварительно загружая тяжёлые образы с помощью DaemonSet’ов и настроив параметры параллельной загрузки образов в конфигурации kubelet.

Стоимость исходящего трафика ненулевая

Даже при гео-репликации вы платите за трафик репликации между регионами. Для небольших команд с маленькими образами это пустяк. У нас средний размер образа — около 800 МБ по дюжине сервисов, и ежемесячные расходы на репликацию оказались ощутимы. Бюджет не лопнул, но мы не учли это в первоначальных расчётах. Закладывайте этот пункт ещё до того, как идёте к инженерному менеджеру с оценкой затрат.

Что я сделал бы иначе

Если бы начинал всё с нуля, вот что я бы изменил.

Во-первых, я бы инструментировал реестр с первого дня. ACR отдаёт метрики по задержке загрузки, частоте ошибок и лагу репликации. Мы настроили алерты только после первого инцидента. Добавляйте их заранее — до того, как они понадобятся.

Во-вторых, с самого начала я бы принудительно ввёл закрепление дайджестов в продакшне с помощью чего-то вроде Kyverno. Мы внедрили эту политику позже, и раскатить её на шесть кластеров без нарушения работающих нагрузок оказалось болезненно.

В-третьих, я бы протестировал региональный failover до того, как реальный инцидент сам проведёт этот тест. Симулируйте отказ реплики и убедитесь, что кластеры справляются с этим корректно. Мы провели такую проверку через шесть месяцев и обнаружили граничный случай: у одного кластера был устаревший DNS-кеш, из-за которого он продолжал обращаться к упавшей реплике вместо того, чтобы переключиться на рабочую. Найти это на плановых учениях в 14:00 — несравнимо лучше, чем обнаружить это во время реального инцидента в 2:00 ночи.

Итог

Гео-репликация ACR в связке с аутентификацией через Managed Identity — по-настоящему хорошее решение для мультирегиональных AKS-окружений. Операционные накладные расходы невелики по сравнению с запуском собственных инстансов Harbor или конвейерами с пушем в несколько реестров. Но, как и во всём в распределённых системах, граничные случаи прячутся в тайминге и неочевидных допущениях.

Следите за лагом репликации. Закрепляйте дайджесты. Настраивайте алерты на метрики реестра. И тестируйте failover до того, как это за вас сделает продакшн.

© 2026 meganuke