はじめに
AWS ECSを使ったアプリケーションを簡単にデプロイできるツールであるAWS Copilot CLIでSQSを使ってみます。
Copilotにはインターネットからアクセスされるウェブアプリケーションをデプロイできる Load Balanced Web Service や定期的に起動するバッジである Scheduled Job と並んで Worker Service というメッセージを受信して処理するタイプのサービスがあります。が、この Worker Service で具体的にどのようなアプリケーションがデプロイされるのかいま一つイメージがつかなかったため、実際にWorker Serviceをデプロイして、非同期サービス間通信を体験してみました。
参考
公式ドキュメント
前提
$ copilot version
version: v1.14.0, built for linux
CopilotにおけるApplication、Serviceとはなにか、などCopilotに関する基礎知識に関する説明は省略します。また、Application、Environmentは作成済みとしてそこへServiceをデプロイする部分の手順だけ記載します。
目標
次の図のような一連の流れを実現させようと思います。(ALBなどを省略した簡略図です)
Load Balanced Web Service をECSにデプロイし、これをメッセージを送信するパブリッシャーとします。
具体的には、今回の試行ではAPIをデプロイし、インターネットからPOSTアクセスがあればそのアクセスに含まれるデータをSNSに送信します。
さらにもう一つ、 Worker Service をECSにデプロイし、メッセージを処理するサブスクライバーとします。
このサービスがSQSからメッセージを取得し、処理します。
Copilotのドキュメントによると Worker Service はpub/sub アーキテクチャを実装するものらしいです。
私はpub/sub アーキテクチャについての知見が無いため、これについて解説することはできないのですが、ともかくまずは最小の構成で試してみます。
パブリッシャーをデプロイ
まず、SNS経由でSQSにメッセージを送信する側であるパブリッシャーをデプロイします。
Load Balanced Web Service で試してみましたが、パブリッシャー側は Scheduled Job などどのタイプのサービスでも良さそうです。
今回デプロイしてみたアプリケーションはGitLabに置いてあります。send-message-apiで確認できます。
Spring Boot + JavaによるWebアプリケーションになっており、POSTリクエストがあるとそのBodyをSNSへ送信します。Controllerで全ての処理を実施している簡易的な実装です。
Service作成
copilot svc init -a demo-app -n send-message-api -t "Load Balanced Web Service"
demo-app
というApplicationは事前にできているものとします。send-message-api
というServiceをLoad Balanced Web Service
タイプでinitします。
次にcopilotがプロジェクト内に作成したmanifestに次のようなpublish
セクションを追加します。
publish:
topics:
- name: order-events
参考のためにmanifest全体を折りたたみ中に記載しておきます。
manifest.yml
name: send-message-api
type: Load Balanced Web Service
http:
path: 'send-message-api'
healthcheck: '/send-message-api/actuator/health'
image:
build: Dockerfile
port: 8080
cpu: 256
memory: 512
count: 1
exec: true
publish:
topics:
- name: sample-sns
デプロイ
copilot svc deploy -a demo-app -e environment -n send-message-api
でデプロイします。develop
Environmentは作成済みとします。
demo-app
Applicationのdevelop
Environmentにsend-message-api
というServiceがデプロイされます。
publish
セクションの内容に沿って、SNSが作成されます。
自分が試したときにはdemo-app-develop-send-message-api-sample-sns
という名前のSNSになりました。
SNSの名称は
${Application}-${Environment}-${Service}-${manifestファイル中のpublish.topics.name}
のように命名され、一意になるということのようです。
なお、実際はSNSのARNは環境変数経由で取得できるため、SNSが具体的にどのような名前で作られたかは基本的に意識する必要はありません。
環境変数に格納されるSNSのARN
作成されたSNSのARNは環境変数としてコンテナに設定されます。
具体的には今回の動作確認ではCOPILOT_SNS_TOPIC_ARNS
という環境変数に{"sample-sns":"arn:aws:sns:ap-northeast-1:xxxxxxxx:demo-app-develop-send-message-api-sample-sns"}
のようなJSON文字列が設定されました。
単純にARNが設定されるのではなく、JSON文字列であることに注意が必要です。パースしてARNを取り出さなければなりません。
サブスクライバーをデプロイ
次にSQSからメッセージを取り出して処理するサブスクライバーをデプロイします。
今回デプロイしてみたアプリケーションはパブリッシャー同様GitLabに置いてあります。receive-message-batchで確認できます。
ほぼメインメソッドしかない超簡易的な実装です。
Service作成
copilot svc init -a demo-app -n receive-message-batch -t "Worker Service"
receive-message-batch
というServiceをWorker Service
タイプでinitします。
次にcopilotがプロジェクト内に作成したmanifestのsubscribe
セクションを次のように記載します。
subscribe:
topics:
- name: sample-sns
service: send-message-api
Worker Serviceのmanifestについてのドキュメント
上記のような書き方をすることでsend-message-api
Serviceのsample-sns
トピックをサブスクライブします。つまり、先程デプロイしたServiceが送信するメッセージを受け取れるようになります。
参考のためにmanifest全体を折りたたみ中に記載しておきます。
manifest.yml
name: receive-message-batch
type: Worker Service
image:
build: Dockerfile
cpu: 256
memory: 512
count: 1
exec: true
subscribe:
topics:
- name: sample-sns
service: send-message-api
command: java -jar receive-message-batch-all.jar
デプロイ
copilot svc deploy -a demo-app -e environment -n receive-message-batch
でデプロイします。demo-app
Applicationのdevelop
Environmentにreceive-message-batch
というServiceがデプロイされます。
demo-app-develop-receive-message-batch-EventsQueue-yyyyyy
のような名称のSQSが追加されます。さらに、SNSのへのサブスクリプション、必要なポリシーなども自動で作成されます。
SNSと似たように、SQSの名称は
${Application}-${Environment}-${Service}-EventsQueue-${ランダム文字列}
のように命名され、一意になるということのようです。
パブリッシャーにおけるSNS同様にSQSのURIは環境変数経由で取得できるため、具体的にどのような名前のSQSが作成されたかなどは基本的に意識する必要はありません。
環境変数に格納されるSNSのARN
作成されたSQSのARNは環境変数としてコンテナに設定されます。
具体的には今回の動作確認ではCOPILOT_QUEUE_URI
という環境変数にhttps://sqs.ap-northeast-1.amazonaws.com/xxxxxx/demo-app-develop-receive-message-batch-EventsQueue-yyyyyy
のような文字列が設定されました。今回はJSONではありません。
Worker Serviceの挙動
勝手な思い込みでしかないのですが、当初私は Worker Service はSQSにメッセージが送信されるたびに起動されてメッセージを処理し、それ以外のタイミングでは起動しないものだと思っていました。
例えばlambdaだとそのような起動の仕方が可能だったはずです。
しかし、実際の Worker Service はそのようなものではなく、基本的にコンテナは常駐し、SQSをポーリングしメッセージがあるか確認する処理などは自前で実装する必要があります。
どのような実装が良いのかはよくわかりませんでしたが、一旦while文で無限ループさせ、SQSをポーリングするようにしました。(この部分です)
動作確認
ここまでの手順でパブリッシャーとサブスクライバーがデプロイされ、SNS・SQSを介して連携するようになりました。
今回私はサンプルとしてsend-message-api(パブリッシャー)、receive-message-batch(サブスクライバー)をデプロイしています。
今回の場合、次のように動作確認しました。
パブリッシャーへPOSTリクエスト
パブリッシャーは Load Balanced Web Service としてSNSへメッセージを送信するWebアプリケーションをデプロイしているので、
curl -X POST -H "Content-Type: application/json" -d '{"message":"java"}' http://${デプロイしたLoad Balanced Web Serviceに紐づくALBのDNS名}/send-message-api/send
のようにPOSTリクエストします。
SNSからSQSへとメッセージの伝達
先述のPOSTリクエストでパブリッシャーはリクエストボディ取得してSNSへ送信します。(この部分です)
Worker Serviceデプロイ時に作成されたSQSはSNSをサブスクリプションしているので、SQSへメッセージが渡ります。
Worker ServiceはSQSをポーリングしているため、メッセージがSQSに入ると、それを取得し、処理します。今回のreceive-message-batchは処理といってもログ出力(この部分です)するだけですが、メッセージがあると、そのメッセージをログ出力します。
これで2つのサービスがSNS・SQSを介して非同期的にコミュニケーションできていることが確認できました。
その他追加で試したこと
キューの細かい設定
Worker Service で最小の設定として
subscribe:
topics:
- name: sample-sns
service: send-message-api
のような記載をmanifestに書きました。この場合SQSはデフォルト設定で作成されます。
しかし、Worker Serviceに関するドキュメントにはキューのカスタマイズ設定も紹介されています。
目一杯設定すると次のようになります。
subscribe:
topics:
- name: sample-sns
service: send-message-api
queue:
# 可視性タイムアウト
timeout: 45s
# メッセージ保持期間
retention: 71h
# 配信遅延
delay: 30s
dead_letter:
# デッドレターキュー 最大受信数
tries: 5
コメントに書いている項目を設定できるようです。
「可視性タイムアウト」とはなにか、などは前述のCopilotのドキュメントおよびSQSのドキュメント(キューパラメータの設定(コンソール)など)が参考になります。
サブスクライバーが複数いる場合
1つのパブリッシャーに対して複数のサブスクライバーがいるパターンも試しました。
別の Worker Service をcopilot svc init -t "Worker Service"
で作成し、manifestに全く同じように、
subscribe:
topics:
- name: sample-sns
service: send-message-api
と記載してデプロイするだけです。
すると、このサービス用にもう一つSQSが作成され、SNSへのサブスクリプションが設定されます。
パブリッシャーがSNSへメッセージを送信すると両方のSQSにメッセージが送られ、ほぼ同時に2つの Worker Service が同じメッセージを受信して処理を実施します。
FIFOキューについて
現在SQSのFIFOキューには対応していなさそうです。
しかし、下記のIssueを見ると近いうちに対応されるのかもしれません。