Fastlyのパスベースルーティングで実現するWEARのゆるやかなクラウド移行

OGP

はじめに

こんにちは。メディアプラットフォーム本部 WEAR部 WEAR-SREの長尾です。

WEARは2013年にリリースされ、現在8年目のサービスです。そして、2004年にリリースされた当時のZOZOTOWNと同じアーキテクチャを採用しているため、比較的古いシステム構成で稼働しています。本記事では、そのWEARのWebアプリケーション刷新とクラウド移行で実践している、Fastlyを活用したパスベースルーティングによる段階移行の取り組みを紹介します。

WEARをリプレイスする理由

WEARのWebアプリケーションは、データセンターでオンプレミス(以下、オンプレ)上で稼働しています。また、DBはSQL Serverを利用しています。長年このアーキテクチャで成長を続けてきましたが、今後さらに成長を加速させていくためには以下の3点を実現する必要があります。その実現に向け、2年前からリプレイスに着手しています。

  • 開発スピードの加速化
  • コストの削減
  • 人材の増強

経緯に関する考え方はZOZOTOWNのシステムと同様なため、以下のスライドもご覧ください。 speakerdeck.com

リプレイス後のシステムは、クラウドはAWSを採用し、ALB、ECS、Fargate、Railsアプリケーションをベースにした構成です。 WEARシステム構成図

Fargate x Railsアプリケーションの詳細は、id:takanamitoさんの記事で紹介されているので、合わせてご覧ください。 techblog.zozo.com

リプレイスにおける課題と解決策

WEARは日々新しい機能追加や機能改善をしているサービスです。そのため、新機能を取り込みつつ、全く新しいアプリケーションを作って切り替える、ビックバンアプローチは難しいと考えました。

そこで、パス単位でオンプレ環境とAWS環境に適宜ルーティングする機能(パスベースのルーティング)を用意し、パス単位でAWS側に機能を作成して徐々に切り替えていくアプローチをとることにしました。

パスベースのルーティング図

パスベースのルーティングを実現する方法として、CloudFrontとFastlyを比較しました。その結果、これから紹介する2つの理由により、Fastlyのほうが導入の難易度が低く、保守性も高いと判断し、Fastlyを採用しました。

1. DNSの移行が不要

WEARのホストはZone Apexである「wear.jp」を利用しています。wear.jpのDNSはAWS外で管理されており、CloudFrontを利用するためには事前にDNSをRoute 53に移設する必要があります。一方で、FastlyはAnycast Addressが払い出され、それをAレコードに利用できるため、DNSを移行しなくてもZone Apexの設定が可能です。

詳しくは公式ドキュメントをご参照ください。

2. 設定反映が速い

Fastlyは設定の反映が高速で、ロールバックが必要になったとしてもすばやく対応できます。WebのトラフィックをすべてFastlyで受けるため、トラブルが発生した際の影響を最小限にするためにも、最短でロールバックできることは大きなメリットだと考えました。

サービス 設定反映までの時間 参考リンク
Fastly 5秒程度 Fastly network map | Fastly
CloudFront 5分程度 AWS Blog

Fastlyを利用したパスベースのルーティング設定

まずはじめに、Fastlyについて少し触れておきます。一般的なFastlyのイメージはCDN(Content Delivery Network)だと思いますが、VCLを使った柔軟な設定ができるという特徴があります。下図のように「あらかじめ定義したバックエンドに対し、条件に合わせてルーティングを設定する」という用途にも適しています。

パスベースのルーティング構成図

パスベースのルーティング設定例

以下のサンプルは、Edge Dictionariesとtable.contains関数を利用したパスベースのルーティング設定例です。

詳細については公式のドキュメントをご確認ください。

docs.fastly.com

まずバックエンド(オリジンサーバ)を作成します。

backend backend1 {
    .between_bytes_timeout = 10s;
    .connect_timeout = 1s;
    .dynamic = true;
    .first_byte_timeout = 15s;
    .host = "backend1のHost名";
    .max_connections = 600;
    .port = "443";
    .share_key = "xxxxxxxxxxxxxx";
    .ssl = true;
    .ssl_cert_hostname = "Fastly側のHost名";
    .ssl_check_cert = always;
    .ssl_sni_hostname = "Fastly側のHost名";
    .probe = {
        .expected_response = 200;
        .initial = 3;
        .interval = 5s;
        .request = "HEAD /healthcheck HTTP/1.1" "Host: xxxxx.jp" "Connection: close" "User-Agent: Varnish/fastly (healthcheck)";
        .threshold = 3;
        .timeout = 32.767s;
        .window = 5;
      }
}
 
backend backend2 {
    .between_bytes_timeout = 10s;
    .connect_timeout = 1s;
    .first_byte_timeout = 15s;
    .host = "backend2のHost名";
    .max_connections = 600;
    .port = "80";
    .share_key = "xxxxxxxxxxxxxx";
    .probe = {
        .expected_response = 200;
        .initial = 3;
        .interval = 5s;
        .request = "HEAD /healthcheck HTTP/1.1" "Host: xxxxx.jp" "Connection: close" "User-Agent: Varnish/fastly (healthcheck)";
        .threshold = 3;
        .timeout = 32.767s;
        .window = 5;
      }
}∂

