Bước deploy của bạn treo ở một hostname nội bộ, trong khi đúng lệnh đó trả lời tức thì từ laptop, và bản build client trên hai máy giống hệt nhau. Job chạy trên runner do GitHub lưu trữ: một máy ảo được tạo riêng cho job đó trong đám mây của GitHub, không có tuyến vào không gian địa chỉ riêng của bạn, không có bản ghi resolver cho vùng nội bộ và không có chứng chỉ mà endpoint riêng sẽ chấp nhận. Chỉ hai thiết kế phù hợp với ràng buộc đó, và không thiết kế nào bật lên được sau này mà không dựng lại workflow. Hãy chọn trước dòng YAML đầu tiên, vì di chuyển về sau tốn hơn lần thử đầu.

Lựa chọn nằm giữa runner đặt ở nơi mạng đã kết thúc và đường hầm chỉ tồn tại trong đúng một job. Cả hai đã gánh deploy production nhiều năm, và chúng hỏng theo những cách khác nhau.

Ranh giới thật sự nằm ở đâu

Mọi thứ bạn cài trên laptop đều dừng ở laptop, và mọi thứ bạn cài trong job đều biến mất cùng runner. Không có namespace mạng thường trú nào để client bám vào giữa các lần chạy, nên mỗi lần chạy đều bắt đầu từ cùng một trạng thái không tới được. Azure ghi nhận ranh giới này cho các agent do Microsoft lưu trữ, nơi liên kết VPN và ExpressRoute vào mạng riêng không được hỗ trợ, và runner do GitHub lưu trữ mang đúng dạng hạn chế đó chứ không phải một cơ chế riêng. Hãy đọc nó như thuộc tính của sản phẩm, không phải khuyết điểm cần lách.

Không cấu hình repository nào nới lỏng điều đó. Nhãn của job quyết định pool nào nhận việc, và nhãn lưu trữ nghĩa là một máy nằm ngoài vành đai của bạn suốt cả lần chạy.

Vì thế khả năng tới được mạng riêng phải đến từ thứ bạn sở hữu. Một runner đặt nơi mạng đã kết thúc, hoặc một đường hầm sống đúng một job. Phần còn lại của bài nói về việc chủ động trả tiền cho một trong hai, thay vì phát hiện hoá đơn trong production.

Quyết định theo tần suất chạy, không theo sự quen thuộc

Dùng runner tự dựng trong VPC hoặc mạng văn phòng khi deploy về nhiều lần mỗi ngày, khi job chạm vào nhiều hơn một cổng riêng, hoặc khi đội nền tảng đã phụ trách vá lỗi và luân chuyển thông tin đăng nhập. Job trên máy đó phân giải và định tuyến như bất kỳ host nào sau tường lửa. Công việc mạng diễn ra một lần lúc cài đặt, thay vì mỗi lần chạy.

Dựng đường hầm ngay trong job khi truy cập chỉ thỉnh thoảng và bạn không muốn một agent sống lâu giữ thông tin đăng nhập vào mạng của mình. Job trở thành client mạng của chính nó trong vài phút và không để lại gì, mà thường đó chính là mục đích.

Ước lượng chi phí của đường hầm trước khi chọn. Mỗi lần chạy đều trả cho việc hiện thực hoá profile, khởi động client, chờ sẵn sàng và tháo dỡ. Một đường hầm dựng lên nhưng không bao giờ được kiểm chứng sẽ tạo ra deploy hỏng giữa chừng và để trạng thái riêng ở dạng không ai hoạch định. Lần thử lại sau đó hiếm khi sạch, và thất bại thứ hai thường bị quy cho phiên bản client.

Viết profile để bước đó không thể dừng giữa chừng

Giữ profile và thông tin đăng nhập ngoài repository và hiện thực hoá chúng từ secret ngay trong bước cần dùng. Thứ tự quyết định job có sống hay không: một tệp tạo dưới sudo thuộc về root, và shell không đặc quyền của bước đó không ghi vào được nữa.

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

Cả hai chi tiết trong khối đó đều dành cho các bước sau. Với umask 077, profile rơi vào chế độ 0600 thuộc về người dùng runner, sudo vẫn đọc được, và client ghi log vào tệp mà runner đã sở hữu, nên vòng chờ sẵn sàng đọc được log đó mà không cần sudo thứ hai. --writepid ghi lại PID mà bước tháo dỡ sẽ nhắm tới.

Profile xác thực bằng chứng chỉ client cần khoá được ghi theo cùng cách, và profile static-challenge cần đúng chế độ auth tương ứng. Profile đẩy tuỳ chọn DNS cần openvpn-systemd-resolved trên Ubuntu và Debian, vì chỉ đẩy thôi thì không thay đổi gì trên ảnh gốc.

Sẵn sàng là phép dò, không phải cú tạm dừng

Một sleep cố định chỉ trụ được cho tới khi concentrator chậm lại, và khi đó bước deploy chạy trước khi tuyến tồn tại. Thất bại sau đó hiện ra trong công cụ deploy như một timeout với host riêng, và điều đó không nói gì về đường hầm.

- 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

Vòng lặp quan sát hai điều vì bất kỳ điều nào cũng có thể là nửa hỏng. getent hosts chứng minh resolver được đẩy trả lời cho cái tên đó, và việc mở socket chứng minh có thứ gì đó đang lắng nghe trên cổng mà deploy sẽ dùng; profile chỉ có tuyến cần địa chỉ nguyên văn và xoá dòng getent.

Khi vòng lặp hết hạn, log là thứ duy nhất tách ba lỗi khác nhau: chứng chỉ bị từ chối, chế độ auth sai và một tuyến không bao giờ tới. Lọc trước tail giữ đầu ra đủ ngắn để đọc trong khi job vẫn đang hỏng. Đọc lỗi đầu tiên thay vì dòng cuối, vì client không xác thực được sẽ lặp lại cho tới khi vòng lặp bỏ cuộc. ip -4 route show dev tun0 trả lời câu hỏi đơn giản hơn là có tuyến nào được đẩy hay không, và chỉ tốn một dòng trong cùng vòng lặp.

Tháo dỡ, và câu hỏi về artefact

Kết thúc job bằng một bước được bảo vệ bởi if: always() chỉ chạy sudo kill "$(sudo cat "$RUNNER_TEMP/openvpn.pid")" và không gì khác. Trên runner tự dựng dùng chung, đừng bao giờ giết theo tên tiến trình: job khác có thể sở hữu client cùng binary và tham số tương tự. Thất bại khi đó rơi vào job chạy sau, và log của nó chỉ vào một workflow không làm gì sai.

Bảo vệ quan trọng vì một deploy hỏng vẫn để lại đường hầm còn sống. Không có always(), bước dọn dẹp bị bỏ qua đúng lúc cần đến nó, và lần chạy tiếp theo bắt đầu với một client đã kết nối sẵn.

Còn một câu hỏi trước khi nhận một trong hai thiết kế: giai đoạn build có thật sự cần tuyến riêng không? Một agent triển khai đã ngồi trong mạng có thể kéo artefact đã được duyệt, và điều đó giữ các build thông thường tránh xa đường riêng. Ít job giữ đường hầm hơn nghĩa là ít chỗ hơn cho một tuyến cũ ẩn náu.

Bài viết liên quan