Cloud PubSubとSendGridを使ってメール送信機能を切り離す


PrAha Inc.のCEO兼エンジニアのdowannaです

サービス内でなんらかのアクションがあった時にユーザにメール通知したい時、ありますよね。
でもサーバにメール送信まで任せてしまうと、どこまでサーバに対して負荷がかかるか心配です。

そんな時はPubSubなどのキューイングサービスを使って、メール送信機能を本体から切り離したいもの。
今回の記事はPubSubとSendGridを使ったメール送信機能のマイクロサービスを作ってみます。

やりたい事

  • 送信予定のメールをPubSubのmessageとしてキューしておく
  • 1日1回、PubSubからmessageをpullしてメール送信
  • メール送信が完了したらmessageを削除

大まかな作業の流れ

  • Topic, Subscriptionを作成
  • Topicに向けてmessageを配信
  • SubscriberClientを作成して、messageを受信
  • messageを使ってメールをsendgridから送信
  • 送信を確認次第、messageをacknowledgeしてPubsubから削除

Topicに向けてmessageを配信

まずはPubSubにTopicを作成する必要がある。

Topic作成方法1:GCPコンソール

コンソールを開き、Pub/Subを選択して、画面上の「トピックを作成」から作成する

Topic作成方法2:CLIから以下コマンドを実行

ローカルで認証済みのCLI、もしくはGCPコンソールから「クラウドシェル」を起動して、Topic作成

gcloud pubsub topics create my-topic

SDK使ってTopicを作る事も可能だけど、ひとまずこの2つを知っておけば困らないのではなかろうか。

Topicにmessageを飛ばす

import { PubSub } from '@google-cloud/pubsub'

const pubsub = new PubSub({ projectId: 'some-project-id' })
const topic = await pubsub.topic('my-topic')
await topic.publishJSON({ text: 'hello!' })

これだけで、先ほど作成した my-topic にメッセージが飛ぶ。

例えばユーザが誰かに「いいね」を送った時にメール通知したいのであれば、いいね直後にサーバからtopicにpublishすれば良い。
メール送信の負荷とか、送信の成否とか一切気にする事無く、ただPubSubに丸投げすれば良いので、
システムの依存性や関心の範囲を少し下げられる。

今回はメール送信に使う本文 'hello!' をPubSubに貯めてみた。

1日1回、PubSubからmessageをpullしてメール送信

まずはコンソールでmessageをpullしてみる

まずメッセージを取得するためには、Topicだけではダメ。
Subscriptionも作成しなければいけない。
CLIで、こんなコマンドを叩いて
topic: my-topic に
subscription: my-sub を追加する

gcloud pubsub subscriptions create --topic my-topic my-sub

これでmessageをpull出来るようになった。
試しにさっきpublishJSONしたmessageを確認してみよう。

コンソール画面から、my-topicを選択して「メッセージを表示」をクリックする

先ほど作成したsubscriptionを選択する

そしてPULLをクリックする

先ほど送信したmessageが取得できる事が確認した。
あとは今のコンソールと同じことを、コードからクライアントライブラリを使って実装してみよう。

クライアントライブラリを使ってmessageをpullしてみる

pullの方法は大きく分けて非同期/同期pullが用意されている。
今回は「すでに登録されているmessageをまとめて取得したい」ので、
公式サイトに載ってるsynchronous pullを使ってみる。

まずはv1パッケージをimportしてSubscriberClientを初期化する

  import { v1 } from '@google-cloud/pubsub'
  const subClient = new v1.SubscriberClient()

続いてclientに「どのSubscriptionからmessageを取得するか、および取得上限数」を定義する。

  const formattedSubscription = subClient.subscriptionPath(
    projectId, // 今回なら some-project-id
    subscriptionName // 今回なら my-sub
  );

  const request = {
    subscription: formattedSubscription,
    maxMessages: 10, // とりあえず上限10通
  };

最後にpullする。これでコンソールで実行したのと同じ事が実装できた。

  const [response] = await subClient.pull(request);

ちなみにこの時点では、まだmessageは削除していない。

メール送信が完了したらmessageを削除

ここから先は各々の実装により異なるけど、例えばsendGridを使っている場合を想定すると、こんな感じ。

    const ackIds = []

    for (const receivedMessage of response.receivedMessages) {
      SendGrid.send({
        to: '[email protected]',
        from: '[email protected]',
        subject: 'hello world',
        text: receivedMessage.message.data.toString() // ByteなのでStringに変換しておく
      })

      // 失敗した場合の処理はここでは考えていない
      ackIds.push(receivedMessage.ackId) // あとで削除するためにAcknowledgeIdを保管しておく
    }

実務であれば「メール送信に失敗した場合、どうする?message消しちゃって大丈夫?」と検討するけれど、ここでは一旦気にせずackIdsにpushしちゃおう。

全てのメール送信タスクを投げるのに完了したら、これでmessageを一括消去する

    const ackRequest = {
      subscription: formattedSubscription,
      ackIds: ackIds
    }
    await subClient.acknowledge(ackRequest)

acknowledgeされたmessageはPubSubから消える。

以上でPubSubを使ったメール配信の実装が完了した。

まとめ

  • Topic, Subscriptionを作成
  • Topicに向けてmessageを配信
  • SubscriberClientを作成して、messageを受信
  • messageを使ってメールを送信
  • 送信を確認次第、messageをacknowledgeしてPubsubから削除