【Serverless Framework】S3アップロードしたらLambda実行できるようにする。

# この記事のゴール
・Serverless Frameworkの基本的な使い方を学ぶ。
・ESLint × PrettierでTypeScriptのソースコードを自動整形できるようにする。
・S3アップロードイベントで実行されるLambda関数を作成してみる。
　　→ 実用性がありそう＆応用が効きそうなケース

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653955/4885ce4b-1d1e-a113-691f-91daed51734d.png)

# もくじ
o1. Serverless Frameworkプロジェクト基盤作成
o2. S3アップロード時にLambdaが発火するようにする。
o3. CSVファイル読込
o4. mysqlインポート

# ソースコード
https://github.com/daisuketakakuwa/serverless-practice

# o1. Serverless Frameworkプロジェクト基盤作成

## プロジェクトフォルダ作成＆`npm init`でpackage.json作成
```
mkdir serverless-framework
cd serverless-framework
npm init
```

## serverless CLIインストール
```
npm install -g serverless
```

## 必要なライブラリ追加
```
// TypeScript
npm install --save-dev typescript

// Serverless関連
npm install --save-dev serverless-offline
npm install --save-dev serverless-offline-lambda
npm install --save-dev serverless-s3-local
npm install --save-dev serverless-plugin-typescript

// AWS関連
npm install aws-lambda
npm install --save-dev @types/aws-lambda
npm install aws-sdk
npm install --save-dev @types/aws-sdk

// CSVパース関連
npm install async
npm install --save-dev @types/aws-sdk
npm install csv-parse
npm install iconv-lite

// DB関連
npm install promise-mysql
```

## TypeScriptコンパイル設定(tsconfig.json追加)
TypeScriptコンパイル用の設定ファイル(tsconfig.json)をルートディレクトリに追加する。tsconfig.jsonの詳細は[こちら](https://qiita.com/crml1206/items/8fbfbecc0b40968bfc42)。
```json
{
    "compilerOptions": {
      "target": "es2020",
      "module": "commonjs",
      "lib": ["es2020"],
      "sourceMap": true,
      "outDir": "./dist",
      "rootDir": "./src",
      "strict": true,
      "moduleResolution": "node",
      "baseUrl": "src",
      "esModuleInterop": true,
      "experimentalDecorators": true,
      "emitDecoratorMetadata": true,
      "skipLibCheck": true,
      "forceConsistentCasingInFileNames": true
    },
    "include": ["src/**/*"],
    "exclude": ["dist", "node_modules"],
}
```

## ESLint導入
・ESLintとは**JavaScriptのリンター**(ソースコードを分析し問題点を指摘してくれる静的解析ツール)である。
・**JavaScriptのリンター**であるESLintにTypeScriptを適用するためには、下記のプラグインが必要なので追加する。
```
npm i -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
```
### `lint`コマンドでエラー検知できるようにする
ルート直下に`.eslintrc.json`を作成。
```
{
    "root": true, // .eslintrc.jsonがプロジェクトのルートに配置させているか（指定がないと上位階層へ設定ファイルを探索される）
    "parser": "@typescript-eslint/parser", // ESLintにTypeScriptを理解させる
    "parserOptions": {
      "project": "./tsconfig.json" // tsconfig.jsonの場所を指定
    },
    "plugins": [
      "@typescript-eslint" // ESLintのTypeScriptプラグインのルールを適用できる様にする（/eslint-pluginは省略可）
    ],
    "extends": [
      "eslint:recommended", // ESLintのJavaScriptルールセットを適用
      "plugin:@typescript-eslint/eslint-recommended", // eslint:recommendedに含まれるルールを型チェックでカバーできるものは無効化
      "plugin:@typescript-eslint/recommended-requiring-type-checking", // 型チェックが必要なルールを適用
    ]
}
```
package.jsonにlintコマンドを追加する。
```json
  "scripts": {
    "lint": "eslint . --ext ts"
  }
```

試しに下記の`src/hello.ts`を作成してみて、`yarn lint`を実行してみる。
```ts
var hello = undefined;
```
ちゃんと怒られる。
```
% yarn lint
yarn run v1.22.17
$ eslint . --ext ts

/Users/daisuke/Desktop/serverless-practice/src/hello.ts
  1:1  error  Unexpected var, use let or const instead    no-var
  1:5  error  'hello' is assigned a value but never used  no-unused-vars

✖ 2 problems (2 errors, 0 warnings)
  1 error and 0 warnings potentially fixable with the `--fix` option.

error Command failed with exit code 1.
```
修正して再度`yarn lint`実行。
```ts
const hello = "";
console.log(hello);
```
無事にlintコマンドが通るようになった！
```
% yarn lint
yarn run v1.22.17
$ eslint . --ext ts
✨  Done in 3.29s.
```

### ファイル保存時に自動フォーマットを適用する
#### Prettierインストール
コードを自動整形してくれるツールとして**Prettier**をインストールする。
```
npm install --save-dev prettier eslint-config-prettier
```
　**ESLint「ここのソースコード、汚えから整形した方がええで」
　Prettier「おk。整形しとくわ」**

というように、**ESLintで設定したルールに基づいて自動フォーマットを行うには「ESLint✖️Prettier」の両方必要**というわけだ。

>eslint-config-prettierは ESLint のコードフォーマットに関連するルールを無効化し、バグを検出するルールのみを有効にするプラグイン。

https://zenn.dev/big_tanukiudon/articles/c1ab3dba7ba111

`.eslintrc.json`のextendsにPrettierを追加
```
{
  /* 中略 */
  extends: [
    /* 中略 */
    "prettier", // 追加。他の設定の上書きを行うために、必ず最後に配置する。
  ],
};
```

#### VSCode拡張機能インストール
ESLint(dbaeumer.vscode-eslint)と Prettier(esbenp.prettier-vscode) の拡張機能をインストールする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653955/7428e1df-336c-0959-67a9-f1a97ad8733a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653955/634e6674-b52b-5093-702f-b670bc7213cb.png)

