プラットフォームが大きく異なる旧Webサーバと新Webサーバをnginxでうまく繋いで、新プラットフォームへの移行をゆるやかに行えるようにする方法の紹介です。
はじめに
ウェブサービスを支えるサーバサイドのプラットフォームの変化は目を見張るものがありますね。振り返ってみると、PerlでCGIの時代からPHPになったと思ったら、Ruby on Railsは今やウェブの標準的なプラットフォームのひとつになっています。きっと今後も、新しいプラットフォームが出て来て、古いプラットフォームが置き換えられていくことがあると思います。
僕の会社で作っている、テストを自動化するウェブサービス「ShouldBee」では、PHPをサーバサイドに採用しています。サーバサイドと言っても、クライアントのHTTPリクエストを処理して、レスポンスを返すのが主な役割です。サービスの中核であるテスト自動化の部分はScalaで書かれています。
PHPは僕自身10年以上の経験があり、事業開始当初は「PHPなら素早くフロントエンドを実装できる」と考えて採用しました。そのときはScala未経験だったこともあります。しかし、最近はPHP未経験の開発者も加わり、バックエンドがScalaであることもあり、「フロントエンドもScalaで書きたい!」というモチベーションが高くなり、実装し直すことになりました。
モダンな技術は楽しいが、お客さんにとっての価値は?
新プラットフォームへの移行と言うと、今まで作ってきたプラットフォームを捨て去って、フルスクラッチで作りなおすことが考えられます。しかし、フルスクラッチとなると、今稼働しているシステムと同レベルにするだけでも何ヶ月かかかります。
フルスクラッチで心機一転して、プラットフォームが新しくなることは、開発者の意欲を高めることではありますが、お客さんにとって、システムがPHPからScalaに移植されることにどんな価値があるでしょうか?
お客さんの中には、Scala化よりも新しい機能のほうを待ちわびている人がいます。現にShouldBeeでは、着手できてないプロダクトバックログアイテムと、バックログにすら入っていないお客さんからの要望が山のように積み重なっています(ちなみにShouldBeeでは要望とバックログを誰でも見れるようにしています)。
このようなお客さんにとっては、システムの移植をしている数ヶ月間は、価値が提供されない空白期間になってしまいます。
WebサーバMixinで価値提供とシステム移行を両立
現状では、フルスクラッチをするにしても、機能提供も続けなら移行を進めていく必要があります。そこで、旧システムを稼働させたままにし、機能追加は部分的に新システムにしていくことにしました。そして、旧システムの機能は徐々に新システム側に移植していきます。新システムへの移植が完了するまでの過渡期は、次の図のようなアーキテクチャになります。
新旧システムが共存する過渡期のアーキテクチャ
Nginxがリバースプロキシになり、クライアントのリクエストをさばいて、既存機能に関するリクエストであれば、旧システムへリクエストし、新機能に関するものは新システムのリソースをレスポンスとして返します。
このようなアーキテクチャになっていれば、旧システムの機能を移植が完了するまで活かしたまま残しつつ、新しいプラットフォームはゆるやかに移行していくことができます。
サーバをMixinするというのは、僕が考えた表現です。アーキテクチャを図にしてみると、まるでオブジェクト指向の継承のように、2つのサーバを継承した1つのサーバがあるように見えるのでMixinと表現することにしました。
パスをまぜこぜにする
Mixinや継承と表現したのはもうひとつ理由があります。単にドメインやパスで新旧のシステムを分けるのではなく、2つのサービスに同じドメイン、同じポート、同じパスでアクセスできるようにしたいからです。例えば、http://example.com/old-feature
へのリクエストは旧システムが処理し、http://example.com/new-feature
へのリクエストは新システムが処理するようになります。
既存機能は旧システムが新機能は新システムが担う
パスまぜこぜのメリットの1つに、新旧のシステムがあたかもひとつのシステムに見えるという点があります。ドメインが分かれているとユーザがドメイン名で混乱することがあるかもしれませんが、それがありません。
さらに、パスを共有することで、新システムで旧システムのURLを上書きできるようになります。どういうことかと言うと、旧システムの機能を新システム側に移植したとき、URLやドメインを変えることなく、自動的に新システムの機能が使われるようになるということです。オブジェクト指向言語のoverrideのような仕掛けです。
移植が完了すると旧システムのURLが新システムで使える
加えて、いい副作用として、ドメインがひとつになることで、SSL証明書を複数とったり高いワイルドカード証明が不要になります。お金の無いスタートアップには嬉しいことです。新旧システム間で、AjaxするときにCORSにする必要もありません。会社のセキュリティポリシー次第ではファイアウォールの設定でoutboundが80と443しか空いてないお客さんもいるかも知れませんが、ポートも80と443だけなので問題なく使うことができます。
404 Not Found を活用する
パスをまぜこぜにするとなると、機能ごとに呼び出すサーバを切り分けないといけません。やりかたは2つあります。1つは、必要なぶんのパスをNginxの設定ファイルに明示的に定義して、ルーティングする方法です。例えば、/login
なら旧システムへ、/dashboard
から始まるパスなら新システムへ、といった具合です。この方法は、ルーティングが明示的になる反面、ルーティングが増えたり変更があるたびにNginxの設定ファイルを修正しないといけません。
もう1つは、HTTPレスポンスのステータスコード404 Not Foundを活用する方法です。Nginxにリクエストが来たら、新システムにリクエストを投げるようにします。もし新システムからのレスポンスが404以外のステータスであれば、そのレスポンスをそのままクライアントへ返します。逆に、404であれば今度は旧システムにリクエストを処理させます。「移植が済んでない機能」とはつまり、「新システムでNot Foundになるもの」なので、この方法がうまくいくわけです。
新システムが404なら旧システムに投げる
1つ目の方法と比べて、404 Not Foundを活用した方法は、ルーティングが変化してもNginxの設定を変更する必要がないという利点があります。逆に、デメリットとしては新システムがリクエストに対応できるページがないと分かっている場合でも、一度はリクエストが新システムに飛ぶという点です。このデメリットは、そのアプリケーションの性能やビジネスを取り巻く環境にもよりますが、大して問題にならないと僕は考えます。
404 Not Foundを活用したルーティングで注意しておく点としては、パスの意図しない重複です。特にありそうなのはアセットのパスです。旧システムと新システムのアセットのパスがどちらも、/assets
になっている場合はパスが衝突してしまうことが考えられます。/assets/stylesheets/style.css
といったどのフレームワークでも使っていそうです。このような場合は、ぶつかりそうなパスは新システムのパスは、明らかに新システムと分かるようにしておきます。例えば、新システムのアセットのパスを/new-assets
にするなど。アセットは基本的にユーザの目に触れることがないので、旧システムを置き換えてから、/assets
に名前を変えても問題になることは少ないと思います。
加えての注意しないといけないのは、旧システムが、たとえ404になるページであっても、アクセスするごとに何か破壊的な操作をする場合です。このようなシステムでは、404 Not Foundを活用した方法はうまく行かないかも知れません。
Nginxでの設定方法
404 Not Foundを活用したルーティングは、Nginxでは数行のシンプルな設定で実現することができます。
server {
listen 80 default_server;
location / {
try_files $uri @new_app;
}
location @new_app {
proxy_pass http://127.0.0.1:4000$uri;
proxy_intercept_errors on;
recursive_error_pages on;
error_page 404 = @old_app;
}
location @old_app {
proxy_pass http://127.0.0.1:9000$uri;
}
}
proxy_intercept_errors
はプロキシーでエラーになったときに、カスタムのエラーページを出すかどうかの設定です。これがon
になっているとき、error_page
ディレクティブの設定が効きます。単純なリバースプロキシーとしての設定であれば、error_page
に404.html
のようなエラーページを設定するところですが、今回はエラーページを出して終わりではなく、次のサーバへリクエストを飛ばしたいので、@old_app
が設定されています。
Demo
この設定をどなたでも試せるように、デモ用のコードをGitHubのsuin/web-servers-mixin-demoで公開しています。デモ環境の構築は、VagrantとVirtualboxがあればできますので、試してみたい方はREADMEを御覧ください。
デモ環境では、新システムとしてScalaではなくRailsを使っています。RailsのほうがScalaよりもコードをいじってみるなど試せる人が多かなと思ったからです。
この記事では、デモ環境がどのように動くかスクリーンショットでお見せしたいと思います。
まず、http://192.168.0.55/
にアクセスするとRailsのトップページが表示されます。新システムからのレスポンスが返ってきているということになります。
次にhttp://192.168.0.55/index.php
に行ってみると今度は、Slimのトップページが表示されます。SlimはPHPのマイクロフレームワークです。ユーザは旧システムがレスポンスを見ているということになります。
http://192.168.0.55/welcome/index
はRailsのWelcomeController
のindex
メソッドが出力しているページです。
一方、http://192.168.0.55/welcome/php
はPHP側のレスポンスです。/welcome/index
はRails、/welcome/php
はPHPといったように、パスの深いところでも新システムと旧システムを混ぜて使うことができます。
気になるのが開発中のデバッグページです。フレームワークの中には、コードや実行時に問題があると、コードのエラー箇所や関連する情報をウェブページ上で教えてくれるものがあります。そういった情報がNginxのルーティングの結果、見えなくなってしまうのでは?という心配がありますが、Nginxは404しかハンドリングしていないので、それ以外のステータス、例えばエラーの500のレスポンスはそのまま表示されます。
PHP側の500エラーの画面
Rails側の500エラーの画面
もちろん、どちらも500エラーになった場合は、どちらかしか表示されませんが、片方を解消すればもう一方も後から見ることができます。
おわりに
新しいプラットフォームを使いたいという開発者のニーズと、お客さんを待たせることなく新機能を提供したいというビジネス上のニーズ、どちらも満たすために、新プラットフォームへゆるやかに移行していく方法をご紹介しました。
実際やってみると今回紹介した方法だけでは、不十分な部分があります。1つ目はユーザ認証です。新旧のシステム間でセッションをうまく共有する方法が必要です。2つ目は、新旧のシステムで共通の画面コンポーネントをどう管理したらいいかという問題です。3つ目、新旧のシステムが互いに依存しあう必要が出てきたらどうすべきか、最後に、旧システムから新システムへの移植はどういう方針で取り組むべきかといった課題です。これらの課題と解決策については、機会があれば記事にしてみたいと思います。
(あとShouldBeeではスタートアップでこういうアーキテクチャ周りを考えたりScalaをやってみたいというエンジニアさんを募集してます! @suin までメールでご連絡ください)