Push lên master, đợi 2 phút, site lên live. Đó là mục tiêu. Nhưng để đến được đó mình phải đi qua SSH deploy, build trên CI, caching, và vài lần chạy workflow suýt giết hosting. Đây là hành trình phát triển GitHub Workflow deploy cho project JavaScript của mình.
Cách đầu tiên đơn giản: SSH vào server, git pull && yarn. Mình cho ra cái workflow như thế này:
name: Deploy
on:
push:
branches: [master]
jobs:
Deploy:
runs-on: ubuntu-latest
steps:
- name: Main workflow
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
password: ${{ secrets.SSH_PASSWORD }}
port: ${{ secrets.SSH_PORT }}
script: |
cd ${{ secrets.PATH_TO_PROJECT }}
git pull
yarn install
yarn build
# ...
Nhưng sau khi test thử thì mình nhận ra vấn đề:
-
Workflow không thể trigger thủ công trên Github.
-
Workflow có thể đồng thời chạy nhiều instance cùng lúc, điều này gây ra vấn đề không đồng nhất vì workflow thao tác trực tiếp vào hosting.
Do đó mình chỉnh sửa lại như sau:
name: Deploy
on:
push:
branches: [master]
workflow_dispatch: # Để có thể trigger workflow trên Github
concurrency:
# Đặt tên group cho các instance, các instance có cùng tên group
# sẽ không được chạy cùng thời điểm. Ở đây mình lấy tên group = tên của
# workflow + tên branch nên mỗi branch chỉ có một instance chạy
# tại cùng một thời điểm
group: ${{ github.workflow }}-${{ github.ref }}
# Nếu trong cùng một group mà có nhiều instance được tạo ra thì sẽ dừng
# các instance cũ hơn lại và chạy instance mới nhất
cancel-in-progress: true
# ...
Nhìn có vẻ ổn rồi nhưng lại xảy ra vấn đề. Khi workflow chạy đến câu lệnh yarn install && yarn build thì các website đặt trên hosting của mình đều không truy cập được do câu lệnh trên đã sử dụng hết số lượng process có thể tạo ra rồi, ngoài ra còn đẩy RAM lên gần chạm limit nữa.
Thế nên mình đã nảy ra ý tưởng tại sao ta không chuyển phần build code sang môi trường của workflow và sau đó chỉ cần upload đống code đã build lên hosting rồi run thôi? Nghĩ như thế nên mình chỉnh sửa lại workflow như sau:
name: CD
# ...
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
env:
ZIP_FILE: ${{ github.event.repository.name }}-${{ github.ref_name }}.zip
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Run install
run: yarn install --frozen-lockfile --silent
- name: Build code
run: yarn build
env:
NODE_ENV: production
- name: Compress
run: rm -rf node_modules && zip -qr $ZIP_FILE .
- name: Configure SSH
run: |
mkdir -p ~/.ssh/
echo "$SSH_KEY" > ~/.ssh/server
chmod 600 ~/.ssh/server
cat > ~/.ssh/config <<END
Host server
HostName $SSH_HOST
User $SSH_USERNAME
IdentityFile ~/.ssh/server
PubkeyAuthentication yes
ChallengeResponseAuthentication no
PasswordAuthentication no
StrictHostKeyChecking no
END
env:
SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
SSH_KEY: ${{ secrets.SSH_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }}
- name: Upload
run: scp $ZIP_FILE ${{ secrets.SSH_USERNAME }}@server:~
- name: Migrate
run: # ...
Vậy là hosting đã không cần phải gánh phần build code nữa, cũng không cần phải cài đặt Git trên hosting để có thể pull code mới nhất về nữa vì giờ đây code mới đã được upload lên hosting thông qua SSH rồi.
Nhưng sau một vài lần test nữa mình thấy rằng trong mỗi workflow instance thì khi chạy câu lệnh yarn install đều phải tải các thư viện về từ đầu. Điều này làm tăng thời gian chờ đợi. Do đó mình sử dụng thêm một số thư viện caching để có thể sử dụng lại những thư viện đã được tải về trong những instance trước đó.
Ngoài ra mình còn quên để ý đến một thứ nữa đó là version của node.js, để sử dụng chính xác version của node.js trên hosting để tránh việc xảy ra lỗi không cần thiết thì mình đã tạo thêm file .nvmrc trong project và define version mình muốn sử dụng. File .nvmrc này được sử dụng bởi thư viện nvm dùng để quản lý version của node.js trong hệ thống.
Kết quả sau khi thêm caching và node.js version manager ta được như sau:
name: CD
# ...
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
env:
ZIP_FILE: ${{ github.event.repository.name }}-${{ github.ref_name }}.zip
steps:
- name: Checkout
uses: actions/checkout@v3
# nvm
- uses: actions/setup-node@v3
with:
node-version-file: ".nvmrc"
# Lấy đường dẫn của yarn cache trong workflow instance
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
# Caching
- uses: actions/cache@v3
id: yarn-cache
with:
# Đường dẫn đến yarn cache path đã lấy bên trên
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
# Sử dụng key để quản lý version của cache, ở đây thư viện sẽ tạo
# cache mới khi mà file `yarn.lock` thay đổi. Việc sử dụng key này
# tương tự như Redis
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
# Tiền tố của các key thay thế nếu key bên trên không tồn tại
restore-keys: |
${{ runner.os }}-yarn-
# Thư viện này hỗ trợ việc chạy các câu lệnh của yarn. Ngoài ra còn
# hỗ trợ caching và node.js version manager tương tự như 2 thư viện
# bên trên. Có thể sử dụng thay thế.
- name: Run install
uses: borales/actions-yarn@v4
with:
cmd: install --frozen-lockfile --silent
- name: Build code
run: yarn build
env:
NODE_ENV: production
- name: Compress
run: rm -rf node_modules && zip -qr $ZIP_FILE .
- name: Configure SSH
run: |
mkdir -p ~/.ssh/
echo "$SSH_KEY" > ~/.ssh/server
chmod 600 ~/.ssh/server
cat > ~/.ssh/config <<END
Host server
HostName $SSH_HOST
User $SSH_USERNAME
IdentityFile ~/.ssh/server
PubkeyAuthentication yes
ChallengeResponseAuthentication no
PasswordAuthentication no
StrictHostKeyChecking no
END
env:
SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
SSH_KEY: ${{ secrets.SSH_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }}
- name: Upload
run: scp $ZIP_FILE ${{ secrets.SSH_USERNAME }}@server:~
- name: Migrate
run: # ...
Và kết quả sau khi thêm caching: thời gian chạy trung bình giảm từ 2 phút 31 giây xuống 2 phút 16 giây. Không phải con số ấn tượng, nhưng khi iterate nhanh thì mỗi giây đều đáng.
Sau đó mình chuyển từ Strapi sang Laravel và viết lại workflow từ đầu. Nhưng các pattern cốt lõi vẫn giữ nguyên: build trên CI (không phải trên server), upload artifacts, rồi restart. Nếu bạn đang setup auto-deploy lần đầu, bắt đầu từ cách SSH rồi cải tiến dần khi gặp vấn đề thực tế.