CI/CD cho phần mềm nhúng¶
Trang này trình bày tích hợp liên tục và chuyển giao liên tục (CI/CD) với các đặc thù của phần mềm nhúng. Nội dung đi theo thứ tự: các khái niệm CI, Continuous Delivery và Continuous Deployment; các thành phần của một hệ thống CI; pipeline điển hình cho firmware; môi trường build và cache cho Yocto, Buildroot; test trên phần cứng thật; một pipeline ví dụ; và cuối cùng là bước triển khai tới thiết bị.
Vị trí của CI/CD trong toàn bộ quy trình phát triển phần mềm được trình bày ở trang Quy trình phát triển phần mềm.
Khái niệm CI và CD¶
Continuous Integration (CI, tích hợp liên tục) là cách làm trong đó các thành viên đưa thay đổi vào nhánh chính thường xuyên, và mỗi lần tích hợp được kiểm tra bằng build và test tự động. Mục tiêu là phát hiện xung đột và lỗi ngay khi chúng xuất hiện, thay vì dồn tới một đợt tích hợp lớn trước ngày release.
Chữ CD được dùng cho hai khái niệm khác nhau, phân biệt ở bước cuối cùng:
| Khái niệm | Kết quả của pipeline | Ai quyết định phát hành |
|---|---|---|
| Continuous Integration | Thay đổi đã được build và test trên nhánh chính | Chưa có bước phát hành |
| Continuous Delivery (chuyển giao liên tục) | Mỗi thay đổi qua pipeline tạo ra một artifact sẵn sàng phát hành | Con người |
| Continuous Deployment (triển khai liên tục) | Mỗi thay đổi qua pipeline được tự động triển khai tới người dùng | Pipeline, không cần phê duyệt thủ công |
Với thiết bị nhúng, "triển khai" là cập nhật firmware trên thiết bị đang hoạt động ở hiện trường. Lý do để giữ bước phê duyệt thủ công ở đây được trình bày ở mục Triển khai tới thiết bị.
Thành phần của hệ thống CI¶
Một hệ thống CI gồm các thành phần sau, dù dùng nền tảng nào:
| Thành phần | Vai trò | Ví dụ |
|---|---|---|
| Kho mã nguồn | Nơi phát sinh sự kiện kích hoạt pipeline: push, pull request, tag | GitHub, GitLab, Gerrit |
| Định nghĩa pipeline | File mô tả các bước, đặt trong repo cùng mã nguồn để được review và quản lý phiên bản như code | .github/workflows/*.yml, .gitlab-ci.yml, Jenkinsfile |
| CI server | Đọc định nghĩa pipeline, xếp hàng và điều phối các job | GitHub Actions, GitLab CI, Jenkins |
| Runner (agent) | Máy thực thi job; có thể là máy của nhà cung cấp hoặc máy tự quản lý (self-hosted) | Máy ảo, container, máy build riêng |
| Kho artifact | Lưu kết quả build: image, gói cập nhật, log, báo cáo test | Artifact storage của nền tảng CI, server lưu file riêng |
| Test farm | Các board thật được điều khiển tự động để chạy test trên phần cứng | Xem mục Test trên phần cứng |
Phần mềm nhúng thường cần runner tự quản lý vì hai lý do: build image Linux đầy đủ cần nhiều CPU, bộ nhớ, dung lượng đĩa và thời gian hơn mức runner dùng chung cung cấp; và test trên phần cứng cần runner nối trực tiếp với board.
Pipeline cho phần mềm nhúng¶
Hình dưới là pipeline điển hình. Ba bước đầu thuộc CI, chạy ở mọi thay đổi; ba bước sau thuộc Continuous Delivery.
- Commit và push: thay đổi được đẩy lên nhánh hoặc mở pull request, kích hoạt pipeline.
- Build: cross compile cho kiến trúc đích, hoặc build cả image bằng Yocto hay Buildroot. Nên build cho mọi biến thể phần cứng mà sản phẩm hỗ trợ, vì lỗi thường chỉ xuất hiện ở một cấu hình.
- Phân tích tĩnh và unit test: chạy công cụ phân tích tĩnh (static analysis), kiểm tra coding convention và unit test trên máy build, không cần phần cứng.
- Đóng gói artifact: lưu image, gói cập nhật và thông tin build (commit, phiên bản toolchain, cấu hình) để có thể truy lại về sau.
- Test trên board: nạp image lên board thật và chạy test tích hợp, test hệ thống.
- Release hoặc triển khai: phát hành artifact, hoặc đẩy bản cập nhật tới thiết bị qua OTA.
Bước nào lỗi thì pipeline dừng và báo lại cho người commit. Thay đổi chỉ được merge vào nhánh chính khi các bước CI đều đạt.
Không phải bước nào cũng chạy ở mọi lần commit. Bước nhanh (build ứng dụng, unit test) chạy ở mọi pull request; bước chậm (build toàn bộ image, test dài trên board) có thể chạy khi merge vào nhánh chính hoặc theo lịch, ví dụ mỗi đêm (nightly build).
Môi trường build¶
Kết quả build phải giống nhau dù chạy trên máy nào. Nếu image build trên runner khác với image build trên máy của lập trình viên, lỗi tìm được ở một bên có thể không tái hiện được ở bên kia. Các biện pháp chính:
- Đóng gói môi trường build trong container: toolchain, công cụ build và thư viện của host nằm trong một image container có phiên bản. Runner và lập trình viên dùng chung image đó. Cách viết Dockerfile cho môi trường build và các lưu ý riêng cho Yocto được trình bày ở trang Docker cho môi trường build.
- Cố định phiên bản: phiên bản toolchain, phiên bản layer Yocto hoặc Buildroot, và phiên bản các dependency đều được ghi trong repo, không lấy "bản mới nhất" lúc build.
- Build tái tạo được (reproducible build): từ cùng mã nguồn và cùng môi trường, build ra kết quả giống hệt từng bit. Muốn vậy, kết quả build không được phụ thuộc vào thời điểm build, đường dẫn thư mục hay tên máy. Dự án Reproducible Builds tổng hợp các nguồn gây khác biệt thường gặp và cách loại bỏ chúng.
Cache khi build Yocto và Buildroot¶
Build một image Linux từ đầu bằng Yocto hoặc Buildroot tốn nhiều thời gian, vì phải tải và biên dịch toàn bộ toolchain, kernel và các gói. Nếu mỗi lần chạy pipeline đều build lại từ đầu, vòng phản hồi của CI trở nên quá chậm. Cả hai công cụ đều có cơ chế cache, và CI cần giữ các thư mục cache giữa các lần chạy.
| Công cụ | Cache | Biến cấu hình | Nội dung |
|---|---|---|---|
| Yocto | Thư mục download | DL_DIR |
Mã nguồn đã tải về (tarball, bản clone git) |
| Yocto | Shared state cache (sstate) | SSTATE_DIR |
Kết quả của từng task build; task có đầu vào không đổi thì lấy kết quả từ cache thay vì chạy lại |
| Buildroot | Thư mục download | BR2_DL_DIR |
Mã nguồn đã tải về |
| Buildroot | ccache | BR2_CCACHE |
Cache kết quả biên dịch của trình biên dịch C/C++ |
Thư mục download và sstate có thể đặt trên ổ dùng chung hoặc server để nhiều runner cùng dùng. Với Buildroot, ccache chỉ tránh được bước biên dịch lại; khi đổi cấu hình lớn (đổi toolchain, đổi kiến trúc), Buildroot vẫn cần build lại từ đầu, nên build sạch (clean build) định kỳ vẫn cần thiết để chắc chắn image build được mà không phụ thuộc vào trạng thái cũ.
Test trên phần cứng¶
Unit test và test trên trình giả lập không phát hiện được lỗi phụ thuộc phần cứng thật: timing, driver, nguồn, ngoại vi. Vì vậy pipeline của sản phẩm nhúng thường có thêm bước test trên board thật, gọi là hardware-in-the-loop (HIL). Một test farm cho phép pipeline điều khiển board mà không cần người thao tác, gồm các thành phần:
| Thành phần | Vai trò |
|---|---|
| Board cần test | Một hoặc nhiều board cho mỗi biến thể phần cứng |
| Điều khiển nguồn | Relay hoặc ổ cắm điều khiển qua mạng (PDU) để bật, tắt, khởi động lại board, kể cả khi board bị treo |
| Serial console | Đọc log boot và gửi lệnh tới board; là kênh duy nhất còn dùng được khi image lỗi không lên mạng |
| Cơ chế nạp image | Boot qua mạng (TFTP cho kernel, NFS cho rootfs), hoặc nạp vào bộ nhớ của board qua USB hay JTAG |
| Máy điều khiển | Nhận job từ CI, nạp image, cấp nguồn, chạy test, thu log và trả kết quả |
| Thiết bị đo và mô phỏng ngoại vi (tùy chọn) | Tạo tín hiệu đầu vào cho board hoặc đo tín hiệu đầu ra, ví dụ mô phỏng cảm biến |
Cách dựng TFTP, NFS root và serial console trên máy host được trình bày ở trang Môi trường phát triển trên host. LAVA (Linaro Automated Validation Architecture) là một framework mã nguồn mở để quản lý test farm: nó điều phối việc nạp image, boot và chạy test trên nhiều loại board.
Bước test trên board chậm hơn và có số máy giới hạn, nên số test ở bước này ít hơn unit test. Một phần có thể chuyển sang trình giả lập để chạy nhanh và song song hơn, ví dụ boot image trên QEMU để kiểm tra rootfs và dịch vụ trước khi lên board thật.
Pipeline ví dụ¶
Ví dụ dưới là một workflow GitHub Actions cho một dự án C nhỏ. Workflow chạy khi có push lên nhánh main hoặc khi có pull request, cài toolchain trong container Debian, chạy unit test trên host, cross compile cho ARM rồi lưu kết quả build làm artifact. Ví dụ giả định dự án có Makefile với target test, nhận biến CROSS_COMPILE và đặt kết quả build vào thư mục build/.
name: firmware-ci
on:
push:
branches: [main]
pull_request:
jobs:
build-test:
runs-on: ubuntu-latest
container: debian:bookworm # môi trường build cố định
steps:
- uses: actions/checkout@v4
- name: Cài toolchain
run: |
apt-get update
apt-get install -y make gcc gcc-arm-linux-gnueabihf
- name: Unit test trên host
run: make test
- name: Cross compile cho ARM
run: make CROSS_COMPILE=arm-linux-gnueabihf-
- name: Lưu artifact
uses: actions/upload-artifact@v4
with:
name: firmware
path: build/
Trong dự án thật, bước cài toolchain nên được đưa vào một image container dựng sẵn để không phải cài lại ở mỗi lần chạy, và để runner dùng đúng phiên bản toolchain mà lập trình viên dùng. Với GitLab CI, cấu trúc tương tự được viết trong file .gitlab-ci.yml với các khái niệm stage và job.
Triển khai tới thiết bị¶
Với dịch vụ web, một bản triển khai lỗi có thể được rollback trên server trong vài phút. Với thiết bị nhúng, bản cập nhật lỗi có thể làm thiết bị không khởi động được, và thiết bị nằm ở nơi không ai tới sửa được. Vì vậy bước triển khai thường được kiểm soát chặt hơn:
- Phê duyệt thủ công: pipeline dừng ở Continuous Delivery, một người quyết định phát hành sau khi xem kết quả test.
- Cập nhật theo từng đợt (staged rollout): bản cập nhật được đẩy tới một nhóm nhỏ thiết bị trước, theo dõi lỗi, rồi mới mở rộng ra toàn bộ.
- Cơ chế rollback trên thiết bị: hệ thống cập nhật A/B giữ bản cũ trên phân vùng còn lại, để thiết bị tự quay về bản cũ nếu bản mới không boot được.
Các chiến lược cập nhật và công cụ OTA được trình bày ở trang OTA Update.
Liên quan¶
- Quy trình phát triển phần mềm (Programming): vị trí của CI/CD trong toàn bộ quy trình.
- Git (Programming).
- Yocto, Buildroot, Môi trường phát triển trên host và OTA Update (Embedded).
- Docker cho môi trường build và QEMU cross-arch (Virtualization).
Tài liệu tham khảo¶
- M. Fowler, Continuous Integration.
- M. Fowler, Continuous Delivery.
- GitHub Actions documentation.
- GitLab CI/CD documentation.
- Yocto Project, Overview and Concepts Manual: Shared State Cache.
- The Buildroot user manual.
- Reproducible Builds.
- LAVA documentation.