日々雑記

旅行の事ばっかり。神社成分多め。
Recent Tweets @

小ネタ。

Rails 5から、production environmentでのDB破壊系のtaskの実行を防止する仕組みが入りました。

例えば、production environmentでdb:dropを実行しようとすると、下記のようなエラー(ActiveRecord::ProtectedEnvironmentError)が発生します。

$ RAILS_ENV=production ./bin/rails db:drop
rails aborted!
ActiveRecord::ProtectedEnvironmentError: You are attempting to run a destructive action against your 'production' database
If you are sure you want to continue, run the same command with the environment variable
DISABLE_DATABASE_ENVIRONMENT_CHECK=1

これは見ての通り、誤ってproduction environmentのDBを破壊してしまうのを防ぐ為の仕組みです(因みにPRのコメントによると、実際にHerokuで誤ってDBを破壊してしまい復旧をお願いしたい、という依頼は割ときていたそうです)。

意図的にproduction environmentのDBを破壊したい場合は、エラーメッセージにも表示されている通り、環境変数 DISABLE_DATABASE_ENVIRONMENT_CHECK=1指定してあげればOKです。

防止処理の対象となるtaskは下記の通りです。

  • db:drop
  • db:drop:all
  • db:purge
  • db:purge:all
  • db:purge:test

デフォルトではproduction environmentのみが防止対象となっていますが、これは変更可能となっています。production以外のenvironmentを指定したい場合は、ActiveRecord::Base.protected_environmentsに防止対象となる environmentを指定してあげればOKです。

# config/application.rb
ActiveRecord::Base.protected_environments = %w(production staging)
$ RAILS_ENV=staging ./bin/rails db:drop
rails aborted!
ActiveRecord::ProtectedEnvironmentError: You are attempting to run a destructive action against your 'staging' database
If you are sure you want to continue, run the same command with the environment variable
DISABLE_DATABASE_ENVIRONMENT_CHECK=1

ActiveRecord::Base.protected_environmentsにはArrayを指定出来ますので、当然複数のenvironmentsを防止対象とする事が可能です(値は上書きされるので、production以外にプラスして environmentを指定したい場合、productionを明示的に指定する必要があります)。

因みに、environmentsのチェックは、最後にmigration処理等を実施したenvironmentとの比較で行われます。

これはどういう事かというと、Rails 5から、db:migrateや、schemaのload処理を行った場合、その処理を行ったenvironmentの情報をDBに保持するようになっています。

具体的には、Rails内部でar_internal_metadataという名前のテーブル(デフォルト場合)が作成され、そこにenvironmentの情報が保持されるようになっています。

irb(main):001:0* ActiveRecord::InternalMetadata.all
  ActiveRecord::InternalMetadata Load (0.2ms)  SELECT "ar_internal_metadata".* FROM "ar_internal_metadata"
=> #<ActiveRecord::Relation [#<ActiveRecord::InternalMetadata key: "environment", value: "development", created_at: "2016-04-01 08:15:10", updated_at: "2016-04-01 08:15:10">]>

※ table_name_prefix / table_name_suffixが設定されていた場合、その値がテーブル名の前後に付きます。また、テーブル名はActiveRecord::Base.internal_metadata_table_nameで変更可能です。 更にいうと、Rails 5.0.0.beta2までは、デフォルトのテーブル名はactive_record_internal_metadatas という名前でした。

ar_internal_metadataテーブルに格納されている値と、処理を実行しようとするenvironmentを比較するので、当然ar_internal_metadataテーブルが無いと処理が行えません。

そのため、ar_internal_metadataが無い状態(Rails 4からRails 5に更新して、一度もdb:migrateを実行していない状態の場合)でdb:drop等の破壊系のtaskを実行しようとすると、Environmentの情報が無い旨エラー(ActiveRecord::NoEnvironmentInSchemaError)が発生します。

$ ./bin/rails db:drop
rails aborted!
ActiveRecord::NoEnvironmentInSchemaError:

Environment data not found in the schema. To resolve this issue, run:

    bin/rails db:environment:set RAILS_ENV=development

これは防止対象のenvironmentであるかどうかに関係無く発生します。

解消するには、エラーメッセージに表示されている通りdb:environment:set taskを実行してあげればOKです(db:migrateを実行するとかでもOKです)。

因みに、何らかの理由によりar_internal_metadataテーブルのenvironmentに、処理を実行しよとしているenvironmentと異なる値が入っていた場合、これもまたエラー(ActiveRecord::EnvironmentMismatchError)となります。

例えば、development environmentのar_internal_metadataテーブルのenvironmentに、testという値が入ってしまっていたとします。

irb(main):001:0* Rails.env
=> "development"
irb(main):002:0> ActiveRecord::InternalMetadata.all
  ActiveRecord::InternalMetadata Load (0.2ms)  SELECT "ar_internal_metadata".* FROM "ar_internal_metadata"
=> #<ActiveRecord::Relation [#<ActiveRecord::InternalMetadata key: "environment", value: "test", created_at: "2016-04-01 11:35:11", updated_at: "2016-04-01 11:35:31">]>
irb(main):003:0>

この状態でDB破壊系のtaskを実行すると以下のようになります。

$ ./bin/rails db:drop
rails aborted!
ActiveRecord::EnvironmentMismatchError: You are attempting to modify a database that was last run in `test` environment.
You are running in `development` environment.If you are sure you want to continue, first set the environment using:

    bin/rails db:environment:set RAILS_ENV=development

こちらの場合も、エラーメッセージに出ている通りにdb:environment:set taskを実行してあげればOKです。

異なるenvironmentsで同じDBを使っている場合に、地味にトラブりそうな気がします。

雑感

Rails 5に入ったDB破壊系taskの防止処理がデフォルトで提供されるようになったの自体は大変良い事かと思います。完全に余談なのですが、同じような事を行う為のgemを作ったことがあるのですが、不要になりました。いや良いことなんですけどね。

あとちょいちょい ActiveRecord::EnvironmentMismatchError がおきてしまう気がする…。

参考

  1. kennakatm09tdからリブログしました
  2. atm09tdy-yagiからリブログしました
  3. y-yagiの投稿です