🧑‍💻

AWS Lambdaでサムネイル作成 (2020-2021ver.)

2021/02/27に公開

はじめに

ある動作をトリガーとして決まった処理を行う場合、AWS Lambdaを使うと便利です。
ここではよくあるパターンとして、S3に画像がアップロードされたときにその画像のサムネイルを自動的に作成する方法について解説します。

この手の記事は検索してみるとたくさんあるのですが、AWSのバージョンが違っていたり設定したい形が微妙に違っていたりと、やりたい形と合致するものが見つけられなかったので、2020年11月時点の方法として手順を残しておきます。

設定条件と設定の流れ

やりたいこと

  • S3にアップされた画像のでサムネイルをLambda自動生成したい
  • サムネイルはアップされた画像と同じバケット内にディレクトリ分けして保存したい

今回の設定では元画像をアップロードするS3バケットと、作成したサムネイルを保存するバケットは別のものにもできますが、今回は1つのバケットのみを使ってサムネイルも同じものに保存するかたちにします。
「同じバケット内にディレクトリ分けして保存したい」というのが今回のポイントです。

環境

  • Amazon S3
  • Amazon Lambda

設定の流れ

以下になります。

  1. S3バケットを作成
  2. lambdaの関数を作成
  3. S3のアクセス権限設定
  4. 関数の動作テスト
  5. 公開

1. S3バケットの作成

元画像をアップロードするS3バケットを作成します。既に作成済みの場合はスルーしてOKです。

AWSコンソールからS3を選び、右上の[バケットを作成]をクリックします。

任意のバケット名を入力して、リージョンを選択します。特にこだわりがなければ初期設定のまま東京リージョンでOKです。

ブロックパブリックアクセスのバケット設定は使用する状況によりますが、サムネイルを作成する場合は一般的に外部から参照する場合が多いので「パブリックアプセスをブロック」のチェックを外します。

そのままページ下までスクロールして「バケットを作成」をクリックして完了です。

2. lambdaの関数を作成

Lambda関数の初期設定

S3バケットを作成したら、それ以降の設定はLambdaの設定から行います。

Lambdaの設定画面を開き、右上の[関数の作成]をクリック。
Lambdaは1から関数を作ることもできますが、あらかじめテンプレートが用意されているのでそれを利用してみます。

[設計図の利用]を選択。

検索窓に“nodejs”と“S3”を追加して「s3-get-object」を表示、選択します。

関数名に任意の名前を入力します。識別できる好きな名前でOK。

実行ロールのところで、関数のアクセス権限を設定します。はじめて作成する場合は初期設定のまま「AWSポリシーテンプレートから新しいを作成」を選択。

ロール名は任意の名前を入力。ポリシーテンプレートはデフォルトのままでOK。

次の項目でトリガーとなるS3の設定を行います。

  • バケット:対象のS3バケットを選択
  • イベントタイプ:デフォルトのままでOK
  • プレフィックス-オプション:実行トリガーの対象とするファイルのプレフィックス条件を設定します。下記ではimages/を指定していますが、この場合はimagesディレクトリにアップロードされたファイルのみを対象にします。
  • サフィックス-オプション:対象のトリガーファイル拡張子をjpgのみにする場合などに指定します。(複数の拡張子を指定する場合は…だれかご存知でしたら教えてください。)

今回のように1つのバケットのみを使ってサムネイルも作成する場合は、必ずプレフィックス-オプションの指定でサムネイルの出力ディレクトリを分けてください。

入力のディレクトリとサムネイル出力するディレクトリを同じにすることは設定上可能ですが、自動作成されたサムネイルが再び入力トリガーとなり無限ループする可能性があるため、設定を誤るととんでもない請求額となる可能性があります。

Lambda関数のコードは後々変更しますがここでは編集出来ないので、そのままページ下部にスクロールして[関数の作成]をクリックして完了。

関数のカスタマイズ

作成した関数の設定画面が開いてきますので、ここで関数のコードをカスタマイズします。カスタマイズというか、実際は全変更します。笑

S3をトリガーにしたサムネイル作成のドキュメントは、実は公式のドキュメントがあります。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-s3-example.html
ただ、例によって微妙に設定が不足していたり、やりたいことが違ったりするので、参考にしつつ変更したものを作ります。

また上記の公式ドキュメントでサムネイル作成に「sharp」という外部モジュールを使っていますが、これがとても優れているので公式通りそのモジュールを使用して行います。

