2018-12-26に更新

Lambda@Edgeで画像をリアルタイムにリサイズできる仕組みを作った

今作成中のサービスで画像をリアルタイム変換できる仕組みを AWS の Lambda@Edge を用いて実現してみたので紹介します。

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

Lambda@Edge とは

Lambda@Edge とは、一言で言うと CloudFront (CDN) の HTTP リクエスト・レスポンスを操作できる Lambda 関数です。

下記のタイミングで CloudFront リクエスト・レスポンスを変更することができます。
* CloudFront がビューワーからリクエストを受信した後 (ビューワーリクエスト)
* CloudFront がリクエストをオリジンサーバーに転送する前 (オリジンリクエスト)
* CloudFront がオリジンからレスポンスを受信した後 (オリジンレスポンス)
* CloudFront がビューワーにレスポンスを転送する前 (ビューワーレスポンス)

a.png

作った仕組み

CloudFront のオリジンサーバーに S3 を指定し、S3 からの画像レスポンスを Lambda@Edge で変換する仕組みを構築しました。

URLのパラメータを変えることでリサイズされた画像を取得できます。
例えば下記のように幅と高さを指定してリクエストするとそれぞれのサイズの画像が返却されるようにしています。

・https://cdn.example.com/images/user/1234/avatar/001.jpg?d=100x100 → 100x100の画像
・https://cdn.example.com/images/user/1234/avatar/001.jpg?d=300x300 → 300x300の画像
・https://cdn.example.com/images/user/1234/avatar/001.jpg?d=1000x1000 → 許可しないサイズはエラー

コード

いろいろ説明をはしょってコードだけ貼っときます。。

ビューワーリクエスト

'use strict';

const querystring = require('querystring');

const allowedDimensions = [ {w: 100, h: 100}, {w: 300, h: 300} ];

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

  // URIが特定の画像パスに一致するかチェック(特定の画像以外はクエリストリングを削除してリクエスト)
  const match = request.uri.match(/\/images\/user\/\d+\/avatar\/\d+\.jpg/);
  if (match == null) {
    request.querystring = '';
    callback(null, request);
    return;
  }

  // クエリストリングを key-value のペアにパース
  const params = querystring.parse(request.querystring);

  // dimensionパラメータがない場合、クエリストリングを削除してリクエスト
  if (!params.d) {
    request.querystring = '';
    callback(null, request);
    return;
  }

  // dimensionパラメータを'x'で分割
  const dimensionMatch = params.d.split('x');
  const width = dimensionMatch[0];
  const height = dimensionMatch[1];

  // dimensionパラメータが、許可リストに一致するかチェック
  let matchFound = false;
  for (let dimension of allowedDimensions) {
    if (width == dimension.w && height == dimension.h) {
      matchFound = true;
      break;
    }
  }
  // 許可リストにない場合、クエリストリングを削除してリクエスト
  if (!matchFound) {
    request.querystring = '';
    callback(null, request);
    return;
  }

  // クエリストリングにd={width}x{height}を設定してリクエスト
  request.querystring = querystring.stringify({ d: width + 'x' + height });
  callback(null, request);
};

オリジンレスポンス

'use strict';

const querystring = require('querystring');
const AWS = require('aws-sdk');
const S3 = new AWS.S3();
const Sharp = require('sharp');

const BUCKET = 'XXXXXXXX';

exports.handler = (event, context, callback) => {
  let response = event.Records[0].cf.response;

  if (response.status == 200) {
    const request = event.Records[0].cf.request;

    // クエリストリングを key-value のペアにパース
    const params = querystring.parse(request.querystring);

    // dimensionパラメータがない場合、そのままレスポンスを返す
    if (!params.d) {
      callback(null, response);
      return;
    }

    // S3キー用にuriの最初のスラッシュを除いた文字列を取得
    const key = request.uri.substring(1);

    // dimensionパラメータを'x'で分割
    const dimensionMatch = params.d.split("x");
    const width = parseInt(dimensionMatch[0], 10);
    const height = parseInt(dimensionMatch[1], 10);

    // S3から画像を読み込み
    S3.getObject({ Bucket: BUCKET, Key: key }).promise()
      // リサイズ
      .then(data => Sharp(data.Body)
        .resize(width, height)
        .toBuffer()
      )
      .then(buffer => {
        // レスポンスを生成
        response.status = 200;
        response.body = buffer.toString('base64');
        response.bodyEncoding = 'base64';
        response.headers['content-type'] = [{ key: 'Content-Type', value: 'image/jpeg' }];
        callback(null, response);
      })
      .catch( err => {
        callback(null, response);
      });
  } else {
    // 200以外のステータスコードの場合、そのままレスポンスを返す
    callback(null, response);
  }
};

なぜ作ったか

普通なら「非同期ジョブでリサイズ処理してS3に複数サイズ保存」とかで事足りるのではと思います。

今回は、
* アップロード時間を短くしたい(アップロード時はバリデートのみで変換処理しない)
* 画像をアップロードした直後の画面でリサイズされた画像を表示したい
* リサイズ画像をキャッシュしたい
* 画像サイズあとで変えたいかも

といった理由で、CDNを通してリアルタイム変換という選択肢を選びました。

感想

AWS Lambda 自体初めて触りましたが、サーバレスは便利だな〜と感じました。
ただ、Lambdaの開発やテストやデプロイをどうやるのが正解なのかよくわからず、追加開発があるような比較的規模の大きいプロジェクトだとLambdaは大変そう(?)と思いました。

今回は画像リサイズだけでしたが、透かしを入れたり合成したり、画像以外にも動的にキャッシュできるので、いろいろ使い所ありそうです。

早くサービスリリースできるよう頑張ります。。

ツイッターでシェア
みんなに共有、忘れないようにメモ

オクムラダイキ

ソフトウェアエンジニャー🐈

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント