CI/CD: Tự Động Hóa Build, và Deploy Canary Có Rollback

K
Kai··6 min read

Suốt mười sáu bài, mỗi thay đổi đều qua tay: gõ sam deploy, chờ, kiểm. Cách đó ổn khi đang dựng, nhưng đưa ra production thì cần một quy trình tự động và an toàn: mỗi thay đổi được build và kiểm trước, rồi deploy theo cách không làm gãy dịch vụ nếu có lỗi. Bài này dựng đúng vòng đó, và gặp hai cái bẫy thật trên đường.

Mục tiêu

Dựng CI bằng GitHub Actions để mỗi push được build và validate tự động, và CD an toàn bằng canary qua CodeDeploy: dịch dần traffic sang phiên bản mới rồi tự rollback nếu alarm lỗi kêu. Phần CI chạy thật trên GitHub, phần canary chạy thật trên AWS. Chi phí không đáng kể.

CI: build và kiểm mỗi push

CI lo phần integration: mỗi lần đẩy code, một máy sạch tự build lại và kiểm, để lỗi lộ ra ngay. Workflow GitHub Actions cho việc đó không cần credentials AWS, vì sam validate --lint chạy offline bằng cfn-lint:

name: CI
on:
  push: { branches: [main] }
  pull_request:
env:
  AWS_DEFAULT_REGION: ap-southeast-1
jobs:
  build-and-validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - run: npm install -g esbuild
      - uses: aws-actions/setup-sam@v2
        with: { use-installer: true }
      - run: sam build
      - run: sam validate --lint

Bẫy thứ nhất: esbuild không có trên PATH

Lần chạy CI đầu tiên fail ngay ở sam build:

Build Failed
Error: ... Esbuild Failed: Cannot find esbuild. esbuild must be installed on the
host machine to use this feature.

Dưới máy thì build chạy ngon, nhưng CI thì không. Lý do: CodeUri của các hàm là thư mục src, ở đó không có package.json riêng, nên SAM không tìm thấy esbuild đã cài ở node_modules gốc. Dưới máy nó tình cờ thấy được, trên runner sạch thì không. Cách sửa là cài esbuild toàn cục trong CI, dòng npm install -g esbuild. Sau đó CI xanh:

✓ build-and-validate in 23s
  ✓ npm ci
  ✓ npm install -g esbuild
  ✓ SAM build
  ✓ SAM validate (lint, offline)
✓ main CI · success

Đây là loại khác biệt môi trường mà chỉ CI mới lộ ra, và cũng là lý do nên có CI: nó chạy trên một máy sạch giống production hơn máy của bạn.

CD: deploy canary, không deploy một phát

Phần deploy của series tới giờ là thay toàn bộ hàm cùng lúc. Với production, an toàn hơn là canary: đưa phiên bản mới ra cho một phần nhỏ traffic trước, theo dõi, rồi mới chuyển hết. SAM làm việc này qua CodeDeploy bằng DeploymentPreference, gắn với một alias tự publish và một alarm để rollback:

ResolveLinkFunction:
  Properties:
    FunctionName: !Sub "${AWS::StackName}-resolve"
    AutoPublishAlias: live
    DeploymentPreference:
      Type: Canary10Percent5Minutes
      Alarms:
        - !Ref ResolveErrorAlarm

AutoPublishAlias: live khiến mỗi deploy publish một version mới và trỏ alias live vào đó. DeploymentPreference bảo CodeDeploy dịch 10% traffic sang version mới, giữ trong 5 phút, rồi chuyển nốt 100% nếu không có sự cố. Alarms là điều kiện rollback: nếu ResolveErrorAlarm (đặt ở bài 14) chuyển sang ALARM trong cửa sổ đó, CodeDeploy tự đưa traffic về version cũ.

Bẫy thứ hai: phụ thuộc vòng

Cách khai trên thoạt nhìn tạo một vòng: hàm tham chiếu alarm (qua Alarms), còn alarm tham chiếu hàm (qua chiều FunctionName trong dimension !Ref ResolveLinkFunction). CloudFormation từ chối vì phụ thuộc vòng. Cách cắt là đặt FunctionName tường minh cho hàm, rồi để alarm trỏ tới tên đó như một chuỗi cố định thay vì !Ref:

