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

はじめに

前編で解説したAtlasのDBマイグレーションの具体的な動作イメージに続いて、後編ではDevOps環境のCI/CDとの統合方法や、実践する上でのTIPSや注意点を中心に解説していきます。

対象読者のレベル感

本記事は、前編の内容(Atlasによるバージョン管理アプローチの実装とPostgreSQL環境での検証)を理解されている中級者以上の方を主な対象としています。
特に、DBマイグレーションツールをCI/CDパイプラインに組込み、実践利用していきたいと考えている方を想定しています。

前提知識として、CI/CDパイプラインやDB運用についての基礎知識があることを想定しています。

記事のゴール

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

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

CI/CDとの統合

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

CI/CDパイプラインへ組込み

前回の記事でDBマイグレーションの流れを見てきた通り、schemas ディレクトリ配下にあるスキーマ定義は、データベースのあるべき姿(最終的なスキーマ定義) を表すファイルになるので、Gitで適正に管理する必要があります。
また、atlas migrate diff コマンドで migrations ディレクトリ内に作成されたマイグレーションSQLは、マイグレーション処理に利用されるだけではなく、マイグレーションSQL作成時の比較元として利用されるため、こちらもGitでの管理対象とする必要があります。

ということで、CI/CDパイプラインを利用した場合のDBマイグレーションのワークフローの一例を考えてみました。

  1. 開発者がローカル環境でスキーマ定義の変更を行う。
  2. 開発者がローカル環境で atlas migrate diff コマンドを実行し、マイグレーションSQLを作成する。
  3. 変更結果(schemas および migrations ディレクトリ内のファイル)をGitHubやGitLabといったソースコード管理ツールにPUSHする。
  4. CDパイプライン内で atlas migrate apply コマンドが実行され、各環境のDBにマイグレーションSQLが適用される。

オプションとして、CIパイプライン内で schemas ディレクトリ内のファイルの内容を元にER図を作成したり、atlas migrate apply --dry-run コマンドでマイグレーションSQLのテストを行うというのも良いかも知れませんね。

複数環境DBへのマイグレーション

複数環境あるDBに対するマイグレーションも、前編で検証してきた内容を応用すれば実現できます。
例えば、プロジェクトルート配下のAtlasの設定ファイルに、以下のような内容を追記しておきます。

  • atlas.hcl
...前略(既存の内容)

# TST環境DBへの migrate apply 実行時の設定
env "tst" {
  migration {
    dir = "file://migrations"       # マイグレーションSQL格納ディレクトリ
  }
  url = "postgres://<ユーザー名>:<パスワード>@:5432/<DB名>?search_path=<スキーマ名>"  # TST環境DBへの接続文字列
  dev = "docker://postgres/16/dev"  # 処理用の一時DB
}

こうした環境ごとの設定を入れておくことで、例えばTST環境DBへのマイグレーションを行う際は、CI/CDパイプライン内で以下のようなコマンド実行すればOKです。

atlas migrate apply --env tst

※ セキュリティ上の注意

上記の例では設定ファイル内に、ユーザー名やパスワードを直接記述するような形になっていますが、ソースコードにシークレット(ユーザー名やパスワード)を平文で記述するのはセキュリティ的にNGです。
この課題に対応するため、クラウドサービスのシークレットマネージャーを利用する方法が公式ドキュメントで公開されています。

たとえば、AWSのSSMパラメータストアを利用する場合、以下のように data "runtimevar" ブロックを追加し、シークレットはそのブロックを参照するような記述に変更することで対応できます。

data "runtimevar" "db_migration_user" {
    url = "awsparamstore:///DB/MIGRATION/USER?region=ap-northeast-1&decoder=string"
}

data "runtimevar" "db_migration_password" {
    url = "awsparamstore:///DB/MIGRATION/PASSWORD?region=ap-northeast-1&decoder=string"
}

...中略...

