GitHub Actions kan ikke nå dit VPN: selvhostet eller tunnel
En hostet runner er en frisk VM uden for dit netværk. Sæt enten en runner ind i det netværk, eller tunnel fra jobbet og vent på en rigtig rute.
Dit deploy-trin hænger på et internt værtsnavn, mens den samme kommando svarer med det samme fra din laptop, og klientbygget på begge maskiner er identisk. Jobbet kører på en GitHub-hostet runner: en virtuel maskine oprettet til netop det job inde i GitHubs sky, uden rute ind i dit private adresserum, uden resolver-post for den interne zone og uden certifikat, som den private endpoint vil acceptere. Kun to designs passer til den ramme, og ingen af dem kan slås til senere uden at bygge workflower om. Vælg mellem dem før den første linje YAML, for migrering bagefter koster mere end det første forsøg.
Valget står mellem en runner placeret der, hvor netværket allerede slutter, og en tunnel, der findes så længe et job varer. Begge har båret produktionsdeploys i årevis, og de fejler på forskellige måder.
Hvor grænsen faktisk ligger
Alt, hvad du installerer på din laptop, stopper ved din laptop, og alt, hvad du installerer under et job, forsvinder med runneren. Der findes ikke et vedvarende netværksnamespace, som en klient kan blive hængende i mellem kørsler, så hver kørsel begynder i samme utilgængelige tilstand. Azure dokumenterer denne grænse for Microsoft-hostede agenter, hvor VPN- og ExpressRoute-links ind i private netværk ikke understøttes, og GitHub-hostede runners bærer den samme form for begrænsning i stedet for en separat mekanisme. Læs det som en produktegenskab, ikke som en fejl, der skal routes udenom.
Ingen repository-indstilling lemper på det. Jobbets label afgør, hvilken pool der tager arbejdet op, og et hostet label betyder en maskine uden for din perimeter i hele kørslen.
Privat tilgængelighed skal derfor komme fra noget, du ejer. En runner placeret der, hvor netværket allerede slutter, eller en tunnel, der lever et enkelt job. Resten af artiklen handler om at betale for en af dem bevidst i stedet for at opdage regningen i produktion.
Afgør efter kørselsfrekvens, ikke efter vane
Tag en selvhostet runner i VPC'en eller kontornetværket, når deploys lander flere gange om dagen, når jobbet rører mere end én privat port, eller når et platformsteam allerede ejer patching og credential-rotation. Et job på den maskine resolver og router som enhver anden host bag firewalls. Netværksarbejdet sker én gang, ved installationen, i stedet for ved hver kørsel.
Byg tunnelen inde i jobbet, når adgangen er sporadisk, og du ikke vil have en langlivet agent, der holder en credential ind i dit netværk. Jobbet bliver sin egen netværksklient i et par minutter og efterlader intet, hvilket ofte er hele pointen.
Vurdér tunnelens pris, før du vælger den. Hver kørsel betaler for at materialisere profilen, starte klienten, vente på parathed og rive ned. En tunnel, der kommer op, men aldrig verificeres, giver et deploy, der fejler halvvejs og efterlader privat tilstand i en form, ingen har planlagt. Gentagelsen bagefter er sjældent ren, og den anden fejl bliver som regel skudt på klientversionen.
Skriv profilen, så trinnet ikke kan afbrydes
Hold profiler og credentials ude af repositoryet og materialisér dem fra secrets i det trin, der har brug for dem. Rækkefølgen afgør, om jobbet overlever: en fil oprettet under sudo tilhører root, og trinnets uprivilegerede shell kan derefter ikke skrive i den.
- 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"Begge detaljer i den blok findes for senere trin. Med umask 077 lander profilen på tilstand 0600 ejet af runner-brugeren, sudo kan stadig læse den, og klienten skriver sin log til en fil, som runneren allerede ejer, så parathedsløkken kan læse loggen uden et ekstra sudo. --writepid registrerer det PID, som nedrivningen skal ramme.
En profil, der autentificerer med et klientcertifikat, skal have sin nøgle skrevet på samme måde, og en static-challenge-profil skal have den matchende auth-tilstand. Profiler, der pusher DNS-optioner, skal have openvpn-systemd-resolved på Ubuntu og Debian, fordi pushet alene ikke ændrer noget på et basisimage.
Parathed er en probe, ikke en pause
En fast sleep holder præcis indtil koncentratoren er langsom, og så kører deploy-trinnet, før ruten findes. Fejlen dukker derefter op inde i deploy-værktøjet som en timeout mod den private host, hvilket intet siger om tunnelen.
- 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 1Løkken ser på to observationer, fordi hver af dem kan være den ødelagte halvdel. getent hosts beviser, at den pushede resolver svarer for det navn, og socket-åbningen beviser, at noget lytter på den port, deployet skal bruge; en profil med kun en rute har brug for den bogstavelige adresse, og linjen getent skal slettes.
Når løkken udløber, er loggen det eneste, der skiller tre forskellige fejl: et afvist certifikat, en forkert auth-tilstand og en rute, der aldrig nåede frem. Filtrering før tail holder outputtet kort nok til at læse, mens jobbet stadig fejler. Læs den første fejl i stedet for den sidste linje, for en klient, der ikke kan autentificere, gentager sig, indtil løkken giver op. ip -4 route show dev tun0 svarer på det enklere spørgsmål, om der overhovedet blev pushet en rute, og det koster én linje i samme løkke.
Nedrivning og spørgsmålet om artefakten
Afslut jobbet med et trin, der er beskyttet af if: always() og kører sudo kill "$(sudo cat "$RUNNER_TEMP/openvpn.pid")" og intet andet. På en delt selvhostet runner gælder: dræb aldrig på processnavn: et andet job kan eje en klient med samme binær og lignende argumenter. Fejlen lander så på det job, der kørte sidst, og dets log peger på en workflow, der ikke gjorde noget galt.
Beskyttelsen betyder noget, fordi et mislykket deploy stadig efterlader en levende tunnel. Uden always() springes oprydningstrinnet over præcis, når det er nødvendigt, og næste kørsel starter mod en klient, der allerede er forbundet.
Et spørgsmål står tilbage, før du tager en af de to designs i brug: har byggetrinnet overhovedet brug for en privat rute? En deployment-agent, der allerede sidder inde i netværket, kan hente en godkendt artefakt, hvilket holder almindelige builds væk fra den private vej. Færre job med en tunnel betyder færre steder, hvor en forældet rute kan gemme sig.