9 хв читанняІнтеграції

Скорочувач URL у GitLab CI: короткі посилання з кожного пайплайна

Використовуйте GitLab CI як скорочувач URL: створюйте короткі посилання на review app для кожного merge request і перенаправляйте стабільне посилання latest на тегах за допомогою замаскованого захищеного ключа.

Marius Voß
DevRel · edge infra
Пайплайн скорочувача URL у GitLab CI, намальований як піксельні етапи, де build, test і deploy завершуються завданням link, що записує коротке посилання для кожного merge request і перенаправляє посилання latest на тегах

Так, GitLab CI може працювати як ваш скорочувач URL. Завдання з curl і одним API-ключем може створювати коротке посилання для кожного merge request, спрямовувати його на review app і переміщувати стабільне посилання latest на новий реліз, коли ви надсилаєте тег. Це близько сорока рядків YAML. І це вже працює.

Люди помиляються не в HTTP-виклику. Проблема в ключі: де він зберігається, які пайплайни можуть його читати і якої шкоди він може завдати, якщо завдання з гілки його витече. Тому в цьому посібнику змінним і обмеженню доступу приділено не менше уваги, ніж самому .gitlab-ci.yml. Якщо ви волієте декларативно керувати довгоживучими посиланнями, краще підійде підхід із Terraform до коротких посилань; пайплайни добре підходять для посилань, які з'являються та прибираються разом із кодом.

Спершу коротка примітка про статус. Нативна інтеграція Elido з GitLab готується, але ще не працює, і нижче вона не потрібна. Хочете керовану версію? На сторінці інтеграції з GitLab є список очікування.

Що робить завдання скорочувача URL у GitLab CI

Скорочувач у пайплайні робить рівно три речі. Він створює посилання, якщо slug ще не існує, оновлює призначення, якщо він уже є, і вимикає посилання, коли зникає те, на що воно вказувало. Аналітика та QR-коди залишаються на боці Elido.

Поверхня API невелика. Посилання розташовані в /v1/workspaces/{workspace_id}/links: POST створює посилання і потребує domain_id та destination_url, PATCH /links/{link_id} змінює поля наявного посилання, а GET /links?q= шукає за slug, призначенням або назвою. Для автентифікації потрібен один заголовок: Authorization: Bearer elido_.... Ключ береться зі сторінки API-ключів у дашборді.

Ось і весь контракт. В огляді API та SDK перелічено решту endpoint, але пайплайну рідко потрібно більше за ці три.

Зберігання ключа як замаскованої захищеної змінної

GitLab дає вам два важливі перемикачі, і вони виконують різні завдання. Маскування приховує значення в журналах завдань. Захист визначає, які пайплайни взагалі отримують це значення.

У Settings, CI/CD, Variables під час створення змінної виберіть Masked and hidden. Hidden (зазвичай доступне починаючи з GitLab 17.6) означає, що пізніше ніхто не зможе розкрити значення на сторінці налаштувань, а саме цього й потрібно для облікових даних. У документації GitLab про змінні CI/CD наведено вимоги до замаскованого значення: один рядок, без пробілів, щонайменше 8 символів. Ключі Elido складаються з elido_ і base32, тож відповідають вимогам.

На тій самій сторінці прямо сказано про обмеження: маскування "is not a guaranteed way to prevent malicious users from accessing variable values." Завдання, яке кодує змінну в base64 і виводить її, легко обходить маскування. Сприймайте маскування як гігієну журналів, а не як контроль доступу.

Захист і є контролем доступу. Захищена змінна потрапляє лише в пайплайни на захищених гілках або захищених тегах, і тут виникає проблема, з якою кожна команда стикається в перший тиждень: ваш пайплайн merge request працює на функціональній гілці, тому захищений ключ надходить як порожній рядок, а завдання завершується помилкою 401, схожою на друкарську помилку.

Я вирішив би це двома ключами замість послаблення обмежень одного. Ось налаштування, яким я б користувався:

ЗміннаВидимістьЗахищенаЧитає
ELIDO_PREVIEW_KEYMasked and hiddenНіПайплайни merge request
ELIDO_RELEASE_KEYMasked and hiddenТакПайплайни тегів на захищених тегах
ELIDO_PREVIEW_WS, ELIDO_RELEASE_WSVisibleНіБудь-яке завдання (ідентифікатори не є секретами)
ELIDO_DOMAIN_ID, SHORT_HOSTVisibleНіБудь-яке завдання

