localstackをもっと使いましょうという話
本記事は、NewsPicks Advent Calendar 2022 の12/20公開分の記事になります。
はじめに
現在開発に携わっているプロダクトではインフラはほとんどAWSを使っていますので、ローカル開発に役立つlocalstackの活用例をユースケースごとにお見せししたいと思います。もともと少しは使っていましたが、最近活用シーンが多くなってきていますので、これからもっと使って行きたいと思います。
localstackとは
一言でいうとAWS環境をローカルでエミュレートできるツール群です。2016年スタートした割と新しいOSSなのですが、すでにGithub star数が4.5K、Dockerのダウンロード数が100万回を超えたポピュラーな開発ツールになって来ました。
2022年にはバージョン1.0が正式にリリースされ様々な機能が追加されましたので、これからもどんどん活用シーンが増えるのではないかと思います。
localstack導入前のローカル開発の事情
ほとんどの開発者は最初にCloud Nativeなアプリケーションを作り始める時には、ローカルでも一通りアプリケーション全体を動かせるような開発体験をしたいと思ったことがあるでしょう。僕もその一人でした。
しかし、簡単には行きませんでした。
- 無理やりmock serviceを作って決まったデータしか取得できないようにしたり
- そもそもローカルで全体の構築をやめてLambdaなどは都度
aws cli
を使ってawsにアップロードして確認する
など、ひと工夫をしないとできませんでした。
そこで見つかったのは、localstackです。そして試しに入れて見たら以外にスムーズに設定できたので、これからも開発の必須ツールとして使って行きたいなと思うことになりました。
localstackの導入
最近ほとんどの開発チームではローカル開発でdocker-composeを利用していると思いますので、
docker-composeで導入します。
version: "3.8"
services:
example-localstack:
container_name: example-localstack
image: localstack/localstack:1.3.1
ports:
- 54566:4566
environment:
- DEFAULT_REGION=ap-northeast-1
- DOCKER_SOCK=/var/run/docker.sock # locakstackでもローカルDocker利用できるようにする設定
- DEBUG=1 # トラブルシューティングに役立つため、DEBUGログをonに設定
volumes:
- example-localstack:/var/lib/localstack/
- /var/run/docker.sock:/var/run/docker.sock
- ./local_script:/etc/localstack/init/ready.d # localstackのDocker起動時に実行される.shスクリプトの置き場
networks:
example-network:
networks:
example-network:
volumes:
example-localstack:
このdocker-compose.ymlファイルを作成し、docker-compose up
コマンドでlocalstackをローカルのDocker上に立ち上げることができます。
活用例
それでは、実際現在localstackを使ってローカルで開発している具体例をお見せしたいと思います。
ユースケース①
API経由でS3にファイルアップロード、ダウンロードするアプリケーション
このアプリケーションの場合、登場するAWSのServiceとしてはS3があります。それでは、localstackでS3の構築をしていきます。
S3バケット作成スクリプト
#!/bin/bash
set -x
awslocal s3 mb s3://example-bucket
set +x
作成スクリプトは作りましたが、localstackが起動時に一緒に作って欲しいですね。ここで上記のdocker-compose.yml
に設定した
- ./local_script/aws:/etc/localstack/init/ready.d # localstackのDocker起動時に実行される.shスクリプトの置き場
local_script
ディレクトリにそのまま作成スクリプトをおきます。すると、localstackは起動時に自動的にそのスクリプト実行して、起動完了したらexample-bucket
が作成された状態になります!
S3バケット作成スクリプトが実行されて成功したDockerのログサンプル
2022-12-19T04:04:14.855 DEBUG --- [ MainThread] localstack.runtime.init : Running READY script /etc/localstack/init/ready.d/01_s3_bucket.sh
+ awslocal s3 mb s3://example-bucket
2022-12-19T04:04:15.220 DEBUG --- [ asgi_gw_0] plugin.manager : instantiating plugin PluginSpec(localstack.aws.provider.s3:default = <function s3 at 0xffffb122f250>)
2022-12-19T04:04:15.220 DEBUG --- [ asgi_gw_0] plugin.manager : loading plugin localstack.aws.provider:s3:default
2022-12-19T04:04:15.224 INFO --- [-functhread6] l.services.motoserver : starting moto server on http://0.0.0.0:40151
2022-12-19T04:04:15.224 INFO --- [ asgi_gw_0] localstack.services.infra : Starting mock S3 service on http port 4566 ...
2022-12-19T04:04:15.356 INFO --- [ asgi_gw_0] localstack.request.aws : AWS s3.CreateBucket => 200
make_bucket: example-bucket
+ set +x
S3はlocalstack上に動くようになりましたので、後は、ソースコード中でS3向き先をhttp://localhost:54566
にすれば、ローカルでもS3を使った開発ができるようになります。
ちなみに、向き先をローカルとAWS環境をそれぞれ使い分ける部分のソースはこんな感じです。
Goバージョンのlocalstack使い分けサンプル
func getS3Client() (*s3.Client, error) {
if true { // サンプルのため常にlocalstackを見るようにしています。何かしら環境がわかるようなflagを参照して使い分けることができると思います。
customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{
URL: "http://localhost:54566",
HostnameImmutable: true,
SigningRegion: "ap-northeast-1",
}, nil
})
cfg, err := config.LoadDefaultConfig(context.TODO(),
config.WithEndpointResolverWithOptions(customResolver),
)
if err != nil {
return nil, err
}
return s3.NewFromConfig(cfg), nil
}
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("ap-northeast-1"))
if err != nil {
return nil, err
}
return s3.NewFromConfig(cfg), nil
}
ユースケース②
SQSにメッセージを送信したら、それがトリガーになってLambdaを実行するアプリケーション
このアプリケーションの場合、登場するAWSのServiceとしてはSQSとLambdaがあります。それでは、localstackでSQSとLambdaの構築していきます。
SQS queueを作成するスクリプト
#!/bin/bash
set -x
awslocal sqs create-queue --queue-name example-queue
set +x
ここもlocal_script
ディレクトリにスクリプトをおきますと、localstackが起動したらexample-queue
が作成された状態になります!
SQSのqueueが作成が成功されたDockerのログサンプル
example-localstack | 2022-12-19T05:12:35.227 DEBUG --- [ MainThread] localstack.runtime.init : Running READY script /etc/localstack/init/ready.d/02_sqs_queue.sh
example-localstack | + awslocal sqs create-queue --queue-name example-queue
example-localstack | 2022-12-19T05:12:35.436 DEBUG --- [ asgi_gw_0] plugin.manager : instantiating plugin PluginSpec(localstack.aws.provider.sqs:default = <function sqs at 0xffff90a5f910>)
example-localstack | 2022-12-19T05:12:35.436 DEBUG --- [ asgi_gw_0] plugin.manager : loading plugin localstack.aws.provider:sqs:default
example-localstack | 2022-12-19T05:12:35.443 DEBUG --- [ asgi_gw_0] l.services.plugins : checking service health sqs:4566
example-localstack | 2022-12-19T05:12:35.531 DEBUG --- [ asgi_gw_0] l.services.sqs.provider : creating queue key=example-queue attributes=None tags=None
example-localstack | 2022-12-19T05:12:35.532 INFO --- [ asgi_gw_0] localstack.request.aws : AWS sqs.CreateQueue => 200
example-localstack | {
example-localstack | "QueueUrl": "http://localhost:4566/000000000000/example-queue"
example-localstack | }
example-localstack | + set +x
続きまして、Lambdaのlocalstackデプロイを進めます。Lambdaはソースからbuildしてデプロイしますので、初期起動スクリプトでは作成しません。別途Lambdaをデプロイするスクリプトで実行します。
Lambdaデプロイスクリプト
#!/usr/bin/env bash
# M1 Macなので、arm64でビルドします。
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o artifact/main .
cd artifact
zip main.zip main
# 毎回DeleteしてからCreateしています。
# LambdaのDelete
aws lambda delete-function --function-name example-sqs-consumer --endpoint-url http://localhost:54566 > /dev/null 2>&1
# LambdaのCreate
aws lambda create-function --function-name example-sqs-consumer \
--runtime go1.x --architectures arm64 --handler main \
--zip-file fileb://main.zip --endpoint-url http://localhost:54566 > /dev/null 2>&1
# Lambdaの設定
aws lambda update-function-configuration --function-name example-sqs-consumer \
--environment "Variables={設定したい環境変数など}" \
--endpoint-url http://localhost:54566 > /dev/null 2>&1
# SQS Queue イベントトリガーを設定
aws lambda create-event-source-mapping --function-name example-sqs-consumer \
--event-source-arn arn:aws:sqs:ap-northeast-1:000000000000:example-queue --endpoint-url http://localhost:54566 > /dev/null 2>&1
このスクリプトを実行しますと、上記で作成したSQSのqueueにメッセージが送られるとそれがトリガーになってデプロイしたLambdaが実行されるようになります。
Lambdaデプロイ成功されてDockerログサンプル
example-localstack | 2022-12-19T05:31:37.442 WARN --- [ asgi_gw_0] localstack.aws.accounts : Ignoring production AWS credentials provided to LocalStack. Falling back to default account ID.
example-localstack | 2022-12-19T05:31:37.470 DEBUG --- [ asgi_gw_0] plugin.manager : instantiating plugin PluginSpec(localstack.aws.provider.lambda:default = <function awslambda at 0xffff90a5ea70>)
example-localstack | 2022-12-19T05:31:37.470 DEBUG --- [ asgi_gw_0] plugin.manager : loading plugin localstack.aws.provider:lambda:default
example-localstack | 2022-12-19T05:31:37.763 INFO --- [ asgi_gw_0] localstack.services.infra : Starting mock Lambda service on http port 4566 ...
example-localstack | 2022-12-19T05:31:38.386 INFO --- [ asgi_gw_0] localstack.utils.bootstrap : Execution of "require" took 916.42ms
example-localstack | 2022-12-19T05:31:38.390 INFO --- [ asgi_gw_0] localstack.request.aws : AWS lambda.DeleteFunction => 404 (ResourceNotFoundException)
example-localstack | 2022-12-19T05:31:39.265 WARN --- [ asgi_gw_0] localstack.aws.accounts : Ignoring production AWS credentials provided to LocalStack. Falling back to default account ID.
example-localstack | 2022-12-19T05:31:39.272 DEBUG --- [uest_thread)] l.s.a.e.sqs_event_source_l : Starting SQS message polling thread for Lambda API
example-localstack | 2022-12-19T05:31:39.277 INFO --- [ asgi_gw_0] localstack.request.aws : AWS lambda.CreateEventSourceMapping => 200
実際localstack上で動いた時のログサンプル
2022-12-12T12:53:21.590 DEBUG --- [functhread12] l.s.a.e.sqs_event_source_l : Sending event from event source arn:aws:sqs:ap-northeast-1:000000000000:example-queue to Lambda arn:aws:lambda:ap-northeast-1:000000000000:function:example-sqs-consumer
2022-12-12T12:53:21.591 DEBUG --- [functhread12] l.s.awslambda.lambda_api : Running lambda arn:aws:lambda:ap-northeast-1:000000000000:function:example-sqs-consumer
2022-12-12T12:53:21.702 DEBUG --- [functhread12] l.u.c.container_client : Getting networks for container: example-localstack
2022-12-12T12:53:21.707 INFO --- [functhread12] l.u.container_networking : Determined main container network: example-network-localstack
2022-12-12T12:53:21.708 DEBUG --- [functhread12] l.u.c.container_client : Getting ipv4 address for container example-localstack in network example-network-localstack.
2022-12-12T12:53:21.718 INFO --- [functhread12] l.u.container_networking : Determined main container target IP: 172.21.0.3
2022-12-12T12:53:21.718 INFO --- [functhread12] l.s.a.lambda_executors : Running lambda: arn:aws:lambda:ap-northeast-1:000000000000:function:example-sqs-consumer
2022-12-12T12:53:21.718 DEBUG --- [functhread12] stevedore.extension : found extension EntryPoint(name='docker_separate_lambda_execution', value='localstack_ext.services.awslambda.lambda_launcher:docker_separate_lambda_execution', group='localstack.hooks.on_docker_separate_execution')
2022-12-12T12:53:21.720 ERROR --- [functhread12] plugin.manager : error importing entrypoint EntryPoint(name='docker_separate_lambda_execution', value='localstack_ext.services.awslambda.lambda_launcher:docker_separate_lambda_execution', group='localstack.hooks.on_docker_separate_execution'): No module named 'localstack_ext.services.awslambda.lambda_launcher'
2022-12-12T12:53:21.720 DEBUG --- [functhread12] plugin.manager : no extensions found in namespace localstack.hooks.on_docker_separate_execution
2022-12-12T12:53:21.720 DEBUG --- [functhread12] l.u.c.docker_sdk_client : Creating container with attributes: {'mount_volumes': None, 'ports': <PortMappings: {}>, 'cap_add': None, 'cap_drop': None, 'security_opt': None, 'dns': '', 'additional_flags': '', 'workdir': None, 'privileged': None, 'command': 'main', 'detach': False, 'entrypoint': None, 'env_vars': {'AWS_ACCESS_KEY_ID': 'test', 'AWS_SECRET_ACCESS_KEY': 'test', 'AWS_REGION': 'ap-northeast-1', 'DOCKER_LAMBDA_USE_STDIN': '1', 'LOCALSTACK_HOSTNAME': '172.21.0.3', 'EDGE_PORT': '4566', '_HANDLER': 'main', 'AWS_LAMBDA_FUNCTION_TIMEOUT': '3', 'AWS_LAMBDA_FUNCTION_NAME': 'example-sqs-consumer', 'AWS_LAMBDA_FUNCTION_VERSION': '$LATEST', 'AWS_LAMBDA_FUNCTION_INVOKED_ARN': 'arn:aws:lambda:ap-northeast-1:000000000000:function:example-sqs-consumer'}, 'image_name': 'lambci/lambda:go1.x', 'interactive': True, 'name': None, 'network': 'example-network-localstack', 'remove': True, 'self': <localstack.utils.container_utils.docker_sdk_client.SdkDockerClient object at 0xffffb66f3850>, 'tty': False, 'user': None}
2022-12-12T12:53:21.764 DEBUG --- [functhread12] l.u.c.docker_sdk_client : Copying file /tmp/function.zipfile.c62a03de/. into 7911ae29f403a6655848998492d5d29a414943a228e5b9f87de012aa7b65de91:/var/task
2022-12-12T12:53:22.657 DEBUG --- [functhread12] l.u.c.docker_sdk_client : Starting container 7911ae29f403a6655848998492d5d29a414943a228e5b9f87de012aa7b65de91
2022-12-12T12:53:23.350 DEBUG --- [functhread12] l.s.a.lambda_executors : Lambda arn:aws:lambda:ap-northeast-1:000000000000:function:example-sqs-consumer result / log output:
null
>START RequestId: fff6421e-8921-1c11-0325-cf5d49eb15a5 Version: $LATEST
> END RequestId: fff6421e-8921-1c11-0325-cf5d49eb15a5
> REPORT RequestId: fff6421e-8921-1c11-0325-cf5d49eb15a5 Init Duration: 314.54 ms Duration: 29.22 ms Billed Duration: 30 ms Memory Size: 1536 MB Max Memory Used: 67 MB
2022-12-12T12:53:23.357 DEBUG --- [ asgi_gw_1] plugin.manager : instantiating plugin PluginSpec(localstack.aws.provider.logs:default = <function logs at 0xffffa23fb910>)
2022-12-12T12:53:23.357 DEBUG --- [ asgi_gw_1] plugin.manager : loading plugin localstack.aws.provider:logs:default
2022-12-12T12:53:23.363 DEBUG --- [ asgi_gw_1] l.services.plugins : checking service health logs:4566
2022-12-12T12:53:23.405 DEBUG --- [ asgi_gw_1] l.services.sqs.models : deleting message 9c33c78c-769e-4720-9636-5434619e860b from queue arn:aws:sqs:ap-northeast-1:000000000000:example-queue
ユースケース③
S3にファイルがアップロードされたらそれがトリガーになってLambdaを実行するアプリケーション
このアプリケーションの場合、登場するAWSのServiceとしてはS3、EventBridgeとLambdaがあります。S3とLambdaの構築やデプロイについて上記のユースケースで記載しましたので、こちらでは割愛します。EventBridgeによるS3とLambdaの連携のところのみ具体的に書きます。
Lambdaの作成スクリプト
#!/usr/bin/env bash
# M1 Macなので、arm64でビルドします。
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o artifact/main .
cd artifact
zip main.zip main
# 毎回DeleteしてからCreateしています。
# LambdaのDelete
aws lambda delete-function --function-name example-lambda --endpoint-url http://localhost:54566 > /dev/null 2>&1
# LambdaのCreate
aws lambda create-function --role testes --function-name example-lambda \
--runtime go1.x --architectures arm64 --handler main \
--zip-file fileb://main.zip --endpoint-url http://localhost:54566 > /dev/null 2>&1
# Lambdaの設定
aws lambda update-function-configuration --function-name example-lambda \
--environment \
"Variables={環境変数設定}" \
--endpoint-url http://localhost:54566 > /dev/null 2>&1
# Event rule作成
aws events put-rule --name "example-lambda-event-rule" --event-pattern \
"{ \
\"source\":[\"aws.s3\"], \
\"detail-type\":[\"Object Create\"], \
\"detail\":{\"bucket\":{\"name\":[\"example-bucket\"]}}, \
\"resources\":[\"arn:aws:s3:::example-bucket\"] \
}" \
--endpoint-url http://localhost:54566 > /dev/null 2>&1
# Event ruleにLambdaをターゲットとして登録
aws events put-targets --rule "example-lambda-event-rule" --targets \
"{
\"Id\":\"example-lambda-target\",
\"Arn\":\"arn:aws:lambda:ap-northeast-1:000000000000:function:example-lambda\"
}" \
--endpoint-url http://localhost:54566 > /dev/null 2>&1
最後に
いかがでしょうか?個人的にはこれからどんどんローカルの開発で活用していきたいと思います。
機能面でもV1.0正式版リリースしてからかなり充実していますので、今後、RDBやStep Function、IAMロール検証なども導入して見ようかなと思います。
サポートしているService一覧についてはこちらを確認ください。
localstackの最新の情報は公式ドキュメントをご参照ください。
Discussion