Xây dựng CI/CD Pipeline tự động triển khai với GitHub Actions và Docker

Trong quy trình kỹ thuật phần mềm hiện đại, tốc độ chuyển giao mã nguồn từ môi trường phát triển cục bộ lên hệ thống máy chủ Production quyết định trực tiếp khả năng thích ứng của doanh nghiệp nhưng đồng thời cũng là điểm nhạy cảm nhất về mặt độ tin cậy vận hành. Tại nhiều tổ chức công nghệ, quy trình phát hành ứng dụng vẫn phụ thuộc vào các thao tác thủ công rời rạc: kỹ sư tự chạy lệnh docker build trên máy cá nhân, gắn nhãn ghi đè :latest đẩy lên kho lưu trữ chung, rồi đăng nhập qua giao thức SSH vào máy chủ để khởi động lại container. Mô hình thủ công này tiềm ẩn 4 rủi ro kỹ thuật nghiêm trọng: sự sai lệch môi trường biên dịch (Configuration Drift), mã nguồn chưa qua kiểm thử tự động vẫn lọt lên môi trường thật, hình ảnh container chứa các lỗ hổng bảo mật hệ điều hành (CVEs) mức độ nghiêm trọng không được phát hiện, và thời gian chết (Downtime) kéo dài khi bản cập nhật mới bị treo lúc khởi động mà không có cơ chế tự động quay lui (Rollback).

Việc xây dựng CI/CD Pipeline với GitHub Actions và Docker theo chuẩn Production giải quyết triệt để các điểm nghẽn trên bằng cách thiết lập một dây chuyền lắp ráp phần mềm tất định (Deterministic Software Delivery Pipeline). Mọi thay đổi mã nguồn đều phải vượt qua các chốt chặn kiểm tra tự động: từ phân tích cú pháp tĩnh (Linting), chạy kiểm thử đơn vị (Unit Testing), đóng gói hình ảnh container tốc độ cao nhờ cơ chế bộ nhớ đệm lớp của Docker Buildx (BuildKit), quét lỗ hổng bảo mật bắt buộc bằng Aqua Security Trivy, định danh tạo tác bất biến theo mã băm Git SHA trên GitHub Container Registry (GHCR), cho đến bước triển khai nguyên tử (Atomic Rollout) có kiểm tra sức khỏe dịch vụ (/healthz) và tự động phục hồi trạng thái cũ nếu phát hiện lỗi.

Bài viết kỹ thuật chuyên sâu này hướng dẫn từng bước thiết kế, cấu hình và vận hành một luồng CI/CD hoàn chỉnh từ lúc kỹ sư thực hiện lệnh git push cho đến khi container vận hành an toàn trên máy chủ Production, cung cấp toàn bộ mã cấu hình chuẩn xác sẵn sàng áp dụng cho các hệ thống thực tế.


Mục Lục Nội Dung

1. Kiến trúc Luồng CI/CD Pipeline Hiện đại từ Git Commit đến Production

1.1 Phân tách Trách nhiệm giữa Continuous Integration (CI) và Continuous Deployment (CD)

Một kiến trúc đường ống phát hành phần mềm chuẩn mực luôn phân định rõ ranh giới kỹ thuật cũng như quyền hạn bảo mật giữa hai pha Tích hợp Liên tục (Continuous Integration — CI) và Triển khai Liên tục (Continuous Deployment — CD):

  1. Tầng Continuous Integration (CI — Phản hồi Nhanh & Kiểm chứng Chất lượng): Được kích hoạt trên mọi sự kiện pull_request và push vào nhánh chính. Nhiệm vụ duy nhất của tầng này là xác minh tính đúng đắn của mã nguồn thông qua bộ công cụ kiểm tra định dạng (Linter), kiểm tra kiểu tĩnh (Static Type Checking) và kiểm thử đơn vị (Unit Tests). Tầng CI hoàn toàn không có quyền truy cập vào thông tin xác thực của kho lưu trữ container hay khóa SSH của máy chủ Production.
  2. Tầng Đóng gói và Kiểm định Tạo tác (Artifact Packaging & DevSecOps Gate): Thực hiện nguyên lý "Biên dịch một lần, Triển khai mọi nơi" (Build Once, Deploy Anywhere). Thay vì biên dịch lại mã nguồn trên từng máy chủ đích, đường ống đóng gói ứng dụng thành một Docker Image duy nhất được gắn nhãn bất biến theo mã commit Git (sha-<commit>), sau đó quét toàn diện các gói thư viện hệ điều hành và ngôn ngữ lập trình bằng công cụ bảo mật Trivy trước khi cho phép đăng tải lên Registry. Để hình ảnh container tạo ra ở bước này đạt kích thước tối giản và loại bỏ hoàn toàn các trình biên dịch thừa, bạn cần kết hợp chặt chẽ với kỹ thuật được trình bày trong bài viết Tối ưu hóa Docker Image với Multi-stage Build và Alpine Linux.
  3. Tầng Continuous Deployment (CD — Phát hành Nguyên tử & Tự phục hồi): Chỉ được kích hoạt khi cả tầng CI và tầng quét bảo mật đều trả về mã thoát thành công (Exit Code 0) trên nhánh main. Tầng CD chịu sự giám sát của quy tắc bảo vệ môi trường (GitHub Environments), kéo đúng phiên bản hình ảnh đã được kiểm định từ GHCR xuống máy chủ đích, khởi chạy container mới, xác minh điểm cuối kiểm tra sức khỏe (/healthz) và tự động khôi phục về phiên bản ổn định liền trước nếu bản phát hành mới gặp sự cố khởi tạo.

Bảng dưới đây đối chiếu sự khác biệt kiến trúc giữa quy trình phát hành thủ công truyền thống và đường ống CI/CD đa tầng chuẩn Production:

