この記事は一休.comアドベントカレンダー2017の 8 日目です。
一休.com の宿泊開発基盤のお手伝いをしている id:shiba-yanです。
はてなインターン時代の縁で naoya さんから声をかけていただき、基本フリーランスですが一休で週に 3 日ほどの作業を 2016 年 4 月から行っています。
最近は shibayan とも一緒に改善を進めている
4ヶ月の間に一休.comで起きた変化 - zimathon blog
2016 年 4 月末から現在までに、一休社内でどのようなことに取り組んできたか、公開できる範囲で思うままに書いていきます。長いです。
ユニットテスト基盤
これまではテストが書ける状態ではなかったのですが、xUnit.net を利用してユニットテストをコンポーネント単位で書けるような仕組みを用意しました。
しかし、残念ながら上手く回っていません。例外的ですが、複雑化した URL Rewrite ルールに対するユニットテストに関しては、上手く回すことができています。チームメンバーの id:minato128 が LT で話したスライドもあります。
URL Rewrite のテストは勢いでライブラリを作り、それをすぐに実践投入しました。クラウド移行のタイミングではこのテストのおかげで不具合が発覚し、非常に助けられています。
新しいメールテンプレート
長く開発されてきた結果、一つのシステム内に複数のメールテンプレートが実装されていて、それがアプリケーション内に分散している状態でした。
さらに条件によって項目が変わるメールは非常に複雑なテンプレートとして定義されていて、メンテナンスが非常に難しい状態になっていたため、ASP.NET MVC で使われている Razor ベースの新しいテンプレートを作成しました。
しかし、残念ながら利用は広がっていません。既にあるテンプレートを移行することを考えると非常に高コストになり、最近では新しくメールを増やすといったことがほぼないのが理由でした。このあたりはヒアリング不足で作業を進めてしまったのが反省点です。
メール配信基盤
これまで一休ではオンプレミスのメールサーバーを利用して来ましたが、一斉メール送信に時間がかかる問題が顕著になり、さらに保守にかかる手間とコストやクラウド移行という課題が出てきたため、AWS 上に新しく実装することになりました。
実装したメール配信基盤の概要は以下のようになります。
- SendGrid を利用
- SQS と Elastic Beanstalk を使った非同期処理
- メールの配信結果は DynamoDB に格納
- Webhook は API Gateway と Lambda で処理
- ASP.NET Core を利用して実装
去年から段階的に一休.com のサービスで利用を始め、今ではほぼ全てのメールが AWS 上のメール配信基盤から送信されるようになっています。メール配信基盤に関しても id:minato128 がセッションを行っているので、そちらも参考にしてください。
今回のアドベントカレンダーでも書かれているので、こちらもどうぞ。
初期は多少 DynamoDB の予約スループットを使い切ってしまうことがありましたが、実装の改善により今では全く発生しなくなっています。SendGrid の障害が一時頻繁に発生した時には悩まされましたが、現在は対策を行い安定した運用を行えています。
宿泊クラウド移行
直近の大きな作業として、一休.com サービス全体のクラウド移行がありました。既に一休.com のサービスは全て AWS に移行が完了しているので、皆さんが見ている www.ikyu.com は AWS の東京リージョンから提供されています。
AWS への移行を行うことでサービス自体の可用性が高まったり、柔軟なスケーリングが可能になったりとクラウドのメリットを享受することが出来ていますが、移行完了までには様々な問題が山積みでした。
単純に宿泊のアプリケーションと言っても、正しく動かすためには関係するアプリケーションも同時に移行する必要があります。例えば、実際に宿泊のアプリケーションでは 4 つのアプリケーションを移行する必要がありました。
- 宿泊サイト本体(www.ikyu.com)
- マイページ(my.ikyu.com)
- 管理画面
- 外部連携用 API
その他にも内部から利用している API が複数あり、宿泊アプリケーションが AWS へ移行するためには、それらを先に AWS へ持っていく必要があったのです。他にもいろいろあり、担当している宿泊基盤チームだけで合計 8 アプリケーションを AWS に移行することになりました。
いきなり巨大な宿泊サイトから行うのは非効率なので、まずは規模の小さいアプリケーションから地道にノウハウを溜めつつ進めることにしました。
移行方法の調査・検証
まずはオンプレミスで動作しているアプリケーションを、どのように AWS 上で実行するかを検討しました。一番単純かつ時間がかからないのはオンプレと同じ構成を EC2 で作成して、今と同じアプリケーションをデプロイする方法でしたが、開発の現場では日々デプロイの問題に悩まされていて、今の状態のまま AWS に持って行っても悪くなるだけなのは、火を見るよりも明らかでした。
当時のデプロイでの辛さは、これもまた id:minato128 が話したスライドがあります。
デプロイ完全自動化から1年で起きたこと /ikyu-deploy // Speaker Deck
さらに当時採用していた Jenkins を利用したデプロイは、リポジトリ内の変更されたファイルのみを対象としていたため、EC2 に持って行った場合にインスタンスごとにファイルの整合性を担保することが非常に難しいことが分かりました。
その他にもたくさんの課題がありましたが、調査と検証を重ねていった結果として以下のように方針を決定しました。
- オンプレと同じ仕組みは一切持ち込まない
- EC2 インスタンスは何時でも破棄が出来るように
- 差分ではなくアプリケーションに必要なファイル全てをデプロイする
- ビルドには Jenkins を止めて AppVeyor を利用する
- 一般的な方法でアプリケーションのビルドが行えるように構造を変える
実際に実行する EC2 インスタンスは Elastic Beanstalk を利用して、インスタンスの生成と破棄やデプロイなどを全て任せることにしました。最初は多少デプロイが非効率になったとしても、まずはオンプレ構成からの脱却が重要だと考えました。
実行環境の調査・検証
アプリケーションは Elastic Beanstalk を使って実行する方向に舵を切りましたが、宿泊アプリケーションでは一部に特殊なコンポーネントを利用しているため、事前に開発環境でデプロイ用のパッケージをビルドして、問題なく動作が可能かを検証しました。
当初は ebextensions を利用してインストールを試しましたが、Cloud Formation の実行権限が特殊で上手くいかなかったため、カスタム AMI を作成して運用する方向に決めました。
- Windows Server 2012 R2 を利用(当時は 2016 がリリースされていなかったため)
- CircleCI を利用して AMI を自動で生成
- Windows Update は新しい AMI を作成して対応
出来るだけ手動で行う部分を減らして、運用時の負荷を下げるように工夫しました。CircleCI を使った新しい AMI のビルドは 20 分以内で完了するので、Beanstalk に設定すれば EC2 が自動的に作り直されて作業は完了します。
今回はカスタム AMI を使わざるを得ない状況でしたが、結果的にプロビジョニング時間の短縮に繋がりました。
アプリケーションの分離と整理
一休.com のアプリケーションは ASP.NET で実装されていますが、歴史的経緯から先ほど述べた 4 つのアプリケーションが 1 つのアプリケーションとしてマージされた形で実装されていました。しかし、プロジェクトファイルは別々に存在していたので、このままでは MSBuild を使ってデプロイ用の資材をビルドすることも難しい状態でした。
まずは一つになっているアプリケーションを、役割ごとに分離する作業は必須だと考えました。実際のアプリケーション単位で分離することができれば、後は普通の ASP.NET アプリケーションと同様の方法が使えるからです。
当時は分離が本当に必要なのかという意見が何回か出てきましたが、移行を担当したチーム内では必要な作業だという認識があり、理由の周知を行い対応しました。
まずは Qiita に DesignDoc を作成してフィードバックを貰い、内容に問題が無ければ周知用のエントリとして仕上げるという作業を繰り返し行いました。
実際の作業を行ったのは私なので、その時に方向性を以下のように定めました。要するに普通の ASP.NET アプリケーションに組み替える作業です。
- 1 つの巨大なアプリケーションを 4 つに分ける
- 適切な粒度で分離
- アプリケーション毎にソリューションを用意してフルビルドが簡単に行えるように
- 参照がアセンブリ直指定になっていた部分をすべて修正
- アプリケーションの一部であればプロジェクト参照
- NuGet で配布されている場合はパッケージ参照
実際に開発が日々行われているリポジトリに対して、非常に大きな変更を行うことは開発チーム全体への影響が大きいため、事前のリハーサルを何度も行いビルドやデプロイに影響が出ないように努力しました。
どのくらいの規模だったかは、GitHub の Insights が物語ってくれています。
各アプリケーションを順番に分離していき、作業が完了するまでに 2,3 ヵ月が必要でした。しかしこの分離作業が完了すれば、クラウド移行に必要な作業の 9 割は完了すると考えていたので、あえて時間をかけて丁寧に作業しました。
AppVeyor での CI / CD
アプリケーションの分離を行った結果、フルビルドやデプロイ用パッケージの作成は MSBuild を 1 回実行するだけで完了するようになり、大幅に処理をシンプルにすることが出来ました。
これまでの Jenkins を利用したデプロイでは、環境に依存する Web.config や App.config などの設定ファイルをスクリプトでコピーするような形になっていましたが、その方法を止めて全て Xml Document Transform を使ったビルド時の自動切り替えを利用するようにしました。
Web.config Transformation Syntax for Web Project Deployment Using Visual Studio
よくある Web.Release.config などと同じ方法で、ステージングや本番といった単位で変換ファイルを用意しました。環境ごとにデプロイパッケージを作成する場合も、環境名*1を MSBuild のパラメータに指定するだけで済みます。
ここまでの変更で AppVeyor でも簡単にデプロイパッケージが作れるようになりました。
実際に今では 1 日に数多くのビルドを行い、Elastic Beanstalk へのデプロイを実行しています。
少しビルドとデプロイに時間がかかってしまっているのが課題としてありますが、今後のアプリケーション改善によって短縮を見込んでいます。
本番環境の移行
本番は規模の小さいアプリケーションから順に移行していきましたが、メインとなる宿泊アプリケーションに関してはリクエストが多く、移行作業での障害が発生した場合には多大な影響が発生します。そのため、アプリケーションの移行準備が完了してからの 1 ヵ月ほどは動作検証と負荷テストに時間を割り当てました。
基本的な動作検証には、既に利用している Selenium ベースの E2E テストを利用して実行しました。他にも運用担当者にお願いをして、AWS 上のアプリケーションで普段と同じ業務を試してもらうといった方法で進めていきました。
負荷テストはインスタンスサイズの大きな EC2 を用意し、そこから JMeter を使って特に重要なページと API に関して負荷をかけ、必要なインスタンスサイズと台数を決定しました。この辺りはテストの鬼 id:akasakas がほぼやってくれました。
www.ikyu.com は AWS 移行の前に Fastly への全面的な移行を完了していたため、本番環境を AWS 上に用意してしまえば、後は Fastly 側でバックエンドを切り替えるだけで移行が完了します。
何か意図しない挙動が発生した場合には即座にオンプレに戻すと決めていましたが、そういったことは発生することなく、極めて平和に終わった本番移行でした。
その後の運用
準備と検証をしっかりと行ったため、クラウド移行は特に大きな問題もなく、スムーズにほぼ予定通りに完了することが出来ました。事前の負荷テストによって決定したサイジングもほぼ想定通りでした。
今回上げた内容以外にも、クラウド向けにアプリケーション側で最適な形となるように作業を行った結果、サービス提供に必要なマシン台数も大幅に減らしつつ、可用性を高めることが出来ています。ぱっと思いつくだけでも以下のようなメリットが、AWS への移行で得られました。
- オートスケーリンググループによる柔軟なリソースの割り当て
- 不良インスタンスは自動的に破棄、再生成
- Web サーバーのメンテナンスがほぼ不要に
- Datadog と New Relic のメトリックを見るぐらいに
- ホスト OS に対する更新をイミュータブルに実行
- ローリングアップデートを自動で行い、ヘルスチェックが通らない場合は自動的に元に戻される
- デプロイが原因となる障害発生なし
半分は Elastic Beanstalk を利用したことによって得られています。一休社内では Elastic Beanstalk を多用していて、今 naoya さんが中心になって進めている、新レストランサービスでも Elastic Beanstalk と Docker が利用されています。
オンプレ時代にデプロイの課題を解決するために作られた Slack チャンネルがアーカイブされたのも、クラウド移行に伴ってデプロイに起因する問題が解消されたことの表れでもあります。
今回、移行には非常に長い時間がかかってしまいましたが、単純に移行するのではなく最適化した形で持って行ったことで、数多くの課題が同時に解決されたと考えています。
移行が完了して 1 ヵ月後に打ち上げを行い、クラウド移行での思い出を語り合いました。
内容と全く関係はありませんが、赤坂には良いレストランが数多くあり、一休.comレストランを使えば簡単に予約することができます。実際に打ち上げ会場は一休レストランで予約しました。
まだまだ積み残した課題は多いですが、なかなか経験できない重要な作業に参加することが出来て、非常に勉強になりましたし楽しかったです。
宿泊アーキテクチャの改善
元々は宿泊アプリケーションのアーキテクチャ改善のために誘われていたのですが、クラウド移行が完了したので最近になってようやく本格的に取り掛かれるようになりました。
クラウド移行のタイミングでプロジェクト間の依存関係を徹底的に整理した結果、Visual Studio と ReSharper を使った機械的な解析が行えるようになりました。現在は naoya さんからの助言もありコードリーディングを深くまで行い、問題点をしっかりと理解してまとめる作業をしています。
まだ始まったばかりですが、アーキテクチャを改善し開発効率だけではなく、それに伴ってパフォーマンスの向上までを目標としています。
終わりに
明日は juri-t さんによる「最近流行りのword2vecをLDAと比較してみた」です。
*1:Production / Staging など