CI pipeline: build một lần, deploy nhiều nơi
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 đề
- Không tái lập được: build trên laptop với JDK, file local, branch chưa push của từng người.
- Tag
latest: không biết prod đang chạy code nào, rollback về đâu. - Bỏ test: bug lên thẳng prod.
- Build lại cho từng môi trường: image chạy prod khác image đã test ở staging.
- Credential registry nằm trên laptop.
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ắc | Nghĩa |
|---|---|
| Build once, deploy many | Một image duy nhất đi qua dev → staging → prod; chỉ config khác nhau |
| Artifact versioning | Tag theo git SHA (bất biến) + semver khi release; không deploy latest |
| Fail fast | Bước rẻ (compile, unit test) chạy trước bước đắt |
| Cache | Cache ~/.m2 và Docker layer để pipeline nhanh |
| Secrets ngoài code | Dù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 | Ưu | Nhược |
|---|---|---|
| Hardcode trong YAML | — | Lộ trong git mãi mãi. Không bao giờ. |
CI secrets (secrets.X) | Mã hóa, che trong log | Key dài hạn, phải rotate tay |
| OIDC federation | Không lưu key; token ngắn hạn, gắn với repo/branch | Phải cấu hình trust trên cloud |
| Vault / Secrets Manager | Tập trung, audit, rotate | Thê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
- Nên: mọi service có pipeline riêng, trigger theo path; PR phải xanh mới merge.
- Không nên: build lại image ở mỗi môi trường, tag
latestcho deploy, chạy E2E dài 40 phút trên mỗi PR (đẩy sang nightly). - Không nên: dùng chung một secret "admin" cho mọi job; mỗi job chỉ quyền cần thiết.
order-service:3f9c2ab rồi. Đưa nó lên prod thế nào để người dùng không thấy lỗi, và lỡ hỏng thì quay về trong vài giây?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:
- Mỗi build trên main: tag
:3f9c2ab(bất biến, truy ngược code chính xác). - Khi release: tạo git tag
v1.5.0, CI gắn thêm tag:1.5.0cho cùng digest, không build lại.
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:
- Chỉ build cái đổi:
pathsfilter +mvn -pl order-service -am. - Song song: tách unit test và integration test thành 2 job chạy cùng lúc; matrix theo service.
- Docker layer cache: Dockerfile tách layer dependency với layer code (Spring Boot layered jar), dùng
cache-from: type=ghacủa buildx. - Testcontainers: tái dùng container trong một lần chạy, không start Postgres mới cho mỗi class.
- Đ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
- build → test → scan → image → push; rẻ trước, đắt sau.
- Một image, tag git SHA, đi qua mọi môi trường.
- Không
latest, không build lại theo môi trường. - Secret: OIDC > CI secrets >> hardcode.
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.