PumaのPhased Restartで環境変数を再読込する
PumaのPhased Restartで環境変数の読み込みに挑戦したお話です。検証していませんが、UnicornのGraceful Restartでも同じ手が使えるはずです。
TL; DR
on_worker_fork
でフックすることで環境変数を無理やり更新することは可能- 本番での導入は見送ったので注意
Phased Restartでは環境変数が更新されない
Pumaにはダウンタイムなしでアプリケーションコードを更新するPhased Restartという機能があります(UnicornにもGraceful Restartという同じような機能があります)。
Phased RestartではMaster Processを使い回すため、新しいWorker Processはサーバー起動時のMaster Processからフォークされます。結果として、デフォルトで環境変数はPhased Restartしても更新されません。
on_worker_forkで環境変数を上書きする
/rails_app
|_/config
|_/puma
|_printenv.rb
|_puma.rb
|_env.sh
puma.rb
...
on_worker_fork do
io = IO.popen(". env.sh > /dev/null && bundle ex ruby config/puma/printenv.rb")
env = io.gets io.close
unless $?.success?
raise 'Fails to load and print new env'
end ENV.replace(eval(env))
end
...
printenv.rb
puts ENV.to_h
まず前提として env.sh
というファイルに新しい環境変数が入っているとします。 ここでは以下の手順で環境変数を更新しています。
on_worker_fork
でフォークされたWorker Processの子プロセスでprintev.sh
を実行する- 親プロセス(Worker Process)ではパイプを繋いで親プロセスの標準出力を取得する
- 標準出力をRubyのHashへ変換(
eval
)して現在のProcessの環境変数を置き換える
あと安全側に倒すのであれば、 ENV.replace(eval(env))
のところはロック取っておいたほうがいいかもしれません(PumaはWorkerをフォークする際シングルスレッドで動くため必要はないはず)。
あと上のコードだと環境変数の削除には対応できないため、削除に対応する必要がある場合は少しゴニョゴニョする必要がありそうです。
結局どうしたか
以上のコードはテスト環境で期待通りに動いていることは確認したのですが、結局以下の理由から本番環境への導入は見送りました。
eval
の部分が本番環境へ導入するには心理的に抵抗がある- Pumaを再起動するジョブを用意して運用でカバーできる
eval
の方は言わずもがな。運用でカバーできるというのは、環境変数を同期的に更新する必要がある場合はPumaを再起動するデプロイジョブを走らせ、それ以外の場合はPhased Restartを使うジョブでデプロイするという感じです。