NginxリバースプロキシのバックエンドにApache+Passenger+Sinatraで個人開発環境を構築する


概要

こんにちは!最近スプラトゥ◯ンにハマっているWebエンジニア2年目の@mejilebenです。

今回は、弊社の2年目エンジニア研修”SET”で学んだ内容の中から、サーバー周りの設定について記事を書いてみます。

対象読者

  • Nginxでリバースプロキシさせる設定を知りたい方
  • Apache+Passenger+Sinatraで環境構築したい方
  • AWS Route53を使って個人開発環境用のURLを設定したい方
  • とにかくインフラ周りにアレルギーのあるエンジニアの方(※自分です)

結論

先に今回の記事の結論を示しますので、詳しく知りたい!と思った方は該当の章に飛んでください!

  • 同じIPで複数のURLを割り当てたいときはDNSのCNAMEレコードを使う
  • ApacheではVirtualHostを使って、ホスト名毎に異なるソースコードを実行できる
  • Nginxのリバースプロキシではproxy_pass以外にもHostヘッダーを追加する等の設定をすることが望ましい
  • ApacheでURLをRewriteした上でバックエンドに流すような用途の場合、PではなくPTオプションを設定する

開発背景/環境

今回開発しているサービスについての詳細は割愛しますが、サーバー/ネットワークの構成は下記のようになっています。#圧倒的パワポ感

ざっくり説明しますと、このような感じです。

  • 最初にNginxで構築したリバースプロキシにリクエストが来る
  • NginxはURLをチェックし、/authで始まるものは認証サーバー、それ以外はWebサーバーへ流す
  • 認証サーバーはApacheで動いており、リクエストを受け取ると、URLの/authを/に置換し、Passengerに流す
  • PassengerがSinatraアプリケーションを動かし、適切なページ(ログインページ等)をレンダリングして返す

研修で作っていることから、勉強のためにあえてELBやALBを使わなかったり、NginxとApacheを両方使うといった選択肢を採っています。
本記事では、認証サーバーにおいてPassengerと連携したり/authを/に置換したりといったややこしい設定をすることになったので、ここを中心に話していきたいと思います。

ゴール

開発するサービスでは既に「www.hogehoge.com」といったドメインをRoute53にて取得し、ローカルマシンのブラウザから開いて動作確認するところまでは完成していました。
(動作確認前から解説すると長くなりますし、既存記事の焼き直しになりそうなので割愛します)

