Unicorn の graceful restart と環境変数
Unicorn の graceful restart は無停止でのデプロイを可能にして非常に便利だが、fork を用いて実装されている都合で古いプロセスから新しいプロセスに環境変数が引き継がれるため、そのことに起因するトラブルがいくつかある。
dotenv の設定が書き変わらない
設定情報を dotenv で管理している人も多いと思うけど、環境変数を使っているので罠がある。
例えば最初に .env に MEMCACHE_SERVERS=memcache-server-001:11211
と書いてあったとする。
このとき Unicorn を起動すると、dotenv によって MEMCACHE_SERVERS=memcache-server-001:11211
が環境変数に追加される。
その後、接続先として memcache-server-002:11211 を追加したくなって .env を編集して MEMCACHE_SERVERS=memcache-server-001:11211,memcache-server-002:11211
に変える。
ここで Unicorn を graceful restart すると、古い MEMCACHE_SERVERS
の値は次のプロセスにも引き継がれ、dotenv はデフォルトでは既にある環境変数を上書きしないため、新しいプロセスでも MEMCACHE_SERVERS
の値は古いままで、memcache-server-002:11211 が追加されない。
Bundler のバージョンが上がらない
Bundler は環境変数 RUBYLIB
と RUBYOPT
を指定することで、Ruby の起動時に Bundler を読み込ませセットアップを行うようなしくみになっている。
RUBYLIB
には初回起動時の Bundler の libdir が入っており、Unicorn を graceful restart してもこの環境変数が引き継がれるため、同じく引き継がれた RUBYOPT=-rbundler/setup
によりずっと初回起動時の Bundler の libdir から bundler/setup がロードされ続ける。
解決策
どちらも before_exec
でなんとかすることができそう。
dotenv については Dotenv.overload
を呼ぶことで .env の内容で環境変数を上書きできる。
なので、before_exec
でこれを呼ぶことによって新しい設定が使われるようにできる。
もし .env 以外の方法でセットしている環境変数があった場合、そこでセットされたものよりも .env が優先されるようになってしまうけど、まぁ .env 使ってるときにそういう環境変数は無いと思う。
before_exec do |server| Dotenv.overload end
Bundler については RUBYLIB
を最新の Bundler を指すように書き換えてやればいいわけで、ややトリッキーな方法だが以下のような before_exec
を書くことでできそう。
もしデフォルトで RUBYLIB
, RUBYOPT
, GEM_HOME
が存在するような環境なら、env.delete
のかわりにその値をセットするようにすればいいはず。
before_exec do |server| env = ENV.to_hash %w[RUBYLIB RUBYOPT GEM_HOME].each do |key| env.delete(key) end rubylib = IO.popen([env, 'bundle', 'exec', 'ruby', '-e', 'puts ENV["RUBYLIB"]', unsetenv_others: true], &:read).chomp ENV['RUBYLIB'] = rubylib end
追記
RUBYLIB
を出力するときにわざわざ ruby を使う必要は無いという指摘があった。たしかに bundle exec env
でよさそう。
before_exec do |server| env = ENV.to_hash %w[RUBYLIB RUBYOPT GEM_HOME].each do |key| env.delete(key) end rubylib = IO.popen([env, 'bundle', 'exec', 'env', unsetenv_others: true], &:read).slice(/^RUBYLIB=(.+)$/, 1).chomp ENV['RUBYLIB'] = rubylib end