GitHub Actions не стига до вашата VPN: собствен runner или тунел
Хостваният runner е нова VM извън вашата мрежа. Или поставете runner в тази мрежа, или вдигнете тунел от job и изчакайте реален маршрут.
Стъпката ви за деплой виси на вътрешно име на хост, докато същата команда отговаря мигновено от лаптопа, а клиентската компилация на двете машини е идентична. Job се изпълнява на runner, хостван от GitHub: виртуална машина, създадена за точно този един job в облака на GitHub, без маршрут към вашето частно адресно пространство, без resolver запис за вътрешната зона и без сертификат, който частният endpoint ще приеме. Само два дизайна се побират в това ограничение и нито един не може да се включи по-късно без преизграждане на workflow. Изберете между тях преди първия ред YAML, защото миграцията след това струва повече от първия опит.
Изборът е между runner, поставен там, където мрежата вече свършва, и тунел, който живее колкото един job. И двете години наред носят продукционни деплои и се провалят по различен начин.
Къде всъщност минава границата
Всичко, което инсталирате на лаптопа, спира на лаптопа, а всичко, което инсталирате по време на job, изчезва с runner. Няма постоянен мрежов namespace, към който клиент да остане прикачен между пусканията, затова всяко пускане започва в същото недостъпно състояние. Azure документира тази граница при агентите, хоствани от Microsoft, където VPN и ExpressRoute връзки към частни мрежи не се поддържат, а runner, хоствани от GitHub, носят същата форма на ограничение вместо отделен механизъм. Четете го като свойство на продукта, не като дефект, който да се заобикаля.
Никоя настройка на репозитория не го облекчава. Етикетът на job решава кой pool ще поеме работата, а хостван етикет означава машина извън периметъра ви за цялото пускане.
Частната достъпност следователно трябва да идва от нещо, което притежавате. 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, които държат тунел, означава по-малко места, където остарял маршрут може да се скрие.