env "tst" {
    migration {
        dir = "file://migrations" # マイグレーションSQL格納ディレクトリ
    }
    url = "postgres://${data.runtimevar.db_migration_user}:${data.runtimevar.db_migration_password}@:5432/?search_path=" # TST環境DBへの接続文字列
    dev = "docker://postgres/16/dev" # 処理用の一時DB
}

実践する上でのTIPSや注意点

schemas ディレクトリ内のファイル名

前編でやったローカルでの検証時に作成した3つのテーブルのスキーマ定義間には以下のように依存関係がありました。

  • 1_users.sql
  • 2_repos.sql :usersテーブルのidを参照
  • 3_commits.sql : usersテーブルのid と reposテーブルのidを参照

こういった関係がある場合は、普通にDB上で手動でスキーマ定義を作成する場合でも、参照先のカラムが存在しないと、依存関係の問題でエラーとなってしまいます。
Atlasにおいてもこれは同様で、atlas migrate diff 実行時に schemas ディレクトリ配下のスキーマ定義ファイルから現在のスキーマ状態を再現する際には、ファイル名順に処理を行います。
そのため、スキーマ定義ファイル間に依存関係がある場合は、ファイル名の先頭に連番を振るなどして、依存関係が解決される順番になるように命名しておく必要がありますので注意してください。

マイグレーションのロールバック

Atlasでは、マイグレーションを元に戻すロールバック(マイグレーションダウン)も実行可能です。
これは、デプロイ後の問題発生時などに非常に役立ちます。
こちらの公式ドキュメントにやり方について記載がありますが、ロールバックは atlas migrate down コマンドで行えます。
まずドライランモードで挙動を確認してみましょう。

atlas migrate down --dry-run --env demo

ドライランの結果、直前のマイグレーションを取り消す処理(今回のケースでは commits テーブルの削除)のログが出力されます。

$ atlas migrate down --env demo --dry-run
Migrating down from version 20251010030347 to 20251010023825 (1 migration in total):

  -- checks before reverting version 20251010030347
    -> SELECT NOT EXISTS (SELECT 1 FROM "commits") AS "is_empty"
  -- ok (901.743µs)

  -- reverting version 20251010030347
    -> DROP TABLE "commits"
  -- ok (915.907µs)

  -------------------------
  -- 939.729µs
  -- 1 migration
  -- 1 sql statement

ドライランモードで挙動が確認できたので、実際にロールバックを実行してみます。

atlas migrate down --dry-run --env demo

ドライランモードの時と同じようなログが出力されました。

$ atlas migrate down --env demo
Migrating down from version 20251010030347 to 20251010023825 (1 migration in total):

  -- checks before reverting version 20251010030347
    -> SELECT NOT EXISTS (SELECT 1 FROM "commits") AS "is_empty"
  -- ok (521.776µs)

  -- reverting version 20251010030347
    -> DROP TABLE "commits"
  -- ok (4.899343ms)

  -------------------------
  -- 28.148766ms
  -- 1 migration
  -- 1 sql statement

では実際に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 | repos                  | table | postgres
 public | users                  | table | postgres
(3 rows)
  • 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 "repos" CONSTRAINT "fk_repo_owner" FOREIGN KEY (owner_id) REFERENCES users(id)
  • users テーブルのスキーマ定義 ⇒ 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)
  • atlas_schema_revisions テーブルの内容 ⇒ 最新のバージョン 20251010030347 が無くなった(他の内容によって上書きされた?)
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=
 .atlas_cloud_identifier | 6c907020-5bc0-4830-9fc9-fe0417030ae3 |
(3 rows)

このように1つ前のマイグレーションがきれいにロールバックされているのが確認できました。

ちなみに、--to-version 20251009103327 のようにフラグ指定することで、狙った特定のバージョンに戻すことも可能です。

※ ロールバック後の注意点

ロールバックを実行してDBのスキーマが変更前の状態に戻っても、schemas ディレクトリ内の 3_commits.sql はそのまま残っているので、その状態のまま atlas migrate apply を実行してしまうと、ロールバックで削除したものが復活してしまいます。
そのためロールバック後は、必要に応じてファイルを削除するなり、git revert で変更を取り消すなどするようにしてください。

