くりにっき

フルスタックキュアエンジニアです

Railsでdb:migrateの時だけdatabase.ymlを自動で差し替える

前置き

GitLabでPostgreSQLを使う時にPgBouncer経由でDBに接続してる(database.ymlにpgbouncerの接続先を書いている)のですが、 その状態で rake db:migrate すると下記のようなエラーが出ます。

ActiveRecord::ConcurrentMigrationError: 

Failed to release advisory lock


  from active_record/migration.rb:1385:in `with_advisory_lock'
  from active_record/migration.rb:1229:in `migrate'
  from active_record/migration.rb:1061:in `up'
  from active_record/migration.rb:1036:in `migrate'
  from active_record/tasks/database_tasks.rb:238:in `migrate'
  from active_record/railties/databases.rake:86:in `block (3 levels) in <top (required)>'
  from active_record/railties/databases.rake:84:in `each'
  from active_record/railties/databases.rake:84:in `block (2 levels) in <top (required)>'
  from rake/task.rb:273:in `block in execute'
  from rake/task.rb:273:in `each'
  from rake/task.rb:273:in `execute'
  from rake/task.rb:214:in `block in invoke_with_call_chain'
  from monitor.rb:235:in `mon_synchronize'
  from rake/task.rb:194:in `invoke_with_call_chain'
  from rake/task.rb:183:in `invoke'
  from rake/application.rb:160:in `invoke_task'
  from rake/application.rb:116:in `block (2 levels) in top_level'
  from rake/application.rb:116:in `each'
  from rake/application.rb:116:in `block in top_level'
  from rake/application.rb:125:in `run_with_threads'
  from rake/application.rb:110:in `top_level'
  from rake/application.rb:83:in `block in run'
  from rake/application.rb:186:in `standard_exception_handling'
  from rake/application.rb:80:in `run'
  from bundle/ruby/2.6.0/gems/rake-12.3.3/exe/rake:27:in `<top (required)>'
  from bundle/ruby/2.6.0/bin/rake:23:in `load'
  from bundle/ruby/2.6.0/bin/rake:23:in `<main>'

https://docs.gitlab.com/omnibus/update/README.html

If you’re using PgBouncer:

You’ll need to bypass PgBouncer and connect directly to the database master before running migrations.

とあるように db:migrate する時だけPgBouncerを使わず直接PostgreSQLにつなぐのが大正解です。

しかし

  • GitLabのバージョンアップの時だけdatabase.ymlを書き換えるのは面倒
    • 忘れがちなので自動化すべき
    • 運用でカバーするのは時代遅れなので仕組み化すべき
  • いつもCIからのデプロイでGitLabのバージョンアップを行っているのだが、database.ymlの接続先をPostgreSQLにしてデプロイ&自動でdb:migrate(1回目のデプロイ)と接続先をPgBouncerに戻す作業(2回目のデプロイ)で2回デプロイするのが面倒
    • 忘れがちなので(ry
    • 仮に2回デプロイ行うとしても一時的にPostgreSQLのコネクションが少なくなるのでオンラインでバージョンアップを行うとコネクションが足りずにエラーが多発する可能性がある
      • workerの台数によるけど常時100コネクション近く張り付いてる状態なのでメンテいれずにpgbouncer外すのは厳しいと判断
    • SwarmでBlue/GreenデプロイでGitLabをノーメンテでバージョンアップするのに慣れすぎて*1 、バージョンアップ程度でメンテ入れるのが嫌

という理由で、今のデプロイフローをなるべく変えずにどうにかdb:migrateの時だけ自動で接続先を切り替える方法がないか考えました。

モンキーパッチの縛りプレイの内容

モンキーパッチを作るにあたって下記のような縛りプレイの条件がありました

  • RailsやGitLabのバージョンアップでなるべく壊れないようにする
  • Dockerイメージで配布されているGitLabを使っているので *2既存のソースは全て変更不能
    • Dockerイメージの時点でモンキーパッチなんて不可能だと思われがちですが、GitLabのアプリケーション本体はRailsなので config/initializers/lib/tasks にファイルをmountすればRailsが自動で読み込むのでモンキーパッチを適用することができます *3
  • 既存のソースが変更できないのでGitLabに入っていないgemを使うのは難しい
    • bundle installされた状態でDockerイメージとして配布されてるので、そこに新しいgemを追加するのは茨の道
  • 利用しているDockerイメージは https://github.com/sameersbn/docker-gitlab でだが、起動時の処理はDockerイメージ内のスクリプト *4 にあるため、そこに特定の処理を差し込むのはほぼ不可能

モンキーパッチ

このようなモンキーパッチを

version: '3.7'
services:
  gitlab:
    volumes:
      - "./monkey_patch/db_migrate_monkey_patch.rake:/home/git/gitlab/lib/tasks/db_migrate_monkey_patch.rake:ro"

のような感じでDockerコンテナ内にmountして使っています。

注意点

  • かなり黒魔術なので自己責任でお願いします
  • database.ymlの中身は適宜書き換えてください
  • オンラインで実行しても問題ないはずですが、 rake db:migrate 実行中にpumaやunicornをrestartすると意図しない設定が読み込まれるので注意してください

モンキーパッチのポイント

rake taskの実体は Rake::Taskインスタンスで、rake task実行時には Rake::Task#invoke が呼ばれます。

そのため Rake::Task["db:migrate"].invoke という特異メソッドに対してモンキーパッチ用のmoduleを prependすることで rake db:migrate 実行時のみに特定の処理を差し込んでいます。

ボツ案1

Rake::Task["db:migrate"].enhance(["db:migrate:before"]) do
  Rake::Task["db:migrate:after"].invoke
end

みたいな感じで db:migrate:before でdatabase.ymlを書き換えて db:migrate:after で元に戻すという方法もあったのですが、これだと db:migrate でエラーになった時に db:migrate:after が実行されずにdatabase.ymlが元に戻せなくなる不具合があるのでボツになりました。

そのため、上の方で書いているように Rake::Task["db:migrate"].invoke に対してモンキーパッチをあてるしかないと思ってます。

ボツ案2

begin
  Rake::Task["db:migrate"].enhance(["db:migrate:before"])
ensure
  # ロールバック処理
end

これだとrake taskの定義時に ensure が評価されてrake taskの実行時には ensure は評価されないので意図通りに動かないです。

あつ森でDQ3のアリアハン大陸を作った

島の全体図

Before

f:id:sue445:20200527134508p:plain

島改装前のスクショがこれしか残ってなかった。改装直前は建物の他に橋や坂も結構設置してました

After

f:id:sue445:20200526230649j:plain

f:id:sue445:20200527200518j:plain

頑張って作った割には似てないorz

下のスクショはswitch版のDQ3のものです

動かせる建物は全部動かしたので2~3週間くらいかかってます

内部の解説

アリアハンの城下町

街の南西に自宅

f:id:sue445:20200526230726j:plainf:id:sue445:20200527194334j:plain

北西にルイーダの酒場(博物館)

酒場なので仲間が待機してます

f:id:sue445:20200526230843j:plainf:id:sue445:20200527194351j:plain

ちょい東に道具屋(タヌキ商店)

f:id:sue445:20200526231021j:plainf:id:sue445:20200527194405j:plain

写真の左下が宿屋で右下が武具屋(仕立て屋)

f:id:sue445:20200526231054j:plainf:id:sue445:20200527194451j:plain

橋を渡って池の中にあるのがお城(案内所)

どうでもいいですが島の名前がキュアスカーレッ島なので キュアスカーレット の旗です。

f:id:sue445:20200526231130j:plainf:id:sue445:20200527194413j:plain

城下町の南西に井戸

f:id:sue445:20200526231227j:plain

城下町の北東に教会

f:id:sue445:20200526231252j:plainf:id:sue445:20200527194505j:plain

教会なので賛美歌が流れてます

f:id:sue445:20200526231301j:plain

岬の洞窟、ナジミの塔

アリアハンの城下町の西にはみんなおなじみのナジミの塔 *1

f:id:sue445:20200526231357j:plainf:id:sue445:20200527194523j:plain

街を全部配置した時に住民の家が1件だけ余ってしまったので岬の洞窟の位置に置いてます

f:id:sue445:20200526232529j:plain

レーベの村

村の入口。主人公の上にあるのが道具屋

f:id:sue445:20200526231445j:plainf:id:sue445:20200527194614j:plain

道具屋の隣の家と池

f:id:sue445:20200526231504j:plain

北東の家には馬がいます

f:id:sue445:20200526231547j:plainf:id:sue445:20200527194622j:plain

村の南側の宿屋と武具屋

f:id:sue445:20200526231606j:plainf:id:sue445:20200527194629j:plain

南東部の岩

f:id:sue445:20200526231620j:plainf:id:sue445:20200527194637j:plain

いざないの洞窟

いざないの洞窟の南の祠(キャンプサイト

f:id:sue445:20200526231804j:plainf:id:sue445:20200527194817j:plain

いざないの洞窟

f:id:sue445:20200526231824j:plainf:id:sue445:20200527194822j:plain

余談

これは完全に偶然なんですが今日ドラクエ1の誕生日でした。おめでとうございます!

*1:これを言いたかっただけ

Cloud Runでは/_ah/で始まるパスは使えない

タイトルが全て

事象

Cloud RunにDockerコンテナをデプロイしたが、gcloud run deploy --allow-unauthenticated *1 で全公開にしてるにも関わらず /_ah/health (コンテナ内で動いてるアプリのヘルスチェック用のエンドポイント)を呼ぼうとして401エラーになっていた。

$ curl -s https://xxxxxxxxxxxx.a.run.app/_ah/health

<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>401 Unauthorized</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Unauthorized</h1>
<h2>Your client does not have permission to the requested URL <code>/_ah/health</code>.</h2>
<h2></h2>
</body></html>

原因

別のパスだと呼べたのでおそらくGCP側の仕様。

ググった範囲だとCloud Runの公式ドキュメントにはどこにも書かれていなかったが、Google App Engineだと /_ah/start/_ah/warmup が特別なパスとして予約されていてIAMの権限がないとアクセスできない仕様になっていたので*2、おそらく同じ仕様がCloud Runでも適用されていると思われる。

Dockerizeする前の元のアプリで /_ah/health実装済だったのでそれを使っていたのだが、まさか --allow-unauthenticated が効かないとは思わなかった、、、

公式ドキュメントに書いてないのはハマるのでこの事象はフィードバックで報告しています。

CircleCIでマトリックスビルドがサポートされたのでさっそく使ってみた

公式ドキュメント

実際に使ったPR

github.com

github.com

これはビルドマトリクスの軸が1つだけなので大したリファクタリングになってないですが、軸が2つ以上になった時にかなり .circleci/config.yml がシンプルになると思われます。

所感

過去にブログ

  • ビルドマトリクスの軸が2つ以上になる場合はTravis CIの方が圧倒的に楽
    • というか、CircleCIやGitLab CIで2軸以上のマトリクステストは人間がメンテできる気がしない、、、

って書きましたが、これでだいぶ改善されました。

社内ライブラリをメンテしているとついついビルド1回で30個以上のジョブが作られるような超巨大マトリクスビルドを作りがちですが、CircleCIにはパフォーマンスプランがあるのでこれからはオートスケールの並列数で殴れるようになるのではないかと思っています。

余談

【CVE-2020-2179】Jenkins Yaml Axis Plugin v0.2.1をリリースした

昔作ったJenkins Yaml Axis Pluginにリモートコード実行の脆弱性が見つかったので修正してv0.2.1をリリースしています。ご利用の方は速やかにアプデお願いします。

https://github.com/jenkinsci/yaml-axis-plugin/blob/master/CHANGELOG.md#021-2020-04-07

jenkins.io

gitpanda v0.9.0をリリースした

https://github.com/sue445/gitpanda/blob/master/CHANGELOG.md#v090

最近のGitLabでは https://gitlab.com/gitlab-org/gitlab/-/merge_requestshttps://gitlab.com/gitlab-org/gitlab/-/issues みたいに /-/ が含まれるURLになっているようなので*1、そういった形式のURLに対応するようにしました

chatwork-ruby 0.12.2とpixela 1.4.1をリリースした

それぞれfaraday v1.0対応です。

https://github.com/asonas/chatwork-ruby/blob/master/CHANGELOG.md#v0122

https://github.com/sue445/pixela/blob/master/CHANGELOG.md#v141

実はfaraday v1.0対応自体は1月くらいにはできていてfaraday_middleware v1.0.0が出るのを待っていたのですが、なかなかリリースされないしv1.0.0.rc1があるしまあいいかくらいの軽いノリで出しました。

どっちのgemもfaraday 0系と1系両方対応してますがメンテつらくなったタイミングで0系のサポートをきります