Amazon Web Services ブログ

AWS Copilot を用いた pub/sub アーキテクチャの実装

イントロダクション

AWS Copilot CLI は、Amazon Elastic Container Service ( Amazon ECS ), AWS Fargate 及び AWS App Runner 上で コンテナのビルドや管理運用する際にデベロッパーが用いるツールで、2020 年にローンチしました。

このブログでは、AWS Copilot CLI を使用して、Amazon ECS 及び AWS Fargate 上で publisher サービスと subscriber ワーカーサービスを簡単に実装する方法説明します。これらのサービスは、pub/sub アーキテクチャ内でそれぞれがイベントを publish 及び consume します。

この機能を説明するために、このブログに掲載されたモノと似たアーキテクチャを元に、publish/subscribe(pub/sub) アーキテクチャを作成します。しかし、AWS Lambda に頼ることなく、AWS Fargate 上で動作する Amazon ECS サービスを用いると共に、AWS Copilot CLI を用いてリソースの作成と管理をします。

このブログでは、あなたは E コマースプラットフォームの所有者と仮定します。そして、プラットフォームに注文が送信される度にマイクロサービスがトピックにメッセージを送信し、このメッセージに関心があるいくつかのマイクロサービスは、非同期に受け取った注文に対して処理を始めます。注文を処理できるマイクロサービスは様々あると考えられ、たとえば、注文フルフィルメントマイクロサービス、インボイスのマイクロサービス、特定の閾値を超える注文に対してキャンペーンコードを生成するようなプロモーションマイクロサービスなどです。このブログでは、フルフィルメントマイクローサビス及びプロモーションマイクロサービスを実装します。

ソリューションのアーキテクチャは下記の図の通りです。

ソリューションの概要

pub/sub でのメッセージングは非同期メッセージングパターンで、送信者または受信者のidを知ることなくメッセージを交換できます。このパターンでは、サービス間の通信を疎結合にできるので、アプリケーションを分離できます。送信者 ( publisher とも呼ばれる ) は、トピックに対してメッセージをブロードキャストします。一方で受信者 ( subscriber とも呼ばれる ) は異なるトピックをサブスクライブし、当該トピックにメッセージが送信されるとフィルタリングポリシーを参照し、合致するメッセージを受信します。

受信者は、送信者からのメッセージを無限に受け取ることができますが、topic-queue chaining と呼ばれるパターンを用いることをオススメします。この名前が示すように、SNS トピックを SQS キューに紐付けます。もし、サービスが例外を実行した際やメンテナンスを必要としている際でも、メッセージはキューに保持されます。これには、キューがバッファとして機能し、最終的にロードバランサーとして機能するという利点もあります。

AWS Copilot CLIは、topic-queue chaining パターンのpub/sub アーキテクチャを以下のように簡単に実装できます。

  • サービスマニフェストを修正するだけで、Amazon SNS トピックにメッセージを送信する publisher サービスを作成します。
  • トピックに送信された通知を処理するため 1 つ以上の Amazon SQS キューを含む worker サービス、障害を処理するデッドレターキュー ( DLQ )、キューからメッセージを取得し、非同期にメッセージを処理するAWS Fargate 上で動作する Amazon ECS サービスを作成します。

このブログでは、AWS Copilot CLI が提供する Load Balanced Web Service と Worker Service を用いて以下のアーキテクチャを実装します。

ウォークスルー

ここからは、皆さんと次の作業を実施していきます。

  • サンプルリポジトリのクローンとコードの確認
  • AWS Copilot CLI を用いて、マイクロサービスのための環境の構築
  • publisher と SNS トピックの作成
  • subscriber、SQS キュー、subscriber のポリシーの作成
  • 実装した pub/sub アーキテクチャがどのように動作するか確認

前提条件

このウォークスルーでは、以下の前提条件を満たす必要があります。

サンプルリポジトリのクローン

最初のステップでは、Github リポジトリをクローンするディレクトリに移動し、git clone を実行します。

