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

  1. Nguyên liệu thô (raw materials) được đưa vào → tương đương source code từ developer
  2. Trạm hàn khung → tương đương build stage — biên dịch code thành artifact
  3. Trạm kiểm tra khung → tương đương unit test — kiểm tra từng component riêng lẻ
  4. Trạm lắp động cơ + hộp số → tương đương integration test — ghép các module lại
  5. Trạm kiểm tra chất lượng QC → tương đương security scan + code quality gate
  6. Trạm sơn + hoàn thiện → tương đương packaging (Docker image, artifact)
  7. Trạm test lái thử → tương đương staging environment test
  8. 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:

MetricEliteHighMediumLow
Deployment Frequency (tần suất deploy)On-demand (nhiều lần/ngày)1 lần/tuần – 1 lần/tháng1 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ần1 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ày1 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ệmTiếng ViệtMô tảVí dụ
Continuous Integration (CI)Tích hợp liên tụcDeveloper merge code vào shared branch thường xuyên (nhiều lần/ngày). Mỗi merge trigger automated build + testMỗi PR tự chạy lint, unit test, integration test
Continuous Delivery (CD)Phân phối liên tụcCode 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ụcMọi commit pass pipeline đều tự động lên production. Không cần approvalGitHub 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:

  1. Declarative (khai báo): Toàn bộ hệ thống được mô tả dưới dạng code trong Git
  2. Versioned & Immutable (có phiên bản & bất biến): Git history = audit log tự nhiên
  3. Pulled Automatically (tự động kéo): Agent trong cluster tự pull thay đổi từ Git
  4. 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)
