Postfix aliasesとS3とSWFでメール受信時にイベントドリブン的に堅く処理する

**本投稿は賞味期限切れ**

[Postfix Advent Calendar 2014](http://qiita.com/advent-calendar/2014/postfix) 22日目の記事です。

メールの内容解析して別システムに取り込む処場合など、Postfixが受信したメールを

* できるだけ早く
* メールをロストさせずに非同期で何か処理する

のやり方の一つです。
環境はAWSです。

できるだけ早くメールの受信に気づく
==============================

まず考えるのが、

* 数分毎にCRONでスクリプト起動して、POP3などでポーリングする

この方法だとCRON実行間隔の分のタイムラグがイケてない。ビミョーです。

イベントドリブン的にやる方法はないものかとググってみたら、
Postfix aliasesを使うと[メールの受信トリガーに何かスクリプトをキックできる](http://www.plustar.jp/lab/blog/?p=9303)らしいことがわかりました。

素振りとして、メールを受信したタイミングでスクリプトを実行して、メールの内容ファイルに出力してみます。

* /etc/aliasesを編集

```diff
[root@postfix5 etc]# git diff
diff --git a/aliases b/aliases
index 1069e99..dd52bff 100644
--- a/aliases
+++ b/aliases
@@ -94,3 +94,5 @@ decode:               root

 # Person who should get root's mail
 #root:         marc
+
+root: "| /usr/local/bin/ruby /tmp/hoge.rb" 
```

* メールは標準入力から渡される

```ruby
[root@postfix5 etc]# cat /tmp/hoge.rb 
File.write('/tmp/fuga.txt', $stdin.read)
```

* /etc/aliasesを編集したらpostaliasコマンドの実行が必要

```bash
[root@postfix5 tmp]# postalias /etc/aliases
[root@postfix5 tmp]# service postfix reload
```

注意点は、Postfix aliasesでキックされるスクリプト実行ユーザのデフォルトはnobodyであること。
キックするスクリプトの配置場所によっては権限のエラーになります。
これで数時間ハマりました;-)

```diff
[root@postfix5 postfix]# git diff
diff --git a/main.cf b/main.cf
index 1267fc7..591df6f 100644
--- a/main.cf
+++ b/main.cf
@@ -63,7 +63,7 @@ mail_owner = postfix
 # These rights are used in the absence of a recipient user context.
 # DO NOT SPECIFY A PRIVILEGED USER OR THE POSTFIX OWNER.

-#default_privs = nobody
+default_privs = ec2-user

```

というわけでCRONのポーリング的なやり方ではなく、
イベントドリブン的に処理するためにはPostfix aliasesが使えそうです。


メールをロストせずに非同期でスケールアウト可能な仕組み
==============================

Postfix aliasesがキックするスクリプトが激重な場合、メール受信処理に影響が出そうなので、非同期的に処理したほうがいいんじゃね？スケールアウト可能な仕組みにしたい。
スクリプト内でエラーが起きた場合のメールのロスト対策やリトライはどうしようか。。
悩みは続きます。

僕は以下のようなことを実現したいんです。

* Postfix aliasesがキックするスクリプトはできるだけ軽くしたいので非同期的に処理する
* スクリプト内でエラーが起きた時に、メールをロストせずにリトライ可能な仕組み
* スケールアウト可能な構成

今回はAWS環境なので、これらを実現するためには、[Amazon Simple Workflow (SWF)](http://aws.amazon.com/jp/swf/)が使えそうです。
SWFの使い方は、この[サンプル](https://github.com/awslabs/aws-flow-ruby-samples/tree/master/Samples/hello_world)が参考になりました。

最初SWFにメールの内容を丸々渡そうと考えましたが、SWFの実装上の仕様で引数の最大サイズが32768であることがわかったので、まずS3にぶち込んでからそのURLをSWFに渡すようにしました。

* SWFの引数サイズが大きい場合以下のエラーが発生

```ruby
 at 'input' failed to satisfy constraint: Member must have length less than or equal to 32768
    from /usr/local/lib/ruby/gems/2.0.0/gems/aws-sdk-1.51.0/lib/aws/core/client.rb:476:in `client_request'
    from (eval):3:in `start_workflow_execution'
    from /usr/local/lib/ruby/gems/2.0.0/gems/aws-flow-1.3.0/lib/aws/decider/workflow_client.rb:373:in `start_external_workflow'
    from /usr/local/lib/ruby/gems/2.0.0/gems/aws-flow-1.3.0/lib/aws/decider/workflow_client.rb:193:in `start_execution'
    from /home/ec2-user/aws-flow-ruby-samples/Samples/postfix_to_redis/lib/postfix_to_redis_workflow_starter.rb:9:in `<main>'
```

というわけで、最終的にこんな感じの構成になりました。
![postfix.png](https://qiita-image-store.s3.amazonaws.com/0/22462/ed137cd6-818e-5f3d-7380-cff91da65daa.png)

* メール受信トリガーでPostfix aliasesでSWFのstarterを実行
* starterの中ではまず最初にS3にメール本文をupload。成功した時点でメールのロストはなくなる
* （失敗したらEBSにダンプしてCRONが自動でS3にupload）
* S3にuploadできたらSWFをキック(引数はS3上のURL)。
* SWFのActivityの中でRailsのRest APIを叩く。（失敗したらSWFの機能でリトライ）
* SWFの処理がボトルネックになった場合は横にスケールアウトする

まとめ
=================
* Postfix aliasesでメール受信トリガーでスクリプトをキック
* S3に入れた時点でメールのロストはほぼなくなる（イレブンナイン）
* SWFを使えば非同期処理、リトライ可能、スケールアウト可能
* AWSパネェまじで