この記事は CyberAgent Developers Advent Calendar 2021 20日目の記事です。

はじめに

こんにちは。株式会社CAMでバックエンドエンジニアをしている大村GOです。普段は、執行役員としても動きつつ、プレイヤーとしてもサービスの開発運用をしています。
今回はSPAで動いている既存サービスの特定パス以降を、Gatsbyを使いSSGで作られたコードを見てほしいという要件に、CloudFront + Lambda@Edgeを使い対応したお話をしたいと思います。
複数のやり方を模索したので、複数案を書いていますが結果案3になったので、方法のみ知りたい方は案3まで飛んでいただけると幸いです。

流れ

とある日、社内の最強フロントエンジニアに「新技術を導入したいから、特定のパス以降はこっちのS3を向いてほしい。ページスピードも上がるから!」とお願いされました。当然メリットが大きいのでやるという判断になり、実際どのような設計にするか考えた際のお話になります。
その詳しい要件は以下の通りでした。

要件

  1. 同じホストで特定のPath以降は、既存と違うS3を見てほしい
    具体的には、SPA用S3とは別のS3(以降SSG用S3といいます)に新コードを置いて、/hogehoge/* のアクセスはSSG用S3を見るようにしてほしい
  2. /hogehoge/* 以外は今まで通りSPA用S3のindex.htmlを返してほしい
  3. /hogehoge/*配下のアクセスの場合、hogehogeを消したパスへアクセスしてほしい
  4. /hogehoge/page-data/app-data.jsonのような実際に存在するファイルはそのまま返してほしい
  5. /hogehoge/{userId} のように可変的で存在しないファイルの場合は、SSG用S3直下の404.htmlを返してほしい
    ※ どうやらGatsbyで生成した404.htmlが、userIdのページへフロント内でいい感じにルーティングしてくれるみたいです。

表にまとめるとこうなります。

path S3 返すファイル
/fuga SPA バケット直下のindex.html
/hogehoge SSG バケット直下のindex.html
/hogehoge/page-data/app-data.json SSG page-data/app-data.json
/hogehoge/{userId} SSG バケット直下の404.html

 

アーキテクチャ

SPA

既存のSPAのサイトはCloudFrontのエラーページ機能を使った、以下のようなアーキテクチャになっています。

SPAアーキテクチャ

 

では、実際同ホストどのようにしてSSGのサイトを共存させたのか複数案を出し試してみたので、ひとつひとつお話したいと思います。

案1 CloudFront エラーページ + Lambda@Edgeでやる

この設計は結論NG
理由は後ほど説明します。

先ほどの説明の通り、元々SPAの方はCloudFrontのエラーページ機能を使って、CloudFront上でリダイレクトさせていました。

案1の方法

  • ビヘイビアに/hogehoge/* の場合、SSG用S3を指す設定を追加 (全案共通)
  • hogehogeを消すLambda@Edge関数をCloudFrontのorigin requestに入れる
  • 存在しないパスだった場合S3から403が返ってくるので、レスポンスコードを416(適当)にLambda@Edgeで書き換える
  • エラーページ機能で、416(適当)に/hogehoge/404.html へリダイレクトする設定を追加

Lambda@Edge

// エラーページに書き換える
response.body = res.Body.toString() || '';
response.status = '416';
response.headers = response.headers || {};
response.headers['content-type'] = [{
  key: 'content-type',
  value: 'text/html',
}];

エラーページ設定

エラーページ設定

NG理由

  • エラーページのカスタムコード数には限界がある
  • そもそも気持ち悪いので嫌だ

 

案2 S3 静的ウェブサイトホスティングでリダイレクトする

この設計も結論NG
理由は後ほど説明します。

詳しくはこちらのブログが参考になります。

案2の方法

  • ビヘイビアに/hogehoge/* の場合、SSG用S3を指す設定を追加 (全案共通)
  • エラーの場合、S3の静的ウェブサイトホスティングのリダイレクト設定にて404.htmlを返すように設定

NG理由

  • webhostingではフロントエンドまで返ってリダイレクトするので、URLが変わってしまう + そもそも今回の要件に合ってない
  • 静的ウェブサイトホスティングを有効にするにはS3を公開状態にしなければならず、CloudFrontのみからのアクセスを許可するという制限が入れれない

 

案3 CloudFront + Lambda@Edgeでやる

結論としては、この案を採用しました。
具体的にどのように行なったか詳しく説明したいと思います。

案3の方法

  • ビヘイビアに/hogehoge/* の場合、SSG用S3を指す設定を追加 (全案共通)
  • Lambda@Edge関数をCloudFrontのorigin requestに入れる
    • hogehogeを消す + /hogehoge配下はindex.htmlを返す
  • Lambda@Edge関数をCloudFrontのorigin responseに入れる
    • 403の場合、origin responseで404.htmlを取得してresponse bodyに入れて返す

流れ

SSGアーキテクチャ

origin request

exports.handler = async (event) => {
  const { request } = event.Records[0].cf;
  // /hogehoge/* から /hogehogeを消す
  const paths = uri.split('/').splice(2);
  let uri = path.join(...paths);

  // /hogehoge直下はindex.htmlを返す (web-hostingと同じ役割)
  if (uri === '.') uri = 'index.html';
  request.uri = `/${uri}`;
  return request;
}

origin response

exports.handler = async (event) => {
  const { response } = event.Records[0].cf;
  if (status === 403) {
    // エラーページ取得
    const res = await s3
      .getObject({ Bucket: 'hoge.com', Key: '404.html' })
      .promise()
      .catch(() => {});
    if (!res) {
      console.log('Failed get 404.html.', JSON.stringify(response));
      return response;
    }
        // エラーページに書き換える
    response.body = res.Body.toString() || '';
    response.status = '404';
    response.headers = _response.headers || {};
    response.headers['content-type'] = [{
      key: 'content-type',
      value: 'text/html',
    }];
  }
  return response;
}

注意点

まとめ

様々な案を検討したのですが、要件が複雑なためシンプルにCloudFront (origin request, origin response) に Lambda@Edge関数を導入する方法にしました。
今回の方法が使えるか分かりませんが、何かの参考になれば幸いです。

また、私事ですが12/23日に株式会社CAMの方のアドベントカレンダーも投稿するので、よければそちらもご覧ください!
そちらはよく使用しているSendgridについて書いています.
CAMアドベントカレンダー

2017年入社のバックエンドエンジニア。2021年10月より株式会社CAMの執行役員に就任。Node.jsを使い新規サービス開発などにも従事。