GitHub Actions თქვენს VPN-ს ვერ აღწევს: საკუთარი runner თუ გვირაბი
ჰოსტინგზე გაშვებული runner თქვენი ქსელის გარეთ არსებული ახალი VM-ია. ან runner ჩადეთ ამ ქსელში, ან job-იდან გვირაბი გააკეთეთ და ნამდვილ მარშრუტს დაელოდეთ.
თქვენი deploy ნაბიჯი შიდა hostname-ზე ჩერდება, მაშინ როცა იგივე ბრძანება თქვენი ლეპტოპიდან მყისიერად პასუხობს და client-ის build ორივე მანქანაზე იდენტურია. job მუშაობს GitHub-ის ჰოსტინგის runner-ზე: ვირტუალური მანქანა, რომელიც სწორედ ამ ერთი job-ისთვის შეიქმნა GitHub-ის ღრუბელში; თქვენს კერძო მისამართების სივრცეში მარშრუტი არ არის, შიდა ზონისთვის resolver ჩანაწერი არ არის და არც სერტიფიკატი, რომელსაც კერძო endpoint მიიღებს. ამ შეზღუდვაში მხოლოდ ორი დიზაინი ეტევა და არცერთი მათგანი workflow-ის ხელახლა აწყობის გარეშე არ ჩაირთვება მოგვიანებით. აირჩიეთ მათ შორის YAML-ის პირველ სტრიქონამდე, რადგან მოგვიანებით გადატანა პირველ ცდაზე ძვირია.
არჩევანია runner-ს, რომელიც იქ არის განთავსებული, სადაც ქსელი უკვე მთავრდება, და გვირაბს შორის, რომელიც მხოლოდ ერთი job-ის განმავლობაში არსებობს. ორივე წლების განმავლობაში ატარებს production deploy-ებს და სხვადასხვა გზით ცვივა.
სად არის სინამდვილეში საზღვარი
ყველაფერი, რასაც ლეპტოპზე დააყენებთ, ლეპტოპზე ჩერდება, ხოლო ყველაფერი, რასაც job-ის დროს დააყენებთ, runner-თან ერთად ქრება. არ არსებობს მუდმივი ქსელური namespace, რომელზეც client გაშვებებს შორის დარჩება მიბმული, ამიტომ ყოველი გაშვება იმავე მიუწვდომელ მდგომარეობიდან იწყება. Azure ამ საზღვარს Microsoft-ის ჰოსტინგის agent-ებისთვის აღწერს: კერძო ქსელებში VPN და ExpressRoute კავშირები იქ არ არის მხარდაჭერილი, ხოლო GitHub-ის ჰოსტინგის runner-ები ცალკე მექანიზმს არა, იგივე ფორმის შეზღუდვას ატარებენ. წაიკითხეთ ეს პროდუქტის თვისებად და არა როგორც ხარვეზი, რომელიც უნდა ავუაროთ.
არცერთი რეპოზიტორიის პარამეტრი ამას არ ასუსტებს. job-ის ეტიკეტი წყვეტს, რომელი pool აიღებს სამუშაოს, ხოლო ჰოსტინგის ეტიკეტი ნიშნავს მანქანას თქვენი პერიმეტრის გარეთ მთელი გაშვების განმავლობაში.
ამიტომ კერძო ხელმისაწვდომობა უნდა მოვიდეს იმიდან, რაც თქვენ გაქვთ. runner, რომელიც იქ არის განთავსებული, სადაც ქსელი უკვე მთავრდება, ან გვირაბი, რომელიც ერთ job-ს ცოცხლობს. დანარჩენი სტატია იმაზეა, რომ ამ ორიდან ერთს განზრახ გადავიხადოთ, ნაცვლად იმისა, რომ ანგარიში production-ში აღმოვაჩინოთ.
გადაწყვიტეთ გაშვების სიხშირით, და არა ჩვევით
გამოიყენეთ საკუთარი runner VPC-ის ან ოფისის ქსელის შიგნით, როცა deploy-ები დღეში რამდენჯერმე მოდის, როცა job ეხება ერთზე მეტ კერძო პორტს, ან როცა პლატფორმის გუნდი უკვე პასუხისმგებელია patch-ებსა და სერტიფიკატების როტაციაზე. იმ მანქანაზე job ისე წყვეტს სახელებს და მარშრუტებს, როგორც firewall-ის უკან ნებისმიერი host. ქსელური სამუშაო ერთხელ, ინსტალაციის დროს ხდება და არა ყოველ გაშვებაზე.
ააგეთ გვირაბი job-ის შიგნით, როცა წვდომა იშვიათია და არ გსურთ გრძელვადიანი agent, რომელიც თქვენს ქსელში სერტიფიკატს იჭერს. job რამდენიმე წუთით საკუთარი ქსელური client ხდება და უკან არაფერს ტოვებს, რაც ხშირად მთელი მიზანია.
გვირაბის ღირებულება შეაფასეთ მის არჩევამდე. ყოველი გაშვება იხდის პროფილის შექმნას, client-ის გაშვებას, მზადყოფნას და დემონტაჟს. გვირაბი, რომელიც ადის, მაგრამ არასდროს მოწმდება, წარმოქმნის deploy-ს, რომელიც შუაში ცვივა და კერძო მდგომარეობას ისეთ ფორმაში ტოვებს, რომელიც არავის დაუგეგმავს. შემდგომი ხელახალი ცდა იშვიათად სუფთაა და მეორე წარუმატებლობას ჩვეულებრივ client-ის ვერსიას აბრალებენ.
პროფილის ისე დაწერა, რომ ნაბიჯი ვერ შეწყდეს
პროფილები და სერტიფიკატები რეპოზიტორიის გარეთ შეინახეთ და საჭირო ნაბიჯში secret-ებიდან შექმენით. თანმიმდევრობა წყვეტს, გადარჩება თუ არა 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"ამ ბლოკის ორივე დეტალი შემდგომი ნაბიჯებისთვისაა. umask 077 გამოიყენება და პროფილი 0600 რეჟიმში runner-ის მომხმარებლის საკუთრებაში ხვდება, sudo კვლავ კითხულობს, client კი ლოგს იმ ფაილში წერს, რომელიც runner-ს უკვე აქვს, ამიტომ მზადყოფნის ციკლს ეს ლოგი მეორე sudo-ს გარეშე წაკითხვა შეუძლია. --writepid იწერს იმ PID-ს, რომელსაც დემონტაჟი დაუმიზნებს.
client სერტიფიკატით ავთენტიფიკაციის პროფილს მისი გასაღები იმავე წესით დაწერილი სჭირდება, ხოლო static-challenge პროფილს შესაბამისი auth რეჟიმი. DNS ოპციების push-ის პროფილებს Ubuntu-სა და Debian-ზე openvpn-systemd-resolved სჭირდება, რადგან თავად push ბაზურ იმიჯზე არაფერს ცვლის.
მზადყოფნა ზონდია და არა პაუზა
ფიქსირებული sleep ზუსტად მანამდე ძლებს, სანამ კონცენტრატორი შენელდება, შემდეგ კი deploy-ის ნაბიჯი მარშრუტის არსებობამდე ეშვება. შეცდომა მერე deploy-ის ხელსაწყოს შიგნით კერძო host-ის მიმართ timeout-ად ჩნდება, რაც გვირაბზე არაფერს ამბობს.
- 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-ის მიერ გამოსაყენებელ პორტზე ვიღაც ისმენს; მხოლოდ მარშრუტის მქონე პროფილს პირდაპირი მისამართი და წაშლილი getent სტრიქონი სჭირდება.
ციკლის ამოწურვისას ერთადერთი, რაც სამ სხვადასხვა გაუმართაობას ჰყოფს, ლოგია: უარყოფილი სერტიფიკატი, არასწორი auth რეჟიმი და მარშრუტი, რომელიც არასდროს მოვიდა. tail-ამდე ფილტრაცია ამ გამონატანს საკმარისად მოკლედ ინახავს, რომ job-ის ჯერ კიდევ ჩავარდნისას წაიკითხოთ. წაიკითხეთ პირველი შეცდომა და არა ბოლო სტრიქონი, რადგან client, რომელსაც ავთენტიფიკაცია არ შეუძლია, თავს იმეორებს მანამ, სანამ ციკლი დანებდება. ip -4 route show dev tun0 პასუხობს უფრო მარტივ კითხვას, საერთოდ გაშვებულ იქნა თუ არა მარშრუტი, და იმავე ციკლში ერთი სტრიქონი ღირს.
დემონტაჟი და არტეფაქტის კითხვა
job დახურეთ if: always() დაცვით შექმნილი ნაბიჯით, რომელიც მხოლოდ sudo kill "$(sudo cat "$RUNNER_TEMP/openvpn.pid")" ასრულებს. საერთო საკუთარ runner-ზე არასდროს მოკლათ პროცესის სახელით: სხვა job-ს შეიძლება ჰქონდეს client იგივე binary-ითა და მსგავსი არგუმენტებით. შეცდომა მაშინ მეორედ გაშვებულ job-ზე ვარდება და მისი ლოგი workflow-ზე მიუთითებს, რომელსაც არაფერი დაუშავებია.
დაცვა მნიშვნელოვანია, რადგან წარუმატებელი deploy მაინც ტოვებს ცოცხალ გვირაბს. always() გარეშე გასუფთავების ნაბიჯი ზუსტად მაშინ გამოტოვდება, როცა ის საჭიროა, და შემდეგი გაშვება უკვე დაკავშირებული client-ის წინააღმდეგ იწყება.
ნებისმიერი დიზაინის მიღებამდე ერთი კითხვა რჩება: ნამდვილად სჭირდება build-ის ეტაპს კერძო მარშრუტი? deployment agent, რომელიც უკვე ქსელში ზის, შეიძლება დამტკიცებული არტეფაქტი ჩამოქაჩოს და ეს ჩვეულებრივ build-ებს კერძო გზისგან მოშორებით ინახავს. ნაკლები job, რომელიც გვირაბს იჭერს, ნიშნავს ნაკლებ ადგილს, სადაც ძველი მარშრუტი დაიმალება.