Когда наша команда начала расширять инфраструктуру — с одного кластера 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 до того, как это за вас сделает продакшн.