Hướng Dẫn Tự Động Hóa Database Schema Migration MySQL Với Liquibase và CI/CD

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

Quick start: Chạy migration đầu tiên trong 5 phút

Cảnh tượng này hẳn rất quen thuộc: bạn gửi file patch_v2.sql qua Slack lúc nửa đêm, hồi hộp nhờ DevOps chạy tay trên production. Chỉ cần sót một lệnh hoặc lệch thứ tự file, toàn bộ ứng dụng lăn ra lỗi. Liquibase xử lý dứt điểm rủi ro này bằng cách biến schema database thành code, versioning chặt chẽ như Git.

Không cần cài đặt phức tạp. Bạn chỉ cần Docker và 2 file cấu hình để bắt đầu.

Đầu tiên, tạo file liquibase.properties để khai báo kết nối MySQL:

changeLogFile=changelog.xml
url=jdbc:mysql://localhost:3306/app_db?useSSL=false&allowPublicKeyRetrieval=true
username=root
password=rootpassword
driver=com.mysql.cj.jdbc.Driver

Tiếp theo, tạo file changelog.xml định nghĩa bảng users mẫu:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
    http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="20241015-01" author="hoang-dev">
        <createTable tableName="users">
            <column name="id" type="BIGINT" autoIncrement="true">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="username" type="VARCHAR(50)">
                <constraints nullable="false" unique="true"/>
            </column>
            <column name="email" type="VARCHAR(100)">
                <constraints nullable="false"/>
            </column>
            <column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
        </createTable>
    </changeSet>
</databaseChangeLog>

Kích hoạt migration bằng lệnh Docker một dòng:

docker run --rm --network="host" \
  -v $(pwd):/liquibase/changelog \
  liquibase/liquibase \
  --defaults-file=/liquibase/changelog/liquibase.properties \
  update

Kiểm tra MySQL ngay sau khi lệnh chạy xong. Bên cạnh bảng users, bạn sẽ thấy 2 bảng hệ thống mới xuất hiện: DATABASECHANGELOG và DATABASECHANGELOGLOCK. Đây chính là trái tim của Liquibase.

Bên dưới nắp capo: Liquibase vận hành ra sao?

File SQL thuần không thể tự nhớ trạng thái. Khi dự án có 15 developer cùng sửa schema trên hàng chục microservice, bạn không thể biết bảng trên Staging đã khớp với Production hay chưa.

Liquibase kiểm soát toàn bộ chu trình này thông qua hai thành phần cốt lõi.

1. ChangeSet và ChangeLog

Mỗi thay đổi dữ liệu được gói gọn trong một ChangeSet. Tập hợp các ChangeSet nằm chung trong file ChangeLog.

  • id: Mã định danh thay đổi. Nên đặt theo format ngày tháng hoặc mã Jira ticket (ví dụ: PROJ-102-add-phone-col).
  • author: Tên người tạo thay đổi, phục vụ audit trail khi điều tra log.
  • Khóa định danh: Bộ ba id + author + filepath tạo thành unique key. Liquibase dựa vào bộ ba này để quyết định có chạy script hay bỏ qua.

2. Quy trình xử lý với 2 bảng hệ thống

Khi bạn gõ update, Liquibase thực thi 4 bước tuần tự:

  1. Kiểm tra khóa: Đọc bảng DATABASECHANGELOGLOCK. Cột LOCKED được set thành 1. Cơ chế này chặn xung đột khi 2 pipeline CI/CD chạy cùng lúc.
  2. Đối soát Checksum: Quét bảng DATABASECHANGELOG, đọc mã MD5 hash của từng ChangeSet để phát hiện những script đã chạy.
  3. Thực thi: Áp dụng các ChangeSet mới vào MySQL. Ghi nhận ngay một dòng audit log kèm thời gian thực thi (execution time tính bằng millisecond).
  4. Mở khóa: Đặt lại LOCKED = 0 trong DATABASECHANGELOGLOCK để nhường quyền cho tiến trình tiếp theo.

3. Rollback an toàn khi có sự cố

