SESを待機系受信サーバとして活用してみました

2023.02.22

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

6年くらいほぼ無停止で稼働を続けていた我が家のサーバですが、
近々配置換えで半日程度ですが停止することにしました。

中身的には自分用のサービスで一時停止になっても基本困らないようにしてるのですが、
受信メールサーバだけは流石に取りこぼしが起きてしまうため
せっかくの機会なのでSESを使って冗長化することにしました。

今回の想定範囲

基本的には停止した場合でもその間のメール受信が継続でき、
後々そのメールを取得先を変えずに閲覧可能というレベルを考えております。

停止中も継続的にメールの送信や閲覧できるまでは考えていません。

構成

以下のような構成をとっています。

冗長化の手法としてはにMXレコードの優先度を利用した振り分けを利用しています。

% dig xxxx.com mx +short
10 mail.xxxx.com.
100 inbound-smtp.us-east-1.amazonaws.com.

SESの受信機能の料金的には利用しなければ$0です。
万が一使う場合でも自分の普段の受信料的に$1未満に収まりそうです。

https://aws.amazon.com/jp/ses/pricing/
0〜1,000 件の E メール 0 USD 受信メールチャンク 1,000 件につき 0.09 USD (詳細については、「料金の詳細」をご覧ください) 1,000 件を超える E メール 0.10 USD/1000 件の E メール

受信の設定は東京リージョンのマネコンでは確認できませんでした。

調べてみたところ受信のエンドポイントはus-east-1、us-east-2、eu-west-1のみとなるようです。
Amazon Simple Email Service エンドポイントとクォータ

今回はus-east-1を利用しています。

メールユーザの管理について

受信するメールアドレスについては受信ルールで管理しています。
イメージとしては以下のような感じです。

流石に存在しないアドレスに受信はさせたくないことと、
ドキュメントを読む限りS3格納時のプレフィックスも動的にできなさそうなため、
ユーザ毎に受信ルールを作成しています。

メールユーザの管理は定期的にメールユーザの一覧を取得し
CFnのマクロでいい感じにそれを元にルールを作る方法を考えています。

ただしばらくユーザが増えることはないのでまだこの部分は組み込んでいません。
(ひとまずCFnテンプレート上にベタ書きしてます)

実装

以下のようなCFnテンプレートでリソースを作成します。
ルールセットはCFnで有効化できないため実行後に別途マネコンから手動で有効化します。

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  MyDomain:
    Type: String
  MailUser:
    Type: String
Resources:
  RuleSet:
    Type: AWS::SES::ReceiptRuleSet
    Properties: 
      RuleSetName: main-ruleset
  RuleDetail:
    Type: AWS::SES::ReceiptRule
    Properties: 
      RuleSetName: !Ref RuleSet
      Rule: 
        Name: !Sub ${MailUser}_${MyDomain}
        Recipients: 
          - !Sub ${MailUser}@${MyDomain}
        Enabled: True
        TlsPolicy: Require
        ScanEnabled: True
        Actions:
          - S3Action:
              BucketName: !Ref MailBucket
              ObjectKeyPrefix: !Ref MailUser
  MailBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${MyDomain}-mail-stocker
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - BucketKeyEnabled: True
            ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
  MailBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref MailBucket
      PolicyDocument: 
        Version: 2012-10-17
        Statement: 
        - Sid: AllowPutFromSES
          Effect: Allow
          Principal: 
            Service: ses.amazonaws.com
          Action: s3:PutObject
          Resource: !Sub arn:aws:s3:::${MailBucket}/*
          Condition: 
            StringEquals: 
              aws:Referer: !Ref AWS::AccountId

SNSを設定しておくと擬似的な死活監視が兼用できていいかもしれません。

メールサーバダウン時の挙動を見る

稼働しているメールサーバ機のpostfixを停止し別のメールアドレスから送信してみます。

せっかくなので送信側のログも見てみたかったのですが
メールサーバの設定を少し触らないといけないため別の機会にやろうと思います。

受信失敗

メールを送信してみましたが送信元に配信失敗のメールが届きました。

Reporting-MTA: dns; googlemail.com
Received-From-MTA: dns; xxxxx@gmail.com
Arrival-Date: Fri, 27 Jan 2023 04:11:23 -0800 (PST)
X-Original-Message-ID: <xxxxx@gmail.com>

Final-Recipient: rfc822; xxxxx@xxxxx.com
Action: failed
Status: 5.1.1
Remote-MTA: dns; inbound-smtp.us-east-1.amazonaws.com. (176.32.101.207, the
  server for the domain xxxxx.com.)
Diagnostic-Code: smtp; 550 5.1.1 Requested action not taken: mailbox
unavailable
Last-Attempt-Date: Fri, 27 Jan 2023 04:11:24 -0800 (PST)

Remote-MTAヘッダを見る限りSES側には届いているため
MXレコードによる切り替えは成功しておりSES側の設定に問題がありそうです。

CloudTrailにもそれらしき出力がなくどうしようかと思いドキュメントを見直していたところ
今回SESを利用するリージョンではドメインの検証を済ませてないことに気づきました。

受信成功

DKIMレコードを登録し検証を済ませ再度送信したところ無事にS3に配信が確認されました。
サンドボックスの解除については受信機能のみを利用する場合は不要です。

ドキュメントに明確な記載はなかったのですが
AMAZON_SES_SETUP_NOTIFICATIONは設定した時に生成されるファイルのようです。

S3に保存されるファイルはeml形式で格納されていました。 (AMAZON_SES_SETUP_NOTIFICATIONもeml形式となっていました)

Return-Path: <xxxxxx@gmail.com>
Received: from mail-pl1-f171.google.com (mail-pl1-f171.google.com
[209.85.214.171])
  by inbound-smtp.us-east-1.amazonaws.com with SMTP id xxxx
  for xxxx@xxxx.com;
  Fri, 27 Jan 2023 12:19:20 +0000 (UTC)
X-SES-Spam-Verdict: PASS
X-SES-Virus-Verdict: PASS
Received-SPF: pass (spfCheck: domain of _spf.google.com designates
209.85.214.171 as permitted sender) client-ip=209.85.214.171;
envelope-from=xxxxx@gmail.com; helo=mail-pl1-f171.google.com;
Authentication-Results: amazonses.com;
  spf=pass (spfCheck: domain of _spf.google.com designates
209.85.214.171 as permitted sender) client-ip=209.85.214.171;
envelope-from=xxxx@gmail.com; helo=mail-pl1-f171.google.com;
  dkim=pass header.i=@gmail.com;
  dmarc=pass header.from=gmail.com;
X-SES-RECEIPT:....

メールサーバ側に取り込み

前述の通りS3にはeml形式で保存されていますので
そのままメールサーバにコピーすればメーラー側で表示可能です。

ファイル名の方は実際のメールを見ると何かしらの規則性は有りそうですが
調べてみてもそれらしき情報がいまいち見当たらないのでそのまま取り込んでみます。
Maildir形式で保持してるので/home/{user_name}/Maildir/new/に取り込みます。

$ aws s3 cp s3://xxxxxxxx-mail-stocker/xxxxx/c482gncvv28ocnss24o02r1baqrgsskig74lmp81 ./
download: s3://xxxxxx-mail-stocker/xxxx/c482gncvv28ocnss24o02r1baqrgsskig74lmp81 to ./c482gncvv28ocnss24o02r1baqrgsskig74lmp81
$ ls -ltr
total 8
-rw-r--r-- 1 xxxxxx xxxxxx 4727 Jan 27 21:19 c482gncvv28ocnss24o02r1baqrgsskig74lmp81

メーラーで確認してみると取り込まれ表示されることが確認できました。

実際の稼働としては少しスクリプトを組み込んで、
取り込み->取り込み済みのものは削除という感じのものをcrontabで定期的に動かすようにしています。

終わりに

今回SESを使うことでサービス費用も実装コストを抑えて受信サーバの待機系(しかもアクティブ)を準備することができました。

自宅環境だとなかなか使うこともあるかわからない待機系のためにサービス費用を払って...
となるところですがこの方法なら維持自体には費用がからないため非常にありがたく思ってます。

余談ですが改めて思い化してみるとサーバ機としての稼働は6年ですが、
それ以前(非サーバ時代)を含めるとトータル10年近くパーツ交換なしで動いていました。

流石にいつ壊れてもおかしくないので実は今回の冗長化はいい機会だったのかもしれません。