DBマイグレーションツールAtlasでDevOps環境におけるDBスキーマ管理を実践してみよう(前編)

はじめに

対象読者のレベル感

本記事は、DevOps環境におけるDBスキーマ管理の課題に直面している中級者以上の方、または、DBマイグレーションツールをCI/CDパイプラインに組み込みたいと考えている方を主な対象としています。

前提知識として、Gitなどのバージョン管理システムの基本的な操作、およびPostgresqlなどのRDBの基礎知識があることを想定しています。

記事のゴール

本記事を通じて、読者は以下の点を達成することを目指したいと思います。

  • Atlasによるバージョン管理アプローチによるマイグレーションの実装イメージをつかむ (今回の内容)
  • DBスキーマの初期作成から、変更の追加適用の具体的な手順をPostgresql環境で検証する (今回の内容)
  • CI/CDとの統合方法について学ぶ (次回の内容)
  • マイグレーションのロールバックなど、Atlasを使用する際の注意点や実践的なTIPSを学ぶ (次回の内容)

DBスキーマ管理の課題

DevOps環境におけるDBスキーマ管理

皆さん、DBのスキーマ(テーブル定義)の管理ってどうしてますか?
DBeaverなどのSQLクライアントが持つスキーマ管理機能を利用したり、古典的な方法だと1つファイルでDDLを管理していたりと、色々とやり方はあると思います。
複数環境があるようなシステムでは、アプリケーションのリリースとDBスキーマの変更が適切に管理されていないと、アプリケーションはリリースされたもののスキーマ変更が漏れてしまい、システムが正常に動作しないといったトラブルが発生することがあります。

DevOps環境において、DBスキーマの管理をIaC(Infrastructure as Code)やCI/CDの文脈で適切に行うには、DBマイグレーションツールを利用して、スキーマ定義の変更内容をコードとして管理し、それをCI/CDパイプラインに組込むことが推奨されます。

Atlasとは:「Database Schema as Code」ツール

Atlasは、DBスキーマ管理という長年の課題に対して、現代のDevOpsの原則を適用した「Database Schema as Code」ツールです。
他の類似ツールとの違いとして、最終的なスキーマ定義への宣言的マイグレーションのアプローチを採用しているため「DB用のTerraform」と呼ばれることもあるようです。

Atlasには無償利用できるStarterプランを含む3つのプランがあり、今回はStarterで利用できる機能(※Openとなっている機能)だけを利用して、DBマイグレーションの実装イメージをつかんでいきましょう。

宣言的マイグレーション と バージョン管理マイグレーション

Atlasは宣言的マイグレーションだけでなく、バージョン管理マイグレーションの利用も可能です。2つの違いについては以下の通りです。

マイグレーション方式概要メリットデメリット
宣言的目指すスキーマ状態を定義し、現在の状態との差分をAtlasが計算し、SQL自動生成・適用まで自動で行う。開発者が手動でマイグレーションSQLを作成・適用する必要がない。変更内容が不透明になり、履歴が明確に残らない。
バージョン管理差分からマイグレーションSQLを自動生成するが、適用は別プロセスで行う。適用されるSQLファイルが事前に作成されるため、実行前に適用内容を完全に把握でき、変更履歴を明確に追跡できる(安全性が高い)。差分として管理するという特性から、適用漏れによりスキーマのずれ(ドリフト)が発生した場合、手動での復旧が必要になる場合がある。

今回は変更時の安全性の高さを優先し、バージョン管理方式を採用したいと思います。


検証環境の構築と初期準備

検証した環境

今回検証で利用した環境は以下通りです。

  • Windows 11 + WSL2 (Ubuntu 22.04.5 LTS)
  • Docker CE 28.4.0

この環境に対して、Atlas公式の手順通り、以下のコマンドで Atlas の最新版(記事執筆時点ではv0.28.2)をインストールしています。

curl -sSf https://Atlas.sh | sh

プロジェクト構成とAtlas設定ファイル

プロジェクトルートには、以下のようにディレクトリ/ファイルを配置します。

プロジェクトルート/
 ├ schemas/
 ⇒ 目指すスキーマ状態の定義が格納されるディレクトリ
 │ ├ 1_users.sql ⇒ 後ほど作成
 │ └ 2_repos.sql ⇒ 後ほど作成
 ├ migrations/ ⇒ 歴代のマイグレーションSQLが格納されるディレクトリ
 │ └ (初期状態では何もファイルが無い状態)
 └ atlas.hcl ⇒ Atlasの設定ファイル、この後作成

プロジェクトルート配下の Atlasの設定ファイルを作成します。

  • atlas.hcl