Deploy thất bại giữa đêm là rủi ro luôn chực chờ. Khối <rollback> cho phép bạn trả database về trạng thái cũ chỉ với một lệnh rollback-count 1:

<changeSet id="20241015-02" author="hoang-dev">
    <addColumn tableName="users">
        <column name="phone_number" type="VARCHAR(20)"/>
    </addColumn>
    <rollback>
        <dropColumn tableName="users" columnName="phone_number"/>
    </rollback>
</changeSet>

Tích hợp Liquibase vào pipeline GitHub Actions

Đừng bao giờ để developer chạy lệnh migration từ máy cá nhân lên server production. Toàn bộ thao tác phải được cô lập trong pipeline CI/CD.

Dưới đây là workflow GitHub Actions chuẩn mẫu:

name: Database Migration

on:
  push:
    branches:
      - main
    paths:
      - 'db/changelog/**'

jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Source Code
        uses: actions/checkout@v4

      - name: Liquibase Update
        uses: liquibase-github-actions/[email protected]
        with:
          changelogFile: 'db/changelog/changelog.xml'
          url: ${{ secrets.DB_JDBC_URL }}
          username: ${{ secrets.DB_USER }}
          password: ${{ secrets.DB_PASSWORD }}
          headless: true

Áp dụng quy trình 3 chặng để đảm bảo an toàn tuyệt đối:

  • Chặng 1 (Dry-run & Review): Chạy lệnh update-sql để trích xuất raw SQL ra artifact. DBA có thể review chính xác câu lệnh sắp chạy trước khi chạm vào dữ liệu thật.
  • Chặng 2 (Staging Migration): Tự động migrate trên môi trường Staging với dữ liệu tương đương Production để phát hiện sớm lỗi timeout hoặc cú pháp.
  • Chặng 3 (Production Approval): Cài đặt Manual Approval Gate trên GitHub Actions. Chỉ khi Tech Lead bấm duyệt, job mới kết nối vào database chính thức.

3 bài học xương máu khi chạy Liquibase trên MySQL

Trên cụm MySQL 8.0 chứa 40 triệu bản ghi (khoảng 85GB data), một lệnh DDL bất cẩn có thể làm tê liệt toàn bộ hệ thống bán hàng. Hãy ghi nhớ các nguyên tắc sau:

  • Coi chừng Metadata Lock trên bảng lớn: Dù MySQL 8.0 hỗ trợ ALGORITHM=INSTANT cho một số thao tác thêm cột, các lệnh đổi kiểu dữ liệu hay đánh index vẫn yêu cầu Exclusive Lock. Với bảng trên 5 triệu dòng, hãy bọc công cụ online schema change như gh-ost hoặc pt-online-schema-change vào thẻ <customChange> thay vì gọi DDL trực tiếp.
  • Tuyệt đối không sửa ChangeSet đã chạy: Liquibase kiểm tra tính toàn vẹn bằng MD5 checksum. Nếu bạn sửa file đã apply trên production, pipeline sẽ văng lỗi ValidationFailedException: MD5Sum check failed và dừng ngay lập tức. Muốn sửa logic, hãy tạo ChangeSet mới với ID tiếp theo.
  • Phòng ngừa lỗi trùng lặp bằng Preconditions: Dùng điều kiện kiểm tra trước khi chạy. Thẻ <preConditions> giúp script chạy lại an toàn mà không quăng lỗi bảng hoặc index đã tồn tại:
<changeSet id="20241015-03" author="hoang-dev">
    <preConditions onFail="MARK_RAN">
        <not>
            <indexExists indexName="idx_users_email" tableName="users"/>
        </not>
    </preConditions>
    <createIndex indexName="idx_users_email" tableName="users">
        <column name="email"/>
    </createIndex>
</changeSet>

Tự động hóa schema migration không chỉ giải phóng lập trình viên khỏi những đêm trực deploy căng thẳng. Quan trọng hơn, nó biến database thành một phần có thể kiểm thử, theo dõi và phục hồi trong chu trình DevOps hiện đại.

Share: