Deploy Github Self Host Runner với Docker Compose dùng Replicated Mode
Cập nhật gần nhất: 10/11/2024
Hello các bạn lại là mình đây 👋👋👋👋
Cả tháng rồi mới lại được ngồi viết bài, mỗi ngày nhìn thấy blog mốc meo, muốn viết 1 cái gì đó nhưng toàn hết ngày, trong khi vẫn muốn được viết rất nhiều cùng các bạn
Quay trở lại với series học Docker, hôm nay mình sẽ hướng dẫn các bạn cách dùng Replicated mode của Docker compose để deploy và scale Github runner của chúng ta lên nhiều instance nhé.
Anh em lên thuyền cùng tôi nào 🚢🚢
Github Runner là gì vậy nhỉ?
Trong series này chúng ta thường thực hành với Gitlab, và từ bài CICD trở đi, thì khi chúng ta commit code lên, sẽ có 1 loạt job được chạy kiểu như check linting, format code, unit test,... Các job đó được thực hiện bởi Runner
Mặc định nếu trong cấu hình CICD ta không nói gì thì nó sẽ dùng Runner public, shared giữa tất cả mọi người trên thế giới, dẫn tới việc job của chúng ta có thể phải "chờ" để được chạy, và ta bị limit bởi thời gian chạy theo tháng (tính bằng phút), nếu ta vượt quá limit thì hoặc là job ta sẽ không được chạy nữa, hoặc ta phải trả thêm tiền. 🥲
Vậy nên thường với các công ti, tổ chức, họ thường sẽ chạy Runner của riêng người ta chứ không dùng chung, vì lượng job 1 ngày cực kì nhiều
Điều tương tự với bên Github, mô hình y hệt.
Tại sao lại là Github ?
Khi mình mới bắt đầu viết Series học Docker này thì cũng từ 3 năm trước rồi, đoạn đó thì Github Runner còn chưa nổi, chưa có mấy tính năng nên mình vẫn dùng Gitlab vì nó free hầu hết mọi thứ.
Nhưng tới hôm nay 12/2022 thì Github Runner đã rất oke rồi, và đặc biệt là nó có 1 cái market place cực cực cực kì nhiều những cái Actions được build sẵn, gần như ta chả phải tự viết cái gì luôn, cần gì là có sẵn hết 💪💪💪
Tổng quan
Ở bài này ta sẽ cùng nhau setup self-hosted Github Runner dành cho repo của riêng chúng ta. Mỗi khi commit thì sẽ có 1 loạt job cần được chạy, và các job đó đều được thực hiện bởi Runner của chúng ta.
Bởi vì Github Runner chạy theo kiểu pull-model, tức là nó sẽ liên tục gọi đến Github và hỏi lấy job, chứ không phải Github gọi nó. Do vậy ta có thể setup Github Runner ở bất kì đâu, và ở bài này ta sẽ làm trực tiếp trên localhost. Nếu các bạn có VM(VPS, server) thì làm y hệt luôn nhé
Chuẩn bị
Đầu tiên các bạn clone project mẫu của mình về ở đây, nhánh master nhé các bạn.
Sau đó các bạn tạo 1 repo mới của riêng các bạn, rồi push lên repo đó nhé. Nhớ bước này nhen, vì lát nữa ta setup Runner cho repo đó. Hoặc đơn giản là các bạn fork repo của mình về của các bạn cũng được
Cùng xem project ta có gì nhé:
- đây là project backend, NodeJS, framework NestJS (1 framework nodejs khá là hot ở thời điểm hiện tại 2022)
- project này mình tạo mới tinh, không có gì mấy, chỉ có 1 module app, có 1 route trả về Hello World nếu ta gọi vào
- mở file
package.json
, mụcscripts
, ta sẽ thấy tất cả các command mà ta có thể chạy trên project này. Ở bài này ta sẽ quan tâm các command sau:
"build": "nest build",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:cov": "jest --coverage",
"test:e2e": "jest --config ./test/jest-e2e.json"
Các command trên lần lượt là: build project, Lint project với Eslint, Unit Test với Jest, Coverage Test và cuối cùng là E2E test.
Và mỗi 1 command trên lát nữa sẽ là 1 job mà sẽ được chạy bởi Github Runner.
Nếu máy các bạn có cài sẵn NodeJS thì ta chạy thử lên xem thế nào nhé: (còn không có thì cũng không sao cả, project chạy được sẵn rồi )
npm install
npm run start:dev
Sau khi start lên mở trình duyệt thấy như sau là oke nhé:
Sau đó các bạn cũng có thể lần lượt chạy các command ta vừa nói ở trên (build, test....) để xem chúng cho kết quả như thế nào nhé (nếu các bạn muốn 🤣🤣)
À thêm nữa là mình cũng đã chuẩn bị 1 Dockerfile để build app của chúng ta, lát nữa đây cũng sẽ là 1 job mà ta sẽ chạy trên Runner
Oke dzô phần chính thôi
Vẫn là chuẩn bị
Với project thật, thì thường mỗi khi code được commit lên, ta sẽ muốn chạy tất cả các job để đảm bảo code của chúng ta ko có lỗi về lint, format, build không lỗi lầm, test qua được tất cả các test case....
Và ta muốn làm chúng tự động chứ không phải là trông chờ vào các anh dev tự chạy (kiểu này thì 96,69% là dev sẽ "giả vờ" như không biết và cứ thế commit
Âu cây, nãy giờ luyên thuyên đủ rồi, giờ ta vào tiết mục chính là setup Github Runner thôi nào 🚀🚀
Trước khi làm đảm bảo là các bạn đã tạo repo mới và push code lên repo của các bạn rồi nhé
Sau khi các bạn đã có repo riêng và code được push lên repo đó, thì ta cùng vào repo của riêng chúng ta trên Github vào mục Settings và xem cấu hình Runner hiện tại có gì nhé:
Như ở trên thì repo của mình chưa có Runner nào cả. Thì để tạo Runner ta có 2 cách:
- 1 là tạo thủ công bằng cách bấm vào nút New self-hosted Runner màu xanh lá cây như hình trên, Sau khi bấm vào đó thì Github sẽ hiển thị 1 loạt các bước ta cần làm (như hình dưới)
- cách 2 là ta dùng luôn các Docker image đã được người ta làm sẵn các bước thủ công kia, và ta chỉ cần truyền vào 1 số biến là xong
Và hiển nhiên theo tiêu đề bài này, và thực tế thì chúng ta sẽ chọn cách 2 rồi (chắc là trừ trường hợp ta muốn chạy Runner cho MacOS thôi, vì hiện tại Docker chưa chính thức support MacOS )
Giờ mới bắt đầu
Sau khi đã tường tận những gì ta cần làm, thì tiếp theo, ở local, trong code của chúng ta, các bạn tạo file docker-compose.yml
với nội dung như sau nhé:
version: "3.7"
services:
runner:
image: myoung34/github-runner:latest
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
RUNNER_SCOPE: repo
RUNNER_NAME_PREFIX: myrunner
LABELS: some-label
REPO_URL: https://github.com/maitrungduc1410/viblo-docker-replicated
EPHEMERAL: 1
ACCESS_TOKEN: <PAT_TOKEN>
deploy:
replicas: 3
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.2'
memory: 256M
Chú ý: các bạn nhớ phải bỏ file docker-compose.yml
ra ngoài folder code của các bạn nhé, các bạn bỏ ở đâu cũng được, chứ đừng để chung với code, xong lỡ tí nữa commit lên mà có cái ACCESS_TOKEN, là ngay lập tức Github sẽ revoke
(huỷ) bỏ token của các bạn và Runner bị disconnect ngay lập tức đó nhé
Cùng xem qua ở đây ta có những gì nào:
- thì vẫn như thường lệ với 1 file docker-compose thì ta có 1 service
runner
, ở đó ta dùng image được anh em người ta build sẵn làmyoung34/github-runner
, taglatest
(image này mình vẫn đang dùng và thấy oke nhất so với những image khác) - tiếp theo ta có restart policy là
always
để đảm bảo runner luôn start nếu failed hoặc khi docker daemon khởi động lại (khi ta khởi động lại máy) - tiếp là đến các biến môi trường ta cần truyền vào, phần này tẹo mình sẽ giải thích bên dưới từng cái 1 nhé
- cuối cùng là nhân vật chính của chúng ta ngày hôm nay, đó là cấu hình deploy cho service của chúng ta với docker compose, ta sẽ cùng đi sâu vào ở phần tiếp theo nhé.
À còn nữa, tí quên, 🤪🤪, ta có mount 1 volume dùng cho việc build image ở trong Runner, vì nếu ta không mount docker sock vào, thì lát nữa khi Runner chạy command docker build...
sẽ bị lỗi command not found.
Chú ý với các bạn dùng windows: đường dẫn phải là như sau nhé //var/run/docker.sock:/var/run/docker.sock (thêm 1 dấu gạch ở đầu)
Biến môi trường
Các biến môi trường ở trên ta truyền vào là do người tạo ra image đó người ta setup như thế và ta cần phải làm theo thì Runner mới chạy được, cùng điểm mặt xem có những gì nhé.
Đầu tiên RUNNER_SCOPE=repo
vì bài này ta setup Runner trực tiếp cho repo cá nhân, còn trường hợp khác ví dụ ta setup Runner cho cả công ty thì scope có thể là org
(organization) hoặc enterprise
Tiếp theo đó là RUNNER_NAME_PREFIX=myrunner
và LABELS=some-label
cái này nó là tiền tố cho tên của các Runner của chúng ta và nhãn (label) cho Runner, lát nữa khi chạy lên thì ta sẽ thấy nó show như sau:
tiếp theo REPO_URL=....
cái này các bạn phải dùng URL tới repo của các bạn nhé, nhớ cho chính xác nhen, chứ đừng dùng y hệt cái của mình nhé
Tiếp nữa ta có EPHEMERAL: 1
, cái này là để ta chạy Runner trong ephemeral
mode, mỗi khi chạy xong job thì môi trường của Runner sẽ được xoá, Runner sẽ kết nối lại với github, đảm bảo là mọi thứ sạch sẽ mỗi khi hoàn thành 1 job. Github khuyến khích ta dùng các này để dễ scale lên/xuống, đồng thời job cũng sẽ được đảm bảo hơn.
Cuối cùng là ACCESS_TOKEN=...
, đây là Github Personal Access Token của accound của các bạn, ở trên mình chưa điền giá trị vào, bởi vì các bạn phải tự lấy và tự điền vào nhé, các bạn làm theo các bước bên dưới nhé.
Đầu tiên là vào Settings:
Tiếp theo ở phía tay trái ta vào Developer Settings:
Tiếp theo ta mở Personal Access Tokens > Token (Classic) > Generate New Token > Classic:
Ta nhập tên cho token vào mục Note, ở đoạn Expiration để ta set thời hạn cho token này, thì các bạn để là No Expiration nhé, chứ không mai kia đang chạy Runner tự nhiên token hết hạn thì lại mệt . Cuối cùng ở mục Select scopes
thì các bạn tích vào repo như trong hình của mình nhé. Cuối cùng là kéo xuống dưới cùng và bấm Generate Token
. Ngay lập tức ta sẽ nhận được token, các bạn copy và giấu nó đi cho riêng mình nhé 🤣🤣:
Khi đã có token rồi thì các bạn thay vào biến môi trường ACCESS_TOKEN
ở file docker-compose.yml nhé
Thế là xong phần chuẩn bị cho Github Runner. Ta đi vào nhân vật chính của chúng ta ngày hôm nay nhé (ủa nói đi vô nhân vật chính nãy giờ mà chưa thấy nói 😪😪😪
Replicated Mode với Docker Compose
Mục tiêu bài này của ta là deploy 1 loạt các Runner y hệt như nhau, có thể dễ dàng tăng giảm số lượng khi ta muốn.
Và để làm điều đó thì docker compose cho ta option deploy
, ở đó ta thoải mái cấu hình trạng thái mong muốn cho app (runner) của chúng ta.
Như các bạn thấy ở file docker-compose.yml
của chúng ta, service runner
ta cần thêm vào option deploy
là 1 Object, bên trong lần lượt có các thuộc tính sau:
mode
: ta muốn deploy service này theo kiểu nào, có thể làglobal
(đúng 1 container/node) hayreplicated
(thích bao nhiêu container thì cứ thế phang zô )replicas
: số container (runner) ta muốnresources
: ở đây ta định nghĩa resource (RAM/CPU) mà mỗi container cần tối thiểu (reservations
) và tối đalimits
có thể được dùng là bao nhiêu. Nếu container chạm tới mức tối đa thì nó sẽ bị "throttled", kiểu bị "hãm" lại, performance sẽ kém đi rõ rệt. Nếu các bạn đã làm việc với Kubernetes, thì ở đây cũng y vậy. Ở đó ta
Quá đơn giản phải không, ây xời, muỗi 😎😎
Ngoài ra thì option
deploy
còn có nhiều thuộc tính khác nhưupdate_config
,rollback_config
hayrestart_policy
, nhưng chúng chỉ được dùng khi ta dùng Docker Swarm.
Triển
Khởi động Runner
Phìuuuuuuu, viết nãy giờ mệt quá 😥😥, già rồi giờ nói dài là không có sức các bạn ạ 🤣🤣🤣
Mọi thứ ô sờ kê rồi đó ta triển thôi, như thường lệ ta cũng up
Runner của ta lên nhé:
docker compose up -d
Sau khi khởi động Runner lên, ta chờ 1 chút khoảng 1 phút, sau đó quay trở lại Repo Github của các bạn vào Settings và kiểm tra xem Runner lên chưa nhé:
Mạng lag quá pull image mãi chưa xong nên là chưa thấy Runner nào online trên Github 🥲🥲
Sau khi các Runner đã connect thành công tới Repo github của chúng ta thì các bạn F5 lại trình duyệt sẽ thấy như sau nhé:
Yayyyyyyyyyy 🥳🥳🥳
Như các bạn thấy các Runner đang ở trạng thái nghỉ ngơi (Idle
), và sẵn sàng để nhận job rồi đó 💪💪💪💪
Tạo file workflow
Tiếp theo ta sẽ tạo file github workflow, định nghĩa các job mà ta muốn chạy nhé.
Ở trong folder code của chúng ta, các bạn tạo cho mình folder .github
, trong đó ta tạo tiếp folder workflows
nhé, trong đó ta tạo file my-jobs.yml
:
Ở trong file my-jobs.yml
, ta sẽ liệt kê ra tất cả các job mà ta muốn chạy mỗi khi có commit lên repo của chúng ta và cách chạy từng job như thế nào nhé:
name: My Jobs
on: [push]
jobs:
build:
runs-on: [self-hosted, some-label]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm install
- name: Build App
run: npm run build
lint:
runs-on: [self-hosted, some-label]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm install
- name: Lint
run: npm run lint
test:
runs-on: [self-hosted, some-label]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm install
- name: Unit tests
run: npm run test
test_cov:
runs-on: [self-hosted, some-label]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm install
- name: Coverage test
run: npm run test:cov
test_e2e:
runs-on: [self-hosted, some-label]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm install
- name: E2E test
run: npm run test:e2e
build_image:
runs-on: [self-hosted, some-label]
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t my-app .
Ở bên trên ta có 1 Action tên là My Jobs
, sẽ được chạy bất kì khi nào ta push lên repo, ở bất kì branch nào. Bên dưới là 1 loạt jobs ta muốn chạy với mỗi commit được push lên. Các bạn tự "thấm" xem từng job làm gì nhé
Xong thì ta tiến hành commit thôi nào, nhớ bỏ file docker-compose.yml
ra ngoài đừng commit lên nhé các ban j
git add .
git commit -m "ci: setup github actions"
git push origin master
Ngay sau khi ta commit thành công, quay trở lại github, mở tab Actions
ta sẽ thấy job đang chạy:
Click vào job thì sẽ thấy là 3 job hiện tại đang được chạy (vì ta có 3 runner - replicas
)
chờ vài phút sau đó quay trở lại check...
click vào job build
xem sao chạy lâu thế nhỉ:
5 phút chưa xong??????? Chạy có tí npm sao lâu vậy nhỉ??
Thì ra là do máy của mình kết nối kém, tải mãi không được trang Google, bảo sao Runner chạy trên máy mình nó chạy npm install lâu là phải 😂
chờ thêm tẹo nữa thì cuối cùng cũng đã xong:
Yayyyyy, vậy là ta đã hoàn thành việc setup Runner của riêng chúng ta cho repo trên Github rồi đó 🥳🥳🥳
Không lo Github charge tiền hay bị giới hạn thời gian chạy Actions nữa, với Runner riêng thì chúng ta chạy bao nhiêu tuỳ ý
Vọc vạch chút
Ta kiểm tra xem trạng thái các container của ta đang ăn bao nhiêu CPU/RAM nhé. Các bạn chạy command:
docker stats
Ta sẽ thấy như sau:
Như ở trên các bạn thấy 3 container của ta mỗi cái đang "rảnh" (Idle), không làm gì cả nên ăn có tí teo CPU, RAM thì ăn có 33Mb/1GB giới hạn.
Ta quay trở lại repo trên Github > Actions, chọn lại job cũ rồi chạy lại toàn bộ job để xem sự thay đổi về CPU và RAM nhé:
chờ một chút, F5 trình duyệt tới khi nào thấy có job bắt đầu chạy:
Thì ta quay trở lại docker stats
tiếp sẽ thấy như sau:
Như các bạn thấy thì lượng CPU và RAM mà các Runner đang sử dụng sẽ tăng lên, chú ý rằng CPU % và RAM % là tính theo số limit nhé, chứ không phải là nó đang ăn vào 90% CPU cả máy của chúng ta đâu 😂😂
Như các bạn thấy thì hiện tại ta có 3 Runner, mỗi Runner chỉ chạy 1 job tại 1 thời điểm, mà ta có 6 jobs, dẫn tới việc thời gian chờ đợi sẽ lâu hơn. Giờ ta sẽ scale số Runner lên 6 xem sao nhé, các bạn mở docker-compose.yml
và sửa replicas
thành 6
nhé:
...
deploy:
replicas: 6
...
Sau đó ta chỉ việc up
luôn mà không cần down nhé:
docker compose up -d
Ngay sau khi ta up
thì ta thấy như sau:
Docker sẽ tạo thêm 3 container nữa, và tạo lại 3 container cũ, tức là thực tế ta sẽ có 6 runner mới hoàn toàn.
Sau đó ta quay trở lại Github kiểm tra số Runner hiện có:
Yay, ngon rồi đó, giờ ta quay trở lại mục Actions, và Re-run
lại cái job cũ nhé:
Tuyệt vời ông mặt trời, cả 6 job đều đang quay đều quay đều, tức là đang chạy rồi, không job nào phải chờ nữa 😎😎
Vọc vạch sâu hơn chút
Quay trở lại với file docker-compose.yml
, ở biến môi trường ta có EPHEMERAL=1
, ý bảo Runner là "ê, mỗi khi chạy xong 1 job thì anh không cần chú nữa, chú deregister (huỷ đăng kí) đi nhé".
Tức là khi kết thúc 1 job, thì Runner sẽ tự ngắt kết nối với Github, thế tại sao khi kiểm tra trên Github thì số Runner online luôn đủ??
Lí do là vì sau khi deregister, Runner sẽ exit - container exit, mà ta đang để restart=always
, nên ngay lập tức Docker sẽ tạo lại container mới thay thế
Việc chạy Runner ở chế độ EPHEMERAL
, đảm bảo cho ta môi trường sạch sẽ cho từng job, không để thừa lại cái gì (file, secret,....)
À còn 1 điều nữa là ta có thể tự tay ngắt kết nối Runner từ Github trên web UI như sau nhé:
Kiểu ngắt kết nối này thì container của ta sẽ ngay lập tức bị failed và
không thể kết nối lại nữa, nó sẽ liên tục restarting
Câu hỏi liên quan
Giả sử từ service khác muốn gọi vào Runner thì sao?
Thì như bình thường thôi các bạn 😂😂
Giả sử ta ở service app
muốn gọi vào runner
thì vẫn gọi tới địa chỉ runner:<port>
, và Docker sẽ tự điều hướng request vào từng container cho ta
Nếu muốn map port ra ngoài thì làm thế nào?
Các bạn sẽ thắc mắc, ủa vậy nếu muốn gọi từ ngoài vào thì làm kiểu gì?
Để làm điều này thì ta phải map 1 "range" port - tức 1 dải port, hoặc các port cụ thể , nhưng số lượng phải lớn hơn hoặc bằng số replicas
vì Docker sẽ map từng port vào từng container, nên nếu ít hơn sẽ bị báo lỗi "port đã được sử dụng".
Cách làm như sau:
# giả sử replicas=3
runner:
image: myoung34/github-runner:latest
ports:
- "7171-7173:8080" # map 3 port từ 7171->7173 và container ở port 8080
khi ta docker-compose up -d
lại thì sẽ thấy như sau:
Như các bạn thấy, Docker đã tự động map từng port vào từng container cho ta, kiểu này thì khi gọi từ ngoài vào ta phải chỉ định chính xác port nào ta muốn gọi, và request sẽ chỉ đi vào container đó
Nên dùng cách deploy này khi nào?
Bởi vì như các bạn thấy cái kiểu map port mà phải map 1 dải port (hoặc nhiều port cụ thể khác nhau), khá bất tiện, và giả sử ta có 50 container không lẽ ta phải map 1 range 50 port?, kiểu này thì máy của các bạn sẽ giật tung, vì để map 1 port thì Docker cũng sẽ cần CPU/RAM cho việc đó (để chứng minh thì các bạn thử tạo 1 container map 50-100 port cho nó xem nhé )
Vậy nên mình thấy cách ta làm ngày hôm nay thường thích hợp với các service dạng Runner, Worker. Kiểu mà không cần ai gọi vào nó, chỉ nó đi gọi người ta, như vậy thì không cần expose/map nó ra cho ai gọi cả
Thực tế mình rất hay dùng cách này cho các dạng worker để gửi email, broadcast message, hay làm các task ở background
Bao nhiêu là đủ?
Oke giờ đã hiểu cách chạy Runner, cách scale lên xuống số lượng Runner rồi, vậy thì câu hỏi là bao nhiêu replicas
thì đủ? bao nhiêu resources
, limits
như thế nào?
Cái ngày thì phải cân nhắc theo trường hợp của các bạn, số lượng repo, số lượng job, máy (server) của các bạn có CPU/RAM/disk bao nhiêu. Ta cứ chọn 1 con số ban đầu, chạy thử, nếu thấy job chậm, hay phải đợi, mọi người kêu ca quá thì tăng thêm tìm 1 con số phù hợp
À và chú ý rằng, bởi vì trong thực tế, Runner hay phải chạy các job build/test,... và các job đó ăn nhiều CPU/RAM, nên các bạn không nên dùng chung server với các app khác nhé. Cho Runner 1 VPS/Server riêng để nó thoải mái
Kết bài
Phù, tưởng ngắn mà sao viết dài vỡ mặt thớt vậy nhỉ 🤣🤣🤣
Qua bài này ta hi vọng rằng các bạn đã biết cách setup Self hosted Runner trên Github, 1 cách mà các công ty/tổ chức hay dùng để có thể thoải mái tự do trong việc chạy các job của riêng họ, mà không lo bị giới hạn.
Lần tới công ty có kêu ca Github Runner bị hết quota thì mạnh dạn dơ tay "anh để em setup Runner cho riêng công ty mình" nhé 😎😎😎
Hẹn gặp lại các bạn ở những bài sau. Chúc các bạn cuối tuần vui vẻ.
Merry Christmas 🥳🥳🥳
All rights reserved