LocalStack On Docker

以前からLocalStackのPro版を使っていたのですが、忘れっぽいので記事にしたいと思います。今回はDocker上にLocalstack(pro)を立てて、Lambda・SNSを作成し外部端末からLocalstackのSNSトピック宛てメッセージを送ってみたいと思います。Lambda関数は外部メールを送信するようセットしておきます。

<<イメージ図>>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335933/d888c9d8-a3ba-ed65-41c5-3be6698a47ec.png)

# 前提
ホストOS：Ubuntu 22.04.1 LTS
Docker version: 27.0.3, build 7d4bcd8
Docker Compose version: v2.28.1

# 1. Dockerコンテナ構築
まずは任意のディレクトリを作成し、そこにdocker-compose.ymlを準備します。
※マニュアル記載の内容からアドレスのみ変更(ホストOSのIP)に変更します。
```yaml
version: "3.8"

services:
  localstack:
    container_name: "${LOCALSTACK_DOCKER_NAME:-localstack-main}"
    image: localstack/localstack-pro  # required for Pro
    ports:
      - "10.x.x.x:4566:4566"            # 自分の環境に合わせる
      - "10.x.x.x:4510-4559:4510-4559"  # 自分の環境に合わせる
      - "10.x.x.x:443:443"              # 自分の環境に合わせる
    environment:
      # Activate LocalStack Pro: https://docs.localstack.cloud/getting-started/auth-token/
      - LOCALSTACK_AUTH_TOKEN=${LOCALSTACK_AUTH_TOKEN:?}  # required for Pro
      # LocalStack configuration: https://docs.localstack.cloud/references/configuration/
      - DEBUG=${DEBUG:-0}
      - PERSISTENCE=${PERSISTENCE:-0}
    volumes:
      - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"

```

コンテナを起動する前にAuthTokenを確認します。
LocalStack管理画面のWorkspace -> Auth Token から確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335933/c267f1bc-3732-266e-fd2a-156143096310.png)

AuthTokenが確認できたら、以下コマンドでAuthTokenをexportしたのちにコンテナを起動します。

```bash
export LOCALSTACK_AUTH_TOKEN="ls-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
docker compose up -d
```

# 2. 管理画面からステータスを確認する。
以下、URLにアクセスします。
https://app.localstack.cloud/inst/default/status

画面右上に表示されているEditボタンをクリックして、EndpointのIPをホストOSのIPに変更しておきます。正しく反映されるとステータスがrunningになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335933/98bd09e7-ffec-61ed-bfc3-2f8130b81c99.png)

# 3. Lambda関数作成
続いて、LocalstackのコンテナにてLambda関数を登録していきます。コンテナにログインしてコマンドを実行てLambda関数用のコードを作成します。

```bash
docker exec -it localstack-main /bin/bash
apt update
apt install vim #ファイル作成の為インストールしておく
vim send-e-mail.py
```

```python:send-e-mail.py
import json
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import os

def lambda_handler(event, context):
    sns_message = event['Records'][0]['Sns']['Message']

    sender_email = 'xxxxxxxx@example.com'     # 送信アドレス
    recipient_email = 'xxxxxxxx@example.com'  # 宛先アドレス
    smtp_server = 'example.com'               # メールサーバ
    smtp_port = '587'                         # Port番号
    smtp_user = 'xxxxxxxx@example.com'        # ユーザ
    smtp_password = 'XXXXXXXXXX'              # パスワード

    msg = MIMEMultipart()
    msg['From'] = sender_email
    msg['To'] = recipient_email
    msg['Subject'] = 'SNS Notification'
    msg.attach(MIMEText(sns_message, 'plain'))

    try:
        with smtplib.SMTP(smtp_server, smtp_port) as server:
            server.starttls()
            server.login(smtp_user, smtp_password)
            server.sendmail(sender_email, recipient_email, msg.as_string())

        return {
            'statusCode': 200,
            'body': json.dumps('Email sent successfully!')
        }

    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps(f'Failed to send email: {str(e)}')
        }
```

