AWS API Gateway + Lambda + S3 でバイナリデータを保存する

Qiitaにて他の方も書いている内容になります

今回これを紹介するにあたり画像加工を行うマイクロサービスを作りました
今回はそのサービスの一部のHTTP通信でS3にリソースファイルをアップロードする部分をピックアップしています

## 作製したマイクロサービス

1. HTTP通信でS3（INPUT用バケット）にアップロード
※直接S3にアップロードしても可

1. S3（INPUT）にアップロードされるとトリガーで画像加工Lambdaが起動
※GM on ImageMagick
アップロードするファイルは以下の３つ
 * 画像リソース（複数）
 * 加工処理レシピファイル
 * コールバックファイル（任意）

1. 画像加工処理
 * リサイズ
 * 画像合成
 * テキスト合成
 * 加工後ファイルに画像合成して更に加工する
 * ファイルフォーマット変更

1. 完了後にS3（OUTPUT）に保存
※ホスティングを有効にしています

1. コールバックファイルがある場合は、コールバック先へ加工後のリソースのURLとコールバックファイル内に記載されたレスポンスデータを返します


## 環境

`ServerlessFramework 1.24.1`
`Node.js 6.10`

## 実装

### コード

````yml:serverless.yml
# Welcome to Serverless!

service: serverless-imagemanager

# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
frameworkVersion: "=1.24.1"

# 設定定義
custom:
  defaultStage: dev
  # AWSの接続先（aws_credentials）
  profiles:
    dev: default
    prod: default

# AWSに反映する設定定義
provider:
  name: aws
  runtime: nodejs6.10
  region: ap-northeast-1
  stage: ${opt:stage, self:custom.defaultStage}
  profile: ${self:custom.profiles.${self:provider.stage}}
  memorySize: 1024
  timeout: 12
  deploymentBucket: deploy.${self:provider.stage}.${self:service}
  environment:
    suffix: ${self:provider.stage}
    service_name: ${self:service}

  # Lambda function's IAM Role
  # https://docs.aws.amazon.com/ja_jp/general/latest/gr/aws-arns-and-namespaces.html
  iamRoleStatements:
    - Effect: 'Allow'
      Action:
        # Gives permission to Lambda in a specific region
        - lambda:InvokeFunction
        # Gives permission to S3 bucket in a specific
        - s3:*
      Resource:
        - 'arn:aws:s3:::${self:service}.input.dev/*'
        - 'arn:aws:s3:::${self:service}.input.prod/*'
        - 'arn:aws:s3:::${self:service}.output.dev/*'
        - 'arn:aws:s3:::${self:service}.output.prod/*'
        - '*' # Lambda

# you can add packaging information here
package:
  exclude:
    - .DS_Store
    - .git/**
    - .serverless/**
    - .npmignore
    - .gitignore

# APIリスト
# https://serverless.com/framework/docs/providers/aws/guide/events/
# https://serverless.com/framework/docs/providers/aws/guide/serverless.yml/
functions:
  # HTTPリクエストでS3にアップロード.
  request_s3_upload_input:
    handler: src/request_s3_upload_input.handler
    memorySize: 256
    events:
      - http:
          path: imagemanager/uploads
          method: post

# CloudFormation resource templates
# http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html
resources:
  Resources:
    # S3
    input:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:service}.input.${self:provider.stage}
      #DeletionPolicy : Retain
    output:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:service}.output.${self:provider.stage}
      #DeletionPolicy : Retain
````

````js:src/request_s3_upload_input.handler
'use strict';

const Aws = require('../libs/aws.js');
const Logger = require('../libs/logger.js');

const Resource = require('../model/resource.js');

/**
 * HTTPプロトコルからアップロード.
 **/
module.exports.handler = (event, context, callback) => {
  Logger.info("start request_s3_upload_input.");

  s3upload(event, context)
    .then((result) => {
      callback(null, {
        statusCode: 200,
        body: JSON.stringify(result)
      });
    })
    .catch((error) => {
      Logger.error(error);
      callback(null, {
        statusCode: 500,
        body: JSON.stringify(error)
      });
    });
};

function s3upload(event, context) {
  return new Promise((resolve, reject) => {
    Logger.info(event);

    // コンテントタイプの文字列が固定されていなかったので網羅.
    let contentType = event.headers["Content-Type"];
    if (!contentType) {
      contentType = event.headers["Content-type"];
    }
    if (!contentType) {
      contentType = event.headers["content-type"];
    }
    if (!contentType) {
      contentType = event.headers["content-Type"];
    }

    const queryParams = event.queryStringParameters;
    const key = queryParams.key;

    const resource = new Resource();

    Logger.debug("contentType: " + contentType);
    Logger.debug(queryParams);

    let body;
    switch (contentType) {
      case "image/jpeg":
      case "image/gif":
      case "image/png":
        // リクエストボディに設定された画像データはBase64エンコードされているので、デコードする
        body = Buffer.from(event.body, 'base64');
        break;
      default:
        body = event.body;
    }

    const params = {
      Bucket: resource.getBucketName(),
      Key   : key,
      Body  : body,
      ContentType: contentType
    };
    Logger.debug(params);
    resource.savePromiseForS3(params).then(resolve());
  });
}
````

````js:model/resource.js
'use strict';

const Aws = require('../libs/aws.js');
const Logger = require('../libs/logger.js');

module.exports = class Resource {
  getBucketName() {
    return process.env.service_name + ".input." + process.env.suffix;
  }
  promiseForTakeOverData(object) {
    return new Promise(function(resolve, reject) {
      resolve(object);
    });
  }

  // S3 リソースを参照.
  loadPromiseForS3(bucketName, key) {
    const s3 = Aws.s3();

    return new Promise(function(resolve, reject) {
      // S3から読み込み
      s3.getObject({Bucket: bucketName, Key: key}, function(err, data) {
        if (err) {
          Logger.error("Error: loadPromiseForS3." + err.toString());
          reject(err);
        } else {
          Logger.info("Success: loadPromiseForS3 " + key);
          resolve(data.Body);
        }
      });
    });
  }

  // S3 保存.
  savePromiseForS3(params) {
    const s3 = Aws.s3();

    return new Promise(function(resolve, reject) {
      // S3に書き込み
      s3.putObject(params, function(err) {
        if (err) {
          Logger.error("Error: savePromiseForS3." + err.toString());
          reject(err);
        } else {
          Logger.info("Success: savePromiseForS3 " + JSON.stringify(params, null, 2));
          resolve();
        }
      });
    });
  }
}
````

## 使い方

デプロイすることでAPIGatewayにリクエストを受けつけるURLが作られます

+ メソッド: POST
+ Body部: Base64エンコードしたバイナリソース
+ URLクエリパラメータ: 『?key=<<保存ファイル名>>』

## 注意点

APIGatewayの設定のバイナリメディアタイプ
![03.png](https://qiita-image-store.s3.amazonaws.com/0/266407/9cf4f352-4f51-7ac7-d8e9-b0f88ba44f1c.png)
*※設定を反映するには再度デプロイが必要になります*