こんにちは、グノシー開発部の山本です。 今回は、CloudFrontの機能であるLambda@Edge + Headless Chromeを使ってアプリ内に表示される画像の自動生成 + ホスティングを行った事例について紹介します。
この記事は Gunosy Advent Calendar 2019 の19日目の記事です。 前回の記事は @macyuto さんの 新しくサービスを作り始める上で考え実践していること でした。
はじめに
Lambda@Edge とはAmazon CloutFrontの機能で、ユーザーに近いエッジロケーションにおいて任意のコードを実行し、その結果をレスポンスやオリジンへのリクエストとして利用することができます。利用方法や自動でスケールする点、サーバーレスであることは一般のLambdaと同じですが
- NodeあるいはPythonランタイムしか利用できない
- デプロイ先としてイベントタイプを選択する必要がある
- デプロイパッケージのサイズ制限が普通のLambdaより厳しい
- 50 MBあるいは1MB(イベントタイプによって異なる)
という違いがあります。
イベントタイプとはLambda@Edgeがどのタイミングで実行されるかを指定するもので上記の図のように4種類(Viewer request, Origin request …)あり、要件ごとに切り替える必要があります。
背景
Gunosy社では、グノシー・ニュースパス・LUCRA ・オトクルの4つのアプリでクーポンコンテンツを提供しています。 クーポン一覧画面では各クーポンの内容が以下のように並べられています。
リリース当初はこれらの画像は、クーポンが新しく追加されるたびに手動で作成されており
- クーポン運用担当者だけで作業を完結できない
- デザインのアップデートが容易ではない(すべて入稿し直す必要がある)
- 手作業で作成しているためにレイアウトが微妙に違うことがある
- 各アプリのデザインに合わせてアクセントカラー等を変えると作成コストがn倍になってしまう
といった問題がありました。 そのような問題に対して、クーポン画像自動生成の仕組みを作り、運用コストを大幅に下げたいというお気持ちが今回の記事の背景になります。
概要
今回はCloudFront + Lambda@Edge(Node) + Puppeteer + Headless Chromeという構成で画像生成を行いました。 処理の流れは
- ユーザーがクーポン情報をつけて画像をリクエストする
- CloudFrontがすでに生成した画像でなければOrigin RequestイベントでLambdaを起動する
- Lambda@Edgeでクーポン情報からHTMLを生成する
- Puppeteerを使ってHeadless ChromeでHTMLを表示する
- Puppeteerを使ってスクリーンショットを取得し、画像データを返す
- CloudFrontのキャッシュに生成された画像が保存される
- ユーザーに表示される
となります。 処理速度が当初懸念としてあったのですが、コールドスタートを除けば200ms程度で生成できており、キャッシュに乗ってしまえば一瞬でレスポンスが返るので速度的には問題がなかったです。アプリエンジニアから見るとただの画像URLをロードしているだけなので、クライアントサイドも特に特別な処理をせずに動的な画像を表示できています。
また、アプリごとの個別のデザインもURLにどのアプリからのアクセスかを含めることで、動的にデザインが変わるようになっています。
採用理由
今回Lambda@Edge以外には
- クーポン作成時にAPIで画像を生成し、S3等に保存する
- CDN -> API Gateway -> Lambdaで画像生成
- CDN -> 画像生成サーバー
- アプリで見た目がおなじになるようにレイアウトする
といった選択肢も考えられましたが、最終的には
- 管理対象が非常に少ない(CloudFront + Lambda@Edgeのみ)
- スケールを気にしないで良い
- デザインのアップデートが容易(Lambdaをデプロイし、invalidateすれば更新される)
- 各アプリで個別に実装する必要がない
といった利点が大きかったためLambda@Edgeを採用しました。
また、Lambda@Edgeでの実際の画像生成についてはHeadless Chrome + Puppeteerでクーポン画像を模したHTMLページのスクリーンショットとして生成する方式を採用しました。HTMLのスクリーンショット方式を採用した理由としては、もともとが手動で画像を生成していたこともあり、以下のようにレイアウトの種類が多く、今後の要件も読みづらかったため、パーツのHTML入稿をしてもらう方が柔軟に対応できると考えたためです。
技術的な詳細
Nodeのライブラリ選定
今回(Node.js 8.10)使用したライブラリは
“@serverless-chrome/lambda”: “^1.0.0-55”, “child_process”: “^1.0.2”, “chrome-remote-interface”: “^0.26.1”, “puppeteer”: “^1.20.0”
です。Lambdaでheadless chromeを使う場合には一般的な設定だと思います。 child_process
は後述の日本語フォントの読み込みに利用しています。
注意点としては .npmrc
で puppeteer_skip_chromium_download=true
を指定しないと、puppeteerに付属するChromeも合わせてダウンロードしてしまうため、デプロイパッケージのサイズが大きくなり、Lambdaのデプロイ制限にひっかかってしまいます。
指定しても48MB程度になってしまうので、Lambda@Edge(50MBまで)でこれ以上にライブラリを複数入れるのは現実的に厳しいところがあります。
イベントタイプの選択
今回のようなLambda@Edgeを用いて動的にコンテンツを生成し、キャッシュに生成物を保存したい場合、Origin Request, Origin Responseの2種類から選択することになりますが、
- Origin Request
- 利点
- Originを全く経由せずにデータを返せる(ので速いはず)
- CloudFrontのキャッシュ上にしか生成物が存在しないので、invalidateすると結果が即時に反映される
- 欠点
- CloudFront上のキャッシュが削除された場合にもう一度起動されてしまうので、キャッシュ時間が短い場合に呼び出し頻度が高くなってしまう(コストがかかる, レイテンシの低下)
- ダミーのoriginを設定しないといけない(実害はないですが)
- 利点
- Origin Response
- 利点
- Originに生成物を保存できる
- 生成物をS3に保存するコードを書くと、キャッシュが消えてしまっても生成物自体はS3に存在するのでレスポンス速度の低下が軽減される。(この場合、Origin S3から403が帰ってきたときだけ生成する処理になります)
- 上記の理由から、画像の動的リサイズ/変換等の入力パラメータに対して出力が一定のものに適している
- 公式のガイドも Amazon CloudFront & Lambda@Edge で画像をリサイズする | Amazon Web Services ブログ この仕組でコストを抑えている。
- Originに生成物を保存できる
- 欠点
- (Originに生成物を保存した場合) Lambda@Edgeのコードの変更反映にオリジンファイルの更新あるいは削除が必要
- 利点
のように、それぞれ利点/欠点があるのでそれを踏まえて選択すると良いと思います。
今回は、キャッシュ期間を十分に長く取れるため、Originに生成物を置くことによるコスト削減メリットはあまり大きくないことが想定されたため、アップデートが容易にできるOrigin Requestを採用しました。
フォント問題
上記のライブラリのまま素で実行すると、日本語が正しく表示されない、あるいは想定したフォント以外で表示されない問題があります。 その場合は
が参考になります。 また、Lambda@Edgeの場合はフォントサイズも50MB制限のネックになってくるのでサブセットフォントを使うなどの工夫も必要になります。
おわりに
今回はLambda@Edgeでのコンテンツ画像生成の事例について紹介しました。 CDNのEdgeコンピューティングは画像のリサイズ/認証等で利用されることが多いですが、応用の幅が広い技術なので参考になれば幸いです。
今回のクーポン画像生成は、インターンのYくんや弊社の井口さんに技術的に協力していただきました。ありがとうございました。
明日は弊社の @gumigumi4f さんの何らかの記事になると思います。楽しみですね。