git clone https://github.com/aws-samples/aws-copilot-pubsub

クローンしたディレクトリに移動し、サブディレクトリを確認してください。サービスごとにフォルダーがあり、1 つは publisher 用、もう 1 つは fulfilment と promotion という名前の 2 つの subscriber 用です。下記にフォルダー構成を示します。

aws-copilot-pubsub/ 
├─ subscribers/ 
│ ├─ fulfilment/ 
│ │ └─ ... 
│ ├─ promotion/ 
│ │ ├─ requirements.txt 
│ │ ├─ promotion.py 
│ │ └─ Dockerfile ├─ publisher/ 
│ └─ ...

アプリケーションと環境の作成

まず最初に、Service、Environment、Pipeline を関連づける論理グループを作成します。AWS Copilot では、これを Application と呼びます。

copilot app init pubsub

上記のコマンドを実行すると、AWS Copilot はマニフェストと呼ばれる IaC の YAML 構成ファイルを保持するために ./copilot フォルダを利用します。それにより、AWS Copilot CLI を用いて AWS 上にコンテナ化されたアプリケーションを簡単にデプロイすることを可能にします。合わせてリソース作成に使われるインフラストラクチャロールもいくつか作成されます。

次のステップでは、デプロイするアプリケーションのための環境を構築します。AWS Copilot では、アプリケーションのデプロイメントを論理的に分離した環境を非常に簡単な方法で作成する事ができます。一般的なユースケースは、テスト環境 及び 独立した本番環境を用意し、テスト環境で検証された場合にのみ、アプリケーションを本番環境にデプロイすることです。このウォークスルーでは、以下のコマンドで作成した test という名前のテスト環境にサービスをデプロイします。

copilot env init \
     --app pubsub \
     --name test \
     --region 'eu-west-1' \
     --default-config \
     --profile default

上記のコマンドを実行すると、AWS Copilot は指定されたプロファイル認証情報 (default profile) を使用して、サービスをホストするために必要なインフラストラクチャを作成します。プロファイルを省略すると、~/.aws/credentials ファイル内のプロファイルのうち一つを選択するように促されます。AWS Copilot は、選択された認証情報を用い、ユーザに代わってリソースの作成を開始します。初期構成が出来上がった後は、ユーザは、./copilot/environments/test/manifest.yaml ファイルを修正することにより環境をアップデートできます。今回は、環境に変更を加えることはしません。下記のコマンドで環境をデプロイします。

copilot env deploy --name test

作成された環境ごとに、AWS Copilot は、分離されたネットワークスタック、コンピュートエンジンに AWS Fargate を用いた Amazon ECS クラスタを作成します。このプロセスが完了するまで、およそ 2 分かかります。少しストレッチして待ちましょう。もし、AWS Copilot が裏で何を作成しているか詳しく知りたいなら、AWS CloudFormation コンソールへアクセスしてスタックの進行状況を確認しましょう。スタックは、<appName>-<envName> と名づけられるので、今回の場合は、pubsub-test です。

publisher の作成

現在、皆さんの環境はデプロイ済みであり、当該環境にAmazon ECS クラスタが既に存在するので 皆さんは SNS トピックにメッセージを送信する “publisher” と命名されたマイクロサービスをデプロイすることができます。

まず最初に、./publisher ディレクトリを探しましょう。内部には、ロジックを実装する Python ファイルと、コードと依存関係を含むコンテナイメージの作成に用いる Dockerfile があることがわかります。

publisher/
│  ├─ templates
│  │  ├─ index.html
│  │  └─ order.html
│  ├─ requirements.txt
│  ├─ publisher.py
│  └─ Dockerfile

コードはとてもシンプルです。なぜなら、FlaskJinja のテンプレートを活用して、以下の画像に示すような小規模なフロントエンドを作成するからです。

