Amazon SNS + SQSを使って30分で構築する、なんちゃってpub/subシステム

 [Ateam Lifestyle Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ateam-lifestyle) の11日目は[株式会社エイチームライフスタイル](http://life.a-tm.co.jp/)のWebエンジニア@kyntkが担当します。

最近はReact、TypeScriptでの開発や、Lambda、DynamoDBを使ったサーバーレス系のインフラを触っております。

## はじめに
今回はAmazon SNS、Amazon SQSを使用して、イベント駆動のpub/subメッセージングを実現したいと思います。今まで使ったことはなかったのですが、イベント駆動のマイクロサービスアーキテクチャをこの機会に手軽に実装してみたいなと思い触ってみました。初めて触るサービスはまず料金が気になるところですが、今回の内容は**無料枠内**でできるので、気軽に手を動かしながらやってみてください。

初めて使ってみたので、どのように利用するとより有用なのかがまだ分かっていないので、もしご指摘がありましたらコメントいただきたいです！！！

## なぜこれをやりたかったか

pub/subを利用して、イベント駆動のマイクロサービスアーキテクチャの利点を受けたいと思ったからです。

ユーザーの申し込みを受けて、その後ユーザーにメール送信をしたり、外部のAPIとのやりとりをするサービスがあったとします。通常は、レスポンスを受けて、バリデーションを行い、データの保存、メール送信...とすべて一連の流れで処理を行います。これを小さいサービスに分けることで以下のようにすることができます。

1. データの保存までを行う
2. 保存されたデータを受け取って、メール送信を行う
3. 保存されたデータを受け取って、外部APIとのやりとりを行う

そして上記1のデータ保存の後、2、3との間をつなぐ部分にSNS、SQSを活用しようと思いました。

1〜3のサービスはインターフェースを決めておくことでお互いを意識せず、別々に開発することができます。このようにサービスを分離することで、ひとつのサービスの変更が他サービスへ及ぼす影響を減らすことができます。

|大きな1つのシステムの場合|小さな複数のサービスに分けた場合|
|----|----|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/7249d315-66bf-22e6-b14f-438e4ef5c726.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/373b772a-832d-12f9-8195-c448f7bd3868.png)|

また、上記1のデータ保存は2、3以降については何を行うかという点には関与しないので、4が追加されたり、3がなくなったりしても1の変更をせずに済み、メッセージの送り手と受け手の分離をすることができます。

また、その他の理由として、SNSとSQSは完全マネージドなサービスとなっており、面倒な保守について考えなくて済むこともメリットとなっています。

## 概要

まずざっくりSNSとSQSについて説明します。

### ■ SNSとは

SNSは完全マネージドなpub/subメッセージングサービスです。メッセージを受けた後、複数のサブスクライバーエンドポイントに通知することができます。メッセージのフィルタリングができ、今回の記事では、このフィルタリング機能を使用します。

参考: [Amazon SNS（サーバーレスアプリのための pub/sub メッセージングサービス）| AWS](https://aws.amazon.com/jp/sns/?whats-new-cards.sort-by=item.additionalFields.postDateTime&whats-new-cards.sort-order=desc)

### ■ SQSとは

SQSは完全マネージド型のメッセージキューイングサービスです。

参考: [Amazon SQS（サーバーレスアプリのためのメッセージキューサービス）| AWS](https://aws.amazon.com/jp/sqs/)

### ■ SNSとSQSの関係について
SNSはメッセージを受けた後、メッセージのフィルタリングをして、サブスクライブをしているキューにのみメッセージの配信を行います。

下記画像では今回の内容に合わせてJSONメッセージを送っていますが、textメッセージも送れます。
![sns_summary.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/179cf1ff-8075-d636-19d6-3bcd5ae17a74.png)

### ■ 今回作ってみたもの
![sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/237cdb19-376d-b445-434b-2576522603f1.png)

1. Lambda1: メッセージのパブリッシュ
2. SNS: メッセージをフィルタリングし、サブスクライブしているSQSに送信
3. SQS: メッセージをキューに蓄積
4. Lambda2: 検証用のログ目的

Lambda1はDynamoDBの保存などのイベントを受けてメッセージをパブリッシュする役割を想定しています。

Lambda2は今回はログ出力しかしませんが、マイクロサービスを想定しています。

今回はSNSのサブスクリプションとSQSを1つしか構築していませんが、紐づくサービス数に応じて、こちらを増やすことで適切なサービスにメッセージを通知する仕組みを構築することができます。

## やってみる

どんどん構築してみましょう。

### ■ SNS構築

#### ◯ Topicの作成

まずはTopicを作成します。設定は細かくできますが、お試しなので、デフォルトの設定のままで大丈夫です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/48da9808-d920-d58d-96ed-e0eb8edd2837.png)

### ■ SQS構築

#### ◯ キューの作成
標準キューで作成してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/64a22b60-6afa-52f8-1ba1-219b8a0d0002.png)

