Phần 3 – Engineering | Chương trước: Kubernetes | Chương sau: Observability
Factor 5 định nghĩa ba giai đoạn build–release–run; chương này xây cỗ máy thi hành chúng: pipeline CI/CD hoàn chỉnh với GitHub Actions, và mô hình GitOps cho phần deploy.
1. Bản chất: CI/CD là gì — tách bạch hai khái niệm
- CI (Continuous Integration): mọi thay đổi được tích hợp vào trunk thường xuyên, và mỗi lần tích hợp được kiểm chứng tự động (build + test). CI là về niềm tin vào trạng thái của trunk: trunk luôn ở trạng thái ship được.
- CD có hai nghĩa cần phân biệt: Continuous Delivery — mọi commit qua CI đều sẵn sàng deploy (artifact + quy trình tự động, người bấm nút); Continuous Deployment — bỏ luôn nút bấm, mọi commit xanh tự lên production. Đa số tổ chức trưởng thành dừng ở Delivery cho production và Deployment cho staging — đó là lựa chọn quản trị rủi ro, không phải mức độ “tiến hóa”.
Điểm nối với 12-Factor: pipeline chính là ranh giới cưỡng chế của các factor — codebase (chỉ build từ Git), dependencies (verify trong CI), build/release/run (mỗi stage một job), parity (cùng artifact đi xuyên môi trường). Kỷ luật con người thất bại theo thời gian; kỷ luật pipeline thì không.
2. Pipeline hoàn chỉnh — GitHub Actions
# .github/workflows/ci.yml
name: ci
on:
pull_request:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE: ghcr.io/${{ github.repository }}
jobs:
# ---------- STAGE 1: VERIFY — chạy cho cả PR lẫn main ----------
verify:
runs-on: ubuntu-latest
services: # backing service THẬT cho integration test (F10)
postgres:
image: postgres:17.4-alpine
env: { POSTGRES_USER: test, POSTGRES_PASSWORD: test, POSTGRES_DB: test }
options: >-
--health-cmd pg_isready --health-interval 2s --health-retries 15
ports: ["5432:5432"]
redis:
image: redis:7.2-alpine
ports: ["6379:6379"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version-file: go.mod, cache: true }
- name: Dependencies hygiene (F2)
run: |
go mod tidy && git diff --exit-code go.mod go.sum
go mod verify
- name: Lint
uses: golangci/golangci-lint-action@v6
- name: Unit + integration tests
env:
DATABASE_URL: postgres://test:test@localhost:5432/test?sslmode=disable
REDIS_URL: redis://localhost:6379/0
run: go test -race -count=1 -coverprofile=cover.out ./...
- name: Vulnerability scan (F2)
run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
# ---------- STAGE 2: BUILD ARTIFACT — chỉ trên main (F5) ----------
build:
needs: verify
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions: { contents: read, packages: write, id-token: write }
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & push (tag = commit SHA — F1+F5)
id: push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ env.IMAGE }}:${{ github.sha }}
build-args: |
COMMIT_SHA=${{ github.sha }}
VERSION=${{ github.ref_name }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.IMAGE }}:${{ github.sha }}
exit-code: "1"
severity: CRITICAL,HIGH
# Ký image + SBOM: chuỗi cung ứng kiểm chứng được (cosign keyless)
- name: Sign image
run: cosign sign --yes ${{ env.IMAGE }}@${{ steps.push.outputs.digest }}
# ---------- STAGE 3: RELEASE — cập nhật repo GitOps (xem mục 3) ----------
release-staging:
needs: build
runs-on: ubuntu-latest
steps:
- name: Update GitOps repo (staging tự động — Continuous Deployment)
run: |
# clone repo gitops, cập nhật digest trong overlays/staging, commit, push
# (dùng deploy key/app token có quyền hẹp)
yq -i '.images[0].digest = "${{ needs.build.outputs.digest }}"' \
overlays/staging/kustomization.yaml
git commit -am "staging: myapp ${{ github.sha }}" && git push
release-production:
needs: release-staging
environment: production # GitHub Environments: bắt buộc phê duyệt —
runs-on: ubuntu-latest # Continuous DELIVERY: người bấm nút
steps:
- name: Update GitOps repo (production)
run: |
yq -i '.images[0].digest = "${{ needs.build.outputs.digest }}"' \
overlays/production/kustomization.yaml
git commit -am "production: myapp ${{ github.sha }}" && git push
Những đường nét quan trọng: PR chỉ verify (không build artifact — tiết kiệm, và artifact chỉ sinh từ trunk); một artifact duy nhất đi từ staging đến production (chỉ digest được sao chép giữa hai overlay — F5/F10 bằng cơ chế, không bằng lời hứa); quyền hẹp (job build không có quyền deploy; token GitOps chỉ ghi được repo config).
3. GitOps — deploy bằng pull, không phải push
Mô hình truyền thống (push): CI có credential của cluster, chạy kubectl apply. Vấn đề: CI trở thành chỗ chứa chìa khóa vạn năng của production; trạng thái cluster trôi dạt khỏi Git theo thời gian (ai đó kubectl edit); khôi phục cluster = khảo cổ học.
GitOps (pull) đảo chiều:
Repo GitOps (mô tả TOÀN BỘ trạng thái mong muốn) Cluster
├── apps/myapp/base/ ┌────────────────────┐
├── apps/myapp/overlays/{staging,production}/ ◀────│ Argo CD / Flux │
└── infrastructure/ (ingress, observability...) sync │ agent TRONG cluster│
│ liên tục đối chiếu │
CI chỉ được COMMIT vào repo này ──────────────────▶ │ Git ↔ thực tế │
(không bao giờ chạm cluster) └────────────────────┘- Git là nguồn sự thật duy nhất của trạng thái vận hành — mở rộng Factor 1 từ code sang cả hạ tầng khai báo. Mọi thay đổi production là một commit: có tác giả, review, lịch sử, revert.
- Rollback =
git revert— hiện thân sạch nhất từng có của “re-release bản cũ” (F5). - Drift tự chữa: ai
kubectl edittay, agent phát hiện lệch và đưa về đúng Git (hoặc cảnh báo) — immutable infrastructure có máy tuần tra. - Bảo mật đảo chiều: không credential cluster nào rời khỏi cluster; bề mặt tấn công CI giảm hẳn.
Chi phí thật của GitOps: thêm một repo + một hệ thống (Argo CD/Flux) phải vận hành; vòng phản hồi gián tiếp hơn (commit → chờ sync → xem trạng thái ở UI Argo thay vì ngay trong log CI); secret cần lời giải riêng (External Secrets Operator kéo từ secret manager là lời giải phổ biến — đừng SealedSecrets nếu tổ chức đã có Vault/ASM). Với 1–2 service và một cluster, kubectl apply từ CI với credential hẹp vẫn là lựa chọn hợp lý — GitOps tỏa sáng từ khi số service × số môi trường đủ lớn.
4. Chiến lược test trong pipeline — hình chóp thực dụng
┌────────┐ E2E/smoke sau deploy staging: ÍT (đắt, chậm, mong manh)
┌─┴────────┴─┐ integration (testcontainers/service thật): VỪA —
┌─┴────────────┴─┐ đây là tầng bắt bug thật nhiều nhất với backend
│ unit (-race) │ NHIỀU: nhanh, rẻ, chạy mọi PR
└────────────────┘Ba nguyên tắc: -race luôn bật (bug concurrency Go rẻ nhất khi bắt ở CI); test phụ thuộc hành vi không phụ thuộc thứ tự/dữ liệu chung (mỗi test tự tạo dữ liệu của mình — chạy song song được); pipeline phải nhanh — mục tiêu <10 phút từ push đến staging, vì tốc độ pipeline quyết định mọi kỷ luật khác (chương 10 đã lập luận).
5. Anti-patterns
- Deploy từ máy cá nhân bằng kubeconfig admin — mọi kỷ luật pipeline thành trang trí; thu hồi bằng RBAC, không bằng lời nhắc.
- PR test một đằng, main build một nẻo không chung bước verify — artifact sinh ra từ code chưa qua đúng bộ test.
- Secret production trong CI vars dùng chung cho mọi workflow/branch — PR từ fork chạy code tùy ý với secret thật; dùng environment-scoped secrets + OIDC (id-token) thay long-lived key.
- Test quarantine vĩnh viễn (
skip flaky) — flaky test là bug thật của test hoặc của app; skip là tắt chuông báo cháy. - Pipeline 45 phút — dev gộp commit to, né test, deploy dồn cục; tốc độ pipeline là đòn bẩy văn hóa số một.
kubectl apply -fthẳng từ CI khi đã tuyên bố GitOps — hai nguồn sự thật đánh nhau; chọn một mô hình và đóng đường kia bằng quyền.- Đè tag image thay vì digest trong manifest — Argo “synced” mà cluster chạy image khác; đã nhắc từ F5, xuất hiện lại ở đây vì GitOps làm hậu quả kín đáo hơn.
6. Khi nào KHÔNG cần đầy đủ
- Side project / MVP một người: CI tối thiểu (test + build) vẫn đáng có (30 phút setup), CD có thể là script
deploy.shgọi Cloud Run — miễn build từ Git, artifact có version. - Phần mềm ship theo quý cho khách on-prem: CD liên tục không áp; nhưng CI + artifact ký + reproducible build lại càng quan trọng.
- GitOps: như mục 3 — dưới ngưỡng phức tạp nhất định, push-based đơn giản hơn là đúng.
Tóm tắt
- CI = trunk luôn ship được (verify tự động mỗi lần tích hợp); CD = delivery (sẵn sàng, người bấm) hoặc deployment (tự động hoàn toàn) — chọn theo khẩu vị rủi ro từng môi trường.
- Pipeline là cơ chế cưỡng chế 12-Factor: một artifact từ trunk, đi nguyên vẹn qua các môi trường, chỉ config đổi; quyền hẹp từng stage; ký + scan + SBOM cho chuỗi cung ứng.
- GitOps: Git là nguồn sự thật của trạng thái vận hành, agent pull và tự chữa drift; rollback = git revert — Factor 1 + 5 + immutable infrastructure hội tụ.
- Giữ pipeline <10 phút — tốc độ pipeline là nền của mọi kỷ luật còn lại.