Tiêu chí Kỹ thuật Quy trình Build & Deploy Thủ công / Script Rời rạc Pipeline CI/CD Đa tầng (GitHub Actions + Buildx + Trivy + Atomic CD)
Định danh Phiên bản Image Sử dụng nhãn động :latest, không thể truy vết chính xác container đang chạy tương ứng với commit Git nào Gắn nhãn bất biến sha-<7_ky_tu_commit> kết hợp nhãn chuẩn OCI (org.opencontainers.image.revision)
Cơ chế Lưu đệm Biên dịch (Build Cache) Phụ thuộc vào ổ đĩa cục bộ của máy cá nhân hoặc phải tải lại toàn bộ layer từ đầu trên mỗi GitHub Runner mới Chia sẻ bộ nhớ đệm lớp trung gian qua dịch vụ GitHub Actions Cache (type=gha,mode=max) giữa mọi lần chạy
Kiểm soát Lỗ hổng Container (CVE Gate) Không kiểm tra hoặc chỉ quét định kỳ sau khi container đã chạy trên Production Chặn cứng ngay trong pipeline (exit-code: 1) nếu phát hiện lỗ hổng mức CRITICAL hoặc HIGH đã có bản vá
Quản lý Thông tin Xác thực (Registry Auth) Lưu trữ Personal Access Token (PAT) hoặc mật khẩu Docker dài hạn dễ bị rò rỉ Sử dụng mã thông báo tạm thời GITHUB_TOKEN tự động hết hạn ngay khi kết thúc phiên chạy job
Cơ chế Phục hồi Khi Lỗi (Rollback Strategy) Kỹ sư phải tự đăng nhập SSH, tìm lại bản cũ và chạy lại lệnh khởi tạo thủ công trong lúc hệ thống đang lỗi Kịch bản deploy-rollout.sh tự động phát hiện trạng thái unhealthy và quay lui về PREVIOUS_TAG trong 60 giây

1.2 Sơ đồ Kiến trúc Luồng Công việc Đa Tầng và Chuỗi Tương tác Thực thi Runtime

Để ngăn chặn hiện tượng một bản cập nhật lỗi đi thẳng lên môi trường vận hành, các tác vụ (Jobs) trong GitHub Actions được tổ chức thành một Đồ thị Có hướng Không Chu trình (Directed Acyclic Graph — DAG) thông qua từ khóa needs. Công việc đứng sau chỉ được phép khởi tạo máy ảo thực thi (Runner) khi công việc đứng trước đã hoàn tất trọn vẹn.

Sơ đồ kiến trúc dưới đây mô tả luồng kiểm soát chất lượng qua 4 tầng từ lúc kỹ sư đẩy mã nguồn cho đến khi hoàn tất triển khai hoặc tự động quay lui:

flowchart TD
    subgraph SourceControl ["1. Quản lý Mã nguồn: GitHub Repository"]
        A["Developer git push / Pull Request"] --> B["GitHub Actions Trigger: main / PR"]
        B --> C["Concurrency Guard & Least-Privilege GITHUB_TOKEN"]
    end

    subgraph StageCI ["2. Tầng Continuous Integration: Job test"]
        C --> D["Setup Python 3.11 & Pip Dependency Cache"]
        D --> E["Chạy Linter Ruff & Unit Tests Pytest"]
        E --> F{"Tests & Linter Pass?"}
        F -- "Thất bại: Exit != 0" --> X1["Chặn Merge / Dừng Pipeline & Báo lỗi"]
    end

    subgraph StageBuildScan ["3. Tầng Buildx & Chốt chặn DevSecOps: Job build-scan-push"]
        F -- "Thành công: Exit 0" --> G["Khởi tạo Docker Buildx & GHA Layer Cache: type=gha"]
        G --> H["Build Candidate Image với Tag bất biến: sha-commit"]
        H --> I["Quét Lỗ hổng bằng Trivy: CRITICAL & HIGH CVEs"]
        I --> J{"Trivy Gate Pass?"}
        J -- "Phát hiện CVE Nghiêm trọng" --> X2["Hủy Image & Chặn Push lên Registry"]
        J -- "An toàn: 0 CRITICAL / HIGH" --> K["Push Image & Cập nhật GHA Cache mode=max lên GHCR"]
    end

    subgraph StageCD ["4. Tầng Continuous Deployment: Job deploy-production"]
        K --> L["GitHub Environment Protection: Production Approval"]
        L --> M["SSH Thực thi scripts/deploy-rollout.sh trên Server"]
        M --> N["Kiểm tra Healthcheck /healthz trong 60 giây"]
        N --> O{"Container Healthy?"}
        O -- "Healthy" --> P["Hoàn tất Rollout & Dọn dẹp Image cũ"]
        O -- "Unhealthy / Timeout" --> Q["Tự động Rollback về PREVIOUS_TAG & Cảnh báo"]
    end

Ở góc độ giao thức truyền thông giữa các hệ thống trong suốt thời gian chạy (Runtime Execution), trình tự trao đổi dữ liệu giữa GitHub Actions Runner, dịch vụ lưu đệm GHA Cache, trình quét lỗ hổng Trivy, kho chứa GHCR và máy chủ Docker Production diễn ra tuần tự như sau:

