Wiz テックブログ

Wizは、最新のIoTやICTサービスをお客様に届ける「ITの総合商社」です。

Lambda@EdgeでCloudFrontの配信をコントロールしたい

Lambda@Edgeとは

CloudFrontのエッジサーバでコードを実行するLambda関数です。

配信をカスタマイズ出来ます。

感覚としてはCloudFrontのフックで動くLambda、という感じですが考慮する点・制約がいくつかあります。

  • 番号付きバージョンのみをトリガーに設定出来る(エイリアスに設定出来ない)
  • バージニア北部(us-east-1)リージョンのみ

詳しくはドキュメント

設定出来るフック

f:id:wiz-yoshitomi:20210126161055j:plain

Lambda@Edgeがフックに設定出来る箇所は四つあります。

  • ビューワーリクエス
  • ビューワーレスポンス
  • オリジンリクエス
  • オリジンレスポンス

キャッシュがある時はオリジンリクエスト・オリジンレスポンスは発生しません。

例:webp画像を配信して表示速度高速化したい

  1. リクエストした画像(jpeg,png)のwebpファイルが存在したらwebpファイルを取得する
  2. 存在しない場合はwebpファイルを作成してから取得する

このようなLambda@Edgeを作成し、オリジンリクエストに設定してみます。

前提:S3+CloudFrontの構成は既に作成されているものとする。

Lambdaを用いたS3画像の取得・加工はドキュメントを参考にしています。

①IAMロールを作成

アクセス許可

AWSLambdaExecuteをアタッチ

信頼関係

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "edgelambda.amazonaws.com",
          "lambda.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

② Lambda関数を作成

最初に述べた制約に則り関数を作成します。 今回の設定は以下です。

リージョン:us-east-1 ランタイム言語:Node.js 12.x 実行ロール:前項目で作成したロールを設定

③関数コードを作成

index.jsファイルを作成

const AWS = require('aws-sdk');
const sharp = require('sharp');
const https = require('https');
const url = require('url');
const s3 = new AWS.S3();

const BASE_URL = 'CloudFrontURL';
const BUCKET_NAME = 'S3バケット名';

exports.handler = async (event, context) => {
    const request = event.Records[0].cf.request;
    
    // 画像のみ
    if (request.uri.match(/\.(jpe?g|png)$/) && request.uri.split[1] !== 'tmp') {

        const webpUri = request.uri + '.webp';
        let cloudfrontReq = url.parse(BASE_URL + webpUri);
        let webpExits = false;

        // webpファイルへ正常にアクセス出来るか
        await new Promise((resolve, reject) => {
            cloudfrontReq.method = 'HEAD';
            cloudfrontReq.timeout = 3000;
            https.request(cloudfrontReq, cloudfrontres => {
                if (cloudfrontres.statusCode >= 200 && cloudfrontres.statusCode < 300)
                    webpExits = true;

                resolve();
            }).on('error', reject).end();
        });

        // webpファイルが無い場合は作成する
        if (!webpExits) {

            const key = request.uri.substr(1)
            const webpKey = webpUri.substr(1)

            // オリジンの画像を取得
            try {
                const params = {
                    Bucket: BUCKET_NAME,
                    Key: key
                };
                let origimage = await s3.getObject(params).promise();

            } catch (error) {
                console.log(error);
                return;
            }

            // webpへ変換
            try {
                let buffer = await sharp(origimage.Body).webp().toBuffer();

            } catch (error) {
                console.log(error);
                return;
            }
            
            // S3へput
            try {
                const putParams = {
                    Bucket: BUCKET_NAME,
                    Key: webpKey,
                    Body: buffer,
                    ContentType: "image"
                };

                const putResult = await s3.putObject(putParams).promise();
                webpExits = true;
            } catch (error) {
                console.log(error);
                return;
            }
        }
        
        // webpが存在する or 変換が成功した場合webpのuriでrequestを上書きする
        if (webpExits)
            request.uri = webpUri;
    }

    return request;
};

④デプロイパッケージを作成

convert-webpフォルダ(名前は任意)を作成し、そこにindex.jsを入れ、そこにnpmでsharpライブラリをインストールします。

$ npm install --arch=x64 --platform=linux --target=12.13.0 sharp

今回の場合のファイル構造は、以下のようになります。

convert-webp
├ index.js
└ node_modules
    ├ sharp
    ├ ...

⑤.zipファイルをアップロード

作成したファイルをzipへ圧縮し、「関数コード」→「アクション」→「.zipファイルをアップロード」の手順でアップロードを行います。

⑥テストを行ってみる

こちらのドキュメントより、オリジンリクエストのイベント構造を確認出来ます。

⑦ Lambda@EdgeをデプロイしCloudFrontへ設定する

「デザイナー」→「トリガーの追加」をクリックします。

「トリガーを選択」→「CloudFront」で出現する「Lambda@Edgeへのデプロイ」項目を選択します。

f:id:wiz-yoshitomi:20210126181909p:plain

以上を入力し「デプロイ」をクリックで設定完了です。

⑧確認

DevToolを開き「Networkタブ」→対象の画像を選択

Responce headerのcontent-typeがimage/webpになっていればOKです!

注意点なのですが今回はオリジンリクエストにフックしているため、キャッシュが残っている状態だとオリジンリクエストが発生せず関数が発火しません。

確認の際はご注意ください。

まとめ

今回は省略していますが、CloudFrontのheaderのホワイトリストAcceptを追加し、webpを許可しているブラウザかの確認の処理があった方が良いかと思います。

Lambda@Edgeを使用せずにS3へのオブジェクト追加をフックにしたLambdaでwebpファイルを生成するという方法も考えられますが、同一バケットの「オブジェクト生成」フックで新規オブジェクト追加する構成は再起的に実行されてしまう危険性があるため推奨されていません。

やはりキャッシュがない時(オリジンリクエストが発生する時)の起動は体感でも遅く感じるので、ユースケースに合わせてキャッシュの有効期限を長くしたりなど対処した方が良いかなと思いました。

最近話題のLambdaのコンテナイメージフォーマットもどうやら使えるようなので、こちらも試してローカルまで一貫したサクサク開発にしてみたいです。

〜最後に〜

Wizではエンジニアを募集中です!

興味のある方は是非ご覧ください!

【フロントエンドエンジニア】

場所にとらわれず自社メディア成長に貢献したいフロントエンドエンジニア募集! - 株式会社WizのWebエンジニアの求人 - Wantedly

【バックエンドエンジニア】

勤務地自宅を叶える!バックエンドエンジニアとして事業を成長させたい方募集 - 株式会社WizのWebエンジニアの求人 - Wantedly