Το GitHub Actions δεν φτάνει το VPN σας: δικός σας runner ή τούνελ
Ένας φιλοξενούμενος runner είναι ένα φρέσκο VM έξω από το δίκτυό σας. Είτε βάλτε runner μέσα σε αυτό το δίκτυο, είτε ανοίξτε τούνελ από το job και περιμένετε πραγματική διαδρομή.
Το βήμα deploy σας κολλάει σε ένα εσωτερικό hostname, ενώ η ίδια εντολή απαντά αμέσως από το laptop σας, και το build του client είναι πανομοιότυπο και στα δύο μηχανήματα. Το job τρέχει σε runner φιλοξενούμενο από το GitHub: ένα VM που δημιουργήθηκε για αυτό το ένα job μέσα στο cloud του GitHub, χωρίς διαδρομή προς τον ιδιωτικό σας χώρο διευθύνσεων, χωρίς εγγραφή resolver για την εσωτερική ζώνη και χωρίς πιστοποιητικό που θα δεχτεί το ιδιωτικό endpoint. Μόνο δύο σχέδια χωρούν σε αυτόν τον περιορισμό, και κανένα δεν μπορεί να ενεργοποιηθεί αργότερα χωρίς να ξαναχτιστεί το workflow. Διαλέξτε ανάμεσά τους πριν από την πρώτη γραμμή YAML, γιατί η μετανάστευση εκ των υστέρων κοστίζει περισσότερο από την πρώτη προσπάθεια.
Η επιλογή είναι ανάμεσα σε runner τοποθετημένο εκεί όπου το δίκτυο τελειώνει ήδη και σε τούνελ που υπάρχει για τη διάρκεια ενός job. Και τα δύο κουβαλούν production deploys για χρόνια, και αποτυγχάνουν με διαφορετικούς τρόπους.
Πού βρίσκεται πραγματικά το όριο
Ό,τι εγκαθιστάτε στο laptop σταματά στο laptop, και ό,τι εγκαθιστάτε κατά τη διάρκεια ενός job εξαφανίζεται μαζί με τον runner. Δεν υπάρχει μόνιμο network namespace στο οποίο ένας client θα έμενε συνδεδεμένος ανάμεσα σε εκτελέσεις, οπότε κάθε εκτέλεση ξεκινά στην ίδια απρόσιτη κατάσταση. Η Azure τεκμηριώνει αυτό το όριο για agents φιλοξενούμενους από τη Microsoft, όπου οι συνδέσεις VPN και ExpressRoute σε ιδιωτικά δίκτυα δεν υποστηρίζονται, και οι runners που φιλοξενεί το GitHub φέρουν την ίδια μορφή περιορισμού αντί για ξεχωριστό μηχανισμό. Διαβάστε το ως ιδιότητα του προϊόντος, όχι ως ελάττωμα προς παράκαμψη.
Καμία ρύθμιση του repository δεν το χαλαρώνει. Η ετικέτα του job αποφασίζει ποιο pool θα πάρει τη δουλειά, και μια φιλοξενούμενη ετικέτα σημαίνει μηχάνημα έξω από την περίμετρό σας για όλη την εκτέλεση.
Η ιδιωτική προσβασιμότητα πρέπει επομένως να έρθει από κάτι που σας ανήκει. Runner τοποθετημένος εκεί όπου το δίκτυο τελειώνει ήδη, ή τούνελ που ζει ένα job. Το υπόλοιπο άρθρο αφορά το να πληρώσετε συνειδητά ένα από τα δύο αντί να ανακαλύψετε τον λογαριασμό στην παραγωγή.
Αποφασίστε με τη συχνότητα εκτελέσεων, όχι με την εξοικείωση
Καταφύγετε σε δικό σας runner μέσα στο VPC ή στο δίκτυο του γραφείου όταν τα deploys φτάνουν πολλές φορές την ημέρα, όταν το job αγγίζει περισσότερες από μία ιδιωτικές πόρτες ή όταν μια ομάδα πλατφόρμας έχει ήδη την ευθύνη των patches και της εναλλαγής διαπιστευτηρίων. Ένα job σε αυτό το μηχάνημα κάνει resolve και route όπως κάθε άλλος host πίσω από το firewall. Η δουλειά δικτύου γίνεται μία φορά, στην εγκατάσταση, αντί σε κάθε εκτέλεση.
Χτίστε το τούνελ μέσα στο job όταν η πρόσβαση είναι περιστασιακή και δεν θέλετε έναν μακρόβιο agent που κρατά διαπιστευτήριο προς το δίκτυό σας. Το job γίνεται ο δικός του client δικτύου για λίγα λεπτά και δεν αφήνει τίποτα πίσω, που συχνά είναι όλο το νόημα.
Υπολογίστε το κόστος του τούνελ πριν το διαλέξετε. Κάθε εκτέλεση πληρώνει την υλοποίηση του profile, την εκκίνηση του client, την ετοιμότητα και την αποσυναρμολόγηση. Ένα τούνελ που σηκώνεται αλλά δεν επαληθεύεται ποτέ παράγει ένα deploy που αποτυγχάνει στα μισά και αφήνει ιδιωτική κατάσταση σε μορφή που κανείς δεν σχεδίασε. Η επανάληψη μετά από αυτό σπάνια είναι καθαρή, και η δεύτερη αποτυχία συνήθως χρεώνεται στην έκδοση του client.
Γράφοντας το profile ώστε το βήμα να μην μπορεί να ματαιωθεί
Κρατήστε profiles και διαπιστευτήρια έξω από το repository και υλοποιήστε τα από secrets στο βήμα που τα χρειάζεται. Η σειρά αποφασίζει αν το job επιβιώνει: ένα αρχείο που δημιουργήθηκε κάτω από sudo ανήκει στο root, και το μη προνομιούχο shell του βήματος δεν μπορεί πια να γράψει σε αυτό.
- 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"Και οι δύο λεπτομέρειες σε αυτό το block υπάρχουν για επόμενα βήματα. Με umask 077 το profile προσγειώνεται σε mode 0600 και ανήκει στον χρήστη του runner, το sudo μπορεί ακόμα να το διαβάσει, και ο client γράφει το log του σε αρχείο που ο runner κατέχει ήδη, ώστε ο βρόχος ετοιμότητας να διαβάζει αυτό το log χωρίς δεύτερο sudo. --writepid καταγράφει το PID στο οποίο θα στοχεύσει η αποσυναρμολόγηση.
Ένα profile που πιστοποιείται με πιστοποιητικό client χρειάζεται το κλειδί του γραμμένο με τον ίδιο τρόπο, και ένα profile στατικής πρόκλησης χρειάζεται την αντίστοιχη λειτουργία authentication. Τα profiles που σπρώχνουν επιλογές DNS χρειάζονται openvpn-systemd-resolved σε Ubuntu και Debian, γιατί το σπρώξιμο μόνο του δεν αλλάζει τίποτα σε βασική εικόνα.
Η ετοιμότητα είναι ανιχνευτής, όχι παύση
Ένα σταθερό sleep αντέχει μέχρι ακριβώς τη στιγμή που ο concentrator γίνει αργός, και τότε το βήμα deploy τρέχει πριν υπάρξει η διαδρομή. Η αποτυχία εμφανίζεται μετά μέσα στο εργαλείο deploy ως timeout προς τον ιδιωτικό host, που δεν λέει τίποτα για το τούνελ.
- 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 που σπρώχτηκε απαντά για αυτό το όνομα, και το άνοιγμα του socket αποδεικνύει ότι κάτι ακούει στην πόρτα που θα χρησιμοποιήσει το deploy; ένα profile μόνο με διαδρομή χρειάζεται τη γραμμική διεύθυνση και τη διαγραμμένη γραμμή getent.
Όταν ο βρόχος λήξει, το log είναι το μόνο που χωρίζει τρεις διαφορετικές βλάβες: ένα απορριφθέν πιστοποιητικό, μια λάθος λειτουργία authentication και μια διαδρομή που δεν έφτασε ποτέ. Το φιλτράρισμα πριν το tail κρατά αυτή την έξοδο αρκετά σύντομη για ανάγνωση όσο το job ακόμα αποτυγχάνει. Διαβάστε το πρώτο σφάλμα αντί για την τελευταία γραμμή, γιατί ένας client που δεν μπορεί να πιστοποιηθεί επαναλαμβάνεται μέχρι ο βρόχος να τα παρατήσει. Το ip -4 route show dev tun0 απαντά στην απλούστερη ερώτηση αν σπρώχτηκε καθόλου διαδρομή, και κοστίζει μία γραμμή μέσα στον ίδιο βρόχο.
Αποσυναρμολόγηση και το ζήτημα του artifact
Κλείστε το job με ένα βήμα φυλαγμένο από if: always() που εκτελεί sudo kill "$(sudo cat "$RUNNER_TEMP/openvpn.pid")" και τίποτα άλλο. Σε κοινόχρηστο δικό σας runner μην σκοτώνετε ποτέ με όνομα διεργασίας: άλλο job μπορεί να κατέχει client με το ίδιο binary και παρόμοια ορίσματα. Η αποτυχία τότε πέφτει στο job που έτρεξε δεύτερο, και το log του δείχνει ένα workflow που δεν έκανε τίποτα λάθος.
Η προστασία μετράει γιατί ένα αποτυχημένο deploy αφήνει παρόλα αυτά ζωντανό τούνελ. Χωρίς το always() το βήμα καθαρισμού παραλείπεται ακριβώς όταν χρειάζεται, και η επόμενη εκτέλεση ξεκινά απέναντι σε client που είναι ήδη συνδεδεμένος.
Μένει μια ερώτηση πριν υιοθετήσετε οποιοδήποτε από τα δύο σχέδια: χρειάζεται καθόλου το στάδιο build μια ιδιωτική διαδρομή? Ένας agent deployment που κάθεται ήδη μέσα στο δίκτυο μπορεί να τραβήξει εγκεκριμένο artifact, κάτι που κρατά τα συνηθισμένα builds μακριά από την ιδιωτική διαδρομή. Λιγότερα jobs που κρατούν τούνελ σημαίνει λιγότερα σημεία όπου μπορεί να κρυφτεί μια παλιά διαδρομή.