LocalStack ローカル環境でAWSを使おう

# この記事は？
AWSのエミュレーションツール[LocalStack](https://github.com/localstack/localstack)を使って仮想のAWS環境を立ち上げ、その中にLambdaとS3をデプロイして動かしたときの備忘録

https://localstack.cloud/

# LocalStackとは？
Dockerコンテナ内でAWSのエミュレートし、その環境内にデプロイしたりできるツール
無料版では利用できるAWSサービスに制限があるものの、メジャーどころは大体抑えられている

:::note info
無料版で利用できるのは[このページ](https://docs.localstack.cloud/aws/feature-coverage/#aws-feature-coverage)にあるサービスのうち`(Pro)`がついていないもの
**Lambda**, **S3**, **DynamoDB**など主要なサービスはだいたい無料で使える
:::

https://docs.localstack.cloud/aws/feature-coverage/#aws-feature-coverage

## メリット
- (一部対象外のサービスはあるけど)無料でAWSを使える
- 実際のクラウド上の環境を汚さずに動作確認できる
  - CIで統合テスト回したいときとかに便利

## デメリット
- 一部のAWSサービスは有料
- (極端な表現をすると)Lambdaとかのコード上にテスト環境でのみ実行する処理を置かないといけない場合がある
  - [こちら](#コードを実装)を参照

# 今回デプロイするものは？
↓のようなファイルを保存しておくS3バケットとそれにPutするためのLambda関数
**LocalStack内のLambdaからLocalStack内のAWSリソースへ触りに行くケース**の実験をするためにこれを用意した
![構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2840455/6c3be86e-5895-ce9b-6899-a1525a71612e.png)

# LocalStackのインストールと準備
実装に取り掛かる前にLocalStackとその前提となる環境の用意をする

## 前提
LocalStackを使うのに必要なのが↓

- Python(3.7~3.10)
- Docker

今回はCDKやAWS CLIも使うのでそれらも必要

- AWS CLI
- Node.js
- AWS CDK

## LocalStackのインストール
```bash
pip install localstack
```
:::note warn
[LocalStackのドキュメント](https://github.com/localstack/localstack#installing)にある通り、rootユーザーでインストールしないように注意！
> Note: Please do not use sudo or the root user - LocalStack should be installed and started entirely under a local non-root user. If you have problems with permissions in macOS High Sierra, install with pip install --user localstack
:::

インストールしたら↓のコマンドで起動&ステータス確認してみよう

```bash
localstack start -d
localstack status services
┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
┃ Service                  ┃ Status      ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
│ acm                      │ ✔ available │
│ apigateway               │ ✔ available │
│ cloudformation           │ ✔ available │
│ cloudwatch               │ ✔ available │
│ config                   │ ✔ available │
│ dynamodb                 │ ✔ available │
...
```

## AWS Localのインストール
続いてAWS Localをインストール

https://github.com/localstack/awscli-local

:::note
**AWS Localとは？**
AWS CLIをLocalStack向けにラップしたツール
通常であればAPIのエンドポイントがAWSクラウトを向いているところ、LocalStackのDockerコンテナに向けてくれる
:::

```bash
pip install awscli-local
awslocal --version
```

デフォルトリージョンも指定しておく(ACCESS_KEYなどは不要)

```bash
export DEFAULT_REGION=ap-northeast-1
```

※ [このドキュメント](https://docs.localstack.cloud/integrations/aws-cli/#configurations)を読んだ感じだと`awslocal configure`で設定できそうだけど自分の環境ではコマンドが見つからないと言われてしまった

## CDK Localのインストール
最後にCDK Localをインストール

https://github.com/localstack/aws-cdk-local

:::note
**CDK Localとは？**
こちらもAWS Localと同じようにAWS CDKのエンドポイントをLocalStackのコンテナに向けたツール
:::

```bash
npm install -g aws-cdk-local aws-cdk
cdklocal --version
```

こちらは特に設定する必要はなし
普通にCDKでinitしたプロジェクト以下で、CDKの代わりに`cdklocal`でコマンド実行するだけでOK

# コードを実装
↓にサンプルを用意したので適当にClone

https://github.com/damepo9696/localstack-example

:::note warn
**実装のポイント**
基本的には普通にAWSを使うときの感覚で実装すればOK
ただし、**AWS SDKのエンドポイント指定に注意！**
触りに行くリソース(今回の場合はS3バケット)がAWSクラウド上ではなくLocalStackにあるので、エンドポイントをそちらに向けてあげる必要がある

さらに、S3の場合は[URIの指定方法が2通りある](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html)関係上、`forcePathStyle`というプロパティをtrueにしないといけない(デフォルトではvirtual-hosted-styleで処理されるが、LocalStackで使うときはpath-styleじゃないとダメ)

**とりあえずこうやって書けば動く**

```typescript
function initS3Client() {
  const localStackHostName = process.env["LOCALSTACK_HOSTNAME"];
  const edgePort = process.env["EDGE_PORT"];
  // LocalStack環境じゃない時のハンドリングはよしなに
  const endpoint = `https://${localStackHostName}:${edgePort}`;
  return new S3Client({
    endpoint,
    forcePathStyle: true,
  });
}
```

なお、DynamoDBなどの場合は`forcePathStyle`がどうのこうの関係なく、↓のように設定してあげればOK
```typescript
const dynamoDBClient = new DynamoDBClient({endpoint});
```
:::

# LocalStackへデプロイ

## bootstrap
CDKを初めて使う場合はbootstrapコマンドで`cdk-xxx-assets-{ACCOUNT}-{REGION}`というバケットを作る必要があるので実行する

```bash
cdklocal bootstrap
awslocal s3 ls
2022-09-10 17:50:37 cdk-hnb659fds-assets-000000000000-ap-northeast-1
```

:::note warn
localstackのコンテナを停止すると**デプロイしたリソースもすべて揮発する**(＝再度bootstrapからやり直しになる)ので注意！
:::

## デプロイ

```bash
cdklocal deploy LocalstackStack
awslocal cloudformation describe-stacks \
  --stack-name LocalstackStack
```

## テスト
Lambdaを実行して期待通りバケットにファイルが置かれているか確認する

```bash
# Lambda実行
awslocal lambda invoke \
  --function-name test-function	\
  --payload '{ "file": "test", "key": "test.txt" }' \
  --cli-binary-format raw-in-base64-out \
  response.json

# バケットにファイル置かれてるか見に行く
awslocal s3api get-object \
  --bucket test-bucket
  --key test.txt \
  response.json
```