Ключ для попереднього перегляду належить окремому робочому простору, у якому зберігаються лише посилання на перевірки. Будь-хто, хто може надіслати гілку, теоретично може викрасти незахищену змінну, тож переконайтеся, що найгірше, до чого вона дає доступ, - це купа тимчасових посилань mr-142, тоді як ключ релізу живе у вашому справжньому робочому просторі й працює лише на захищених вами тегах.

Надайте обом ключам роль Editor і строк дії; для ключа попереднього перегляду підійде 90 днів. Editor - це найнижча готова роль, що може записувати посилання, і вона також може їх видаляти; API-ключі використовують одну з готових ролей, а я хотів би мати готову роль лише для створення й оновлення саме для такого випадку, але її ще немає. Саме розділення робочих просторів реально обмежує радіус ураження.

Робоче завдання .gitlab-ci.yml для створення короткого посилання

Ось спільна частина: операція upsert шукає slug, створює посилання, якщо його немає, і в іншому разі змінює його. Помістіть її в приховане завдання та розширюйте його.

.elido_upsert:
  image: alpine:3.20
  before_script:
    - apk add --no-cache curl jq
  script:
    - API="https://api.elido.app/v1/workspaces/${ELIDO_WS}"
    - AUTH="Authorization: Bearer ${ELIDO_KEY}"
    - |
      find_id() {
        curl -sS --fail-with-body -H "$AUTH" "$API/links?q=${SLUG}&limit=50" |
          jq -r --arg s "$SLUG" --argjson d "$ELIDO_DOMAIN_ID" \
            '.items[] | select(.slug == $s and .domain_id == $d) | .id' | head -n1
      }
      ID="$(find_id)"
      if [ -z "$ID" ]; then
        CODE=$(curl -sS -o resp.json -w '%{http_code}' -X POST "$API/links" \
          -H "$AUTH" -H "Content-Type: application/json" \
          -H "Idempotency-Key: ${CI_PIPELINE_ID}-${SLUG}" \
          -d "$(jq -n --arg s "$SLUG" --arg u "$TARGET" --argjson d "$ELIDO_DOMAIN_ID" \
                '{domain_id: $d, slug: $s, destination_url: $u, tags: ["ci"]}')")
        case "$CODE" in
          201) ;;
          409) ID="$(find_id)" ;;   # another pipeline created it first
          *) cat resp.json; exit 1 ;;
        esac
      fi
      if [ -n "$ID" ]; then
        curl -sS --fail-with-body -X PATCH "$API/links/$ID" \
          -H "$AUTH" -H "Content-Type: application/json" \
          -d "$(jq -n --arg u "$TARGET" '{destination_url: $u, status: "active"}')"
      fi
    - echo "SHORT_URL=https://${SHORT_HOST}/${SLUG}" >> link.env
  artifacts:
    reports:
      dotenv: link.env

Пошук q збігається з підрядком, тому фільтр jq звужує результат до точного slug на точному домені. Без нього пошук web-mr-14 охоче повернув би web-mr-142. Одного разу отримайте свій domain_id через GET /v1/workspaces/{id}/domains і збережіть його як звичайну змінну; брендований хост, налаштований через кастомні домени, у merge request виглядає краще за загальний.

Життєвий цикл короткого посилання GitLab review app: пайплайн merge request створює або оновлює slug, записує SHORT_URL у звіт dotenv, який використовується як URL середовища, а завдання зупинки вимикає посилання, коли merge request закривається

Короткі посилання review app для кожного merge request

Review apps - це назва GitLab для тимчасового середовища на гілку або merge request, а документація review apps будує їх на динамічних середовищах. Їхні URL зазвичай непривабливі: хеш, простір імен, хостнейм хмарного провайдера. Коротке посилання на кшталт go.example.com/web-mr-142 можна вимовити вголос на щоденному стендапі.

review_link:
  extends: .elido_upsert
  stage: deploy
  needs: [deploy_review]
  variables:
    ELIDO_KEY: $ELIDO_PREVIEW_KEY
    ELIDO_WS: $ELIDO_PREVIEW_WS
    SLUG: "${CI_PROJECT_NAME}-mr-${CI_MERGE_REQUEST_IID}"
    TARGET: "https://${CI_ENVIRONMENT_SLUG}.review.example.com"
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: $SHORT_URL
    on_stop: stop_review_link
    auto_stop_in: 1 week
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

Секрет тут - звіт dotenv. Операція upsert записує SHORT_URL у link.env, GitLab читає його назад, а environment:url стає коротким посиланням, тож кнопка View app у merge request відкриває web-mr-142, а не необроблений хостнейм. Документація про середовища описує цей шаблон динамічного URL.

