CodlessCode
AWS

Lambda を使ってS3に画像を保存するAPIを実装(API Gatewayも使用)

先日、AWS Lambdaを使ってS3に画像を保存するAPIを実装したので、流れを簡単にまとめてみました。
LambdaやS3のほかにAPI Gatewayも使用しているので、どうやって組み合わせていくかなども、参考になれば幸いです。

 

この記事はこんな悩みを持つ方を対象にしています。

  • Lambdaを使う具体的な手順がイメージできない
  • 手っ取り早くLambdaを使ったAPIを実装してみたい
  • サーバーレスアプリケーション>の雰囲気をつかみたい

今回作成するものは、
Lambdaを使ってS3に画像を保存するAPI
です。API Gatewayも使用しています。
この記事を読むことで

Lambdaのシンプルな使い方が具体的に理解できる。
「サーバーレスで構築する」ことをなんとなく理解できる。
LambdaとS3、API Gatewayを組み合わせる方法がわかる。

このようになっていただけたらなと思います。

Lambdaを使ってS3に画像を保存するAPI完成形

システム構成図

System Configuration

概要

APIについて
  • Lambda、API Gateway、S3バケットを使用した、サーバーレスな画像保存API
  • POSTされた1枚の画像を、適度にリサイズしてPNG形式にフォーマットし、S3バケットの任意の場所に保存
  • コードはNode.js
その他
  • AWSアカウントを持っていることが前提です
  • 手っ取り早くやるために、アクセス制限などの細かい手順をスキップしてます
  • リクエストのバリデーションについて、今回は説明していません
動作確認済の環境
  • npm v7.20.2(Node.js v15.6)

npm v6.14.15以上(Node.js v14.17.6以上)であると良いです。
画像をリサイズするためには外部ライブラリが必要なので、それをインストールするときに使用します。リサイズ処理をしない場合は不要です。

S3バケットを準備【ステップ1】

最初に、必要なものを作ります。

S3バケット作成

  1. S3コンソール を開く
  2. バケットを作成を選択
  3. 任意のバケット名を入力(例:my-images-20210912)
  4. リージョンを選択(例:アジアパシフィック(東京)ap-northeast-1)
  5. 「パブリックアクセスをすべてブロック」のチェックを外し、バケットを作成を実行
  6. 作成したバケットをクリックして表示
  7. アクセス許可タブを開いてバケットポリシーに以下を入力して保存
    {
         "Version": "2012-10-17",
         "Id": "AccessAllowPolicy",
         "Statement": [
           {
             "Sid": "Stmt1",
             "Effect": "Allow",
             "Principal": "*",
             "Action": "s3:*",
             "Resource": "S3バケットのARN"
           }
         ]
    }

    ※環境にあわせてResourceを修正してください※
    ※世界中の誰もがアクセスできるようになるため、用が済んだら適宜変更※

Lambdaで処理を記述【ステップ2】

Lambdaレイヤ作成(オプション)

次に、外部ライブラリをインストールします。
こちらはオプションなので、リサイズ処理しない場合はスキップしてください。

ライブラリのインストール

  1. 適当な場所にnodejsというフォルダを作成し、そのフォルダ内でコマンドプロンプト(ターミナル)を起動
  2. npm install –arch=x64 –platform=linux sharpを実行
    ディレクトリ構成は以下のようになります。
    nodejs/
      ├ package.json
      ├ package-lock.json
      └ node_modules/
          ├ sharp/
          ├ ...省略

    node_modulesがあればOKです。
    sharpまでのパスがnodejs/node_modules/sharpになっていることが重要です ※詳細

  3. nodejsフォルダをnodejs.zipに圧縮

レイヤ作成

Create Lambda layer
  1. Lambdaコンソールでレイヤーページを開く
  2. レイヤーの作成
  3. レイヤー設定で任意の名前説明を入力
  4. .zip ファイルをアップロードをチェックし、先ほど作成したnodejs.zipをアップロード
  5. 互換性のあるランタイムでNode.js 14.xを選択
  6. 作成を実行

(私は画像のように作成しました。)

Lambda関数の作成

それでは、今回のメインを作成します。