フロントエンドは、2つのフィールドを持つフォームを提供しています。一つは顧客名、もう一つは注文金額です。これは、リクエストの処理をトリガーするためのシンプルな方法です。Send ボタンが押されると、マイクロサービスはフォームを処理し、注文のデータを DynamoDB テーブルへ記録し、SNS トピックへメッセージを送信します。それにより、subscriber マイクロサービス上で非同期に処理が開始できます

そのようなマイクロサービスを実装するためには、複数のインフラストラクチャコンポーネントを作成する必要があり、そのプロセスに時間がかかる場合があります。インフラストラクチャの作り方を理解するのに時間を費やすよりも、マイクロサービス開発に時間を効率的に使うために、AWS Copilot は いくつかの CLI コマンドや 追加設定した AWS Copilot YAML マニフェストファイルを通して Elastic Load Balancing (Application Load Balancer または Network Load Balancer のいずれか)、Amazon ECR リポジトリ、タスク定義、Amazon ECS タスクに加え、 SNS トピックや DynamoDB テーブルといったリソースを作成する手助けをします。

ここでは、Load Balanced Web Service と呼ばれる AWS Copilot のパターンを使用し、Amazon ECS サービス 及び パブリックアクセスが可能な Application Load Balancer(ALB) を作成します。そのため、以下のコマンドで実行します。

copilot svc init \
    --app pubsub  \
    --svc-type "Load Balanced Web Service" \
    --name "publisher" \
    --port 5000 \
    --dockerfile "publisher/Dockerfile"   

上記のコマンドを実行すると、コンテナイメージを安全に保存でき、プライベートで利用可能なAmazon ECR リポジトリと、サービスの構成オプションが記載されたマニフェストファイルが copilot/publisher/manifest.yml に作成されます。

サービスをデプロイする前に生成された manifest.yml ファイルを確認します。マニフェストファイルがどのようにサービス構成を保持しているか確認し、割り当てられたCPU、メモリ、タスク数などの構成オプションを変更できます。

このウォークスルーでは、2 つの追加リソースを加える必要があります。publisher がメッセージを送信するための SNS トピックとリクエストを保持するデータベースです。

AWS Copilot CLI で SNS トピックを簡単に作成するために、下記のセクションをマニフェストファイルに追加します。

publish:
  topics:
    - name: ordersTopic

宣言したトピックごとに AWS Copilot は標準 SNS トピックを作成し、COPILOT_SNS_TOPIC_ARNS という環境変数を通してリソースの Amazon Resource Name(ARN) を注入します。そして、トピックへメッセージを送信するために Amazon ECS タスクに適切な権限を渡します。当該環境変数は JSON 構造で、key にトピック名、各 key はトピック ARN を value に持ちます。それゆえ Python では、以下のようなコードでこれらの辞書のような構造体にアクセスします。

dest_topic_name = 'ordersTopic' 
sns_topics_arn = json.loads(os.getenv("COPILOT_SNS_TOPIC_ARNS")) 
topic_arn = sns_topics_arn[dest_topic_name]

ここでは、標準 SNS トピックを作成していることに注意して下さい。FIFO (first-in, first-out) が必要なケースでは、マニフェストファイルにトピック設定に fifo property を追加してこの動作を有効にできます。もし FIFO を有効するならば、全ての subscriber は同様に FIFO SQS キューを利用しなければならないことを覚えておいてください。ここでは、物事をシンプルにするために、標準 SNS トピックを利用します。

データベーステーブルを追加するために、ターミナルで以下のコマンドを実行します。

copilot storage init \
    --name ordersTable \
    --storage-type DynamoDB \
    --workload publisher \
    --partition-key id:S \
    --no-sort --no-lsi

このコマンドは、 copilot/publisher ディレクトリの下に addons/ordersTable.yml を作成します。当該ファイルには、AWS Copilot CLI を用いてデプロイされた DynamoDB テーブルの設定が記載されています。

