0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Railsでマイグレーションが実行されなかった原因 - RLS導入時に気づいた落とし穴

0
Last updated at Posted at 2026-05-04

はじめに

Rails 8.1 + PostgreSQL 17 で、マルチテナント対応のタスク管理アプリを開発しているが、RLS関連ではまったのでまとめてみました。
https://github.com/yutnagase/rails_hotwire_opa_tenant_manager

大雑把な構成は以下

  • テナント間のデータ分離にはPostgreSQLのRow Level Security(RLS)を採用し、マイグレーションファイル内で execute を使ってRLSポリシーやロールを定義

  • 開発環境はDevContainer(Docker Compose)で構築しており、PostgreSQL、OPA(Open Policy Agent)、Railsアプリの3コンテナ構成

開発中、検証の為DevContainerを再作成してみようとした
db:create, db:migrate, db:seed を実行し、ブラウザでアクセスしたが、
画面が真っ白で何も表示されない。ターミナルにもエラーは出ない状況

ここから色々ハマってしまったので、ご参考までに経緯を展開させて頂きます

環境

項目 バージョン / 構成
Ruby 3.4
Rails 8.1
PostgreSQL 17(RLS有効)
開発環境 DevContainer(Docker Compose)
認証 Devise + omniauth-auth0
認可 Open Policy Agent(OPA)
マルチテナント acts_as_tenant + PostgreSQL RLS

背景

このアプリでは、RLSによるテナント分離のために以下の設計を採用している。

  • DBへの接続は postgres(スーパーユーザー / BYPASSRLS)で行う
  • リクエスト処理時に around_action 内で SET ROLE rails_user に切り替える
  • rails_userNOSUPERUSER / NOBYPASSRLS なので、RLSポリシーが適用される
  • リクエスト終了時に RESET ROLE でスーパーユーザーに戻す

rails_user ロールの作成は、マイグレーションファイル 20260426025740_create_rls_role.rb で行う設計だった。

修正前のマイグレーションファイル: 20260426025740_create_rls_role.rb

class CreateRlsRole < ActiveRecord::Migration[8.1]
  def up
    role = ENV.fetch("RLS_ROLE", "rails_user")
    role_pw = ENV.fetch("RLS_ROLE_PASSWORD", "rails_password")

    execute <<~SQL
      DO $$ BEGIN
        IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '#{role}') THEN
          CREATE ROLE #{role} WITH LOGIN PASSWORD '#{role_pw}' NOSUPERUSER NOBYPASSRLS;
        END IF;
      END $$;
    SQL

    execute "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO #{role};"
    execute "GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO #{role};"
    execute "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO #{role};"
    execute "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO #{role};"
  end

  def down
    role = ENV.fetch("RLS_ROLE", "rails_user")
    execute "ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE ALL ON TABLES FROM #{role};"
    execute "ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE ALL ON SEQUENCES FROM #{role};"
    execute "REASSIGN OWNED BY #{role} TO postgres;"
    execute "DROP OWNED BY #{role};"
    execute "DROP ROLE IF EXISTS #{role};"
  end
end

このマイグレーションファイルで行っているrails_user ロール関連の設定がはまったポイント

症状としては

Dev Container再作成後、ブラウザで何も表示されない症状であった

Railsサーバーを起動し、http://company-a.localhost:8080/ にアクセスしても反応がない。
ターミナル上のPumaのログにもリクエストが記録されていない。

まず、DevContainer内からcurlで直接アクセスを試みた。サブドメインベースのテナント識別を使っているため、Host ヘッダーを明示的に指定している

curl -v -H "Host: company-a.localhost" http://localhost:8080/ > /tmp/response_body.txt 2> /tmp/response_headers.txt

レスポンスヘッダーを確認すると、HTTPステータスは500。

< HTTP/1.1 500 Internal Server Error

レスポンスボディにはRailsのエラー画面がHTMLで返っていた。

<h2>
  PG::InvalidParameterValue: ERROR: role &quot;rails_user&quot; does not exist
</h2>

rails_user というPostgreSQLロールが存在しないことが原因だとここで分かった。
前述の通り、このロールはRLSを機能させるために不可欠な存在である。

調査

マイグレーションは「実行済み」なのにロールがない

マイグレーションのステータスを確認すると、全て up になっている。