# migrate diff 実行時の設定
env "temp" {
  migration {
    dir = "file://migrations"       # マイグレーションSQL格納ディレクトリ
  }
  src = "file://schemas"            # スキーマ定義格納ディレクトリ
  dev = "docker://postgres/16/dev"  # 処理用の一時DB
}

# migrate apply 実行時の設定
env "demo" {
  migration {
    dir = "file://migrations"       # マイグレーションSQL格納ディレクトリ
  }
  url = "postgres://postgres:pass@:5432/demo?search_path=public&sslmode=disable"  # マイグレーション対象DBへの接続文字列
  dev = "docker://postgres/16/dev"  # 処理用の一時DB
}

初期スキーマ定義の作成

次に初期のDBスキーマとして、schemas ディレクトリ内に users テーブルと repos テーブルの定義ファイルを作成します。

  • schemas/1_users.sql
-- Create "users" table
CREATE TABLE "public"."users" (
    "id" bigint NOT NULL,
    "name" character varying NOT NULL,
    PRIMARY KEY ("id")
);
  • schemas/2_repos.sql
-- Create "repos" table
CREATE TABLE "public"."repos" (
    "id" bigint NOT NULL,
    "name" character varying NOT NULL,
    "owner_id" bigint NOT NULL,
    PRIMARY KEY ("id"),
    CONSTRAINT "fk_repo_owner" FOREIGN KEY ("owner_id") REFERENCES "public"."users" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION
);

検証用DBの起動

最後に検証用のDBとして、ローカル環境でPostgresqlをDockerで起動します。

docker run --rm -d --name atlas-demo -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=demo -p 5432:5432 postgres

Atlasによるマイグレーション手順

1. マイグレーションSQLの作成(初期スキーマ)

初期スキーマの定義ファイルを準備した状態で、atlas migrate diff コマンドを実行し、マイグレーションSQLを自動作成します。

atlas migrate diff initial --env temp

このコマンドにより、まずコンテナで実行される一時DBに対してmigratinsディレクトリ配下にある歴代のマイグレーションSQLを順番に適用し、直近のDBスキーマ状態を再現します。(※ただし、今回は初回なのでスキーマ定義は何も作成されません)
次に、一時DBの状態と、schemas 配下の最終的なスキーマ定義との差分をAtlasが計算して、差分を埋めるためのマイグレーションSQLを自動生成し、migrations ディレクトリに出力します。

今回は初回であるため、出力されるSQLは(一部異なりますが)基本的に users テーブルと repos テーブルの CREATE TABLE 文をマージした内容になります。

$ atlas migrate diff initial --env temp

$ ls migrations/
20251009010729_initial.sql  atlas.sum

$ cat migrations/20251009010729_initial.sql
-- Create "users" table
CREATE TABLE "users" ("id" bigint NOT NULL, "name" character varying NOT NULL, PRIMARY KEY ("id"));
-- Create "repos" table
CREATE TABLE "repos" ("id" bigint NOT NULL, "name" character varying NOT NULL, "owner_id" bigint NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "fk_repo_owner" FOREIGN KEY ("owner_id") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION);

2. DBにマイグレーションSQLを適用

作成されたSQLを検証用DBに適用します。

atlas migrate apply --env demo

このコマンドにより、マイグレーション対象のDB内のマイグレーション履歴テーブル atlas_schema_revisions と履歴の照合が行われ、migrations ディレクトリ配下の未適用のマイグレーションSQLが順番に適用されます。
この時点ではマイグレーション履歴テーブルが空のため、全てのマイグレーションSQLが対象となりますが、今回のケースでは初期のDBスキーマ定義を作成する1ファイルだけとなります。

適用ログには、users テーブルと repos テーブルが作成されたことが示されます。

$ atlas migrate apply --env demo
Migrating to version 20251009103327 (1 migrations in total):
  -- migrating version 20251009103327
    -> CREATE TABLE "public"."users" ("id" bigint NOT NULL, "name" character varying NOT NULL, PRIMARY KEY ("id"));
    -> CREATE TABLE "public"."repos" ("id" bigint NOT NULL, "name" character varying NOT NULL, "owner_id" bigint NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "fk_repo_owner" FOREIGN KEY ("owner_id") REFERENCES "public"."users" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION);
  -- ok (5.648392ms)

  -------------------------
  -- 40.526546ms
  -- 1 migration
  -- 2 sql statements

実際にDBに接続して確認してみると、初期のスキーマ定義が作成されているのと、マイグレーション履歴テーブルに履歴が書き込まれていることが確認できました。

$ psql -h localhost -p 5432 -d demo -U postgres
Password for user postgres:
psql (15.14 (Ubuntu 15.14-1.pgdg22.04+1), server 17.2 (Debian 17.2-1.pgdg120+1))
Type "help" for help.
  • テーブル一覧
demo=# \dt
                 List of relations
 Schema |          Name          | Type  |  Owner
--------+------------------------+-------+----------
 public | atlas_schema_revisions | table | postgres
 public | repos                  | table | postgres
 public | users                  | table | postgres
(3 rows)
  • users テーブルのスキーマ定義
demo=# \d users
                    Table "public.users"
 Column |       Type        | Collation | Nullable | Default
--------+-------------------+-----------+----------+---------
 id     | bigint            |           | not null |
 name   | character varying |           | not null |
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)
Referenced by:
    TABLE "repos" CONSTRAINT "fk_repo_owner" FOREIGN KEY (owner_id) REFERENCES users(id)
  • repos テーブルのスキーマ定義
demo=# \d repos
                     Table "public.repos"
  Column  |       Type        | Collation | Nullable | Default
----------+-------------------+-----------+----------+---------
 id       | bigint            |           | not null |
 name     | character varying |           | not null |
 owner_id | bigint            |           | not null |
Indexes:
    "repos_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "fk_repo_owner" FOREIGN KEY (owner_id) REFERENCES users(id)
  • atlas_schema_revisions テーブルの内容
demo=# SELECT version,description,hash FROM atlas_schema_revisions;
    version     | description |                     hash
----------------+-------------+----------------------------------------------
 20251009103327 | initial     | k6B212fSjYGW/i/4rF8PUQUk0+WOHcpWOMrE+80j3fs=
(1 row)

3. スキーマ定義の変更とマイグレーションSQLの追加作成(テーブル変更)

先ほど作成した repos テーブルに description カラムを追加する修正を行います。

  • schemas/2_repos.sql
-- Create "repos" table
CREATE TABLE "public"."repos" (
    "id" bigint NOT NULL,
    "name" character varying NOT NULL,
    "description" character varying NULL,  -- ここにカラムを追加
    "owner_id" bigint NOT NULL,
    PRIMARY KEY ("id"),
    CONSTRAINT "fk_repo_owner" FOREIGN KEY ("owner_id") REFERENCES "public"."users" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION
);

修正後、再度 atlas migrate diff コマンドを実行します。

atlas migrate diff mod_repos --env temp

migratins ディレクトリ配下にマイグレーションSQLが1つあるので、それを適用することで一時DBに初期のスキーマ状態が再現されます。
Atlasは、一時DBと schamas 配下のスキーマ定義ファイルの差分を検知し、その差分を埋めるためのマイグレーションSQLを自動作成して migrations ディレクトリ内に追加出力します。

今回は既存テーブルの変更なので、ALTER TABLE 文のマイグレーションSQLが出力されました。

$ atlas migrate diff mod_repos --env temp

$ ls migrations/
20251009103327_initial.sql  20251010023825_mod_repos.sql  atlas.sum

$ cat migrations/20251010023825_mod_repos.sql
-- Modify "repos" table
ALTER TABLE "public"."repos" ADD COLUMN "description" character varying NULL;

4. スキーマ定義の変更とマイグレーションSQLの追加作成(テーブル追加)

さらに別の変更として、commits テーブルを新規追加します。

  • schemas/3_commits.sql
-- Create "commits" table
CREATE TABLE "commits" (
  "id" bigint,
  "message" character varying NOT NULL,
  "repo_id" bigint NOT NULL,
  "author_id" bigint NOT NULL,
  PRIMARY KEY ("id"),
  FOREIGN KEY ("repo_id") REFERENCES "repos" ("id"),
  FOREIGN KEY ("author_id") REFERENCES "users" ("id")
);

ファイル作成後、再度 atlas migrate diff コマンドを実行します。

atlas migrate diff add_commits --env temp

今度は migratins ディレクトリ配下にマイグレーションSQLが2つあるので、それを順番に適用することで、一時DBに直近のDBスキーマ状態(reposテーブル変更後の状態)が再現されます。
Atlasは先ほどと同様、一時DBと schamas 配下のスキーマ定義ファイルの差分を検知し、マイグレーションSQLを自動作成して migrations ディレクトリ内に追加出力します。

今回はテーブルの新規追加なので、CREATE TABLE 文のマイグレーションSQLが追加で作成されました。

$ atlas migrate diff add_commits --env temp

$ ls migrations/
20251009103327_initial.sql  20251010023825_mod_repos.sql  20251010030347_add_commits.sql  atlas.sum

