Разработчик, которому сказали «мы переезжаем на собственный GitLab», обычно мысленно составляет такой список того, что должно быть на новом месте:
- репозиторий со всеми ветками, тегами и историей;
- issues и merge requests, на которые ссылаются коммиты;
- образы контейнеров в registry, из которых деплоится прод;
- pipeline, который собирает, тестирует и выкатывает;
- секреты: токены деплоя, ключи, пароли от внешних сервисов.
Пять пунктов выглядят как одна кнопка «Перенести». На деле они разъезжаются по трём разным маршрутам, а часть вообще не поедет сама. Если понять это до переезда, неделя нервной отладки превращается в аккуратную работу по списку. Если после — получается классическая картина: репозиторий на месте, зелёная галочка в интерфейсе есть, а релиз не собирается, потому что pipeline ищет переменную, которой больше не существует.
Чтобы не рассуждать абстрактно, возьмём один условный проект и один его релиз и проведём этот релиз по всем частям системы.
Один релиз как маршрут: приложение «Лампа» версии 1.4.0
Представим небольшой сервис «Лампа»: веб-приложение, которое упаковывается в контейнер и выкатывается на сервер. Это придуманный пример, но структура у него типичная. В репозитории есть код, Dockerfile и файл .gitlab-ci.yml. Pipeline по тегу вида v1.4.0 собирает образ, кладёт его в container registry проекта и затем по SSH или через API раскатывает на стенд. Для этого ему нужны переменные: токен деплоя, адрес стенда, ключ доступа, пароль от внешнего почтового сервиса. Кроме того, у проекта настроен webhook, который сообщает в чат о завершённых сборках.
Теперь проследим, что произойдёт с каждым звеном при переезде:
- Репозиторий и метаданные проекта переносятся через экспорт и импорт проекта.
- Образы в registry экспортом не затрагиваются, их надо переносить отдельно.
- Переменные CI/CD в архив не попадают, их придётся восстановить руками или из внешнего хранилища секретов.
- Webhooks и pipeline triggers тоже нужно создавать заново.
- Runner — вообще не часть проекта, а инфраструктура, и новому GitLab нужен свой исполнитель.
Каждое звено требует отдельного решения. Пойдём по порядку, начав с того, что действительно лежит в архиве.
Что даёт экспорт проекта, а чего в нём нет
Встроенный механизм экспорта и импорта проекта — естественная отправная точка. Он упаковывает проект в архив, который можно загрузить в другой экземпляр GitLab. Но важна не столько сама возможность, сколько её границы, и документация описывает их достаточно прямо. Согласно официальному описанию Project import/export, в экспорт не входят CI/CD variables, образы package и container registry, job traces и artifacts, pipeline triggers, webhooks и ряд других элементов.
Прочитаем это применительно к нашему релизу 1.4.0. Код и история переедут. А вот всё, из чего релиз «собирается в жизнь», останется на старой площадке:
- образ
lampa:1.4.0из registry; - токен деплоя и пароль от почтового сервиса;
- логи и артефакты прошлых сборок;
- webhook в чат и триггер, которым внешняя система запускала pipeline.
Здесь полезна одна привычка: относиться к архиву проекта как к переносу содержимого репозитория и связанных с ним метаданных, а не как к резервной копии аккаунта на GitLab.com. Архив одного проекта — не снимок всей SaaS-инстанции, и обещать, что из него получится восстановить «всё, как было», нельзя. Документация к тому же называет ограничения, которые влияют на планирование. Совместимость версий между источником и приёмником не безгранична, а размер архива имеет предел. Поэтому крупный проект с тяжёлой историей лучше не переносить впервые в день переключения. Сначала на репетиции выясняется, принимает ли ваш экземпляр этот архив вообще.
И ещё одно требование из той же документации: импортировать следует архивы доверенного происхождения. Для собственного переезда это легко выполнить — архив вы выгрузили сами и положили на свой диск. Но правило стоит держать в голове, если кто-то из коллег присылает «архив проекта» в мессенджере. Чужой архив — это входные данные, которые ваш сервер будет разбирать, и относиться к нему стоит как к любому недоверенному файлу.
Как репетировать импорт
Репетиция импорта — это не «нажмём и посмотрим». Полезнее заранее решить, что именно вы сравниваете после загрузки:
- число веток и тегов в старом и новом репозитории, а также указатели ключевых тегов на коммиты;
- наличие и читаемость issues и merge requests, на которые ссылаются коммиты и документация;
- настройки проекта: защищённые ветки, правила слияния, участники;
- что именно не появилось — и есть ли это в вашем списке на ручное восстановление.
Последний пункт самый ценный. Расхождение, которое вы предсказали, — это пункт плана. Расхождение, которое вы обнаружили на проде, — это инцидент.
Образы контейнеров: registry переезжает отдельным рейсом
Вернёмся к «Лампе». Pipeline по тегу v1.4.0 кладёт образ в registry проекта, а стенд при деплое тянет его оттуда. Экспорт проекта образы не переносит, поэтому после импорта registry нового проекта пуст. Если просто переключить деплой на новый адрес, первый же выкат упадёт с сообщением, что образ не найден. Это самый честный вид отказа: понятный и быстрый. Хуже другой сценарий, когда на стенде остался закэшированный старый образ и деплой «успешно» запускает версию, которой в новом registry нет.
Поэтому registry стоит рассматривать как самостоятельный перенос с собственной инвентаризацией. Для этого полезно ответить на три вопроса.
Какие образы нужны на самом деле
В registry за годы накапливаются сотни тегов: сборки веток, отладочные версии, временные образы для проверок. Переносить их все — значит платить дисковым пространством за мусор. Разумный критерий выбора выглядит так:
- теги, соответствующие выпущенным релизам, которые вы можете захотеть откатить;
- образ, который сейчас работает в проде и на стенде (его адрес и дайджест можно прочитать с самих серверов);
- базовые образы, на которые опираются ваши Dockerfile и CI-файлы, если вы их собираете сами.
Всё остальное можно не переносить, а при необходимости пересобрать из тега в репозитории. Это сильный довод в пользу переноса именно после импорта кода: репозиторий с историей — это источник истины, а образ — производное.
Как перенести выбранное
Технически перенос образа — это скачивание из старого registry и загрузка в новый тем же контейнерным клиентом. Адреса ниже условные:
docker pull registry.gitlab.com/example-group/lampa:1.4.0
docker tag registry.gitlab.com/example-group/lampa:1.4.0 registry.example.org/example-group/lampa:1.4.0
docker push registry.example.org/example-group/lampa:1.4.0Перед этим нужна авторизация в обоих registry, и вводить пароли в историю командной оболочки не стоит: для этого у клиента есть вход с чтением пароля со стандартного ввода. По тегам прогоняется простой цикл, но смысл не в нём, а в проверке после. Сравните дайджесты исходного и перенесённого образа. Если они совпали, вы перенесли тот же образ, а не «что-то с тем же именем».
Где всё это будет жить
Registry — самая прожорливая по диску часть переезда. Слои образов копятся быстро, а при частых сборках ещё и дублируются. Поэтому при выборе сервера под собственный GitLab полезно считать не только размер репозиториев, но и объём образов, которые вы собираетесь держать. На VPS с NVMe-диском скорость чтения и записи слоёв обычно перестаёт быть узким местом, а вот объём диска — остаётся. В линейке MAATRIX есть конфигурации от 40 до 320 ГБ NVMe, но эти цифры — характеристики тарифов, а не обещание вместимости под вашу нагрузку. Реальный запас вы определите по инвентаризации: сколько занимают нужные теги плюс сколько новых образов вы будете выпускать в неделю.
Защищённые переменные: переносить владельцев, а не значения
Теперь самая деликатная часть. Переменные CI/CD в экспорт проекта не входят, и это, пожалуй, к лучшему: секреты не должны путешествовать в архивах вместе с кодом. Но у разработчика остаётся вопрос: а как тогда их восстановить?
Важно понимать две вещи. Во-первых, если переменная помечена как masked, интерфейс вам её значение не покажет, и рассчитывать на «выгрузку» нельзя. Во-вторых, это не повод искать обходные пути вытащить значение. Это повод считать переезд плановой ротацией секретов: самое естественное решение — выпустить новые значения у тех, кто за них отвечает.
Поэтому инвентаризация переменных строится не вокруг значений, а вокруг ответов на вопросы «что это», «кто владеет» и «как получить заново». Вот как это может выглядеть для «Лампы» (значений в таблице нет и быть не должно):
| Переменная | Назначение | Владелец | Как восстановить |
|---|---|---|---|
| DEPLOY_TOKEN | Доступ деплоя к registry | Тимлид или админ GitLab | Выпустить новый токен на новом инстансе |
| STAGING_SSH_KEY | Подключение к стенду | Админ стенда | Создать новую пару ключей, добавить публичную часть на стенд, старую убрать |
| SMTP_PASSWORD | Отправка писем | Владелец почтового сервиса | Сгенерировать новый пароль или ключ в самом сервисе |
| CHAT_WEBHOOK_URL | Уведомления о сборках | Владелец чата | Создать новый входящий вебхук в чате |
Обратите внимание на колонку «Как восстановить». Почти везде ответ — «выпустить новое». Это не недостаток, а возможность: за время жизни проекта токены обрастают лишними правами, ключи оказываются у людей, давно покинувших команду, а пароли дублируются между проектами. Переезд — редкий момент, когда есть законное основание всё это зачистить.
Есть и порядок действий, который снижает риск:
- Составьте реестр по образцу таблицы выше — только имена, назначение, владельцы, способ восстановления.
- Выпускайте новые значения для нового окружения, не отзывая старые до успешного релиза.
- Заносите значения в новый проект напрямую, без пересылки через чаты и почту.
- После переключения отзывайте старые секреты: они остались на площадке, которую вы больше не контролируете как основную.
Отдельно о хранении. Если секретов много, их владельцем разумнее сделать внешнее хранилище, а CI-переменные использовать как точку подключения. Тогда следующий переезд будет проще, потому что значения не привязаны к конкретному GitLab.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Ссылки, webhooks и внешние интеграции
Пока вы занимались образами и секретами, в проекте тихо остаётся ещё один слой: всё, что ссылается на старый адрес. Он не ломается громко, а протухает постепенно. Для начала вспомним, что webhooks и pipeline triggers, по описанию в документации по экспорту проекта, в архив не входят. Значит, после импорта в проекте нет ни уведомлений в чат, ни триггера, которым внешняя система запускала сборку.
Но это только очевидная часть. Остальное придётся искать самим, и полезно пройтись по репозиторию поиском старого домена и пути. Что обычно находится:
- Dockerfile и compose-файлы с образами из старого registry в строках
FROMиimage; - .gitlab-ci.yml с подключаемыми шаблонами и скриптами по старому адресу;
- скрипты деплоя, которые тянут образ или артефакт по полному пути;
- README и документация с командами клонирования и бейджами статуса сборки;
- зависимости из git-адресов в файлах менеджера пакетов, если какие-то библиотеки подтягиваются прямо из соседних репозиториев;
- внешние сервисы, где прописан адрес вашего GitLab: системы мониторинга, трекеры задач, чаты, платформы деплоя.
Последний пункт самый коварный, потому что находится за пределами репозитория. Для него реестр интеграций ведётся отдельно, по той же схеме, что и реестр секретов: что подключено, кто владелец, что нужно переключить. Если в реестре есть хотя бы одна строка «никто не помнит, кто это настраивал», лучше выяснить это до переключения, а не по звонку от коллеги, у которого перестали приходить уведомления.
Один практичный приём: на период репетиции оставьте на старой площадке заметное предупреждение в описании проекта о том, что работа ведётся на новом адресе. Тогда разработчик, который по привычке откроет старую закладку, поймёт, куда идти, а не будет час писать коммиты в архивное место.
Runner: исполнитель, у которого нет адреса прописки
Теперь о том, что чаще всего забывают, потому что оно не выглядит частью проекта. Pipeline — это описание шагов, а выполняет их runner. В облачном GitLab у вас, вероятно, были либо предоставленные платформой исполнители, либо собственные. Новый экземпляр — это отдельная система со своим набором исполнителей, и пока вы не подключите хотя бы один, pipeline будет стоять в очереди со статусом ожидания. Выглядит это странно: всё настроено, а сборка не стартует.
Собственный runner имеет смысл рассматривать как отдельную задачу с тремя вопросами.
Где его запускать
Runner может жить на том же сервере, что и сам GitLab, но у такого решения есть цена: тяжёлая сборка и работа веб-интерфейса начинают конкурировать за процессор, память и диск. Для небольшой команды и редких сборок это приемлемо. Если сборки частые и объёмные, а контейнеры собираются прямо в pipeline, исполнителя удобнее вынести на отдельный VPS. Тогда падение или перегрузка сборочной машины не затронет доступность самого GitLab. Какая именно конфигурация нужна, определяется замером: возьмите самый тяжёлый pipeline проекта и посмотрите, сколько он потребляет процессора и памяти. Цифры из каталога тарифов — лишь верхняя граница того, что вы можете арендовать, но не мерило нагрузки. Как поднять сам GitLab на VPS, разобрано в статье про установку и настройку GitLab CE, и после неё удобно переходить к исполнителям.
Как его подключить
Runner регистрируется на конкретном экземпляре GitLab с использованием токена, который выдаёт именно этот экземпляр. Старые токены и регистрации к новой системе отношения не имеют, поэтому переносить с прежнего места тут нечего: исполнитель создаётся заново, и для него стоит выбрать понятные метки, по которым pipeline будет его находить. Если в .gitlab-ci.yml для заданий указаны теги исполнителей, их нужно сверить с тем, как вы назвали новый. Расхождение в одном слове даёт ту же картину зависшего pipeline.
К чему ему нужен доступ
Здесь полезно мыслить от минимума. Чему исполнитель обязан иметь доступ, чтобы выпустить «Лампу» 1.4.0?
- к новому GitLab — клонировать код и отдавать результаты;
- к новому registry — публиковать образ;
- к стенду — выкатывать, но только на стенд, а не на прод, пока вы репетируете;
- к интернету или внутреннему зеркалу — скачивать зависимости.
Каждый из этих доступов обеспечивается секретом из вашего реестра. Если доступ выдан шире, чем нужно, это стоит заметить сейчас, пока всё записано на бумаге, а не потом, когда он «просто работает» годами.
Ещё одна мелочь, о которой полезно знать заранее: job traces и artifacts старых сборок, как сказано в документации по экспорту, в архив не входят. Если для вас важна история логов — например, по требованиям аудита, — нужно решить отдельно, сохраняете ли вы её и где. Обычно для разработки достаточно того, что новые сборки пишут новые логи, но решение должно быть осознанным, а не обнаруженным постфактум.
Репетиционный релиз: v1.4.0-rc1 на новом месте
Теперь у нас есть импортированный проект, перенесённые образы, восстановленные переменные, пересозданные webhooks и подключённый runner. Но всё это пока гипотеза. Проверяется она одним способом: выпустить релиз из нового проекта так, как вы делаете это обычно. Не копией, не вручную, а полноценным путём от тега до работающего стенда.
Для «Лампы» репетиция может выглядеть так:
- Создать в новом проекте тег v1.4.0-rc1 на том же коммите, что и боевой релиз 1.4.0.
- Дождаться, пока pipeline запустится на новом runner. Если задания висят в очереди — вернуться к меткам и регистрации.
- Проверить, что собранный образ появился в новом registry, и сравнить его с перенесённым образом 1.4.0: состав и поведение должны совпасть, хотя побитовое равенство дайджестов для пересборки не гарантируется.
- Выкатить образ на стенд с новыми секретами и убедиться, что приложение отвечает, письма уходят, а чат получает уведомление через новый webhook.
- Откатить стенд на перенесённый образ 1.4.0 — чтобы проверить, что путь назад тоже работает, а не только путь вперёд.
Пятый пункт обычно пропускают, а зря. Откат — это именно то действие, которое понадобится в самый неудобный момент. И если оно впервые выполняется в ту самую минуту, то выясняется всё сразу: тег не перенесён, ключ не подходит, а стенд тянет образ по старому адресу.
После репетиции полезно ответить на несколько контрольных вопросов. Остался ли хоть один шаг, который вы сделали руками и не можете воспроизвести по записи? Остались ли в логах сборки обращения к старому адресу? Приходят ли уведомления туда, куда должны, и только туда? Каждое «да» в первых двух вопросах — задача до переключения, а не после.
Как выглядит удачная репетиция
Она скучная. Pipeline отрабатывает, образ собирается, стенд обновляется, откат проходит. Никто ничего не чинит по ходу. Если на репетиции вы за вечер починили шесть вещей, это успех репетиции, а не повод её игнорировать: ровно эти шесть вещей не сломали вам боевой день.
Только когда такая репетиция проходит без вмешательств, имеет смысл назначать день переключения. Он тоже должен быть скучным: заморозить изменения в старом проекте, сделать свежий экспорт, импортировать его, довести до конца уже отрепетированный список по образам и секретам, перенаправить интеграции. Старый проект на это время лучше оставить доступным только для чтения или по крайней мере с заметным предупреждением, чтобы коммиты не расходились по двум площадкам.
Когда репозиторий — это ещё не проект
Удобно считать переезд законченным, когда в новом GitLab открывается репозиторий, а история на месте. Но репозиторий — лишь одна из пяти вещей из списка, с которого мы начали. Настоящий критерий готовности другой: команда может делать на новой площадке то, ради чего GitLab вообще существует.
Поэтому последний тест — не технический, а человеческий. Пусть кто-то из команды, не участвовавший в переезде, возьмёт небольшую задачу и пройдёт весь цикл на новом месте:
- создаст ветку и откроет merge request;
- получит прогон pipeline и увидит результаты тестов;
- пройдёт ревью, включая настроенные правила слияния и защищённые ветки;
- вольёт изменения и выпустит следующую версию — «Лампу» 1.4.1 с одной мелкой правкой;
- увидит свежий образ в новом registry, выкат на стенд и сообщение в чате;
- при необходимости откатит выпуск, и всё это — без обращения к старой площадке.
Если этот цикл проходит и в какой-то момент никто не говорит «подожди, это надо делать по-старому», переезд завершён. До этого момента у вас есть копия кода на новом сервере — полезная, но не заменяющая рабочий процесс. А когда релиз 1.4.1 выходит и все забывают, что вообще куда-то переезжали, значит, работа сделана хорошо.