#### .vscode/settings.json追加
ルート直下に`.vscode/settings.json`を追加する。
```json
{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true 
}
```
ここまでの設定を完了すれば、VSCodeでのファイル保存時に`.eslintrc.json`の設定に基づいてPrettierが自動フォーマットしてくれる👍

# o2. S3アップロード時にLambdaが発火するようにする。
準備系が一通り終わったので、次に`S3アップロードしたらLambdaを発火させる`というのを**ローカル環境で**動作確認できるようにする。

## serverless.yml追加
Serverless Frameworkの設定ファイル。
```yml
service: serverless-practice

frameworkVersion: '3'

// プロバイダ(AWS/GCP/Azure)関連の設定
provider:
  name: aws
  runtime: nodejs14.x
  region: ${opt:region, 'us-east-1'}
  stage: ${opt:stage, 'offline'}
  endpointType: private
  // process.env.XXXX で参照したい変数をここに定義する。
  environment: ${file(serverless-config/env/env.${self:provider.stage}.yml)}

plugins:
  // ---.tsをコンパイル対象にする
  - serverless-plugin-typescript
  // ローカル環境でLambda動作確認用
  - serverless-offline-lambda
  - serverless-offline
  // ローカル環境でS3動作確認用
  - serverless-s3-local

custom:
  // serverless-s3-local用の情報
  // ./tmpで http://localhosst:4568 でS3をホストする
  s3:
    host: localhost
    port: 4569
    directory: ./tests/resources

// sls offline start, sls deploy時にS3バケットを作成する。
resources:
  Resources:
    NewResource:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: local-bucket

// Lambda関数
functions:
  csvRead:
    handler: src/functions/csvRead/handler.run 
    events:
      - s3:
          bucket: local-bucket
          event: s3:ObjectCreated:*
```

## src/functions/csvRead/handler.ts追加
```ts
import { S3Event } from "aws-lambda";

export const run = (event: S3Event) => {
  if (!event.Records || event.Records.length == 0) {
    return null;
  }
  console.log(`BucketName: ${event.Records[0].s3.bucket.name}`);
  console.log(`ObjectName: ${event.Records[0].s3.object.key}`);
};
```

## 動作確認

### ローカルS3接続用のAWS認証情報設定
```
% aws configure --profile s3local
AWS Access Key ID [None]: S3RVER
AWS Secret Access Key [None]: S3RVER 
Default region name [None]: us-east-1
Default output format [None]: json
```

### テスト用CSVファイル用意 / ローカルS3バケット用のフォルダ作成
・ルート直下に`tests/resources/sample_user.csv`を作成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653955/ec9c14d0-b13a-90e7-2165-e693b8296522.png)

