CI pipeline: build một lần, deploy nhiều nơi

4 câu hỏi~25 phútTừ khóa: GitHub Actions, stage, cache, artifact, image tag, git SHA, build once deploy many, OIDC, secrets, Trivy

Tiếp đi: CI pipeline. Hiện mỗi lần release là một bạn build image trên laptop rồi docker push lên registry.

Bài trước code đã vào main gọn gàng. Bài này tự động hóa mọi thứ từ lúc merge tới khi có một image sẵn sàng deploy.

1. Bài toán

Release order-service hiện làm tay:

mvn package -DskipTests        # "test chạy lâu quá"
docker build -t shop/order-service:latest .
docker push shop/order-service:latest

2. Vấn đề

3. Giải pháp: pipeline CI

push/PR ─► build ─► test ─► scan ─► image ─► push registry
            mvn      unit +    SAST,     docker     ghcr.io/shop/order-service:<git-sha>
            compile  IT        deps CVE  build      (bất biến)
Nguyên tắcNghĩa
Build once, deploy manyMột image duy nhất đi qua dev → staging → prod; chỉ config khác nhau
Artifact versioningTag theo git SHA (bất biến) + semver khi release; không deploy latest
Fail fastBước rẻ (compile, unit test) chạy trước bước đắt
CacheCache ~/.m2 và Docker layer để pipeline nhanh
Secrets ngoài codeDùng secret store của CI, tốt nhất là OIDC thay vì key dài hạn

Quality gate (Surefire/Failsafe, JaCoCo, phân tích tĩnh) đã có ở bài 5.8; ở đây chỉ gọi lại mvn verify.

4. Code từng bước (GitHub Actions)

Bước 1: chỉ build service bị thay đổi trong monorepo

# .github/workflows/order-service.yml
name: order-service
on:
  push:
    branches: [main]
    paths: ["order-service/**", "shared-lib/**"]
  pull_request:
    paths: ["order-service/**", "shared-lib/**"]

permissions:
  contents: read
  packages: write
  id-token: write          # cho OIDC

Bước 2: build + test với cache Maven

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "21"
          cache: maven                 # cache ~/.m2 theo hash pom.xml
      - run: ./mvnw -B -pl order-service -am verify

Bước 3: scan, build image, push với tag SHA

      - name: Build image
        run: |
          docker build -t ghcr.io/shop/order-service:${{ github.sha }} order-service
      - name: Scan image
        uses: aquasecurity/trivy-action@0.24.0
        with:
          image-ref: ghcr.io/shop/order-service:${{ github.sha }}
          severity: CRITICAL,HIGH
          exit-code: "1"
      - name: Push
        if: github.ref == 'refs/heads/main'
        run: |
          echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
          docker push ghcr.io/shop/order-service:${{ github.sha }}

PR chỉ build + test + scan; chỉ main mới push image. Cách viết Dockerfile layered cho cache tốt xem bài 16.2.

Bước 4: version trong app để kiểm tra

<!-- pom.xml: spring-boot-maven-plugin goal build-info -->
# application.yml
management.info.git.mode: simple
curl -s https://staging.shop.example/actuator/info | jq .git.commit.id
# "3f9c2ab"   → khớp tag image đang chạy

5. Cách hoạt động: build once, deploy many

commit 3f9c2ab ─► CI ─► image order-service:3f9c2ab  (digest sha256:ab12…)
                                 │
          ┌──────────────────────┼───────────────────────┐
          ▼                      ▼                       ▼
        dev                   staging                   prod
   values-dev.yaml       values-staging.yaml      values-prod.yaml
   (cùng image, khác ConfigMap/Secret, khác replicas)

Image chứa code, không chứa config môi trường. Config đi qua ConfigMap/Secret (bài 17.5), values Helm (bài 17.8).

6. So sánh cách quản lý secret trong CI

CáchƯuNhược
Hardcode trong YAML—Lộ trong git mãi mãi. Không bao giờ.
CI secrets (secrets.X)Mã hóa, che trong logKey dài hạn, phải rotate tay
OIDC federationKhông lưu key; token ngắn hạn, gắn với repo/branchPhải cấu hình trust trên cloud
Vault / Secrets ManagerTập trung, audit, rotateThêm thành phần vận hành
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/shop-ci-push
          aws-region: ap-southeast-1      # không có access key nào trong repo