マニフェストファイルとアドオンファイルで必要なリソースを定義できたので、実際にリソースを作成していきます。ローカルで実行している Docker デーモンが、コンテナイメージをビルドし、その後、Amazon Elastic Container Registry (Amazon ECR) にアップロードされ、Amazon ECS タスクのイメージとして利用されます。

備考:下記のコマンドを実行する前 または コマンドの実行失敗の際には Docker デーモンが起動していることを確認してください。

copilot svc deploy --name publisher --env test

ターミナルにリソースの作成状況が表示されます。サービスとアドオンが作成された後、Application Load Balancer の DNS 名を受け取ります。これでインターネットを介して、サービスにアクセスできるようになりました。

1 つ目の subscriber (fulfilment) の作成

前のステップで、注文を送信する publisher 用のインフラストラクチャと サービスを作成しました。しかし、注文を処理する subscriber 用のインフラストラクチャとサービスは作成していません。それでは、1 つ目の subscriber サービスを作成しましょう。まず初めに、下記のコマンドでサービスの定義を作成しましょう。

copilot svc init \
    --app pubsub \
    --svc-type "Worker Service" \
    --name fulfilment \
    --port 5000 \
    --dockerfile "subscribers/fulfilment/Dockerfile" \
    --subscribe-topics "publisher:ordersTopic"

ここでは、AWS Copilot CLI が提供する Worker Service というサービスタイプを用います。Worker Service では、バッファとして動作し、メッセージを保持する SQS キュー と AWS Fargate 上で動作する Amazon ECS サービスが作成されます。

さらに、<svcName>:<topicName> の表記でサブスクライブしたいトピックを選択していることに注目してください。あるいは、上記のフラグ引数をせず、省略することも可能です。その場合、コマンドを実行した際に 既に存在する SNS トピックをサブスクライブするように促されます。その際は、スペースバーを押してトピックを選択し、最後にエンターキーを押します。

Which topics do you want to subscribe to? [Use arrows to move, space to select, type to filter, ? for more help] 
> [x] ordersTopic (publisher)

コマンドが発行されると、copilot/fulfilment ディレクトリ下にサービス用の新しいマニフェストファイルが作成されていることがわかります。マニフェストファイル内に、トピックのサブスクリプションが追加されたセクションがあることを確認してください。

subscribe:
  topics:
    - name: ordersTopic
      service: publisher

上記の設定により、AWS Copilot CLI は COPILOT_QUEUE_URI 環境変数を注入するため、SQS キュー内のイベントへアクセスする事ができます。キューから何度読んでもアプリケーションが正常に処理できない特定のメッセージがある際、通常 当該メッセージは、手動で検査するために Dead-Letter Queue (DLQ) と呼ばれる別のキューへルーティングされます。AWS Copilot では、DLQ の作成 及び 再送設定の指定がとても簡単です。皆さんがすることは、以下のセクションをマニフェストファイルに追加することだけです。

subscribe:
  topics:
    - name: ordersTopic
      service: publisher
  queue:
    dead_letter:
      tries: 3

マニフェストを修正したら、以下のコマンドを用いて Amazon ECS 及び AWS Fargate に subscriber サービスをデプロイできます。

copilot svc deploy --name fulfilment --env test

2 つ目の subscriber (promotion) の作成

前のステップでは、ordersTopic トピックに送られた各メッセージを処理する subscriber サービスを作成しました。しかし、いくつかのマイクロサービスでは、受け取った全てのメッセージを処理する必要はなく、特定の特徴を持った一部のメッセージのみ処理する必要があります。そのため、多くの顧客は新しいトピックを作成したり、またはメッセージを処理するかどうかを決定するコンシューマーで何かしらの前処理をします。しかし、それは良いプラクティスではありません。ベストプラクティスは、SNS のネイティブの機能を利用することです。SNS は、メッセージコンテキストに沿ってメッセージ属性を公開することができるため、subscriber はサブスクリプションフィルタリングポリシーを指定して、受信するメッセージを定義できます。これにより、追加のトピックの作成 または 前処理が不要になります。

