📔

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
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以外からアクセスできないようにしていきます。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/urls-configuration.html

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を保護できるようになります。

設定手順は、こちらの記事を参考にさせていただきました。ありがとうございました。
https://qiita.com/Kanahiro/items/85573c9ae724df435a6a

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経由で取得できることを確認できました。

rss.js
const response = await fetch("https://xxx.cloudfront.net/");
const articles = await response.json();

会社ホームページに最新のnote記事6件を表示する

ハマったこと

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記事情報を取得できました。シンプルな構成で、静的サイトでも動的な情報連携が可能になりました。

GAOGAO Engineer Blog

Discussion