次にEdge Dictionariesを使用して、パスを登録していきます。

table path_routing_dict {
    "/path1": "aws",
}

table.contains関数を利用して、Edge Dictionariesに登録されているパスとURLパスが一致するかを判定します。

if (table.contains(path_routing_dict,req.url.path)) {
    set req.backend = backend1;
} else {
    set req.backend = backend2;
}

Edge Dictionariesを使った方法は、シンプルな記述ができるというメリットがありますが、パスが完全一致する場合にしか使かえないという制約がありました。そのため、「/path1/以下はすべてbackend1にルーティングする」という場合は、前方一致のif文を記述しています。

if (req.url.path ~ "^/path1/") {
    set req.backend = backend1;
} else {
    set req.backend = backend2;
}

導入時に発生した課題と解決策

次に、Fastlyをクライアント(ブラウザなど)とバックエンドの間に挟むことで発生した課題とその解決策をご紹介します。

発生した課題

弊社がオンプレ環境で利用しているロードバランサは、歴史的経緯でパーシステンス機能を有効にしており、以下のようなロジックで動いています。

ロードバランサのパーシステス機能フロー図

これにより、同一クライアントのリクエストは同一アプリケーションサーバに転送する仕組みを実現しています。

クライアントとバックエンドの関係図

パスベースのルーティング構成ではクライアントとロードバランサの間にFastlyが挟まっています。そのため、ロードバランサは下図のように、Fastly EdgeとのTCPコネクション(Keep-Alive)からサーバ割り当てを判定します。

FastlyEdgeとバックエンドの関係図

クライアントは毎回同じFastly Edgeを経由する保証が無いため、リクエスト毎に違うアプリケーションサーバを割り当てられてしまうという事象が発生しました。これにより、既存システムの仕様により一部の機能が正常に動作しなくなることが判明しました。

解決策

解決方法は2つ考えられました。

  1. ロードバランサの判定ロジックを変更する
  2. FastlyでKeep-Aliveを無効化する

検討の結果、ロードバランサで対応する場合は影響範囲の確認と動作検証に時間を要すると考え、FastlyでKeep-Aliveを無効化する方針を採用することにしました。

【設定例1】コネクションをクローズさせる設定

sub vcl_miss {

    # 省略
    set bereq.http.connection = "close";
}
sub vcl_pass {

    # 省略
    set bereq.http.connection = "close";
}
sub vcl_pipe {

    # 省略
    set bereq.http.connection = "close";
}

Keep-Aliveを無効化すると、リクエストは下図のように転送されるため、発生していた課題を解決できました。

Keep-Aliveの無効化後の関係図

判定ロジックにより、リクエストがすべて下図の赤枠部分に該当するようになります。

Keep-Aliveの無効化後のパーシステンス機能フロー図

【設定例2】1Edge当たりのコネクション最大数(max_connections)の設定

前述の設定により課題は解決したのですが、新しい懸念も発生しました。この構成では1リクエストごとに新規セッションを確立するため、コネクション数が枯渇するリスクがあります。また、TCPコネクションを確立するためのオーバーヘッドがレスポンスタイムに影響します。この点は、システムの計測とチューニングを行った結果、現状問題は発生していません。

backend backend1 {

    #省略
    .max_connections = 1000;

}

今回紹介した課題と解決策はほんの一例です。既存環境に新しいリソースを組み込むことから、想定外の挙動は発生するものだと考え、柔軟に対応していく必要があります。そのため入念な検証を実施することをお勧めします。

Fastlyの運用

実際に、どのようにFastlyを運用しているのかを簡単に紹介します。

リリースの仕組み

Fastlyの設定はすべてTerraformで作成しており、そのコードはGitHubで管理しています。CI/CDはCircleCIを利用しています。mainブランチへマージしたタイミングでステージング環境に反映し、リリースタグを切ったタイミングで本番環境へ反映するように自動化しています。CI/CDに関する詳細な説明は、後日別の記事で紹介したいと思います。

CI/CD

監視の仕組み

監視は、FastlyのメトリクスとログをDatadogに取り込んで実現しています。

コストの観点から、Datadog Logsのログ保存期間は2週間にしており、それ以前のものはS3へ転送することで長期保存も実現しています。過去ログの検索はS3のログをAthenaで検索する運用にしています。

ログ監視の仕組み

次に、監視している項目を一部ご紹介します。

503エラー

503エラーをレスポンスのメッセージごとにカウントし、それぞれ対策を実施していきます。

503エラーグラフ

503エラーの原因については、公式のドキュメントに詳しく記載されていますのでご参照ください。 docs.fastly.com

同一IPからのリクエスト数

同一IPから大量にリクエストが来ていないかを監視しています。一定の閾値を超えたIPに対し、攻撃かクローラーアクセスかを判定し、必要なものはブロックする運用をしています。

同一IPからのリクエストグラフ

まとめ

オンプレ環境とAWS環境を共存させながら、パス単位で徐々にクラウド移行していくアプローチについて、Fastlyを利用した事例をご紹介しました。今後はFastlyを利用したサイト高速化へのアプローチや、DoS防御などのセキュリティ向上への取り組みに関しても別の記事でご紹介できればと思います。

さいごに

ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

tech.zozo.com

カテゴリー