ここに、

  • リバースプロキシを使って/auth/*のパスなら認証サーバーを向くようにしたい!
  • www-unit-mejileben.hogehoge.comといった開発環境用URLを開発メンバーの数だけ作って、それぞれのURLがそれぞれの開発ディレクトリに向くようにしたい!

というのが今回のゴールです。

【Route53】個人開発環境URLの設定

DNS上においては、www-unit-mejileben.hogehoge.comwww.hogehoge.comも同じIPアドレス(リバースプロキシのPublic IP)を向かせるところですので、DNSのCNAMEレコードを利用します。

現状、www.hogehoge.comのDNS設定は完了していたため、以下のようにCNAMEレコードとして個人開発環境を追加することになります。
今回は2人分の個人開発環境用のURLを登録しました。

【Nginx】/authで来たリクエストを認証サーバーに流す

/authでも、/auth/でも、/auth/hogehogeでも転送にするため、少しだけ正規表現テクが必要でした。

nginx.conf
        location ~ ^\/auth(|\/.*)$ {
            # 認証サーバーへの転送
            proxy_pass http://[認証サーバーのPrivate IP];
        }

        location / {
            # メインサーバーへの転送
            proxy_pass http://[メインサーバーのPrivate IP];
        }

【Apache】VirtualHostを用いた個人開発環境の切り分け

さて、ここまでの設定で、www.hogehoge.com/auth/signinでもwww-unit-mejileben.hogehoge.com/auth/signinでも認証サーバーへとリクエストが飛ぶようになっています。
ここで、認証サーバーにApacheを立ててPassenger+Sinatraの設定をし、httpdを起動すると、どちらのURLでもページがブラウザから見れるようになります。

この辺の設定に関しては既存文献が多いため、例えば下記を参照してください。
Amazon LinuxにRuby Sinatra環境構築(rbenv + ruby-build + Ruby 2.3.1 + Sinatra 1.4.7インストール)
さくらVPS(CentOS)にPassenger+Apache+Sinatra
安定の公式ドキュメント

問題は、個人開発環境と本番環境で違うレスポンスを返すことができるかですが、ApacheのVirtualHostという仕組みを使えば対応可能です。
本来本番環境と個人開発環境が同じサーバー内にあることは無いですが、個人開発環境を1サーバー内に複数用意したいことは多々あると思うので、活用できると思います。

conf.d/passenger.conf
NameVirtualHost *

PassengerLogFile /var/log/passenger.log # PassengerのログファイルはVirtualHost毎に分けられない。。。
PassengerLogLevel 3

<VirtualHost *>
  RewriteEngine on                      # Sinatraでいちいち/authを付けるのが面倒なのでRewriteする
  RewriteRule ^/?auth/(.*)$ /$1 [PT,L]  # ここを[P,L]にすると動かなくなる※後述
  RewriteLog /var/log/httpd/rewrite.log
  RewriteLogLevel 9

  DocumentRoot [本番環境用のソースが置かれている絶対パス]/public #/publicを忘れない
  ServerName www.hogehoge.com

  <Directory [本番環境用のソースが置かれている絶対パス]/public>
    Allow from all
    Options -MultiViews
  </Directory>
</VirtualHost>

<VirtualHost *>
  RewriteEngine on
  RewriteRule ^/?auth/(.*)$ /$1 [PT,L]
  RewriteLog /var/log/httpd/rewrite.log
  RewriteLogLevel 9

  DocumentRoot [開発環境用のソースが置かれている絶対パス]/public
  ServerName www-unit-mejileben.hogehoge.com

  <Directory [開発環境用のソースが置かれている絶対パス]/public>
    Allow from all
    Options -MultiViews
  </Directory>
</VirtualHost>

【Nginx】Hostヘッダーを付け足す設定

ここまでの設定で、振り分けができていると思ったのですが、httpdを再起動してみると、どちらのURLでも本番環境のページを見に行ってしまう動きになりました。

実は、VirtualHostのServerNameディレクティブはHTTPリクエストにおけるHostヘッダーを参照した上で、同一のServerNameのVirtualHost内の設定を適用する、といった挙動をしています。
Apacheドキュメント

ちなみに、認証サーバーにログインし、下記のようにcurlでHostヘッダーを指定して実行すると、Hostヘッダーによってレスポンスが違うことを確認できます。

curl -H 'Host: www.hogehoge.com' http://localhost/auth/signin

ですが、今回のケースだとNginxでリバースプロキシさせたときにHostヘッダーが変わってしまうため、狙い通りに動かなくなってしまいました。

全てのServerNameにマッチしなかった場合は一番上に書かれたVirtualHostが適用されるため、上記の設定だと本番環境を見に行ってしまう動きになります。

そこで、Nginxの設定nginx.confに戻り、下記の設定を付け足しました。

nginx.conf
        proxy_set_header Host               $host;

【Apache】最後に陥った罠

ここまでやっても実はもう一つ罠が控えていました。
先程は説明を省略しましたが、VirtualHostにはRewriteの設定をしており、ApacheでURLの/authを消した上でSinatraに渡しています。
Sinatraで全部のルーティングに/authを付けるのが面倒だったのでこうしました(Sinatraで容易に対応できる方法は後になって先輩に教えていただきました)。

conf.d/passenger.conf
  RewriteEngine on                      # Sinatraでいちいち/authを付けるのが面倒なのでRewriteする
  RewriteRule ^/?auth/(.*)$ /$1 [PT,L]  # ここを[P,L]にすると動かなくなる※後述
  RewriteLog /var/log/httpd/rewrite.log
  RewriteLogLevel 9

ここのRewriteRuleの末尾に[PT,L]とありますが、これを僕は当初[P,L]としていたために、www.hogehoge.com/auth/signin等にアクセスしてもページが表示されませんでした。延々と読み込みが続いて、タイムアウトになるような挙動をしたのです。

[ ]で囲んでいる部分はRewriteのオプションなのですが、PはProxyという意味です。
公式ドキュメントを見て、「if you wanted all image requests to be handled by a back-end image server」と書いてあったため、今回のようにバックエンドのPassengerにリクエストを送る用途にピッタリだ!と思い採用していましたし、実際www.hogehoge.comしか無かったときはこれで動いていました。

しかし、先程Nginxの設定で、リクエストのHostヘッダーを書き換えました。
そのことが影響し、Proxyオプションの動作としてはHostヘッダーを参照した先に、Rewriteした結果を送るような挙動になってしまったのです。
結果、例えばwww.hogehoge.com/auth/signinにアクセスした場合は、

  1. NginxがHostヘッダーに「www.hogehoge.com」を付与
  2. NginxがApache認証サーバーに転送
  3. ApacheがRewriteで/authを削除
  4. その結果をPassengerではなくHostヘッダーのwww.hogehoge.comに転送
  5. リバースプロキシはHTTPでリクエストを受け付けていない(HTTPSのみ)のため、応答が返らずタイムアウト

となっていたと考えられます(ここは検証しきれていませんが……)。

そのため、P(Proxy)ではなくPT(PassThrough)に書き換えることで、狙い通りPassengerにリクエストが流れるように設定できました。

まとめ

冒頭の内容を再掲します。

  • 同じIPで複数のURLを割り当てたいときはCNAMEレコードを使う
  • ApacheではVirtualHostを使って、ホスト名毎に異なるソースコードを実行できる
  • Nginxのリバースプロキシではproxy_pass以外にもHostヘッダーを追加する等の設定をすることが望ましい
  • ApacheでURLをRewriteした上でバックエンドに流すような用途の場合、PではなくPTオプションを設定する

教訓

  • 英語のドキュメントから逃げない。戦う
  • 闇雲にエラーを治そうとしない。丁寧に切り分けてから原因を探る