sequenceDiagram
    autonumber
    actor Dev as Kỹ sư Phần mềm
    participant GH as GitHub Actions Runner
    participant Cache as GitHub Actions Cache
    participant Trivy as Trivy Scanner
    participant GHCR as GitHub Container Registry
    participant Prod as Production Docker Host

    Dev->>GH: git push origin main (Commit SHA: a1b2c3d)
    GH->>GH: Job 1 (test): Chạy Ruff Linter & Pytest Suite
    GH->>Cache: Job 2: Tải Docker Layer Cache (cache-from: type=gha)
    Cache-->>GH: Trả về Cached Builder Layers (Cache Hit)
    GH->>GH: Build Candidate Image (ghcr.io/infoteck-org/api-gateway:sha-a1b2c3d)
    GH->>Trivy: Quét OS Packages & Lockfiles (severity: CRITICAL,HIGH)
    alt Phát hiện lỗ hổng CRITICAL hoặc HIGH
        Trivy-->>GH: Trả về Exit Code 1 kèm danh sách CVE
        GH-->>Dev: Dừng Pipeline (Chặn đẩy Image lỗi lên GHCR)
    else Không có lỗ hổng CRITICAL / HIGH
        Trivy-->>GH: Trả về Exit Code 0 (Clean)
        GH->>GHCR: Push Image (sha-a1b2c3d) bằng GITHUB_TOKEN
        GH->>Cache: Lưu Layer Cache mới (cache-to: type=gha,mode=max)
        GH->>Prod: Job 3 (deploy): SSH kích hoạt deploy-rollout.sh
        Prod->>Prod: Lưu PREVIOUS_TAG & Pull Image sha-a1b2c3d
        Prod->>Prod: docker compose up -d & Poll /healthz (60s)
        alt Healthcheck trả về HTTP 200 OK (healthy)
            Prod-->>GH: Xác nhận Rollout thành công (Exit Code 0)
            GH-->>Dev: Thông báo Deployment Production hoàn tất
        else Healthcheck thất bại hoặc quá thời gian (unhealthy)
            Prod->>Prod: Tự động khôi phục container về PREVIOUS_TAG
            Prod-->>GH: Trả về Exit Code 1 (Đã Rollback an toàn)
            GH-->>Dev: Cảnh báo Rollout thất bại & Đã tự động Rollback
        end
    end


2. Thiết lập Tầng Continuous Integration (CI): Linting, Unit Testing và Concurrency Control

2.1 Kiểm soát Xung đột Pipeline bằng Concurrency Group và Phân quyền GITHUB_TOKEN Tối thiểu

Khi nhiều kỹ sư cùng đẩy các bản sửa lỗi liên tiếp lên một nhánh tính năng (Feature Branch) hoặc khi hai commit được gộp (merge) vào nhánh main cách nhau vài giây, nếu không có cơ chế kiểm soát đồng thời, hai luồng triển khai có thể chạy song song và ghi đè trạng thái của nhau trên máy chủ đích (Race Condition).

Theo tài liệu chính thức của GitHub Actions (Workflow syntax for GitHub Actions), chúng ta cần áp dụng hai lớp bảo vệ ngay ở cấp độ toàn cục của tệp cấu hình workflow:

  1. Nguyên tắc Đặc quyền Tối thiểu (permissions: contents: read): Mặc định, một số kho lưu trữ cũ cấp quyền đọc/ghi rộng rãi cho biến GITHUB_TOKEN. Việc khóa cứng quyền mặc định ở mức chỉ đọc mã nguồn (contents: read) ở đầu tệp YAML đảm bảo rằng nếu một thư viện bên thứ ba trong bước chạy Unit Test bị tấn công chuỗi cung ứng (Supply-chain Attack), mã độc đó cũng không thể đẩy gói tin giả mạo lên GHCR hoặc chỉnh sửa nhánh mã nguồn. Quyền ghi (packages: write) chỉ được cấp cục bộ tại đúng job đóng gói container.
  2. Kiểm soát Tương tranh Thông minh (concurrency): Chúng ta nhóm các lần chạy theo tên workflow và nhánh Git. Đối với các nhánh Pull Request (github.ref != 'refs/heads/main'), cờ cancel-in-progress: true sẽ tự động hủy bỏ các phiên chạy cũ ngay khi kỹ sư đẩy commit mới lên cùng nhánh, giúp tiết kiệm tài nguyên máy chạy (Runner Minutes). Ngược lại, trên nhánh main, biểu thức điều kiện sẽ đặt cancel-in-progress: false để đảm bảo một tiến trình triển khai Production đang diễn ra không bao giờ bị ngắt ngang giữa chừng mà các lần chạy tiếp theo sẽ được xếp hàng chờ tuần tự.

2.2 Tự động hóa Linting, Static Analysis và Unit Testing với Pytest và Coverage

Trước khi khởi tạo tiến trình Docker Buildx tốn nhiều tài nguyên CPU và băng thông mạng, công việc đầu tiên (test) đóng vai trò như bộ lọc chất lượng tốc độ cao:

  • Lưu đệm gói phụ thuộc ngôn ngữ (cache: 'pip'): Tích hợp trực tiếp vào actions/setup-python@v5 dựa trên mã băm của tệp requirements.txt, giúp khôi phục môi trường thư viện chỉ trong vài giây.
  • Kiểm tra quy chuẩn mã nguồn với ruff: Phát hiện tức thì các biến không sử dụng, lỗi cú pháp, hoặc vi phạm chuẩn định dạng PEP8.
  • Thực thi kiểm thử đơn vị với pytest: Chạy toàn bộ bộ kiểm thử tự động kèm ngưỡng độ phủ mã nguồn tối thiểu (--cov-fail-under=80). Nếu bất kỳ bài kiểm thử nào thất bại, job test trả về mã lỗi khác 0 và ngăn chặn hoàn toàn việc tạo Docker Image.

3. Tối ưu hóa Tốc độ Build Docker Image với BuildKit & GitHub Actions Cache (type=gha)

3.1 Cơ chế Hoạt động của Docker Buildx và Bộ nhớ Đệm Lớp (Layer Caching type=gha,mode=max)

Một đặc điểm kiến trúc cốt lõi của GitHub Actions là mỗi lần workflow được kích hoạt, hệ thống sẽ cấp phát một máy ảo Ubuntu hoàn toàn mới (Ephemeral Clean VM). Điều này đồng nghĩa với việc bộ nhớ đệm lớp cục bộ của Docker daemon (/var/lib/docker) luôn trống rỗng khi bắt đầu job. Nếu sử dụng lệnh docker build truyền thống, máy chạy sẽ phải tải lại toàn bộ các gói hệ điều hành (apk add / apt-get) và biên dịch lại toàn bộ thư viện ở mọi lần chạy.