$ cat migrations/20251010030347_add_commits.sql
-- Create "commits" table
CREATE TABLE "public"."commits" ("id" bigint NOT NULL, "message" character varying NOT NULL, "repo_id" bigint NOT NULL, "author_id" bigint NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "commits_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "public"."users" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "commits_repo_id_fkey" FOREIGN KEY ("repo_id") REFERENCES "public"."repos" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION);

5. DBへの変更の追加適用

先ほど作成された2つのマイグレーションSQL(カラム追加とテーブル新規追加)をDBに適用します。

atlas migrate apply --env demo

今回は初回のマイグレーション履歴があるため、atlas_schema_revisions と照合され、追加で作成された2つのマイグレーションSQLが順番に適用されます。

再び度DBに接続して状態を確認してみましょう。

$ psql -h localhost -p 5432 -d demo -U postgres
Password for user postgres:
psql (15.14 (Ubuntu 15.14-1.pgdg22.04+1), server 17.2 (Debian 17.2-1.pgdg120+1))
Type "help" for help.
  • テーブル一覧 ⇒ commits テーブルが追加
demo=# \dt
                 List of relations
 Schema |          Name          | Type  |  Owner
--------+------------------------+-------+----------
 public | atlas_schema_revisions | table | postgres
 public | commits                | table | postgres  -- commitsテーブルが追加
 public | repos                  | table | postgres
 public | users                  | table | postgres
(4 rows)
  • commits テーブルのスキーマ定義 ⇒ 新規追加
demo=# \d commits
                     Table "public.commits"
  Column   |       Type        | Collation | Nullable | Default
-----------+-------------------+-----------+----------+---------
 id        | bigint            |           | not null |
 message   | character varying |           | not null |
 repo_id   | bigint            |           | not null |
 author_id | bigint            |           | not null |
Indexes:
    "commits_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "commits_author_id_fkey" FOREIGN KEY (author_id) REFERENCES users(id)
    "commits_repo_id_fkey" FOREIGN KEY (repo_id) REFERENCES repos(id)
  • repos テーブルのスキーマ定義 ⇒ description カラムが追加 + commits テーブルからの参照が追加
demo=# \d repos
                       Table "public.repos"
   Column    |       Type        | Collation | Nullable | Default
-------------+-------------------+-----------+----------+---------
 id          | bigint            |           | not null |
 name        | character varying |           | not null |
 owner_id    | bigint            |           | not null |
 description | character varying |           |          |           -- カラムが追加
Indexes:
    "repos_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "fk_repo_owner" FOREIGN KEY (owner_id) REFERENCES users(id)
Referenced by:
    TABLE "commits" CONSTRAINT "commits_repo_id_fkey" FOREIGN KEY (repo_id) REFERENCES repos(id)  -- commitsテーブルからの参照が追加
  • users テーブルのスキーマ定義 ⇒ commits テーブルからの参照が追加
demo=# \d users
                    Table "public.users"
 Column |       Type        | Collation | Nullable | Default
--------+-------------------+-----------+----------+---------
 id     | bigint            |           | not null |
 name   | character varying |           | not null |
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)
Referenced by:
    TABLE "commits" CONSTRAINT "commits_author_id_fkey" FOREIGN KEY (author_id) REFERENCES users(id)  -- commitsテーブルからの参照が追加
    TABLE "repos" CONSTRAINT "fk_repo_owner" FOREIGN KEY (owner_id) REFERENCES users(id)
  • atlas_schema_revisions テーブルの内容 ⇒ 2つのマイグレーション履歴が追加
demo=# SELECT version,description,hash FROM atlas_schema_revisions;
    version     | description |                     hash
----------------+-------------+----------------------------------------------
 20251009103327 | initial     | k6B212fSjYGW/i/4rF8PUQUk0+WOHcpWOMrE+80j3fs=
 20251010023825 | mod_repos   | 4t3rp8ghYpS7eU+fH1/zaR1CUZcvzSEdbWnubL241jc=
 20251010030347 | add_commits | s5LUiesbVt4Pu6mcpS/EptFWZBae75ee38Ed8L1pJrc=
(3 rows)

2回のスキーマ定義の変更が正しく反映されているのと、マイグレーション履歴テーブルに2つのマイグレーション履歴が追加されていることが確認できました。


まとめ

学んだことの整理

今回の検証を通じて、Atlasのバージョン管理マイグレーションアプローチの具体的な動作を理解しました。

  • DBスキーマの最終定義 (schemasディレクトリ) と現在の状態との差分を計算し、マイグレーションSQLを自動生成すること。
  • マイグレーション履歴がmigrationsディレクトリ内にタイムスタンプ付きのファイルとして残り、Gitで変更履歴を明確に追跡できること。
  • atlas migrate diffatlas migrate applyを組み合わせることで、初期構築から継続的なスキーマ更新までをコードベースで安全に管理できること。

次のステップ

後編では引き続き、CI/CDとの統合方法についてや、マイグレーションのロールバックなど、実践に向けての注意点・TIPSなどについて記載していきたいと思います。

新規CTA