### Lambda/S3バケットをローカル起動
```
% npx sls offline start
starting handler
Found S3 event listener for local-bucket
Offline Lambda Server listening on http://localhost:4000
S3 local started ( port:4569, family: IPv4, address: 127.0.0.1 )
Compiling with Typescript...
Using local tsconfig.json - tsconfig.json
Warning: "rootDir" from local tsconfig.json is overriden
Typescript compiled.
Watching typescript files...

Starting Offline at stage dev (us-east-1)

Offline [http for lambda] listening on http://localhost:3002
Function names exposed for local invocation by aws-sdk:
           * csvRead: serverless-practice-dev-csvRead
```

### ローカルS3にファイルアップロード
別のターミナル/コマンドプロンプトを起動して、S3アップロードコマンドを実行する。
```
% aws --endpoint http://localhost:4569 --profile s3local s3 cp ./tests/resources/sample_user.csv s3://local-bucket/sample_user.csv
upload: tests/resources/sample_user.csv to s3://local-bucket/sample_user.csv
```
`npx sls offline start`を実行したターミナル側では、以下のログが出力される。
```
info: Stored object "sample_user.csv" in bucket "local-bucket" successfully
BucketName: local-bucket
ObjectName: sample_user.csv
info: PUT /local-bucket/sample_user.csv 200 46ms 0b
```
・S3バケット(local-bucket)にsample_user.csvが無事にアップロードされたこと
・アップロードをトリガーにLambda関数(src/functions/csvRead/handler.run)起動されていること
が確認できる。

# o3. CSVファイル読込
S3にアップロードされたCSVファイルの中身を読み込んで標準出力してみる。
## src/utils/s3Client.ts追加
```ts
import AWS from "aws-sdk";
import * as stream from "stream";

let S3: AWS.S3 | null = null;
async function connectS3(): Promise<AWS.S3> {
  if (S3 == null) {
    if (process.env.NODE_ENV == "offline") {
      S3 = new AWS.S3({
        // force urls like http://{host}/{bucket}/{key} instead of http://{bucket}.
        s3ForcePathStyle: true,
        endpoint: new AWS.Endpoint(process.env.S3_ENDPOINT || ""),
        accessKeyId: "S3RVER",
        secretAccessKey: "S3RVER",
      });
    } else {
      S3 = new AWS.S3({
        // force urls like http://{host}/{bucket}/{key} instead of http://{bucket}.
        s3ForcePathStyle: true,
        endpoint: new AWS.Endpoint(process.env.S3_ENDPOINT || ""),
      });
    }
  }
  return S3;
}

export async function getObjectReadStream(key: string): Promise<stream.Readable> {
  const request: AWS.S3.GetObjectRequest = {
    Bucket: process.env.S3_BUCKET || "",
    Key: key,
  };
  const s3: AWS.S3 = await connectS3();
  return await s3
    .getObject(request)
    .createReadStream()
    .on("error", (err: Error) => {
      console.log(err);
      return null;
    });
}
```

## 環境変数定義ファイル追加
S3やDB関連のように環境ごとに値が変わるものは`serverless-config/env/env.${ENV}.yml`に定義する。

```yml
NODE_ENV: ${self:provider.stage}

// S3
S3_ENDPOINT: http://localhost:4569
S3_BUCKET: local-bucket
```

## src/functions/csvRead/handler.ts修正
・CSVパーサでファイル読込＆標準出力する。
```ts
import { S3Event } from "aws-lambda";
import { Parser, parse } from "csv-parse";
import iconv from "iconv-lite";
import { getObjectReadStream } from "../../utils/s3Client";

export const run = async (event: S3Event) => {
  if (!event.Records || event.Records.length == 0) {
    return null;
  }
  // get csv file's s3 object key
  const objectKey = event.Records[0].s3.object.key;

  // configure how csv parser processes each row
  const parser: Parser = parse();
  parser.on("readable", () => {
    let row;
    while ((row = parser.read())) {
      console.log(row);
    }
  });
  // read csv file
  const csvReadStream = await getObjectReadStream(objectKey);
  // write stream into csv parser
  csvReadStream.pipe(iconv.decodeStream("Shift_JIS")).pipe(parser);
};
```

## 動作確認
・CSVファイル(`tests/resources/sample_user.csv`)に下記を追記する。
```csv
'00001','Anthony',10
'00002','Bryant',20
'00003','Carmelo',30
```
・先ほどと同じく`npx sls offline start`でローカル起動してから、別ターミナルからCSVファイルをS3にアップロードする。
```
% aws --endpoint http://localhost:4569 --profile s3local s3 cp ./tests/resources/sample_user.csv s3://local-bucket/sample_user.csv
```
```
% npx sls offline start
　　〜〜一部割愛〜〜
Function names exposed for local invocation by aws-sdk:
           * csvRead: serverless-practice-offline-csvRead
info: Stored object "sample_user.csv" in bucket "local-bucket" successfully
offline: (λ: csvRead) RequestId: cl36vrfq70000rgo03zy44q38  Duration: 533.83 ms  Billed Duration: 534 ms
info: PUT /local-bucket/sample_user.csv 200 577ms 0b
info: GET /local-bucket/sample_user.csv 200 8ms 84b
[ '00001', 'Anthony', 10 ]
[ '00002', 'Bryant', 20 ]
[ '00003', 'Carmelo',30 ]
```
↑の通り、S3にアップロードしたCSVファイルが読み込まれ、中身が標準出力に表示されている👍

# o4. mysqlインポート
CSVファイルに格納されているデータが`DB投入用データ`と仮定して、次の動きを実現する。
`S3アップロード → CSVファイル読込 → MySQLにデータ投入`

## docker-compose.yml追加
・ローカルでMySQLコンテナを起動するための`docker-compose.yml`をルート直下に作成。
```yml
version: "3.8"

services:
  db:
    image: mysql:latest
    container_name: serverless-practice-db
    environment:
      MYSQL_DATABASE: serverlessPracticeDb
      MYSQL_ROOT_PASSWORD: passw@rd
      TZ: "Asia/Tokyo"
    volumes:
      - ./docker/db/data:/var/lib/mysql
      - ./docker/db:/tmp/db
      - ./docker/db/my.cnf:/etc/mysql/conf.d/my.cnf
    ports:
      - 3309:3306
```
`docker/db/my.cnf`作成
```
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
default_authentication_plugin = mysql_native_password

[client]
default-character-set=utf8mb4
```
### default_authentication_plugin = mysql_native_passwordについて
・**MySQL8.0以降、認証プラグインは「caching_sha2_password」がデフォルト**。
・生パスワード(mysql_native_password)で認証したい場合は、my.cnfの[mysqld]にて下記の設定を行う。
```
[mysqld]
default_authentication_plugin = mysql_native_password
```

https://take-it-easy.site/mysql/mysql-mycnf-default-authentication-plugin/

https://www.petitmonte.com/database/mysql_authentication_plugin.html

## DB情報を環境定義ファイルに追加
先ほど作成した`serverless-config/env/env.offline.yml`に、DB情報を追加する。
```yml
NODE_ENV: ${self:provider.stage}

// S3
S3_ENDPOINT: http://localhost:4569
S3_BUCKET: local-bucket

// DB
DB_HOST: localhost
DB_PORT: 3309
DB_NAME: serverlessPracticeDb
DB_USER: root
DB_PASSWORD: passw@rd
```

## テーブル作成SQLファイル作成
`docker/db/create_table.sql`作成。
```sql
create table if not exists sample_user (
  user_id char(15),
  user_name varchar(50),
  age int(2)
);
```

## MySQLコンテナ起動＆テーブル作成
```
// MySQLコンテナ起動
docker compose up -d

// コンテナログイン
docker exec -it serverless-practice-db bash -p 

// テーブル作成SQL実行
root@f8bda5a9fe02:/# cd /tmp/db
root@f8bda5a9fe02:/tmp/db# mysql --user=root --password=passw@rd < create_table.sql  serverlessPracticeDb

// データベースserverlessPracticeDb に sample_userテーブルが作成されていること
root@f8bda5a9fe02:/tmp/db# mysql --user=root --password=passw@rd serverlessPracticeDb
mysql> show tables;
+--------------------------------+
| Tables_in_serverlesspracticedb |
+--------------------------------+
| sample_user                    |
+--------------------------------+
1 row in set (0.01 sec)
```

