CloudFront配下にRailsを置いたときにSSL対応するMiddleware
最近CloudFront配下にアプリケーションサーバーを置くのが徐々に流行ってる気がします。
想定する構成としては、ブラウザが直接叩くのがCloudFrontで、その直下にELB、そのさらに下に直接pumaというような形式。 nginx使わないパターンですね。 assets以下はどのみちCloudFrontでキャッシュされるから、nginxで高速化するまでもないっていうのがこの設計の思想かなと思ってます。 pumaが重くならない程度に同時接続数を制御する役割もELBがになってくれますし。
さて、ともかくそんな構成にすると、SSL対応で手間取ったのでメモ。
バージョンは以下の想定です。
- Rails 5.1.2 (4系でも変わらないとは思います)
- Puma 3.7
困ること
何が困るかというと、CloudFrontでSSL→HTTPに変換して、ELB以降はずっとHTTPで取り回すと、X-Forwarded-Protoヘッダが渡ってこない事です。CloudFrontは何故か独自のCloudFront-Forwarded-Protoというヘッダを渡してきます。
これによって、 force_ssl!
も動かないし、そもそも post,put,patchリクエストはCSRFをブロックする機構によって弾かれてしまいます。
フロントがELBだったら問題ないんですが、ここでは最前面にCoudFrontを置く想定…。
ここで対策は2つあります。
ここでは後者を採用しました。
やったこと
以下のRack Middlewareを用意します。
# lib/cloud-front-header.rb class CloudFrontHeader def initialize(app) @app = app end def call(env) cf_proto = env['HTTP_CLOUDFRONT_FORWARDED_PROTO'].to_s if cf_proto && cf_proto.length > 0 env['HTTP_X_FORWARDED_PROTO'] = cf_proto end @app.call(env) end end
そして、 application.rb に以下の内容を足します。 他にmiddleware使ってない場合は、どこに書いても問題ないと思います。
# config/application.rb module MyApp class Application < Rails::Application # 前略 require './lib/cloud-front-header.rb' config.middleware.use CloudFrontHeader # 後略 end end
と、強引な方法ではありますが、 CloudFront-Forwarded-Proto を勝手に X-Forwarded-Proto に上書きするという荒技でどうにかしたのでした。
Rack Middleware がバグるのは鬱陶しいので、to_s等を使った防衛的なコードになってますが、気に入らない人は適宜書き換えると良いです。
あと、置き場所を考え直すと require は要らないかも知れないです。 もし良い方法あったら誰かコメントとかで教えてください。
こういう事が出来るのはRackの優れたレイヤアーキテクチャのたまものですね。