Ditt deploy-steg hänger på ett internt värdnamn medan samma kommando svarar direkt från din laptop, och client-bygget på båda maskinerna är identiskt. Jobbet körs på en GitHub-hostad runner: en virtuell maskin skapad för just det jobbet inne i GitHubs moln, utan route in i ditt privata adressutrymme, utan resolver-post för den interna zonen och utan certifikat som den privata endpointen accepterar. Bara två konstruktioner passar den ramen, och ingen av dem kan slås på senare utan att workflowen byggs om. Välj mellan dem före första raden YAML, för att migrera i efterhand kostar mer än första försöket.

Valet står mellan en runner placerad där nätverket redan slutar och en tunnel som finns så länge ett jobb varar. Båda har burit produktionsdeployer i åratal, och de misslyckas på olika sätt.

Var gränsen faktiskt går

Allt du installerar på din laptop stannar vid din laptop, och allt du installerar under ett jobb försvinner med runner. Det finns ingen beständig nätverksnamespace som en client kan hänga kvar i mellan körningar, så varje körning börjar i samma onåbara tillstånd. Azure dokumenterar denna gräns för Microsoft-hostade agenter, där VPN- och ExpressRoute-länkar in i privata nätverk inte stöds, och GitHub-hostade runners bär samma slags begränsning i stället för en egen mekanism. Läs det som en produktegenskap, inte som en defekt att routa runt.

Ingen repository-inställning mjukar upp det. Jobbets label avgör vilken pool som tar upp arbetet, och en hostad label betyder en maskin utanför din perimeter under hela körningen.

Privat nåbarhet måste därför komma från något du äger. En runner placerad där nätverket redan slutar, eller en tunnel som lever ett jobb. Resten av artikeln handlar om att betala för en av dem medvetet i stället för att upptäcka räkningen i produktion.

Avgör efter körfrekvens, inte efter vana

Ta en egenhostad runner i VPC:n eller kontorsnätet när deployer landar flera gånger om dagen, när jobbet rör mer än en privat port, eller när ett plattformsteam redan äger patchning och credential-rotation. Ett jobb på den maskinen resolvar och routar som vilken annan host som helst bakom brandväggen. Nätarbetet sker en gång, vid installationen, i stället för vid varje körning.

Bygg tunneln inne i jobbet när åtkomsten är sporadisk och du inte vill ha en långlivad agent som håller en credential in i ditt nät. Jobbet blir sin egen nätverksklient i några minuter och lämnar inget efter sig, vilket ofta är hela poängen.

Uppskatta tunnelns kostnad innan du väljer den. Varje körning betalar för att materialisera profilen, starta klienten, vänta på beredskap och riva ned. En tunnel som kommer upp men aldrig verifieras ger en deploy som faller halvvägs och lämnar privat tillstånd i en form ingen planerat. Omförsöket efteråt är sällan rent, och det andra felet skylls oftast på klientversionen.

Skriv profilen så att steget inte kan avbrytas

Håll profiler och credentials utanför repositoryt och materialisera dem från secrets i det steg som behöver dem. Ordningen avgör om jobbet överlever: en fil skapad under sudo tillhör root, och stegets oprivilegierade shell kan då inte skriva 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"

Båda detaljerna i det blocket finns för senare steg. Med umask 077 hamnar profilen på läge 0600 ägd av runner-användaren, sudo kan fortfarande läsa den, och klienten skriver sin logg till en fil som runnern redan äger, så beredskapsloopen kan läsa loggen utan ett andra sudo. --writepid registrerar det PID som nedrivningen ska rikta in sig på.

En profil som autentiserar med ett klientcertifikat behöver sin nyckel skriven på samma sätt, och en static-challenge-profil behöver matchande auth-läge. Profiler som pushar DNS-optioner behöver openvpn-systemd-resolved på Ubuntu och Debian, eftersom pushen i sig inte ändrar något på en basimage.

Beredskap är en probe, inte en paus

En fast sleep håller precis till dess att koncentratorn är långsam, och då kör deploy-steget innan routen finns. Felet dyker då upp inne i deploy-verktyget som en timeout mot den privata hosten, vilket inte säger något om tunneln.

- 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

Loopen tittar på två observationer eftersom vilken som helst av dem kan vara den trasiga halvan. getent hosts bevisar att den pushade resolvern svarar för det namnet, och socket-öppningen bevisar att något lyssnar på den port som deployen ska använda; en profil med bara route behöver den litterala adressen, och raden getent ska tas bort.

När loopen löper ut är loggen det enda som skiljer tre olika fel åt: ett avvisat certifikat, ett felaktigt auth-läge och en route som aldrig kom. Att filtrera före tail håller utdata kort nog att läsa medan jobbet fortfarande misslyckas. Läs det första felet i stället för sista raden, för en klient som inte kan autentisera upprepar sig tills loopen ger upp. ip -4 route show dev tun0 svarar på den enklare frågan om en route pushades över huvud taget, och det kostar en rad i samma loop.

Nedrivning och artefaktfrågan

Avsluta jobbet med ett steg som skyddas av if: always() och som kör sudo kill "$(sudo cat "$RUNNER_TEMP/openvpn.pid")" och inget annat. På en delad egenhostad runner gäller: döda aldrig på processnamn: ett annat jobb kan äga en klient med samma binär och liknande argument. Felet landar då på det jobb som körde sist, och dess logg pekar på en workflow som inte gjorde något fel.

Skyddet spelar roll eftersom en misslyckad deploy ändå lämnar en levande tunnel. Utan always() hoppas städsteget över precis när det behövs, och nästa körning startar mot en klient som redan är ansluten.

En fråga återstår innan du antar någon av konstruktionerna: behöver byggsteget alls en privat route? En deployment-agent som redan sitter inne i nätet kan hämta en godkänd artefakt, vilket håller vanliga byggen borta från den privata vägen. Färre jobb som håller en tunnel betyder färre ställen där en gammal route kan gömma sig.

Relaterade artiklar