Tuần 12: CI/CD Pipeline
“Nếu deploying gây cho bạn stress, bạn đang deploy chưa đủ thường xuyên. Nếu mỗi ngày deploy 50 lần, mỗi lần chỉ là một thay đổi nhỏ — và rollback cũng chỉ là một click.”
Tags: system-design cicd devops security gitops Student: Hieu Prerequisite: Tuan-11-Microservices-Pattern Liên quan: Tuan-13-Monitoring-Observability · Tuan-14-AuthN-AuthZ-Security · Tuan-15-Data-Security-Encryption · Tuan-01-Scale-From-Zero-To-Millions
1. Context & Why
Analogy đời thường — Dây chuyền sản xuất ô tô
Hieu, tưởng tượng em đứng trong nhà máy Toyota. Một chiếc xe đi qua dây chuyền sản xuất (assembly line / dây chuyền lắp ráp):
- Nguyên liệu thô (raw materials) được đưa vào → tương đương source code từ developer
- Trạm hàn khung → tương đương build stage — biên dịch code thành artifact
- Trạm kiểm tra khung → tương đương unit test — kiểm tra từng component riêng lẻ
- Trạm lắp động cơ + hộp số → tương đương integration test — ghép các module lại
- Trạm kiểm tra chất lượng QC → tương đương security scan + code quality gate
- Trạm sơn + hoàn thiện → tương đương packaging (Docker image, artifact)
- Trạm test lái thử → tương đương staging environment test
- Xuất xưởng giao cho đại lý → tương đương deploy to production
Điểm mấu chốt của Toyota Production System:
- Mỗi trạm có kiểm tra chất lượng — lỗi phát hiện sớm thì chi phí sửa rẻ
- Dây chuyền tự dừng khi phát hiện lỗi (Andon cord / dây kéo Andon) — tương đương pipeline fail = block deploy
- Mỗi xe ra khỏi dây chuyền đều đạt chuẩn — tương đương every commit that passes pipeline is deployable
- Tốc độ dây chuyền ổn định — tương đương predictable deployment cadence
Nếu Toyota không có dây chuyền tự động mà mỗi xe được một thợ thủ công lắp tay từ A-Z → chậm, không nhất quán, lỗi nhiều. Đó chính là tình trạng deploy thủ công (manual deployment / triển khai bằng tay) mà nhiều team vẫn đang làm.
Tại sao CI/CD quan trọng trong System Design?
Trong interview, khi thiết kế hệ thống quy mô lớn, interviewer sẽ hỏi:
“How do you deploy changes to 10,000 servers without downtime?”
Câu trả lời không phải “SSH vào từng server rồi git pull”. Câu trả lời là CI/CD pipeline với deployment strategy phù hợp.
DORA Metrics (DevOps Research and Assessment) — 4 chỉ số đánh giá hiệu quả delivery:
| Metric | Elite | High | Medium | Low |
|---|---|---|---|---|
| Deployment Frequency (tần suất deploy) | On-demand (nhiều lần/ngày) | 1 lần/tuần – 1 lần/tháng | 1 lần/tháng – 1 lần/6 tháng | < 1 lần/6 tháng |
| Lead Time for Changes (thời gian từ commit → production) | < 1 giờ | 1 ngày – 1 tuần | 1 tháng – 6 tháng | > 6 tháng |
| Change Failure Rate (tỉ lệ deploy gây lỗi) | 0–15% | 16–30% | 16–30% | 46–60% |
| MTTR (thời gian khôi phục) | < 1 giờ | < 1 ngày | 1 ngày – 1 tuần | > 6 tháng |
Aha Moment: Team elite deploy hàng trăm lần/ngày nhưng failure rate lại thấp hơn team deploy 1 lần/tháng. Tại sao? Vì mỗi lần deploy thay đổi ít → dễ kiểm tra → dễ rollback.
2. Deep Dive — Các khái niệm cốt lõi
2.1 CI vs CD vs CD — Ba khái niệm, hai chữ viết tắt
CI ─────────── CD (Delivery) ─────────── CD (Deployment)
Continuous Continuous Continuous
Integration Delivery Deployment
Developer Artifact sẵn sàng Tự động deploy
merge code deploy bất kỳ lúc nào lên production
vào main (cần approval thủ công) (không cần approval)
| Khái niệm | Tiếng Việt | Mô tả | Ví dụ |
|---|---|---|---|
| Continuous Integration (CI) | Tích hợp liên tục | Developer merge code vào shared branch thường xuyên (nhiều lần/ngày). Mỗi merge trigger automated build + test | Mỗi PR tự chạy lint, unit test, integration test |
| Continuous Delivery (CD) | Phân phối liên tục | Code luôn ở trạng thái deployable. Deploy lên production cần manual approval (one-click deploy) | Artifact ready, nhấn nút “Deploy to Prod” |
| Continuous Deployment (CD) | Triển khai liên tục | Mọi commit pass pipeline đều tự động lên production. Không cần approval | GitHub merge → production trong 15 phút |
Quan trọng: Hầu hết công ty dùng Continuous Delivery (cần approval) chứ không phải Continuous Deployment. Ngay cả Netflix, Google cũng có gate trước production cho critical services.
2.2 GitOps Workflow
GitOps = Git là single source of truth (nguồn sự thật duy nhất) cho cả application code lẫn infrastructure.
Nguyên tắc GitOps:
- Declarative (khai báo): Toàn bộ hệ thống được mô tả dưới dạng code trong Git
- Versioned & Immutable (có phiên bản & bất biến): Git history = audit log tự nhiên
- Pulled Automatically (tự động kéo): Agent trong cluster tự pull thay đổi từ Git
- Continuously Reconciled (liên tục đồng bộ): Agent đảm bảo actual state = desired state
┌─────────────┐ push ┌──────────┐ detect ┌───────────┐
│ Developer │ ─────────> │ Git │ <─────────── │ ArgoCD │
│ (commit) │ │ Repo │ (watch) │ (agent) │
└─────────────┘ └──────────┘ └─────┬─────┘
│
sync │ (pull)
│
┌──────▼──────┐
│ Kubernetes │
│ Cluster │
└─────────────┘
Push-based (truyền thống: Jenkins push lên server) vs Pull-based (GitOps: ArgoCD pull từ Git):
| Tiêu chí | Push-based (Jenkins) | Pull-based (GitOps/ArgoCD) |
|---|---|---|
| Credentials | CI server cần SSH/kubectl access vào cluster | Agent trong cluster tự pull, không cần expose cluster |
| Security | CI server bị hack → toàn bộ cluster bị compromise | Agent chỉ pull từ Git, attack surface nhỏ hơn |
| Drift detection (phát hiện trôi cấu hình) | Không có | Agent liên tục so sánh actual vs desired |
| Audit | Phải check CI logs | Git history = audit trail tự nhiên |
2.3 Branching Strategies — Chiến lược phân nhánh
GitFlow
main ─────●────────────────────●──────────────── (production)
│ ▲
release ──────┼──────●────────────●│───── (release/1.2)
│ ▲ ││
develop ─────●┼──●───┼──●───●────●│───── (integration)
││ │ │ ▲ ▲ │
feature ─────┼┼──● │ │ │ │ (feature/login)
││ │ │ │ │
feature ─────┼┼──────●──● │ │ (feature/payment)
││ │ │
hotfix ─────┼┼─────────────●─────● (hotfix/fix-crash)
| Branch | Mục đích | Lifetime |
|---|---|---|
main | Production code, mỗi commit là một release | Vĩnh viễn |
develop | Integration branch, merge feature vào đây | Vĩnh viễn |
feature/* | Phát triển tính năng mới | Ngắn (vài ngày) |
release/* | Chuẩn bị release, bug fix cuối | Ngắn (1-2 tuần) |
hotfix/* | Fix bug production khẩn cấp | Rất ngắn (giờ) |
Ưu điểm: Rõ ràng, phù hợp team lớn, release theo lịch (scheduled release). Nhược điểm: Phức tạp, merge conflict nhiều, không phù hợp continuous deployment.
Trunk-Based Development (Phát triển trên nhánh chính)
main/trunk ──●──●──●──●──●──●──●──●──●──●──●── (production)
│ │ │ │
short-lived ●──● ●──● ● ●──● (< 1 ngày)
feature
branches
Nguyên tắc:
- Feature branch sống tối đa 1-2 ngày
- Merge vào
mainthường xuyên (nhiều lần/ngày) - Dùng Feature Flags (cờ tính năng) để ẩn feature chưa hoàn thiện
mainluôn deployable
Ưu điểm: Đơn giản, ít merge conflict, phù hợp continuous deployment, Google và Meta dùng. Nhược điểm: Đòi hỏi test coverage cao, cần feature flags, cần team discipline.
Khi nào dùng gì?
- GitFlow: Team lớn, release theo sprint/cycle, product có versioning (mobile app, SDK)
- Trunk-based: Team muốn deploy liên tục, web app, microservices
2.4 Deployment Strategies — Chiến lược triển khai
Blue-Green Deployment (Triển khai xanh-lục)
┌─────────────┐
│ Load │
Users ────> │ Balancer │
└──────┬──────┘
│
┌───────────┼───────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐
│ BLUE │ │ GREEN │
│ (v1) │ │ (v2) │
│ ACTIVE │ │ IDLE │
└─────────────┘ └─────────────┘
Step 1: Deploy v2 lên GREEN (users vẫn dùng BLUE)
Step 2: Test GREEN
Step 3: Switch traffic: LB → GREEN
Step 4: BLUE trở thành idle (sẵn sàng rollback)
| Ưu điểm | Nhược điểm |
|---|---|
| Zero downtime (không có thời gian chết) | Cần gấp đôi infrastructure |
| Rollback tức thì (switch lại BLUE) | Database migration phức tạp (cả hai version phải compatible) |
| Test production-like environment trước khi switch | Chi phí cao |
Canary Deployment (Triển khai kiểu chim hoàng yến)
Tên gọi từ thợ mỏ ngày xưa mang chim hoàng yến (canary) vào hầm mỏ — nếu chim chết nghĩa là có khí độc.
Phase 1: [████████████████████████████████████████████] v1 (100%)
[█ ] v2 (2%)
Phase 2: [████████████████████████████████ ] v1 (75%)
[██████████ ] v2 (25%)
Phase 3: [████████████████ ] v1 (50%)
[████████████████████ ] v2 (50%)
Phase 4: [ ] v1 (0%)
[████████████████████████████████████████████] v2 (100%)
| Ưu điểm | Nhược điểm |
|---|---|
| Giảm blast radius (phạm vi ảnh hưởng) — lỗi chỉ affect 2% users | Phức tạp hơn blue-green |
| Cho phép monitor metrics trước khi tăng traffic | Cần infrastructure hỗ trợ traffic splitting |
| Tự động rollback nếu error rate tăng | Hai version chạy song song → DB compatibility |
Rolling Deployment (Triển khai tuần tự)
Servers: [A] [B] [C] [D] [E]
Step 1: [v2] [v1] [v1] [v1] [v1] ← Update A
Step 2: [v2] [v2] [v1] [v1] [v1] ← Update B
Step 3: [v2] [v2] [v2] [v1] [v1] ← Update C
Step 4: [v2] [v2] [v2] [v2] [v1] ← Update D
Step 5: [v2] [v2] [v2] [v2] [v2] ← Update E ✓
| Ưu điểm | Nhược điểm |
|---|---|
| Không cần thêm infrastructure | Hai version chạy đồng thời trong quá trình deploy |
| Kubernetes mặc định dùng strategy này | Rollback chậm (phải rolling lại) |
Cấu hình maxSurge + maxUnavailable | Không thể test version mới trước khi users thấy |
A/B Testing Deployment (Triển khai thử nghiệm A/B)
┌─────────────┐
Users ────> │ Router │
│ (rules) │
└──────┬──────┘
│
┌───────────┼───────────┐
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Version A │ │ Version B │
│ (control) │ │ (experiment)│
│ US users │ │ EU users │
└─────────────┘ └─────────────┘
Khác biệt với Canary: A/B testing route dựa trên user attributes (country, device, user tier), trong khi Canary route random percentage.
Mục đích: Đo business metrics (conversion rate, revenue), không chỉ technical metrics.
2.5 Feature Flags (Cờ tính năng)
Feature flags cho phép tách deployment khỏi release. Code được deploy lên production nhưng feature ẩn sau flag — bật/tắt mà không cần redeploy.
# Không có feature flag
def checkout():
process_payment_v1() # Muốn đổi sang v2 phải deploy lại
# Có feature flag
def checkout():
if feature_flags.is_enabled("new_payment_flow", user=current_user):
process_payment_v2() # Bật cho 5% users, rồi 20%, rồi 100%
else:
process_payment_v1()Công cụ phổ biến:
| Tool | Loại | Đặc điểm |
|---|---|---|
| LaunchDarkly | SaaS (dịch vụ đám mây) | Enterprise, real-time updates, targeting rules phức tạp |
| Unleash | Self-hosted / Cloud | Open-source, đủ dùng cho hầu hết use case |
| Flagsmith | Self-hosted / Cloud | Open-source, hỗ trợ remote config |
| ConfigCat | SaaS | Nhẹ, pricing theo feature flag count |
Các loại feature flag:
| Loại | Mục đích | Lifecycle |
|---|---|---|
| Release flag (cờ phát hành) | Ẩn feature chưa hoàn thiện | Ngắn (xoá sau khi feature stable) |
| Experiment flag (cờ thử nghiệm) | A/B testing | Trung bình (xoá sau khi có kết quả) |
| Ops flag (cờ vận hành) | Circuit breaker, kill switch | Dài hạn |
| Permission flag (cờ phân quyền) | Premium feature, beta access | Dài hạn |
Pitfall: Feature flag debt (nợ kỹ thuật cờ tính năng) — flag cũ không dọn sẽ chồng chất. Quy tắc: mỗi flag phải có expiry date và owner.
2.6 Infrastructure as Code (IaC) — Hạ tầng dưới dạng mã
IaC = Quản lý infrastructure bằng code thay vì click trên console.
| Tool | Ngôn ngữ | Đặc điểm |
|---|---|---|
| Terraform (HashiCorp) | HCL (HashiCorp Configuration Language) | Multi-cloud, declarative, state management |
| Pulumi | TypeScript/Python/Go/C# | Dùng ngôn ngữ lập trình thật, không cần học DSL |
| AWS CloudFormation | YAML/JSON | AWS-only, native integration |
| Ansible | YAML | Configuration management, procedural |
Declarative (khai báo) vs Imperative (mệnh lệnh):
# Terraform (Declarative) — Mô tả trạng thái mong muốn
# "Tôi muốn 3 servers"
resource "aws_instance" "web" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
}
# Nếu hiện tại có 2 servers → Terraform tạo thêm 1
# Nếu hiện tại có 5 servers → Terraform xoá 2# Imperative — Mô tả các bước thực hiện
# "Tạo 3 servers"
for i in 1 2 3; do
aws ec2 run-instances --image-id ami-xxx --instance-type t3.medium
done
# Nếu chạy lại → tạo thêm 3 servers nữa! (tổng 6)2.7 Immutable Infrastructure (Hạ tầng bất biến)
Mutable (truyền thống): Server được SSH vào, cài đặt, patch, update → dần dần mỗi server khác nhau (configuration drift / trôi cấu hình).
Immutable: Không bao giờ sửa server đang chạy. Muốn thay đổi → tạo server mới từ image mới → xoá server cũ.
Mutable: Server A ──[patch]──> Server A' ──[hotfix]──> Server A''
(mỗi server trở nên unique, khó reproduce)
Immutable: Image v1 → Server A (xoá)
Image v2 → Server B (xoá)
Image v3 → Server C (đang chạy)
(mỗi server identical với image gốc)
Analogy: Mutable = sửa xe đang chạy trên đường. Immutable = thay xe mới hoàn toàn, bỏ xe cũ.
2.8 Container Registry & Artifact Management
Container Registry (kho chứa container image):
| Registry | Loại | Đặc điểm |
|---|---|---|
| Docker Hub | Public/Private | Phổ biến nhất, rate limit cho free tier |
| Amazon ECR | Private | Tích hợp AWS, image scanning |
| Google Artifact Registry | Private | Tích hợp GCP, hỗ trợ nhiều format |
| GitHub Container Registry (ghcr.io) | Public/Private | Tích hợp GitHub Actions |
| Harbor | Self-hosted | Open-source, enterprise features, vulnerability scanning |
Artifact Management — không chỉ container images:
| Artifact | Tool | Ví dụ |
|---|---|---|
| Docker images | ECR, Harbor | myapp:v1.2.3 |
| npm packages | npm registry, Artifactory | @company/shared-utils@1.0.0 |
| JAR/WAR files | Nexus, Artifactory | payment-service-1.2.3.jar |
| Helm charts | ChartMuseum, OCI registry | myapp-chart-1.2.3.tgz |
| Terraform modules | Terraform Registry | modules/vpc/v1.2.0 |
Tagging strategy (chiến lược đánh tag):
- Semantic versioning:
v1.2.3(major.minor.patch) - Git SHA:
abc1234(traceability hoàn hảo) - Kết hợp:
v1.2.3-abc1234(best practice) - NEVER dùng
latesttrong production — không biết version nào đang chạy
2.9 Rollback Strategies (Chiến lược hoàn tác)
| Strategy | Tốc độ | Phức tạp | Khi nào dùng |
|---|---|---|---|
| Revert deploy (deploy lại version cũ) | Nhanh (phút) | Thấp | Lỗi đơn giản, không liên quan DB |
| Blue-Green switch back | Tức thì (giây) | Trung bình | Khi dùng Blue-Green deployment |
| Feature flag off | Tức thì (giây) | Thấp | Feature mới có flag |
| Database rollback | Chậm (giờ) | Rất cao | Tránh bằng mọi giá — dùng backward-compatible migration |
| Git revert + redeploy | Trung bình (10-30 phút) | Thấp | Khi cần audit trail rõ ràng |
Golden Rule: Mọi deployment phải có rollback plan trước khi deploy. Nếu không thể rollback → không nên deploy.
2.10 Database Migrations trong CI/CD
Database migration (di chuyển cơ sở dữ liệu) là phần nguy hiểm nhất của CI/CD vì data không thể rollback dễ dàng.
Expand-Contract Pattern (Mở rộng — Thu gọn):
Phase 1 — EXPAND (backward compatible):
- Thêm column mới, KHÔNG xoá column cũ
- Code mới ghi vào CẢ HAI column
- Deploy code v2 (ghi cả old + new column)
Phase 2 — MIGRATE:
- Backfill data từ old column → new column
- Verify data consistency
Phase 3 — CONTRACT:
- Deploy code v3 (chỉ đọc/ghi new column)
- Xoá old column
-- Phase 1: Expand — thêm column mới
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
-- Code v2: ghi cả first_name + full_name
-- Phase 2: Backfill
UPDATE users SET full_name = first_name || ' ' || last_name
WHERE full_name IS NULL;
-- Phase 3: Contract — xoá column cũ (SAU KHI v3 stable)
ALTER TABLE users DROP COLUMN first_name;
ALTER TABLE users DROP COLUMN last_name;NEVER: Chạy destructive migration (DROP column, DROP table) trong cùng deploy với code change. Luôn tách ra ít nhất 2 deploys.
Tools cho DB migration:
| Tool | Ngôn ngữ | Đặc điểm |
|---|---|---|
| Flyway | Java/SQL | Version-based, SQL scripts |
| Liquibase | Java/XML/SQL | Changeset-based, rollback support |
| Alembic | Python (SQLAlchemy) | Auto-generate migrations |
| golang-migrate | Go | Lightweight, CLI + library |
| Prisma Migrate | TypeScript | Schema-first, type-safe |
3. Estimation — Pipeline Performance & Cost
3.1 Deployment Frequency ảnh hưởng đến MTTR
MTTR (Mean Time To Recovery / Thời gian trung bình để khôi phục):
Với manual deployment (1 lần/tháng):
Với CI/CD pipeline (deploy liên tục):
Cải thiện: MTTR giảm 4x nhờ automated deploy + monitoring + smaller changes.
Impact lên Availability:
Giả sử 12 incidents/năm:
Từ two 9s lên gần three 9s chỉ nhờ CI/CD.
3.2 Build Time Optimization
Giả sử team 50 developers, mỗi người mở 3 PRs/ngày:
Nếu mỗi build mất 20 phút:
Nếu optimize build xuống 5 phút:
Aha Moment: Giảm build time 15 phút có thể tiết kiệm ~$60K/tháng cho team 50 người. Build time optimization có ROI cực cao.
3.3 Pipeline Cost Estimation
GitHub Actions pricing (ví dụ):
| Runner | Cost/min | Specs |
|---|---|---|
| Linux (standard) | $0.008 | 2 vCPU, 7GB RAM |
| Linux (large) | $0.016 | 4 vCPU, 16GB RAM |
| macOS | $0.08 | 3 vCPU, 14GB RAM |
Ước lượng chi phí monthly:
Với 150 builds/day, mỗi build 15 phút, Linux standard runner:
Thêm self-hosted runner cho heavy builds (Docker build, E2E tests):
Tổng CI/CD infrastructure cost:
| Component | Monthly Cost |
|---|---|
| GitHub Actions (cloud runners) | $540 |
| Self-hosted runners (2x EC2) | $490 |
| Container Registry (ECR, 50GB) | $5 |
| Artifact Storage (S3, 100GB) | $2.30 |
| ArgoCD (runs in cluster) | $0 (included in K8s cost) |
| Total | ~$1,037/month |
4. Security First — Supply Chain & Pipeline Security
4.1 Supply Chain Security (Bảo mật chuỗi cung ứng phần mềm)
SolarWinds attack (2020) cho thấy: attacker không cần hack production server — chỉ cần chèn mã độc vào build pipeline → malicious code được deploy tự động lên 18,000 tổ chức.
SBOM (Software Bill of Materials / Bảng kê vật liệu phần mềm)
SBOM liệt kê tất cả dependencies (thư viện phụ thuộc) trong application, bao gồm transitive dependencies (phụ thuộc gián tiếp).
{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"components": [
{
"type": "library",
"name": "express",
"version": "4.18.2",
"purl": "pkg:npm/express@4.18.2",
"hashes": [
{
"alg": "SHA-256",
"content": "a1b2c3d4..."
}
]
},
{
"type": "library",
"name": "lodash",
"version": "4.17.21",
"purl": "pkg:npm/lodash@4.17.21"
}
]
}Tools tạo SBOM:
- Syft (Anchore) — generate SBOM từ container image hoặc source code
- CycloneDX — standard format, nhiều tool hỗ trợ
- SPDX — Linux Foundation standard
Dependency Scanning (Quét thư viện phụ thuộc)
| Tool | Đặc điểm |
|---|---|
| Dependabot (GitHub) | Tự động mở PR update vulnerable dependency |
| Snyk | SaaS, hỗ trợ nhiều ngôn ngữ, license scanning |
| OWASP Dependency-Check | Open-source, NVD database |
| Renovate | Open-source, tự động update dependencies |
4.2 Container Image Scanning (Quét lỗ hổng container image)
Trivy (Aqua Security) — open-source, quét OS packages + application dependencies:
# Quét image trước khi push lên registry
trivy image --severity HIGH,CRITICAL myapp:v1.2.3
# Output:
# myapp:v1.2.3 (alpine 3.18.4)
# ════════════════════════════════════
# Library Vulnerability Severity Fixed Version
# ────────────── ────────────── ──────── ──────────────
# libcrypto3 CVE-2024-0727 HIGH 3.1.4-r4
# libssl3 CVE-2024-0727 HIGH 3.1.4-r4
#
# Node.js (package.json)
# ════════════════════════════════════
# Library Vulnerability Severity Fixed Version
# ────────────── ────────────── ──────── ──────────────
# express CVE-2024-xxxx CRITICAL 4.18.3Policy: Pipeline phải fail nếu phát hiện vulnerability CRITICAL hoặc HIGH chưa có fix.
4.3 Secrets trong CI/CD — NEVER in Code
Nguyên tắc #1: Không bao giờ commit secrets (API key, password, token) vào Git.
Nơi lưu secrets:
| Giải pháp | Khi nào dùng |
|---|---|
| GitHub Actions Secrets | CI/CD variables, encrypted at rest |
| HashiCorp Vault | Enterprise, dynamic secrets, rotation |
| AWS Secrets Manager | AWS-native, automatic rotation |
| Azure Key Vault | Azure-native |
| SOPS (Mozilla) | Encrypt secrets file trong Git, decrypt lúc deploy |
Nguyên tắc #2: Secrets phải masked trong logs (che trong nhật ký). CI/CD tools tự mask secrets đã register, nhưng nếu secret leak qua echo hoặc error message → vẫn bị lộ.
# ĐÚNG: Dùng secrets reference
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
# SAI: Hardcode secret
env:
DB_PASSWORD: "MyP@ssw0rd123" # ← BỊ LỘ TRONG GIT HISTORY VĨNH VIỄNPitfall: Xoá secret khỏi code rồi commit lại KHÔNG đủ. Secret vẫn còn trong Git history. Phải dùng
git filter-branchhoặc BFG Repo-Cleaner VÀ rotate secret ngay lập tức.
4.4 Signed Commits (Commit có chữ ký)
Signed commits đảm bảo commit thực sự đến từ developer đã claim, không phải attacker giả mạo.
# Cấu hình GPG signing
git config --global commit.gpgsign true
git config --global user.signingkey <GPG_KEY_ID>
# Commit sẽ tự động được sign
git commit -m "feat: add payment flow"
# Verify commit
git log --show-signature
# gpg: Signature made Mon 18 Mar 2026 10:00:00 AM
# gpg: Good signature from "Hieu Nguyen <hieu@example.com>"GitHub: Bật “Require signed commits” trong branch protection rules → chỉ accept verified commits.
4.5 SAST/DAST trong Pipeline
| Loại | Tên đầy đủ | Thời điểm | Phát hiện gì |
|---|---|---|---|
| SAST | Static Application Security Testing (Kiểm tra bảo mật tĩnh) | Build time — quét source code | SQL injection, XSS, hardcoded secrets, insecure patterns |
| DAST | Dynamic Application Security Testing (Kiểm tra bảo mật động) | Runtime — quét app đang chạy | Runtime vulnerabilities, misconfigurations, auth bypass |
| SCA | Software Composition Analysis (Phân tích thành phần phần mềm) | Build time — quét dependencies | Known CVEs trong third-party libraries |
Tools phổ biến:
| Tool | Loại | Đặc điểm |
|---|---|---|
| SonarQube | SAST | Multi-language, quality gate, technical debt tracking |
| Semgrep | SAST | Lightweight, custom rules, fast |
| CodeQL (GitHub) | SAST | Semantic analysis, free cho public repos |
| OWASP ZAP | DAST | Open-source, API scanning |
| Snyk Code | SAST | Real-time, IDE integration |
4.6 Policy as Code (OPA — Open Policy Agent)
OPA (Open Policy Agent / Tác nhân chính sách mở) cho phép viết security policies dưới dạng code và enforce trong pipeline.
# policy/deployment.rego
package kubernetes.admission
# Deny containers running as root
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not container.securityContext.runAsNonRoot
msg := sprintf("Container '%v' must not run as root", [container.name])
}
# Deny images without specific tag (no 'latest')
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
endswith(container.image, ":latest")
msg := sprintf("Container '%v' uses 'latest' tag — must use specific version", [container.name])
}
# Require resource limits
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not container.resources.limits
msg := sprintf("Container '%v' must have resource limits", [container.name])
}5. DevOps — Tooling & Practices
5.1 GitHub Actions Workflow
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# ==================== LINT ====================
lint:
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 run type-check # TypeScript check
# ==================== TEST ====================
test:
runs-on: ubuntu-latest
needs: lint
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: test_db
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run test:unit -- --coverage
- run: npm run test:integration
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test_db
REDIS_URL: redis://localhost:6379
- uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
# ==================== BUILD ====================
build:
runs-on: ubuntu-latest
needs: test
permissions:
contents: read
packages: write
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=
type=semver,pattern={{version}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name == 'push' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ==================== SECURITY SCAN ====================
security:
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
- name: Run Semgrep SAST
uses: semgrep/semgrep-action@v1
with:
config: >-
p/security-audit
p/secrets
p/owasp-top-ten
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: cyclonedx-json
output-file: sbom.json
# ==================== DEPLOY STAGING ====================
deploy-staging:
runs-on: ubuntu-latest
needs: [build, security]
if: github.event_name == 'push'
environment: staging
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
run: |
# Update image tag in GitOps repo
cd k8s/overlays/staging
kustomize edit set image \
myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: Commit and push to GitOps repo
run: |
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git add .
git commit -m "deploy: staging ${{ github.sha }}"
git push
- name: Wait for ArgoCD sync
run: |
argocd app wait myapp-staging --timeout 300
- name: Run smoke tests
run: |
npm run test:smoke -- --base-url=https://staging.myapp.com
# ==================== DEPLOY PRODUCTION ====================
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging
if: github.event_name == 'push'
environment:
name: production
url: https://myapp.com
steps:
- uses: actions/checkout@v4
- name: Deploy to production (canary)
run: |
cd k8s/overlays/production
kustomize edit set image \
myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: Commit and push to GitOps repo
run: |
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git add .
git commit -m "deploy: production ${{ github.sha }}"
git push
- name: Monitor canary metrics
run: |
# Check error rate for 10 minutes
sleep 600
ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query?query=rate(http_requests_total{status=~'5..'}[5m])")
echo "Error rate: $ERROR_RATE"5.2 Jenkins Pipeline (Declarative)
// Jenkinsfile
pipeline {
agent {
kubernetes {
yaml '''
spec:
containers:
- name: node
image: node:20-alpine
command: ['sleep', '99d']
- name: docker
image: docker:24-dind
securityContext:
privileged: true
- name: trivy
image: aquasec/trivy:latest
command: ['sleep', '99d']
'''
}
}
environment {
REGISTRY = 'ecr.aws/mycompany'
IMAGE_NAME = 'myapp'
IMAGE_TAG = "${env.GIT_COMMIT?.take(7)}"
}
stages {
stage('Lint & Type Check') {
steps {
container('node') {
sh 'npm ci'
sh 'npm run lint'
sh 'npm run type-check'
}
}
}
stage('Test') {
parallel {
stage('Unit Tests') {
steps {
container('node') {
sh 'npm run test:unit -- --coverage'
}
}
post {
always {
publishHTML(target: [
reportDir: 'coverage/lcov-report',
reportFiles: 'index.html',
reportName: 'Coverage Report'
])
}
}
}
stage('Integration Tests') {
steps {
container('node') {
sh 'npm run test:integration'
}
}
}
}
}
stage('Build & Push Image') {
steps {
container('docker') {
sh """
docker build -t ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} .
docker push ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}
"""
}
}
}
stage('Security Scan') {
parallel {
stage('Trivy Scan') {
steps {
container('trivy') {
sh """
trivy image --severity HIGH,CRITICAL \
--exit-code 1 \
${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}
"""
}
}
}
stage('SAST') {
steps {
container('node') {
sh 'npx semgrep --config=auto .'
}
}
}
}
}
stage('Deploy Staging') {
when { branch 'main' }
steps {
sh """
kubectl set image deployment/myapp \
myapp=${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} \
-n staging
kubectl rollout status deployment/myapp -n staging --timeout=300s
"""
}
}
stage('Deploy Production') {
when { branch 'main' }
input {
message "Deploy to production?"
ok "Yes, deploy!"
}
steps {
sh """
kubectl set image deployment/myapp \
myapp=${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} \
-n production
kubectl rollout status deployment/myapp -n production --timeout=300s
"""
}
}
}
post {
failure {
slackSend(
channel: '#deployments',
color: 'danger',
message: "Pipeline FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
success {
slackSend(
channel: '#deployments',
color: 'good',
message: "Pipeline SUCCESS: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
}
}5.3 ArgoCD cho GitOps
# argocd/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp-production
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/mycompany/k8s-manifests.git
targetRevision: main
path: overlays/production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true # Xoá resource không còn trong Git
selfHeal: true # Tự sửa nếu ai đó kubectl edit trực tiếp
syncOptions:
- CreateNamespace=true
retry:
limit: 3
backoff:
duration: 5s
factor: 2
maxDuration: 3m
# Health checks
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # Ignore HPA-managed replicas# argocd/rollout.yaml — Argo Rollouts cho Canary
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: myapp
namespace: production
spec:
replicas: 10
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: ghcr.io/mycompany/myapp:abc1234
ports:
- containerPort: 3000
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
readinessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
strategy:
canary:
steps:
- setWeight: 5 # 5% traffic sang version mới
- pause: { duration: 5m }
- analysis: # Automated analysis
templates:
- templateName: success-rate
args:
- name: service-name
value: myapp
- setWeight: 25
- pause: { duration: 10m }
- setWeight: 50
- pause: { duration: 10m }
- setWeight: 100
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: service-name
metrics:
- name: success-rate
interval: 1m
count: 5
successCondition: result[0] >= 0.99 # 99% success rate
failureLimit: 2
provider:
prometheus:
address: http://prometheus:9090
query: |
sum(rate(http_requests_total{service="{{args.service-name}}",status=~"2.."}[5m]))
/
sum(rate(http_requests_total{service="{{args.service-name}}"}[5m]))5.4 Terraform Basics
# terraform/main.tf
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# Remote state — KHÔNG lưu state trên local
backend "s3" {
bucket = "mycompany-terraform-state"
key = "production/terraform.tfstate"
region = "ap-southeast-1"
dynamodb_table = "terraform-locks" # State locking
encrypt = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "terraform"
Team = "platform"
}
}
}
# ==================== VARIABLES ====================
variable "aws_region" {
description = "AWS region"
type = string
default = "ap-southeast-1"
}
variable "environment" {
description = "Environment name"
type = string
default = "production"
}
variable "app_name" {
description = "Application name"
type = string
default = "myapp"
}
# ==================== VPC ====================
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.0"
name = "${var.app_name}-${var.environment}"
cidr = "10.0.0.0/16"
azs = ["${var.aws_region}a", "${var.aws_region}b", "${var.aws_region}c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = false # HA: 1 NAT per AZ
enable_dns_hostnames = true
}
# ==================== ECS CLUSTER ====================
resource "aws_ecs_cluster" "main" {
name = "${var.app_name}-${var.environment}"
setting {
name = "containerInsights"
value = "enabled"
}
}
# ==================== ECR ====================
resource "aws_ecr_repository" "app" {
name = var.app_name
image_tag_mutability = "IMMUTABLE" # Prevent tag overwrite
image_scanning_configuration {
scan_on_push = true # Auto scan mỗi khi push image
}
encryption_configuration {
encryption_type = "AES256"
}
}
# ECR Lifecycle — auto clean old images
resource "aws_ecr_lifecycle_policy" "app" {
repository = aws_ecr_repository.app.name
policy = jsonencode({
rules = [
{
rulePriority = 1
description = "Keep last 20 images"
selection = {
tagStatus = "any"
countType = "imageCountMoreThan"
countNumber = 20
}
action = {
type = "expire"
}
}
]
})
}
# ==================== OUTPUTS ====================
output "vpc_id" {
value = module.vpc.vpc_id
}
output "ecr_repository_url" {
value = aws_ecr_repository.app.repository_url
}
output "ecs_cluster_name" {
value = aws_ecs_cluster.main.name
}5.5 Docker Multi-Stage Build
# Dockerfile — Multi-stage build
# Stage 1: Dependencies (cached nếu package.json không đổi)
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production && \
cp -R node_modules /production_modules && \
npm ci # Install devDependencies for build stage
# Stage 2: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && \
npm run test:unit # Run tests in build stage
# Stage 3: Production image (minimal)
FROM node:20-alpine AS runner
LABEL maintainer="platform-team@mycompany.com"
LABEL org.opencontainers.image.source="https://github.com/mycompany/myapp"
# Security: run as non-root user
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 appuser
WORKDIR /app
# Copy only production dependencies + built code
COPY --from=deps /production_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# Security: no shell access
RUN apk --no-cache add dumb-init && \
rm -rf /var/cache/apk/*
# Security: read-only filesystem
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/healthz || exit 1
EXPOSE 3000
# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]Tại sao Multi-Stage Build quan trọng?
| Single-stage | Multi-stage | |
|---|---|---|
| Image size | ~1.2 GB (Node.js full + devDeps + source) | ~150 MB (Alpine + production deps + built code) |
| Attack surface | Nhiều packages = nhiều CVE tiềm ẩn | Minimal packages |
| Build cache | Mỗi thay đổi rebuild toàn bộ | Layer cache cho dependencies |
| Secrets | devDependencies có thể chứa tooling credentials | Chỉ production code |
6. Code Examples
6.1 Complete GitHub Actions CI/CD Workflow
# .github/workflows/complete-pipeline.yml
name: Complete CI/CD Pipeline
on:
push:
branches: [main, 'release/**']
pull_request:
branches: [main]
permissions:
contents: read
packages: write
security-events: write
env:
NODE_VERSION: '20'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # Cancel previous runs on same branch
jobs:
# ────────────────── STAGE 1: LINT ──────────────────
lint:
name: "Lint & Format Check"
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: ESLint
run: npm run lint -- --format=json --output-file=eslint-report.json
continue-on-error: true
- name: Prettier check
run: npx prettier --check "src/**/*.{ts,tsx,json}"
- name: TypeScript type check
run: npx tsc --noEmit
- name: Upload lint report
if: always()
uses: actions/upload-artifact@v4
with:
name: eslint-report
path: eslint-report.json
# ────────────────── STAGE 2: TEST ──────────────────
test-unit:
name: "Unit Tests"
runs-on: ubuntu-latest
needs: lint
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run test:unit -- --coverage --forceExit
env:
CI: true
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-unit
path: coverage/
- name: Coverage gate (fail if < 80%)
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
echo "Line coverage: ${COVERAGE}%"
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "::error::Coverage ${COVERAGE}% is below 80% threshold"
exit 1
fi
test-integration:
name: "Integration Tests"
runs-on: ubuntu-latest
needs: lint
timeout-minutes: 15
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: test_db
POSTGRES_USER: test
POSTGRES_PASSWORD: test_password
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports: ['6379:6379']
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- name: Run DB migrations
run: npm run db:migrate
env:
DATABASE_URL: postgresql://test:test_password@localhost:5432/test_db
- name: Integration tests
run: npm run test:integration -- --forceExit
env:
DATABASE_URL: postgresql://test:test_password@localhost:5432/test_db
REDIS_URL: redis://localhost:6379
CI: true
# ────────────────── STAGE 3: BUILD ──────────────────
build:
name: "Build Docker Image"
runs-on: ubuntu-latest
needs: [test-unit, test-integration]
outputs:
image-digest: ${{ steps.build.outputs.digest }}
image-tag: ${{ steps.meta.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
if: github.event_name == 'push'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name == 'push' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true # SLSA provenance attestation
sbom: true # Auto-generate SBOM
# ────────────────── STAGE 4: SECURITY SCAN ──────────────────
scan-image:
name: "Container Security Scan"
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1' # Fail pipeline on CRITICAL/HIGH
- name: Upload scan results to GitHub Security
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
scan-code:
name: "SAST & Secret Scan"
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for secret scanning
- name: Semgrep SAST
uses: semgrep/semgrep-action@v1
with:
config: >-
p/security-audit
p/secrets
p/owasp-top-ten
p/typescript
- name: Gitleaks secret detection
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
scan-dependencies:
name: "Dependency Scan"
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- name: npm audit
run: npm audit --audit-level=high
- name: License check
run: npx license-checker --onlyAllow "MIT;ISC;BSD-2-Clause;BSD-3-Clause;Apache-2.0"
# ────────────────── STAGE 5: DEPLOY ──────────────────
deploy-staging:
name: "Deploy to Staging"
runs-on: ubuntu-latest
needs: [build, scan-image, scan-code, scan-dependencies]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment:
name: staging
url: https://staging.myapp.com
steps:
- uses: actions/checkout@v4
with:
repository: mycompany/k8s-manifests
token: ${{ secrets.GITOPS_TOKEN }}
path: k8s-manifests
- name: Update staging manifest
run: |
cd k8s-manifests/overlays/staging
kustomize edit set image \
myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: Commit to GitOps repo
run: |
cd k8s-manifests
git config user.name "ci-bot"
git config user.email "ci-bot@mycompany.com"
git add .
git commit -m "deploy(staging): ${{ github.sha }} from ${{ github.event.head_commit.message }}"
git push
- name: Smoke tests
run: |
sleep 60 # Wait for ArgoCD to sync
curl --fail --retry 5 --retry-delay 10 https://staging.myapp.com/healthz
deploy-production:
name: "Deploy to Production"
runs-on: ubuntu-latest
needs: deploy-staging
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment:
name: production
url: https://myapp.com
steps:
- uses: actions/checkout@v4
with:
repository: mycompany/k8s-manifests
token: ${{ secrets.GITOPS_TOKEN }}
path: k8s-manifests
- name: Update production manifest
run: |
cd k8s-manifests/overlays/production
kustomize edit set image \
myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: Commit to GitOps repo
run: |
cd k8s-manifests
git config user.name "ci-bot"
git config user.email "ci-bot@mycompany.com"
git add .
git commit -m "deploy(production): ${{ github.sha }}"
git push
- name: Notify deployment
uses: slackapi/slack-github-action@v1
with:
channel-id: '#deployments'
slack-message: |
:rocket: *Production Deploy*
Commit: `${{ github.sha }}`
Author: ${{ github.actor }}
Message: ${{ github.event.head_commit.message }}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}6.2 Docker Compose for Local Pipeline Testing
# docker-compose.pipeline.yml
# Mô phỏng CI/CD pipeline locally để debug
version: '3.9'
services:
# ── App ──
app:
build:
context: .
dockerfile: Dockerfile
target: builder # Dùng builder stage để test
volumes:
- .:/app
- /app/node_modules
environment:
NODE_ENV: test
DATABASE_URL: postgresql://dev:dev@postgres:5432/dev_db
REDIS_URL: redis://redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
# ── Database ──
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: dev_db
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dev"]
interval: 5s
timeout: 5s
retries: 5
# ── Cache ──
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
# ── Pipeline Runner ──
pipeline:
build:
context: .
dockerfile: Dockerfile
target: deps
volumes:
- .:/app
- /app/node_modules
environment:
DATABASE_URL: postgresql://dev:dev@postgres:5432/dev_db
REDIS_URL: redis://redis:6379
CI: "true"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
entrypoint: /bin/sh
command: >
-c "
echo '=== STAGE 1: Lint ===' &&
npm run lint &&
npm run type-check &&
echo '=== STAGE 2: Unit Tests ===' &&
npm run test:unit -- --coverage &&
echo '=== STAGE 3: Integration Tests ===' &&
npm run db:migrate &&
npm run test:integration &&
echo '=== STAGE 4: Build ===' &&
npm run build &&
echo '=== STAGE 5: Security Scan ===' &&
npm audit --audit-level=high &&
echo '✅ Pipeline completed successfully!'
"
# ── Security Scanner ──
trivy:
image: aquasec/trivy:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- trivy-cache:/root/.cache/trivy
entrypoint: ["trivy"]
command: ["image", "--severity", "HIGH,CRITICAL", "myapp:local"]
volumes:
pgdata:
trivy-cache:# Chạy local pipeline
docker compose -f docker-compose.pipeline.yml run --rm pipeline
# Chạy security scan
docker build -t myapp:local .
docker compose -f docker-compose.pipeline.yml run --rm trivy6.3 Terraform Basic Infrastructure
# terraform/ecs-service.tf
# ECS Fargate service with Blue-Green deployment
resource "aws_ecs_task_definition" "app" {
family = "${var.app_name}-${var.environment}"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 512
memory = 1024
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
container_definitions = jsonencode([
{
name = var.app_name
image = "${aws_ecr_repository.app.repository_url}:latest"
portMappings = [
{
containerPort = 3000
protocol = "tcp"
}
]
environment = [
{ name = "NODE_ENV", value = var.environment },
{ name = "PORT", value = "3000" }
]
secrets = [
{
name = "DATABASE_URL"
valueFrom = aws_ssm_parameter.db_url.arn
},
{
name = "REDIS_URL"
valueFrom = aws_ssm_parameter.redis_url.arn
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "ecs"
}
}
healthCheck = {
command = ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3000/healthz || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
}
])
}
resource "aws_ecs_service" "app" {
name = var.app_name
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = 3
launch_type = "FARGATE"
deployment_controller {
type = "CODE_DEPLOY" # Blue-Green via CodeDeploy
}
network_configuration {
subnets = module.vpc.private_subnets
security_groups = [aws_security_group.app.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.blue.arn
container_name = var.app_name
container_port = 3000
}
lifecycle {
ignore_changes = [task_definition, load_balancer] # Managed by CodeDeploy
}
}
# ── ALB with Blue-Green Target Groups ──
resource "aws_lb" "app" {
name = "${var.app_name}-${var.environment}"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = module.vpc.public_subnets
}
resource "aws_lb_target_group" "blue" {
name = "${var.app_name}-blue"
port = 3000
protocol = "HTTP"
vpc_id = module.vpc.vpc_id
target_type = "ip"
health_check {
path = "/healthz"
healthy_threshold = 2
unhealthy_threshold = 3
interval = 15
timeout = 5
}
}
resource "aws_lb_target_group" "green" {
name = "${var.app_name}-green"
port = 3000
protocol = "HTTP"
vpc_id = module.vpc.vpc_id
target_type = "ip"
health_check {
path = "/healthz"
healthy_threshold = 2
unhealthy_threshold = 3
interval = 15
timeout = 5
}
}
resource "aws_lb_listener" "production" {
load_balancer_arn = aws_lb.app.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = var.acm_certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.blue.arn
}
lifecycle {
ignore_changes = [default_action] # Managed by CodeDeploy
}
}7. System Design Diagrams (Mermaid)
7.1 CI/CD Pipeline Flow
flowchart TD A["Developer Push Code"] --> B["Git Repository<br/>(GitHub/GitLab)"] B --> C{"Trigger:<br/>PR or Merge?"} C -->|PR| D["CI Pipeline"] C -->|Merge to main| E["CI + CD Pipeline"] subgraph CI["CI — Continuous Integration"] D --> D1["1. Lint<br/>(ESLint, Prettier)"] D1 --> D2["2. Type Check<br/>(TypeScript)"] D2 --> D3["3. Unit Tests<br/>(Jest, coverage ≥ 80%)"] D3 --> D4["4. Integration Tests<br/>(DB, Redis)"] D4 --> D5["5. SAST Scan<br/>(Semgrep, CodeQL)"] D5 --> D6["6. Dependency Scan<br/>(npm audit, Snyk)"] end subgraph CD_Build["CD — Build & Scan"] E --> E1["7. Docker Build<br/>(multi-stage)"] E1 --> E2["8. Push Image<br/>(GHCR/ECR)"] E2 --> E3["9. Container Scan<br/>(Trivy)"] E3 --> E4["10. Generate SBOM<br/>(CycloneDX)"] end subgraph CD_Deploy["CD — Deploy"] E4 --> F1["11. Deploy Staging"] F1 --> F2["12. Smoke Tests"] F2 --> F3{"Pass?"} F3 -->|Yes| F4["13. Manual Approval<br/>(or auto)"] F3 -->|No| F5["❌ Alert Team<br/>Block Deploy"] F4 --> F6["14. Deploy Production<br/>(Canary 5% → 25% → 100%)"] F6 --> F7["15. Monitor Metrics<br/>(error rate, latency)"] F7 --> F8{"Healthy?"} F8 -->|Yes| F9["✅ Complete"] F8 -->|No| F10["🔄 Auto Rollback"] end style D1 fill:#e3f2fd,stroke:#1976d2 style D3 fill:#e3f2fd,stroke:#1976d2 style D5 fill:#ffebee,stroke:#c62828 style D6 fill:#ffebee,stroke:#c62828 style E3 fill:#ffebee,stroke:#c62828 style F6 fill:#e8f5e9,stroke:#2e7d32 style F10 fill:#fff3e0,stroke:#e65100
7.2 Blue-Green Deployment Diagram
sequenceDiagram participant Dev as Developer participant CI as CI Pipeline participant Reg as Container Registry participant LB as Load Balancer participant Blue as Blue (v1 — Active) participant Green as Green (v2 — Idle) participant Mon as Monitoring Dev->>CI: Push code (v2) CI->>CI: Lint → Test → Build → Scan CI->>Reg: Push image v2 Note over LB,Blue: Traffic: 100% → Blue (v1) CI->>Green: Deploy v2 to Green CI->>Green: Health check Green-->>CI: ✅ Healthy CI->>CI: Run smoke tests against Green CI-->>CI: ✅ Tests pass Note over LB: Switch traffic LB->>Green: Route 100% traffic → Green (v2) LB--xBlue: Stop routing to Blue Mon->>Green: Monitor error rate, latency Mon-->>Mon: Error rate < 1% ✅ Note over Blue: Blue becomes idle (standby for rollback) alt Rollback needed Mon->>LB: Error rate spike detected! LB->>Blue: Route traffic back → Blue (v1) LB--xGreen: Stop routing to Green Note over Blue: Rollback in seconds! end
7.3 GitOps Flow
flowchart LR subgraph Developer_Flow["Developer Workflow"] A["Developer"] -->|1. Push code| B["App Repo<br/>(application code)"] end subgraph CI_Pipeline["CI Pipeline"] B -->|2. Trigger| C["CI Build"] C -->|3. Test & Build| D["Docker Image"] D -->|4. Push| E["Container<br/>Registry"] end subgraph GitOps_Repo["GitOps Repository"] C -->|5. Update image tag| F["K8s Manifests<br/>(desired state)"] end subgraph Cluster["Kubernetes Cluster"] G["ArgoCD<br/>(GitOps Agent)"] H["Production<br/>Workloads"] end F -->|6. Watch & Pull| G G -->|7. Apply| H G -->|"8. Reconcile<br/>(actual = desired)"| H E -->|"9. Pull image"| H style G fill:#f9a825,stroke:#333,stroke-width:2px style F fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
8. Aha Moments & Pitfalls
Aha Moments
#1 — Deploy frequency inversely correlates with risk: Deploy 100 lần/ngày nghe “nguy hiểm” nhưng thực tế an toàn hơn deploy 1 lần/tháng. Mỗi deploy nhỏ = dễ review, dễ test, dễ rollback. Deploy lớn = hàng nghìn dòng code thay đổi, không ai review kỹ, rollback phức tạp.
#2 — CI/CD is a culture, not a tool: Chỉ cài Jenkins/GitHub Actions chưa đủ. Team phải commit thường xuyên, viết test đủ tốt, review PR nhanh. Tool chỉ là enabler — culture mới là foundation.
#3 — “Works on my machine” dies with CI/CD: Khi mọi build đều chạy trong container chuẩn (Docker), environment mismatch biến mất. Cái gì pass CI = chạy được mọi nơi.
#4 — Feature flags decouple deploy from release: Đây là insight lớn nhất cho production safety. Code có thể được deploy nhưng feature ẩn sau flag. Bật dần dần cho 1% → 10% → 100% users. Lỗi? Tắt flag — không cần rollback code.
#5 — GitOps = Git as the single source of truth: Muốn biết production đang chạy gì? Nhìn Git repo. Muốn rollback?
git revert. Muốn audit?git log. Toàn bộ infrastructure state nằm trong version control.
Pitfalls
Pitfall 1: Long Build Times (Thời gian build dài)
Vấn đề: Build 30 phút → developer mở PR rồi đi uống cà phê → quay lại quên context → productivity giảm.
Giải pháp:
- Docker layer caching (chỉ rebuild layer thay đổi)
- Parallel test execution (chạy test song song)
- Incremental builds (chỉ build module thay đổi)
- Self-hosted runner gần Docker registry (giảm pull time)
- Tách pipeline: fast checks trước (lint, type-check ~2 phút), heavy checks sau (integration test, scan)
Pitfall 2: Flaky Tests Blocking Deploys (Test không ổn định chặn deploy)
Vấn đề: Test pass 9/10 lần, fail ngẫu nhiên → team bắt đầu bỏ qua CI failures → CI mất ý nghĩa.
Giải pháp:
- Quarantine flaky tests — đánh dấu và chạy riêng, không block pipeline
- Track flaky rate: nếu test fail > 2% lần chạy → investigate root cause
- Common root causes: race conditions, time-dependent assertions, shared test state, external API calls without mocking
- Rule: Fix flaky test trong 48 giờ hoặc delete
Pitfall 3: Secrets Leaking in Logs (Bí mật bị lộ trong nhật ký)
Vấn đề: Pipeline logs hiển thị environment variables, database URLs, API keys.
Giải pháp:
- Dùng CI/CD native secrets (tự động masked trong logs)
- NEVER
echo $SECREThoặcprintenvtrong pipeline- Review pipeline logs trước khi share
- Dùng
set +xtrong bash scripts để tắt command echo- Cấu hình log sanitization: regex filter cho patterns giống API key/password
Pitfall 4: No Rollback Plan (Không có kế hoạch hoàn tác)
Vấn đề: Deploy xong mới phát hiện lỗi nhưng không biết rollback như nào. Database đã migrate, data đã thay đổi.
Giải pháp:
- Pre-deploy checklist: Mỗi deploy phải có rollback plan trước khi bắt đầu
- Database migration phải backward compatible (expand-contract pattern)
- Giữ previous version image luôn sẵn sàng trong registry
- Blue-Green hoặc Canary cho critical services
- Practice rollback regularly — chaos engineering cho deploy process
Pitfall 5: Monolith CI/CD cho Microservices
Vấn đề: Một pipeline duy nhất build + test + deploy tất cả microservices → service A thay đổi trigger rebuild service B, C, D.
Giải pháp:
- Mỗi service có pipeline riêng
- Path-based triggers: chỉ trigger pipeline khi files trong folder service đó thay đổi
- Shared pipeline templates (DRY) nhưng execution riêng
Pitfall 6: Không test pipeline chính nó
Vấn đề: Pipeline config thay đổi → break → toàn team blocked.
Giải pháp:
- Test pipeline changes trên branch trước khi merge
acttool cho GitHub Actions: chạy workflow locally- Pipeline có self-test: nếu pipeline config thay đổi, chạy dry-run
9. Internal Links — Liên kết nội bộ
| Topic | Link | Liên quan CI/CD |
|---|---|---|
| Scale from Zero | Tuan-01-Scale-From-Zero-To-Millions | CI/CD cho phép scale deployment process |
| Estimation | Tuan-02-Back-of-the-envelope | Ước lượng pipeline cost, build time |
| Networking & CDN | Tuan-03-Networking-DNS-CDN | Deploy assets lên CDN qua pipeline |
| API Design | Tuan-04-API-Design-Protocols | API versioning strategy ảnh hưởng deployment |
| Load Balancer | Tuan-05-Load-Balancer | Traffic shifting cho Blue-Green/Canary |
| Cache Strategy | Tuan-06-Cache-Strategy | Cache invalidation khi deploy version mới |
| Database | Tuan-07-Database-Sharding-Replication | DB migration trong CI/CD pipeline |
| Message Queue | Tuan-08-Message-Queue | Queue backward compatibility khi deploy |
| Rate Limiter | Tuan-09-Rate-Limiter | Rate limit cho deployment API |
| Unique ID | Tuan-10-Unique-ID-Generator | Build number, deployment ID generation |
| Microservices | Tuan-11-Microservices-Pattern | Independent deployment per service |
| Monitoring | Tuan-13-Monitoring-Observability | Deploy verification, canary metrics |
| AuthN/AuthZ | Tuan-14-AuthN-AuthZ-Security | Pipeline RBAC, deployment permissions |
| Data Security | Tuan-15-Data-Security-Encryption | Secrets management, signed artifacts |
Tham khảo
- Alex Xu, System Design Interview — Deployment & DevOps patterns
- Jez Humble & David Farley, Continuous Delivery — The definitive book on CD
- Gene Kim et al., The DevOps Handbook — DORA metrics & transformation
- Google, Accelerate: State of DevOps Report — Data-driven DevOps research
- GitHub Actions Documentation
- ArgoCD Documentation
- Terraform Documentation
- OWASP CI/CD Security Guidelines
- Tuan-11-Microservices-Pattern — Microservices cần CI/CD per service
- Tuan-13-Monitoring-Observability — Monitoring deploy health
- Tuan-15-Data-Security-Encryption — Secrets & encryption trong pipeline
Tuần tới: Tuan-13-Monitoring-Observability — Từ deploy xong đến biết hệ thống có healthy không