## src/utils/dbUtils.ts作成
・MySQLに接続しSQLを実行するための処理を追加。
```ts
import mysql from "promise-mysql";
import BlueBird from "bluebird";

export async function createDbConnection(): Promise<BlueBird<mysql.Connection>> {
  return mysql.createConnection({
    host: process.env.DB_HOST,
    database: process.env.DB_NAME,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    port: process.env.NODE_ENV == "offline" ? 3309 : 3306,
    timezone: "jst",
  });
}
```

## src/functions/csvRead/handler.ts修正
・CSVファイルの中身をINPUTに、MySQLに対してINSERT文を実行する。
```ts
import { S3Event } from "aws-lambda";
import { Parser, parse } from "csv-parse";
import iconv from "iconv-lite";
import mysql from "promise-mysql";
import { getObjectReadStream } from "../../utils/s3Client";
import { createDbConnection } from "../../utils/dbUtils";

export const run = async (event: S3Event) => {
  if (!event.Records || event.Records.length == 0) {
    return null;
  }
  const objectKey = event.Records[0].s3.object.key;

  // create db connection
  const mysqlConnection: mysql.Connection = await createDbConnection();

  // configure how csv parser processes each row
  const parser: Parser = parse();
  parser.on("readable", () => {
    let row;
    while ((row = parser.read())) {
      const statement = `INSERT INTO sample_user VALUES(${row})`;
      console.log(`executed: ${statement}`);
      mysqlConnection.query(statement);
    }
  });
  // read csv file
  const csvReadStream = await getObjectReadStream(objectKey);
  // write stream into csv parser
  csvReadStream.pipe(iconv.decodeStream("Shift_JIS")).pipe(parser);
};
```

## 動作確認
Lambda起動
```
npx sls offline start
```
ローカルS3にCSVファイルアップロード
```
aws --endpoint http://localhost:4569 --profile s3local s3 cp ./tests/resources/sample_user.csv s3://local-bucket/sample_user.csv
```
Lambda起動中のターミナルログ
```
info: Stored object "sample_user.csv" in bucket "local-bucket" successfully
info: PUT /local-bucket/sample_user.csv 200 811ms 0b
offline: (λ: csvRead) RequestId: cl38pcfy6000092o05fir7azs  Duration: 1043.45 ms  Billed Duration: 1044 ms
info: GET /local-bucket/sample_user.csv 200 6ms 62b
executed: INSERT INTO sample_user VALUES('00001','Anthony',10)
executed: INSERT INTO sample_user VALUES('00002','Bryant',20)
executed: INSERT INTO sample_user VALUES('00003','Carmelo',30)
```
DBに反映されていること
```
// コンテナログイン
docker exec -it serverless-practice-db bash -p

// DBログイン
mysql --user=root --password=passw@rd serverlessPracticeDb

// 確認
mysql> select * from sample_user;
+---------+-----------+------+
| user_id | user_name | age  |
+---------+-----------+------+
| 00001   | Anthony   |   10 |
| 00002   | Bryant    |   20 |
| 00003   | Carmelo   |   30 |
+---------+-----------+------+
3 rows in set (0.00 sec)
```

# o5. RDS作成 ※後日追記
# o6. デプロイ（RDSと同じVPC内にLambda作成） ※後日追記

# 余談

## MySQL「複数のINSERT文」「1つのINSERT文with複数VALUE」どっちが速い？

結論：1つのINSERT文with複数VALUE の方が速い。

https://stackoverflow.com/questions/1793169/which-is-faster-multiple-single-inserts-or-one-multiple-row-insert

## MySQL INSERT multiple rows limit

`max_allowed_packet `に設定されている。
```
mysql> SHOW VARIABLES LIKE 'max_allowed_packet';
+--------------------+---------+
| Variable_name      | Value   |
+--------------------+---------+
| max_allowed_packet | 4194304 |
+--------------------+---------+
1 row in set (0.11 sec)
```

## MySQL my.cnf設定関連
```
mysql> SHOW VARIABLES like 'char%';
+--------------------------+--------------------------------+
| Variable_name            | Value                          |
+--------------------------+--------------------------------+
| character_set_client     | utf8mb4                        |
| character_set_connection | utf8mb4                        |
| character_set_database   | utf8mb4                        |
| character_set_filesystem | binary                         |
| character_set_results    | utf8mb4                        |
| character_set_server     | utf8mb4                        |
| character_set_system     | utf8mb3                        |
| character_sets_dir       | /usr/share/mysql-8.0/charsets/ |
+--------------------------+--------------------------------+
8 rows in set (0.00 sec)

mysql> SET character_set_results = utf8mb4;
```

