← Backend← Backend
BackendBackend19 Th7, 2026Jul 19, 202617 phút đọc15 min read

CI/CD & Triển khaiCI/CD & Deployment

Thuộc bộ kiến thức Backend Roadmap.

Tổng quan

CI/CD là con đường tự động đưa một thay đổi code từ lúc developer commit cho đến khi chạy trên production, một cách an toàn và lặp lại được. Nó loại bỏ các bước thủ công chậm chạp và dễ sai — “chạy trên máy tôi mà”, copy artifact bằng tay, những đêm SSH sửa lỗi — và thay bằng một pipeline: một chuỗi stage được định nghĩa rõ, chạy trên mỗi thay đổi và hoặc cho thay đổi đi tiếp, hoặc chặn nó lại.

Với một backend engineer, điều này rất quan trọng vì một backend service tồn tại lâu dài, có state (databases!), và thường được nhiều client dùng chung. Một lần deploy hỏng có thể làm rớt request, hỏng dữ liệu, hoặc làm sập tất cả consumer cùng lúc. CI/CD cho bạn công cụ để ship nhiều thay đổi nhỏ một cách tự tin: feedback nhanh về tính đúng đắn, build tái lập được, rollout có kiểm soát, và một đường quay lại nhanh khi có sự cố.

Ghi chú này tập trung vào backend và bổ sung cho bài DevOps chuyên sâu tại ../../devops/vi/11-ci-cd.md — bài đó bao quát tooling pipeline theo hướng tổng quát hơn. Ở đây ta tập trung vào những gì đội backend làm cụ thể: đóng gói service thành container, xử lý database migrations, chọn deployment strategy, và đạt được zero-downtime deploy cùng rollback.

Kiến thức nền tảng

CI, Continuous Delivery và Continuous Deployment

Ba thuật ngữ này liên quan nhưng khác nhau — điểm khác biệt nằm ở automation đi xa tới đâu.

Thuật ngữTự động hóa cái gìCó cổng người duyệt?Kết quả khi pipeline xanh
Continuous Integration (CI)Merge → build → test → static analysis trên mỗi push/PRKhông áp dụng (developer merge thường xuyên)Một artifact đã được kiểm chứng, đã đóng gói
Continuous Delivery (CD)Mọi thứ của CI, thêm deploy lên staging và sẵn sàng release lên prod — người bấm nút “deploy to prod”Artifact luôn deploy được; release prod chỉ còn một nút bấm
Continuous Deployment (CD)Mọi thứ ở trên, và cả release prodKhông — mỗi build xanh tự động lên prodThay đổi đã live trên production, không cần bước con người

Ý tưởng cốt lõi của CItích hợp sớm và thường xuyên: mọi người merge vào một nhánh main chung nhiều lần mỗi ngày, và mỗi lần merge được kiểm chứng tự động nên các vấn đề tích hợp lộ ra trong vài phút thay vì dồn vào một “tuần merge” đau khổ về sau. Xem Version Control & Collaboration cho workflow trunk-based giúp điều này khả thi.

Cả hai loại CD đều xây trên nền CI. Câu hỏi duy nhất là bước release production cuối cùng do con người kích hoạt (Delivery) hay tự động (Deployment). Continuous Deployment đòi hỏi độ tin cậy cao vào automated test và observability.

Pipeline: build → test → package → deploy

Một backend pipeline là một chuỗi các stage. Thay đổi chỉ đi sang stage kế tiếp nếu stage hiện tại pass.

commit ─▶ build ─▶ test ─▶ package ─▶ deploy (staging) ─▶ [gate] ─▶ deploy (prod)
  1. Build — compile code, resolve dependencies. Với ngôn ngữ thông dịch thì đây là cài dependency + kiểm tra syntax/type.
  2. Test — chạy unit test, integration test, linter, security scanner. Đây là các quality gate. Xem Testing.
  3. Package — tạo ra một artifact bất biến (immutable), có version: thường là một container image (hoặc jar, wheel, binary). Chính artifact này được deploy ở mọi nơi.
  4. Deploy — đưa artifact ra một environment, chạy migrations, và xác minh bằng health check.

Quy tắc vàng: build một lần, deploy nhiều nơi. Artifact được test ở staging phải giống hệt (byte-for-byte) artifact tới production. Rebuild riêng cho từng environment sẽ tái tạo lại nhóm bug kiểu “chạy được ở staging”.

Khái niệm chính

CI trong thực tế

