🐻‍❄️

LocalStackはいいぞ!

2024/12/28に公開

※勝手に宣伝してるだけです。

はじめに

AWSに強く依存したプロダクトを開発する際、どのように開発環境を構築するでしょうか?
S3なら、MinIO?SESなら?SQSなら?
もしかしたら、AWSに共有の開発環境用のリソースを作成している場合もあるかもしれません。

個別の代替サービスを導入するには、選定や維持に手間がかかります。
AWSに共有の開発環境用のリソースを設置する場合も、費用的問題が大きいと思います。
(休日や夜間は停止するとはいえ。

LocalStackとは

LocalStackについての説明を公式ページから引用します。

Develop and test your AWS applications locally to reduce development time and increase product velocity. Reduce unnecessary AWS spend and remove the complexity and risk of maintaining AWS dev accounts.
https://www.localstack.cloud/

↓ざっくり日本語訳

AWSアプリケーションをローカルで開発・テストすることで、開発時間を短縮し、製品開発のスピードを向上させることができます。不要なAWSの支出を削減し、AWS開発アカウントの維持に伴う複雑さとリスクを排除することができます。

要するに、AWSのサービスをローカルで再現してくれるサービスです。

https://www.localstack.cloud/

↓(おそらく)Community EditionのGitHub Repository
https://github.com/localstack/localstack

Community EditionとPro Editionの違い

端的にいうと、「永続化の有無」「サービスの選択肢」です。

Community Editionですとデータの永続化ができません。
例えば、S3のバケットそしてその中のデータはdocker compose downすると消えてしまいます。

また、サービスの選択肢にも違いが出てきます。
現状、主に以下のようなサービスが制限 or Pro Editionでのみ使用可能です。
※最新の情報は公式ドキュメントを参照してください。

  • Pro Editionのみ
    • Amplify
    • API Gateway v2
    • CloudFront
    • CloudTrail
    • ELB, ELB v2
  • 制限
    • SES: SMTP経由でのメール送信
    • Lambda: Custom Image

Pro Editionいくらや

※最新の情報は公式ドキュメントを参照してください。

Starter Teams
料金 $35/month/user $70/month/user

快適な開発環境 & AWSの費用がかからないと考えれば、月5000円/ユーザー前後の出費は個人的には許容範囲かな。と感じてます。

AWS上に開発環境のマスタデータを用意しておいて、docker composer upと同時にそれらを同期する。というような実装にすれば、Community Editionでもいいのかな。。?
(開発環境内のデータが消えてしまうというリスクがありますが。

自分も一応課金してます。

一応。。。ね?

実際のユースケース

S3で署名付きURLを用いる場合

例えば、LambdaでLambda Web Adapterを用いている場合、Lambdaのレスポンス制限がかかってしまいます。
そういう時はS3の署名付きURL(GetObjectCommand, PutObjectCommand)を発行するケースが多いと思います。
(S3の公開バケットにすることもあると思いますが。。

手っ取り早く試したい人向け

https://github.com/kyoya0819/zenn-20241228

にコードサンプルをご用意しています。

#S3で署名付きURLを生成するセクション等をご覧ください。

docker-compose.yaml

Community Edition
services:

    localstack:
        container_name: app-localstack
        image: localstack/localstack
        ports:
            - "4566:4566"
        environment:
            - SERVICES=s3
            - PERSISTENCE=1
            - DISABLE_CORS_CHECKS=1
            - CORS_ALLOW_ORIGINS=*
        volumes:
            -   type: volume
                source: app-localstack
                target: /var/lib/localstack
            -   type: bind
                source: ./.docker/development/localstack
                target: /etc/localstack/init
            -   type: bind
                source: /var/run/docker.sock
                target: /var/run/docker.sock

volumes:
    app-localstack:
Pro Edition
services:

    localstack:
        container_name: app-localstack
        image: localstack/localstack-pro
        ports:
            - "4566:4566"
        environment:
            - SERVICES=s3
            - PERSISTENCE=1
            - DISABLE_CORS_CHECKS=1
            - CORS_ALLOW_ORIGINS=*
            - LOCALSTACK_AUTH_TOKEN=秘密だよ
        volumes:
            -   type: volume
                source: app-localstack
                target: /var/lib/localstack
            -   type: bind
                source: ./.docker/development/localstack
                target: /etc/localstack/init
            -   type: bind
                source: /var/run/docker.sock
                target: /var/run/docker.sock

volumes:
    app-localstack:

ちょびっと解説すると、

SERVICES=s3

ここでサービスを指定しています。
例えば、S3とSESを使いたい場合は以下のような指定になります。

SERVICES=s3,ses

.env

NODE_ENV=development

# AWS Common Config
AWS_REGION=ap-northeast-1
AWS_ENDPOINT_URL=http://localhost:4566
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test

# AWS S3 Config
S3_BUCKET=test

AWS_ENDPOINT_URL:今回Dockerで動作しているLocalStackでAWSへのリクエストをフックしたいです。そこで、AWS_ENDPOINT_URLにLocalStackのエンドポイントを指定することで、SDK側でリクエストする先を変更してくれます。
参考:https://docs.aws.amazon.com/ja_jp/sdkref/latest/guide/feature-ss-endpoints.html

AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY:今回、LocalStackでは認証が必要ないため、適当な値を入れています。本当に適当な値で良いです。

コード

push
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";

const s3client = new S3Client({
    forcePathStyle: process.env.NODE_ENV === "development" ? true : undefined
});

const key = "test.txt";
const body = "This is body.";

await s3client.send(new PutObjectCommand({
    Bucket: process.env.S3_BUCKET,
    Key: key,
    Body: body
}));

console.log("success");
forcePathStyle: process.env.NODE_ENV === "development" ? true : undefined

LocalStack側の都合により、PathStyleを強制する必要があります。
LocalStack的にはS3向けのアクセスのエンドポイントはhttps://s3.localhost.localstack.cloud:4566が推奨です。
これを用いる場合は、forcePathStyleの設定は不要です。
一方、そうするとS3向けのエンドポイントの設定項目を環境変数で持つ必要があってめんどくさいので、この方法はとっていません。
https://docs.localstack.cloud/user-guide/aws/s3/#path-style-and-virtual-hosted-style-requests

getSignedUrl
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3client = new S3Client({
    forcePathStyle: process.env.NODE_ENV === "development" ? true : undefined
});

const key = "test.txt";

const command = new GetObjectCommand({
    Bucket: process.env.S3_BUCKET,
    Key: key
});

const signedUrl = await getSignedUrl(
    s3client,
    command,
    {
        expiresIn: 60 * 60 * 24 * 7 // 1週間 + 5分
    }
);

console.log(signedUrl);

ここは特に注釈が必要な点はないかな。と思ってます。

SESでメールを送信する場合

バッチ処理を組む場合が多いと思うので、あんまりこれを直で実装するケースは多くないと思いますが。。?

手っ取り早く試したい人向け

https://github.com/kyoya0819/zenn-20241228

にコードサンプルをご用意しています。

#SESでメールを送信するセクション等をご覧ください。

docker-compose.yaml

Community Edition
services:

    localstack:
        container_name: app-localstack
        image: localstack/localstack
        ports:
            - "4566:4566"
        environment:
            - SERVICES=ses
            - PERSISTENCE=1
            - DISABLE_CORS_CHECKS=1
            - CORS_ALLOW_ORIGINS=*
        volumes:
            -   type: volume
                source: app-localstack
                target: /var/lib/localstack
            -   type: bind
                source: ./.docker/development/localstack
                target: /etc/localstack/init
            -   type: bind
                source: /var/run/docker.sock
                target: /var/run/docker.sock

volumes:
    app-localstack:
Pro Edition
services:

    localstack:
        container_name: app-localstack
        image: localstack/localstack-pro
        ports:
            - "4566:4566"
        environment:
            - SERVICES=ses
            - PERSISTENCE=1
            - DISABLE_CORS_CHECKS=1
            - CORS_ALLOW_ORIGINS=*
            - LOCALSTACK_AUTH_TOKEN=秘密だよ
        volumes:
            -   type: volume
                source: app-localstack
                target: /var/lib/localstack
            -   type: bind
                source: ./.docker/development/localstack
                target: /etc/localstack/init
            -   type: bind
                source: /var/run/docker.sock
                target: /var/run/docker.sock

volumes:
    app-localstack:

ほぼ、S3の例と同じです。
SERVICESが変わっているだけです。

.env

NODE_ENV=development

# AWS Common Config
AWS_REGION=ap-northeast-1
AWS_ENDPOINT_URL=http://localhost:4566
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test

S3の例と全く同じです。

コード

sendEMail
import { SendEmailCommand, SESClient, VerifyEmailIdentityCommand } from "@aws-sdk/client-ses";

// 変える場合は、 /.docker/development/localstack/ready.d も編集してください。
const from = "noreply@example.com";

const to = "hello@example.com";
const subject = "Hello, World!";
const body = "Hello, World! This is Body!!!";

const sesClient = new SESClient();

await sesClient.send(new VerifyEmailIdentityCommand({
    EmailAddress: from
}));

// メール送信処理実行
await sesClient.send(new SendEmailCommand({
    Source: from,
    Destination: {
        ToAddresses: [to]
    },
    Message: {
        Subject: {
            Data: subject
        },
        Body: {
            Text: {
                Data: body
            }
        }
    }
}));

console.log("success");

普段から、AWSのSDKを用いた開発をされている方には馴染みのある、特段特徴のないコードかなぁと思います。

結論

LocalStackはいいぞ!

https://github.com/kyoya0819/zenn-20241228

あとがき

めちゃくちゃ遅いですが本記事は「Nihon University Advent Calendar 2024」の15日目の記事となります。

Discussion