Create Lambda function
  1. Lambdaコンソールで関数ページを開く
  2. 関数の作成を選択
  3. 一から作成を選択
  4. 任意の関数名を入力(例:saveResizeImage)
  5. ランタイムでNode.js 14.xを選択
  6. 関数の作成を実行

index.jsの修正

次に、index.jsを以下のように修正し、保存してDeployします。
必要であれば、ライブラリインポートとリサイズ処理の部分を消してください。
※リクエストのバリデーションは今回の題材から除外しているので手動でやってます。気持ち悪い方は調べてみてください!

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

// リサイズ用のライブラリインポート
const sharp = require('sharp');


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

    let bucketName = null;
    let filepath   = null;
    let data       = null;

    if (event.pathParameters && "bucket" in event.pathParameters) {
        bucketName = event.pathParameters.bucket;
    }

    if (event.queryStringParameters && "key" in event.queryStringParameters) {
        filepath = event.queryStringParameters.key;
    }

    if ("body" in event) {
        data = event.body;
    }

    if (!bucketName || !filepath || !data) {
        return response(400, false, { message: "すべて必須です", event });
    }

    if (!event.isBase64Encoded) {
        return response(400, false, { message: "不正なデータです", event });
    }

    const resized = filepath.split('.')[0] + '_re.png';

    // -*- ここからリサイズ処理 -*-
    try {
        data = await sharp(Buffer.from(data, 'base64'))
            .resize(256, 256, { fit: 'outside' })
            .toFormat('png', { quality: 60 })
            .toBuffer();

    } catch (error) {

        return response(500, false, { message: "リサイズに失敗しました。", error });
    }
    // -*- ここまでリサイズ処理 -*-

    var statusCode = 500;
    var success    = false;

    var result = await S3.putObject({
            Body: data,
            Bucket: bucketName,
            ContentType: 'image/png',
            Key: resized,
        }).promise()
        .then((_res) => {
            statusCode = 200;
            success = true;
            return _res;

        }).catch((error) => {
            console.error("Error!", error);
            return error;
        });

    return response(statusCode, success, { result });
};


function response(statusCode, success, params) {
    return {
        statusCode,
        body: JSON.stringify({
            success,
            ...params,
        }),
    };
}

設定の調整

Lambda関数を実行し終えるまで3秒以上かかる場合、APIが失敗してしまうので、デフォルトのタイムアウトを変更しておきます。

  1. 先ほど作成したLambda関数のページで、設定タブを開く
  2. 一般設定の編集を実行し、タイムアウト10秒に変更して保存

レイヤの追加(オプション)

Lambdaレイヤを作成していない場合は不要です。

  1. 先ほど作成したLambda関数のページで、コードタブを開く
  2. ページ下部のレイヤーからレイヤーの追加を実行
  3. カスタムレイヤーをチェックし、先ほど作成したレイヤを選択
  4. 最新のバージョンを選択し、追加を実行
 

API Gatewayでエンドポイント作成【ステップ3】

次に、エンドポイントPOST /{bucket}を作成します。
分からない方は以下を参考にしていただければと思います!

API GatewayでAPIを作成

  1. API Gatewayコンソールを開く
  2. 左のサイドメニューでAPIを選択し、APIの作成を実行
  3. REST APIを構築
  4. REST新しいAPIにチェック
  5. 任意のAPI名を入力(例:SaveResizeImage)
  6. エンドポイントタイプでリージョンを選択し、APIの作成実行
  7. 作成後のページで、リソースから「/」をクリックしてアクティブにする
  8. アクションプルダウンからリソースの作成を選択
  9. 任意でリソース名を入力(例:Bucket)
  10. リソースパスに{bucket}と入力しリソースの作成を実行 ※重要※

作成すると以下のようになります。

Create API resources

メソッドを定義

  1. 先ほど作成したリソースの「/{bucket}」をクリックしてアクティブにする
  2. アクションプルダウンからメソッドの作成を実行
  3. プルダウンからPOSTを選択し、✅をクリック
  4. 統合タイプはLambda関数を選択
  5. Lambda プロキシ統合の使用にチェック
  6. Lambdaリージョンは先ほど作成したLambda関数と同じものを選択(例:ap-northeast-1)
  7. Lambda関数に、先ほど作成したLambda関数名を入力(例:saveResizeImage)
  8. デフォルトタイムアウトの使用のまま、保存を実行
  9. Lambda 関数に権限を追加するのポップアップでOKを選択

リクエストパラメータの設定

このAPIに必要なリクエストは以下の3つです。
API Gatewayでこれらを受け取り、Lambdaに渡すための設定していきます。

パラメータ名種類意味
bucketパスパラメータS3バケット名
keyクエリパラメータS3バケットのルートからのパス
(body)Bodyパラメータバイナリ(画像)

パスパラメータ(確認)

  1. メソッド定義後の画面で、リソースから/{bucket}のPOSTを選択
  2. メソッドリクエストを選択
  3. リクエストパスをクリックして開き、bucketがあることを確認

クエリパラメータ

  1. 前項のパスパラメータと同じページでURLクエリ文字列パラメータをクリックして開く
  2. クエリ文字列の追加をクリックし、keyと入力して✅する

ボディパラメータ

画像のようなバイナリデータをbase64エンコードしてLambdaに渡すための設定です。
バイナリではなく文字列として扱った方が安全且つ早く処理できるため、よく使われます。

  1. 前項のクエリパラメータと同じページでHTTPリクエストヘッダーをクリックして開く
  2. ヘッダーの追加をクリックし、Content-Typeと入力して✅する
  3. 左のサイドメニューから「API:任意のAPI名」配下の設定を選択
  4. 下の方のバイナリメディアタイプで、バイナリメディアタイプの追加をクリックし、*/*と入力して変更の保存を実行 ※重要※

バイナリメディアタイプの補足

API GatewayでLambdaプロキシ統合を利用すると、バイナリデータは基本的にbase64エンコードされるようになっています。
*/*とは、どんなContent-Typeであってもボディパラメータはバイナリデータとして扱うという意味です。

つまり、JSONパラメータであろうと、Lambdaにはbase64エンコードされた状態で渡されます。
PNG画像だけ扱いたいよって場合は「image/png」だけ入力すればOKですね。

APIのデプロイ

最後に、APIをデプロイして完成させます。

  1. リクエストパラメータ設定後、左のサイドメニューのリソースを開く
  2. アクションプルダウンからAPIのデプロイを選択
  3. 新しいステージを選択し、任意のステージ名を入力(例:v1)
  4. デプロイを実行
  5. デプロイ後の画面でURLの呼び出しに記載されたURLをコピーする

APIを実行

それでは、Postmanを使って実際に動かしてみます。(クライアント側はお好みです!)
先ほどコピーしたURLをリクエストURLに入力し、環境に合わせてbucketとクエリパラメータkeyを指定します。
そして、メソッドをPOSTにして、Sendします。
(例:bucket=my-images-20210912、key=sample/resize_v1.png)
以下のようにsuccess=trueであれば成功です。

Exec API

S3バケットの任意の場所に画像が保存されているか確認してみてください。
保存された画像は、ファイル名の末尾が「_re.png」になっています。
(例:sampleフォルダにresize_v1_re.pngという画像が保存されています)

また、リサイズ処理をいれた場合は、送信前より大きさとサイズが小さくなっているかと思います。

おわりに

お疲れ様でした!
この記事がLambdaと仲良くなるきっかけになってくれたら嬉しいです。
最後までお読みいただきありがとうございました✨

すべてが完了したら

S3バケットのアクセス許可を適宜修正してください

たとえば、私はCloudFront経由でS3にアクセスすることが多いので、以下のようなポリシーを設定していたりします。

バケットポリシーのサンプル

// ブロックパブリックアクセス (バケット設定)をすべてONに設定した上で

{
    "Version": "2012-10-17",
    "Id": "PolicyName",
    "Statement": [
        {
            "Sid": "Stmt1631808064466",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::アカウントID:user/ユーザー名"
            },
            "Action": "s3:*",
            "Resource": "arn:aws:s3:::S3バケット名/*"
        },
        {
            "Sid": "2",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFrontのOAI名"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::S3バケット名/*"
        },
        {
            ... その他のポリシー
        }
    ]
}

この辺りはこちらで紹介しています!

不要なリソースは削除しておきましょう

AWSは従量課金なので使わなければ大丈夫ですが、忘れてしまわないように一応…。

以上です。