Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 25 — Self-hosted Runners

Đã đọc 0/13 mục · còn khoảng 12 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Nếu bạn đã đi qua các bài trước, bạn quen thuộc với việc viết một workflow GitHub Actions rồi thấy nó chạy trên một máy ảo nào đó "trên mây" mà mình chẳng bao giờ nhìn thấy mặt. Đó là hosted runner — máy do nhà cung cấp (GitHub, GitLab) dựng lên, chạy job của bạn xong thì huỷ. Tiện, nhanh, không phải lo gì. Vậy tại sao còn cần học về self-hosted runner — tức là runner do chính bạn tự dựng và tự quản lý?

Câu trả lời ngắn gọn: có những lúc hosted runner không đủ, không được phép, hoặc quá đắt. Một team ở TP.HCM cần build một ứng dụng native compile ngốn 32GB RAM — máy hosted mặc định chỉ có 7GB, job cứ chạy được 20 phút thì bị kill vì hết bộ nhớ. Một ngân hàng ở Hà Nội bị quy định không cho source code rời khỏi lãnh thổ Việt Nam — mà runner của GitHub thì đặt ở Mỹ. Một startup deploy 4000 job mỗi ngày, hoá đơn CI đội lên gần 2000 USD/tháng trong khi họ có sẵn một con server nhàn rỗi trong văn phòng.

Trong tất cả những tình huống đó, self-hosted runner là lời giải. Nhưng nó cũng đi kèm một cái giá: bạn phải tự chịu trách nhiệm cho việc bảo trì, bảo mật và mở rộng nó. Bài này sẽ giúp bạn hiểu rõ khi nào nên dùng, dựng nó ra sao, và tránh những cái bẫy mà rất nhiều team Việt Nam đã sụp vào.

Khái niệm cốt lõi

Runner là gì và self-hosted khác hosted ở đâu

Một runner đơn giản là một tiến trình (process) lắng nghe hàng đợi job từ hệ thống CI/CD trung tâm. Khi có job mới, runner nhận về, thực thi các bước (checkout code, cài dependency, build, test...), rồi báo kết quả ngược lại. Bản thân runner chỉ là một agent nhỏ; nơi nó chạy mới là điều quyết định.

  • Hosted runner: máy do GitHub/GitLab cấp phát động (ephemeral), sạch sẽ cho mỗi job, tính tiền theo phút. Bạn không quản lý gì cả.
  • Self-hosted runner: một máy (vật lý hoặc ảo) bạn sở hữu, cài phần mềm agent lên, đăng ký nó với repo/organization của mình. Máy này thuộc mạng của bạn, dùng cấu hình của bạn, và tồn tại đến khi bạn tắt nó.
Điểm mấu chốt cần nhớ: với self-hosted runner, bạn đảo ngược mô hình kết nối. Runner chủ động gọi ra ngoài (long-poll) tới GitHub để hỏi "có job cho tôi không?". Nghĩa là bạn không cần mở port inbound trên firewall — runner nằm sau NAT trong văn phòng vẫn hoạt động tốt. Đây là lý do self-hosted runner có thể chạy trong một mạng nội bộ bị khoá chặt mà vẫn nhận được job.

Ba lý do chính để tự host

1. Nhu cầu tài nguyên lớn hoặc đặc thù. Hosted runner có cấu hình cố định và khiêm tốn. Nếu bạn build C++/Rust cần RAM lớn, cần GPU để train model, cần chạy trên macOS để build iOS, hoặc cần một ổ SSD nhanh cho database test — bạn tự cấp máy đúng nhu cầu, thậm chí gắn GPU RTX vào.

2. Compliance và data residency (dữ liệu không rời khu vực). Nhiều ngành ở Việt Nam — ngân hàng, fintech, y tế — chịu ràng buộc của Nghị định 13/2023 về bảo vệ dữ liệu cá nhân, hoặc yêu cầu nội bộ rằng source code và secret không được rời khỏi hạ tầng công ty. Self-hosted runner đặt trong data center ở Việt Nam đáp ứng điều này.

3. Networking — truy cập tài nguyên nội bộ. Đây là lý do bị đánh giá thấp nhất nhưng hay gặp nhất. Runner của bạn cần chạm được database staging nằm trong VPC riêng, một artifact registry nội bộ, hay một service chỉ mở cho IP nội bộ. Hosted runner đứng ngoài internet thì bó tay; self-hosted runner nằm ngay trong mạng đó thì làm được ngay.

Bên cạnh đó, khi khối lượng job đủ lớn, chi phí cũng là động lực — nhưng hãy nhớ "miễn phí" của máy tự host là ảo, vì bạn trả bằng thời gian vận hành và rủi ro bảo mật.

Ephemeral vs persistent runner

Đây là khái niệm sống còn về bảo mật. Một self-hosted runner có thể chạy theo hai kiểu:

  • Persistent (bền vững): một máy chạy hết job này đến job khác, không reset giữa các job. Nhanh (cache còn nguyên) nhưng nguy hiểm: một job độc hại có thể để lại "quà" (backdoor, biến môi trường bị đầu độc) cho job của người khác chạy sau.
  • Ephemeral (dùng một lần): runner chỉ chạy đúng một job rồi tự huỷ, máy mới tinh được dựng cho job kế tiếp. An toàn hơn hẳn, gần với mô hình hosted runner. Đây là kiểu được khuyến nghị khi repo của bạn nhận contribution từ bên ngoài.
Với repo private nội bộ, team nhỏ, tin cậy nhau, persistent runner thường chấp nhận được. Với repo public hoặc có nhiều người ngoài gửi pull request, bắt buộc dùng ephemeral — nếu không bạn đang mời kẻ tấn công vào chạy code tuỳ ý trên hạ tầng của mình.

Tình huống thực tế

Ví dụ 1 — Studio game ở TP.HCM và bài toán 32GB RAM

Amanotes-style studio game (giả định tên "PixelWave Studio", ~40 kỹ sư) build engine game bằng Unreal viết C++. Mỗi lần compile full ngốn 28–30GB RAM và mất 55 phút trên hosted runner loại lớn nhất họ mua được. Tệ hơn, thỉnh thoảng job bị OOM-killed giữa chừng, phải chạy lại — có ngày pipeline "đỏ" chỉ vì thiếu bộ nhớ chứ không phải lỗi code.

Họ mua một workstation 64GB RAM, CPU 16 nhân, cài GitHub Actions runner lên, gắn nhãn (label) self-hosted, unreal, highmem. Trong workflow, họ chỉ cần đổi runs-on: [self-hosted, highmem]. Kết quả: build xuống còn 22 phút (nhờ cache local + RAM dư dả), không còn OOM. Chi phí một lần cho workstation (~40 triệu VND) hoàn vốn sau chưa đầy 3 tháng so với hoá đơn hosted trước đó.

Bài học: khi job đụng trần tài nguyên của hosted runner, tự host không chỉ rẻ hơn mà còn ổn định hơn. Nhãn (label) là công cụ then chốt để route đúng job tới đúng máy.

Ví dụ 2 — Ngân hàng số ở Hà Nội và ràng buộc data residency

Một ngân hàng số (giả định "VietBank Digital") có bộ phận compliance yêu cầu rõ: mọi build chạm tới core-banking code phải diễn ra trong data center của ngân hàng tại Hà Nội, log build phải lưu tại chỗ, và secret production tuyệt đối không đi qua hạ tầng nước ngoài.

Họ dựng một cụm self-hosted runner ephemeral trên Kubernetes nội bộ dùng Actions Runner Controller (ARC). Mỗi job PR spawn một pod runner mới, chạy xong huỷ ngay. Runner nằm trong VPC của ngân hàng, chạm được database staging nội bộ mà hosted runner không bao giờ thấy. Toàn bộ traffic ra ngoài chỉ là kết nối outbound tới GitHub để nhận job — được whitelist qua proxy và ghi log đầy đủ cho audit.

Có một lần đội security phát hiện một dependency npm khả nghi cố gọi ra một IP lạ trong lúc npm install. Vì runner nằm sau proxy egress có log, họ chặn được ngay và truy ra nguồn. Nếu chạy trên hosted runner ở ngoài, họ sẽ không bao giờ thấy sự cố này.

Bài học: self-hosted runner biến CI thành một điểm kiểm soát an ninh, không chỉ là nơi build. Với ngành bị quản lý chặt, đây thường là yêu cầu bắt buộc chứ không phải lựa chọn.

Ví dụ 3 — Startup e-commerce và cái bẫy chi phí ẩn

Một startup thương mại điện tử ở Đà Nẵng (~15 người) chuyển hết CI sang self-hosted để tiết kiệm, chạy 3 runner persistent trên một con server cũ dưới gầm bàn. Ban đầu hoá đơn CI từ 1500 USD/tháng về gần 0. Đội hí hửng.

