Привет, Хабр!
Если вы работаете с GitLab и у вас больше одного окружения — вы наверняка знаете этот ритуал: открываешь Settings → CI/CD → Variables, начинаешь вбивать переменные вручную, на пятой ошибаешься, на двадцатой теряешь счёт, на пятидесятой начинаешь сочувствовать тем, кто хранит секреты прямо в коде.
Я написал glenv — CLI-инструмент на Go, который синхронизирует .env файлы с GitLab CI/CD переменными через API. Под катом — история о том, почему существующих решений не хватило, как это устроено внутри и несколько примеров использования.
Предыстория
Всё началось с простой задачи: нужно было завести ~80 переменных для нового production-окружения. Существующие переменные жили в .env.production файле, который использовался локально. Оставалось только перенести их в GitLab.
Первая попытка — веб-интерфейс. Медленно, муторно, после двадцатой переменной начинаешь делать опечатки.
Вторая попытка — bash-скрипт на curl:
while IFS='=' read -r key value; do [[ "$key" =~ ^#.*$ ]] && continue curl -s -X POST "https://gitlab.com/api/v4/projects/$PROJECT_ID/variables" \ --header "PRIVATE-TOKEN: $TOKEN" \ --form "key=$key" \ --form "value=$value" done < .env.production
Работало, пока не перестало. Проблемы обнаруживались по одной: нет обработки masked/protected флагов, нет rate limiting (привет, 429), нет retry при ошибках, нет возможности посмотреть что изменится перед применением. И главное — никакого diff: запустил скрипт, получил непонятный результат, иди разбирайся.
После третьего "а что сейчас в GitLab, то же что в файле?" я решил написать нормальный инструмент.
Что уже есть и почему не подошло
Перед тем как писать своё, поискал готовые решения:
glab variable(официальный CLI) — работает с одной переменной за раз.glab variable set KEY value— это не bulk операции, это тот же ручной процесс, только в терминале.nodejs-glabenv — Node.js, базовый import/export без классификации и rate limiting. Требует Node в окружении.
gitlab-dotenv — Python, аналогично. Работает, но нет diff, нет умной классификации переменных.
Bash + curl — уже описал выше. Хрупко и без обратной связи.
Ни один не делал того, что реально нужно в production: показать diff перед применением, автоматически проставить masked/protected флаги, нормально обработать ошибки API, работать с несколькими окружениями из одного конфига.
Что умеет glenv
Коротко о возможностях:
Bulk sync — загружает весь
.envфайл в GitLab за одну командуDiff перед применением — показывает что создастся, обновится или удалится, ничего не трогая
Автоклассификация — сам определяет
masked,protectedиfile-тип по имени ключа и значениюRate limiting — token bucket, общий на всех воркеров; корректно обрабатывает 429 с
Retry-AfterMulti-environment — production, staging и любые кастомные окружения из одного YAML конфига
Export — скачивает текущие переменные из GitLab в формат
.envDry-run — показывает что произошло бы, без единого API-вызова
Self-hosted — работает с любым инстансом GitLab, настраиваемые лимиты
Написан на Go: статический бинарник, нет зависимостей рантайма, работает на Linux, macOS, Windows.
Установка
# macOS/Linux через Homebrew brew install ohmylock/tools/glenv # Через go install go install github.com/ohmylock/glenv/cmd/glenv@latest
Или скачать бинарник под свою платформу со страницы релизов.
Основной сценарий использования
1. Смотрим что изменится
Сначала всегда стоит запустить diff:
export GITLAB_TOKEN="glpat-xxxxxxxxxxxx" export GITLAB_PROJECT_ID="12345678" glenv diff -f .env.production -e production
Вывод:
+ DB_HOST=postgres.internal + DB_PORT=5432 ~ API_KEY: *** → *** [masked] - OLD_DEPRECATED_VAR = LOG_LEVEL
+ — создастся, ~ — обновится, - — удалится, = — не изменится. Masked-значения показываются как ***.
Только убедившись что всё правильно, применяем:
glenv sync -f .env.production -e production
2. Автоклассификация переменных
GitLab требует чтобы masked-переменные были однострочными, минимум 8 символов, без спецсимволов. Проставлять это руками — боль. glenv делает это автоматически:
Свойство |
Условие |
|---|---|
masked |
Ключ содержит |
protected |
Окружение |
file |
Ключ содержит |
Переменные с плейсхолдерами (your_api_key_here, CHANGE_ME, REPLACE_WITH_) автоматически пропускаются — в GitLab не попадут.
Паттерны настраиваются через конфиг. Например, если у вас есть MAX_TOKENS (лимит запросов), его не нужно маскировать:
classify: masked_exclude: - "MAX_TOKENS" - "TIMEOUT" - "PORT"
3. Несколько окружений
Создаём .glenv.yml в корне проекта:
gitlab: token: ${GITLAB_TOKEN} # поддерживается подстановка env-переменных project_id: "12345678" rate_limit: requests_per_second: 10 max_concurrent: 5 environments: staging: file: deploy/.env.staging production: file: deploy/.env.production
Теперь можно синхронизировать все окружения одной командой:
glenv sync --all
Окружения обрабатываются последовательно (в алфавитном порядке), ошибки агрегируются — если staging завалился, production всё равно попробует отработать, а в конце будет общий отчёт.
4. Export
Выгрузить текущие переменные из GitLab в файл:
glenv export -e production -o .env.production.backup
File-type переменные (сертификаты, PEM-ключи) пропускаются и заменяются комментарием # KEY (file type, skipped) — в .env формат они всё равно не влезут корректно.
5. Использование в GitLab CI
sync-variables: image: golang:1.23-alpine script: - go install github.com/ohmylock/glenv/cmd/glenv@latest - glenv sync -f deploy/.env.${CI_ENVIRONMENT_NAME} -e ${CI_ENVIRONMENT_NAME} variables: GITLAB_TOKEN: ${DEPLOY_TOKEN} GITLAB_PROJECT_ID: ${CI_PROJECT_ID}
Немного про устройство изнутри
Rate limiting
GitLab.com пропускает ~2000 запросов в минуту (~33/сек). При 5 воркерах без ограничений легко улететь в 429.
glenv использует token bucket rate limiter, который делится между всеми воркерами. По умолчанию — 10 запросов в секунду. При 429-ответе читается заголовок Retry-After, инструмент ждёт указанное время, затем повторяет попытку с экспоненциальным backoff + jitter. Максимум 3 ретрая на операцию.
Для self-hosted инстансов лимиты настраиваются:
glenv sync -f .env -e production --workers 10 --rate-limit 50
Парсинг .env
Поддерживаются:
KEY=value QUOTED="value with spaces" SINGLE_QUOTED='value' # Многострочные значения PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA... -----END RSA PRIVATE KEY-----"
Переменные с интерполяцией (${OTHER_VAR}/path) пропускаются — они скорее всего не имеют смысла как GitLab-переменные.
Diff engine
Перед применением изменений движок делает:
Парсит локальный
.envфайлПолучает текущие переменные из GitLab (с пагинацией)
Сравнивает по ключу и environment scope
Формирует список изменений: CREATE / UPDATE / DELETE / UNCHANGED / SKIPPED
При
sync— раздаёт изменения по воркер-пулу с rate limiting
Текущие ограничения
Честно о том, чего пока нет:
Только project-level переменные. Group-level — в планах, но пока не реализовано
Один проект за раз. Несколько проектов — несколько конфигов и несколько вызовов
Нет
glenv importдля копирования переменных между проектамиИнтеграционные тесты требуют реального GitLab-инстанса и
GITLAB_TEST_PROJECT_ID
Планы
Group-level переменные — управление переменными на уровне группы
glenv import— копирование переменных между проектами или инстансамиWatch mode — отслеживать изменения в
.envфайле и синхронизировать автоматическиPre-built binary в GitHub Actions — чтобы не делать
go installв каждом pipeline
Итого
glenv решает конкретную задачу: синхронизировать .env файлы с GitLab CI/CD переменными без ручной работы и без риска случайно что-то сломать. Diff перед применением, автоматическая классификация masked/protected, нормальная обработка rate limit — то, чего не хватало в существующих решениях.
Исходники: github.com/ohmylock/glenv. Буду рад вопросам, issues и PR-ам.
А как вы управляете GitLab CI/CD переменными в своих проектах? Пишете скрипты, пользуетесь официальным CLI или нашли другое решение? Расскажите в комментариях.
Комментарии (6)

1024rk
25.02.2026 10:11Эм, простите, но зачем 80 переменных в env-файле тащить в гитлаб полностью? У меня как правило (там, где деплой на один сервер) всего две переменных - токен docker registry и весь енв самого проекта в одной переменной типа file. CI читает эту переменную, source'ит из неё всё в окружение, работает прекрасно и довольно удобно.

ZiBoX Автор
25.02.2026 10:11Да все просто, я параноик и не люблю секреты держать в файлах. Сначала юзал SOPS - задолбался с ключами и доступами, особенно когда секретов стало больше десятка.
А тут GitLab из коробки дает protected/masked на каждую переменную отдельно. Плюс практичные штуки: надо глянуть какой сейчас DATABASE_URL на проде - открыл UI и видишь. CI упал - поправил один ключ за 5 секунд, не редактируя весь блоб. glenv просто мост между локальным .env и GitLab, без лишней инфраструктуры.
paramtamtam
Разрешите вопрос - а почему не мигрировали с env в сторону vault/doppler/etc? (может я был не внимателен и не заметил ответа в теле поста...)
ZiBoX Автор
Хороший вопрос. Если коротко — glenv не конкурент Vault/Doppler, он работает на другом уровне.
GitLab CI/CD переменные — это уже встроенный менеджер секретов внутри GitLab: masked, protected, environment scope, audit log. Если вы и так на
GitLab — зачем поднимать и оплачивать дополнительную инфраструктуру?
glenv решает ровно одну вещь: неудобный интерфейс управления этими переменными. Перенести 80 переменных из .env файла в GitLab через веб-UI —
мучение. glenv делает это одной командой с diff-preview.
На мой взгляд Vault/Doppler имеют смысл когда:
Нужен мультиплатформенный доступ к секретам (не только GitLab, но и K8s, AWS, etc.)
Нужна динамическая ротация, fine-grained ACL, аудит на уровне отдельного секрета
Команда работает с несколькими инструментами за пределами GitLab-экосистемы
Если же стек — это GitLab + GitLab CI, то встроенные переменные отлично справляются, а glenv просто убирает боль их обслуживания.
paramtamtam
Та я к тому, что так и нак нужен секрет для доступа к api - в вашем случае от gitlab, с случае с vault/doppler - он него. Вот только последние как раз спроектированы для работы с секретами, а gitlab env он даже называется именно env, и не очень хорош для работы с чувствительными данными, как будто в вашем случае он несколько не по прямому назначению используется что-ли (хотя и сам так и делал, и делаю в мелких проектах, но постоянно грызет совесть за это).
Так же не покидает ощущение что ваш комментарий сгенерирован ии - длинные дефисы, нет разговорных оборотов, слишком "вылизанный" текст. Я прав, или мне это показалось?
ZiBoX Автор
Да, GitLab Variables это не Vault. Но masked + protected + audit log для большинства задач хватает. Vault нужен когда динамическая ротация, lease, интеграция с k8s и тд. Для "закинуть переменные и иногда обновлять" - оверкилл.
По поводу ИИ - да, помогал структурировать, каюсь. Но суть от этого не меняется) Но если для вас это принципиально, буду писать с ошибками и сумбурно )