Open3

ElasticMQを使ってローカルでAWS SQSをモックしてみる

るんしょーるんしょー

ElasticMQのセットアップ

参考記事

https://note.shiftinc.jp/n/n5954af51eaaf
https://devops-blog.virtualtech.jp/entry/20240614/1718341862

docker-compose.yml 作成

docker-compose.yml
version: "3"

services:
  elasticmq:
    container_name: elasticmq
    image: softwaremill/elasticmq:latest
    ports:
      - "9324:9324"
      - "9325:9325"

以下コマンドでElasticMQを起動する
docker-compose up -d

以下のURLで管理画面を開くことができる
http://localhost:9325/

キューを作成

--endpoint-urlを指定することで、AWSではなくローカルのElasticMQへ接続する
aws sqs create-queue --queue-name test-queue --endpoint-url http://localhost:9324

作成されたキューを確認(管理画面でもOK)
aws sqs list-queues --endpoint-url http://localhost:9324

{
    "QueueUrls": [
        "http://localhost:9324/000000000000/test-queue"
    ]
}

メッセージの送受信

キューを送信してみる
aws sqs send-message --queue-url http://localhost:9324/queue/test-queue --message-body "ElasticMQ Test Message" --endpoint-url http://localhost:9324

管理画面を見るとキューがあることを確認できる

受信してみる
aws sqs receive-message --queue-url http://localhost:9324/queue/test-queue --endpoint-url http://localhost:9324

{
    "Messages": [
        {
            "MessageId": "65ba8f9b-3c22-4220-9e92-b0f5b469ef85",
            "ReceiptHandle": "65ba8f9b-3c22-4220-9e92-b0f5b469ef85#06e57300-cb75-45fb-ae7c-27b349179980",
            "MD5OfBody": "b336d90ba4892d57a9cc3140502d4634",
            "Body": "ElasticMQ Test Message"
        }
    ]
}

先ほど送ったメッセージを受信できた

メッセージの削除

管理画面を確認すると、受信しただけではメッセージは削除されていないっぽい
以下のコマンドでメッセージを削除する
aws sqs delete-message --queue-url http://localhost:9324/queue/test-queue --receipt-handle 65ba8f9b-3c22-4220-9e92-b0f5b469ef85#06e57300-cb75-45fb-ae7c-27b349179980 --endpoint-url http://localhost:9324
--receipt-handleは先ほど受信したときの値を入力

削除されたことを管理画面で確認できる

るんしょーるんしょー

ローカルでSQSのメッセージを送信してみる

SQSへメッセージを送信するようなLambdaを作成する。
(本筋ではないのでセットアップは省略する)

SAMを利用して以下のようなコードを作成する

app.mjs
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";

const client = new SQSClient();
const SQS_QUEUE_URL = "http://localhost:9324/queue/test-queue";

export const lambdaHandler = async (event, context) => {
  const command = new SendMessageCommand({
    QueueUrl: SQS_QUEUE_URL,
    MessageBody: "ElasticMQ Test Message on Lambda",
  });

  const response = await client.send(command);
  console.log(response);
  return response;
};

前回同様、test-sqsというキューにメッセージを送信する。

しかし、このまま実行すると、AWSのSQSにメッセージを送信しようとしてしまう。
SQSClientのコンストラクタにendpoint: "http://localhost:9324"を指定してやればElasticMQに送信される。
が、コード上でendpointを指定するのはあまり好きではない。(環境の切り替えなど、本筋ではない処理をコード上に含ませたくない)

なので、以下の記事を参考に、環境変数にendpointを渡すようにしてみる。
https://dev.classmethod.jp/articles/aws-endpoint-url-environment-varable-is-supported-on-sdks/

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sqs-test

  Sample SAM Template for sqs-test

Parameters:
  AwsEndpointSQSUrl:
    Type: String
    Default: ""
  
Globals:
  Function:
    Timeout: 3

Resources:
  SQSTestFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: sqs-test/
      Handler: app.lambdaHandler
      Runtime: nodejs20.x
      Architectures:
        - x86_64
      Environment:
        Variables:
          AWS_ENDPOINT_URL: !Ref AwsEndpointSQSUrl

AwsEndpointSQSUrlというパラメータを作成して、実行時に渡してみる。
sam local invoke --parameter-overrides AwsEndpointSQSUrl=http://localhost:9324