Ba tháng sau, sự cố ập đến. Ổ cứng runner đầy vì Docker image và build cache không ai dọn — pipeline đỏ toàn bộ lúc 2 giờ sáng ngày sale lớn. Không ai được phân công trực runner, không có monitoring. Một runner còn bị treo vì phiên bản agent quá cũ, GitHub ngừng gửi job tới. Tổng thời gian downtime và mất doanh thu ngày sale lớn hơn nhiều so với 1500 USD họ tiết kiệm.

Họ sửa lại: chuyển sang runner ephemeral tự động scale trên cloud VN, thêm cron dọn disk, thêm alert khi runner offline, và ghi rõ trách nhiệm on-call. Chi phí lên khoảng 400 USD/tháng nhưng ổn định.

Bài học: self-hosted runner không miễn phí — bạn trả bằng công vận hành. Persistent runner "dưới gầm bàn" không có ai chăm là một quả bom hẹn giờ. Hãy tính tổng chi phí sở hữu (TCO), không chỉ tiền hoá đơn.

Hướng dẫn từng bước

Dưới đây là quy trình dựng một self-hosted runner cho GitHub Actions ở cấp organization (cách phổ biến nhất cho công ty).

Bước 1 — Chuẩn bị máy. Chọn máy (VM hoặc vật lý) đúng nhu cầu tài nguyên. Cài sẵn các công cụ nền: Docker, Git, ngôn ngữ cần thiết. Tạo một user riêng không phải root để chạy runner, và giới hạn quyền của user đó — đừng bao giờ chạy runner bằng root.

Bước 2 — Lấy token đăng ký. Vào Settings → Actions → Runners → New runner ở cấp org (hoặc repo). GitHub sẽ cho bạn một đoạn lệnh kèm registration token (token này hết hạn sau ~1 giờ).

Bước 3 — Cài agent. Trên máy, tải gói runner và cấu hình:

mkdir actions-runner && cd actions-runner
curl -o runner.tar.gz -L https://github.com/actions/runner/releases/download/vX.Y.Z/actions-runner-linux-x64-X.Y.Z.tar.gz
tar xzf runner.tar.gz
./config.sh --url https://github.com/YOUR-ORG --token REGISTRATION_TOKEN \
  --labels self-hosted,linux,highmem --ephemeral

Chú ý cờ --ephemeral: nó khiến runner tự gỡ đăng ký sau một job. Kết hợp với một vòng lặp bên ngoài (systemd hoặc ARC) để dựng lại runner mới.

Bước 4 — Chạy như service. Đừng chạy ./run.sh trong terminal rồi để đó. Cài nó thành service để tự khởi động lại:

sudo ./svc.sh install
sudo ./svc.sh start

Bước 5 — Route job bằng label. Trong workflow, trỏ job tới runner của bạn qua label:

jobs:
  build:
    runs-on: [self-hosted, linux, highmem]
    steps:
      - uses: actions/checkout@v4
      - run: make build

Bước 6 — Cân nhắc auto-scaling. Với khối lượng thay đổi, một máy cố định sẽ lúc quá tải lúc nhàn rỗi. Dùng Actions Runner Controller (ARC) trên Kubernetes để tự động spawn pod runner ephemeral theo hàng đợi job, hoặc dùng runner group để phân quyền repo nào được dùng runner nào.

Bước 7 — Giám sát và bảo trì. Thiết lập alert khi runner offline, cron dọn disk (docker system prune), và quy trình cập nhật agent định kỳ. Đây là bước hay bị bỏ qua nhất nhưng quyết định thành bại về lâu dài.

Lỗi thường gặp & mẹo

Lỗi 1 — Chạy runner bằng root. Nếu một job build có thể chạy code tuỳ ý (đặc biệt trên repo có PR từ ngoài), chạy runner bằng root nghĩa là trao cả máy cho kẻ tấn công. Luôn dùng user hạn chế quyền, và ưu tiên ephemeral.

Lỗi 2 — Dùng persistent runner cho repo public. Đây là lỗi bảo mật nghiêm trọng nhất. Bất kỳ ai fork repo và gửi PR đều có thể sửa workflow để chạy lệnh trên runner của bạn. Repo có contributor ngoài phải dùng ephemeral runner, và cân nhắc yêu cầu approval trước khi chạy workflow cho PR.

