ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

Lambda@Edge を CloudFront Functions で置き換える

この記事は ユニファ Advent Calendar 2022 の 5 日目の記事です。

adventar.org

こんにちは、rightgo09 です。今回は Amazon CloudFront 上で実行できるエッジコンピューティング「Lambda@Edge」で実行するコードを「CloudFront Functions」に置き換えてみます。

昨日: Lambda@Edge と CloudFront Functions の料金比較 - ユニファ開発者ブログ

料金面でのメリットを確認したので、では実際に置き換えを考えてみます。

題材は、動的にサムネイルを生成する AWS 公式のブログで紹介されている Lambda@Edge です。

aws.amazon.com

ビューワリクエストとオリジンレスポンス、それぞれに設置するための 2 つのコードが掲載されています。

ビューワリクエスト用のコードは URL を都合よく変換しているだけなので、今回はこちらを CloudFront Functions に置き換えます。

現在オリジンレスポンスに CloudFront Functions を設置することはできませんが、それを抜きにしても、ここにはサムネイルを生成する重量級の処理があり、軽量向けの CloudFront Functions では耐えられそうにもないため、こちらは素直に Lambda@Edge で稼働させるのが良さそうです。

掲載されているビューワリクエスト用 Lambda@Edge の JavaScript コード

コード内容

このコードの仕事は、以下のようにリクエストの URL を変換することです。

GET /images/image.jpg?d=180x180 HTTP/2
Host: example.com
Accept: image/avif,image/webp,*/*
...

↓ Lambda@Edge 実行後

GET /images/200x200/webp/image.jpg HTTP/2
Host: example.com
Accept: image/avif,image/webp,*/*
...
  • URL パスの変換
    • d パラメータがあればそれに応じて URL に含める
    • Accept に webp が含まれているかによって URL に含めるパスを変える
コード

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

index.js

'use strict';

const querystring = require('querystring');

// defines the allowed dimensions, default dimensions and how much variance from allowed
// dimension is allowed.

const variables = {
  allowedDimension: [{ w: 100, h: 100 }, { w: 200, h: 200 }, { w: 300, h: 300 }, { w: 400, h: 400 }],
  defaultDimension: { w: 200, h: 200 },
  variance: 20,
  webpExtension: 'webp'
};

exports.handler = (event, context, callback) => {
  const request = event.Records[0].cf.request;
  const headers = request.headers;

  // parse the querystrings key-value pairs. In our case it would be d=100x100
  const params = querystring.parse(request.querystring);

  // fetch the uri of original image
  let fwdUri = request.uri;

  // if there is no dimension attribute, just pass the request
  if (!params.d) {
    callback(null, request);
    return;
  }
  // read the dimension parameter value = width x height and split it by 'x'
  const dimensionMatch = params.d.split("x");

  // set the width and height parameters
  let width = dimensionMatch[0];
  let height = dimensionMatch[1];

  // parse the prefix, image name and extension from the uri.
  // In our case /images/image.jpg

  const match = fwdUri.match(/(.*)\/(.*)\.(.*)/);

  let prefix = match[1];
  let imageName = match[2];
  let extension = match[3];

  // define variable to be set to true if requested dimension is allowed.
  let matchFound = false;

  // calculate the acceptable variance. If image dimension is 105 and is within acceptable
  // range, then in our case, the dimension would be corrected to 100.
  let variancePercent = (variables.variance / 100);

  for (let dimension of variables.allowedDimension) {
    let minWidth = dimension.w - (dimension.w * variancePercent);
    let maxWidth = dimension.w + (dimension.w * variancePercent);
    if (width >= minWidth && width <= maxWidth) {
      width = dimension.w;
      height = dimension.h;
      matchFound = true;
      break;
    }
  }
  // if no match is found from allowed dimension with variance then set to default
  //dimensions.
  if (!matchFound) {
    width = variables.defaultDimension.w;
    height = variables.defaultDimension.h;
  }

  // read the accept header to determine if webP is supported.
  let accept = headers['accept'] ? headers['accept'][0].value : "";

  let url = [];
  // build the new uri to be forwarded upstream
  url.push(prefix);
  url.push(width + "x" + height);

  // check support for webp
  if (accept.includes(variables.webpExtension)) {
    url.push(variables.webpExtension);
  }
  else {
    url.push(extension);
  }
  url.push(imageName + "." + extension);

  fwdUri = url.join("/");

  // final modified url is of format /images/200x200/webp/image.jpg
  request.uri = fwdUri;
  callback(null, request);
};

CloudFront Functions に置き換えてみる

CloudFront Functions の制約は以下となります。

ref. CloudFront Functions の使い方を学ぶ - ユニファ開発者ブログ

  • 1 ms 以下で処理を完了させなければならない
  • リソース設定(CPU, メモリ)はできない
  • 環境変数は使えない
  • 使用可能言語が JavaScript 一択(機能制限もあり)
  • HTTP のボディ部分(リクエスト・レスポンスともに)には関与できない

今回は JavaScript から JavaScript への置き換えなので特に大きくは変更はなくできそうです。実際に変更するときに見ていくところは以下です。

  • 1 ms 以下で走り切るかを念頭に置く(=重いことは極力しない)
  • let, const は慎重に var に置き換える
  • 起動する関数名は hanlder() 固定
  • event.request 構造を return すると後続に継続する
置き換えてみた CloudFront Functions に登録する JavaScript コード
var allowedDimension = [
  { w: 100, h: 100 },
  { w: 200, h: 200 },
  { w: 300, h: 300 },
  { w: 400, h: 400 },
];
var defaultDimension = { w: 200, h: 200 };
var variancePercent = 20 / 100;
var webpExtension = 'webp';

function handler(event) {
  var request = event.request;
  var querystring = request.querystring;

  if (!querystring.d) {
    return request;
  }

  var dimensionMatch = querystring.d.value.split("x");
  var width = dimensionMatch[0];
  var height = dimensionMatch[1];

  var fwdUri = request.uri;
  var match = fwdUri.match(/(.*)\/(.*)\.(.*)/);

  var prefix = match[1];
  var imageName = match[2];
  var extension = match[3];
  var matchFound = false;

  for (var i in allowedDimension) {
    var dimension = allowedDimension[i];
    var minWidth = dimension.w - (dimension.w * variancePercent);
    var maxWidth = dimension.w + (dimension.w * variancePercent);
    if (width >= minWidth && width <= maxWidth) {
      width = dimension.w;
      height = dimension.h;
      matchFound = true;
      break;
    }
  }
  if (!matchFound) {
    width = defaultDimension.w;
    height = defaultDimension.h;
  }

  var url = [];
  url.push(prefix);
  url.push(width + "x" + height);

  var headers = request.headers;
  if (headers.accept && headers.accept.value.includes(webpExtension)) {
    url.push(webpExtension);
  } else {
    url.push(extension);
  }

  url.push(imageName + "." + extension);
  fwdUri = url.join("/");

  request.uri = fwdUri;

  return request;
}

実際に稼働させてみると、Compute Utilization (コンピューティング使用率)はおおよそ 40 以下で、最大の 100 にはまだまだ余裕があるような結果でした。

Compute Utilization とは
ref. https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/test-function.html#compute-utilization

まとめ

もし Lambda@Edge が稼働していて、それが CloudFront Functions に置き換えが可能なのであれば、メリットとデメリットを考慮しつつ、CloudFront Functions を使っていくのが良いかと思います。


ユニファでは一緒に働いてくれる仲間を募集中です! unifa-e.com