関数コードの変更は設定画面内の「関数コード」のところに記述します。ただ外部モジュールを使用しない場合はこのままここに記述していけばよいのですが、今回のように外部モジュールを利用する場合はローカルでモジュールと実行ソースを含めたデプロイパッケージを作成し、アップロードすることで指定します。(手順は後述)

ということで、一旦AWSコンソールから離れてローカルでの作業に移ります。

[ローカル作業] sharpライブラリをインストール

この辺りの手順は公式と同じです。
ローカルで適当な作業ディレクトリを作り、そこにsharpライブラリをnpmでインストールします。
macの場合は下記コマンドになります。

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

[ローカル作業] index.jsの作成

メインの実行プログラムであるindex.jsを作業ディレクトリ直下に作成します。
内容は下記になります。各ステップでの動作はコード内のコメントを参照してください。

// dependencies
const AWS = require('aws-sdk');
const util = require('util');
const sharp = require('sharp');

// get reference to S3 client
const s3 = new AWS.S3();

exports.handler = async (event, context, callback) => {

    // 対象ファイルのキー(=ファイル名)とバケット名を定義
    const srcBucket = event.Records[0].s3.bucket.name;
    const srcKey    = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " "));

    // 出力先のバケット名。今回は同じバケットを使用するためsrcBucketと同じ
    const dstBucket = srcBucket;

    // サムネイルのサイズ設定 600×600のサイズに収まる最大値でサムネイルが作成されます
    const thumbSize = [600, 600];

    // 拡張子を取得
    const typeMatch = srcKey.match(/\.([^.]*)$/);
    if (!typeMatch) {
        console.log("Could not determine the image type.");
        return;
    }

    // 対象とする拡張子かどうかチェック
    const imageType = typeMatch[1].toLowerCase();
    if (imageType != "jpg" && imageType != "jpeg" && imageType != "png") {
        console.log(`Unsupported image type: ${imageType}`);
        return;
    }
    // サムネイルの書き出しファイル名を作成(thumbnailディレクトリに同名ファイル名で作成)
    const thumbnailKey = srcKey.replace(/^(images\/)(.*?)\.([a-zA-Z]{3,4})$/, "thumbnail/$2.$3")

    // S3から対象ファイルを取得
    try {
        const params = {
            Bucket: srcBucket,
            Key: srcKey
        };
        var origimage = await s3.getObject(params).promise();

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

    // sharpでサムネイル画像を作成
    try { 
        var buffer = await sharp(origimage.Body)
        .resize(thumbSize[0], thumbSize[1],{
            fit: 'inside',
            withoutEnlargement: true,
        })
        .rotate()
        .toBuffer();
            
    } catch (error) {
        console.log(error);
        return;
    } 

    // 作成したサムネイル画像をS3にアップロード
    try {
        const destparams = {
            Bucket: dstBucket,
            Key: thumbnailKey,
            Body: buffer,
            ContentType: "image",
            ACL: 'public-read', // 公開から読み取りの権限付与
        };

        const putResult = await s3.putObject(destparams).promise(); 
        
    } catch (error) {
        console.log(error);
        return;
    } 
    
    // 完了メッセージを出力
    console.log('Successfully resized ' + srcBucket + '/' + srcKey +
        ' and uploaded to ' + dstBucket + '/' + thumbnailKey); 
};

ここまで作成したら、作成したindex.jsとnode_modulesをまとめてzip圧縮して、AWSにアップロードします。

アップロードはデプロイパッケージを一式zip圧縮して、管理画面「関数コード」左上の[アクション]から[.zipファイルをアップロード]からアップロードします。

3. S3のアクセス権限設定

初期状態だと作成したサムネイル画像をS3に保存するためのアクセス権限が無いため、S3の追加設定を行います。

AWSコンソールの[S3] → 一覧から対象のバケット名をクリック

バケットの詳細画面で [アクセス許可]タブをクリック

スクロールして「バケットポリシー」の[編集する]をクリック

ポリシーに下記JSONコードを記載します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AddPerm",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:PutObject", # アップロード権限
                "s3:GetObject", # ダウンロード権限
                "s3:PutObjectAcl", # ファイル毎のアクセス権設定の権限を付与
            ],
            "Resource": "arn:aws:s3:::mynewbucket02/*" # 対象のバケットを指定
        }
    ]
}

内容ですが、Effectのところで許可(Allow)を指定し、Actionのところで許可する権限を与えているかたちになります。

  • "s3:PutObject" : アップロード権限
  • "s3:GetObject" : ダウンロード権限
  • "s3:PutObjectAcl" : ファイル毎のアクセス権設定の権限を付与

またResourceのところで対象のバケットを指定します。(でもこのバケットの設定のところに書いているのだから、何故指定が必要なのでしょうか。。)

入力完了したら保存して権限設定は完了です。

4. 関数の動作テスト

ここまでできればようやく動かすことができます!
が、そのまま実戦投入するともし不具合が発生していても気付きませんので、テストを行います。
Lambda関数は実際に実行する前に実行テストを行うことができます。

テストはテスト用画像をあらかじめS3にアップロードしておき、テストコード内でその画像を指定することで「アップロードされた」とみなしてその後の動作を実行できます。

まずAWSのS3の設定画面から、imagesディレクトリ内にtest.jpgの画像をアップロードします。

次にLambdaの設定画面に戻り、右上の[テスト]ボタンの横から[テストイベントの設定]をクリックします。

[新しいテストイベントの作成]を選択し、イベントテンプレートで[s3-put]を選択します。

そうするとほぼ出来上がった状態のサンプルコードが表示されるので、それを一部修正することでテストコードが書けます。
下記のような感じになります。

{
  "Records": [
    {
      …(略)…
      "s3": {
        "s3SchemaVersion": "1.0",
        "configurationId": "testConfigRule",
        "bucket": {
          "name": "mynewbucket02", ←バケット名
          "ownerIdentity": {
            "principalId": "EXAMPLE"
          },
          "arn": "arn:aws:s3:::mynewbucket02" ←バケット名
        },
        "object": {
          "key": "images/test.jpg", ←テスト画像
          "size": 1024,
          "eTag": "0123456789abcdef0123456789abcdef",
          "sequencer": "0A1B2C3D4E5F678901"
        }
      }
    }
  ]
}

入力完了したら保存して、[テスト]ボタンを押すと実行できます。

下記のように実行結果OKとなればテスト完了です。…が、おそらくこのようにならない可能性が高いかと思います。

エラー対応

もし下記のようにエラーとなった場合は、処理の重さとLambdaの処理能力が関係しているかもしれません。

Lambda設定画面を下の方にスクロールしていくと下記の「基本設定」画面があり、ここでスペックを指定できます。
初期状態だとメモリ128MB、タイムアウト3秒となっているので、128MBのメモリスペックで3秒以内に完了しない処理はすべてエラーとなってしまいます。

今回のサムネイル作成のような画像処理系はほぼ間違いなくスペック不足、時間不足でエラーとなってしまうのでこの設定を調整します。右上の[編集]ボタンから変更します。

メモリを4倍の512MB、タイムアウト10秒くらいにすればいけると思います。
なおメモリを上げることでLambdaの利用単価が上がるので金額的な不安がありますが、スペックを上げることで実行時間が短縮されることから、利用料金は利用単価×実行時間で計算されるためコストは実質上がらない場合が多いです。
このあたりは細かく検証されている以下のブログが参考になります。
https://recipe.kc-cloud.jp/archives/10437

5. 公開

ここまでできればいよいよ本番動作確認です!
本番動作を有効にするには[トリガーの有効化]を行います。

トリガーの有効化

Lambda関数の設定画面上部「デザイナー」のところで[S3]をクリックします。
すぐ下の項目にS3の設定状況が表示されるので、ここでS3にチェックを入れて、右上の[Enable]をクリックで有効化できます。

動作確認

では実際にS3に画像をアップロードして実行させてみましょう。
テストのときと同様に、S3設定画面からimagesディレクトリに適当なファイルをアップロードさせて、thumbnailディレクトリにサムネイルができていれば完了です!
おつかれさまでした。

おわりに

Lambdaの作成はそれなりに手順が多いですが、一度作成してしまえばソースの使い回しも可能で、わりと簡単に設定することができます。

Lambdaにサムネイル作成を振ってしまうことでEC2でのプログラムによるサムネイル作成を行う必要が無くなり、役割の明確化という意味に加えてEC2のリソース負荷を大幅に下げることができますので負荷対策にもなります。
またLambdaはかなりの無料枠があるため、費用削減にも繋がりますのでオススメです。

Discussion