CI_MERGE_REQUEST_IID унікальний у межах проєкту й не змінюється протягом життя merge request, тому кожне надсилання змін до того самого MR потрапляє на той самий slug, а операція upsert змінює посилання, а не створює дублікати. У довіднику попередньо визначених змінних є повний список, якщо вам потрібен інший ключ.

Очищення виконує завдання з action: stop. Воно має використовувати ті самі rules, що й завдання запуску, інакше GitLab не зможе запустити його автоматично:

stop_review_link:
  image: alpine:3.20
  stage: deploy
  variables:
    GIT_STRATEGY: none
    SLUG: "${CI_PROJECT_NAME}-mr-${CI_MERGE_REQUEST_IID}"
  script:
    - apk add --no-cache curl jq
    - API="https://api.elido.app/v1/workspaces/${ELIDO_PREVIEW_WS}"
    - ID=$(curl -sS -H "Authorization:
        Bearer ${ELIDO_PREVIEW_KEY}" "$API/links?q=${SLUG}" |
        jq -r --arg s "$SLUG" --argjson d "$ELIDO_DOMAIN_ID" '.items[] | select(.slug == $s and .domain_id == $d) | .id' | head -n1)
    - '[ -z "$ID" ] || curl -sS --fail-with-body -X PATCH "$API/links/$ID" -H "Authorization: Bearer ${ELIDO_PREVIEW_KEY}" -H "Content-Type: application/json" -d "{\"status\":\"disabled\"}"'
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop
  when: manual
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

Я вимикаю посилання, а не видаляю його. Вимкнене посилання зберігає історію кліків, і якщо хтось знову відкриє MR, наступний пайплайн знову переведе його в active через ту саму операцію upsert. GIT_STRATEGY: none потрібен, бо на той момент гілки може вже не бути.

Якщо кількість ваших review apps у десять разів перевищує кількість релізів, саме тут починають діяти ліміти плану. Перевірте дозволену кількість посилань на сторінці цін, перш ніж підключати це до активного монорепозиторію, і створіть безкоштовний робочий простір для попередніх переглядів на час тестування.

Перенаправлення стабільного посилання latest у пайплайнах тегів

Другий шаблон запускається на тегах і робить протилежне до посилання перевірки: це один slug, який ніколи не змінюється, а його призначення рухається вперед із кожним релізом. Ваш README може назавжди посилатися на go.example.com/cli-latest.

latest_link:
  extends: .elido_upsert
  stage: release
  variables:
    ELIDO_KEY: $ELIDO_RELEASE_KEY
    ELIDO_WS: $ELIDO_RELEASE_WS
    SLUG: "cli-latest"
    TARGET: "${CI_PROJECT_URL}/-/releases/${CI_COMMIT_TAG}"
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/

Поєднайте це правило із шаблоном захищених тегів на кшталт v*, щоб лише супроводжувачі могли створювати теги, які його запускають; інакше захищеного ключа просто не буде, і завдання завершиться з помилкою, що є потрібною поведінкою. Якщо вам також потрібне постійне посилання на кожну версію, вдруге запустіть те саме завдання з SLUG: "cli-${CI_COMMIT_REF_SLUG}", яке перетворить v1.4.0 на cli-v1-4-0.

Не встановлюйте redirect_status у 301 для посилання latest. 301 - це постійна обіцянка, яку браузери можуть кешувати, а посилання latest порушує цю обіцянку з кожним релізом. Elido за замовчуванням використовує 302, якщо залишити поле порожнім, а в нашому матеріалі про редиректи 301 проти 302 розглянуто випадки, де цей вибір справді має значення.

Є одна чесна примітка: зміненому призначенню може знадобитися кілька хвилин, щоб дістатися кожної edge-локації, тому smoke-тест, який виконує curl короткого посилання вже наступної секунди, може все ще побачити попередній реліз. Перевіряйте відповідь API або зачекайте перед перевіркою заголовка Location.

Принцип найменших привілеїв для скорочувача URL у GitLab CI: незахищений ключ попереднього перегляду, обмежений робочим простором попередніх переглядів для пайплайнів merge request, і захищений ключ релізу, який можуть читати лише пайплайни захищених тегів для перенаправлення посилання latest

Ідемпотентність, повторні спроби та ліміти запитів

Пайплайни повторюються. Ранери завершуються посеред завдання, хтось натискає Retry на червоному завданні, а два надсилання змін відбуваються з інтервалом у тридцять секунд і змагаються між собою. Операція upsert вище переживає всі три випадки, і варто знати чому.

