CloudFront で 画像最適化を行う 〜 Lambda Function URLの利用

# 初めに
何番煎じだというネタですが
サイトを運営する場合、画像の最適化は基本的な考慮事項だと思います。
特にtoC向けにサイトを提供する場合、サイトの表示速度はGoogle の検索スコアにも影響を与えるため、スマホやPCなど各プラットフォームごとに画像を最適なサイズやフォーマットで配信することがとても重要です。
ただ、サイトのページが対象にあったり、ユーザー投稿型の場合、自動でさまざまなサイズの画像を用意するのは難しいことが多いと思います。
もちろん、バックグラウンドで処理を行うというのが一般的ではありつつも、配信時に自動でリサイズしてくれるとバックエンド処理の考慮がなくなり、運用が楽になります。
そのため、現在多くのCDNで自動の画像最適化に対応しています。例えば、Akamiの場合は [Image & Video Manager](https://www.akamai.com/ja/products/image-and-video-manager) がありますし、Cloudflare であれば[Image Optimization](https://developers.cloudflare.com/images/)、また画像配信専用のさくらインターネットの[Imageflux](https://imageflux.sakura.ad.jp/)などがあります。

# CloudFrontの場合
さて、かの有名な AWS の CDN サービスである CloudFront ですが、　なんと画像最適化の機能を持っていません！
しかし、　そこはビルダーのためのサービスと自称している AWS 、無いなら同じようなことができるソリューションを提案しています！！

## CloudFront と Lambda@Eddge による画像最適化
１つは Lambda@Edgeを利用する方法です。

https://aws.amazon.com/jp/blogs/news/resizing-images-with-amazon-cloudfront-lambdaedge-aws-cdn-blog/

「CloudFront 画像最適化」で検索すると多くの記事でこれが出てきます。
昔からある方法であり、ビューアーリクエストでLambda@Edgeがリクエストパラメーターを最適化します。
また、オリジンレスポンスで Lambda@Edge が最適化済み画像が無い場合に最適化処理を行行い、S3に画像をアップロードしつつレスポンスとして最適化画像を返すというものです。
一度 Lambda@Edgeで処理を行った場合はCloudFrontのキャッシュが利用され、キャッシュが無くなった場合でも、S3に最適化後の画像をアップロードするため、Lambda@Edgeで同じ最適化処理は行われません。
なお、[ドキュメント](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/Expiration.html)にも書かれていますが、CloudFront はキャッシュしたファイルを有効期限切れの前に削除することがあります。
> エッジロケーションに頻繁にリクエストされないファイルがあれば、CloudFront は、頻繁にリクエストされるようになったファイル用にスペースを確保するために、そのファイルを削除する (そのファイルの有効期限が切れる前に削除する) 場合があります。

そのため、S3上に最適化後の画像はアップロードした方がいいと私は考えています（ここは色々ご意見があるポイントだとは思いますが）。

## CloudFront と Lambda Function URL による画像最適化
もう一つの方法が Lambda@Edge よりも後に提供された Lambda Function URL を利用する方法です。

https://aws.amazon.com/jp/developer/application-security-performance/articles/image-optimization/

ざっくり説明すると、ビューアーリクエストでパラメーターを最適化するために Lambda@Edge ではなく、CloudFront Function が利用されています。
CloudFront Function とは Lambda@Edge よりも新しい機能であり、Lambda@Edgeよりも拡張性がない代わりにより低レイテンシーでよりコストの安い機能です。
ドキュメントにどちらを利用するべきか書かれており、クエリパラメーターなどの変換では CloudFront Function の方が適切です。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/edge-functions-choosing.html

次に、画像最適化処理ですが、Lambda@Edge の場合ではオリジンレスポンスで処理しますが、 こちらのソリューションではオリジンフェイルオーバーを利用します。

具体的には CloudFront に２つのオリジンを作成します。
- S3 バケット を指すオリジン
- Lambda Fcuntion URL を指すオリジン

![スクリーンショット 2024-08-13 10.05.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1005574/ba117e30-482f-dbcd-2fae-a7deb820e849.png)

Lambda Function URL とは Lambda 関数を呼び出すための Lambda 関数の機能です。この機能が登場するまでは API Gateway を利用して Lambda関数を呼び出す必要がありましたが、この機能が登場し、直接 Lambda 関数を呼び出すことが可能となりました。

https://aws.amazon.com/jp/blogs/aws/announcing-aws-lambda-function-urls-built-in-https-endpoints-for-single-function-microservices/

また、Lambda Function URL は呼び出しに IAM認証を利用することもできます。　

![スクリーンショット 2024-08-13 10.34.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1005574/68ca5813-2c49-177a-7847-a2fce67becfe.png)

この機能を利用することで CloudFront の Origin access control を併用し、CloudFront からのみ起動できるようにすることが可能です。

![名称未設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1005574/765213b7-61c6-d5dc-a64d-b26388a70615.png)

CloudFront上にAWS CLIでの作成コマンドが出ているため、CloudShellなどでポリシーを作成します。
なお、マネジメントコンソールで作成したい場合は下記記事が参考になります。

https://qiita.com/Kanahiro/items/85573c9ae724df435a6a

ポリシーを作成すると、 対象の Lambda 関数の「設定 > アクセス権限 > リソースベースのポリシーステートメント」 に表示されます。
![スクリーンショット 2024-08-13 10.37.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1005574/da4373ea-723c-a680-b2a6-f8b07d1956f1.png)

次に、オリジングループを作成し、1st オリジンに「S3 バケット を指すオリジン」を 2stオリジンに「Lambda Fcuntion URL を指すオリジン」を指定します。
また、Failover の条件を指定します。（ 例えば、「403 Forbidden」）

![スクリーンショット 2024-08-13 10.06.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1005574/e1d3e5d2-b47a-055b-fcba-9462227b1067.png)

behavior では 作成したオリジングループを指定します。
これにより、 1st オリジンである　S3 に、条件に合致した画像が存在しない場合（403 Forbidden）、2stオリジンである Lambda が呼び出されます。

![スクリーンショット 2024-08-13 10.09.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1005574/7706efcd-865e-886f-6364-36b734fe955b.png)

Lambda の処理は Lambda@Edge とほぼ変わらず、最適化画像を作成し、 S3 にアップロードし、最適化画像自体をレスポンスとして返します。

なお、 Lambda@Edge の場合は 1MB 以上のレスポンスを返せませんが、Lambda Function URL は通常の Lambda と同じため、より大きな画像を返すことができます。
また、 コンテナが利用できたり、言語も Rust などが利用できるためより柔軟性があります。
Lambda@Edge の場合は エッジローケーションで実行されますが、Lambda Function URL の場合はリージョンで実行されるため、レイテンシーは多少遅くなる可能性がありますが、コスト面では Lambda Function URL の方が安いと考えられます。

| |リクエスト|処理時間|無料枠|
|:----|:----|:----|:----|
|Lambda (x86)|0.20USD/100万リクエスト |0.0000166667USD/GB-秒|1 か月あたり 100 万件の無料リクエスト、1 か月あたり 40 万 GB-s のコンピューティングタイム|
|Lambda@Edge|0.60 USD/100万リクエスト |0.00005001USD/GB-秒|無料利用枠はありません|
|CloudFront Function|0.10 USD/100万リクエスト |-|1 か月あたり 200万件 の 呼び出し|

さて、ここまではソリューションの紹介であり、前振りでしたが、ここからが本題です。

# 問題発生
Lambda Function URL のソリューションですが、MACで行う場合は実はこのままでは動きません。
実際に Github の readme で書かれている手順
```
git clone https://github.com/aws-samples/image-optimization.git 
cd image-optimization 
npm install 
cdk bootstrap 
npm run build 
cdk deploy -c DEPLOY_SAMPLE_WEBSITE=true
```
でデプロイし、動作を試してみると Lambda でエラーが発生します。
```
"errorType": "Error",
        "errorMessage": "Could not load the \"sharp\" module using the linux-x64 runtime\nPossible solutions:\n- Ensure optional dependencies can be installed:\n    npm install --include=optional sharp\n    yarn add sharp --ignore-engines\n- Ensure your package manager supports multi-platform installation:\n    See https://sharp.pixelplumbing.com/install#cross-platform\n- Add platform-specific dependencies:\n    npm install --os=linux --cpu=x64 sharp\n- Consult the installation documentation:\n    See https://sharp.pixelplumbing.com/install",
        "stack": [
            "Error: Could not load the \"sharp\" module using the linux-x64 runtime",
            "Possible solutions:",
            "- Ensure optional dependencies can be installed:",
            "    npm install --include=optional sharp",
            "    yarn add sharp --ignore-engines",
            "- Ensure your package manager supports multi-platform installation:",
            "    See https://sharp.pixelplumbing.com/install#cross-platform",
            "- Add platform-specific dependencies:",
            "    npm install --os=linux --cpu=x64 sharp",
            "- Consult the installation documentation:",
            "    See https://sharp.pixelplumbing.com/install",
            "    at Object.<anonymous> (/var/task/node_modules/sharp/lib/sharp.js:114:9)",
            "    at Module._compile (node:internal/modules/cjs/loader:1358:14)",
            "    at Module._extensions..js (node:internal/modules/cjs/loader:1416:10)",
            "    at Module.load (node:internal/modules/cjs/loader:1208:32)",
            "    at Module._load (node:internal/modules/cjs/loader:1024:12)",
            "    at Module.require (node:internal/modules/cjs/loader:1233:19)",
            "    at require (node:internal/modules/helpers:179:18)",
            "    at Object.<anonymous> (/var/task/node_modules/sharp/lib/constructor.js:10:1)",
            "    at Module._compile (node:internal/modules/cjs/loader:1358:14)",
            "    at Module._extensions..js (node:internal/modules/cjs/loader:1416:10)"
```

Googole で検索すると下記のように解決策が見つかります。　

https://celeron1ghz.hatenablog.com/entry/2024/07/15/094525

つまり、プラットフォームにあった「sharp」が見つからないのです（ログの通り！！）。
　というわけで、明示的に sharp をインストールする必要があります。
 ```
 npm install --os=linux --cpu=x64 --save sharp
 ```
を追加し、CloudShell で下記の一覧のコマンドを実行します。
 ```
git clone https://github.com/aws-samples/image-optimization.git 
cd image-optimization 
npm install --os=linux --cpu=x64 --save sharp
npm install 
cdk bootstrap 
npm run build 
cdk deploy -c DEPLOY_SAMPLE_WEBSITE=true
```

これで問題なく動作しました。

ちなみに、10MBのPNG画像を用意し、「/images/rio/10MB.png?width=200&height=302&format=auto」を試したところ Lambda の実行時間は 500msecぐらいで ブラウザ上でのレスポンスは 700msec ぐらいでした。

もし、　速度的に心配な場合はクロール等などを利用して、先んじで画像を作っていくのがいいかもしれません。