新規テーブルに初期データをINSERT

AtlasはDBマイグレーションツールであり、基本的にデータのマイグレーション機能は
しかし実際には、リリースプロセスの中で新規に追加したテーブルへ初期データを投入したいというケースもあるかと思います。
このような場合は、(ちょっと搦め手的なやり方になってしまうのですが)以下のように手動でマイグレーションSQLを編集することで対応できます。

まず insert テーブルを作成するため、以下のファイルを追加作成します。

  • schemas/insert.sql
-- Create "insert" table
CREATE TABLE "test" (
  "id" bigint NOT NULL,
  "data" character varying NOT NULL,
  PRIMARY KEY ("id")
);

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

atlas migrate diff add_insert --env temp

いつものようにマイグレーションSQLが作成されるため、ファイル名を控えておきます。

$ atlas migrate diff add_insert --env temp

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

atlas migrate edit コマンドでマイグレーションSQLが編集できるので、末尾にINSERT文を追記して保存します。

atlas migrate edit 20251014074650_add_insert.sql
-- Create "test" table
CREATE TABLE "public"."insert" ("id" bigint NOT NULL, "data" character varying NOT NULL, PRIMARY KEY ("id"));

-- 以下を追記
-- Insert initial data
INSERT INTO "insert" ("id", "data") VALUES
(1, 'Initial Record A'),
(2, 'Initial Record B'),
(3, 'Initial Record C');

この状態で、atlas migrate apply コマンドを実行します。

atlas migrate apply --env demo

コマンド実行の結果、テーブルの作成と INSERT のログが出力されました。

$ atlas migrate apply --env demo
Migrating to version 20251014074650 from 20251010030347 (1 migrations in total):

  -- migrating version 20251014074650
    -> CREATE TABLE "public"."insert" ("id" bigint NOT NULL, "data" character varying NOT NULL, PRIMARY KEY ("id"));
    -> INSERT INTO "insert" ("id", "data") VALUES
       (1, 'Initial Record A'),
       (2, 'Initial Record B'),
       (3, 'Initial Record C');
  -- ok (23.662576ms)

  -------------------------
  -- 30.017242ms
  -- 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.

初期データとして3件のレコードが追加されているのが確認できました。

demo=# SELECT * FROM insert;
 id |       data
----+------------------
  1 | Initial Record A
  2 | Initial Record B
  3 | Initial Record C
(3 rows)

※ 初期データINSERT時の注意点

この機能も便利な機能なのですが、マイグレーションSQLを手動で編集して初期データを挿入すると、前述の atlas migrate down によるロールバックが利用できなくなるという点に注意が必要です。
これはAtlasが、安全性の観点から、レコードが存在するテーブルはロールバック時に削除しない仕様になっているためです。(公式ブログを見ると色々と理由があってこの形になっているようです)
そのため、もしロールバックが必要になった場合は、ロールバックを実行する前に、手動で該当テーブルのレコードを退避または削除しておく必要がある点にも注意してください。


まとめ

学んだことの整理

今回の後編記事を通じて、以下の実践的な知識とTIPSを学びました。

  • DBマイグレーションをCI/CDパイプラインに組込む場合は、CDプロセスで atlas migrate apply を実行することで実現できる。
  • 複数環境へのマイグレーションを行う際は、atlas.hcl に環境設定を追加し、シークレットを平文で記述せず、AWS SSMパラメータストアなどのランタイム変数(runtimevar)を利用すべき。
  • スキーマ定義ファイル(schemas ディレクトリ内)に依存関係がある場合、ファイル名順に処理されるため、ファイル名の付け方に注意が必要。
  • atlas migrate down コマンドにより、マイグレーションのロールバックが可能。ただし、ロールバック後は schemas ディレクトリのファイルを適切に管理する必要がある。
  • 初期データINSERTなどのデータの操作を行いたい場合は、atlas migrate edit でマイグレーションSQLを手動編集することで対応できる。ただし、atlas migrate down によるロールバックは利用できなくなるため、注意が必要。
新規CTA