cat blog/.md
GitHub Actions CI/CD: Tự động hóa từ test đến deploy
Mỗi lần sửa xong code, bạn phải: chạy test → build → SSH vào server → pull code → restart service. Lặp đi lặp lại, mỗi ngày vài lần. Rồi một hôm bạn quên chạy test, deploy thẳng lên production, và… nhận ticket từ QA lúc 11 giờ đêm.
Giải pháp? CI/CD — để máy làm những việc lặp đi lặp lại, còn bạn tập trung vào code. Và với GitHub Actions, bạn không cần setup Jenkins server hay dùng tool bên thứ ba — mọi thứ nằm ngay trong repo.
GitHub Actions cơ bản: Workflow, Job, Step
Một workflow là file YAML nằm trong .github/workflows/. Cấu trúc đơn giản:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm test
3 khái niệm cốt lõi:
- Workflow: Toàn bộ pipeline, trigger bởi events (push, PR, schedule…)
- Job: Một nhóm steps chạy trên cùng một runner. Các jobs mặc định chạy song song
- Step: Một action hoặc command cụ thể
Pipeline thực tế: Test → Build → Deploy
Đây là pipeline mình dùng cho các dự án Node.js, bao gồm 3 stages:
name: CI/CD Pipeline
on:
push:
branches: [main]
jobs:
# Stage 1: Test
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test -- --coverage
# Stage 2: Build Docker image
build:
needs: test # Chỉ chạy khi test pass
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
tags: |
myapp:latest
myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Stage 3: Deploy to VPS
deploy:
needs: build # Chỉ chạy khi build xong
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_PORT }}
script: |
cd /data/www/myapp
docker pull myapp:latest
docker compose up -d --force-recreate
docker image prune -f
Flow: Push code → Test tự chạy → Pass thì build Docker image → Push image → SSH vào VPS deploy. Toàn bộ mất khoảng 3-5 phút, thay vì 10-15 phút làm thủ công.
Deploy static site (như blog này)
Không phải project nào cũng cần Docker. Với static site (Astro, Next.js static export, Hugo…), pipeline đơn giản hơn nhiều:
name: Deploy Blog
on:
push:
branches: [main]
paths:
- 'src/**' # Chỉ deploy khi content thay đổi
- 'public/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Deploy to VPS
uses: burnett01/rsync-deployments@7
with:
switches: -avz --delete
path: dist/
remote_path: /data/www/mysite/public/
remote_host: ${{ secrets.VPS_HOST }}
remote_port: ${{ secrets.VPS_PORT }}
remote_user: ${{ secrets.VPS_USER }}
remote_key: ${{ secrets.VPS_SSH_KEY }}
Trick hay: dùng paths filter để chỉ trigger deploy khi file content thay đổi — không deploy lại khi bạn chỉ sửa README hay config CI.
Bảo mật secrets
Tuyệt đối không hardcode credentials trong workflow file. Dùng GitHub Secrets:
Settings → Secrets and variables → Actions → New repository secret
Một số rules mình tuân thủ:
- Dùng fine-grained tokens thay vì personal access tokens toàn quyền
- SSH key riêng cho CI/CD — không dùng key cá nhân
- Giới hạn quyền của deploy user trên server — không dùng root
# Giới hạn permissions cho workflow
permissions:
contents: read # Chỉ đọc code, không ghi
packages: write # Cho phép push Docker image
Thêm permissions block giúp áp dụng principle of least privilege — nếu workflow bị compromise, attacker không thể push code hay xóa branches.
Tips tối ưu thời gian build
1. Cache dependencies
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm' # Cache npm packages giữa các lần build
Không có cache: npm ci mất 30-60 giây. Có cache: 3-5 giây. Một dòng config, tiết kiệm hàng phút mỗi lần build.
2. Chạy jobs song song khi có thể
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- run: npm test
# Build chỉ cần chờ cả lint và test pass
build:
needs: [lint, test]
Lint và test chạy đồng thời — tổng thời gian bằng job chậm nhất, không phải tổng cộng.
3. Skip CI khi không cần
# Commit message chứa [skip ci] sẽ không trigger workflow
git commit -m "docs: update README [skip ci]"
Hoặc filter bằng paths:
on:
push:
paths-ignore:
- '**.md'
- '.vscode/**'
- 'docs/**'
4. Matrix builds cho multi-version testing
jobs:
test:
strategy:
matrix:
node-version: [18, 20, 22]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm test
6 combinations chạy song song — test trên 3 Node versions x 2 OS chỉ mất thời gian bằng 1 lần chạy.
Sai lầm mình từng mắc
1. Deploy trên mọi push
Ban đầu mình set trigger on: push cho cả deploy job. Kết quả: mỗi commit fix typo cũng trigger full deploy. Giải pháp: chỉ deploy trên main branch, hoặc dùng workflow_dispatch cho manual trigger.
2. Không lock action versions
# ❌ Dùng tag mới nhất — có thể break bất cứ lúc nào
- uses: actions/checkout@main
# ✅ Pin version cụ thể
- uses: actions/checkout@v4
Dùng tag @main hoặc @latest nghĩa là workflow có thể break khi action author push breaking changes. Luôn pin version cụ thể.
3. Không set timeout
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10 # Kill nếu chạy quá 10 phút
Mặc định GitHub Actions cho phép job chạy tới 6 tiếng. Nếu test bị hang, bạn sẽ mất 6 tiếng minutes miễn phí. Set timeout hợp lý.
Tổng kết
GitHub Actions không khó — khó là biết cái gì cần tự động hóa và tự động hóa đúng cách. Checklist ngắn gọn:
- Bắt đầu với CI — chạy test tự động trên mọi PR, đây là baseline
- Thêm CD từ từ — deploy staging trước, production sau khi đã tin tưởng pipeline
- Cache mọi thứ có thể — dependencies, Docker layers, build artifacts
- Bảo mật secrets — fine-grained tokens, SSH key riêng, least privilege
- Pin action versions — tránh bị break bởi upstream changes
- Set timeout — đừng để job chạy 6 tiếng vì test bị hang
CI/CD là một trong những đầu tư có ROI cao nhất cho developer. Setup một lần, tiết kiệm hàng giờ mỗi tuần — và quan trọng hơn, bạn sẽ không bao giờ quên chạy test trước khi deploy nữa.
Bạn đang dùng CI/CD tool nào? Đã setup GitHub Actions cho project chưa? Chia sẻ với mình qua email nhé!