GitHub Actions CI/CD: Tự động hóa từ test đến deploy — hoatq.dev

cat blog/.md

GitHub Actions CI/CD: Tự động hóa từ test đến deploy

date: tags: github-actions, ci-cd, devops, automation

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ủ:

  1. Dùng fine-grained tokens thay vì personal access tokens toàn quyền
  2. SSH key riêng cho CI/CD — không dùng key cá nhân
  3. 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óatự động hóa đúng cách. Checklist ngắn gọn:

  1. Bắt đầu với CI — chạy test tự động trên mọi PR, đây là baseline
  2. Thêm CD từ từ — deploy staging trước, production sau khi đã tin tưởng pipeline
  3. Cache mọi thứ có thể — dependencies, Docker layers, build artifacts
  4. Bảo mật secrets — fine-grained tokens, SSH key riêng, least privilege
  5. Pin action versions — tránh bị break bởi upstream changes
  6. 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é!

// reactions



hoatq@dev : ~/blog $