Để giải quyết bài toán này, chúng ta sử dụng trình biên dịch thế hệ mới Docker Buildx (dựa trên kiến trúc BuildKit) kết hợp với hậu phương lưu trữ đệm GitHub Actions Cache Backend (type=gha):

  • cache-from: type=gha: Trước khi thực thi từng chỉ thị RUN hoặc COPY trong Dockerfile, BuildKit tính toán mã băm nội dung (Content-addressable Checksum) của lớp đó cùng toàn bộ các lớp cha, sau đó truy vấn trực tiếp vào GitHub Actions Cache API. Nếu mã băm khớp (Cache Hit), BuildKit tải ngay lớp đã biên dịch sẵn về mà không cần chạy lại lệnh.
  • cache-to: type=gha,mode=max: Đây là tham số kỹ thuật quan trọng nhất khi làm việc với Multi-stage Dockerfile. Nếu chỉ để chế độ mặc định (mode=min), BuildKit chỉ lưu đệm các lớp nằm trong giai đoạn cuối cùng (runtime stage), còn toàn bộ các lớp cài đặt trình biên dịch và thư viện nằm ở giai đoạn trung gian (builder stage) sẽ bị bỏ qua. Khi thiết lập tường minh mode=max, BuildKit xuất toàn bộ biểu đồ lớp của cả giai đoạn builder lẫn runtime lên bộ nhớ đệm GitHub, giúp các lần chạy tiếp theo khi chỉ thay đổi mã nguồn nghiệp vụ đạt tỷ lệ trúng bộ nhớ đệm (Cache Hit Ratio) tối đa ở tầng phụ thuộc.

3.2 Chiến lược Định danh Image Bất biến (Immutable Git SHA Tagging vs. latest Anti-Pattern)

Trong vận hành hệ thống Production, việc cấu hình máy chủ kéo hình ảnh theo nhãn infoteck/api-gateway:latest là một phản mẫu kiến trúc (Anti-Pattern) cực kỳ nguy hiểm vì ba lý do:

  1. Mất khả năng tái lập và kiểm toán (Non-reproducible State): Nhãn :latest bị ghi đè liên tục sau mỗi lần build. Khi xảy ra sự cố lúc nửa đêm, kỹ sư trực ca không thể biết chính xác container đang chạy tương ứng với bản commit nào trên Git.
  2. Không thể thực hiện Rollback tức thì: Vì bản build mới đã đè lên cùng một tên nhãn :latest, bạn không có nhãn phiên bản cũ rõ ràng để khôi phục ngay lập tức.
  3. Sai lệch bộ nhớ đệm kéo ảnh (Pull Policy Drift): Nếu cấu hình điều phối không ép buộc kiểm tra mã băm digest mới, các nút máy chủ có thể tiếp tục dùng lại bản :latest cũ đang nằm sẵn trên đĩa.

Giải pháp chuẩn công nghiệp là sử dụng docker/metadata-action@v5 để tự động sinh nhãn bất biến dựa trên mã băm commit rút gọn theo định dạng sha-<7_ky_tu_commit> (ví dụ: ghcr.io/infoteck-org/api-gateway:sha-a1b2c3d), đồng thời nhúng đầy đủ các thuộc tính truy vết nguồn gốc theo đặc tả OpenContainers Initiative (OCI Image Annotations) vào manifest của image.


4. Tích hợp Chốt chặn Bảo mật (DevSecOps Security Gate) với Trivy và Đăng tải lên GHCR

4.1 Quét Lỗ hổng CVE Image Tự động bằng Aqua Security Trivy (exit-code: 1 trên CRITICAL/HIGH)

Một sai lầm phổ biến trong nhiều mẫu cấu hình CI/CD là đẩy thẳng Docker Image lên kho lưu trữ bằng một bước build-push-action duy nhất có push: true, rồi mới chạy công cụ quét bảo mật ở bước sau. Nếu làm như vậy, ngay cả khi bước quét phát hiện lỗ hổng nghiêm trọng và báo đỏ, hình ảnh chứa lỗ hổng đó đã nằm sẵn trên Registry và có nguy cơ bị các hệ thống bên ngoài kéo về sử dụng.

Quy trình DevSecOps chuẩn Production tách quá trình này thành 3 bước chặt chẽ trong cùng một job:

  1. Bước 1 — Build & Load cục bộ (load: true, push: false): Buildx biên dịch hình ảnh (tận dụng tối đa bộ nhớ đệm type=gha) và nạp hình ảnh ứng viên (Candidate Image) vào Docker daemon cục bộ của máy chạy Runner mà chưa đẩy lên mạng.
  2. Bước 2 — Quét lỗ hổng với aquasecurity/[email protected]: Trivy phân tích toàn bộ các gói hệ điều hành (Alpine apk, Debian dpkg) và các tệp khóa thư viện ứng dụng (requirements.txt, package-lock.json, go.sum) bên trong hình ảnh ứng viên. Chúng ta cấu hình severity: 'CRITICAL,HIGH', ignore-unfixed: true (chỉ chặn các lỗ hổng đã có bản vá chính thức từ nhà phát hành), và đặt exit-code: '1'. Nếu phát hiện bất kỳ lỗ hổng CRITICAL hoặc HIGH nào chưa được vá, Trivy lập tức trả về mã lỗi 1 và hủy bỏ job ngay tại chỗ.
  3. Bước 3 — Đăng tải Image sạch lên GHCR (push: true): Chỉ khi bước quét Trivy vượt qua an toàn (Exit Code 0), bước đẩy hình ảnh mới được thực thi. Nhờ bộ nhớ đệm BuildKit cục bộ vừa tạo ở Bước 1, bước đẩy này hoàn tất gần như tức thì mà không phải biên dịch lại bất kỳ dòng mã nào.

Để tìm hiểu sâu hơn về cơ chế cơ sở dữ liệu lỗ hổng CVE, cách cấu hình tệp ngoại lệ .trivyignore có thời hạn và so sánh kiến trúc giữa các trình quét container, bạn hãy đọc bài viết chuyên sâu Bảo mật Container và quét lỗ hổng hình ảnh với Trivy và Clair.

4.2 Xác thực và Đẩy Image lên GitHub Container Registry (GHCR) kèm Metadata Chuẩn OCI

Thay vì phải tạo và quản lý mật khẩu Docker Hub hoặc khóa cá nhân (PAT) trên từng kho lưu trữ, GitHub Container Registry (ghcr.io) cho phép xác thực trực tiếp bằng biến ${{ secrets.GITHUB_TOKEN }} thông qua docker/login-action@v3. Đối với các mật khẩu cơ sở dữ liệu ứng dụng, khóa ký JWT hoặc chứng chỉ TLS cần nạp vào môi trường vận hành thực tế bên ngoài phạm vi CI/CD, doanh nghiệp nên kết hợp thêm giải pháp quản lý khóa tập trung được hướng dẫn tại bài viết Quản lý Secret và khóa mã hóa an toàn với HashiCorp Vault.

Dưới đây là toàn bộ nội dung tệp cấu hình .github/workflows/production-cicd.yml (Code Module #1) tích hợp đầy đủ 3 job (test, build-scan-push, và deploy-production):

# File: .github/workflows/production-cicd.yml
name: Production CI/CD Pipeline (Buildx + Trivy + Atomic Rollout)

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

# Khóa quyền mặc định của GITHUB_TOKEN ở mức tối thiểu (Read-Only)
permissions:
  contents: read

# Kiểm soát tương tranh: Hủy các bản chạy PR cũ bị trùng, nhưng bảo vệ hàng đợi trên nhánh main
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # ===================================================================
  # JOB 1: CONTINUOUS INTEGRATION (LINTING & UNIT TESTS)
  # ===================================================================
  test:
    name: CI - Lint & Unit Test Suite
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    steps:
      - name: Checkout Source Code
        uses: actions/checkout@v4

      - name: Set up Python 3.11 with Pip Cache
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: "pip"
          cache-dependency-path: "**/requirements*.txt"

      - name: Install Application & Test Dependencies
        run: |
          python -m pip install --upgrade pip
          python -m pip install ruff==0.7.3 pytest==8.3.3 pytest-cov==5.0.0
          if [ -f requirements.txt ]; then python -m pip install -r requirements.txt; fi

      - name: Run Static Linter & Formatting Check (Ruff)
        run: |
          ruff check .
          ruff format --check .

      - name: Execute Unit Tests with Coverage Enforcement
        run: |
          pytest tests/ --maxfail=1 --disable-warnings -q --cov=app --cov-report=term-missing --cov-fail-under=80

  # ===================================================================
  # JOB 2: BUILDX CACHE, TRIVY CVE GATE & GHCR PUBLISH
  # ===================================================================
  build-scan-push:
    name: DevSecOps - Buildx, Trivy Scan & Push GHCR
    needs: [test]
    runs-on: ubuntu-24.04
    timeout-minutes: 15
    permissions:
      contents: read
      packages: write
      security-events: write
    outputs:
      immutable_tag: ${{ steps.vars.outputs.immutable_tag }}
    steps:
      - name: Checkout Source Code
        uses: actions/checkout@v4

      - name: Compute Immutable Short SHA Tag
        id: vars
        run: |
          SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
          echo "immutable_tag=sha-${SHORT_SHA}" >> "${GITHUB_OUTPUT}"

      - name: Set up Docker Buildx Builder
        uses: docker/setup-buildx-action@v3

      - name: Extract OCI Metadata & Tags for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=sha-,format=short
            type=raw,value=stable,enable=${{ github.ref == 'refs/heads/main' }}

      - name: Build Candidate Image into Local Daemon (Using GHA Layer Cache)
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./Dockerfile
          load: true
          push: false
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.immutable_tag }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Run Aqua Security Trivy Vulnerability Gate (Block CRITICAL & HIGH)
        uses: aquasecurity/[email protected]
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.immutable_tag }}
          format: "table"
          exit-code: "1"
          ignore-unfixed: true
          vuln-type: "os,library"
          severity: "CRITICAL,HIGH"

      - name: Authenticate to GitHub Container Registry (GHCR)
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Push Verified Image to GHCR
        if: github.event_name != 'pull_request'
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./Dockerfile
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha

  # ===================================================================
  # JOB 3: ATOMIC PRODUCTION DEPLOYMENT WITH HEALTHCHECK ROLLBACK
  # ===================================================================
  deploy-production:
    name: CD - Atomic Production Rollout
    needs: [build-scan-push]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    environment:
      name: production
      url: https://api.infoteck.vn
    steps:
      - name: Execute Atomic Rollout & Healthcheck Verification via SSH
        uses: appleboy/[email protected]
        env:
          DEPLOY_IMAGE_TAG: ${{ needs.build-scan-push.outputs.immutable_tag }}
          GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GHCR_USER: ${{ github.actor }}
        with:
          host: ${{ secrets.PROD_SSH_HOST }}
          username: ${{ secrets.PROD_SSH_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          port: ${{ secrets.PROD_SSH_PORT }}
          envs: DEPLOY_IMAGE_TAG,GHCR_TOKEN,GHCR_USER
          command_timeout: 5m
          script: |
            set -euo pipefail
            cd /opt/infoteck/api-gateway
            echo "${GHCR_TOKEN}" | docker login ghcr.io -u "${GHCR_USER}" --password-stdin
            ./scripts/deploy-rollout.sh "${DEPLOY_IMAGE_TAG}"


5. Tự động hóa Continuous Deployment (CD) An toàn với GitHub Environments và Healthcheck Rollback

5.1 Bảo vệ Môi trường Production bằng GitHub Environments, Protection Rules và SSH Bastion

Trong Job 3 (deploy-production) ở trên, thuộc tính environment: name: production kích hoạt cơ chế bảo vệ cấp doanh nghiệp của GitHub Actions:

  1. Tách biệt Biến Bí mật theo Môi trường (Environment-Scoped Secrets): Các khóa nhạy cảm như PROD_SSH_KEY, PROD_SSH_HOST và PROD_SSH_USER không được lưu ở cấp độ Repository chung mà chỉ nằm trong vùng production. Một nhánh tính năng hoặc Pull Request từ bên ngoài không bao giờ có thể đọc được các biến này.
  2. Cổng Phê duyệt Thủ công & Giới hạn Nhánh (Required Reviewers & Deployment Branches): Trong phần cài đặt Settings -> Environments -> production, quản trị viên cấu hình chỉ cho phép nhánh main được kích hoạt môi trường này, đồng thời có thể yêu cầu phê duyệt từ Tech Lead trước khi lệnh SSH được phép thực thi.

5.2 Thiết kế Docker Compose Production với Healthcheck và Quản lý Tài nguyên

Để kịch bản triển khai biết chính xác khi nào container mới đã sẵn sàng nhận lưu lượng truy cập thực tế (thay vì chỉ kiểm tra trạng thái tiến trình Up vốn chưa đảm bảo ứng dụng bên trong đã khởi tạo xong kết nối cơ sở dữ liệu), tệp cấu hình docker-compose.prod.yml bắt buộc phải định nghĩa chỉ thị healthcheck rõ ràng.

Đồng thời, chúng ta áp dụng các biện pháp gia cố bảo mật runtime: gắn hệ thống tệp gốc ở chế độ chỉ đọc (read_only: true), cấp vùng nhớ tạm trên RAM (tmpfs: /tmp), giới hạn trần CPU/RAM để chống cạn kiệt tài nguyên máy chủ, và xoay vòng tệp nhật ký (max-size: "20m", max-file: "5"). Khi quy mô hệ thống của bạn phát triển vượt quá khả năng chịu tải của một máy chủ Docker Compose đơn lẻ và cần chuyển sang điều phối đa nút tự động co giãn, hãy tham khảo lộ trình kiến trúc tại bài viết Kiến trúc Microservices với Docker & Kubernetes: Hướng dẫn thực chiến từ A-Z.

Dưới đây là tệp cấu hình docker-compose.prod.yml (Code Module #2) đặt tại thư mục /opt/infoteck/api-gateway/ trên máy chủ Production:

# File: docker-compose.prod.yml
services:
  api-gateway:
    image: ghcr.io/infoteck-org/api-gateway:${IMAGE_TAG:?Bien moi truong IMAGE_TAG bat buoc phai duoc khai bao}
    container_name: infoteck-api-gateway
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    env_file:
      - /opt/infoteck/api-gateway/.env.production
    environment:
      - APP_ENV=production
      - PORT=8080
    read_only: true
    tmpfs:
      - /tmp:rw,noexec,nosuid,size=64m
    security_opt:
      - no-new-privileges:true
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8080/healthz"]
      interval: 5s
      timeout: 3s
      retries: 3
      start_period: 10s
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 1024M
        reservations:
          cpus: "0.5"
          memory: 256M
    logging:
      driver: "json-file"
      options:
        max-size: "20m"
        max-file: "5"

5.3 Kịch bản Triển khai Nguyên tử (Atomic Rollout) và Tự động Rollback khi Healthcheck Thất bại

Nếu chỉ chạy lệnh docker compose up -d trong CI/CD, lệnh này sẽ trả về mã thành công ngay khi Docker tạo xong container, ngay cả khi ứng dụng bên trong lập tức văng lỗi CrashLoop sau đó 3 giây do thiếu biến môi trường hoặc lỗi di trú cơ sở dữ liệu.

Kịch bản scripts/deploy-rollout.sh (Code Module #3) dưới đây giải quyết triệt để bài toán này bằng quy trình 4 bước nguyên tử:

  1. Truy vấn nhãn hình ảnh hiện tại đang chạy ổn định và lưu vào biến PREVIOUS_TAG.
  2. Tải trước hình ảnh mới (docker compose pull) và cập nhật container (up -d --remove-orphans).
  3. Vòng lặp kiểm tra trạng thái .State.Health.Status của container thông qua lệnh docker inspect mỗi 5 giây (tối đa 12 lần tương đương 60 giây). Nếu phát hiện trạng thái healthy, kịch bản dọn dẹp các hình ảnh cũ không còn sử dụng và kết thúc thành công.
  4. Nếu quá 60 giây hoặc trạng thái chuyển sang unhealthy, kịch bản lập tức in ra 30 dòng nhật ký lỗi cuối cùng của container để kỹ sư chẩn đoán trên giao diện GitHub Actions, sau đó tự động kích hoạt lại docker compose up -d với IMAGE_TAG="${PREVIOUS_TAG}" để phục hồi dịch vụ về bản ổn định trước đó rồi mới trả về mã lỗi exit 1.

#!/usr/bin/env bash
# File: scripts/deploy-rollout.sh
# Mô tả: Kịch bản triển khai nguyên tử (Atomic Rollout) có kiểm tra Healthcheck
# và tự động Rollback về phiên bản ổn định liền trước nếu khởi chạy thất bại.

set -euo pipefail

COMPOSE_FILE="docker-compose.prod.yml"
CONTAINER_NAME="infoteck-api-gateway"
NEW_TAG="${1:-}"

if [[ -z "${NEW_TAG}" ]]; then
  echo "[ERROR] Cú pháp sử dụng: ./scripts/deploy-rollout.sh <immutable_image_tag>" >&2
  exit 1
fi

echo "=== [1/4] GHI NHẬN TRẠNG THÁI PHIÊN BẢN HIỆN TẠI ==="
PREVIOUS_IMAGE=""
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
  PREVIOUS_IMAGE=$(docker inspect --format='{{.Config.Image}}' "${CONTAINER_NAME}" || true)
fi

PREVIOUS_TAG=""
if [[ -n "${PREVIOUS_IMAGE}" && "${PREVIOUS_IMAGE}" == *":"* ]]; then
  PREVIOUS_TAG="${PREVIOUS_IMAGE##*:}"
  echo "[INFO] Phiên bản đang vận hành ổn định hiện tại: ${PREVIOUS_TAG}"
else
  echo "[INFO] Chưa có container '${CONTAINER_NAME}' nào đang chạy (Triển khai lần đầu)."
fi

echo "=== [2/4] TẢI VÀ KÍCH HOẠT PHIÊN BẢN MỚI (${NEW_TAG}) ==="
export IMAGE_TAG="${NEW_TAG}"
docker compose -f "${COMPOSE_FILE}" pull
docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans

echo "=== [3/4] KIỂM TRA HEALTHCHECK TRONG TỐI ĐA 60 GIÂY ==="
MAX_ATTEMPTS=12
SLEEP_SECONDS=5
HEALTHY=false

for ((attempt = 1; attempt <= MAX_ATTEMPTS; attempt++)); do
  STATUS=$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "${CONTAINER_NAME}" 2>/dev/null || echo "missing")
  echo "[HEALTHCHECK] Lần thử ${attempt}/${MAX_ATTEMPTS}: Trạng thái container = '${STATUS}'"

  if [[ "${STATUS}" == "healthy" ]]; then
    HEALTHY=true
    break
  elif [[ "${STATUS}" == "unhealthy" || "${STATUS}" == "missing" ]]; then
    echo "[WARN] Phát hiện container chuyển sang trạng thái lỗi ('${STATUS}')."
    break
  fi
  sleep "${SLEEP_SECONDS}"
done

if [[ "${HEALTHY}" == "true" ]]; then
  echo "=== [4/4] ROLLOUT THÀNH CÔNG! DỌN DẸP IMAGE CŨ KHÔNG SỬ DỤNG ==="
  docker image prune -f --filter "until=72h"
  echo "[SUCCESS] Dịch vụ '${CONTAINER_NAME}' đã vận hành ổn định trên phiên bản '${NEW_TAG}'."
  exit 0
fi

echo "=== [4/4] CẢNH BÁO: ROLLOUT THẤT BẠI! TRÍCH XUẤT LOG LỖI CONTAINER ===" >&2
docker logs --tail 30 "${CONTAINER_NAME}" >&2 || true

if [[ -n "${PREVIOUS_TAG}" && "${PREVIOUS_TAG}" != "${NEW_TAG}" ]]; then
  echo "[ROLLBACK] Đang tự động khôi phục dịch vụ về phiên bản ổn định trước đó: '${PREVIOUS_TAG}'..." >&2
  export IMAGE_TAG="${PREVIOUS_TAG}"
  docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans
  echo "[ROLLBACK] Đã kích hoạt lại phiên bản '${PREVIOUS_TAG}'. Báo lỗi về GitHub Actions." >&2
else
  echo "[ROLLBACK] Không có phiên bản trước đó để khôi phục." >&2
fi

exit 1


6. Kiểm tra Thực thi Pipeline bằng GitHub CLI (gh) và Xử lý Sự cố Thường gặp

6.1 Giám sát Log Workflow và Kiểm tra Cache Hit Ratio qua GitHub CLI (gh run)

Sau khi đẩy cấu hình .github/workflows/production-cicd.yml lên kho lưu trữ, kỹ sư DevOps có thể kiểm tra trực tiếp trạng thái từng job, dung lượng bộ nhớ đệm lớp BuildKit (type=gha) và thời gian thực thi ngay từ cửa sổ dòng lệnh bằng công cụ chính thức GitHub CLI (gh) mà không cần rời khỏi trình soạn thảo mã nguồn.

Tệp kịch bản kiểm chứng verify-pipeline.sh (Code Module #4) dưới đây tổng hợp các lệnh chẩn đoán quan trọng nhất cùng kết quả đầu ra thực tế khi pipeline vận hành chuẩn xác:

#!/usr/bin/env bash
# File: verify-pipeline.sh
# Mô tả: Bộ lệnh kiểm chứng trạng thái CI/CD Pipeline và bộ nhớ đệm BuildKit qua GitHub CLI (gh).

set -euo pipefail

echo "1. Liệt kê 3 phiên chạy gần nhất của workflow Production CI/CD:"
gh run list --workflow="production-cicd.yml" --limit 3

echo -e "n2. Kiểm tra danh sách các bản lưu bộ nhớ đệm (Pip & BuildKit Layer Cache):"
gh cache list --limit 5

echo -e "n3. Xem chi tiết trạng thái các Job của phiên chạy mới nhất trên nhánh main:"
LATEST_RUN_ID=$(gh run list --workflow="production-cicd.yml" --branch main --limit 1 --json databaseId --jq '.[0].databaseId')
gh run view "${LATEST_RUN_ID}"

# -------------------------------------------------------------------------
# KẾT QUẢ ĐẦU RA THỰC TẾ TRÊN TERMINAL KHI KIỂM CHỨNG:
# -------------------------------------------------------------------------
# 1. Liệt kê 3 phiên chạy gần nhất của workflow Production CI/CD:
# STATUS  TITLE                                      WORKFLOW                  BRANCH  EVENT  ID          ELAPSED  AGE
# ✓       feat(api): add rate limit headers          Production CI/CD Pipeline main    push   11678902341 1m42s    2m
# ✓       fix(auth): validate token exp claim        Production CI/CD Pipeline feat/jw pull_request 11678810922 58s 18m
#
# 2. Kiểm tra danh sách các bản lưu bộ nhớ đệm (Pip & BuildKit Layer Cache):
# ID          KEY                                                              SIZE     CREATED
# 982341102   Linux-buildkit-fe90a12c88b19402e7182a...                         148 MB   2m ago
# 982340881   Linux-pip-64a19b002c8174f12e901a88c4...                          42 MB    18m ago
#
# 3. Xem chi tiết trạng thái các Job của phiên chạy mới nhất trên nhánh main:
# ✓ main · Production CI/CD Pipeline · 11678902341
# Triggered via push about 2 minutes ago
#
# JOBS
# ✓ CI - Lint & Unit Test Suite in 28s (ID 32510091821)
# ✓ DevSecOps - Buildx, Trivy Scan & Push GHCR in 41s (ID 32510094510)
# ✓ CD - Atomic Production Rollout in 33s (ID 32510099104)

6.2 Xử lý Sự cố Tràn Dung lượng Cache GitHub Actions (10GB Limit) và Lỗi Quyền GHCR Packages

Trong quá trình vận hành thực tế ở các dự án có tần suất commit dày đặc, đội ngũ kỹ thuật thường gặp hai sự cố điển hình liên quan đến GitHub Actions Cache và quyền ghi gói tin trên GHCR:

  1. Sự cố tràn hạn mức 10GB của GitHub Actions Cache (Cache Thrashing): Mỗi kho lưu trữ trên GitHub được cấp hạn mức lưu trữ bộ nhớ đệm tối đa là 10 GB. Khi sử dụng cache-to: type=gha,mode=max trên các hình ảnh có nhiều lớp nặng, mỗi khi tệp khóa phụ thuộc thay đổi, BuildKit sẽ sinh ra các bản lưu đệm mới. Khi tổng dung lượng vượt quá 10 GB, GitHub tự động xóa các bản lưu ít được truy cập nhất (LRU Eviction), đôi khi làm mất cả bộ nhớ đệm của nhánh main.
    • Cách khắc phục: Chỉ cho phép ghi đè bộ nhớ đệm (cache-to: type=gha,mode=max) khi chạy trên nhánh main, còn các nhánh Pull Request chỉ đọc bộ nhớ đệm (cache-from: type=gha) mà không ghi mới, hoặc sử dụng hành động dọn dẹp tự động các bản lưu đệm của nhánh PR ngay khi PR được đóng lại (closed).
  2. Sự cố lỗi 403 Forbidden: installation not allowed to Write organization package khi đẩy lên GHCR: Ngay cả khi bạn đã khai báo permissions: packages: write trong tệp YAML, lần chạy đầu tiên đẩy hình ảnh lên một tổ chức (GitHub Organization) vẫn có thể bị từ chối.
    • Cách khắc phục: Truy cập vào phần cài đặt của kho lưu trữ (Settings -> Actions -> General -> Workflow permissions) hoặc cài đặt cấp Organization (Settings -> Packages -> Package creation), đảm bảo tùy chọn cho phép GitHub Actions tạo và ghi gói tin thông qua GITHUB_TOKEN đã được bật, đồng thời liên kết gói container vừa tạo trên GHCR với đúng kho lưu trữ mã nguồn.

7. Tổng kết & Tài liệu tham khảo (References)

7.1 Checklist Chuẩn hóa CI/CD Pipeline cho Đội ngũ Kỹ thuật

Trước khi đưa một đường ống CI/CD vào phục vụ hệ thống Production, hãy đối chiếu toàn bộ cấu hình của bạn với bảng kiểm tra kỹ thuật dưới đây:

Hạng mục Kiểm soát Cấu hình Kỹ thuật Bắt buộc Mục tiêu Đảm bảo An toàn & Hiệu năng
Phân quyền Token (permissions) Đặt contents: read ở cấp toàn cục; chỉ cấp packages: write riêng trong job build Ngăn chặn rò rỉ quyền ghi mã nguồn hoặc đẩy gói tin trái phép từ các bước chạy thử nghiệm
Chống xung đột (concurrency) Thiết lập cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} Hủy ngay các bản chạy PR thừa nhưng bảo vệ tuyệt đối tiến trình deploy đang chạy trên nhánh main
Lưu đệm BuildKit (type=gha) Sử dụng docker/setup-buildx-action@v3 kèm cache-to: type=gha,mode=max Lưu đệm toàn bộ các lớp trung gian của Multi-stage Dockerfile, giảm tối đa thời gian biên dịch
Định danh Tạo tác (Immutable Tag) Gắn nhãn theo mã commit sha-<short_sha> thay vì chỉ dùng nhãn :latest Đảm bảo khả năng truy vết 1-1 giữa container đang chạy với lịch sử mã nguồn và hỗ trợ Rollback tức thì
Chốt chặn Bảo mật (Trivy Gate) Quét hình ảnh cục bộ (load: true) với exit-code: "1" trên mức CRITICAL,HIGH Chặn tuyệt đối các hình ảnh chứa lỗ hổng bảo mật đã có bản vá trước khi đẩy lên GHCR
Triển khai Nguyên tử (Atomic CD) Kiểm tra .State.Health.Status == "healthy" và tự động khôi phục về PREVIOUS_TAG khi lỗi Loại bỏ nguy cơ thời gian chết kéo dài khi phiên bản mới gặp lỗi cấu hình lúc khởi động

7.2 Tài liệu tham khảo & Nguồn đối chiếu Chính thống

  1. GitHub Actions Official Documentation. Workflow syntax for GitHub Actions (permissions, concurrency, environments). Truy cập tại: https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions
  2. Docker BuildKit Official Documentation. GitHub Actions cache backend (type=gha,mode=max). Truy cập tại: https://docs.docker.com/build/cache/backends/gha/
  3. GitHub Packages Documentation. Working with the Container registry (ghcr.io & GITHUB_TOKEN Authentication). Truy cập tại: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
  4. Aqua Security Trivy Documentation. Trivy GitHub Action (aquasecurity/trivy-action CI/CD Gatekeeper). Truy cập tại: https://github.com/aquasecurity/trivy-action
  5. OpenContainers Initiative (OCI). OCI Image Format Specification — Pre-Defined Annotation Keys. Truy cập tại: https://github.com/opencontainers/image-spec/blob/main/annotations.md