このステップでデプロイするサービスは、promotion という名前で、$80 以上の注文を処理します。このシナリオでは、20% のクーポンコードが発行され、次回の購入時に利用できます。先にデプロイしたサービスと同様に、以下を実行してサービスを作成します。

copilot svc init \
    --app pubsub \
    --svc-type "Worker Service" \
    --name promotion \
    --port 5000 \
    --dockerfile "subscribers/promotion/Dockerfile" \
    --subscribe-topics "publisher:ordersTopic"

SNS トピックサブスクリプションのフィルターポリシーを指定する新しいセクションを copilot/promotion/manifest.yml マニフェストファイルに追加します。

subscribe:
  topics:
    - name: ordersTopic
      service: publisher
      filter_policy:
        amount:
          - numeric:
              - ">="
              - 80
  queue:
    dead_letter:
      tries: 3

マニフェストを修正した後、以下のコマンドで Amazon ECS 及び AWS Fargate に subscriber サービスをデプロイすることができます。

copilot svc deploy --name promotion --env test

動作確認

全てのマイクロサービスを作成したので、想定通りに機能するかテストします。ブラウザを開き、publisher サービスを作成後に得られたロードバランサーの DNS 名を入力して下さい。もし、DNS 名を忘れてしまった場合、以下のコマンドを実行してください。

copilot svc show --name publisher --json | jq '.routes[0].url'

または、以下を実行して下さい。

copilot svc show --name publisher

そして、COPILOT_LB_DNS という変数の値をコピーして下さい。

ページを更新するたびに、新しい名前と注文の合計が生成されますが、必要に応じてフィールドを変更することができます。

このとき、裏側で起きている事象を知るためには、ターミナルを開き、以下のコマンドを実行して下さい。

copilot svc logs \
    --name publisher \
    --env test \
    --follow  \
    --since 1s

上記のコマンドで publisher サービスのログを表示することができるのため、ウィンドウを調整すれば、フロントエンドの画面とターミナルで同時に確認することができます。最初は何も表示されないかもしれませんが、送信ボタンを押すとすぐに以下のような出力が表示されるはずです。

2 つの subscriber サービスで起きていることを確認するために、以下のコマンドでログを確認します。

copilot svc logs \
    --name fulfilment\
    --env test \
    --follow  \
    --since 1s

copilot svc logs \
    --name promotion \
    --env test \
    --follow  \
    --since 1s

同時に 3 つのターミナルを起動することをオススメします。それにより、各マイクロサービス内でどのような処理がなされているのか確認することができます。さまざまな金額を入力して、閾値を満たさない注文が promotion マイクロサービスでどのように処理されないかを確認してください。注文が永続化されていることを確認するため、送信された全注文を保持する DynamoDB 内のアイテムテーブルを調べることもできます。

クリーンアップ

将来的な料金の発生を避けるため、リソースを削除します。デモのリソースを正しく作成した場合、以下のコマンドを実行することで全てのサービスと関連するインフラストラクチャは削除されます。

copilot app delete pubsub

おわりに

AWS Copilot CLI によって pub/sub アーキテクチャを簡単に構築できました。サービスに必要なインフラストラクチャやポリシーの作成に時間を費やすのではなく、必要なリソースをデプロイするのに役立つサービステンプレート と コマンドを使用することで、本当に重要なことに集中できるようになりました。AWS Copilot CLI は、SNS トピックの構築、SQS キューの構築、サブスクリプションの設定、フィルターポリシーの設定、DLQ の構築、再配送設定、サービスへ URI の設定ができるため、publisher と subscriber を作成することが、これまで以上に簡単になりました。

AWS Copilot は、オープンソースのツールで、パブリックロードマップから状況を確認することができます。また、GitHub issues の作成や Gitter でのディスカッションに参加頂けると幸いです。

それでは。

翻訳はソリューションアーキテクト祖父江が担当しました。原文はこちらです。