Заголовок Idempotency-Key робить повторний POST безпечним: API кешує успішну відповідь протягом 24 годин і відтворює її для того самого ключа, тож повтор тієї самої спроби пайплайна повертає початкове посилання замість помилки. Побудова ключа з CI_PIPELINE_ID і slug означає, що повтори в межах одного пайплайна відтворюють результат, а новий пайплайн отримує нову спробу. Гілка 409 обробляє змагання між двома різними пайплайнами, а шлях пошуку з подальшою зміною робить другий запуск фактично порожньою операцією.

Ліміти запитів діють для кожного ключа додатково до ліміту робочого простору, а щойно створені робочі простори також мають нижчий денний ліміт створення посилань, поки набирають репутацію. Кілька merge request цього не помітять. Монорепозиторій, який одночасно запускає сорок review apps, може помітити, тож вважайте 429 придатним для повторної спроби за допомогою ключового слова retry у GitLab і гучно завершуйтеся з помилкою 402, яка означає ліміт плану, а не тимчасову помилку. У нашій детальнішій статті про ліміти запитів та ідемпотентність API скорочувача відкладені повтори розглянуто докладніше, ніж це потрібно завданню CI.

Пропустіть фільтр точного збігу jq, і пайплайн MR 14 непомітно змінить посилання MR 142. Першою ознакою зазвичай стає розгублений дизайнер. Залиште фільтр.

Якщо shell у YAML стає незручним, ті самі виклики можна акуратно загорнути в скрипт, який ви додасте до репозиторію, а посібник зі скорочувача URL у CLI показує такий підхід.

Читайте матеріал-основу → Короткі посилання як Terraform: керування посиланнями як кодом

Пов'язані матеріали в блозі

Поширені запитання

Чи може GitLab CI створювати короткі посилання?

Так. Будь-яке завдання, здатне запустити curl, може викликати REST API скорочувача URL, тож завдання GitLab CI може створити коротке посилання, оновити його призначення або вимкнути його. API-ключ зберігається в замаскованій змінній CI/CD, а завдання надсилає його як Bearer-токен. Для цього не потрібна нативна інтеграція з GitLab.

Як безпечно зберігати API-ключ у GitLab CI?

Додайте його в Settings, CI/CD, Variables, встановивши видимість Masked and hidden, і ввімкніть Protect variable, якщо читати його мають лише захищені гілки або теги. Маскування прибирає значення з журналів завдань, але власна документація GitLab зазначає, що це не гарантований захист, тож сам ключ потрібно обмежити мінімально необхідними правами.

Чому моя захищена змінна порожня в пайплайні merge request?

Захищені змінні передаються лише в пайплайни, що працюють на захищених гілках або захищених тегах. Пайплайн merge request із функціональної гілки за замовчуванням не відповідає цій умові, тому змінна надходить порожньою. Використовуйте окремий незахищений ключ із меншими правами для завдань перевірки або залиште захищений ключ лише для пайплайнів тегів.

Як надати кожному GitLab review app коротке посилання?

Запустіть завдання в пайплайнах merge request, яке створює або оновлює slug, побудований з назви проєкту та CI_MERGE_REQUEST_IID, і спрямовує його на URL review app. Запишіть отриманий короткий URL у звіт dotenv і використайте його як environment:url, щоб віджет merge request вів прямо на нього. Завдання зупинки вимикає посилання, коли середовище зупиняється.

Для короткого посилання на останній реліз використовувати редирект 301 чи 302?

Використовуйте 302. Посилання latest змінює призначення з кожним релізом, а 301 повідомляє браузерам і кешам, що переміщення є постійним, тож деякі клієнти продовжать надсилати людей на стару версію. Elido за замовчуванням встановлює для нових посилань 302, якщо не вказати redirect_status, і це правильний вибір у цьому випадку.

Чи є для Elido нативна інтеграція з GitLab?

Ще ні. Нативна інтеграція з GitLab готується, і ви можете долучитися до списку очікування на сторінці інтеграції з GitLab. Усе в цьому посібнику вже працює через публічний REST API із завдання пайплайна, і на боці GitLab не потрібно нічого встановлювати, крім змінної CI/CD.

Спробуйте Elido

Вставте URL - отримайте коротке посилання

Без реєстрації. Посилання живе 30 днів. Зареєструйтесь, щоб зберегти назавжди.

Безкоштовно, без реєстрації · 2 на день

Спробуйте Elido

URL-скорочувач із хостингом у ЄС: власні домени, глибока аналітика, відкритий API. Безкоштовний тариф - без кредитної картки.

Теги
gitlab ci url shortener
create short link gitlab pipeline
gitlab review app short link
gitlab ci masked variable api key
short link per merge request
latest release short link

Читати далі