この記事は CyberAgent Developers Advent Calendar 2021 20日目の記事です。
はじめに
こんにちは。株式会社CAMでバックエンドエンジニアをしている大村GOです。普段は、執行役員としても動きつつ、プレイヤーとしてもサービスの開発運用をしています。
今回はSPAで動いている既存サービスの特定パス以降を、Gatsbyを使いSSGで作られたコードを見てほしいという要件に、CloudFront + Lambda@Edgeを使い対応したお話をしたいと思います。
複数のやり方を模索したので、複数案を書いていますが結果案3になったので、方法のみ知りたい方は案3まで飛んでいただけると幸いです。
流れ
とある日、社内の最強フロントエンジニアに「新技術を導入したいから、特定のパス以降はこっちのS3を向いてほしい。ページスピードも上がるから!」とお願いされました。当然メリットが大きいのでやるという判断になり、実際どのような設計にするか考えた際のお話になります。
その詳しい要件は以下の通りでした。
要件
- 同じホストで特定のPath以降は、既存と違うS3を見てほしい
具体的には、SPA用S3とは別のS3(以降SSG用S3といいます)に新コードを置いて、/hogehoge/* のアクセスはSSG用S3を見るようにしてほしい - /hogehoge/* 以外は今まで通りSPA用S3のindex.htmlを返してほしい
- /hogehoge/*配下のアクセスの場合、hogehogeを消したパスへアクセスしてほしい
- /hogehoge/page-data/app-data.jsonのような実際に存在するファイルはそのまま返してほしい
- /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のエラーページ機能を使った、以下のようなアーキテクチャになっています。
では、実際同ホストどのようにして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に入れて返す
流れ
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の仕様で、Lambda関数に含まれているライブラリの最大圧縮サイズの上限が決まっているので、ライブラリは選定しましょう。
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html
まとめ
様々な案を検討したのですが、要件が複雑なためシンプルにCloudFront (origin request, origin response) に Lambda@Edge関数を導入する方法にしました。
今回の方法が使えるか分かりませんが、何かの参考になれば幸いです。
また、私事ですが12/23日に株式会社CAMの方のアドベントカレンダーも投稿するので、よければそちらもご覧ください!
そちらはよく使用しているSendgridについて書いています.
CAMアドベントカレンダー