$ bin/rails db:migrate:status

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20260426025717  Create tenants
   up     20260426025726  Create users
   up     20260426025732  Create projects
   up     20260426025739  Create tasks
   up     20260426025740  Create rls role
   up     20260426025741  Enable rls policies
   up     20260428120000  Add seed admin to users

しかし、PostgreSQLに直接接続してロール一覧を確認すると、rails_user は存在しない。

$ psql -U postgres -h db -c "\du"
                             List of roles
 Role name |                         Attributes
-----------+------------------------------------------------------------
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS

マイグレーションは up なのにロールがない。
当初の期待通り、ロールが作成出来ていない模様

仮説と検証

マイグレーションSQL自体に問題があるのか

まず、マイグレーション内のSQLに問題がないか確認するため、CREATE ROLE を直接実行してみた。

$ psql -U postgres -h db -c "CREATE ROLE rails_user WITH LOGIN PASSWORD 'rails_password' NOSUPERUSER NOBYPASSRLS;"
CREATE ROLE

$ psql -U postgres -h db -c "\du"
                              List of roles
 Role name  |                         Attributes
------------+------------------------------------------------------------
 postgres   | Superuser, Create role, Create DB, Replication, Bypass RLS
 rails_user |

問題なく作成できる。SQL自体に誤りはない。

ロールを削除して、マイグレーションファイルの検証

次に、ロールを削除した上で schema_migrations からレコードを消し、マイグレーションを強制的に再実行してみた。

$ psql -U postgres -h db -c "DROP ROLE rails_user;"
$ psql -U postgres -h db -d tenant_manager_development \
    -c "DELETE FROM schema_migrations WHERE version = '20260426025740';"
$ bin/rails db:migrate:up VERSION=20260426025740
== 20260426025740 CreateRlsRole: migrating ====================================
-- execute("DO $$ BEGIN\n  IF NOT EXISTS ...")
   -> 0.0655s
-- execute("GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES ...")
   -> 0.0045s
...
== 20260426025740 CreateRlsRole: migrated (0.0800s) ===========================

マイグレーションが実行され、ロールも作成された。マイグレーションファイルのコード自体には問題がない模様

再度、DevContainer再作成後、マイグレーションを試す

ここで視点を変えて、クリーンな状態からの db:createdb:migrate の出力を見返す。

$ bin/rails db:create
Created database 'tenant_manager_development'
Created database 'tenant_manager_test'
$ bin/rails db:migrate
$

db:migrate の出力結果が完全に空であった

通常、マイグレーションが実行されれば == 20260426025717 CreateTenants: migrating === のようなログが表示されるはずだが、何も出ていないということは、マイグレーションが1つも実行されていない。

にもかかわらず db:migrate:status では全て up になっている。

通常、db:migrate が何もせずに終わるのは、schema_migrations テーブルに全バージョンが登録済みで、Railsが「DBは最新状態」と判断している場合である。この状態は以下の経路で発生しうる。

  • schema.rb のロード
  • structure.sql のロード
  • 手動で schema_migrations にバージョンをINSERT

今回はスキーマファイルのロードによってこの状態が作られていた。

原因判明

問題の本質は、「schema.rbに含まれないこと」ではなく、「マイグレーションが実行されなかったこと」であった。

Railsのデフォルト設定(config.active_record.schema_format = :ruby)では、db:migrate 実行後に db/schema.rb が自動生成される。このファイルにはActiveRecord DSLで表現されたテーブル定義は含まれているが、ここに CREATE ROLEGRANTENABLE ROW LEVEL SECURITYCREATE POLICY は一切含まれていない。schema.rb はActiveRecord DSLで表現できるものしか保持しない仕様の様だ

ActiveRecord::Schema[8.1].define(version: 2026_04_28_120000) do
  enable_extension "pg_catalog.plpgsql"

  create_table "projects", force: :cascade do |t|
    t.string "name", null: false
    t.bigint "tenant_id", null: false
    # ...
  end

  create_table "tasks", force: :cascade do |t|
    # ...
  end

  # ...
end

なお、db:create はデータベースを作成するだけで、schema.rb のロードは行わない。実際に db:create 直後にDBを確認すると、schema_migrations テーブルすら存在しなかった。