Triggers. Pipeline chạy theo sự kiện: push lên một nhánh, mở hoặc cập nhật một pull request, một tag, một lịch (cron), hoặc kích hoạt thủ công. Cấu hình phổ biến: chạy toàn bộ test suite trên mỗi PR, và chạy build + package + deploy khi merge vào main.

Chạy test và linter. CI job checkout code, cài dependency, rồi chạy lệnh test và lint. Test cần database hay cache sẽ dựng service container (ví dụ một Postgres dùng-một-lần) để integration test chạy trên một engine thật.

Build artifact. Sau khi build xanh, pipeline upload artifact — một container image lên registry, hoặc một binary đã compile — gắn tag theo commit SHA để truy vết được.

Caching. Việc tải dependency và output build được cache giữa các lần chạy (key theo hash của lockfile) để cắt bớt vài phút mỗi build. Docker layer caching làm điều tương tự cho việc build image.

Matrix builds. Chạy cùng một job trên nhiều tổ hợp biến — nhiều version ngôn ngữ, OS, hoặc version database — song song, để bắt lỗi tương thích.

Ví dụ GitHub Actions — một backend service

Pipeline cho một backend Go (hoặc bất kỳ ngôn ngữ nào): lint và test với một Postgres thật, rồi build và push Docker image khi ở nhánh main.

name: ci

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: app_test
        ports: ["5432:5432"]
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.23"
          cache: true            # cache module downloads + build cache
      - name: Lint
        run: go vet ./...
      - name: Test
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/app_test?sslmode=disable
        run: go test -race -coverprofile=cover.out ./...

  build-and-push:
    needs: test                  # only runs if tests pass
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write            # push to GitHub Container Registry
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha   # reuse Docker layer cache across runs
          cache-to: type=gha,mode=max

Chú ý cách job test dùng một services: Postgres kèm health check, và build-and-push khai báo needs: test để việc đóng gói không bao giờ xảy ra trên một build đỏ. Image được gắn tag github.sha — bất biến và truy vết được.

Đóng gói backend thành container

Container là định dạng đóng gói chủ đạo cho backend service: chúng gói app cùng đúng runtime và dependencies, nên nó chạy y hệt trên laptop, trong CI, và trên production. Xem ../kubernetes để orchestrate chúng ở quy mô lớn.

Best practice cho image:

# ---- build stage ----
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download            # cached unless go.mod/go.sum change
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app ./cmd/server

# ---- runtime stage ----
FROM gcr.io/distroless/static:nonroot
COPY --from=build /app /app
USER nonroot:nonroot           # non-root
EXPOSE 8080
ENTRYPOINT ["/app"]

Image cuối cùng chỉ chứa binary static và một base tối giản — không compiler, không shell, không package manager.

Deployment environment & cấu hình

Backend đi qua một chuỗi environment, mỗi cái gần với thực tế hơn:

EnvironmentMục đíchDữ liệuAi dùng
dev / localVòng lặp nhanh khi đang codeGiả/seedDeveloper
staging / pre-prodKiểm chứng cuối, giống prodGiống prod (đã ẩn danh)QA, automated test
productionTraffic thậtThậtNgười dùng cuối

Cùng một artifact bất biến di chuyển qua tất cả; chỉ cấu hình thay đổi giữa các environment. Đây là nguyên tắc Twelve-Factor App: lưu config trong environment, không trong code. Database URL, feature flag, và log level đến từ environment variable — cùng một binary kết nối tới DB dev hay DB prod chỉ tùy theo DATABASE_URL.

Secret trong CI/CD. API key, mật khẩu DB, và signing key tuyệt đối không được nằm trong repo. Lưu chúng trong secret store được mã hóa của CI provider (GitHub Actions Secrets, Vault, cloud secret manager) và inject vào như environment variable lúc deploy. Trong pipeline, mask chúng trong log và giới hạn phạm vi thật hẹp (theo từng environment, least privilege). Xem securityAuth.

Deployment strategy

Cách bạn thay version cũ bằng version mới quyết định phạm vi ảnh hưởng (blast radius) và tốc độ rollback.