#### ◯ トピックへのサブスクライブ

キューの一覧から、「キュー操作」→「SNSトピックへのキューのサブスクライブ」を選択し、先程作成したトピックを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/5017e305-b20f-7a3f-2d91-fd5483e583a6.png)

ここでSNSに戻り、サブスクライブを確認してみると、サブスクライブ一覧に、先程作成したキューのエンドポイントが追加されているのが確認できます。

また、SQSのTestQueueのアクセス許可を確認してみると、TestTopicからのSendMessageのみが許可される設定も追加されています！

### ■ Lambda1の構築

#### ◯ Functionの準備
次はLambdaの構築をします。今回はNode.jsを使用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/7a53aca4-af91-6da6-57c0-52a67710e817.png)

#### ◯ 実装

先に実装したコードを載せます。

```js
const AWS = require('aws-sdk')

exports.handler = async event => {
  const _message = {
    default: 'default message',
    sqs: {
      // 申込みに関する情報
      entry: {
        name: 'Sample Name',
        telNo: '09012345678',
      },
    },
  }
  // ダブルクオートをエスケープする必要があるので二段階で行う
  _message.sqs = JSON.stringify(_message.sqs)
  const message = JSON.stringify(_message)

  AWS.config.update({ region: 'ap-northeast-1' })
  const params = {
    TopicArn: '(topicのarn)',
    // JSONの場合はこの記述が必要
    MessageStructure: 'json',
    Message: message,
    //フィルタリングをするために必要
    MessageAttributes: {
      event_type: {
        DataType: 'String',
        StringValue: 'entry',
      },
    },
  }
  const publishTextPromise = new AWS.SNS({ apiVersion: '2010-03-31' })
    .publish(params)
    .promise()
  try {
    const data = await publishTextPromise
    console.log(`Message: ${params.Message}, topic: ${params.TopicArn}`)
    const response = {
      statusCode: 200,
      body: 'success!',
    }
    return response
  } catch (err) {
    console.error(err)
    const response = {
      statusCode: 500,
      body: 'error!',
    }
    return response
  }
}

```

上記のコードについて説明をしていきますが、まず関数の保存を忘れずに！

##### 送るメッセージ
以下のオブジェクトのsqsに入っているものは、申し込みに関する情報です。

```js
  const _message = {
    default: 'default message',
    sqs: {
      // 申込みに関する情報
      entry: {
        name: 'Sample Name',
        telNo: '09012345678',
      },
    },
  }
  // ダブルクオートをエスケープする必要があるので二段階で行う
  _message.sqs = JSON.stringify(_message.sqs)
  const message = JSON.stringify(_message)
```

データは以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/a691fb5d-2259-4d31-aa4f-19579225fad6.png)

##### メッセージの送信

メッセージに関する情報を記述していきます。特にJSONで送る場合は`MessageStructure`が必要ですのでご注意を。`MessageAttributes`は後ほど説明するメッセージのフィルタリングで使用します。

```js
  const params = {
    TopicArn: '(topicのarn)',
    // JSONの場合はこの記述が必要
    MessageStructure: 'json',
    Message: message,
    //フィルタリングをするために必要
    MessageAttributes: {
      event_type: {
        DataType: 'String',
        StringValue: 'entry',
      },
    },
  }
  const publishTextPromise = new AWS.SNS({ apiVersion: '2010-03-31' })
    .publish(params)
    .promise()
```

#### ◯ 実行ロールに権限の追加

下部にある実行ロールで、Lambdaを実行するロールの設定画面に飛びます。IAMを開いて、該当IAMにポリシーをアタッチします。publishができればいいのですが、今回は手順の省略のために、AmazonSNSFullAccessを付けます。本来は必要な権限のみアタッチしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/cd607755-4149-e6cc-75b6-dbfc56809f50.png)

#### ◯ テスト設定の追加

メッセージ送信テストのためにテストを作成します。保存ボタンの横にあるところからテストを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/7a4fef03-cc32-3ccf-a39f-511fc1da3043.png)

### ■ Lambda2の実装

#### ◯ 実装
もう1つのLambdaはログ用です。下記のように実装しました。

```js
exports.handler = async event => {
  event.Records.forEach(record => {
    const parsedBody = JSON.parse(record.body)
    const message = parsedBody.Message
    const parsedMessage = JSON.parse(message)
    console.log('parsedMessage:', parsedMessage)

    const entry = parsedMessage.entry
    console.log('entry:', entry)
  })
}
```

#### ◯ SQSをトリガーとするための権限追加

こちらも、今回はSQSへアクセスするために、LambdaのIAMロールにAmazonSQSFullAccessを付けます。

#### ◯ トリガーの追加

SQSをトリガーに設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/7c91736d-0e60-4d68-34d6-901ba09cd58a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/efe004f3-e9f4-d6a0-5e16-f62f3057ee32.png)

選択後、SQSの画面でキューを確認すると、Lambdaトリガーが記載されていることが確認できます。

### ■ サブスクリプションの設定

最後に、SNSのフィルタリングの設定を行います。

SNSからサブスクリプションを選択し、TestQueueのサブスクリプションの詳細を開きます。サブスクリプションフィルターポリシーから、編集を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/58bdd090-6798-54e1-897e-24cf7b410e5a.png)

今回は以下のように設定し、event_typeがentryのものの場合、サブスクリプションする用に設定します。条件を配列で渡すことで複数の条件でフィルタリングができます。

```json
{
  "event_type": [
    "entry"
  ]
}
```

このように、サブスクリプションの設定を複数することで、紐付けた各サービスごとに必要なメッセージをフィルタリングをすることができます。

### ■ 確認
最後に、Lambda1のテストを実行し、確認をします。実行後、Lambda2の出力するログをCloudWatch Logsで確認すると、送られたメッセージが確認できます。

## まとめ

いかがだったでしょうか？

今回はサービスが1つ、サブスクリプションも1つだったので、あまりこの構成にする必要がありませんが、サブスクリプションを増やすことで、pub/subの構成を構築することができます。私も、これをもっと発展させていきたいと思います。

また、SNSやSQSには失敗時の設定など細かい設定もできますが、今回きちんと使いこなすことができなかったので、今後深堀りしていきたいと思います。

## 最後に

 [Ateam Lifestyle Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ateam-lifestyle)  の 12日目は、@water_resistantが投稿します。去年のアドベントカレンダーの記事もとても面白かったのですが([TensorFlow.js搭載chrome拡張で誰でも笑顔体験](https://qiita.com/water_resistant/items/ec2e91c5e4e221556c5f))今年も去年を上回る面白い記事になっていますので、絶対ごらんください！

"挑戦"を大事にするエイチームグループでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトをごらんください。
https://www.a-tm.co.jp/recruit/

## 参考

* [Simplify Your Pub/Sub Messaging with Amazon SNS Message Filtering | AWS Compute Blog](https://aws.amazon.com/jp/blogs/compute/simplify-pubsub-messaging-with-amazon-sns-message-filtering/)
* [How to Filter Messages Published to Amazon SNS Topics - AWS](https://aws.amazon.com/jp/getting-started/tutorials/filter-messages-published-to-topics/)