$ bin/rails db:create
Created database 'tenant_manager_development'

$ psql -U postgres -h db -d tenant_manager_development -c "SELECT * FROM schema_migrations;"
ERROR:  relation "schema_migrations" does not exist

理解した仕様としては
schema.rb(または structure.sql)をロードしているのは db:migrate の処理フローの中で行っている模様。Rails 8.1 では、db:migrate 自体が明示的に schema.rb をロードするわけではない。

しかし、空のデータベースに対してはRailsが「未初期化状態」と判断する可能性が有る。その際は、マイグレーションを実行する代わりに、スキーマファイル(schema.rb / structure.sql)をロードしてデータベースを最新状態に揃える最適化が行われる挙動の様に見えた。この時点にて、テーブルが作成されると同時に schema_migrations テーブルにも全マイグレーションのバージョンが記録される。

この挙動はRailsの内部最適化によるものであり、db:migrate が常にマイグレーションファイルを実行するとは限らない点に注意が必要である。

その結果、マイグレーションファイルは1つも実行されない。execute で書いた CREATE ROLEENABLE ROW LEVEL SECURITY も、一度も実行されないまま up と表示される。

今回はまった原因

結論から言うと、RLSを使う時点で、CRUD系アプリを構築する感覚でマイグレーションファイルを扱ってはいけなかった。
schema.rb の扱いで期待通りに動作しない。RLSの導入で以下行おうとしたが、ActiveRecord DSLで以下は実現できない。

  • ENABLE ROW LEVEL SECURITY
  • CREATE POLICY
  • CREATE ROLE
  • GRANT

これらは全て execute で生SQLを書くしかなく、schema.rb には一切反映されない。つまり、RLSをマイグレーションで管理しようとした時点で、schema_format = :sql への切り替えは必須だった。

通常のCRUD系Railsアプリでは、マイグレーションで使うのは create_tableadd_columnadd_index などのActiveRecord DSLだけである。これらは全て schema.rb に反映されるので、schema.rb からのロードでもマイグレーション実行でも結果は同じになる。CRUD系アプリではあまり、表面化しないと思われる。

また、RLS以外でも、execute が必要になるケースはある。

  • トリガー / ストアドファンクション
  • カスタムのCHECK制約
  • テーブルパーティショニング
  • PostgreSQL固有の拡張機能(pg_trgm等)
  • CREATE ROLE / GRANT などの権限管理

これらを使うプロジェクトでは、schema.rb では不十分であり、structure.sql を使う必要がある。

解決策

1. schema_format を :sql に変更する

config/application.rb に以下を追加する。

module Workspace
  class Application < Rails::Application
    # ...
    config.active_record.schema_format = :sql
  end
end

これにより、db:migrate 後に db/schema.rb ではなく db/structure.sqlpg_dump の出力)が生成されるようになる。structure.sql にはRLSポリシー、GRANT文、トリガーなど、execute で実行した内容も含まれる。

新規環境でも、空のDBに対して db:migrate を実行すれば structure.sql からロードされるため、RLSポリシーやGRANT文が正しく再現される。

2. CREATE ROLE はDBコンテナの初期化スクリプトで行う

structure.sql にも含まれないものが1つある。CREATE ROLE だ。

PostgreSQLのロールはクラスタレベルのオブジェクトであり、特定のデータベースに属さない。pg_dump はデータベース単位のダンプなので、ロール定義は出力されない。

そもそも、CREATE ROLE をマイグレーションに含めること自体が責務分離として不自然である。

  • マイグレーション → DBスキーマの管理(テーブル、インデックス、ポリシー等)
  • ロール管理 → インフラ / DB運用レイヤー

ロールは環境によってパスワードや権限が異なることが多く、マイグレーションの再現性も壊れやすい。Dockerの初期化スクリプトやIaCで管理するのが適切だ。

PostgreSQL公式Dockerイメージは、/docker-entrypoint-initdb.d/ ディレクトリに置いたSQLファイルをコンテナ初回起動時に自動実行する仕組みを持っている。

db/init/01_create_rls_role.sql を作成する。

DO $$ BEGIN
  IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'rails_user') THEN
    CREATE ROLE rails_user WITH LOGIN PASSWORD 'rails_password' NOSUPERUSER NOBYPASSRLS;
  END IF;
