【AWS】LocalStackを使ってみる

2022/7/13にAWSの[LocalStackがバージョン1.0に到達してGAになった](https://localstack.cloud/blog/2022-07-13-announcing-localstack-v1-general-availability/)ので、簡単に使い方を確認したいと思います。
この記事では環境構築にコンテナを利用します。Docker, Docker Composeをご準備ください。

# 環境

OSはWindows11です。
(Docker, Docker Composeが使用できるならば、その他OSでも動作するように考慮しているつもりですが、動作確認ができていないです...。)
![env.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166959/83061e13-d52e-ed8a-4697-311324f886fb.png)
```bash:Windows
$ ver
Microsoft Windows [Version 10.0.22000.856]
```

![docker.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166959/f40ea409-0740-9960-56e8-c051bd4d8113.png)

```bash:Docker
$ docker -v
Docker version 20.10.17, build 100c701
```

![logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166959/1f565d35-1424-98af-75ab-cd497a3c35f0.png)
引用元 | https://github.com/docker/compose

```bash:Docker Compose
$ docker-compose -v
docker-compose version 1.29.2, build 5becea4c
```

# LocalStackとは？

LocalStackは、AWSのリソースをローカルで再現できます。
オフラインでAWSに接続できない場合でも、AWSリソースのモックとして使用することで、開発やテストをできる便利なツールです。

## flavorによる機能の違い

LocalStackには料金プランとして数種類のflavorがあります。
![localstack_pricing_20220829.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166959/3e22989b-e509-ca38-e224-72a778b1ba8f.png)
引用元 | [LocalStack - Pricing](https://localstack.cloud/pricing/)

Community版は無料で利用できますが、使用できるAWSリソースが一部に限られます。また、データの永続化はできません。
Pro版はすべてのAWSリソースとデータの永続化が可能です。

:::note info
**データ永続化について**
[flavorによる機能の違い](#flavorによる機能の違い)で紹介した通り、Community版のデータの永続化はできません。しかし、バージョン0.13.0まではサポートされていました。それ以前の旧バージョンを使用することで可能です。
基本的には最新版を使用する方がよいと思いますが、データの永続化を行いたい場合は、後述する環境構築の[LocalStackのダウンロード](#localstack)の手順で0.13.0以前のバージョンを指定して使用してください。
:::

# 環境構築

## 演習用資材

ソースコードは[私のGitHubの`localstack-exercise`](https://github.com/Shoma-progr-0210/localstack-exercise)にあります。
ローカルの任意のフォルダにダウンロードします。

```bash:Gitコマンド - Shoma-progr-0210/localstack-exercise
git clone https://github.com/Shoma-progr-0210/localstack-exercise.git
```

```text:Zipファイルでダウンロード - Shoma-progr-0210/localstack-exercise
https://github.com/Shoma-progr-0210/localstack-exercise/archive/refs/heads/main.zip
```

## LocalStack

![LocalStack Logo](https://localstack.cloud/images/header-logo-new.svg)
引用元 | https://localstack.cloud/

ダウンロードした`localstack-exercise`の直下LocalStackもダウンロードします。
```bash:Gitコマンド - localstack/localstack
git clone https://github.com/localstack/localstack.git
```

```text:Zipファイルでダウンロード - localstack/localstack
https://github.com/localstack/localstack/archive/refs/heads/master.zip
```

フォルダ構成が以下になっていればOKです。

```bash:フォルダ構成
localstack-exercise <- Shoma-progr-0210/localstack-exercise
├─exercises
├─exercise_container
│  └─appendix
├─localstack <- localstack/localstack
...
```

# 使ってみる

環境構築ができたら、いくつかのAWSリソースを作成してみます。
今回は最終的に以下のような**SQS + Lambda + S3**のサーバレスアーキテクチャのシステムをローカルで再現します。

1. AWS CLIでSQSキューにメッセージを送信する
1. Lambda関数がSQSキューをポーリングしてメッセージを処理する
1. Lambda関数がメッセージ処理結果をファイルとしてS3に保存する
1. AWS CLIでS3からファイルをダウンロードして確認する

![serverless.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166959/7d0b626d-fcf7-b32f-7f25-ef622f6a9381.png)

## 最初にすること

![AWSCLI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166959/3d38c4ce-19a3-7f6a-da56-b1b973408c73.png)
まず、`localstack`コンテナと`exercise`コンテナを起動します。

```bash:コンテナの起動
# localstackコンテナの起動
docker-compose -f ./localstack/docker-compose.yml up -d
# exerciseコンテナの起動
docker-compose -f ./exercise_container/docker-compose.yml up -d
```

`exercise`コンテナに入ります。AWS CLIが使用できるLinux環境です。

```bash:exerciseコンテナに入る
docker exec -it exercise /bin/bash
```

:::note info
これ以降のコマンドはexerciseコンテナ内です。
:::

LocalStackはエンドポイントURLが`http://localhost:4566`ですが、コンテナ内からホストへの接続のため`http://host.docker.internal:4566`です。こちらは事前に`exercise`コンテナの`docker-compose.yml`で設定しているので、確認します。

```bash:エンドポイントURLの確認
echo $ENDPOINT_URL
```

LocalStackで使用するプロファイルを作成します。プロファイル名は`localstack`とします。
LocalStackを使用する場合は`Access Key ID`と`Secret Access Key`は適当な値で構いません。

```bash:プロファイルの作成
aws configure --profile=localstack
AWS Access Key ID [None]: dummy
AWS Secret Access Key [None]: dummy
Default region name [None]: ap-northeast-1
Default output format [None]: json
```

あらかじめ使用する値を環境変数として設定しておきます。
LocalStackはAWS アカウント IDが`000000000000`で固定です。

|環境変数名|説明|
|:-:|:-:|
|SQS_QUEUE_NAME|SQSのキュー名|
|SQS_QUEUE_URL|SQSのキューのURL|
|SQS_QUEUE_ARN|SQSのキューのARN|
|S3_BUCKET_NAME|S3のバケット名|
|LAMBDA_FUNCTION_NAME|Lambdaの関数名|
|CLOUDWATCH_LOG_GROUP_NAME|Lambdaのログが記録されるCloudWatchロググループ名|

```bash
export SQS_QUEUE_NAME=words-queue
export SQS_QUEUE_URL=$ENDPOINT_URL/000000000000/$SQS_QUEUE_NAME
export SQS_QUEUE_ARN=arn:aws:sqs:ap-northeast-1:000000000000:$SQS_QUEUE_NAME
export S3_BUCKET_NAME=words-bucket
export LAMBDA_FUNCTION_NAME=words-lambda
export CLOUDWATCH_LOG_GROUP_NAME=/aws/lambda/$LAMBDA_FUNCTION_NAME
```

## SQSの作成

![sqs.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166959/60f45e61-49ff-6fce-ee3b-f4eaa03edd5b.png)
SQSキューを作成します。

```bash:SQSキューの作成
aws sqs create-queue --queue-name $SQS_QUEUE_NAME --endpoint-url=$ENDPOINT_URL --profile localstack
```

キューの一覧は以下のコマンドで確認できます。`words-queue`があると思います。

```bash:キューの一覧
aws sqs list-queues --endpoint-url=$ENDPOINT_URL --profile localstack
```

```bash:結果
{
    "QueueUrls": [
        "http://host.docker.internal:4566/000000000000/words-queue"
    ]
}
```

## S3の作成

![s3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166959/ae85dd15-0abe-6636-bbb9-bc8d7a33065a.png)
Lambda関数がSQSキューを処理した結果ファイルをアップロードするS3バケットを作成します。

```bash:S3バケットの作成
aws s3 mb s3://$S3_BUCKET_NAME --endpoint-url=$ENDPOINT_URL --profile localstack
```

バケットの一覧は以下のコマンドで確認できます。`words-bucket`があると思います。

```bash:S3バケットの一覧
aws s3 ls --endpoint-url=$ENDPOINT_URL --profile localstack
```

```bash:結果
2022-08-29 12:30:26 words-bucket
```

## Lambdaの作成

![Lambda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166959/abf097c9-5570-06a3-b866-a86ed246d555.png)
SQSキューを処理して結果ファイルをS3にアップロードするLambda関数を作成します。
Lambda関数にアップロードするスクリプトは`/exercise/appendix/`にある`process_messages.zip`です。中身は`process_messages.py`です。
Lambda関数からS3に接続するために、必要な値を`--environment`で環境変数として設定しています。
プロファイル同様に権限系の`--role`は適当で構いません。

```bash:Lambda関数の作成
aws lambda create-function \
    --function-name $LAMBDA_FUNCTION_NAME \
    --runtime python3.9 \
    --zip-file fileb:///exercise/appendix/process_messages.zip \
    --role test-role \
    --handler process_messages.lambda_handler \
    --environment "Variables={AWS_ACCESS_KEY_ID=dummy,AWS_SECRET_ACCESS_KEY=dummy,REGION_NAME=ap-northeast-1,ENDPOINT_URL=$ENDPOINT_URL,BUCKET_NAME=$S3_BUCKET_NAME}" \
    --endpoint-url=$ENDPOINT_URL --profile localstack
```

Lambda関数の一覧は以下のコマンドで確認できます。`words-lambda`があると思います。

```bash:Lambda関数の一覧
aws lambda list-functions --endpoint-url=$ENDPOINT_URL --profile localstack
```

```bash:結果
{
    "Functions": [
        {
            "FunctionName": "words-lambda",
            "FunctionArn": "arn:aws:lambda:ap-northeast-1:000000000000:function:words-lambda",
            "Runtime": "python3.9",
            "Role": "test-role",
            "Handler": "process_messages.lambda_handler",
...省略
```

## SQSに対するLambdaのマッピングの作成

![mapping.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166959/07ac821a-3f10-a8cc-bd20-fd7c0cba643b.png)
LambdaがSQSキューをイベントとして受け取れるようにイベントソースマッピングを作成します。
`--batch-size`は1回のイベントで受け取るSQSキューの数です。

```bash
aws lambda create-event-source-mapping \
    --function-name $LAMBDA_FUNCTION_NAME \
    --batch-size 2 \
    --event-source-arn $SQS_QUEUE_ARN \
    --endpoint-url=$ENDPOINT_URL --profile localstack
```

## 全体動作確認

これですべてのリソースを作成したので、動作確認をしてみましょう。
SQSキューにメッセージを送信するとLambda関数が処理してS3に結果ファイルがアップロードされるはずです。

`/exercise/appendix/sqs_input_msg_00x.json`を使用してメッセージを送信します。

```bash:SQSキューにメッセージ送信
aws sqs send-message --queue-url $SQS_QUEUE_URL --message-body file:///exercise/appendix/sqs_input_msg_001.json --endpoint-url=$ENDPOINT_URL --profile localstack
aws sqs send-message --queue-url $SQS_QUEUE_URL --message-body file:///exercise/appendix/sqs_input_msg_002.json --endpoint-url=$ENDPOINT_URL --profile localstack
aws sqs send-message --queue-url $SQS_QUEUE_URL --message-body file:///exercise/appendix/sqs_input_msg_003.json --endpoint-url=$ENDPOINT_URL --profile localstack
```

数十秒後にS3バケットを確認しましょう。
`s3 ls`コマンドで`s3://{S3バケット名}`を指定することでS3バケット内の全てのオブジェクトを確認できます。`queue_output_msg_{メッセージID}.json`の名前のファイルが3つあるはずです。

```bash:S3バケット内のオブジェクト一覧
aws s3 ls s3://$S3_BUCKET_NAME --endpoint-url=$ENDPOINT_URL --profile localstack
```

```bash:結果
2022-08-29 13:40:50         82 queue_output_msg_3c5bb5e4-913a-405c-8b62-5b5f50980a98.json
2022-08-29 13:40:50         84 queue_output_msg_be389e20-9422-4a66-a570-33a5243f02cb.json
2022-08-29 13:40:50         94 queue_output_msg_e3b02c32-0eda-4d70-9ee1-f71e37684d69.json
```

それらのファイルをダウンロードして確認できます。
ダウンロード先は`/tmp/exercise/`にします。ホスト側の`./exercise_container/tmp/`にボリュームマウントしてあるのでホスト側でも確認できます。
ダウンロードしたファイルのJSONに`sentence`フィールドがあればOKです。

```bash:S3バケットのオブジェクトダウンロード
aws s3 cp s3://$S3_BUCKET_NAME/ /tmp/exercise/ --recursive --endpoint-url=$ENDPOINT_URL --profile=localstack
```

```json:結果
{"id": "001", "name": "Alice", "word": "What's up?", "sentence": "Alice said, \"What's up?\""}
{"id": "002", "name": "Bob", "word": "Hello.", "sentence": "Bob said, \"Hello.\""}
{"id": "003", "name": "Charlie", "word": "Hi!", "sentence": "Charlie said, \"Hi!\""}
```

# おわりに

「使ってみる」と軽く題をつけて始めたものの、結構重めの内容となってしまいました。
サーバレスのAWSリソースはローカルでモックを用意しづらいイメージだったのと、SQSを学んでみたかったので、LocalStackを使ってみて良かったと思います。
ただ、無料のCommunity版だとデータの永続化ができないのはショックでした。ライトユーザーはAWSの無料枠でカバーしているのかもしれないですね。
プロファイルや権限周りを無視して試せる手軽さがあるので、これからもAWSリソースを気軽に試したいときに利用してみようと思います。