Webサーバをmixin! 新プラットフォームへの移行をゆるやかに行う技術

プラットフォームが大きく異なる旧Webサーバと新Webサーバをnginxでうまく繋いで、新プラットフォームへの移行をゆるやかに行えるようにする方法の紹介です。


## はじめに

ウェブサービスを支えるサーバサイドのプラットフォームの変化は目を見張るものがありますね。振り返ってみると、PerlでCGIの時代からPHPになったと思ったら、Ruby on Railsは今やウェブの標準的なプラットフォームのひとつになっています。きっと今後も、新しいプラットフォームが出て来て、古いプラットフォームが置き換えられていくことがあると思います。

僕の会社で作っている、テストを自動化するウェブサービス「[ShouldBee]」では、PHPをサーバサイドに採用しています。サーバサイドと言っても、クライアントのHTTPリクエストを処理して、レスポンスを返すのが主な役割です。サービスの中核であるテスト自動化の部分はScalaで書かれています。

PHPは僕自身10年以上の経験があり、事業開始当初は「PHPなら素早くフロントエンドを実装できる」と考えて採用しました。そのときはScala未経験だったこともあります。しかし、最近はPHP未経験の開発者も加わり、バックエンドがScalaであることもあり、「フロントエンドもScalaで書きたい！」というモチベーションが高くなり、実装し直すことになりました。

## モダンな技術は楽しいが、お客さんにとっての価値は？

新プラットフォームへの移行と言うと、今まで作ってきたプラットフォームを捨て去って、フルスクラッチで作りなおすことが考えられます。しかし、**フルスクラッチとなると、今稼働しているシステムと同レベルにするだけでも何ヶ月かかかります**。

フルスクラッチで心機一転して、プラットフォームが新しくなることは、開発者の意欲を高めることではありますが、**お客さんにとって、システムがPHPからScalaに移植されることにどんな価値があるでしょうか？**

