LocalStack環境をネットワーク内で共有する

業務上、開発用のAWS環境でも動作確認レベルの資材を気楽にデプロイできないことは多いですよね。
以前からLocalStackでS3やDynamoDBを立ち上げて動作確認をしていましたが、気付くと他のメンバーの成果物の動作確認を依頼されるようになっていました...
そこで、ネットワーク内の別マシンからも資材配置&アクセスできるLocalStack環境を構築してみました。

ちょうど2023/03/31に[LocalStack v2.0.0](https://github.com/localstack/localstack/releases/tag/v2.0.0)がリリースされていたので、仕様が変わった点や環境構築中に詰まったポイントも含めて構築手順をまとめます。

## 環境構築(サーバ)

LocalStack公式のDockerコンテナ起動方法は以下の5通りあります。
今回は**LocalStack CLI**を使っていきます。

- **LocalStack CLI**
- LocalStack Cockpit
- Docker
- Docker-Compose
- Helm

### 前提ソフトウェア

- Python(3.7~3.10)
- pip
- Docker

```shell-session
$ python --version
Python 3.8.2
$ pip --version
pip 23.0.1 from /usr/local/var/pyenv/versions/3.8.2/lib/python3.8/site-packages/pip (python 3.8)
$ docker --version
Docker version 20.10.23, build 7155243
```

### LocalStackインストール

```shell-session
$ pip install localstack
$ localstack --version
2.0.0.post1

# バージョンを指定してインストールする場合
# pip install localstack==<version>
```

### LocalStack起動

```shell-session
$ GATEWAY_LISTEN=0.0.0.0:4566 localstack start
```

ネットワーク内の別マシンからアクセスできるように環境変数`GATEWAY_LISTEN=0.0.0.0:4566`を設定しています。

:::note warn
以前はIPとポート指定がそれぞれ別の環境変数でしたが、localstack v2.0.0から1つに統合されているので注意が必要です。
[LocalStack v2.0.0リリースノート](https://github.com/localstack/localstack/releases/tag/v2.0.0)より
> We are unifying the variables EDGE_BIND_HOST, EDGE_PORT and EDGE_PORT_HTTP into GATEWAY_LISTEN,
:::
:::note info
以前は環境変数`DATA_DIR`にファイルパスを指定することでデータの永続化ができましたが、[LocalStack v1.0.0](https://github.com/localstack/localstack/releases/tag/v1.0.0)以降はPro版限定の機能となりました。
コンテナを止める度に構築した資材が消えてしまうので注意が必要です。
Pro版であれば`PERSISTENCE=1`でデータ永続化できます。
:::

LocalStack起動中にヘルスチェック用エンドポイントにHTTPアクセスすると、バージョン情報と利用可能なAWSサービスを一覧表示できます。

2023/04/05時点では以下の結果になりました。
今回はCommunity版を使用しているので、Pro版限定機能のAthenaやECSは表示されていません。

```shell-session
$ curl -s http://localhost:4566/_localstack/health | jq .
{
  "services": {
    "acm": "available",
    "apigateway": "available",
    "cloudformation": "available",
    "cloudwatch": "available",
    "config": "available",
    "dynamodb": "available",
    "dynamodbstreams": "available",
    "ec2": "available",
    "es": "available",
    "events": "available",
    "firehose": "available",
    "iam": "available",
    "kinesis": "available",
    "kms": "available",
    "lambda": "available",
    "logs": "available",
    "opensearch": "available",
    "redshift": "available",
    "resource-groups": "available",
    "resourcegroupstaggingapi": "available",
    "route53": "available",
    "route53resolver": "available",
    "s3": "available",
    "s3control": "available",
    "secretsmanager": "available",
    "ses": "available",
    "sns": "available",
    "sqs": "available",
    "ssm": "available",
    "stepfunctions": "available",
    "sts": "available",
    "support": "available",
    "swf": "available",
    "transcribe": "available"
  },
  "version": "2.0.1.dev"
}
```

## 環境構築(クライアント)

- AWS CLI (awsコマンド)
  - 公式の手順に従ってインストール
  - [AWS CLI の最新バージョンをインストールまたは更新します。](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html)
- [任意] awscli-local (awslocalコマンド)
  - AWS CLIのラッパーコマンド
  - 毎度`--endpoint-url=http://{LocalStackホストのIP:Port}`を指定する手間が省ける
  - 別ホストのLocalStackにアクセスする場合、環境変数`LOCALSTACK_HOST={ホスト名orIP}`を設定する
  ```shell-session
  $ pip install awscli-local
  ```

LocalStackサーバのIPをendpoint-urlに指定することで別マシンから資材作成できます。

```shell-session
> aws s3 mb s3://sample-bucket --endpoint-url=http://192.168.0.149:4566 --no-sign-request
make_bucket: sample-bucket
```

## CloudFormationでの資材構築

コンテナを止める度に手動で資材を作成し直すのは手間なのでCloudFormationで資材を構築します。
今回作成する資材は以下の2つです。

- API Gateway(v1)
  - v2はPro版限定
  - GETメソッドのみ実装
- Lambda
  - API GatewayへのGETリクエストをトリガとして起動 & 固定レスポンス`OK!`を返却

![localstack構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3329977/d5d47a30-e5a5-2693-c16f-8842138372b0.png)

```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  FuncName:
    Type: String
    Default: TestFunction

Resources:
  lambda_function:
    Type: AWS::Lambda::Function
    Properties:
      Runtime: nodejs18.x
      Role: arn:aws:iam::123456789012:role/lambda-role
      Handler: index.handler
      FunctionName: !Ref FuncName
      Code:
        ZipFile: |
          exports.handler = async function (event, context) {
            return {statusCode: 200, body: 'OK!'}
          }
      Description: test static function.

  gateway:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: TestApiGateway
      Description: test gateway.

  resource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref gateway
      ParentId: !GetAtt gateway.RootResourceId
      PathPart: !Ref FuncName

  method:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref gateway
      ResourceId: !Ref resource
      AuthorizationType: None
      HttpMethod: GET
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${FuncName}/invocations

  deployment:
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId: !Ref gateway
      StageName: test
```

```shell-session
# デプロイ
$ awslocal cloudformation deploy --stack-name test-stack --template-file "./template.yaml"
!NOTE! awslocal does not currently work with the cloudformation package/deploy commands supplied bythe AWS CLI v2. Please run "pip install awscli" to install version 1 of the AWS CLI

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - test-stack

# 作成したAPI Gatewayのidを控えておく
$ awslocal apigateway get-rest-apis
{
    "items": [
        {
            "id": "sxixclx8jo",
            "name": "TestApiGateway",
            "description": "test gateway.",
            "createdDate": "2023-04-12T14:15:15+09:00",
            "apiKeySource": "HEADER",
            "endpointConfiguration": {
                "types": [
                    "EDGE"
                ]
            },
            "tags": {
                "aws:cloudformation:logical-id": "gateway",
                "aws:cloudformation:stack-name": "test-stack",
                "aws:cloudformation:stack-id": "arn:aws:cloudformation:us-east-1:000000000000:stack/test-stack/380cec49"
            },
            "disableExecuteApiEndpoint": false
        }
    ]
}
```

### 動作確認

以下の宛先にHTTPリクエストを送信します。
`http://{LocalStackホストのIP:Port}/restapis/{APIGatewayのID}/{ステージ名}/_user_request_/{関数名}`
```shell-session
$ curl -s http://localhost:4566/restapis/sxixclx8jo/test/_user_request_/TestFunction
OK!%

# これでもいける
$ curl http://wzwpeatdvb.execute-api.localhost.localstack.cloud:4566/test/TestFunction

# https://github.com/localstack/localstack/tree/master/doc/interaction#invoking-api-gateway より
```

## マルチアカウントについて

LocalStackはアカウントIDとリージョンで資材のアクセス制限ができます。

https://docs.localstack.cloud/references/multi-account-setups/

環境変数`AWS_ACCESS_KEY_ID`でアカウントIDを指定できます。
指定がない or 有効でない値※を指定するとアカウントID`000000000000`が使用されます。
※12桁の数値以外

利用者ごとにIDを決めておけば他の人が作った資材に誤アクセスするトラブルを回避できそうです。

## 参考
- https://docs.localstack.cloud/overview/