Thách thức
Runner CI vẫn hoàn thành tốt nhiệm vụ, và đó chính là vấn đề. Mỗi runner được triển khai theo dạng cụm docker-compose cấu hình thủ công, mount trực tiếp docker socket của máy chủ để workflow có thể khởi tạo container build. Bất kỳ ai chạy được workflow đều có thể chiếm quyền điều khiển một container đặc quyền (privileged container), và từ đó kiểm soát toàn bộ máy chủ, một lượt push mã độc, một action bị xâm nhập, hay một lỗi gõ ở thư viện bên thứ ba đều đủ khiến vùng ảnh hưởng (blast radius) lan ra toàn bộ node.
Bài toán kinh tế cũng không khá hơn: mỗi runner tự duy trì một kho cache riêng biệt, liên tục pull lại cùng một builder image từ registry bên ngoài, và mỗi lần restart phải làm lại từ đầu. Vận hành trên ba máy chủ nghĩa là ba phân mảnh cache độc lập, ba vùng tin cậy (trust) tách biệt, và ba nguy cơ để một workflow lỗi làm gián đoạn cả đội.
Chúng tôi đã làm gì
Chúng tôi tái cấu trúc toàn bộ hệ thống trên nền Nomad.
Cô lập bằng rootless Docker-in-Docker. Ba runner chạy docker-in-docker không đặc quyền (rootless), mỗi runner được ghim vào một host riêng trong pool hiệu năng cao, vận hành một daemon Docker độc lập, biệt lập hoàn toàn. Socket của máy chủ không còn bị mount ra ngoài, nếu một workflow thoát khỏi sandbox, kẻ tấn công chỉ có được quyền root của một user không đặc quyền bên trong một namespace bị cô lập, chứ không chạm được máy chủ vật lý.
Cache tập trung trên Ceph. Một máy chủ Gitea Actions cache độc lập, dùng Ceph RBD volume, được toàn bộ runner trỏ về. Cache hit giờ dùng chung cho cả cụm thay vì tách theo từng host, và sống sót qua các lần restart vì dữ liệu nằm trên thiết bị lưu trữ mạng, không phải tầng ghi tạm thời của container. Toàn bộ dùng domain nội bộ, chứng chỉ Vault-PKI, không public ra internet.
Registry proxy tập trung. Mọi lượt pull image đi qua một zot pull-through proxy, đóng vai trò mirror cho Docker Hub, GitHub Container Registry và namespace image nội bộ của Gitea, dùng chung một CA nội bộ để mã hóa TLS. Runner mặc định tin cậy proxy này, kể cả lượt pull đầu tiên (cold-start) cũng chỉ diễn ra trong mạng LAN.
Toàn bộ kiến trúc (Gitea, cache server, runner, ràng buộc vị trí, liên kết volume, việc fetch chứng chỉ Vault) nằm gọn trong một job spec Nomad duy nhất. Mở rộng thêm năng lực chỉ đơn giản là thay đổi một con số.
Điều gì đã thay đổi
Workflow không còn cách nào chạm tới máy chủ vật lý qua Docker socket. Thời gian pull image giảm xuống bằng tốc độ LAN; cache CI đồng bộ xuyên suốt cả cụm và sống sót qua reboot vì nằm trên Ceph. Thêm một runner thứ tư chỉ mất vài giây.
Runner không tồn tại trên host thì không thể bị xâm nhập trên host.