Lỗi 3 — Đầy disk lúc nửa đêm. Build cache, Docker image, log tích tụ nhanh khủng khiếp. Không có cron dọn dẹp thì runner sẽ chết vào đúng lúc bạn cần nó nhất. Đặt cron docker system prune -af và xoá _work cũ định kỳ.

Lỗi 4 — Không cập nhật agent. GitHub ngừng gửi job tới runner có phiên bản quá cũ. Runner ephemeral tự tải bản mới khi dựng lại; persistent runner thì bạn phải chủ động cập nhật.

Lỗi 5 — Không giới hạn runner theo repo. Một runner đăng ký ở org mặc định phục vụ mọi repo, kể cả repo mới người ta tạo ra. Dùng runner group để chỉ định rõ repo nào được phép dùng runner nhạy cảm.

Mẹo hay:

  • Đặt label có ý nghĩa (gpu, macos, highmem, vn-region) để route job chính xác.
  • Với self-hosted, bạn tự lo cache — tận dụng ổ đĩa local để cache dependency, đó là một lợi thế tốc độ lớn so với hosted (chủ đề caching sẽ đi sâu ở bài riêng).
  • Tách runner cho môi trường nhạy cảm: runner build production nên tách biệt hoàn toàn với runner chạy PR chưa tin cậy.
  • Ghi log egress qua proxy để có khả năng audit — như ví dụ ngân hàng phía trên.

Bài tập thực hành

  • Dựng runner đầu tiên. Tạo một repo test, dựng một self-hosted runner trên máy local (hoặc một VM nhỏ) với label self-hosted, lab. Viết một workflow đơn giản in ra hostname và free -h, xác nhận nó chạy trên máy của bạn chứ không phải máy GitHub.
  • So sánh ephemeral vs persistent. Chạy runner ở chế độ persistent, trong job hãy tạo một file /tmp/leftover.txt. Chạy job lần hai và kiểm tra file còn không. Sau đó cấu hình lại với --ephemeral và lặp lại — quan sát sự khác biệt. Viết 3–4 câu giải thích vì sao điều này quan trọng về mặt bảo mật.
  • Định tuyến bằng label. Tạo hai runner với label khác nhau (fast, highmem). Viết một workflow có hai job, mỗi job chạy trên một loại runner. Xác nhận GitHub route đúng.
  • Tình huống ra quyết định. Cho ba kịch bản: (a) một open-source library nhận PR từ khắp thế giới; (b) một team fintech nội bộ build core-banking; (c) một team cần build iOS trên macOS. Với mỗi kịch bản, quyết định: nên dùng hosted hay self-hosted? Persistent hay ephemeral? Vì sao? Viết một đoạn ngắn cho từng trường hợp.
  • Bảo trì. Viết một script bash nhỏ dọn Docker image cũ hơn 7 ngày và thư mục _work build cũ, rồi lên lịch nó bằng cron chạy mỗi đêm. Giải thích nó phòng ngừa lỗi nào trong bài.

Tóm tắt

Self-hosted runner là công cụ để bạn thoát khỏi giới hạn của hosted runner khi cần: tài nguyên lớn (RAM, GPU, macOS), compliance và data residency (code không rời Việt Nam), hoặc truy cập tài nguyên nội bộ mà internet không chạm tới được. Đổi lại, bạn nhận về trách nhiệm vận hành, bảo trì và — quan trọng nhất — bảo mật.

Ba điều cần khắc cốt: một, ưu tiên runner ephemeral — chạy một job rồi huỷ — đặc biệt với repo nhận contribution từ ngoài; đừng bao giờ để persistent runner phục vụ code không tin cậy. Hai, self-hosted không miễn phí; hãy tính tổng chi phí sở hữu gồm cả công dọn disk, cập nhật agent và trực on-call. Ba, dùng label và runner group để route job chính xác và cô lập môi trường nhạy cảm.

Ba ví dụ trong bài — studio game vượt trần RAM, ngân hàng số ràng buộc data residency, và startup sụp bẫy chi phí ẩn — cho thấy self-hosted runner có thể là cứu cánh hoặc thảm hoạ, tuỳ vào việc bạn có tôn trọng cái giá vận hành của nó hay không. Ở các bài sau, bạn sẽ thấy self-hosted runner kết nối chặt chẽ với chủ đề bảo mật pipeline và tối ưu hiệu năng build — hãy giữ những nguyên tắc ở đây trong đầu khi ta đi tiếp.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi