- Trong quá trình đưa một CI phức tạp quay lại GitHub Actions, merge queue, nhiều runner, build Rust, image Docker và kiểm thử tích hợp đan xen với nhau, khiến chi phí debug lộ rõ là lớn hơn cả việc cấu hình
- Mọi thay đổi đi vào
main đều phải vượt qua kiểm thử, còn các lỗi nhỏ sẽ được tự động sửa như định dạng, dependency không dùng đến, lint; artifact của CI phải giống artifact phát hành
- Để bắt buộc kiểm tra cả trước và sau merge queue, phải đặt tên job giống nhau ở hai giai đoạn; nếu không, check có thể bị kẹt hoặc thay đổi thất bại vẫn có thể được merge
GITHUB_TOKEN, permissions của workflow, token tùy chỉnh, các ngoại lệ với fork và self-hosted runner đan xen, khiến mô hình bảo mật khó hiểu và dễ mắc lỗi hơn
- Việc chạy container Docker, workflow YAML và khả năng kiểm thử cục bộ hạn chế làm chậm tốc độ phát triển, nhưng script CI mới đã giảm đáng kể thời gian merge
CI phức tạp quay lại GitHub Actions
- Trong 2 tuần qua, chúng tôi đã viết lại script CI bằng GitHub Actions
- Đây là lần tái cấu trúc thứ ba của cấu hình CI
- Ban đầu là GitHub Actions
- Sau đó chuyển sang Earthly
- Earthly bị ngừng, nên lại quay về GitHub Actions
- CI hiện tại xử lý đồng thời merge queue, nhiều runner, build Rust, image Docker và các bài kiểm thử tích hợp nặng
- Runner được dùng kết hợp gồm self-hosted, blacksmith.sh và GitHub-hosted
- Khi merge một PR, khoảng 1 giờ thời gian CI được tiêu thụ trên nhiều runner chạy song song
Các điều kiện CI phải đáp ứng
- Mọi thay đổi đi vào
main đều phải vượt qua tất cả kiểm thử
- Những sơ suất nhỏ như định dạng, dependency không dùng đến, vấn đề lint nên được tự động sửa thay vì kết thúc bằng thất bại
- Artifact được kiểm thử trong CI phải giống với artifact phát hành thực tế
- Để có trải nghiệm tốt cho lập trình viên, CI phải kết thúc nhanh
- GitHub Actions hỗ trợ các điều kiện này về mặt kỹ thuật, nhưng quá trình cấu hình đi kèm các cạm bẫy ẩn, hành vi không nhất quán và việc debug khó khăn
Merge queue và status check
- Chìa khóa để duy trì nhánh
main sạch là merge queue của GitHub
- Merge queue rebase PR lên trên
main trước khi chạy CI
- Các lần chạy CI cần thiết được chia thành hai giai đoạn
- Chạy CI trước khi vào queue để tự động sửa các vấn đề nhỏ
- Chạy lại CI trong queue để xác minh trạng thái merge cuối cùng
- Để bắt buộc cả hai lần chạy trong GitHub Actions, phải đặt tên job giống nhau cho hai giai đoạn
- GitHub coi hai lần chạy là cùng một check, nên cả hai phải thành công thì mới có thể merge
- Cách này được tìm ra qua một câu trả lời trên Stack Overflow sau vài giờ debug
- Các cách khác có thể dẫn đến việc status check bị chờ trước khi vào queue khiến job không bắt đầu, hoặc job đáng lẽ phải thất bại trong merge queue vẫn được merge dù thất bại
Mô hình bảo mật GitHub Actions khó hiểu
- Sau sự cố một GitHub Action phổ biến bị xâm phạm gần đây, phản ứng được đưa ra là “hãy pin dependency theo hash”, nhưng trong phần bình luận, gần như không ai làm vậy
- GitHub Actions có token mặc định là
GITHUB_TOKEN
- Token này được khởi tạo với quyền mặc định
- Quyền mặc định có thể được thiết lập trong phần Actions → General → Workflow Permissions của cài đặt repository
- Nếu quyền mặc định của
GITHUB_TOKEN bị giới hạn, cần nâng quyền để chạy các action và lệnh cần thiết; nếu quyền mặc định rộng rãi, có thể loại bỏ một số quyền trong file workflow
- Giá trị mặc định tốt hơn sẽ là bắt đầu từ không có quyền, rồi để người dùng chỉ thêm các quyền cần thiết
- Có nhiều loại quyền, và nếu không phải chuyên gia GitHub thì rất khó nắm được mỗi loại đang bảo vệ điều gì
Ngoại lệ về token và quyền
- Khi tự động tạo GitHub release bằng
softprops/action-gh-release, chúng tôi dùng token tùy chỉnh CI_RELEASE
- name: Release on GitHub
if: env.version_exists == 'false'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ env.CURRENT_VERSION }}
generate_release_notes: true
make_latest: true
token: ${{ secrets.CI_RELEASE }}
- Bản thân việc release vẫn hoàn tất bằng token mặc định, nhưng workflow sau release không được kích hoạt
- Vì không có chỉ báo riêng, phải tìm đến issue do người khác gặp cùng vấn đề để biết nguyên nhân
- Cũng có thể nâng quyền ngay trong YAML của workflow
- Cấu trúc nâng quyền ngay bên trong đoạn code mà mình muốn bảo vệ tạo cảm giác lạ lẫm
- Theo tài liệu GitHub, có thể thêm hoặc loại bỏ quyền đọc của repository fork bằng khóa
permissions, nhưng thường không thể cấp quyền ghi
- Ngoại lệ là khi quản trị viên chọn tùy chọn Send write tokens to workflows from pull requests trong cài đặt GitHub Actions
- Vì có nhiều ngoại lệ và cạm bẫy, mô hình bảo mật GitHub Actions vừa mạnh mẽ vừa làm tăng bề mặt tấn công và khả năng mắc lỗi
Sự bất định của self-hosted runner
- Tài liệu GitHub không khuyến nghị dùng self-hosted runner trong repository công khai
- Lý do là fork của repository công khai có thể chạy code nguy hiểm trên máy self-hosted runner thông qua PR
- GitHub cũng có thiết lập self-hosted runner yêu cầu phê duyệt việc chạy PR của người đóng góp bên ngoài
- Không có câu trả lời rõ ràng trong tài liệu về việc dùng thiết lập này cùng self-hosted runner có an toàn hay không, và trên Internet cũng không có sự đồng thuận
- Do độ phức tạp cao, tình trạng này vẫn còn khó có thể chắc chắn 100%
Xung đột giữa Docker và GitHub Actions
- GitHub Actions có thể chạy job bên trong container
- Ưu điểm là có thể đóng gói sẵn dependency trong dev container thay vì cài đặt lại mỗi lần
- Khi dùng thực tế, vấn đề quyền file lặp lại nhiều lần
- Container build file bằng một user, nhưng GitHub runner có thể chạy với uid/gid khác
- Kết quả là có thể không truy cập được file bên trong container, GitHub workspace hoặc thư mục host tạm thời
- Thư mục
$HOME cũng có thể lệch nhau
- Dev container có thể cài công cụ vào
/home/ubuntu
- Trong GitHub Actions,
$HOME có thể là /github/home
- Các công cụ phụ thuộc vào file dưới
$HOME có thể không tìm thấy file cần thiết
- Các action tương tác với hệ thống host cũng có thể bị hỏng
- Cache của GitHub bị giới hạn ở 10GB, nên chúng tôi mount ổ NVMe để cache bằng sticky disk action của blacksmith
- Nó không hoạt động bên trong container, và được giải quyết sau khi blacksmith.sh sửa lỗi
- Bản thân trường
container cũng có hạn chế
- Không thể override entrypoint
- Cũng không thể chỉ chạy một số step trong container còn phần còn lại chạy bên ngoài
Phát triển và debug workflow YAML
- Logic GitHub Actions được viết bằng YAML, và càng phức tạp thì càng dễ mắc lỗi
- Kiểm tra bằng GitHub YAML linter của RustRover có ích, nhưng vẫn cần kiểm tra tĩnh tốt hơn
- Rất khó kiểm thử đầy đủ hành vi CI thực tế ở local
- act là công cụ quen thuộc, nhưng chỉ hỗ trợ một phần nhỏ những việc CI cần làm
- Cách debug tốt nhất là tạo thêm một repository giống hệt, rồi lặp lại
git commit -a -m "wip" && git push test-ci branch cho đến khi CI hoạt động như kỳ vọng
Tách workflow và tái sử dụng artifact
- Để không phải chạy toàn bộ pipeline CI mỗi lần, chúng tôi giữ từng workflow ở mức nhỏ
- Ở cuối mỗi workflow, artifact được upload, rồi workflow sau đó download lại để không phải build lại từ đầu
- Có thể kiểm thử cô lập workflow bằng cách tải artifact từ lần chạy trước
- Tuy nhiên, khi tải từ lần chạy trước, phải cung cấp token cho action
download-artifact
- Token này có thể là token mặc định, nhưng vì sao vẫn phải chỉ định rõ thì vẫn là một câu hỏi chưa được giải đáp
- File workflow chính trở thành một chuỗi gọi các file YAML khác
jobs:
invoke-build-rust:
name: Build Rust
uses: ./.github/workflows/build-rust.yml
invoke-build-java:
name: Build Java
uses: ./.github/workflows/build-java.yml
invoke-tests-unit:
name: Unit Tests
needs: [invoke-build-rust, invoke-build-java]
uses: ./.github/workflows/test-unit.yml
invoke-tests-adapter:
name: Adapter Tests
needs: [invoke-build-rust]
uses: ./.github/workflows/test-adapters.yml
secrets: inherit
invoke-build-docker:
name: Build Docker
needs: [invoke-build-rust, invoke-build-java]
uses: ./.github/workflows/build-docker.yml
invoke-tests-integration:
name: Integration Tests
needs: [invoke-build-docker]
uses: ./.github/workflows/test-integration.yml
invoke-tests-java:
name: Java Tests
needs: [invoke-build-java]
uses: ./.github/workflows/test-java.yml
- Một số job cần
secrets: inherit
- Khi một workflow gọi workflow khác, secret mặc định không được chia sẻ
- Đây là nguyên nhân của vấn đề: toàn bộ pipeline CI thì thất bại, nhưng khi chạy từng step riêng lẻ lại hoạt động
Merge nhanh hơn, nhưng debug vẫn tốn kém
- Script CI mới đã giảm đáng kể thời gian merge và chúng tôi hài lòng với kết quả
- Tuy nhiên, để đạt được trạng thái đó đã tốn quá nhiều thời gian, và việc debug khi xảy ra vấn đề cần trở nên dễ dàng hơn
Chưa có bình luận nào.