SQL & Database — Thực chiến PostgreSQL/Migration với Atlas — declarative state thay Flyway/Liquibase imperative
~22 phútSchema design lượt xem

Migration với Atlas — declarative state thay Flyway/Liquibase imperative

Atlas declarative workflow + 50+ lint rules. Vì sao thay Flyway 2025-2026. Liquibase chuyển FSL. Demo apply + revert + CI gate.

Dev local thêm column priority vào bảng tasks. Commit, push. Code review pass. Merge vào main. CD pipeline deploy production. Nhưng migration file bị quên — không có trong repo. Kết quả: production thiếu column, app ném lỗi column "priority" does not exist, on-call thức lúc 2 giờ sáng rollback.

Migration tool tồn tại để ngăn đúng kịch bản này: track schema version, đảm bảo mỗi environment chạy đúng set migration theo thứ tự, và block deploy nếu schema chưa đồng bộ. Từ 2022, một paradigm mới xuất hiện cạnh Flyway và Liquibase — declarative migration với Atlas. Bài này map 2 paradigm, Atlas workflow thực tế, CI lint gate, và license change của Liquibase năm 2024 để bạn có đủ context cho quyết định 2025+.

1. Analogy — Git declarative state vs imperative patches

Hình dung bạn restore một project file bị mất. Có 2 cách:

  • Imperative: replay từng patch theo thứ tự — patch 1, patch 2, patch 3... đến trạng thái cuối. Nếu patch 2 bị lỗi hoặc áp sai thứ tự, kết quả sai.
  • Declarative: mô tả trạng thái cuối mong muốn — tool tự tính diff so với trạng thái hiện tại và sinh ra patch tối thiểu để đạt đến đó.

Flyway và Liquibase theo mô hình imperative: dev viết từng SQL migration file theo thứ tự version. Atlas theo mô hình declarative: dev mô tả desired state bằng schema file, tool tự sinh SQL migration.

ConceptImperative (Flyway/Liquibase)Declarative (Atlas)
Dev viết gìSQL migration từng bước (V001__create.sql)Schema desired state (schema.hcl)
Tool làm gìApply SQL theo thứ tự versionDiff current vs desired, sinh SQL
Source of truthTập hợp tất cả migration fileFile schema hiện tại
Diff giữa envCompare version numberCompare state tự động
ReviewReview SQL do dev viếtReview SQL do tool sinh ra
AnalogyShell script deploy thứ tựTerraform infrastructure
💡 Cách nhớ

Imperative = mô tả các bước để đến nơi. Declarative = mô tả nơi muốn đến, tool tìm đường. Terraform vs Ansible. Atlas vs Flyway.

2. Imperative (Flyway/Liquibase) — pattern và lifecycle

Dev viết SQL file rõ ràng từng bước, tool chỉ track và execute:

-- Flyway: migrations/V001__create_users.sql
CREATE TABLE users (
  id    BIGSERIAL PRIMARY KEY,
  email TEXT UNIQUE NOT NULL
);

-- migrations/V002__add_name.sql
ALTER TABLE users ADD COLUMN name TEXT;

-- migrations/V003__add_email_index.sql
CREATE INDEX idx_users_email ON users(email);

Workflow Flyway:

  • Dev viết SQL migration với prefix version (V001, V002...)
  • flyway migrate apply theo thứ tự version tăng dần
  • Track applied migrations trong bảng flyway_schema_history
  • Re-run: skip version đã apply (idempotent qua checksum)
  • Rollback: Flyway Community không có — dev phải tự viết U001__undo_create_users.sql

Liquibase tương tự nhưng dùng XML/YAML/SQL changeset, có rollback command với Pro license.

Pros:

  • Explicit, predictable — SQL rõ ràng do dev kiểm soát hoàn toàn
  • Versioned history dễ đọc — git log migrations/ cho toàn bộ lịch sử schema
  • Mature ecosystem — Flyway từ 2010, Liquibase từ 2006, cộng đồng lớn
  • Framework integration tốt — Spring Boot auto-apply Flyway/Liquibase khi start

Cons:

  • Manual SQL → dev phải tự biết pattern an toàn (expand-contract, CONCURRENTLY, NOT VALID)
  • Không có lint tự động — dev có thể viết blocking DDL vào production
  • Diff giữa env phức tạp — phải compare từng version file, không compare state
  • Flyway Pro (paid) cho features nâng cao: rollback, dry run, diff với target