# tren alarm:
Dimensions:
  - Name: FunctionName
    Value: !Sub "${AWS::StackName}-resolve"

Giờ alarm không còn phụ thuộc vào tài nguyên hàm, vòng bị phá, và stack deploy được.

Canary chạy thật

Lần deploy đầu thêm AutoPublishAlias chỉ tạo alias, chưa canary. Lần deploy sau (có đổi code) mới kích canary. Khi đó CodeDeploy tạo một deployment dịch traffic:

$ aws deploy get-deployment --deployment-id d-K0GQEAB5J \
    --query 'deploymentInfo.{status:status,config:deploymentConfigName}'
{
    "status": "InProgress",
    "config": "CodeDeployDefault.LambdaCanary10Percent5Minutes"
}

Trong lúc canary chạy, dịch vụ vẫn phục vụ bình thường, request mở link vẫn trả 301, vì cả version cũ và mới đều sống và traffic chia theo tỉ lệ:

$ curl -s -o /dev/null -w '%{http_code}' "$API/$CODE"
301
   push code ──▶ GitHub Actions CI (build + validate)  ──▶ (xanh) ──▶ deploy
                                                                        │
                                              CodeDeploy canary         ▼
                                   ┌────────────────────────────────────────┐
                                   │ 10% traffic ──5 phut──▶ 100% traffic     │  (ok)
                                   │      │                                   │
                                   │      └── ResolveErrorAlarm ALARM ──▶ rollback ve version cu
                                   └────────────────────────────────────────┘

Lần deploy ở đây lành (code chỉ đổi một dòng log), nên canary chạy hết 5 phút rồi chuyển 100% và không có rollback nào kích hoạt — ta chứng minh đường đi bình thường chứ không cố tình làm hỏng để gây rollback. Cơ chế rollback thì đã gắn sẵn: nếu trong 5 phút đó tỉ lệ lỗi tăng và ResolveErrorAlarm kêu, CodeDeploy không chuyển nốt traffic mà kéo hết về version cũ, nên chỉ 10% người dùng chạm phải version lỗi trong thời gian ngắn thay vì toàn bộ. Khác biệt là ở chỗ có một cửa sổ quan sát và một lối lui tự động, thay vì đổi thẳng 100% rồi mới biết hỏng.

Còn deploy từ CI thì sao

Workflow trên dừng ở build và validate, không deploy, để giữ phần CI không cần credentials. Bước deploy (CD đầy đủ) sẽ thêm một job dùng OIDC để lấy quyền AWS tạm thời thay vì cất access key dài hạn trong GitHub: GitHub Actions đổi một OIDC token lấy một IAM role qua sts:AssumeRoleWithWebIdentity, rồi chạy sam deploy. Cách này không có khóa bí mật nào nằm trong repo hay secret của CI. Ở đây ta vẫn deploy từ máy để giữ vòng lặp gọn cho việc nghiệm thu, nhưng đường nâng cấp lên deploy tự động qua OIDC là một bước thêm job, không phải dựng lại.

🧹 Dọn dẹp

Các link và user demo đã xóa sau khi kiểm; CI, alias và cấu hình canary là một phần của stack nên giữ lại.

Tổng kết

Quy trình giờ tự động và an toàn hơn. CI build và validate mỗi push trên một máy sạch, và chính máy sạch đó lộ ra cái bẫy esbuild mà máy cá nhân không gặp. CD dùng canary qua CodeDeploy để dịch dần traffic và tự rollback theo alarm, với bẫy phụ thuộc vòng được cắt bằng tên hàm tường minh. Nguyên tắc ở đây: không thay đổi nào ra thẳng 100% mà không qua build, kiểm, và một cửa sổ quan sát có lối lui.

Còn một câu hỏi mà mọi thứ serverless cuối cùng phải trả lời: tốn bao nhiêu tiền? Bài sau bóc hóa đơn của chính sản phẩm này theo từng dịch vụ, chỉ ra cái gì nằm trong free tier, cái gì tính tiền thật, và vài lựa chọn thiết kế trong series đã tiết kiệm ở đâu.