END $$;

.devcontainer/docker-compose.ymldb サービスにマウントを追加する。

db:
  image: postgres:17
  environment:
    POSTGRES_PASSWORD: password
  ports:
    - "5432:5432"
  volumes:
    - pgdata:/var/lib/postgresql/data
    - ../db/init:/docker-entrypoint-initdb.d # 追加
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U postgres"]
    interval: 5s
    timeout: 5s
    retries: 5

なお、マイグレーションファイルからも CREATE ROLE を削除し、GRANT文のみ残す形に修正した。ロール作成はインフラレイヤーの責務として完全に分離している。

修正後の検証

ボリュームを完全に削除してDevContainerを再作成し、クリーンな状態から実行した。

root@9a9119c09157:/workspace# bin/rails db:drop
Dropped database 'tenant_manager_development'
Dropped database 'tenant_manager_test'
root@9a9119c09157:/workspace# bin/rails db:create
Created database 'tenant_manager_development'
Created database 'tenant_manager_test'
root@9a9119c09157:/workspace# bin/rails db:migrate
root@9a9119c09157:/workspace# bin/rails db:migrate:status

database: tenant_manager_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20260426025717  Create tenants
   up     20260426025726  Create users
   up     20260426025732  Create projects
   up     20260426025739  Create tasks
   up     20260426025740  Create rls role
   up     20260426025741  Enable rls policies
   up     20260428120000  Add seed admin to users

root@9a9119c09157:/workspace# bin/rails db:seed
Seed completed:
  Tenants:  2
  Users:    2
  Projects: 3
  Tasks:    7
root@9a9119c09157:/workspace# psql -U postgres -h "db" -c "\du"
Password for user postgres: 
                              List of roles
 Role name  |                         Attributes                         
------------+------------------------------------------------------------
 postgres   | Superuser, Create role, Create DB, Replication, Bypass RLS
 rails_user | 

db:migrate でマイグレーションが正しく実行され、rails_user ロールも作成されている。

Railsマイグレーション仕様の整理

今回の件で、Railsのマイグレーションの仕様について改めて整理できた。

対象 schema.rb structure.sql マイグレーション実行
create_table / add_column 等 含まれる 含まれる 実行される
execute(RLS / トリガー等) 含まれない 含まれる 実行される
CREATE ROLE(クラスタレベル) 含まれない 含まれない 実行される

「マイグレーション実行」の列は全て「実行される」だが、スキーマファイル(schema.rb / structure.sql)が存在する環境では、db:migrate が空のDBに対してスキーマファイルをロードする最適化が働き、マイグレーション自体がスキップされるため、2行目と3行目は実質的に実行されない。これが今回の落とし穴だった。

execute を使うプロジェクトでは schema_format = :sql にする必要が有る
CREATE ROLE のようなクラスタレベルの操作はDBコンテナの初期化スクリプトに分離すべき

この2点を押さえておけば、今回の問題は回避できたと思う

まとめ

  • Railsのデフォルト設定(schema_format = :ruby)では、schema.rb にActiveRecord DSLで表現できない execute の内容は保持されない
  • 空のDBに対して db:migrate を実行すると、Railsの内部最適化によりスキーマファイルがロードされ、schema_migrations に全バージョンが記録されるため、マイグレーションが全てスキップされる
  • 結果として、execute で書いた CREATE ROLEENABLE ROW LEVEL SECURITYCREATE POLICY などは一度も実行されない
  • 通常のCRUD系アプリでは schema.rb で十分だが、RLSやトリガーなどを使うプロジェクトでは config.active_record.schema_format = :sql が必須
  • PostgreSQLのロール(CREATE ROLE)はクラスタレベルのオブジェクトであり、structure.sql にも含まれないため、DBコンテナの初期化スクリプト(docker-entrypoint-initdb.d)で作成する必要がある

大事なこと

  • Railsのマイグレーションは「DB構造」を管理するものであり、「DBの振る舞い(権限・ポリシー)」は完全には表現できない
  • DB固有機能(RLS、トリガー、ロール)を使う場合、schema.rb のみの想定では失敗する
  • Railsのマイグレーションだけで完結させようとせず、インフラとの責務分離を考えて設計する必要がある
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?