https://dev-dub.hatenablog.com/entry/2020/11/03/113818

https://oki2a24.com/2018/10/21/how-to-set-mysql-8-cnf-to-utf8mb4/

https://off.tokyo/blog/mariadb%E3%81%AEcharacter_set_client%E3%81%8Clatin1%E3%81%AE%E3%81%9B%E3%81%84%E3%81%A7%E6%96%87%E5%AD%97%E5%8C%96%E3%81%91/

## serverless.ymlチートシート
```yml:serverless.yml
service: serverless-practice

frameworkVersion: '3'

// プロバイダ(AWS/GCP/Azure)関連の設定
provider:
  name: aws
  runtime: nodejs14.x
  region: ${opt:region, 'us-east-1'}
  stage: ${opt:stage, 'offline'}
  endpointType: private
  // process.env.XXXX で参照したい変数をここに定義する。
  environment: ${file(serverless-config/env/env.${self:provider.stage}.yml)}
  iam.role
  versionFunctions 
  tags
  s3

plugins:
  // ---.tsをコンパイル対象にする
  - serverless-plugin-typescript
  // ローカル環境でLambda動作確認用
  - serverless-offline-lambda
  - serverless-offline
  // ローカル環境でS3動作確認用
  - serverless-s3-local

custom:
  // serverless-s3-local用の情報
  // ./tmpで http://localhosst:4568 でS3をホストする
  s3:
    host: localhost
    port: 4569
    directory: ./tests/resources

// CloudFormationで作成したいAWSリソースを定義する。
// resources配下に定義するのは、CloudFormation templateそのもの。
resources:
  Resources:
    NewResource:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: local-bucket

// Lambda関数
functions:
  csvRead:
    handler: src/functions/csvRead/handler.run 
    events:
      - s3:
          bucket: local-bucket
          event: s3:ObjectCreated:*
```

## S3バケット関連の設定
`provider.s3.HogeBucket`に定義可能ならプロパティは[公式ドキュメント](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html)に一覧化されている。
```yml:serverless.yml
# バケットに設定可能なプロパティは
provider:
  s3:
    fileBucket:
      name: ${self:custom.s3.bucketName}
      bucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              KMSMasterKeyID: ${self:custom.conf.KMS_KEY_ARN}
              SSEAlgorithm: 'aws:kms'
      publicAccessBlockConfiguration:
        BlockPublicAcls: true
        IgnorePublicAcls: true
        BlockPublicPolicy: true
        RestrictPublicBuckets: true
      loggingConfiguration:
        DestinationBucketName: ${self:custom.conf.S3_LOGGING_BUCKET}
        LogFilePrefix: ${self:custom.s3.bucketName}-
      tags: ${file(serverless-config/tags-s3.yml)}


# serverless-s3-local用の情報
# ローカル動作用の設定（ホストURLとホストディレクトリの設定のみ）
# ./tmpで http://localhosst:4568 でS3をホストする
custom:
  s3:
    host: localhost
    port: 4569
    directory: ./tests/resources
```

## バケット関連の設定を理解する。

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html

|プロパティ名|意味|
|---|---|
|BucketEncryption||

# S3について理解する
① サーバ側の暗号化
② 暗号化キータイプ
③ ブロックパブリックアクセス設定
④ バージョニング

## ① "サーバ側"の暗号化とは？
・S3オブジェクトを暗号化して保存する。
・暗号/復号に使う鍵は、**Amazon S3に作成/管理させる/自分でKSMに鍵を作る/別で暗号化キーを用意する** のいずれかの方法で用意する。

・SSE-S3(Amazon S3 が作成、管理、使用する暗号化キー)で暗号化する場合、暗号化にお金はかからない。
・SSE-S3のデフォルト暗号化設定をカスタマイズする場合は、別料金がかかる。

>There are no additional fees for using server-side encryption with Amazon S3-managed keys (SSE-S3). However, requests to configure the default encryption feature incur standard Amazon S3 request charges. For information about pricing, see Amazon S3 pricing.