3. Declarative (Atlas) — schema.hcl và auto-diff

Dev mô tả desired state, Atlas sinh SQL:

# schema.hcl -- desired state, single source of truth
table "users" {
  schema = schema.public
  column "id" {
    type = bigserial
    null = false
  }
  column "email" {
    type = text
    null = false
  }
  column "name" {
    type = text
    null = true
  }
  primary_key {
    columns = [column.id]
  }
  index "users_email_unique" {
    columns = [column.email]
    unique  = true
  }
}

Atlas so sánh schema.hcl với DB hiện tại, sinh ra migration SQL tối thiểu:

-- Atlas-generated: migrations/20260504143000_add_users_name.sql
-- Phat sinh tu diff: schema.hcl them column "name", chua co trong DB
ALTER TABLE "users" ADD COLUMN "name" text;

Pros:

  • Ít manual SQL → ít cơ hội viết unsafe DDL
  • Schema file duy nhất — đọc 1 file biết toàn bộ state của DB
  • Diff giữa env tự động — Atlas compare state, không compare version number
  • Open-source forever (Apache 2.0) — không lo license thay đổi
  • CI lint built-in — 50+ rule catch dangerous DDL trước merge

Cons:

  • SQL do tool sinh ra có thể không optimal cho trường hợp cụ thể — cần review
  • Learning curve HCL và Atlas DSL
  • Ít mature hơn Flyway/Liquibase — Atlas từ 2022, ecosystem nhỏ hơn
  • HCL không quen thuộc với team Java/Spring đã dùng XML config

4. Atlas hands-on demo

# 1. Install Atlas CLI (Mac)
brew install ariga/tap/atlas

# 2. Define desired state: schema.hcl (xem section 3)

# 3. Generate migration tu diff
atlas migrate diff add_users_name \
  --dir  "file://migrations" \
  --to   "file://schema.hcl" \
  --dev-url "docker://postgres/16/dev"
# Output: migrations/20260504143000_add_users_name.sql
# Content: ALTER TABLE "users" ADD COLUMN "name" text;

# 4. Review SQL output (recommend pair review trong PR)

# 5. Apply to target DB
atlas migrate apply \
  --dir "file://migrations" \
  --url "$DATABASE_URL"

# 6. Check status
atlas migrate status \
  --dir "file://migrations" \
  --url "$DATABASE_URL"
# Output: 3 migrations applied, 0 pending

--dev-url docker://postgres/16/dev khởi động container Postgres tạm thời để Atlas thực hiện diff — không cần dev database riêng cho quá trình generate migration. Container tự xoá sau khi diff xong.

Revert migration:

# Rollback 1 migration cuoi
atlas migrate down \
  --dir "file://migrations" \
  --url "$DATABASE_URL" \
  --amount 1

Atlas tự sinh SQL revert (DROP COLUMN, DROP TABLE...) từ context của migration file — không cần dev viết undo script thủ công như Flyway Community.

5. Atlas linting — 50+ rule catch unsafe DDL

Lint chạy trước khi merge, ngăn dangerous DDL vào production:

# Lint migration moi nhat
atlas migrate lint \
  --dir "file://migrations" \
  --latest 1 \
  --dev-url "docker://postgres/16/dev"

Ví dụ rules quan trọng:

RuleMô tảHành động
DS101DROP COLUMN — potential data lossBlock, require manual approval
MF101ADD COLUMN NOT NULL no default trên PG cũ hơn 11Block — full table rewrite
CD101CREATE INDEX không dùng CONCURRENTLYBlock — lock write trong khi build index
PG201ADD FOREIGN KEY không có NOT VALID + VALIDATEBlock — scan full table blocking
MF103Rename columnWarn — app deploy ordering risk

Khi lint phát hiện vi phạm:

-- Atlas lint output vi du:
L1: Destructive change "DROP COLUMN name" detected.
    DS101: Dropping column "name" from table "users" is destructive.
    To suppress, add:  --allow-destructive

Dev phải explicit chấp nhận (--allow-destructive) hoặc sửa migration. Không thể merge ngẫu nhiên.

6. CI gate — block PR nếu unsafe DDL

