GAE/Goでメールサーバを実装したら、100万通以上のメール受信が年間2000円程度しかかからなかった話


Motivation

業が深いエンジニアの chidakiyo は月に数万通のメールを受け取る必要がありました。
Office365で受信した場合、すぐにメールボックスがパンパンになり、しかも削除もなかなかしんどい作業という二重苦にさいなまれていたため、Appengineでメールのアーカイバを実装してみました。
1年程テスト的に動かしてみたのでその知見を共有しようと思います。

日々大量のメールに苦しめられている方や、メールをトリガに何かしたい方の参考になるのではと思います。
ですが、

  • GAEはメールを受信してさばけるという話
  • GAEのスケジューラ(cloud schedulerではない)の進化によるコストの削減(?)と、GAEの圧倒的なコストパフォーマンスに関して

を書き残そうと思いますので細かい実装例は説明しません。

以下の例は go 1.9 ランタイムを利用している例になります。

構成

  • 謎のメール送信者たちからかなかなか大量のメールが送られる(苦
  • 受信したメールサーバでGAEのサーバへもメールを転送する
  • GAEサーバは受信したメールをパースし、DB(Datastore)に格納する

以下の例はメールサーバでリレーしているような図になっていますが、GAEが直接メールを受信することもできます。

GAEでメールを受信する要点

詳しいことは ドキュメント にすべて書かれていますが、簡単に要点をまとめます。

app.yaml に対する設定

inbound_services:mail の定義が必要

app.yaml
inbound_services:
- mail

メールを処理するハンドラの定義が必要

Appengine で受信したメールは /_ah/mail/{mailaddress} のパスに対してのPOSTリクエストとして処理できます。
そのため、以下のようなハンドラの設定が必要です

app.yaml
- url: /_ah/mail/.+
  script: _go_app
  login: admin
  • メールを処理するために必要な app.yaml 全体は以下のような感じになると思います
app.yaml
runtime: go
api_version: go1.9

inbound_services:
- mail

handlers:
- url: /_ah/mail/.+
  script: _go_app
  login: admin

Appengineで受信できるメールアドレスに関して

Appengineでは {user}@{app_id}.appspotmail.com のようなアドレスでメールを受信することができます。
app_id はGCPのプロジェクトのIDです。

user の部分は任意の文字列で受信することができます。
雑な例ですが、成功した際のメールは success@{app_id}.appspotmail.com 失敗時は failure@{app_id}.appspotmail.com などのように送ることができます。
アプリケーションを作成する際に事前に定義する必要はありませんので、送る側で自由に決めて送れます。
ハンドラ側でpathをパースして利用することができます。

メールのドメインは appspot ではなく appspotmail です。注意。
また、カスタムドメインでのAppengine単体でのメール受信は試したことがないので利用できるかは確認していません。

go 側の実装

コードはドキュメントに例があるので出しませんが、ポイントを書きます。

ハンドラは /_ah/mail/:mailaddress でPOSTとして処理できるように実装する

AppengineのWebアプリケーションを作った人ならあまり困らないと思います。
:mailaddress の部分にはAppengineで受信したメールアドレスが入ります。(↑で書いたとおりです)

また、メールのTo, Subject, Bodyなどはマルチバイト文字を利用する場合にはMIMEを判別してデコードする必要があります。
私は github.com/curious-eyes/jmail をカスタムするなどしてそれっぽい雰囲気に仕上げましたが、goで日本語を含むMIMEをうまく扱えるライブラリをご存知の方がいれば教えてください。

GCP(GAE)の運用コスト

1年運用してみたコストは以下のような感じになります。

年月 受信数 GAEインスタンスコスト
2017/12 85171 $0.00
2018/01 97109 $0.12
2018/02 84407 $1.82
2018/03 107962 $2.66
2018/04 124757 $12.34
2018/05 84879 $0.47
2018/06 66939 $1.67
2018/07 66000 $0.30
2018/08 93662 $0.00
2018/09 56384 $0.00
2018/10 63274 $0.00
2018/11 73282 $0.04

処理としては

  • メールを受信する
  • メールをDatastoreにアーカイブする

ということをしていますが、結構なメール通数を処理していますが、インスタンスコストはそれほど高くなりませんでした。(2018/4のみ何故か高いですが)
上記のコストの他に別途、Datastoreへのデータ格納費用($2.56/year)とWrite費用($0/year)がコストとしてかかっています。

ざっくり、年間100万通のメールをアーカイブして2000円少々というコスト感なので、小学生のお小遣いでも運用できますね。

ちなみに、2018年の5月以降、インスタンスコストが減少しているようにみえます。
これは、メールの数が減っているというのもありますが、 こちらの Increase performance while reducing costs with the new App Engine scheduler の記事にある、Appengine のスケジューラの update による影響もありそうです。

実装的には何も変更せずに単に安くなったので非常にありがたいアップデートです。

内容としては過去のスケジューラと比較し

  • 中央値とテールでレイテンシが平均5%減少
  • コールドスタートのリクエスト数が平均30%減少
  • 平均7%コスト削減

と記事にはありますが、条件によってはかなりコスト削減できるという感じなのでしょうか。

Appendix 1 : アーカイブしたメールを柔軟な条件で検索したい場合 (like検索的な)

Datastore にアーカイブしているので like 検索のようなものは苦手です。
ユースケースにもよりますが、Datastore のバックアップを定期的に BigQuery に load し、BQ 上でクエリするのがコスト的にも良さそうです。

その際には ds2bq というプロダクトが便利です。
上記は GCS -> GAE の通知に OCN(object change notification) を利用していますが、GCPのドキュメントはpub/sub notification をおすすめしているようです。
pub/sub notification と BQ の Partition 機能を有効にするためにフォークしたプロダクトを試しに作っていますが、そちらを利用したい方は こちら を利用してみてもいいかもしれません。(動作確認レベルで作ったものなので、pub/sub の設定方法などはまだ記述していません)

Datastore → BQ への投入方法はいろいろ方法があると思いますが、BQ に登録さえされれば SQL を利用してなんとでもできるのでおすすめです。

もしくは GAE の Search API という手もあると思います。

Appendix 2 : カスタムドメインでメールを受信したい

Appengine でカスタムドメインでのメール受信が設定可能かは調査していないのですが、過去に独自のドメインでメールを受信しかつ、GAEでさばきたいという場合には mailgun を利用していました。

mailgun のとある機能でメールを受信し、そのメールを mailgun が webhook として GAE を実行するというイメージです。

こちらは興味ある人がいるようならまた別途記事を書こうと思いますのでリアクションがあると嬉しいです。

まとめ

  • GAEはメールも捌ける
  • しかも安い
  • Datastoreに入れずに直接BQでも良さそう(streaming insertだとちょっとコスト増えそう)
  • go11.1(GAE gen2)だとどうなるんでしょうね。

それでは。