お客さんの中には、Scala化よりも**新しい機能のほうを待ちわびている人がいます**。現にShouldBeeでは、[着手できてないプロダクトバックログアイテム](https://trello.com/b/4JHGSfLf/)と、[バックログにすら入っていないお客さんからの要望](https://trello.com/b/NZedoSDx/shouldbee)が山のように積み重なっています(ちなみにShouldBeeでは要望とバックログを誰でも見れるようにしています)。

このようなお客さんにとっては、システムの移植をしている数ヶ月間は、価値が提供されない空白期間になってしまいます。

## WebサーバMixinで価値提供とシステム移行を両立

現状では、フルスクラッチをするにしても、**機能提供も続けなら移行を進めていく必要があります**。そこで、旧システムを稼働させたままにし、機能追加は部分的に新システムにしていくことにしました。そして、旧システムの機能は徐々に新システム側に移植していきます。新システムへの移植が完了するまでの過渡期は、次の図のようなアーキテクチャになります。

**新旧システムが共存する過渡期のアーキテクチャ**

![](https://qiita-image-store.s3.amazonaws.com/0/889/c7bad563-185f-037e-1fa1-c3c057ed1956.png)

Nginxがリバースプロキシになり、クライアントのリクエストをさばいて、既存機能に関するリクエストであれば、旧システムへリクエストし、新機能に関するものは新システムのリソースをレスポンスとして返します。

このようなアーキテクチャになっていれば、旧システムの機能を移植が完了するまで活かしたまま残しつつ、新しいプラットフォームはゆるやかに移行していくことができます。

サーバをMixinするというのは、僕が考えた表現です。アーキテクチャを図にしてみると、**まるでオブジェクト指向の継承のように、2つのサーバを継承した1つのサーバがあるように見える**のでMixinと表現することにしました。


## パスをまぜこぜにする

Mixinや継承と表現したのはもうひとつ理由があります。単にドメインやパスで新旧のシステムを分けるのではなく、2つのサービスに同じドメイン、同じポート、同じパスでアクセスできるようにしたいからです。例えば、`http://example.com/old-feature`へのリクエストは旧システムが処理し、`http://example.com/new-feature`へのリクエストは新システムが処理するようになります。

**既存機能は旧システムが新機能は新システムが担う**

![](https://qiita-image-store.s3.amazonaws.com/0/889/6bb9ade7-6f59-2aad-d32b-b4b74344403a.png)


パスまぜこぜのメリットの1つに、**新旧のシステムがあたかもひとつのシステムに見える**という点があります。ドメインが分かれているとユーザがドメイン名で混乱することがあるかもしれませんが、それがありません。

さらに、**パスを共有することで、新システムで旧システムのURLを上書きできる**ようになります。どういうことかと言うと、旧システムの機能を新システム側に移植したとき、URLやドメインを変えることなく、自動的に新システムの機能が使われるようになるということです。オブジェクト指向言語のoverrideのような仕掛けです。

**移植が完了すると旧システムのURLが新システムで使える**

![](https://qiita-image-store.s3.amazonaws.com/0/889/0773d51a-f399-4acb-b5d0-9f42eeb9d172.png)


加えて、いい副作用として、ドメインがひとつになることで、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なら旧システムに投げる**

![](https://qiita-image-store.s3.amazonaws.com/0/889/97ccc993-e2d0-c074-a526-f69bfc7bf942.png)


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では数行のシンプルな設定で実現することができます。

```nginx:nginx.conf
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](https://github.com/suin/web-servers-mixin-demo)で公開しています。デモ環境の構築は、VagrantとVirtualboxがあればできますので、試してみたい方はREADMEを御覧ください。

デモ環境では、新システムとしてScalaではなくRailsを使っています。RailsのほうがScalaよりもコードをいじってみるなど試せる人が多かなと思ったからです。

この記事では、デモ環境がどのように動くかスクリーンショットでお見せしたいと思います。

まず、`http://192.168.0.55/`にアクセスするとRailsのトップページが表示されます。新システムからのレスポンスが返ってきているということになります。

![](https://qiita-image-store.s3.amazonaws.com/0/889/10b9ab33-512a-ff43-def2-611981804400.png)

次に`http://192.168.0.55/index.php`に行ってみると今度は、Slimのトップページが表示されます。SlimはPHPのマイクロフレームワークです。ユーザは旧システムがレスポンスを見ているということになります。

![](https://qiita-image-store.s3.amazonaws.com/0/889/c616caa8-f9f7-a408-3a6c-0e802a8e1447.png)

`http://192.168.0.55/welcome/index`はRailsの`WelcomeController`の`index`メソッドが出力しているページです。

![](https://qiita-image-store.s3.amazonaws.com/0/889/5b93ca41-3ddf-4fe4-ecd6-654c840875bd.png)

一方、`http://192.168.0.55/welcome/php`はPHP側のレスポンスです。`/welcome/index`はRails、`/welcome/php`はPHPといったように、パスの深いところでも新システムと旧システムを混ぜて使うことができます。

![](https://qiita-image-store.s3.amazonaws.com/0/889/8c1b85de-f5cd-3f50-8a6d-1ca2bce0c830.png)

気になるのが開発中のデバッグページです。フレームワークの中には、コードや実行時に問題があると、コードのエラー箇所や関連する情報をウェブページ上で教えてくれるものがあります。そういった情報がNginxのルーティングの結果、見えなくなってしまうのでは？という心配がありますが、Nginxは404しかハンドリングしていないので、それ以外のステータス、例えばエラーの500のレスポンスはそのまま表示されます。

PHP側の500エラーの画面

![](https://qiita-image-store.s3.amazonaws.com/0/889/accbffaa-a04f-37b6-bf82-4a38025604fc.png)

Rails側の500エラーの画面

![](https://qiita-image-store.s3.amazonaws.com/0/889/ce42bb8c-bbe0-7d10-8d11-ce3a452b7bf7.png)

もちろん、どちらも500エラーになった場合は、どちらかしか表示されませんが、片方を解消すればもう一方も後から見ることができます。


## おわりに

新しいプラットフォームを使いたいという開発者のニーズと、お客さんを待たせることなく新機能を提供したいというビジネス上のニーズ、どちらも満たすために、新プラットフォームへゆるやかに移行していく方法をご紹介しました。

実際やってみると今回紹介した方法だけでは、不十分な部分があります。1つ目はユーザ認証です。新旧のシステム間でセッションをうまく共有する方法が必要です。2つ目は、新旧のシステムで共通の画面コンポーネントをどう管理したらいいかという問題です。3つ目、新旧のシステムが互いに依存しあう必要が出てきたらどうすべきか、最後に、旧システムから新システムへの移植はどういう方針で取り組むべきかといった課題です。これらの課題と解決策については、機会があれば記事にしてみたいと思います。



(あとShouldBeeではスタートアップでこういうアーキテクチャ周りを考えたりScalaをやってみたいというエンジニアさんを募集してます！ @suin までメールでご連絡ください)

[ShouldBee]: http://shouldbee.at