Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn vừa viết xong một tính năng mới. Nó chạy hoàn hảo trên máy tính của bạn. Bạn tự tin đẩy thẳng lên production, và... trang web sập. Hóa ra máy bạn dùng Node 20, còn server production dùng Node 16. Hóa ra biến môi trường DATABASE_URL trên máy bạn trỏ về một database test có sẵn dữ liệu mẫu, còn production thì trống rỗng. Hóa ra bạn quên rằng production cần một biến STRIPE_SECRET_KEY mà máy bạn chưa từng cần đến.
Đây chính xác là lý do vì sao không đội ngũ phát triển chuyên nghiệp nào đẩy code thẳng từ laptop lên production. Thay vào đó, code phải "leo qua từng bậc thang" — đi qua một chuỗi các môi trường (environment) được sắp xếp cẩn thận, mỗi bậc lại giống production hơn một chút, để lỗi được phát hiện sớm khi cái giá phải trả còn rẻ.
Trong các bài trước, bạn đã học về từng chiến lược triển khai riêng lẻ như rolling update (Bài 17), blue-green (Bài 18), canary (Bài 19). Bài 22 này trả lời một câu hỏi khác, mang tính kiến trúc hơn: làm sao để tổ chức nhiều môi trường thành một đường ống (pipeline) có kỷ luật, để một thay đổi code đi từ "vừa viết xong" đến "phục vụ khách hàng thật" một cách an toàn, tự động và có thể kiểm soát?
Đây là xương sống của mọi hệ thống CI/CD nghiêm túc. Nắm vững nó, bạn sẽ hiểu được bức tranh tổng thể mà tất cả các kỹ thuật khác gắn vào.
Khái niệm cốt lõi
Các tầng môi trường chuẩn (standard tiers)
Một pipeline multi-environment điển hình gồm chuỗi tầng như sau:
Local → Dev → Staging → Production (+ DR)
Mỗi tầng có một vai trò riêng, và đừng nhầm lẫn giữa chúng:
- Local: máy của lập trình viên. Đây là nơi code được viết và chạy thử nhanh. Không ai khác nhìn thấy. Dữ liệu là giả, cấu hình lỏng lẻo.
- Dev (Development): môi trường chung đầu tiên, nơi code của nhiều người được gộp lại. Mỗi lần merge vào nhánh chính là tự động deploy lên đây. Mục tiêu: phát hiện sớm xung đột tích hợp giữa các thành viên. Dev thường không ổn định, và điều đó là bình thường — nó được sinh ra để "vỡ".
- Staging (còn gọi là Pre-production, UAT): đây là bản sao gần nhất của production. Cùng phiên bản hệ điều hành, cùng kích cỡ máy chủ (theo tỷ lệ), cùng cấu trúc database, cùng loại secret. Đây là nơi QA kiểm thử cuối, nơi khách hàng nội bộ nghiệm thu (User Acceptance Testing), nơi chạy các bài kiểm thử hiệu năng. Nguyên tắc vàng: nếu nó chạy được ở staging, nó phải chạy được ở production.
- Production (Prod): môi trường thật, phục vụ người dùng thật, tiền thật. Chỉ những gì đã được kiểm chứng ở staging mới được đặt chân đến đây.
- DR (Disaster Recovery): môi trường dự phòng thảm họa, thường đặt ở một vùng địa lý (region) khác, sẵn sàng tiếp quản khi production chính gặp sự cố nghiêm trọng. Chúng ta sẽ đào sâu về diễn tập DR ở Bài 49; ở đây bạn chỉ cần biết nó là một "tầng" trong bức tranh tổng thể.
Promotion (thăng cấp) — trái tim của multi-environment
Khái niệm quan trọng nhất của bài này là promotion: hành động đưa một artifact (sản phẩm build ra) từ môi trường thấp lên môi trường cao hơn.
Có một nguyên tắc tối quan trọng mà người mới hay vi phạm: build một lần, deploy nhiều lần (build once, deploy many). Nghĩa là bạn build ra một artifact duy nhất — chẳng hạn một Docker image với tag myapp:1.4.7-a3f9c2 — và chính artifact đó, không phải bản build lại, được promote lần lượt qua dev → staging → prod.
Vì sao? Vì nếu bạn build lại ở mỗi tầng, bạn có ba artifact khác nhau về mặt lý thuyết là "giống nhau", nhưng thực tế có thể khác vì một thư viện vừa cập nhật, một cache khác đi, một dòng thời gian khác. Bạn kiểm thử một thứ ở staging rồi lại chạy một thứ khác ở production. Đó là công thức của những lỗi "chỉ xảy ra ở production".
Cấu hình tách khỏi code (config vs code)
Nếu artifact giống hệt nhau qua các tầng, thì cái gì khác biệt? Câu trả lời là cấu hình — và cấu hình phải nằm ngoài artifact. Cùng một image, nhưng ở dev nó đọc DATABASE_URL trỏ về db-dev, ở prod nó đọc DATABASE_URL trỏ về db-prod. Đây chính là nguyên tắc "Config" trong 12-Factor App mà chúng ta sẽ bàn kỹ ở Bài 37.
Cổng phê duyệt (gates & approvals)
Giữa các tầng, đặc biệt là trước production, ta thường đặt cổng phê duyệt. Có hai loại: cổng tự động (chờ tất cả test pass, chờ smoke test xanh) và cổng thủ công (chờ một người có thẩm quyền bấm nút "Approve"). Cổng thủ công trước production là tuyến phòng thủ cuối cùng của con người.
Tình huống thực tế
Ví dụ 1 — Sàn thương mại điện tử Tiki và bài học "staging không giống prod"
Giả sử một đội tại một sàn thương mại điện tử lớn ở Việt Nam (lấy bối cảnh kiểu Tiki) có pipeline gồm Dev → Staging → Prod. Nhìn qua thì rất chuẩn. Nhưng có một chi tiết bị bỏ sót: staging của họ chỉ có 1 pod chạy ứng dụng, trong khi production có 12 pod sau một load balancer.
Một hôm, đội triển khai tính năng "giỏ hàng chia sẻ" — cho phép nhiều thiết bị cùng xem một giỏ hàng. Ở staging với 1 pod duy nhất, mọi request đều vào chung một tiến trình, nên trạng thái giỏ hàng được lưu trong bộ nhớ (in-memory) hoạt động hoàn hảo. QA duyệt, promote lên prod.
Trên production với 12 pod, mỗi request của cùng một người dùng có thể rơi vào một pod khác nhau. Người dùng thêm sản phẩm ở pod số 3, load lại trang thì request đi vào pod số 7 — giỏ hàng trống. Hàng nghìn khách hàng phàn nàn trong giờ cao điểm khuyến mãi.
Bài học rút ra: staging phải giống production về kiến trúc, không chỉ về code. Nếu prod có nhiều instance, staging cũng phải có ít nhất 2 để bộc lộ các lỗi liên quan đến trạng thái phân tán. Sự khác biệt về topology (số lượng node, có hay không có cache tập trung) là nơi ẩn náu yêu thích của bug production.
Ví dụ 2 — Startup fintech và mô hình "build once, deploy many"
Một startup fintech ở TP.HCM, khoảng 15 kỹ sư, ban đầu dùng cách build lại code ở mỗi môi trường: pipeline staging tự git checkout rồi docker build, pipeline prod cũng làm y hệt. Trong 6 tháng, họ gặp 3 sự cố kiểu "chạy ở staging, hỏng ở prod" mà không ai giải thích được.
Nguyên nhân cuối cùng lộ ra: Dockerfile của họ dùng FROM node:18 (không ghim phiên bản chính xác). Bản build staging chạy lúc image node:18 còn là 18.15, bản build prod chạy hai ngày sau khi Docker Hub đã cập nhật node:18 thành 18.16 — mà 18.16 thay đổi hành vi xử lý timezone, làm sai lệch tính toán ngày đáo hạn khoản vay.
Họ sửa bằng cách chuyển sang build once, deploy many: CI build đúng một image, gắn tag theo commit SHA (ví dụ fintech-api:8c1d0a2), đẩy lên registry. Sau đó cả staging lẫn prod đều kéo đúng image tag đó về deploy. Từ đó, số sự cố "chỉ hỏng ở prod" giảm về gần bằng 0.
Bài học rút ra: artifact phải bất biến (immutable) và được định danh duy nhất. Cái bạn kiểm thử phải đúng là cái bạn triển khai — không phải "phiên bản anh em họ hàng" của nó. Chi tiết về chiến lược đặt tag chúng ta sẽ học sâu ở Bài 32.
Ví dụ 3 — Đội SaaS Đông Nam Á và cổng phê duyệt thủ công cứu một đêm
Một công ty SaaS phục vụ khách hàng doanh nghiệp tại Singapore và Việt Nam có pipeline 4 tầng với một cổng phê duyệt thủ công trước production. Ban đầu vài kỹ sư càm ràm rằng cổng này làm chậm việc release, đề nghị bỏ đi cho tự động hoàn toàn.
May là họ giữ lại. Một tối thứ Sáu, một PR merge vào nhánh chính vô tình chứa một thay đổi cấu hình rate-limit từ 1000 request/phút xuống 10. Nó qua được dev (ít tải nên không ai thấy), qua được staging (test tự động không kiểm rate-limit). Khi pipeline dừng ở cổng phê duyệt prod, người phụ trách release đọc phần "diff cấu hình" mà pipeline hiển thị, thấy con số 10 bất thường, và từ chối phê duyệt. Sự cố tiềm tàng bị chặn ngay trước cửa production.
Bài học rút ra: tự động hóa tối đa, nhưng giữ một điểm dừng có con người trước production cho các hệ thống quan trọng — kèm theo việc hiển thị rõ "cái gì sắp thay đổi" để người duyệt có đủ thông tin quyết định. Cổng phê duyệt không phải sự chậm chạp, mà là bảo hiểm.
Hướng dẫn từng bước
Hãy dựng một pipeline multi-environment cơ bản với GitHub Actions, thể hiện đúng promotion flow từ ghi chú gốc của bài.
Bước 1 — Định nghĩa các environment trong repo. Trong GitHub, vào Settings → Environments, tạo ba environment: dev, staging, production. Với production, bật "Required reviewers" để tạo cổng phê duyệt thủ công. Mỗi environment sẽ giữ secret riêng của nó (URL database, khóa API...).
Bước 2 — Tách build khỏi deploy. Job build chạy một lần, tạo artifact và đẩy lên registry với tag theo commit:
name: Multi-env Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.meta.outputs.tag }}
steps:
- uses: actions/checkout@v4
- id: meta
run: echo "tag=myapp:${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
- name: Build & push image
run: |
docker build -t registry.example.com/${{ steps.meta.outputs.tag }} .
docker push registry.example.com/${{ steps.meta.outputs.tag }}
Bước 3 — Deploy lên dev tự động. Job này chạy ngay sau build, không cần phê duyệt:
deploy-dev:
needs: build
runs-on: ubuntu-latest
environment: dev
steps:
- name: Deploy to dev
run: ./deploy.sh dev registry.example.com/${{ needs.build.outputs.image_tag }}
Bước 4 — Promote lên staging sau khi dev ổn. Chú ý needs: deploy-dev — staging chỉ chạy khi dev đã thành công. Chính artifact ở bước build được tái sử dụng:
deploy-staging:
needs: [build, deploy-dev]
runs-on: ubuntu-latest
environment: staging
steps:
- name: Deploy to staging
run: ./deploy.sh staging registry.example.com/${{ needs.build.outputs.image_tag }}
- name: Run smoke tests
run: ./smoke-test.sh https://staging.example.com
Bước 5 — Promote lên production qua cổng phê duyệt. Vì environment: production đã bật required reviewers, pipeline sẽ tạm dừng tại đây và chờ người duyệt bấm nút. Nếu smoke test staging fail, ta không bao giờ đến được đây:
deploy-production:
needs: [build, deploy-staging]
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to production
run: ./deploy.sh production registry.example.com/${{ needs.build.outputs.image_tag }}
- name: Verify production
run: ./smoke-test.sh https://example.com
Bước 6 — Đảm bảo cấu hình đến từ environment, không nhúng vào image. Script deploy.sh phải đọc secret/config từ chính environment tương ứng của GitHub, ví dụ ${{ secrets.DATABASE_URL }}, để cùng một image chạy đúng ở mỗi tầng.
Sau các bước này, mỗi lần bạn merge vào main, hệ thống sẽ tự build một artifact, thả nó qua dev → staging (kèm smoke test), rồi dừng chờ bạn phê duyệt trước khi chạm production.
Lỗi thường gặp & mẹo
Lỗi 1 — "Staging và prod chỉ khác nhau chút xíu". Cái "chút xíu" đó (số instance, có Redis hay không, phiên bản database) chính là nơi bug sinh sống. Mẹo: định kỳ so sánh cấu hình staging và prod bằng công cụ (config diff), giữ chúng đồng bộ càng chặt càng tốt. Nếu ngân sách không cho phép staging bằng đúng prod, ít nhất hãy giữ cùng bản chất kiến trúc theo tỷ lệ nhỏ hơn.
Lỗi 2 — Build lại ở mỗi tầng. Như ví dụ fintech đã chỉ ra, đây là nguồn gốc của lỗi ma. Luôn luôn: build once, promote the same artifact.
Lỗi 3 — Dùng chung một bộ secret cho mọi môi trường. Rất nguy hiểm: nếu dev bị lộ secret, prod cũng toi. Tệ hơn, một test ở dev có thể vô tình gọi vào cổng thanh toán thật của prod. Mỗi environment phải có secret riêng biệt, cô lập hoàn toàn.
Lỗi 4 — Bỏ qua "drift" cấu hình theo thời gian. Ai đó sửa tay một biến trên prod lúc chữa cháy rồi quên cập nhật vào code. Ba tháng sau deploy tiếp theo ghi đè mất, sự cố tái diễn. Mẹo: quản lý cấu hình theo kiểu khai báo (declarative), lưu trong Git, không sửa tay trực tiếp trên môi trường.
Lỗi 5 — Coi dev là môi trường phải luôn ổn định. Dev sinh ra để vỡ và để phát hiện xung đột sớm. Đừng lãng phí công sức bắt dev phải "sạch" 100%. Ngược lại, staging thì phải được bảo vệ nghiêm ngặt.
Mẹo hay: thêm một bước hiển thị "diff" giữa phiên bản đang chạy trên prod và phiên bản sắp deploy (cả code lẫn config) ngay tại cổng phê duyệt. Đúng như ví dụ SaaS, đây là thứ giúp người duyệt bắt được bất thường.
Bài tập thực hành
- Vẽ sơ đồ pipeline của riêng bạn. Chọn một dự án bạn đang làm (hoặc giả định). Vẽ chuỗi môi trường Local → Dev → Staging → Prod. Với mỗi tầng, ghi rõ: ai deploy (tự động hay thủ công), dữ liệu ở đó là gì, secret nào cần, và ai được quyền truy cập.
- Dựng pipeline mẫu. Tạo một repo GitHub nhỏ với một ứng dụng "Hello World", cấu hình đúng 3 environment như phần hướng dẫn. Bật required reviewer cho
production. Merge một thay đổi và quan sát pipeline dừng lại chờ phê duyệt như thế nào.
- Kiểm chứng "build once, deploy many". Trong pipeline trên, in ra image tag ở cả job staging lẫn production. Xác nhận chúng giống hệt nhau. Sau đó cố tình build lại ở prod và giải thích bằng lời tại sao cách này nguy hiểm.
- Săn tìm sự khác biệt. Với một hệ thống thật bạn có quyền xem, liệt kê 5 điểm khác nhau giữa staging và production. Đánh giá điểm nào có thể gây ra bug production, và đề xuất cách thu hẹp khoảng cách đó.
Tóm tắt
Multi-environment pipeline là cách chúng ta biến việc đưa code ra production từ một cú nhảy đầy rủi ro thành một chuỗi bước có kỷ luật. Những điểm cốt lõi cần nhớ:
- Chuỗi tầng chuẩn: Local → Dev → Staging → Prod (+ DR), mỗi tầng có vai trò riêng và mức độ giống production tăng dần.
- Promotion là trái tim: đưa cùng một artifact bất biến leo dần qua các tầng, không build lại.
- Build once, deploy many: cái bạn kiểm thử phải đúng là cái bạn triển khai.
- Config tách khỏi code: cùng một image, cấu hình khác nhau đến từ mỗi environment.
- Staging phải giống prod về kiến trúc, không chỉ về code — sự khác biệt topology là nơi bug ẩn náu.
- Cổng phê duyệt trước production là bảo hiểm, không phải sự chậm trễ.