## ② ３つの暗号化キータイプ
|キータイプ|特徴|
|---|---|
|SSE-S3|Amazon S3 が作成、管理、使用する暗号化キー<br>AES-256方式の共通鍵を使う。|
|SSE-KMS|AWS Key Management Service (AWS KMS) で保護されている暗号化キー。
|SSE-C|ユーザーが暗号化キーを管理|

特に要件がなければ「SSE-S3」を。
複数のバケットに対して１つの暗号鍵を利用したい場合などは「SSE-KMS」を。
他に要件があれば「SSE-C」を。

で一旦OKな気がする。

## ③ ブロックパブリックアクセス設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653955/f99488c5-cf75-5130-24a6-caf1e1d95ac2.png)
↑の設定を理解するためには「ACL（アクセスコントロールリスト）」「バケットポリシー」を理解する必要がある。
### ACL(アクセスコントロールリスト)
・バケット/オブジェクトへのアクセス権限を付与する対象をここに定義する。
・付与対象は①AWSユーザー②事前定義済みS3グループ
・付与する単位について、
|対象|アクション|意味|
|---|---|---|
|バケット|READ|バケット内の全オブジェクトを「リスト/一覧化」可能|
|オブジェクト|READ|オブジェクトデータ、メタデータの参照可能|
|バケット|WRITE|バケット内にオブジェクトを作成可能|
|オブジェクト|WRITE|※該当しない※|
|バケット|READ-ACP|**バケットACL**を読み込み可能|
|オブジェクト|READ-ACP|**オブジェクトACL**を読み込み可能|
|バケット|WRITE-ACP|**バケットACL**を書き込み可能|
|オブジェクト|WRITE-ACP|**オブジェクトACL**を書き込み可能|
|バケット|FULL_CONTROL|バケットに対するREAD/WRITE/READ_ACL/WRITE_ACLと同義|
|オブジェクト|FULL_CONTROL|オブジェクトに対するREAD/WRITE/READ_ACL/WRITE_ACLと同義|

✅ ACL ＝ 文字通りバケット/オブジェクトにアクセスさせる対象を管理できるもの。
✅ 「バケットのACL」と「オブジェクトのACL」の２つある。
✅ **ACLによるアクセス制御の管理単位は**「**AWSユーザー**」なので、<font color="red">ACLによるアクセス制御ではなくバケットポリシーを使うべき。</font>

### バケットポリシー
・デフォルトで「全拒否」なので、設定してあげる必要がある。
・ACLと異なり、アクセス制御の管理単位は下記のように幅広く設定可能。
```
AWSアカウント/ルートユーザー
IAMユーザー
IAMロール
AWSサービス
```

https://qiita.com/irico/items/a3ab1f8ebf1ece9cc783

### AWS「パブリックアクセスは基本的にさせないようにしよ？」
文字通り「**AWSアカウントを持たない外部の人間をアクセスできる状態**」が「**パブリックアクセス**」である。S3の基本的な考え方として「**S3にあげるものはパブリックアクセスできないもの**」であるので、<font color="red">作成されるバケット/オブジェクトがパブリックアクセスされないようにする設定/オプション</font>があるわけだ。

### パブリックアクセスをブロックするための設定
① バケットごとの設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653955/f784e7f1-86d6-bfef-91ad-1319f6a8ebe5.png)
② AWSアカウントが保有する全バケットに対する設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653955/f1046796-58f9-4a1c-31c5-8729dc167e85.png)


#### パブリックアクセスに対する４つのブロッキング設定 -> [参考](https://muessenwirwollen.hatenablog.com/entry/2019/10/19/203706)

|種類|説明|
|---|---|
|BlockPublicAcls|未来にパブリックなACLに変更することを阻止する|
|IgnorePublicAcls|現在のパブリックなACLを無効化する|
|BlockPublicPolicy|未来にパブリックなポリシーに変更することを阻止する|
|RestrictPublicBuckts|現在のパブリックなポリシーを無効化する|

##### 【BlockPublicAcls】 新しいアクセスコントロールリスト (ACL) を介して付与されたバケットとオブジェクトへのパブリックアクセスをブロックする

新しくバケット/オブジェクトを作る。
　↓
バケット/オブジェクトに紐づくACLが存在する。
　↓
このACLが「**パブリックアクセスできるようなもの**」であっても、パブリックアクセスできないようにする。
　↓
<font color="red">未来に作られるパブリックなACL</font>を**Block**する。

