GitHub Actions nie dociera do Twojej sieci VPN: własny runner albo tunel
Hostowany runner to świeża maszyna wirtualna poza Twoją siecią. Albo umieść runner w tej sieci, albo zrób tunel z joba i poczekaj na prawdziwą trasę.
Twój krok wdrożenia zawiesza się na wewnętrznej nazwie hosta, podczas gdy to samo polecenie odpowiada natychmiast z laptopa, a kompilacja klienta na obu maszynach jest identyczna. Job działa na runnerze hostowanym przez GitHub: maszynie wirtualnej utworzonej dla tego jednego joba w chmurze GitHuba, bez trasy do Twojej prywatnej przestrzeni adresowej, bez wpisu resolvera dla strefy wewnętrznej i bez certyfikatu, który zaakceptuje prywatny endpoint. W to ograniczenie wpisują się tylko dwa projekty i żadnego nie da się włączyć później bez przebudowy workflow. Wybierz między nimi przed pierwszą linią YAML, bo migracja po fakcie kosztuje więcej niż pierwsza próba.
Wybór jest między runnerem umieszczonym tam, gdzie sieć już się kończy, a tunelem, który istnieje przez czas jednego joba. Oba od lat obsługują wdrożenia produkcyjne i oba zawodzą w różny sposób.
Gdzie naprawdę przebiega granica
Wszystko, co zainstalujesz na laptopie, kończy się na laptopie, a wszystko, co zainstalujesz podczas joba, znika razem z runnerem. Nie ma trwałej przestrzeni nazw sieci, do której klient mógłby pozostać przypięty między uruchomieniami, więc każde uruchomienie zaczyna się w tym samym nieosiągalnym stanie. Azure dokumentuje tę granicę dla agentów hostowanych przez Microsoft, gdzie łącza VPN i ExpressRoute do sieci prywatnych nie są obsługiwane, a runnery hostowane przez GitHub niosą ten sam rodzaj ograniczenia zamiast odrębnego mechanizmu. Czytaj to jako właściwość produktu, nie jako defekt do obejścia.
Żadne ustawienie repozytorium tego nie luzuje. Etykieta joba decyduje, która pula podejmie pracę, a etykieta hostowana oznacza maszynę poza Twoim obwodem przez całe uruchomienie.
Prywatna osiągalność musi więc pochodzić z czegoś, co posiadasz. Runner umieszczony tam, gdzie sieć już się kończy, albo tunel, który żyje jeden job. Reszta artykułu dotyczy świadomego zapłacenia za jedno z tych rozwiązań zamiast odkrycia rachunku na produkcji.
Decyduj według częstotliwości uruchomień, nie według oswojenia
Sięgnij po własny runner w VPC lub sieci biurowej, gdy wdrożenia trafiają kilka razy dziennie, gdy job dotyka więcej niż jednego prywatnego portu albo gdy zespół platformy już zajmuje się łataniem i rotacją poświadczeń. Job na tej maszynie rozwiązuje nazwy i routuje jak każdy inny host za zaporą. Praca sieciowa odbywa się raz, przy instalacji, zamiast przy każdym uruchomieniu.
Zbuduj tunel wewnątrz joba, gdy dostęp jest okazjonalny, a nie chcesz długowiecznego agenta trzymającego poświadczenie do Twojej sieci. Job staje się własnym klientem sieci na kilka minut i nie zostawia nic po sobie, co często jest całą istotą sprawy.
Oszacuj koszt tunelu, zanim go wybierzesz. Każde uruchomienie płaci za materializację profilu, start klienta, gotowość i demontaż. Tunel, który wstaje, ale nigdy nie jest weryfikowany, daje wdrożenie, które zawodzi w połowie i zostawia prywatny stan w kształcie, którego nikt nie zaplanował. Ponowna próba po tym rzadko jest czysta, a drugą awarię zwykle przypisuje się wersji klienta.
Pisanie profilu tak, by krok nie mógł się przerwać
Trzymaj profile i poświadczenia poza repozytorium i materializuj je z sekretów w kroku, który ich potrzebuje. Kolejność decyduje, czy job przetrwa: plik utworzony pod sudo należy do roota, a nieuprzywilejowana powłoka kroku nie może już do niego pisać.
- 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"Oba szczegóły w tym bloku istnieją dla późniejszych kroków. Dzięki umask 077 profil trafia w trybie 0600 i należy do użytkownika runnera, sudo nadal może go czytać, a klient zapisuje swój log do pliku, który runner już posiada, więc pętla gotowości może czytać ten log bez drugiego sudo. --writepid zapisuje PID, na który będzie celował demontaż.
Profil uwierzytelniający się certyfikatem klienta wymaga klucza zapisanego w ten sam sposób, a profil z wyzwaniem statycznym wymaga odpowiedniego trybu uwierzytelniania. Profile wypychające opcje DNS wymagają openvpn-systemd-resolved na Ubuntu i Debianie, bo samo wypchnięcie niczego nie zmienia w obrazie bazowym.
Gotowość to sonda, nie pauza
Stały sleep wytrzymuje dokładnie do chwili, gdy koncentrator zwalnia, a wtedy krok wdrożenia rusza, zanim trasa istnieje. Błąd wychodzi potem w narzędziu wdrożeniowym jako timeout do prywatnego hosta, co nic nie mówi o tunelu.
- 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 1Pętla obserwuje dwie rzeczy, bo każda z nich może być zepsutą połową. getent hosts dowodzi, że wypchnięty resolver odpowiada dla tej nazwy, a otwarcie gniazda dowodzi, że coś nasłuchuje na porcie, którego użyje wdrożenie; profil z samą trasą wymaga dosłownego adresu i usuniętej linii getent.
Gdy pętla wygaśnie, log jest jedyną rzeczą, która rozdziela trzy różne awarie: odrzucony certyfikat, zły tryb uwierzytelniania i trasę, która nigdy nie dotarła. Filtrowanie przed tail utrzymuje ten wynik na tyle krótki, by dało się go przeczytać, gdy job wciąż zawodzi. Czytaj pierwszy błąd, a nie ostatnią linię, bo klient, który nie potrafi się uwierzytelnić, powtarza się, aż pętla się podda. ip -4 route show dev tun0 odpowiada na prostsze pytanie, czy trasa została w ogóle wypchnięta, i kosztuje jedną linię w tej samej pętli.
Demontaż i kwestia artefaktu
Zamknij job krokiem strzeżonym przez if: always(), który uruchamia sudo kill "$(sudo cat "$RUNNER_TEMP/openvpn.pid")" i nic więcej. Na współdzielonym własnym runnerze nigdy nie zabijaj po nazwie procesu: inny job może mieć klienta z tym samym binarium i podobnymi argumentami. Awaria spada wtedy na job, który ruszył drugi, a jego log wskazuje na workflow, który nie zrobił nic złego.
Zabezpieczenie ma znaczenie, bo nieudane wdrożenie i tak zostawia żywy tunel. Bez always() krok sprzątający jest pomijany dokładnie wtedy, gdy jest potrzebny, a następne uruchomienie startuje przeciwko klientowi, który już jest połączony.
Zostaje jedno pytanie, zanim przyjmiesz którykolwiek z projektów: czy etap kompilacji w ogóle potrzebuje prywatnej trasy? Agent wdrożeniowy, który już siedzi w sieci, może pobrać zatwierdzony artefakt, co trzyma zwykłe kompilacje z dala od prywatnej ścieżki. Mniej jobów trzymających tunel to mniej miejsc, w których może się ukryć nieaktualna trasa.