# .github/workflows/migration-check.yml
name: Migration lint

on:
  pull_request:
    paths:
      - "migrations/**"
      - "schema.hcl"

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Atlas
        uses: ariga/setup-atlas@v0

      - name: Lint migrations
        run: |
          atlas migrate lint \
            --dir "file://migrations" \
            --latest 1 \
            --dev-url "docker://postgres/16/dev"

PR chứa migration bị unsafe DDL → CI fail → không thể merge. Dev phải fix hoặc justify explicit. Pattern này catch hầu hết blocking DDL trước khi vào production — không phụ thuộc vào dev nhớ check thủ công.

7. Liquibase 2024 — license thay đổi sang FSL

Liquibase Core (open-source hơn 20 năm) chuyển sang Functional Source License (FSL) từ tháng 4/2024. FSL không được OSI công nhận là open-source:

  • Cho phép: use, modify, distribute nội bộ
  • Không cho phép: cung cấp "competitive offering" — vendor SaaS chạy Liquibase as a service để cạnh tranh với Liquibase Inc.
  • Sau 4 năm: tự động chuyển về Apache 2.0 (delayed open-source)

Implications thực tế:

  • Team tự host, dùng nội bộ → vẫn OK, không vi phạm FSL
  • Cloud vendor tích hợp Liquibase vào SaaS → phải review kỹ FSL terms
  • Greenfield 2025+ → consider Atlas (Apache 2.0 vĩnh viễn) hoặc Flyway Community (Apache 2.0)

FSL khác gì BSL (Business Source License) của Terraform: cả hai có delayed open-source clause, nhưng FSL restriction hẹp hơn (chỉ competitive offering), BSL rộng hơn (production use threshold). Cả hai không phải OSI open-source.

Reference chính thức: Liquibase License FAQ (mục Deep Dive dưới).

8. Pitfall — generated migration không đảm bảo zero-downtime

Pitfall — Atlas sinh migration, không đảm bảo zero-downtime

Atlas tự động sinh SQL đúng về mặt semantics nhưng không phải lúc nào cũng an toàn cho production traffic. Lint catch nhiều case nhưng không phải tất cả. Human review vẫn bắt buộc trước mỗi production apply.

Ví dụ cụ thể:

-- Atlas generate tu schema.hcl them column priority:
ALTER TABLE tasks ADD COLUMN priority TEXT NOT NULL DEFAULT 'normal';

-- Tren PG 11+: metadata-only change, instant, khong lock
-- Tren PG 10 tro xuong: full table rewrite, lock toan bo table
--   Voi 50 trieu row: co the mat hang gio downtime

Atlas lint không catch case này nếu DB target là PG 11+ (lint chạy trên dev container PG 16, pass). Nhưng nếu production chạy PG 10 vì chưa upgrade, migration này blocking.

Trường hợp khác lint chưa catch đầy đủ:

-- ADD FOREIGN KEY voi data volume lon
ALTER TABLE tasks ADD CONSTRAINT fk_project
  FOREIGN KEY (project_id) REFERENCES projects(id);
-- Scan toan bo tasks de validate, giu ShareRowExclusiveLock
-- Tren bang 100 trieu row: block write hang chuc phut
-- An toan hon: ADD FOREIGN KEY ... NOT VALID, roi VALIDATE CONSTRAINT rieng

Defensive practice:

  • Test migration trên staging với data volume tương đương production trước khi apply prod
  • Profile lock duration với EXPLAIN (ANALYZE, BUFFERS) trên staging
  • Apply trong maintenance window cho risky DDL không thể tránh lock
  • Forward: Module 11 của khoá này deep dive production migration safe pattern — CONCURRENTLY, expand-contract, lock_timeout, statement_timeout

9. Atlas vs Flyway vs Liquibase — comparison table

FeatureFlyway CommunityLiquibase (FSL 2024)Atlas
LicenseApache 2.0FSL, Apache 2.0 sau 4 nămApache 2.0
ApproachImperative SQLImperative XML/YAML/SQLDeclarative HCL
Auto-generate migrationPro onlyYes (via diff)Yes (built-in)
Lint rulesPro onlyLimited50+ built-in
CI native supportManual setupManual setupBuilt-in
RollbackPro onlyYes (với changeset)Yes (auto-gen)
Maturity2010+2006+2022+
Schema-as-codeNoPartial (XML)Yes (HCL)
Best forBrownfield, mature teamEnterprise existingGreenfield 2025+

