noteのRSSをLambdaでJSON変換し、CloudFront経由で配信する
会社ホームページをWordPressから静的サイトに移行する際に、WordPressで運営していたブログをnoteへ移行しました。
会社ホームページに最新note記事6件を表示したかったのですが、クライアントサイドからのnote RSSへのリクエストはCORSエラーになり失敗するので、note RSSへのリクエストを行うサーバーサイドを用意する必要がありました。
最もシンプルな方法を考えた結果、AWS Lambda関数でnote RSSにリクエストし、JSONでレスポンスし、それをクライアントサイドからfetchするのが良いと考えました。
この記事では、やったこと、ハマったことを記載します。
やったこと
サマリー
今回は、note RSSへリクエストし結果をjsonでレスポンスするLambda関数を作成し、CloudFront経由でのみリクエストできるようにし、クライアントサイドからは該当のCloudFront URLをfetchするようにしました。
以下では、やったことの詳細を記載します。
1. Lambda関数を作成し、関数URLを設定する
Lambdaにデプロイしたコードは、noteのRSSフィード(XML形式)から最新6件の記事を取得し、必要な情報(タイトル・リンク・サムネイル)を抽出してJSON形式で返すNode.jsスクリプトです。
RSS の取得は https モジュール + 正規表現で行い、title / link / media:thumbnail のみを抽出しました。コードはシンプルです。
index.mjs
import https from 'https';
const RSS_URL = 'https://note.com/gaogaoasia/rss';
const MAX_ITEMS = 6;
const fetchXml = (url) =>
new Promise((resolve, reject) => {
https.get(url, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => resolve(data));
res.on('error', reject);
});
});
const extractTag = (xml, tag) => {
const match = xml.match(new RegExp(`<${tag}>(.*?)</${tag}>`, 'is'));
return match ? match[1].trim() : '';
};
const parseItems = (xml, max) => {
const itemMatches = Array.from(xml.matchAll(/<item>([\s\S]*?)<\/item>/g)).slice(0, max);
return itemMatches.map(match => {
const itemXml = match[1];
return {
title: extractTag(itemXml, 'title'),
link: extractTag(itemXml, 'link'),
ogImage: extractTag(itemXml, 'media:thumbnail'),
};
});
};
export const handler = async () => {
try {
const xml = await fetchXml(RSS_URL);
const items = parseItems(xml, MAX_ITEMS);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300',
},
body: JSON.stringify(items),
};
} catch (error) {
return {
statusCode: 500,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
},
body: JSON.stringify({
error: 'Failed to fetch RSS',
message: error.message,
}),
};
}
};
またLambda関数には、関数URLを設定しました。関数URLを設定することで、Lambda関数をURLで公開し、実行できるようになります。関数URLの認証方式を「IAM」に設定し、CloudFront以外からアクセスできないようにしていきます。
2. CloudFrontを作成し、Lambdaの関数URLを紐付ける
上記のLambda関数の関数URLだけでも最低限のことはできますが、キャッシュさせたかったこと、またセキュリティ面から、CloudFront経由でLambda関数を実行するようにしました。
CloudFrontを作成し、オリジン編集画面で、Orgin Domainに、Lambda関数URLを設定します。
3. OACを設定し、CloudFront経由のみLambda関数URLにアクセス可能にする
Lambda関数URLの実行は、CloudFront経由のみに制限する必要がありました。
調べたところOAC(Origin Access Control)という方法があるようで、CloudFront OACを使用することで、指定されたCloudFront経由のアクセスのみを許可することで、Lambda関数の関数URLを保護できるようになります。
設定手順は、こちらの記事を参考にさせていただきました。ありがとうございました。
4. CloudFrontにキャッシュポリシーを設定する
note記事は頻繁に変更があるわけではないので、CloudFrontにキャッシュポリシー(TTL(min/max/default)を 60/300/300 秒)を設定しました。
キャッシュで、大量アクセス時でもLambda関数への負荷を実質的に回避
また、大量アクセスが来た場合に、Lambda関数が毎回実行されるのを防ぐ必要もありました。
キャッシュポリシーを設定することで、キャッシュが有効な間は、CloudFrontがレスポンスを返すため、Lambda関数は呼び出されません。これにより、大量アクセスを受けた場合にも毎回Lambda関数が実行されることを回避できます。
当初はWAFやLambda@Edgeでレート制限が必要かと思いましたが、上記のキャッシュポリシーの設定で今回は十分だと考えました。
5. クライアントサイドからfetchする
クライアントサイドから、CloudFrontのURLをfetchすることで、最新のnote記事をrss経由で取得できることを確認できました。
const response = await fetch("https://xxx.cloudfront.net/");
const articles = await response.json();
ハマったこと
1. レスポンスヘッダーのAccess-Control-Allow-Originが重複し、CORSエラーが発生した
作成したCloudFront URLを、クライアントサイドからfetchした際に、CORSエラーが発生し、以下のコンソールエラーが出力されました。
Access to fetch at 'https://xxx.cloudfront.net/' from origin 'https://xxx.com' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values '*, *', but only one is allowed. Have the server send the header with a valid value, or, if an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
原因
実際に誤ってAccess-Control-Allow-Originを重複して設定していました。箇所は、Lambda関数内のレスポンスヘッダーと、Lambda関数の関数URLの許可オリジンです。
Lambda関数内のレスポンスヘッダー
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=300',
},
Lambda関数の関数URLの許可オリジン
修正
Lambda関数内のレスポンスヘッダーの 'Access-Control-Allow-Origin': '*',
を削除し、CORSエラーを解決できました。
まとめ
Lambda関数でnoteのRSSをJSON化し、CloudFront経由で配信することで、CORS問題を回避してnote記事情報を取得できました。シンプルな構成で、静的サイトでも動的な情報連携が可能になりました。
Discussion