StrategyCách hoạt độngDowntimeRollbackChi phíHợp nhất cho
RecreateDừng cũ, khởi động mớiCó (ngắn)Deploy lại bản cũThấpDev, hoặc app chịu được downtime
RollingThay từng ít instance mộtKhôngRollback từ từThấpMặc định cho stateless service / Kubernetes
Blue-greenChạy hai environment đầy đủ; chuyển traffic cùng lúcKhôngTức thì (chuyển ngược lại)Cao (2x hạ tầng)Cutover nhanh, atomic, dễ rollback
CanaryĐẩy một % nhỏ traffic sang version mới, rồi tăng dầnKhôngDừng tăng, route ngược lạiTrung bìnhThay đổi rủi ro cao; cần metric tốt
Feature flagsShip code ở trạng thái tắt, bật tính năng theo user/% lúc runtimeKhôngTắt flag điThấpTách deploy khỏi release; A/B test

Hosting / runtime target cho backend

Artifact thực sự chạy ở đâu? Phổ này đánh đổi giữa quyền kiểm soát và gánh nặng vận hành.

TargetBạn quản lýScalingHợp khi
VMs / bare metalOS, runtime, app, scalingThủ công / autoscaling groupCần toàn quyền, legacy, phần cứng đặc thù, tải ổn định
Container + orchestrator (Kubernetes)App + manifest; platform lo schedulingNgang, tự độngNhiều service, cần portability & self-healing ở quy mô lớn
PaaS (Heroku, Render, Fly.io, App Engine)Chỉ app của bạnTự độngĐội nhỏ muốn nhanh hơn là kiểm soát
Serverless / FaaS (Lambda, Cloud Functions)Chỉ code của functionTự động, xuống tới zeroEvent-driven, tải giật cục, hoặc traffic thấp/không đều

Đi xuống bảng để bớt vất vả vận hành, đi lên để có nhiều kiểm soát hơn. Xem ../kubernetes cho hướng container-orchestration.

Serverless cho backend

FaaS (Functions as a Service) chạy code của bạn để phản hồi sự kiện (HTTP request, message trong queue, cron) mà bạn không phải quản lý server. Platform scale số instance lên xuống — thậm chí xuống zero — và bạn trả tiền theo mỗi lần invoke.

Database migration trong pipeline

Backend có state, và thay đổi schema là phần khó nhằn nhất của deployment. Một migration là một thay đổi schema có version, có thứ tự (tạo table, thêm column, backfill data), được áp dụng bởi công cụ như Flyway, Liquibase, Alembic, hoặc golang-migrate.

Chạy migration như một bước pipeline tường minh trước (hoặc song song với) việc deploy app, chứ không phải từ bên trong app lúc khởi động (việc này bị race giữa các replica). Quy tắc then chốt cho zero-downtime: migration phải backward-compatible — cả version code cũ lẫn mới đều chạy được với schema trung gian.

Dùng pattern expand-and-contract (parallel change) cho các thay đổi phá vỡ (breaking), ví dụ đổi tên một column:

  1. Expand — thêm column mới; deploy code ghi vào cả column cũ lẫn mới.
  2. Backfill — copy dữ liệu hiện có sang column mới.
  3. Migrate reads — deploy code đọc từ column mới.
  4. Contract — khi không còn gì dùng column cũ, drop nó ở một lần deploy sau.

Mỗi bước deploy được độc lập và an toàn để rollback. Đừng bao giờ gộp một thay đổi schema hủy hoại (destructive) với chính lần deploy code phụ thuộc vào nó.

Zero-downtime deploy và rollback

Zero-downtime nghĩa là người dùng không bao giờ thấy lỗi trong lúc release. Nó đòi hỏi:

Rollback là lưới an toàn của bạn. Vì artifact là bất biến và có version, rollback chỉ là deploy lại image tag “tốt đã biết” trước đó. Hãy biến nó thành một thao tác hạng nhất, nhanh, một lệnh (hoặc tự động). Lưu ý sự bất đối xứng: code rollback sạch sẽ; data thì không. Một migration đã drop một column không thể “un-run” mà không mất dữ liệu — đó chính là lý do expand-and-contract giữ các thay đổi destructive tách riêng và làm muộn.

An toàn khi release và progressive delivery

Ship an toàn bằng cách xếp lớp các gate và guardrail:

Best Practices

Tài liệu tham khảo

Part of the Backend Roadmap knowledge base.

Overview

CI/CD is the automated path that carries a code change from a developer’s commit to running in production, safely and repeatedly. It removes the slow, error-prone manual steps — “works on my machine”, hand-copied artifacts, midnight SSH sessions — and replaces them with a pipeline: a defined sequence of stages that runs on every change and either promotes the change or stops it.

