Serverless FrameworkでS3 Triggerをローカルでエミュレートする

## TL;DR
- `serverless-s3-local`というプラグインを使おう
- cliしか使えないのでGUIで操作できないのがちょっとキツイかも
- Serverlessの公式ドキュメントは優しい英語で書かれているので読みやすくて助かる

## 動機
Serverless Frameworkを触っていたらS3 Triggerをローカルで実行したくなりました。
今まではローカルではhttp リクエストを投げることで挙動を確認していましたが、
Handlerに投げられるEventがhttpリクエストとは異なるのでs3 trigger用のmockを用意して
動作確認していましたが、なんか。。。うーん。。。って感じだったので重い腰をあげて調べてみました。

## 前提事項
- serverless frameworkのインストール
- aws-cliのインストール

## 実装
### sls create & プラグインインストール
自分はaws-nodejs-typescriptを使います。
動作確認してはいませんが、他のランタイムを選択してもできると思います。
```
sls create --template aws-nodejs-typescript --path ./sample --name sample 
```

あとは、ローカルでAPI Gatewayをエミュレートする`serverless-offline`とS3をローカルでエミュレートする`serverless-s3-local`をインストールします。

```
npm install -D serverless-offline
npm install -D serverless-s3-local
```

### serverless.tsにリソース情報を変更

`serverless.ts`にpluginを追記しましょう

```serverless.ts
const serverlessConfiguration: AWS = {
  service: 'sample',
  frameworkVersion: '3',
  plugins: ['serverless-esbuild', 'serverless-offline', 'serverless-s3-local'],
  provider: {
    name: 'aws',
    runtime: 'nodejs18.x',
    apiGateway: {
      minimumCompressionSize: 1024,
      shouldStartNameWithService: true,
    },
    environment: {
      AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
      NODE_OPTIONS: '--enable-source-maps --stack-trace-limit=1000',

    ...
```

あとは、`serverless-s3-local`の設定を`custom`に記述します。
記述する内容は以下の公式ドキュメントを参照してください。
https://www.serverless.com/plugins/serverless-s3-local

```serverless.ts
custom: {
    esbuild: {
      ...
    },
    s3: {
      host: 'localhost',
      port: 4569,
      directory: './s3'
    }
  },

```

`serverless.ts`のfunctionsにLambdaの情報を記述します。
今回はめちゃくちゃ単純にconsoleにeventをconsoleに出力するだけの簡単なものにします。

```serverless.ts
...
  functions: {
    trigger: {
      name: 'trigger',
      handler: `${handlerPath(__dirname)}/trigger.trigger`,
      events: [
        {
          s3: {
            bucket: 'trigger_bucket',
            event: 's3:*'
          }   
        }
      ]
    },
  }
...
```

resourcesにS3の情報を書きます。

```serverless.ts
resources: {
    Resources: {
      triggerBucket: {
        Type: 'AWS::S3::BUCKET',
        Properties: {
          BucketName: 'trigger',
        }
      },
    }
}
```

これでリソースの構成の記述がおわりました。
trigger handlerにLambdaの挙動を記述しておきましょう。

```trigger.ts
export const trigger: Handler = async(event) => {
  console.log(event)
  formatResponse({}, 200)
}
```


最後にpackage.jsonのscriptsにコマンド込めましょう。

```package.json
 "dev": "IS_OFFLINE=true sls offline start --reloadHandler"
```

```
npm run dev
```

これでs3とAPI Gatewayをローカルで起動できました。

## aws-cliを使う際のprofileの設定

ここから動作確認でs3にオブジェクトをaws-cliを使って格納します。
その際に認証情報が必要なのでそれをローカルに設定します。

先ほどのcustomに明示的に設定していなければデフォルトではアクセスキー、シークレットキーどちらも`S3RVER`です。
このキーを使ってaws-cliを実行できるようにprofileに設定しましょう。

```~/.aws/config
[profile s3local]
aws_access_key_id = S3RVER
aws_secret_access_key = S3RVER
```

これでaws-cliを使う際に`--profile s3local`とオプションを指定することでこちらの認証情報をつかって実行できます。

## 動作確認

trigger bucketにオブジェクトを作成してコンソールにeventが出力されるか確認しましょう。
適当なファイルを作成しましょう。

```sample.json
{
    "foo": "bar"
}
```

これをbucketにcpコマンドを使って格納します。
```
aws s3 cp sample.json s3://trigger/sample.json --endpoint-url http://localhost:4569 --profile s3local
```

このコマンドを実行すると`npm run dev`したコンソールに以下のようなS3 Triggerのeventが出力できているはずです。

```json
[
    {
      "eventVersion": "2.0",
      "eventSource": "aws:s3",
      "awsRegion": "us-east-1",
      "eventTime": "",
      "eventName": "ObjectCreated:Put",
      "userIdentity": { "principalId": "" },
      "requestParameters": { "sourceIPAddress": "::1" },
      "responseElements": {
        "x-amz-request-id": "",
        "x-amz-id-2": ""
      },
      "s3": {
        "s3SchemaVersion": "1.0",
        "configurationId": "testConfigId",
        "bucket": {
          "name": "sample",
          "ownerIdentity": { "principalId": "" },
          "arn": "arn:aws:s3: : :sample"
        },
        "object": {
          "key": "sample.json",
          "sequencer": "",
          "size": 15,
          "eTag": ""
        }
      }
    }
  ]
```

eventが出力できました。
エラーが出る場合はs3バケットがちゃんと作成されているかなどを確認してください。

## 最後に
- コマンドライン操作しかできなくて大変だなぁ
- minioのほうがGUIで操作できて楽だけどtriggerできないからなぁ
- 使い分けが大事そうだなぁ