##### 【IgnorePublicAcls】 任意のアクセスコントロールリスト (ACL) を介して許可されたバケットとオブジェクトへのパブリックアクセスをブロックする

**これから新しく作る**バケット/オブジェクトが<font color="red">パブリックアクセスできるようなACLであっても</font>パブリックアクセスできないようにする【1】に対して、【2】は対象が全て(**これから新しく作る＋既存のもの**)となる。
　↓
<font color="red">既存＋未来で作られるパブリックなACL</font>を**全てIgnore**する。

##### 【BlockPublicPolicy】 新しいパブリックバケットポリシーを介して許可されたバケットとオブジェクトへのパブリックアクセスをブロックする

【BlockPublicAcls】のバケット版。バケットポリシーでパブリックアクセスできないようにする。

##### 【RestrictPublicBuckts】 任意のパブリックバケットポリシーを介して、バケットとオブジェクトへのパブリックアクセスとクロスアカウントアクセスをブロックする

【IgnorePublicAcls】のバケット版。バケットポリシーでパブリックアクセスできないようにする。



## ④ バージョニング
## 

# その他MEMO
## そもそも暗号鍵/共通鍵暗号方式/AESとは
暗号鍵とは「暗号化したり、元に戻したり（復号）するときに使う**データ/文字列**」
暗号化方式とは「暗号化のやり方（方式）」
暗号方式の１つに「**共通鍵暗号方式**」がある。データの送信者と受信者が持つ鍵は他者に奪われてはいけないものなので「**秘密鍵暗号方式**」とも呼ぶ。
共通鍵暗号方式の１つとして「**AES（Advanced Encryption Standard）**」が挙げられる。

## serverlessによるデプロイはデフォルトでバージョニングされる→容量圧迫
serverless frameworkではデフォルトで、デプロイした全てのバージョンを保持するようになっています。 

https://tech.ga-tech.co.jp/entry/2018/12/12/120000

デプロイするたびに上書きされると思いきや、バージョニングされることで古いものは残りっぱなしになるわけだ。
不要なら`serverless.yml`に`versionFunctions: false`を定義すればバージョニングされない👍
```yml:serverless.yml
provider:
  versionFunctions: false
```

## `sls deploy`時に発生する`Error: EPERM: operation not permitted, unlink`の対処方法
原因は[このissue](https://github.com/serverless/serverless-plugin-typescript/issues/219)にあるとおり、serverless-typescript-pluginのバグ。
中間ファイルの存在がネックとなって引き起こされるバグらしいので、この中間ファイルを消した上で`sls deploy`を実行する。
```:中間ファイルを削除するコマンド
rm -rf .serverless
rm -rf .build
```

## `sls deploy`時に発生する`s3 bucket  already exists in stack`の原因・対処方法
S3バケットを生成するserverelssプロジェクトとして、下記のserverless.ymlを作成した。
しかしこのserverless.ymlだと**２つのS3バケットを作ってしまう。**
　1つめ：NewResourceによるS3バケット作成
　2つめ：events.s3によるS3バケット作成 <-- これを知らなくてハマった [参考](https://stackoverflow.com/questions/59086311/serverless-deplyment-error-s3-bucket-already-exists-in-stack)
```yml:serverless.yml
# CloudFormation template syntax. 
# What goes in this property is raw CloudFormation template syntax
resources:  
  Resources: 
    NewResource:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:custom.s3.bucketName}

functions:
  importCsvData:
    handler: src/functions/importCsvData/importCsvDataHandler.runner
    events:
      - s3:
          bucket: ${self:custom.s3.bucketName}
          event: s3:ObjectCreated:*
          rules:
            - suffix: .csv
```
なので、こうすればOK。
```yml:serverless.yml
functions:
  importCsvData:
    handler: src/functions/importCsvData/importCsvDataHandler.runner
    events:
      - s3:
          bucket: ${self:custom.s3.bucketName}
          event: s3:ObjectCreated:*
          rules:
            - suffix: .csv
```

## Serverless Plugin IfElse で、プロパティ定義自体を差し込めるようにする
・add, remove or change the values of attributes in the yml file.
・It works with both package and deploy commands.

### Install
```
npm i serverless-plugin-ifelse --save-dev
```