[AWS×ServerlessFramework]AmazonAPIGateway経由でS3にJSONを保存する

# はじめに

みなさん。IoTやっていて、こんなこと思ったことありませんか？

「Lambdaでデータを整形して、DynamoDBに保存するなんて大層なことせんでもいいから、デバイスから送ったテキストデータを任意の名前でS3に保存できるだけで十分なのにな〜」

私はあります。(超個人的意見)
なんでそんなこと思ったのか？と言いますと、お客様からこんな要望が上がって来ました。

「デバイスとAWSの疎通をとりあえず、なる早で確認したい。ついでにテキストデータをどこかに保存して欲しい。」

こんなときにサクッとできないかな〜と思って、ServerlessFramework使って実装しました。
別にIoT以外でも利用できる場面はあると思います。
主な用途としては、 **APIGatewayにファイル(JSONなどのテキストファイル)をPOSTしてS3にPUTする** となります。

## アーキテクチャ

![スクリーンショット 2018-04-04 13.33.16.png](https://qiita-image-store.s3.amazonaws.com/0/95875/9a3deb21-24d4-a566-9f5d-3687305509a3.png)

フローとしては
1. Deviceから `APIGatewayのEP/send/{fileName}` にPOSTする
2. APIGatewayからLambdaをキックする
3. LambdaからPOSTのBodyをS3にkeyを{fileName}としてPUTする
みたいな感じです。

## ソースコード

ServerlessFrameworkのインストールやプロジェクトの作成はこの辺りの記事を参考にしてください。
[参考記事はこちら](https://qiita.com/is_ryo/items/613437b7a00f578424f2#serverless-frameworkのインストール)

`serverless.yml` はこんな感じ

```yaml:serverless.yml

service: apigateway-sample

# 基本設定
custom:
  defaultStage: dev
  defaultRegion: ap-northeast-1

# LambdaのruntimeやiamRoleなどの設定
# APIKeyを用意して、APIGatewayに適用します
# iamRole
#   S3 BucketName:serverless-apigateway-sample-bucketに対してGetObject/PutObjectを許可
provider:
  name: aws
  runtime: nodejs6.10
  stage: ${opt:stage, self:custom.defaultStage}
  region: ${opt:region, self:custom.defaultRegion}
  apiKeys:
    - ${self:custom.defaultStage}-send-apiKey
  usegePlan:
    quota:
      limit: 5000
      offset: 2
      period: MONTH
    throttle:
      burstLimit: 200
      rateLimit: 100
  iamRoleStatements:
    - Effect: "Allow"
      Resource: "arn:aws:s3:::serverless-apigateway-sample-bucket"
      Action:
        - "s3:GetObject"
        - "s3:PutObject"

# Lambdaのhandlerやevent(トリガー)の設定
# APIGatewayにAPIKeyを適用しないなら、private: falseに変更
functions:
  putS3FromApiGateway:
    handler: index.handler
    description: iot-serverless-sample LambdaFunction from APIGateway
    events:
      - http:
          path: send/{fileName}
          method: post
          cors: true
          private: true

# その他各種サービスリソースの設定
# 今回はS3のBucketを作成
# BucketNameは適宜書き換えてください
resources:
  Resources:
    S3Bucket:
      Type: "AWS::S3::Bucket"
      Properties:
        BucketName: BucketName
    BucketPolicy:
      Type: "AWS::S3::BucketPolicy"
      Properties:
        PolicyDocument:
          Id: "serverless-sample-bucket-policy"
          Version: "2012-10-17"
          Statement:
            - Sid: "AllowPutObjects"
              Effect: "Allow"
              Principal: "*"
              Action:
                - "s3:GetObject"
                - "s3:PutObject"
              Resource: "arn:aws:s3:::serverless-apigateway-sample-bucket/*"
        Bucket: "serverless-apigateway-sample-bucket"

```

Lambdaのソースコードはこんな感じ

```js:index.js
"use strict";

const AWS = require("aws-sdk");
const s3 = new AWS.S3();
const config = require("../config");

exports.handler = (event, context, callback) => {
  // POSTリクエストのBody(JSON)を取得
  const reqBody = JSON.parse(event["body"]);
  // S3にPUTする際のKey名(ファイル名)を取得
  const fileName = event["pathParameters"]["fileName"];
  // S3にPUTするパラメータを形成
  // serverless.ymlで指定したBucketNameに書き換えてください
  const param = {
    Bucket: BucketName,
    Key: fileName,
    Body: JSON.stringify(reqBody)
  };
  // S3にPutObject
  return s3.putObject(param).promise()
    .then(() => {
      console.log("All Done");
      callback(null, {
        statusCode: 200,
        headers:{ "Access-Control-Allow-Origin" : "*" },
        body: JSON.stringify({message: "SUCCEEDED!", res: ""})
      });
    }).catch((err) => {
      console.error(err);
      callback(null, {
        statusCode: 400,
        headers:{ "Access-Control-Allow-Origin" : "*" },
        body: JSON.stringify({message: "ERROR!", error: err})
      });
    });
};

```

## Deploy実行

作成した `serverless.yml` があるディレクトリで下記コマンドを実行します。

```console
$ sls deploy
・
・
・
・
Service Information
service: apigateway-sample
stage: dev
region: ap-northeast-1
stack: apigateway-sample-dev
api keys:
  dev-send-apiKey: APIKey  //APIKeyの文字列が表示されます
endpoints:
  POST - https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/send/{fileName}
functions:
  putS3FromApiGateway: apigateway-sample-dev-putS3FromApiGateway
```

成功すれば上記のようなメッセージが表示され、APIGatewayのエンドポイントやAPIKey、Lambda名などが表示されます。

## 実際にJSONをPOSTしてみる

ローカルのPCからJSONファイルをPOSTしてみようと思います
JSONファイルの中身はこんな感じ

```json:sample.json
{
  "deviceID": "sample0002",
  "record_time": "2018-03-13T00:00:00.000+0900",
  "val_1": "testtesttest",
  "val_2": "1234567890",
  "val_3": "q2w3e4r5t6y7u8i"
}
```

CURLでPOSTします。
`sample.json` を `sample_post.json` としてS3にPUTすることを想定しています。
※APIKeyを適用していないなら `-H "X-API-KEY: APIKey"` を削除してください。

```console
$ curl -X POST --data @sample.json \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: APIKey" \
  https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/send/sample_post.json

{"message":"SUCCEEDED!","res":""}
```

成功すれば、上記のようにメッセージが返ってきて、S3に保存されていると思います。

## さいごに

AWS非推奨ではありますが、APIKeyを利用して認証を行なっています。
本来であればAWS認証情報(Sig4)を生成してそれを元に認証を行うのですが、デバイスの中には認証情報を生成できるだけのリソースがないという場合が多々あります。
その場合にこのAPIKeyを利用してAWS側へリクエストを送信するという方法を取ることになります。
もう一度言っておきますが **この方法は非推奨ですのでご注意ください**

んじゃどうしたらいいの？というあなたに [**SORACOM**](https://soracom.jp) です。
SORACOMのサービスを利用すれば、このような問題は一気に解決されます。
SORACOM側にAWSの認証情報を持っておくことができるので、デバイスには一切AWSの認証情報を持つ必要が無くなります。
デバイスからSORACOMへリクエストを送信して、SORACOM側であらかじめ設定しておいた認証情報を元にAWS認証情報を生成し、AWSにリクエストを送信します。
認証情報を変えたければコンソールから変えれば良いだけです。
うん、めっちゃ楽。

なんかSORACOMの宣伝みたいになってしまいましたが、IoTデバイスからAWS等のクラウドにリクエストを送信する際には便利なツールですので、ぜひお使いください。

ではまた！