🐷

localstackをもっと使いましょうという話

2022/12/20に公開

本記事は、NewsPicks Advent Calendar 2022 の12/20公開分の記事になります。
https://qiita.com/advent-calendar/2022/newspicks-tech

はじめに

現在開発に携わっているプロダクトではインフラはほとんどAWSを使っていますので、ローカル開発に役立つlocalstackの活用例をユースケースごとにお見せししたいと思います。もともと少しは使っていましたが、最近活用シーンが多くなってきていますので、これからもっと使って行きたいと思います。

localstackとは

一言でいうとAWS環境をローカルでエミュレートできるツール群です。2016年スタートした割と新しいOSSなのですが、すでにGithub star数が4.5K、Dockerのダウンロード数が100万回を超えたポピュラーな開発ツールになって来ました。

2022年にはバージョン1.0が正式にリリースされ様々な機能が追加されましたので、これからもどんどん活用シーンが増えるのではないかと思います。
https://github.com/localstack/localstack

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の最新の情報は公式ドキュメントをご参照ください。
https://docs.localstack.cloud/overview/

Discussion