Ваш крок деплою зависає на внутрішньому імені хоста, хоча та сама команда миттєво відповідає з ноутбука, а збірка клієнта на обох машинах ідентична. Job виконується на runner, розміщеному GitHub: віртуальній машині, створеній для одного цього job у хмарі GitHub, без маршруту до вашого приватного адресного простору, без запису resolver для внутрішньої зони й без сертифіката, який прийме приватний endpoint. Цьому обмеженню відповідають лише дві схеми, і жодну не можна ввімкнути пізніше, не перебудовуючи workflow. Виберіть між ними до першого рядка YAML, бо міграція заднім числом коштує дорожче за першу спробу.

Вибір між runner, розміщеним там, де мережа вже закінчується, і тунелем, що живе рівно один job. Обидва роками тягнуть продакшн-деплої й відмовляють по-різному.

Де насправді проходить межа

Усе, що ви ставите на ноутбук, зупиняється на ноутбуці, а все, що ставите під час job, зникає разом із runner. Постійного мережевого namespace, до якого клієнт міг би лишитися прив’язаним між запусками, немає, тож кожен запуск починається в тому самому недосяжному стані. Azure документує цю межу для агентів, розміщених Microsoft: зв’язки VPN і ExpressRoute у приватні мережі там не підтримуються, а runner, розміщені GitHub, несуть ту саму форму обмеження, а не окремий механізм. Читайте це як властивість продукту, а не як дефект, який треба обійти.

Жодне налаштування репозиторію цього не пом’якшує. Мітка job вирішує, який пул візьме роботу, а розміщена мітка означає машину поза вашим периметром на весь запуск.

Приватна доступність тому має походити з того, що належить вам. Runner там, де мережа вже закінчується, або тунель, що живе один job. Решта статті про те, щоб заплатити за один із варіантів свідомо, а не виявити рахунок у продакшні.

Вирішуйте за частотою запусків, а не за звичкою

Беріть власний runner у VPC або офісній мережі, коли деплої приходять кілька разів на день, коли job торкається більше одного приватного порту або коли команда платформи вже відповідає за патчі й ротацію облікових даних. Job на такій машині резолвить і маршрутизує як будь-який інший хост за файрволом. Мережева робота робиться один раз, під час встановлення, а не під час кожного запуску.

Будуйте тунель усередині job, коли доступ буває зрідка і вам не потрібен довгоживучий агент із обліковими даними у вашу мережу. Job на кілька хвилин стає власним мережевим клієнтом і нічого по собі не лишає — часто в цьому вся суть.

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

Писати профіль так, щоб крок не міг перерватися

Тримайте профілі й облікові дані поза репозиторієм і матеріалізуйте їх із секретів у тому кроці, якому вони потрібні. Порядок вирішує, чи виживе job: файл, створений під sudo, належить root, і непривілейована оболонка кроку вже не може до нього писати.

- name: Bring up OpenVPN
  run: |
    umask 077
    profile="$RUNNER_TEMP/client.ovpn"
    printf '%s\n' "$VPN_PROFILE" > "$profile"
    : > "$RUNNER_TEMP/openvpn.log"
    printf '%s\n' "$VPN_CREDENTIALS" > "$RUNNER_TEMP/credentials.txt"
    sudo openvpn --daemon openvpn-job \
      --config "$profile" \
      --auth-user-pass "$RUNNER_TEMP/credentials.txt" \
      --writepid "$RUNNER_TEMP/openvpn.pid" \
      --log "$RUNNER_TEMP/openvpn.log"

Обидві деталі в цьому блоці існують для наступних кроків. З umask 077 профіль потрапляє в режим 0600 і належить користувачу runner, sudo досі може його читати, а клієнт пише свій лог у файл, яким runner уже володіє, тож цикл готовності читає цей лог без другого sudo. --writepid записує PID, на який цілитиметься демонтаж.

Профілю з автентифікацією за клієнтським сертифікатом потрібен ключ, записаний так само, а профілю зі статичним викликом — відповідний режим автентифікації. Профілям, що проштовхують опції DNS, потрібен openvpn-systemd-resolved на Ubuntu і Debian, бо саме проштовхування на базовому образі нічого не змінює.

Готовність — це проба, а не пауза

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

- name: Await private endpoint
  run: |
    for attempt in $(seq 1 30); do
      if getent hosts private.example.internal > /dev/null && \
         timeout 3 bash -c 'exec 3<>/dev/tcp/private.example.internal/8443' 2>/dev/null; then
        echo "endpoint answered after $attempt attempts"
        exit 0
      fi
      sleep 2
    done
    grep -Ei 'error|verify|auth' "$RUNNER_TEMP/openvpn.log" | tail -40
    exit 1

Цикл дивиться на два спостереження, бо кожне з них може бути зламаною половиною. getent hosts доводить, що проштовхнутий resolver відповідає за це ім’я, а відкриття сокета доводить, що на порту, який використає деплой, хтось слухає; профілю лише з маршрутом потрібна буквальна адреса і видалений рядок getent.

Коли цикл вичерпується, лог — єдине, що розділяє три різні несправності: відхилений сертифікат, хибний режим автентифікації й маршрут, який так і не прийшов. Фільтрація перед tail тримає цей вивід досить коротким, щоб його можна було прочитати, поки job ще падає. Читайте першу помилку, а не останній рядок, бо клієнт, який не може автентифікуватися, повторюється, доки цикл не здасться. ip -4 route show dev tun0 відповідає на простіше питання, чи маршрут узагалі проштовхнули, і коштує одного рядка в тому ж циклі.

Демонтаж і питання про артефакт

Закривайте job кроком під захистом if: always(), який виконує sudo kill "$(sudo cat "$RUNNER_TEMP/openvpn.pid")" і більше нічого. На спільному власному runner ніколи не вбивайте за іменем процесу: інший job може володіти клієнтом із тим самим бінарником і схожими аргументами. Помилка тоді падає на job, який відпрацював другим, і його лог указує на workflow, що нічого не порушив.

Захист важливий, бо невдалий деплой усе одно лишає живий тунель. Без always() крок очищення пропускається рівно тоді, коли він потрібен, і наступний запуск стартує проти вже підключеного клієнта.

Лишається одне питання, перш ніж ухвалити будь-яку зі схем: чи потрібен стадії збірки приватний маршрут узагалі? Агент деплою, який уже сидить усередині мережі, може забрати схвалений артефакт, і це тримає звичайні збірки осторонь приватного шляху. Менше job, що тримають тунель, — менше місць, де може сховатися застарілий маршрут.

Схожі статті