次にIAMロールの登録に必要なJSONファイルをvimで作成しておきます。

```json:trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

では、以下コマンドを順に実行してIAMロール作成します。
```bash
# IAMロールを作成
awslocal iam create-role --role-name lambda-role --assume-role-policy-document file://trust-policy.json

# IAMロールにポリシーをアタッチ
awslocal iam attach-role-policy --role-name lambda-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

# ロールのARNを確認
awslocal iam get-role --role-name lambda-role
```

作成したロールを使ってLambda関数を登録します。登録する前にコードを圧縮しておいて、圧縮したファイルを登録する形になります。

```bash
# 作成したコードを圧縮
zip send-e-mail.zip send-e-mail.py

# Lambda関数を登録
awslocal lambda create-function --function-name send-e-mail --runtime python3.10 --role arn:aws:iam::000000000000:role/lambda-role --handler send-e-mail.lambda_handler --zip-file fileb://send-e-mail.zip
```

無事作成されるとLocalStack管理画面から状況を確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335933/8d3a0b66-4f31-69a6-9c6f-ac30359fcbc7.png)


# 4. SNSトピック作成
次はSNSトピックを作成します。コマンドで作成してもよいのですが、管理画面からも作成できるのでこちらから実施したいと思います。Status画面を開き、SNSをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335933/26141fd0-ccd8-d7de-9462-8ed9e888bf89.png)

Create Topicをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335933/be4b97c3-222f-b6aa-620c-359be10c7fdb.png)

SNSトピック名を入れてSubmitボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335933/1c9324ce-9256-0b0c-7503-cb2903bc4117.png)

作成されたトピックをクリックして、Subscriptionsをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335933/47edd6a1-8500-43e5-4b6e-f350cd0aea79.png)

Create Subscriptionをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335933/7c136cd4-53bb-c198-3a47-e2a603dc084b.png)

ProtocolにLambdaとして、Endpointには作成されたLambda関数のARNを入れて、画面右したのSubmitをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335933/2f65ba51-bbdd-d384-7e3d-4ff4d9e70c8f.png)

これで、Lambda関数とSNSトピックが紐づきました。

試しにDockerコンテナから以下のコマンドを実行してLambda関数が実行されるかどうか確認してみましょう

```bash
awslocal sns publish --topic-arn arn:aws:sns:us-east-1:000000000000:test --message "Hello, this is a test message"
```
LocalStack管理画面からCloudWatch Logsを見ると実行結果がわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335933/c2e01ea1-03b3-a334-6383-64ca6e39cf15.png)

# 5. 外部端末からSNSトピック宛てにアクション
それでは最後に、外部端末からSNSトピック宛てに通信させてみたいと思います。

```python:send-message.py
import boto3
from botocore.config import Config
from botocore.exceptions import NoCredentialsError

# LocalStackエンドポイントを指定
my_config = Config(
    region_name = 'us-east-1',
    signature_version = 'v4',
    retries = {
        'max_attempts': 10,
        'mode': 'standard'
    },
)

# dummy認証情報(これが無いとエラーとなる)
session = boto3.Session(
    aws_access_key_id='dummy',
    aws_secret_access_key='dummy',
    region_name='us-east-1'
)

sns = session.client('sns', endpoint_url='http://10.x.x.x:4566', config=my_config)

topic_arn = 'arn:aws:sns:us-east-1:000000000000:test'

response = sns.publish(
    TopicArn=topic_arn,
    Message='Hello from another Linux OS!',
    Subject='Test Message'
)

print(response)
```

無事メールが送信されてます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335933/f4b0c408-08b2-69ed-3d5d-64b37611bfdb.png)

SNS/Lambdaなどを使って他システムと連携したりするパターンもあるかと思いますが、Localstackを利用すればある程度の事がローカル環境で実現できるので、非常に便利です。