CredentialsCI server cần SSH/kubectl access vào clusterAgent trong cluster tự pull, không cần expose cluster
SecurityCI server bị hack → toàn bộ cluster bị compromiseAgent 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
AuditPhải check CI logsGit 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)
BranchMục đíchLifetime
mainProduction code, mỗi commit là một releaseVĩnh viễn
developIntegration branch, merge feature vào đâyVĩnh viễn
feature/*Phát triển tính năng mớiNgắn (vài ngày)
release/*Chuẩn bị release, bug fix cuốiNgắn (1-2 tuần)
hotfix/*Fix bug production khẩn cấpRấ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 main thườ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
  • main luô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ểmNhượ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 switchChi 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ểmNhược điểm
Giảm blast radius (phạm vi ảnh hưởng) — lỗi chỉ affect 2% usersPhức tạp hơn blue-green
Cho phép monitor metrics trước khi tăng trafficCần infrastructure hỗ trợ traffic splitting
Tự động rollback nếu error rate tăngHai 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ểmNhược điểm
Không cần thêm infrastructureHai version chạy đồng thời trong quá trình deploy
Kubernetes mặc định dùng strategy nàyRollback chậm (phải rolling lại)
Cấu hình maxSurge + maxUnavailableKhô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:

ToolLoạiĐặc điểm
LaunchDarklySaaS (dịch vụ đám mây)Enterprise, real-time updates, targeting rules phức tạp
UnleashSelf-hosted / CloudOpen-source, đủ dùng cho hầu hết use case
FlagsmithSelf-hosted / CloudOpen-source, hỗ trợ remote config
ConfigCatSaaSNhẹ, pricing theo feature flag count

Các loại feature flag:

LoạiMục đíchLifecycle
Release flag (cờ phát hành)Ẩn feature chưa hoàn thiệnNgắn (xoá sau khi feature stable)
Experiment flag (cờ thử nghiệm)A/B testingTrung bình (xoá sau khi có kết quả)
Ops flag (cờ vận hành)Circuit breaker, kill switchDài hạn
Permission flag (cờ phân quyền)Premium feature, beta accessDà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 dateowner.

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.

ToolNgôn ngữĐặc điểm
Terraform (HashiCorp)HCL (HashiCorp Configuration Language)Multi-cloud, declarative, state management
PulumiTypeScript/Python/Go/C#Dùng ngôn ngữ lập trình thật, không cần học DSL
AWS CloudFormationYAML/JSONAWS-only, native integration
AnsibleYAMLConfiguration 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):

RegistryLoạiĐặc điểm
Docker HubPublic/PrivatePhổ biến nhất, rate limit cho free tier
Amazon ECRPrivateTích hợp AWS, image scanning
Google Artifact RegistryPrivateTích hợp GCP, hỗ trợ nhiều format
GitHub Container Registry (ghcr.io)Public/PrivateTích hợp GitHub Actions
HarborSelf-hostedOpen-source, enterprise features, vulnerability scanning

Artifact Management — không chỉ container images:

ArtifactToolVí dụ
Docker imagesECR, Harbormyapp:v1.2.3
npm packagesnpm registry, Artifactory@company/shared-utils@1.0.0
JAR/WAR filesNexus, Artifactorypayment-service-1.2.3.jar
Helm chartsChartMuseum, OCI registrymyapp-chart-1.2.3.tgz
Terraform modulesTerraform Registrymodules/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 latest trong production — không biết version nào đang chạy

2.9 Rollback Strategies (Chiến lược hoàn tác)

StrategyTốc độPhức tạpKhi nào dùng
Revert deploy (deploy lại version cũ)Nhanh (phút)ThấpLỗi đơn giản, không liên quan DB
Blue-Green switch backTức thì (giây)Trung bìnhKhi dùng Blue-Green deployment
Feature flag offTức thì (giây)ThấpFeature mới có flag
Database rollbackChậm (giờ)Rất caoTránh bằng mọi giá — dùng backward-compatible migration
Git revert + redeployTrung bình (10-30 phút)ThấpKhi 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:

ToolNgôn ngữĐặc điểm
FlywayJava/SQLVersion-based, SQL scripts
LiquibaseJava/XML/SQLChangeset-based, rollback support
AlembicPython (SQLAlchemy)Auto-generate migrations
golang-migrateGoLightweight, CLI + library
Prisma MigrateTypeScriptSchema-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ụ):

RunnerCost/minSpecs
Linux (standard)$0.0082 vCPU, 7GB RAM
Linux (large)$0.0164 vCPU, 16GB RAM
macOS$0.083 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:

ComponentMonthly 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
SnykSaaS, hỗ trợ nhiều ngôn ngữ, license scanning
OWASP Dependency-CheckOpen-source, NVD database
RenovateOpen-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.3

Policy: 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ápKhi nào dùng
GitHub Actions SecretsCI/CD variables, encrypted at rest
HashiCorp VaultEnterprise, dynamic secrets, rotation
AWS Secrets ManagerAWS-native, automatic rotation
Azure Key VaultAzure-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ỄN

Pitfall: 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-branch hoặ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ạiTên đầy đủThời điểmPhát hiện gì
SASTStatic Application Security Testing (Kiểm tra bảo mật tĩnh)Build time — quét source codeSQL injection, XSS, hardcoded secrets, insecure patterns
DASTDynamic Application Security Testing (Kiểm tra bảo mật động)Runtime — quét app đang chạyRuntime vulnerabilities, misconfigurations, auth bypass
SCASoftware Composition Analysis (Phân tích thành phần phần mềm)Build time — quét dependenciesKnown CVEs trong third-party libraries

Tools phổ biến:

ToolLoạiĐặc điểm
SonarQubeSASTMulti-language, quality gate, technical debt tracking
SemgrepSASTLightweight, custom rules, fast
CodeQL (GitHub)SASTSemantic analysis, free cho public repos
OWASP ZAPDASTOpen-source, API scanning
Snyk CodeSASTReal-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-stageMulti-stage
Image size~1.2 GB (Node.js full + devDeps + source)~150 MB (Alpine + production deps + built code)
Attack surfaceNhiều packages = nhiều CVE tiềm ẩnMinimal packages
Build cacheMỗi thay đổi rebuild toàn bộLayer cache cho dependencies
SecretsdevDependencies có thể chứa tooling credentialsChỉ 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 trivy

6.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 $SECRET hoặc printenv trong pipeline
  • Review pipeline logs trước khi share
  • Dùng set +x trong 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
  • act tool cho GitHub Actions: chạy workflow locally
  • Pipeline có self-test: nếu pipeline config thay đổi, chạy dry-run

TopicLinkLiên quan CI/CD
Scale from ZeroTuan-01-Scale-From-Zero-To-MillionsCI/CD cho phép scale deployment process
EstimationTuan-02-Back-of-the-envelopeƯớc lượng pipeline cost, build time
Networking & CDNTuan-03-Networking-DNS-CDNDeploy assets lên CDN qua pipeline
API DesignTuan-04-API-Design-ProtocolsAPI versioning strategy ảnh hưởng deployment
Load BalancerTuan-05-Load-BalancerTraffic shifting cho Blue-Green/Canary
Cache StrategyTuan-06-Cache-StrategyCache invalidation khi deploy version mới
DatabaseTuan-07-Database-Sharding-ReplicationDB migration trong CI/CD pipeline
Message QueueTuan-08-Message-QueueQueue backward compatibility khi deploy
Rate LimiterTuan-09-Rate-LimiterRate limit cho deployment API
Unique IDTuan-10-Unique-ID-GeneratorBuild number, deployment ID generation
MicroservicesTuan-11-Microservices-PatternIndependent deployment per service
MonitoringTuan-13-Monitoring-ObservabilityDeploy verification, canary metrics
AuthN/AuthZTuan-14-AuthN-AuthZ-SecurityPipeline RBAC, deployment permissions
Data SecurityTuan-15-Data-Security-EncryptionSecrets management, signed artifacts

Tham khảo


Tuần tới: Tuan-13-Monitoring-Observability — Từ deploy xong đến biết hệ thống có healthy không