これでメッセージを送信、と思ったらエラーが

Invoke Error    {"errorType":"Error","errorMessage":"connect ECONNREFUSED 127.0.0.1:9324","code":"ECONNREFUSED","errno":-111,"syscall":"connect","address":"127.0.0.1","port":9324,"$metadata":{"attempts":3,"totalRetryDelay":184},"stack":["Error: connect ECONNREFUSED 127.0.0.1:9324","    at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1607:16)"]}

接続が拒否されてるっぽい

調べてみると、sam localのコンテナとElasticMQのコンテナ間で通信ができていない様子。
docker networkを作成し、docker-compose.ymlにnetworkの設定を追加する
docker network create sam-local

docker-compose.yml
version: "3"

services:
  elasticmq:
    container_name: elasticmq
    image: softwaremill/elasticmq:latest
    ports:
      - "9324:9324"
      - "9325:9325"
    networks:
      - sam-local
networks:
  sam-local:
    external: true

ElasticMQを再起動して、docker-networkを指定して再度実行
sam local invoke --parameter-overrides AwsEndpointSQSUrl=http://localhost:9324 --docker-network sam-local

今度こそ、と思ったらまだ同じエラーが出る。

コンテナが異なるからURLをlocalhostで指定できなかったぽい。初歩的なミス。

今回、ElasticMQのコンテナのホストはelasticmqとなっている。
コード、コマンドを以下のように修正して実行。

app.mjs
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";

const client = new SQSClient();
const SQS_QUEUE_URL = "http://elasticmq:9324/queue/test-queue";

export const lambdaHandler = async (event, context) => {
  const command = new SendMessageCommand({
    QueueUrl: SQS_QUEUE_URL,
    MessageBody: "ElasticMQ Test Message on Lambda",
  });

  const response = await client.send(command);
  console.log(response);
  return response;
};

sam local invoke --parameter-overrides AwsEndpointSQSUrl=http://elasticmq:9324 --docker-network sam-local

実行するとようやくレスポンスが返ってきた
{"$metadata": {"httpStatusCode": 200, "attempts": 1, "totalRetryDelay": 0}, "MD5OfMessageBody": "d47936c65dcf06da701ae504dff350f3", "MessageId": "245bb1a3-b0d1-4500-b5ab-9a8cd3ffe6ca"}

管理画面からもキューが送信されたことが確認できる

るんしょーるんしょー

起動時にキューを作成したい

ElasticMQ起動時に毎回初期化されてしまうため、起動するたび毎回キューを作成しなくてはいけない。
あまりにめんどうなので、起動時に必要なキューをまとめて作成するようにしたい。

elasticmq.conf

/opt/elasticmq.confの値を変更することで、起動時にキューを作成することができるらしい。
https://github.com/softwaremill/elasticmq/?tab=readme-ov-file#automatically-creating-queues-on-startup
(typesafeってので記述されているっぽい?)

可視性タイムアウトやFIFOキュー、デットレターキューなどの設定もここで行う。

ではconfファイルを作成して、docker-compose.ymlでマウントするようにしよう。
今回は公式で例として挙げられている通りに作ってみた。

elasticmq.conf
# the include should be done only once, at the beginning of the custom configuration file
include classpath("application.conf")

queues {
  queue1 {
    defaultVisibilityTimeout = 10 seconds
    delay = 5 seconds
    receiveMessageWait = 0 seconds
    deadLettersQueue {
      name = "queue1-dead-letters"
      maxReceiveCount = 3 // from 1 to 1000
    }
    fifo = false
    contentBasedDeduplication = false
    copyTo = "audit-queue-name"
    moveTo = "redirect-queue-name"
    tags {
      tag1 = "tagged1"
      tag2 = "tagged2"
    }
  }
  queue1-dead-letters { }
  audit-queue-name { }
  redirect-queue-name { }
}
docker-compose.yml
version: "3"

services:
  elasticmq:
    container_name: elasticmq
    image: softwaremill/elasticmq:latest
    ports:
      - "9324:9324"
      - "9325:9325"
    networks:
      - sam-local
    volumes:
      - ./elasticmq.conf:/opt/elasticmq.conf # ここ追加

ElasticMQを起動しなおせばこのようにキューが作成されていることが確認できる。