10. Applied — TaskFlow migration với Atlas

Schema HCL cho TaskFlow:

# schema/users.hcl -- desired state
table "users" {
  schema = schema.public
  column "id" {
    type = bigserial
    null = false
  }
  column "email" {
    type = text
    null = false
  }
  column "name" {
    type = text
    null = false
  }
  column "created_at" {
    type    = timestamptz
    null    = false
    default = sql("now()")
  }
  primary_key {
    columns = [column.id]
  }
  index "users_email_unique" {
    columns = [column.email]
    unique  = true
  }
}

CI/CD flow cho TaskFlow:

1. Dev thay doi schema.hcl (them column, them table, them index...)
2. atlas migrate diff <name> -- generate SQL migration file
3. Commit ca schema.hcl va migration file vao PR
4. CI chay: atlas migrate lint --latest 1 -- check unsafe DDL
5. Reviewer approve SQL generated (pair review)
6. CD: atlas migrate apply -> staging -> smoke test
7. CD: atlas migrate apply -> production (sau khi staging pass)

Apply staging trước production cho phép phát hiện timing issue (lock duration quá lâu, data validation fail) mà CI lint không catch được vì staging có data thực.

11. Deep Dive — Migration tools

📚 Deep Dive — Migration tools

Ghi chú: Atlas docs cho hands-on workflow, Bytebase blog cho market context và so sánh khách quan, Liquibase FAQ cho quyết định license khi đang đánh giá tool stack mới.

Liên kết khoá học khác

12. Tóm tắt

  • 2 paradigm: imperative (Flyway, Liquibase — dev viết SQL từng step) vs declarative (Atlas — dev mô tả desired state, tool diff và sinh SQL).
  • Atlas declarative: schema.hcl là single source of truth, atlas migrate diff tự động sinh SQL migration từ diff giữa desired và current state.
  • Atlas linting 50+ rule catch unsafe DDL (drop column, blocking index, blocking FK) trong CI — block PR trước khi merge vào main.
  • Liquibase chuyển FSL tháng 4/2024 — không còn fully open-source per OSI; greenfield 2025+ nên cân nhắc Atlas hoặc Flyway Community thay thế.
  • Generated migration không đảm bảo zero-downtime — lint không catch tất cả trường hợp, human review và test trên staging với production data volume vẫn bắt buộc.
  • CI/CD pattern chuẩn: schema change trong PR → lint gate → pair review SQL → apply staging → apply production.
  • Forward: Module 11 của khoá này đi sâu production migration safe pattern — CONCURRENTLY, expand-contract, lock_timeout, statement_timeout.

13. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao paradigm declarative (Atlas) ít sinh ra unsafe DDL hơn imperative (Flyway)? Cơ chế cụ thể nào tạo ra sự khác biệt?

Với Flyway imperative, dev tự viết SQL từng bước — không có guard rail tự động. Dev có thể viết CREATE INDEX ON tasks(status) mà không có CONCURRENTLY, blocking write trong khi index build. Không có gì trong Flyway Community ngăn điều này trước khi apply.

Atlas declarative tách biệt 2 giai đoạn: (1) tool sinh SQL từ diff — Atlas biết best practice cho từng DDL operation và có thể áp dụng pattern an toàn; (2) lint chạy trên SQL đã sinh, check 50+ rule trước khi cho phép apply. Ngay cả khi Atlas sinh SQL không optimal, lint bắt được trước khi merge.

Cơ chế cốt lõi: declarative tách dev khỏi việc viết SQL raw, đưa tool vào giữa để kiểm tra. Imperative để dev bypass hoàn toàn — tốt khi dev giỏi, rủi ro khi team ít kinh nghiệm production DDL.

Q2
Phân biệt khi nào chọn Atlas vs Flyway. Greenfield 2025+ khác gì brownfield 2018 codebase về decision?

Greenfield 2025+: không có migration history cũ, team chưa có habit với Flyway/Liquibase. Atlas phù hợp vì: schema.hcl là single source of truth từ ngày đầu, lint CI gate built-in, declarative approach giảm cognitive load về SQL migration pattern. Không có switching cost.

