localstack + aws cdk でAWS S3をローカル環境に立ち上げてLambdaでアクセスするメモ

## 概要
[前回はローカルでデバッグ実行をできるようにした。](https://qiita.com/hibohiboo/items/fa76084a207e5a3d5955)
今回はS3をローカルで実行する。
リファクタリングは[次回](https://qiita.com/hibohiboo/items/4e5fd8c723e0f70c12b1)。

[ソースコード](https://github.com/hibohiboo/develop/tree/2dca5633f60621882e853598463da20d547e07e8/tutorial/lesson/aws/typescript/projects/cdk_sample)

## 環境
* windows 10
* Docker version 20.10.5, build 55c4c88
* aws-cdk 1.95.1
* aws-cdk-local 1.65.4
* aws-cli/2.1.29 Python/3.8.8 Windows/10 exe/AMD64 prompt/off
* node v14.16.0

## ローカルでS3を動かす
### 検討: localstack
ローカルでS3を実行する方法は、[Minio](https://github.com/minio/minio)(S3クローン)と[localstack](https://github.com/localstack/localstack)がある模様。
localStackについて調べてみると、test/mock環境をローカルに構築してくれるものの模様。
サポートしているものと他の手段があるものを、今調べて分かる範囲で表にしてみた。

localstack|その他の選択肢
--|--
ACM|未調査
API Gateway|[sam local start-api](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-local-start-api.html)
CloudFormation|未調査
CloudWatch|未調査
CloudWatch Logs|未調査
DynamoDB|[Dynamo DB local](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html)
DynamoDB Streams|未調査
EC2|未調査
Elasticsearch Service|未調査
EventBridge (CloudWatch Events)|未調査
Firehose|未調査
IAM|未調査
Kinesis|未調査
KMS|未調査
Lambda|[sam local invoke](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-local-invoke.html)
Redshift|未調査
Route53|未調査
S3|[minio](https://github.com/minio/minio)
SecretsManager|未調査
SES|未調査
SNS|未調査
SQS|[ElasticMQ](https://github.com/softwaremill/elasticmq)
SSM|未調査
StepFunctions|未調査
STS|未調査

### localstackでs3をデプロイする
* `aws cdk`を使ってデプロイする手順。
 
#### localstack用のprofileを作成

* `aws configure`でprofileを作る

```
aws configure --profile localstack
```

入力した内容は以下。

```
AWS Access Key ID [None]: dummy
AWS Secret Access Key [None]: dummy
Default region name [None]: us-east-1
Default output format [None]: json
```

#### localstack用のDockerを作成

* aws-cdkを使ってS3をデプロイするため、aws-cdkの前提条件であるcloudformationもSERVICEに含めている。

```yaml:docker/localstack-docker-compose.yml
version: '3.5'

networks:
  localstack:
    driver: bridge

services:
    localstack:
      image: localstack/localstack
      ports:
        - 4566:4566
      environment:
        - SERVICES=cloudformation,s3
      networks:
        - localstack
```

起動スクリプト。何度も使うのでshにしておく。

```bash:bin/localstack.sh
#!/bin/bash

bin_dir=$(cd $(dirname $0) && pwd)
composeFile=${1:-"localstack-docker-compose.yml"}
cd $bin_dir/../docker && docker-compose -f $composeFile up
```

`./bin/localstack.sh`を実行し、localstackを起動しておく。

#### cdkのファイルを作成

* シンプルにS3を作成するだけのスタック

```ts:cdk/lib/s3-stack.ts

export class S3Stack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    const bucket = new Bucket(this, 'my-bucket', {
      versioned: true,
      bucketName: 'my-sample-bucket'
    });
  }
}
```

```ts:cdk/bin/s3-index.ts
#!/usr/bin/env node

import { App } from '@aws-cdk/core';
import { S3Stack } from '../lib/s3-stack';

const app = new App();
new S3Stack(app, 'S3Stack2021');
```


### Localstack上の S3 に Bucket/Object を作成する

* cdk.jsonにはlambdaのstackを入れておきたいので、`--app`オプションでlambdaのスタックを指定している。
  * ※公式ガイドの`--app "npx ts-node"`を使おうとすると、`Command Not Exist`のエラーが出たので、`npm i -g ts-node`でts-nodeをグローバルインストールしている※注

<details><summary>※注</summary><div>

* package.jsonの中でなら、ローカルインストールでもいけるみたい。何故？
  * `"s3-localdeploy": "cdklocal deploy --app \"npx ts-node --prefer-ts-exts bin/s3-index.ts\" --endpoint-url=http://localhost:4566 --profile=localstack"`

</div></details>

**stackが問題ないことを確認。**

```
npx cdk synth --app "ts-node --prefer-ts-exts bin/s3-index.ts"
```

**デプロイ**

```
npx cdklocal deploy --app "ts-node --prefer-ts-exts bin/s3-index.ts" --endpoint-url=http://localhost:4566 --profile=localstack
```

**ファイルアップロード確認**

```
aws s3 --endpoint-url=http://localhost:4566 cp package.json s3://my-sample-bucket --profile=localstack
```

## S3にアクセスするLambdaの作成
#### 実装

* イベントでKeyを受け取り、愚直にS3にアクセスする

```ts:src/lambda/handlers/s3-sample.ts
import { S3 } from 'aws-sdk';
import { Handler } from 'aws-lambda';

const config = {
  endpoint: (process.env.IS_LOCAL_STACK === "true" ? "http://localstack:4566" : undefined),
  s3ForcePathStyle: process.env.IS_LOCAL_STACK === "true",
}
const s3 = new S3(config);
const { S3_BUCKET } = process.env;
if (!S3_BUCKET) throw Error('env S3_BUCKET is empy')

export const lambdaHandler: Handler = async (event, context, callback) => {
  try {
    const params = {
      Bucket: S3_BUCKET,
      Key: event.Key
    };
    const ret = await s3.getObject(params).promise();
    if (!ret.Body) throw Error(`s3 object is empty. Bucket: ${S3_BUCKET}, Key: ${event.Key}`)
    const message = ret.Body.toString();
    console.log(message);
    callback(null, `access ${event.Key}`)
  } catch (err) {
    console.log(err);
    return err;
  }
};
```

#### S3にアクセスするLambda用のStackの作成

```ts:cdk/bin/s3-lambda-index.ts
#!/usr/bin/env node

import { App } from '@aws-cdk/core';
import { S3LambdaStack } from '../lib/s3-lambda-stack';
import { createLaunch } from '../lib/process/after';

const app = new App();
const stack = new S3LambdaStack(app, 'S3LambdaStack2021');
createLaunch(stack);
```

```ts:cdk/lib/s3-lambda-stack.ts
import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs';
import { entryHandlerDir } from './constants';

export class S3LambdaStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new NodejsFunction(this, 's3-sample', {
      runtime: lambda.Runtime.NODEJS_14_X,
      entry: `${entryHandlerDir}/s3-sample.ts`,
      handler: 'lambdaHandler',
      bundling: {
        sourceMap: true,
      },
      environment: {
        IS_LOCAL_STACK: process.env.IS_LOCAL_STACK || '',
        S3_BUCKET: process.env.S3_BUCKET || ''
      }
    });
  }
}
```

```ts:cdk/lib/constants.ts
export const entryHandlerDir = '../src/lambda/handlers/';
```

## 動作確認

### 実行準備

* 環境変数の準備
* 実行用のスクリプトをpackage.jsonに用意

```json:cdk/env.json
{
  "s3sampleA17B5BBA": {
    "IS_LOCAL_STACK": "true",
    "S3_BUCKET": "my-sample-bucket"
  }
}
```

* docker-composeで立ち上げているlocalstackのネットワークの名前は`docker_localstack`となる
  * --docker-networkにそれを指定。

```diff:cdk/package.json
  "scripts": {
+    "s3-localdeploy": "cdklocal deploy --app \"npx ts-node --prefer-ts-exts bin/s3-index.ts\" --endpoint-url=http://localhost:4566 --profile=localstack",
+    "s3-bundle": "cdk synth --app \"npx ts-node --prefer-ts-exts bin/s3-lambda-index.ts\" > cdk.out/s3-template.yaml",
+    "s3-invoke": "cd cdk.out && echo {\"Key\": \"package.json\"} | sam.cmd local invoke s3sampleA17B5BBA --docker-network docker_localstack --profile=localstack -t s3-template.yaml -n ../env.json --event -",
+    "debug-s3-invoke": "cd cdk.out && echo {\"Key\": \"package.json\"} | sam.cmd local invoke -d 5858 s3sampleA17B5BBA --docker-network docker_localstack --profile=localstack -t s3-template.yaml -n ../env.json --event -"
```

### 実行

```
cd cdk
npm run s3-bundle
npm run s3-invoke
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/990a82f9-a863-1029-d782-5c315f1eb95c.png)

実行できた。


### 失敗：ネットワークエラー

* 指定ポートを間違えて`4577`にしているのが原因。

```
{"message":"Inaccessible host: `localstack'. This service may not be available in the `us-east-1' region.","code":"UnknownEndpoint","region":"us-east-1","hostname":"localstack","retryable":true,"originalError":{"message":"connect ECONNREFUSED 172.21.0.2:4572","errno":-111,"code":"NetworkingError","syscall":"connect","address":"172.21.0.2","port":4572,"region":"us-east-1","hostname":"localstack","retryable":true,"time":"2021-03-27T10:32:44.322Z"},"time":"2021-03-27T10:32:44.322Z"
```



## 参考
[Testable - lambda : 和田卓人](https://speakerdeck.com/twada/testable-lambda-working-effectively-with-legacy-lambda)
[aws-sam-localstack-example](https://github.com/bilzard/aws-sam-localstack-example)
[aws cdk s3](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-s3-readme.html)
[ローカル環境でLambda+S3のテストをする](https://qiita.com/billthelizard/items/22d2457f3d6386d21796)
[github - localstack](https://github.com/localstack/localstack)
[github - minio](https://github.com/minio/minio)
[github - localstack-aws-cdk](https://github.com/localstack/aws-cdk-local)
[github - awscli-local](https://github.com/localstack/awscli-local)
[Localstack + CDK = local AWS development](https://dev.to/_mikigraf/localstack-cdk-local-aws-development-58ff)
[localstackのローカル環境にAWS CDKでS3バケットを作成してみる](https://undersourcecode.hatenablog.com/entry/2020/11/15/203444)
[LocalStackに向けてTerraformを実行する](https://future-architect.github.io/articles/20201113/)
[LocalStackをつかってローカルでLambdaを実行してみた](https://dev.classmethod.jp/articles/localstack-lambda/)
[LocalStackでAWSサービスを試してみた](https://aimstogeek.hatenablog.com/entry/2020/08/03/080000)
[Local Development with LocalStack](https://tech.unifa-e.com/entry/2020/12/16/080000)
[AWS SAM CLI + DynamoDB localを使ってローカル上で完結するAPI開発](https://qiita.com/jacoyutorius/items/72984ff07b3b30576d43)
[elastcisq - sws local](https://github.com/softwaremill/elasticmq)
[docker elasticsq](https://hub.docker.com/r/softwaremill/elasticmq/)
[Lambda Function の開発について悩んだところをまとめてみた](https://qiita.com/billthelizard/items/be87a7662c34fd5c634e)
[LocalStackをGitHub Actions上で動かしてテストする](https://dev.classmethod.jp/articles/localstack-on-github-actions-sample/)
[LaravelをElasticMQ（Amazon SQS互換）と連携してみる](https://qiita.com/nia_tn1012/items/1bd60b1a3900a2b52939)
[開発のためにローカルにもS3が欲しいというわがまま、MINIOが叶えてくれました](https://liginc.co.jp/402383)
[【AWS S3】S3 Presigned URLの仕組みを調べてみた](https://qiita.com/tmiki/items/87697d3d3d5330c6fc08)
[local invoke option](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-local-invoke.html)`
[localstack有料版をちゃんと使ってみる](https://iga-ninja.hatenablog.com/entry/2020/10/24/011800)