DevOps 4 — CI/CD Pipelines
DevOps 4 — CI/CD Pipelines
Nếu bạn từng "deploy bằng tay": SSH vào server, git pull, restart service, rồi cầu mong nó chạy — thì bạn đã trải nghiệm trực tiếp lý do CI/CD ra đời. Mỗi lần làm thủ công là một lần có khả năng quên bước, sai biến môi trường, hoặc đẩy code chưa qua test lên production. CI/CD biến chuỗi thao tác thủ công, dễ sai và khó lặp lại đó thành một đường ống tự động (pipeline) chạy giống hệt nhau mỗi lần.
Bài này đi từ khái niệm gốc đến pipeline thực tế: bạn sẽ hiểu rõ CI khác CD ở đâu, một pipeline được cấu thành từ những gì, viết một workflow GitHub Actions hoàn chỉnh (build, test, build Docker image), quản lý secrets an toàn, và chọn chiến lược triển khai (blue-green, canary, rolling) cùng cách rollback khi mọi thứ hỏng.
CI vs CD (Delivery) vs CD (Deployment)
Ba chữ cái viết tắt này hay bị gộp làm một, nhưng chúng là ba giai đoạn riêng biệt với mức độ tự động hoá tăng dần.
Continuous Integration (CI) — Tích hợp liên tục
CI là thực hành mỗi lập trình viên merge code vào nhánh chung thường xuyên (nhiều lần mỗi ngày), và mỗi lần merge sẽ kích hoạt một quy trình tự động: build dự án + chạy test. Mục tiêu của CI là phát hiện xung đột và lỗi tích hợp càng sớm càng tốt, khi thay đổi còn nhỏ và dễ sửa.
Trước CI, người ta thường làm "integration hell": mỗi người code riêng vài tuần, rồi cuối sprint mới ghép lại — và phát hiện hàng trăm xung đột. CI loại bỏ điều đó bằng cách giữ nhánh chính luôn ở trạng thái "build được, test xanh".
CI trả lời câu hỏi: "Code này có integrate được không?"
Continuous Delivery (CD) — Phân phối liên tục
Continuous Delivery mở rộng CI: sau khi build và test thành công, hệ thống tự động đóng gói artifact và đưa nó vào trạng thái sẵn sàng deploy (đẩy lên staging, tạo Docker image, đẩy lên registry...). Nhưng bước deploy lên production vẫn cần con người bấm nút phê duyệt.
Continuous Delivery trả lời: "Code này có thể release bất cứ lúc nào — chỉ cần ai đó đồng ý."
Continuous Deployment (CD) — Triển khai liên tục
Continuous Deployment đi xa nhất: nếu toàn bộ pipeline xanh, code được deploy thẳng lên production một cách tự động, không cần con người duyệt. Mỗi commit pass test là một lần lên production.
Điều này đòi hỏi độ tin cậy rất cao vào bộ test và hệ thống giám sát/rollback. Continuous Deployment trả lời: "Code đã tự lên production rồi."
| Giai đoạn | Build + Test tự động | Đóng gói + chuẩn bị release | Deploy production |
|---|---|---|---|
| Continuous Integration | ✅ | ❌ | ❌ |
| Continuous Delivery | ✅ | ✅ | Thủ công (bấm duyệt) |
| Continuous Deployment | ✅ | ✅ | ✅ Tự động |
Thực tế, hầu hết tổ chức dừng ở Continuous Delivery — vì giữ một "cổng người" trước production là rất hợp lý khi bộ test chưa đủ tin cậy.
Vì sao phải tự động hoá build–test–deploy
Tự động hoá không chỉ để "tiết kiệm thời gian gõ lệnh". Lý do sâu hơn:
- Tính lặp lại (reproducibility): Pipeline chạy y hệt nhau mọi lần. Không còn "máy tôi chạy được" — vì môi trường build được định nghĩa rõ ràng trong code.
- Phát hiện lỗi sớm: Test chạy ngay khi push, không đợi đến lúc deploy. Lỗi rẻ nhất là lỗi được bắt trong vài phút sau khi viết.
- Giảm rủi ro con người: Quên chạy migration, sai biến môi trường, deploy nhầm branch — những lỗi này biến mất khi máy làm thay.
- Tăng tốc độ release: Team chín muồi có thể release nhiều lần mỗi ngày một cách an toàn, thay vì gom thành một "big bang release" hàng tháng đầy rủi ro.
- Tài liệu sống: File pipeline chính là tài liệu mô tả cách build và deploy dự án. Đọc nó là hiểu quy trình.
Cấu trúc một pipeline
Dù dùng công cụ nào (GitHub Actions, GitLab CI, Jenkins), một pipeline đều chia sẻ cùng các khái niệm. Hãy hiểu chúng trước khi xem cú pháp cụ thể.
Trigger — Cái gì kích hoạt pipeline
Pipeline không tự chạy; nó được trigger bởi một sự kiện: push code lên một nhánh, mở pull request, tạo tag (release), chạy theo lịch (cron), hoặc bấm tay (manual dispatch).
Stage — Giai đoạn lớn
Stage là nhóm công việc theo logic, thường chạy tuần tự: ví dụ build → test → deploy. Stage sau chỉ chạy nếu stage trước thành công. (Trong GitHub Actions không có khái niệm "stage" riêng, mà dùng quan hệ phụ thuộc needs giữa các job để mô phỏng.)
Job — Công việc chạy độc lập
Job là một đơn vị chạy trên một runner (máy ảo/container). Các job trong cùng một stage có thể chạy song song (ví dụ: test trên Linux và test trên Windows cùng lúc). Mỗi job khởi động trong môi trường sạch.
Step — Bước nhỏ trong job
Step là một lệnh hoặc một action cụ thể trong job: checkout code, cài dependency, chạy lệnh test... Các step trong cùng job chạy tuần tự và chia sẻ chung filesystem của runner.
Artifact — Sản phẩm được tạo ra
Artifact là output của pipeline cần được lưu lại hoặc truyền giữa các job: file build (.jar, dist/), Docker image, báo cáo test coverage. Artifact cho phép job "build" tạo ra thứ gì đó để job "deploy" sử dụng, dù chúng chạy trên runner khác nhau.
Kiểm thử trong pipeline
"Test trong CI" không chỉ là chạy unit test. Một pipeline trưởng thành có nhiều lớp kiểm tra, mỗi lớp bắt một loại lỗi khác nhau và có chi phí (thời gian) khác nhau.
Lint & format check
Chạy nhanh nhất, đặt đầu tiên. Kiểm tra code có đúng quy ước style không (ESLint, Black, golangci-lint). Mục đích: thống nhất codebase và bắt những lỗi cú pháp/logic đơn giản trước khi tốn thời gian build.
Unit test
Kiểm tra từng hàm/module riêng lẻ, không phụ thuộc database hay network. Nhanh (mili giây mỗi test), nên chạy hàng nghìn test trong vài phút. Đây là lớp bảo vệ chính.
Integration test
Kiểm tra nhiều thành phần phối hợp: code + database thật, code + API ngoài. Chậm hơn unit test (cần spin up DB), nên thường chạy sau và có thể dùng service container.
Security scan
- SAST (Static Application Security Testing): quét source code tìm lỗ hổng (SQL injection, hardcoded secret) — ví dụ Semgrep, CodeQL.
- Dependency scan: kiểm tra thư viện phụ thuộc có CVE đã biết không —
npm audit, Dependabot, Trivy. - Container scan: quét Docker image tìm lỗ hổng trong base image và package OS — Trivy, Grype.
Nguyên tắc vàng: đặt test nhanh và rẻ lên trước (lint, unit). Nếu chúng fail, pipeline dừng ngay, không lãng phí phút chạy integration test.
Ví dụ GitHub Actions
GitHub Actions định nghĩa pipeline bằng file YAML đặt trong thư mục .github/workflows/. Hãy mổ xẻ cấu trúc cốt lõi: on (trigger), jobs (các job), và trong mỗi job là steps.
Workflow CI cơ bản: build & test
# .github/workflows/ci.yml
name: CI
# Trigger: chạy khi push lên main/develop hoặc mở PR vào main
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
name: Lint & Test
runs-on: ubuntu-latest
# Service container: DB tạm cho integration test
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: testpass
POSTGRES_DB: appdb_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install dependencies
run: pip install -r requirements.txt
- name: Lint (ruff)
run: ruff check .
- name: Run tests
env:
DATABASE_URL: postgresql://postgres:testpass@localhost:5432/appdb_test
run: pytest --cov=app --cov-report=xml
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.xml
Đọc từ trên xuống:
onkhai báo trigger. Workflow chạy khi có push vàomain/develop, hoặc khi có PR nhắm vàomain.jobs.testlà một job chạy trên runnerubuntu-latest.services.postgresdựng một container Postgres tạm cho job — chính là cách chạy integration test với DB thật mà không cần cài đặt gì sẵn.- Mỗi mục trong
stepslà một step.usesgọi một action có sẵn (nhưcheckout),runchạy lệnh shell. - Step cuối lưu file coverage thành artifact để xem lại hoặc đẩy sang công cụ khác.
Build & push Docker image
Sau khi test xanh, ta build image và đẩy lên registry. Job này dùng needs: test để chỉ chạy khi job test thành công, và if để chỉ build trên nhánh main.
build-and-push:
name: Build & Push Docker Image
runs-on: ubuntu-latest
needs: test # chỉ chạy sau khi 'test' xanh
if: github.ref == 'refs/heads/main' # chỉ build trên main
permissions:
contents: read
packages: write # quyền push lên GHCR
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} # secret tự cấp bởi GitHub
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Lưu ý hai tag: latest (luôn trỏ tới bản mới nhất) và ${{ github.sha }} (tag bất biến theo commit). Tag theo SHA rất quan trọng cho rollback — bạn luôn deploy lại được đúng image của một commit cụ thể.
Biến môi trường & secrets
Pipeline cần dữ liệu cấu hình: URL database, API key, mật khẩu registry. Có hai loại, phải phân biệt rõ:
- Biến môi trường thường (
env): giá trị không nhạy cảm, có thể đọc trong log. Ví dụNODE_ENV: production,LOG_LEVEL: info. - Secrets: giá trị nhạy cảm — token, mật khẩu, private key. Không bao giờ hardcode vào file YAML hay commit vào git.
Trong GitHub Actions, secrets được lưu ở Settings → Secrets and variables → Actions của repo (hoặc cấp organization/environment), và truy cập qua ${{ secrets.TÊN_SECRET }}. GitHub tự động che (mask) giá trị secret trong log nếu nó vô tình bị in ra.
- name: Deploy
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }}
run: ./scripts/deploy.sh
Quy tắc an toàn:
- Không in secret ra log. Tránh
echo $DB_PASSWORD. - Phạm vi tối thiểu. Dùng GitHub Environments để giới hạn secret production chỉ cho job deploy production.
- Xoay vòng định kỳ. Coi mọi secret từng lộ là đã bị xâm phạm; rotate ngay.
permissionstối thiểu choGITHUB_TOKEN. Chỉ cấp đúng quyền job cần (như ví dụ trên chỉ cấppackages: write).
Chiến lược triển khai
Khi đã có image sẵn sàng, làm sao đưa nó lên production mà không gây downtime hay rủi ro? Có ba chiến lược phổ biến.
Rolling deployment
Thay thế dần các instance cũ bằng instance mới, từng nhóm nhỏ một. Trong khi instance mới khởi động, instance cũ vẫn phục vụ. Đây là mặc định của Kubernetes (RollingUpdate).
- Ưu: không downtime, không cần gấp đôi tài nguyên.
- Nhược: trong lúc rollout, cả phiên bản cũ và mới cùng chạy → phải tương thích ngược (đặc biệt với schema DB). Rollback chậm hơn.
Blue-Green deployment
Duy trì hai môi trường giống hệt nhau: Blue (đang phục vụ) và Green (phiên bản mới). Deploy lên Green, test kỹ, rồi chuyển toàn bộ traffic từ Blue sang Green chỉ bằng việc đổi load balancer.
- Ưu: chuyển đổi tức thì, rollback chỉ là trỏ lại Blue → cực nhanh.
- Nhược: cần gấp đôi tài nguyên trong lúc chuyển; toàn bộ traffic chuyển một lần nên nếu có lỗi thì 100% user dính.
Canary deployment
Đẩy phiên bản mới cho một tỷ lệ nhỏ traffic trước (ví dụ 5%), theo dõi metrics (lỗi, latency). Nếu ổn, tăng dần lên 25%, 50%, 100%. Nếu xấu, cắt ngay phần canary.
- Ưu: giới hạn ảnh hưởng của lỗi xuống nhóm nhỏ user; phát hiện vấn đề thực tế trước khi rollout toàn bộ.
- Nhược: cần hạ tầng định tuyến traffic theo tỷ lệ và giám sát mạnh; phức tạp nhất.
| Chiến lược | Downtime | Tài nguyên cần | Tốc độ rollback | Mức rủi ro | Độ phức tạp |
|---|---|---|---|---|---|
| Rolling | Không | ~1x | Chậm (rollout ngược) | Trung bình | Thấp |
| Blue-Green | Không | ~2x | Tức thì (đổi LB) | Cao (all-or-nothing) | Trung bình |
| Canary | Không | 1x + chút dư | Nhanh (cắt canary) | Thấp nhất | Cao |
Rollback
Rollback là khả năng quay về phiên bản tốt đã biết khi bản mới gặp sự cố. Đây là phần bị xem nhẹ nhiều nhất nhưng quan trọng nhất.
Nguyên tắc cốt lõi: vì ta tag image theo commit SHA (image bất biến), rollback chỉ đơn giản là deploy lại tag cũ:
# Lăn ngược về image của commit trước
docker compose pull app
docker compose up -d --no-deps app # với tag = SHA cũ trong .env
# Hoặc trên Kubernetes
kubectl rollout undo deployment/app
Cạm bẫy lớn nhất là database migration không tương thích ngược. Nếu bản mới đổi schema theo cách phá vỡ code cũ, rollback code thôi là chưa đủ. Giải pháp: thiết kế migration theo kiểu expand/contract — thêm cột mới trước (expand), deploy code dùng cột mới, rồi mới xoá cột cũ ở release sau (contract), để mọi bước đều tương thích cả hai chiều.
Pipeline as Code
Toàn bộ định nghĩa pipeline sống trong repo dưới dạng file (YAML), được version control cùng source code. Lợi ích:
- Lịch sử & review: thay đổi pipeline đi qua pull request, được review như mọi code khác.
- Tái lập: checkout commit cũ là có đúng pipeline tại thời điểm đó.
- Không "click-ops": không còn cấu hình thủ công trên UI mà không ai biết.
Đây là một biểu hiện của tư duy Infrastructure as Code, áp dụng cho chính quy trình build/deploy.
GitLab CI — một lựa chọn tương đương
GitLab CI dùng file .gitlab-ci.yml ở gốc repo, với khái niệm tương đương: stages (giai đoạn), mỗi job khai báo thuộc stage nào và có script. So với GitHub Actions, GitLab CI có khái niệm stages tường minh hơn:
# .gitlab-ci.yml
stages:
- test
- build
run-tests:
stage: test
image: python:3.12
script:
- pip install -r requirements.txt
- pytest
build-image:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- main
Khái niệm cốt lõi giống hệt — chỉ khác cú pháp. Học một công cụ giúp bạn nhanh chóng nắm bắt công cụ kia.
Tóm tắt
CI/CD biến chuỗi thao tác build–test–deploy thủ công, dễ sai, thành một pipeline tự động và lặp lại được. CI đảm bảo code luôn integrate và test xanh; Continuous Delivery đóng gói sẵn sàng release nhưng cần người duyệt để lên production; Continuous Deployment tự động hoá luôn cả bước đó. Một pipeline gồm trigger, stage, job, step và artifact, với nhiều lớp kiểm thử xếp theo chi phí (lint → unit → integration → security scan). GitHub Actions định nghĩa pipeline bằng YAML trong .github/workflows/, dùng on cho trigger, jobs/steps cho công việc, needs cho phụ thuộc, và secrets cho dữ liệu nhạy cảm. Khi triển khai, chọn giữa rolling (đơn giản, không downtime), blue-green (rollback tức thì, tốn gấp đôi tài nguyên) và canary (rủi ro thấp nhất nhưng phức tạp nhất); và luôn chuẩn bị đường rollback dựa trên image tag bất biến cùng migration tương thích ngược.
Tự kiểm tra
- Phân biệt Continuous Delivery và Continuous Deployment — điểm khác biệt then chốt nằm ở bước nào?
- Trong GitHub Actions, làm sao để một job chỉ chạy sau khi job khác thành công, và chỉ chạy trên nhánh
main? - Vì sao nên đặt lint và unit test trước integration test trong pipeline?
- Khác biệt giữa biến môi trường thường (
env) và secrets là gì, và ba quy tắc an toàn khi dùng secrets? - So sánh blue-green và canary về tốc độ rollback và mức rủi ro khi bản mới có lỗi.
- Vì sao tag Docker image theo commit SHA lại quan trọng cho rollback, và migration kiểu expand/contract giải quyết vấn đề gì?
Đọc tiếp
Bài viết liên quan
Container vs máy ảo, image/layer, Dockerfile, volume, network, Docker Compose và best practices.
Vì sao cần orchestration; Pod, Deployment, Service, Ingress; scaling, self-healing và cấu hình.
Ba trụ cột logging–metrics–tracing, Prometheus/Grafana, alerting, SLI/SLO/SLA và golden signals.
Bản chất DevOps: phá bỏ rào cản Dev–Ops, vòng lặp vô tận, CALMS, và vì sao tự động hoá.