Brownfield 2018 codebase: có thể đã có hàng trăm Flyway migration file, team quen workflow imperative, Spring Boot wired với Flyway auto-apply. Switching sang Atlas đòi hỏi migrate toàn bộ history (hoặc import baseline), retrain team, thay CI pipeline. Switching cost cao — Flyway vẫn tốt nếu team discipline tốt và chấp nhận bỏ qua lint built-in.

Decision tree: nếu starting fresh và không có vendor lock-in vào Flyway ecosystem → Atlas. Nếu đang dùng Flyway/Liquibase ổn định → đánh giá pain point hiện tại trước khi migrate tool. Không có lý do chuyển sang Atlas chỉ vì "mới hơn" nếu không có vấn đề cụ thể cần giải quyết.

Q3
Atlas lint catch DS101 (drop column) — workflow nên xử lý thế nào? Force flag, manual approve, hay alternative pattern?

DS101 block merge vì drop column là destructive — data mất vĩnh viễn, không rollback được. Workflow an toàn là expand-contract pattern thay vì drop ngay:

(1) Deploy app version mới không đọc column cũ nữa. (2) Verify trên production trong N ngày/tuần không có error từ column cũ. (3) Sau đó mới tạo migration drop column và override lint với --allow-destructive. Khi dùng --allow-destructive, CI vẫn ghi nhận decision này trong PR — reviewer phải explicit approve.

Không nên force flag ngay lập tức khi lint report DS101 vì: app code vẫn có thể đọc column đó ở nơi khác chưa update. Force flag bypass guard rail mà không fix root cause. Pattern đúng: lint block → dev xem lại timing deploy → áp dụng expand-contract → drop sau khi safe.

Q4
Liquibase FSL 2024 — decision tree: tiếp tục dùng, migrate sang Atlas, hay chờ 4 năm Apache 2.0?

Tiếp tục dùng: nếu team tự host Liquibase nội bộ, không cung cấp dịch vụ liên quan Liquibase cho bên ngoài → FSL không ảnh hưởng. Không cần action gì — use case này hoàn toàn OK theo FSL terms.

Migrate sang Atlas: nếu đang evaluate tool stack mới cho greenfield project, hoặc lo ngại license change tiếp theo của Liquibase Inc. Atlas Apache 2.0 vĩnh viễn — không có risk license thay đổi. Switching cost phụ thuộc migration history volume.

Chờ 4 năm: delayed open-source clause có nghĩa Liquibase 2024 → Apache 2.0 năm 2028. Nếu không có compliance requirement nghiêm ngặt về OSI open-source, chờ là option. Nhưng Liquibase Inc. có thể tiếp tục FSL cho version mới — chỉ version 2024 guaranteed Apache 2.0 sau 4 năm, không phải version tương lai.

Rule: nếu legal/compliance team yêu cầu OSI-certified open-source → Liquibase không còn qualify từ 2024. Chọn Atlas hoặc Flyway Community (cả hai Apache 2.0).

Q5
Atlas sinh migration `ALTER TABLE tasks ADD COLUMN priority TEXT NOT NULL DEFAULT 'normal'`. Tại sao có thể gây downtime trên PG 10 mà lint không catch? Defensive practice?

Trên PG 11+, PostgreSQL implement optimization: ADD COLUMN ... NOT NULL DEFAULT constant là metadata-only change — không cần rewrite từng row, chỉ update pg_attribute. Instant, không lock.

Trên PG 10 và cũ hơn: không có optimization này. PostgreSQL phải viết lại toàn bộ table để thêm default value vào mỗi row. Với bảng 50 triệu row, có thể lock table hàng giờ. Lỗi gây downtime production.

Atlas lint chạy trên dev container PG 16 → PG 16 pass → lint không report warning. Nhưng nếu production chạy PG 10 vì chưa upgrade, kết quả khác hoàn toàn. Lint check theo target DB version, không detect version mismatch giữa dev và production.

Defensive practice: (1) Đảm bảo lint dev-url trùng PG version với production. (2) Test migration timing trên staging có data volume tương đương production trước khi apply prod. (3) Với risky DDL không tránh được, apply trong maintenance window với lock_timeout set để migration fail nhanh thay vì block lâu.

Bài tiếp theo: Module 5 — Indexing internals

Bài này có giúp bạn hiểu bản chất không?