かつてstagingとproductionの間でSQLマイグレーションファイルを一つひとつ手動で比較して「何が足りないか」を探したことがあるなら、その苦痛はよく分かるだろう。自分も何度もこの経験をした——特に複雑なスキーマを持つPostgreSQLプロジェクトで、ちゃんとした管理ツールなしにチーム全員がマイグレーションをコミットしていた時のことだ。
ArigaのAtlasはまさにその問題を解決するために生まれた:データベーススキーマをコードとして管理(Schema as Code / IaC)し、バージョン管理でき、自動化でき、どの環境でも一貫した結果が得られる。
3つのスキーマ管理方法:実践比較
Atlasの違いをはっきり理解するために、まず3つの一般的なアプローチを見ておこう:
1. 手動マイグレーションファイル(SQL/ORM)
このアプローチが最も一般的だ——Flyway、Liquibase、またはORMマイグレーション(Alembic、Django migrations、Rails ActiveRecord)を使う。SQLファイルを順番に書いてgitにコミットし、デプロイ時にmigrateを実行する。
- メリット:馴染みやすく、ドキュメントが豊富で、ORMとの統合が標準装備
- デメリット:マイグレーションファイルが時間とともに積み重なり、現在のスキーマの監査が難しく、複数人で作業する際にconflictが起きやすく、コードと実際のDB間のdriftを自動検出できない
2. 宣言的スキーマ(望ましい状態を記述する)
「何をするか」(ALTER TABLE…)を書く代わりに、「何が欲しいか」——最終的なスキーマがどうあるべきかを記述する。ツールが自動的にdiffを計算してマイグレーションを生成する。TerraformがインフラでこれをやっているようにAtlasも同じ思想をデータベースに適用する。
- メリット:スキーマが常にsource of truth、読みやすく、レビューしやすく、driftが発生しない
- デメリット:ツールの理解が必要で、一部のエッジケースは手動対応が必要
3. Atlas:ハイブリッドアプローチ——両方のいいとこ取り
Atlasは両方のモードをサポートしている:declarative(schema.sql/HCLで最終状態を記述)とversioned migration(監査可能な.sqlファイルを生成)。プロジェクトのステージに合わせてどちらかを選ぶか、両方を組み合わせることができる。
長所・短所の分析:Atlas vs Flyway/Liquibase
| 比較項目 | Flyway / Liquibase | Atlas |
|---|---|---|
| Schema drift detection | なし | あり(atlas schema diff) |
| Declarative mode | なし | あり |
| CI/CD integration | 普通 | 優秀(GitHub Actions ネイティブ) |
| Lint migration | なし | あり(atlas migrate lint) |
| Multi-database | あり(多数) | PG、MySQL、SQLite、MS SQL |
| スキーマフォーマット | SQL / XML / YAML | HCL + SQL |
Atlasで一番気に入っているのはatlas migrate lintだ——apply前にマイグレーションを分析し、破壊的な操作(DROP TABLE、DROP COLUMN)やforeign keyにindexが欠如している場合に警告してくれる。Flywayにはこの機能がなく、誰もちゃんとレビューしなかったせいでstagingでDROP COLUMNをミスしたことが一度あった。
どんな時にAtlasを選ぶべきか?
いくつかのPostgreSQL productionプロジェクトでAtlasを使った経験から、このツールが最も適しているのは以下の場合だと思う:
- 3人以上のチームが同時にスキーマを変更していて、マイグレーションのレビューと承認が必要な場合
- 複数の環境(dev/staging/prod)があり、スキーマの一貫性を保つ必要がある場合
- 問題がある時にマージをブロックするためにCI/CDパイプラインにマイグレーションチェックを組み込みたい場合
- 比較的複雑なスキーマを持つPostgreSQLまたはMySQLを使っているプロジェクト
逆に、Django/Railsでソロの小規模プロジェクトをやっているなら、フレームワークのORMマイグレーションで十分だ——over-engineerする必要はない。
Atlasのインストール
Atlasはシングルバイナリで、ランタイムの依存関係は不要だ:
# macOS
brew install ariga/tap/atlas
# Linux(ワンライナー)
curl -sSf https://atlasgo.sh | sh
# バージョン確認
atlas version
実践的なデプロイ:PostgreSQLでのバージョン管理マイグレーション
以下のワークフローはproductionで実際に使っているものだ。すべてのデモはPostgreSQL 15を使用。
ステップ1:プロジェクト構造を作成する
mkdir myapp-db && cd myapp-db
mkdir -p migrations
ステップ2:schema.sql(source of truth)を作成する
-- schema.sql: データベースの「あるべき状態」
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
username VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(500) NOT NULL,
body TEXT,
published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_published ON posts(published) WHERE published = TRUE;
ステップ3:最初のマイグレーションファイルを生成する
atlas migrate diff initial_schema \
--dir "file://migrations" \
--to "file://schema.sql" \
--dev-url "docker://postgres/15/dev"
フラグ--dev-url "docker://postgres/15/dev"はかなり便利だ——Atlasが自動的に一時的なPostgreSQLコンテナを起動してdiffを計算してくれるので、ローカルにPostgresをインストールしておく必要がない。実行後:
migrations/
20260521000001_initial_schema.sql
atlas.sum # checksumファイル——マイグレーションと一緒にコミットする
atlas.sumファイルは重要だ:コミット済みのマイグレーションを誰かが編集した場合に検出するためにAtlasが使用する(go.sumと同様の仕組み)。必ずこのファイルをgitにコミットすること。
ステップ4:実際のデータベースにマイグレーションを適用する
atlas migrate apply \
--dir "file://migrations" \
--url "postgres://user:pass@localhost:5432/myapp?sslmode=disable"
# 出力:
# Migrating to version 20260521000001 (1 migration in total):
# -- migrating version 20260521000001
# -> CREATE TABLE users ...
# -> CREATE TABLE posts ...
# -- ok (23ms)
ステップ5:新しいカラムの追加——スキーマ変更のワークフロー
プロダクトからusersテーブルにbioカラムを追加する要件が来た。ALTER TABLEを手書きする代わりに、schema.sqlを修正するだけでいい:
-- usersテーブルの定義に追加:
bio TEXT,
そして再びmigrate diffを実行する:
atlas migrate diff add_user_bio \
--dir "file://migrations" \
--to "file://schema.sql" \
--dev-url "docker://postgres/15/dev"
# Atlasが自動生成:
# ALTER TABLE "users" ADD COLUMN "bio" text;
実践Tips
1. PRをマージする前にmigrate lintを使う
atlas migrate lint \
--dir "file://migrations" \
--dev-url "docker://postgres/15/dev" \
--latest 1
# Atlasがdestructiveな変更を警告:
# L1: Dropping non-virtual column "email" (MF103)
# L2: Adding a non-nullable column "phone" to existing table (MF103)
destructiveな変更が十分にレビューされていない場合にマージをブロックするために、これをGitHub Actionsに統合する:
# .github/workflows/atlas-lint.yml
name: Atlas Migration Lint
on:
pull_request:
paths: ['migrations/**', 'schema.sql']
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ariga/setup-atlas@v0
- name: Lint migrations
uses: ariga/atlas-action/migrate/lint@v1
with:
dir: 'file://migrations'
dev-url: 'docker://postgres/15/dev'
2. スキーマコードと実際のDB間のdriftを検出する
# DBの現在のスキーマをschema.sqlと比較
atlas schema diff \
--from "postgres://user:pass@prod-host:5432/myapp?sslmode=require" \
--to "file://schema.sql"
# driftがある場合、DBがコードからどこで乖離しているかを出力で明確に表示
Flywayにはこの機能がない。誰かがproductionで直接ALTER TABLEをやっていないか早期に検出するためにCIで定期的にこれを実行している——特に深夜2時のホットフィックスの時など、これは思っているよりもよく起きることだ。
3. atlas.hclを使ってコマンドを簡潔にする
# atlas.hcl
env "dev" {
src = "file://schema.sql"
url = "postgres://user:pass@localhost:5432/myapp?sslmode=disable"
dev = "docker://postgres/15/dev"
migration {
dir = "file://migrations"
}
}
env "prod" {
url = getenv("DATABASE_URL")
migration {
dir = "file://migrations"
}
}
このファイルにより、コマンドが大幅に短くなる:
atlas migrate apply --env dev
atlas migrate apply --env prod
よくあるエラーと対処法
- Checksum mismatch:誰かがコミット済みのマイグレーションファイルを編集した。変更が意図的であることを確認してから
atlas migrate hash --forceを実行してchecksumを再生成する。 - docker://が動作しない:DockerがすでにAtlasのsocket権限を持って起動していることが必要。Dockerがない場合は実際のdev用データベースURLで代替する。
- advisory lockでマイグレーションが止まる:前のプロセスが途中でkillされた場合、PostgreSQLで
SELECT pg_advisory_unlock_all();を実行してロックを解放する。
まとめ
Atlasはデータベースマイグレーションの唯一のツールではない。しかしdrift検出、destructiveな変更のlint、CI/CD統合はFlywayやLiquibaseには実現できないことだ。PostgreSQLプロジェクトをAlembicからAtlasに移行した——マージ前にチーム全員でマイグレーションをレビューできるようになった今、戻る理由はまったくない。
新しいプロジェクトのセットアップをするなら、最初からAtlasを試してみよう——最初の30分のセットアップで、スキーマが複雑になっていく中で後々の多くの頭痛を防ぐことができる。