7. Khi nào nên / không nên

Tag theo git SHA thì xấu, khách hỏi "đang chạy bản nào" mình đâu trả lời 3f9c2ab được. Sao không dùng semver luôn?

Đúng là SHA không thân thiện, nhưng hai cái không loại trừ nhau. Cách thường làm:

docker buildx imagetools create -t ghcr.io/shop/order-service:1.5.0 \
  ghcr.io/shop/order-service:3f9c2ab

Manifest deploy nên ghi SHA hoặc digest, vì tag semver vẫn có thể bị ai đó push đè. Bật immutable tag trên registry (ECR, ACR đều có) để chặn việc đó.

Pipeline đang mất 15 phút, cache Maven rồi mà vẫn chậm. Còn chỗ nào tăng tốc được?

Cache ~/.m2 chỉ bỏ được thời gian tải dependency. Thường thời gian nằm ở chỗ khác:

  1. Chỉ build cái đổi: paths filter + mvn -pl order-service -am.
  2. Song song: tách unit test và integration test thành 2 job chạy cùng lúc; matrix theo service.
  3. Docker layer cache: Dockerfile tách layer dependency với layer code (Spring Boot layered jar), dùng cache-from: type=gha của buildx.
  4. Testcontainers: tái dùng container trong một lần chạy, không start Postgres mới cho mỗi class.
  5. Đo trước khi tối ưu: xem job nào lâu nhất trong tab Actions.

Mục tiêu tham khảo: PR feedback dưới 10 phút. Hơn thế dev sẽ bắt đầu gộp nhiều thay đổi vào một PR, đi ngược trunk-based.

Phỏng vấn hỏi về CI pipeline thì trả lời sao?

Q1. Mô tả CI pipeline của dự án bạn.

🇻🇳 Tiếng Việt:

Mỗi service có workflow GitHub Actions trigger theo path. PR chạy compile, unit test, integration test với Testcontainers, phân tích tĩnh, scan dependency. Merge vào main thì build Docker image, scan CVE bằng Trivy, push lên registry với tag là git SHA. Branch protection bắt buộc pipeline xanh.

🇬🇧 English:

Each service has a GitHub Actions workflow triggered by path. Pull requests run compile, unit tests, Testcontainers integration tests, static analysis and dependency scanning. On merge to main we build the Docker image, scan it with Trivy and push it to the registry tagged with the git SHA. Branch protection requires a green pipeline.

Q2. "Build once, deploy many" là gì, vì sao quan trọng?

🇻🇳 Tiếng Việt:

Build một artifact bất biến duy nhất rồi promote qua các môi trường, chỉ thay config. Như vậy thứ chạy trên prod đúng là thứ đã test ở staging, rollback chỉ là đổi về tag cũ, và truy ngược được commit từ image.

🇬🇧 English:

We build one immutable artifact and promote it through environments, changing only configuration. What runs in production is exactly what was tested in staging, rollback is just pointing to a previous tag, and every image traces back to a commit.

Q3. Bạn quản lý secret trong CI thế nào?

🇻🇳 Tiếng Việt:

Không bao giờ để trong repo. Dùng secret store của CI cho số ít giá trị, và ưu tiên OIDC để CI nhận token ngắn hạn từ AWS/Azure thay vì lưu access key. Mỗi job chỉ có quyền tối thiểu.

🇬🇧 English:

Never in the repository. We use the CI secret store for a few values and prefer OIDC federation so the pipeline gets short-lived tokens from AWS or Azure instead of storing access keys. Each job gets least-privilege permissions.

Cách nhớ ngắn gọn


Tự kiểm tra

Vì sao không deploy tag latest?

Tag thay đổi theo thời gian, không biết đang chạy code nào, không rollback chính xác được, node khác nhau có thể kéo bản khác nhau.

Image có nên chứa URL database của prod không?

Không. Image giống nhau cho mọi môi trường; config qua env/ConfigMap/Secret.

Monorepo làm sao để sửa payment-service không build lại order-service?

Trigger theo paths và build module bằng mvn -pl ... -am.

OIDC trong CI giải quyết gì?

CI đổi token định danh của job lấy credential ngắn hạn từ cloud, không cần lưu access key dài hạn.

Gắn tag semver cho release có cần build lại không?

Không, chỉ gắn thêm tag cho cùng digest đã build và test.