Uw deploy-stap blijft hangen op een interne hostnaam, terwijl hetzelfde commando vanaf uw laptop direct antwoordt, en de client-build op beide machines identiek is. De job draait op een door GitHub gehoste runner: een virtuele machine die voor die ene job in de GitHub-cloud is aangemaakt, zonder route naar uw private adresruimte, zonder resolver-entry voor de interne zone en zonder certificaat dat het private endpoint accepteert. Slechts twee ontwerpen passen bij die beperking, en geen van beide is later aan te zetten zonder de workflow opnieuw te bouwen. Kies er vóór de eerste regel YAML tussen, want later migreren kost meer dan de eerste poging.

De keuze gaat tussen een runner op de plek waar het netwerk al eindigt en een tunnel die bestaat zolang één job duurt. Beide dragen al jaren productie-deploys, en ze falen op verschillende manieren.

Waar de grens werkelijk ligt

Alles wat u op uw laptop installeert, stopt bij uw laptop, en alles wat u tijdens een job installeert, verdwijnt met de runner. Er is geen persistente netwerk-namespace waarin een client tussen runs kan blijven hangen, dus elke run begint in dezelfde onbereikbare staat. Azure documenteert deze grens voor door Microsoft gehoste agents, waar VPN- en ExpressRoute-koppelingen naar private netwerken niet worden ondersteund, en door GitHub gehoste runners dragen die vorm van beperking in plaats van een apart mechanisme. Lees het als een producteigenschap, niet als een defect om omheen te werken.

Geen enkele repository-instelling versoepelt dit. Het job-label bepaalt welke pool het werk oppakt, en een gehost label betekent voor de hele run een machine buiten uw perimeter.

Private bereikbaarheid moet dus komen van iets dat u bezit. Een runner op de plek waar het netwerk al eindigt, of een tunnel die één job leeft. De rest van dit artikel gaat over bewust voor een van die twee betalen in plaats van de rekening in productie te ontdekken.

Beslis op runfrequentie, niet op vertrouwdheid

Pak een zelf gehoste runner in de VPC of het kantoornetwerk wanneer deploys meerdere keren per dag landen, wanneer de job meer dan één private poort raakt, of wanneer een platformteam al patching en credential-rotatie bezit. Een job op die machine resolvet en routeert als elke andere host achter de firewall. Het netwerkwerk gebeurt één keer, bij installatie, in plaats van bij elke run.

Bouw de tunnel in de job wanneer toegang incidenteel is en u geen langlevende agent wilt die een credential naar uw netwerk vasthoudt. De job wordt voor een paar minuten zijn eigen netwerkclient en laat niets achter, wat vaak het hele punt is.

Schat de kosten van de tunnel voordat u hem kiest. Elke run betaalt voor het materialiseren van het profiel, het starten van de client, gereedheid en afbreken. Een tunnel die opkomt maar nooit wordt geverifieerd, levert een deploy op die halverwege faalt en private state achterlaat in een vorm die niemand had gepland. De herhaling daarna is zelden schoon, en de tweede fout wordt meestal op de clientversie geschoven.

Het profiel zo schrijven dat de stap niet kan afbreken

Houd profielen en credentials buiten de repository en materialiseer ze uit secrets in de stap die ze nodig heeft. De volgorde bepaalt of de job het overleeft: een bestand dat onder sudo is aangemaakt, is van root, en de niet-geprivilegieerde shell van de stap kan er daarna niet in schrijven.

- 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"

Beide details in dat blok bestaan voor latere stappen. Met umask 077 landt het profiel op modus 0600 in bezit van de runner-gebruiker, sudo kan het nog steeds lezen, en de client schrijft zijn log naar een bestand dat de runner al bezit, zodat de gereedheidslus dat log zonder tweede sudo kan lezen. --writepid legt de PID vast waarop het afbreken zich richt.

Een profiel dat met een clientcertificaat authenticeert, heeft zijn sleutel op dezelfde manier geschreven nodig, en een static-challenge-profiel heeft de bijpassende auth-modus nodig. Profielen die DNS-opties pushen, hebben openvpn-systemd-resolved nodig op Ubuntu en Debian, omdat de push op een basisimage alleen niets verandert.

Gereedheid is een probe, geen pauze

Een vaste sleep houdt stand tot de concentrator traag is, en dan draait de deploy-stap voordat de route bestaat. De fout komt dan in de deploy-tool naar boven als een timeout tegen de private host, wat niets over de tunnel zegt.

- 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

De lus kijkt naar twee waarnemingen omdat elk van beide de kapotte helft kan zijn. getent hosts bewijst dat de gepushte resolver voor die naam antwoordt, en het openen van de socket bewijst dat er iets luistert op de poort die het deploy gaat gebruiken; een profiel met alleen een route heeft het letterlijke adres nodig, en de regel getent moet worden verwijderd.

Wanneer de lus verloopt, is het log het enige dat drie verschillende fouten scheidt: een geweigerd certificaat, een verkeerde auth-modus en een route die nooit aankwam. Filteren vóór de tail houdt die uitvoer kort genoeg om te lezen terwijl de job nog faalt. Lees de eerste fout in plaats van de laatste regel, want een client die niet kan authenticeren herhaalt zich tot de lus opgeeft. ip -4 route show dev tun0 beantwoordt de eenvoudigere vraag of er überhaupt een route is gepusht, en kost één regel in dezelfde lus.

Opruimen, en de vraag over het artefact

Sluit de job af met een stap die door if: always() wordt beschermd en die sudo kill "$(sudo cat "$RUNNER_TEMP/openvpn.pid")" en niets anders uitvoert. Op een gedeelde zelf gehoste runner geldt: nooit op procesnaam doden: een andere job kan een client met hetzelfde binary en vergelijkbare argumenten bezitten. De fout landt dan bij de job die als tweede liep, en het log daarvan wijst naar een workflow die niets fout deed.

De bescherming is belangrijk omdat een mislukte deploy nog steeds een levende tunnel achterlaat. Zonder always() wordt de opruimstap precies overgeslagen wanneer die nodig is, en de volgende run start tegen een client die al verbonden is.

Eén vraag blijft over voordat u een van beide ontwerpen overneemt: heeft de build-stage überhaupt een private route nodig? Een deployment-agent die al in het netwerk zit, kan een goedgekeurd artefact ophalen, wat gewone builds bij het private pad weghoudt. Minder jobs die een tunnel vasthouden betekent minder plekken waar een verouderde route zich kan verstoppen.

Gerelateerde artikelen