For a backend engineer this matters because a backend service is long-lived, stateful (databases!), and often shared by many clients. A bad deploy can drop requests, corrupt data, or take down every consumer at once. CI/CD gives you the tools to ship many small changes with confidence: fast feedback on correctness, reproducible builds, controlled rollout, and a fast path back when something goes wrong.

This note is backend-focused and complements the DevOps deep dive in ../../devops/en/11-ci-cd.md, which covers pipeline tooling in more general breadth. Here we concentrate on what backend teams do specifically: packaging services as containers, handling database migrations, choosing deployment strategies, and achieving zero-downtime releases and rollbacks.

Fundamentals

CI, Continuous Delivery, and Continuous Deployment

These three terms are related but distinct — the difference is how far automation goes.

TermWhat it automatesHuman gate?Result of a green pipeline
Continuous Integration (CI)Merge → build → test → static analysis on every push/PRN/A (developers merge frequently)A verified, packaged artifact
Continuous Delivery (CD)Everything in CI, plus deploy to staging and readiness to release to prodYes — a person clicks “deploy to prod”An artifact that is always deployable; prod release is one button
Continuous Deployment (CD)Everything above, and prod release tooNo — every green build goes live automaticallyThe change is live in production, no human step

The key idea of CI is integrate early and often: everyone merges to a shared main branch many times a day, and each merge is automatically verified so integration problems surface in minutes instead of at a painful “merge week” later. See Version Control & Collaboration for the trunk-based workflow that makes this work.

Both flavors of CD build on CI. The only question is whether the final production release is triggered by a human (Delivery) or automatically (Deployment). Continuous Deployment demands high trust in your automated tests and observability.

The pipeline: build → test → package → deploy

A backend pipeline is a sequence of stages. A change only moves to the next stage if the current one passes.

commit ─▶ build ─▶ test ─▶ package ─▶ deploy (staging) ─▶ [gate] ─▶ deploy (prod)
  1. Build — compile the code, resolve dependencies. For interpreted languages this is dependency install + a syntax/type check.
  2. Test — run unit tests, integration tests, linters, security scanners. These are the quality gates. See Testing.
  3. Package — produce an immutable, versioned artifact: usually a container image (or a jar, wheel, binary). This exact artifact is what gets deployed everywhere.
  4. Deploy — roll the artifact out to an environment, run migrations, and verify with health checks.

The golden rule: build once, deploy many. The artifact tested in staging must be byte-for-byte the same artifact that reaches production. Rebuilding per environment reintroduces the “works in staging” class of bugs.

Key Concepts

CI in practice

Triggers. Pipelines run on events: push to a branch, opening or updating a pull request, a tag, a schedule (cron), or a manual dispatch. A common setup: run the full test suite on every PR, and run build + package + deploy on merges to main.

Running tests and linters. The CI job checks out the code, installs dependencies, then runs the test and lint commands. Tests that need a database or cache spin up service containers (e.g. a throwaway Postgres) so integration tests run against a real engine.

Build artifacts. After a green build, the pipeline uploads the artifact — a container image to a registry, or a compiled binary — tagged with the commit SHA so it’s traceable.

Caching. Dependency downloads and build outputs are cached between runs (keyed by a lockfile hash) to cut minutes off each build. Docker layer caching does the same for image builds.

Matrix builds. Run the same job across combinations of variables — multiple language versions, OSes, or database versions — in parallel, to catch compatibility issues.

GitHub Actions example — a backend service

A pipeline for a Go (or any) backend: lint and test against a real Postgres, then build and push a Docker image on main.

name: ci

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: app_test
        ports: ["5432:5432"]
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.23"
          cache: true            # cache module downloads + build cache
      - name: Lint
        run: go vet ./...
      - name: Test
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/app_test?sslmode=disable
        run: go test -race -coverprofile=cover.out ./...

  build-and-push:
    needs: test                  # only runs if tests pass
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write            # push to GitHub Container Registry
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha   # reuse Docker layer cache across runs
          cache-to: type=gha,mode=max

Note how the test job uses a services: Postgres with a health check, and build-and-push declares needs: test so packaging never happens on a red build. The image is tagged with github.sha — immutable and traceable.

Packaging backends as containers

Containers are the dominant packaging format for backend services: they bundle your app with its exact runtime and dependencies, so it runs identically on a laptop, in CI, and in production. See ../kubernetes for orchestrating them at scale.

Image best practices:

# ---- build stage ----
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download            # cached unless go.mod/go.sum change
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app ./cmd/server

# ---- runtime stage ----
FROM gcr.io/distroless/static:nonroot
COPY --from=build /app /app
USER nonroot:nonroot           # non-root
EXPOSE 8080
ENTRYPOINT ["/app"]

The final image contains only the static binary and a minimal base — no compiler, no shell, no package manager.

Deployment environments & configuration

Backends flow through a chain of environments, each closer to real:

EnvironmentPurposeDataWho uses it
dev / localFast inner loop while codingFake/seedThe developer
staging / pre-prodFinal verification, mirrors prodProd-like (anonymized)QA, automated tests
productionReal trafficRealEnd users

The same immutable artifact moves through all of them; only configuration changes between environments. This is the Twelve-Factor App principle: store config in the environment, not in the code. Database URLs, feature flags, and log levels come from environment variables — the same binary connects to the dev DB or the prod DB depending only on DATABASE_URL.

Secrets in CI/CD. API keys, DB passwords, and signing keys must never live in the repo. Store them in the CI provider’s encrypted secret store (GitHub Actions Secrets, Vault, cloud secret managers) and inject them as environment variables at deploy time. In the pipeline, mask them in logs and scope them narrowly (per-environment, least privilege). See security and Auth.

Deployment strategies

How you replace the old version with the new one determines your blast radius and rollback speed.

StrategyHow it worksDowntimeRollbackCostBest for
RecreateStop old, start newYes (brief)Re-deploy oldLowDev, or apps that tolerate downtime
RollingReplace instances a few at a timeNoneRoll back graduallyLowDefault for stateless services / Kubernetes
Blue-greenRun two full environments; switch traffic at onceNoneInstant (switch back)High (2x infra)Fast, atomic cutover with easy rollback
CanarySend a small % of traffic to the new version, then rampNoneStop the ramp, route backMediumHigh-risk changes; needs good metrics
Feature flagsShip code dark, toggle features on per-user/% at runtimeNoneFlip the flag offLowDecoupling deploy from release; A/B tests

Hosting / runtime targets for backends

Where does the artifact actually run? The spectrum trades control for operational burden.

TargetYou manageScalingFits when
VMs / bare metalOS, runtime, app, scalingManual / autoscaling groupsFull control, legacy, special hardware, predictable load
Containers + orchestrator (Kubernetes)App + manifests; platform handles schedulingHorizontal, automatedMany services, need portability & self-healing at scale
PaaS (Heroku, Render, Fly.io, App Engine)Just your appAutomaticSmall teams wanting speed over control
Serverless / FaaS (Lambda, Cloud Functions)Just the function codeAutomatic, to zeroEvent-driven, spiky, or low/irregular traffic

Move down the table for less ops toil, up for more control. See ../kubernetes for the container-orchestration path.

Serverless for backends

FaaS (Functions as a Service) runs your code in response to events (HTTP request, queue message, cron) without you managing servers. The platform scales instances up and down — even to zero — and you pay per invocation.

Database migrations in the pipeline

Backends have state, and schema changes are the trickiest part of deployment. A migration is a versioned, ordered change to the schema (create table, add column, backfill data), applied by a tool like Flyway, Liquibase, Alembic, or golang-migrate.

Run migrations as an explicit pipeline step before (or alongside) the app deploy, not from inside the app at startup (which races across replicas). The critical rule for zero-downtime: migrations must be backward-compatible — the old and new code versions both run against the transitional schema.

Use the expand-and-contract (parallel change) pattern for breaking changes, e.g. renaming a column:

  1. Expand — add the new column; deploy code that writes to both old and new.
  2. Backfill — copy existing data into the new column.
  3. Migrate reads — deploy code that reads from the new column.
  4. Contract — once nothing uses the old column, drop it in a later deploy.

Each step is independently deployable and rollback-safe. Never combine a destructive schema change with the code deploy that depends on it.

Zero-downtime deploys and rollbacks

Zero-downtime means users never see an error during a release. It requires:

Rollback is your safety net. Because artifacts are immutable and versioned, rolling back is redeploying the previous known-good image tag. Make it a first-class, fast, one-command (or automatic) operation. Note the asymmetry: code rolls back cleanly; data does not. A schema migration that dropped a column cannot be un-run without data loss — which is exactly why expand-and-contract keeps destructive changes separate and late.

Release safety and progressive delivery

Ship safely by layering gates and guardrails:

Best Practices

References