Amazon SES の送信ログを取得・通知する [AWS CloudFormation 利用]

本記事は、Japan AWS Ambassador Advent Calendar 2024 の 2024/12/20 付記事です。

こんにちは、広野です。

最近、社内でメールに DKIM 署名をする対応をしてまして、その機に Amazon SES (Simple Email Service) のメール送信ログアーキテクチャを見直しました。DKIM 署名の記事は後輩に任せましたので、私は Amazon SES のログ取得・通知について書きます。

アーキテクチャ

Amazon SES はデフォルトでは送信ログ取得・通知機能を用意してくれていません。そのため、AWS サービスを組み合わせてデプロイする必要があります。

これ一択、とは言いませんが、安価でスケーラブルなログ取得アーキテクチャは以下がスタンダードなアーキテクチャになると考えております。AWS CloudFormation を活用してデプロイできるようにしました。一部、手環境によって構成が変わると思ったところを手動作業にするようにしています。

  • Amazon SES のログ取得は、設定セットを利用します。ログの出力先として Amazon Data Firehose を指定した設定セットを Amazon SES の ID で手動選択する仕様になっています。
  • Amazon SES のバウンス通知は、フィードバック通知を使用します。通知先として Amazon SNS を手動選択する仕様になっています。Amazon SNS からの通知方法は管理者が選択できるようにするため、設定はしていません。図ではメールでの通知を想定して書いております。
  • Amazon Data Firehose が受け取ったログは Amazon S3 バケットに保存されます。それらを Amazon Athena からクエリしやすいよう、External Table としてカタログ化しておきます。

AWS CloudFormation テンプレート

とりあえず以下のテンプレートを流すと、赤枠のリソースがデプロイされます。手動作業については後述します。

AWSTemplateFormatVersion: 2010-09-09
Description: The CloudFormation template that creates a S3 Bucket, a Data Firehose delivery stream with dynamic partition support for collecting SES logs, a SNS topic and an Athena WG.

Parameters:
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
  LogRetentionInDays:
    Type: Number
    Description: The retention period (days) for SES logs. Enter an integer between 35 to 540.
    Default: 400
    MaxValue: 540
    MinValue: 35

  TagValue:
    Type: String
    Description: Tag value for Cost key.
    Default: SES
    MaxLength: 30
    MinLength: 1

Resources:
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------#
  S3BucketSesLogs:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ses-logs-${AWS::AccountId}-${AWS::Region}
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerPreferred
      LifecycleConfiguration:
        Rules:
          - Id: AutoDelete
            Status: Enabled
            ExpirationInDays: !Ref LogRetentionInDays
      Tags:
        - Key: Cost
          Value: !Ref TagValue

# ------------------------------------------------------------#
# SES invoke Data Firehose Role (IAM)
# ------------------------------------------------------------#
  SesFirehoseRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub SesFirehoseRole-ses-logs-${AWS::AccountId}-${AWS::Region}
      Description: This role allows SES to push logs to Data Firehose.
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ses.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies: 
        - PolicyName: !Sub SesFirehosePolicy-ses-logs-${AWS::AccountId}-${AWS::Region}
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - firehose:PutRecord
                  - firehose:PutRecordBatch
                Resource: !Sub "arn:aws:firehose:${AWS::Region}:${AWS::AccountId}:deliverystream/ses-logs-${AWS::AccountId}-${AWS::Region}"
              - Effect: Allow
                Action:
                  - logs:PutLogEvents
                Resource: !GetAtt LogGroupFirehoseSesLogs.Arn

# ------------------------------------------------------------#
# Data Firehose Role (IAM)
# ------------------------------------------------------------#
  FirehoseRoleSesLogs:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub FirehoseRole-ses-logs-${AWS::AccountId}-${AWS::Region}
      Description: This role allows Data Firehose to delivery logs in S3.
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - firehose.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: !Sub FirehosePolicy-ses-logs-${AWS::AccountId}-${AWS::Region}
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "s3:AbortMultipartUpload"
                  - "s3:GetBucketLocation"
                  - "s3:GetObject"
                  - "s3:ListBucket"
                  - "s3:ListBucketMultipartUploads"
                  - "s3:PutObject"
                Resource:
                  - !Sub "arn:aws:s3:::${S3BucketSesLogs}"
                  - !Sub "arn:aws:s3:::${S3BucketSesLogs}/*"
              - Effect: Allow
                Action:
                  - "logs:PutLogEvents"
                Resource:
                  - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/firehose/*"
    DependsOn:
      - S3BucketSesLogs

# ------------------------------------------------------------#
# Data Firehose delivery stream
# ------------------------------------------------------------#
  FirehoseStreamSesLogs:
    Type: AWS::KinesisFirehose::DeliveryStream
    Properties:
      DeliveryStreamName: !Sub ses-logs-${AWS::AccountId}-${AWS::Region}
      DeliveryStreamType: DirectPut
      ExtendedS3DestinationConfiguration:
        BucketARN: !Sub "arn:aws:s3:::${S3BucketSesLogs}"
        Prefix: "partitioned/!{partitionKeyFromQuery:EventType}/!{timestamp:yyyy/MM/dd}/"
        ErrorOutputPrefix: "errorLog/!{firehose:error-output-type}/dt=!{timestamp:YYYY}-!{timestamp:MM}-!{timestamp:dd}/"
        BufferingHints:
          IntervalInSeconds: 300
          SizeInMBs: 128
        CompressionFormat: GZIP
        RoleARN: !GetAtt FirehoseRoleSesLogs.Arn
        DynamicPartitioningConfiguration:
          Enabled: true
          RetryOptions:
            DurationInSeconds: 300
        CloudWatchLoggingOptions:
          Enabled: true
          LogGroupName: !Ref LogGroupFirehoseSesLogs
          LogStreamName: S3Delivery
        ProcessingConfiguration:
          Enabled: true
          Processors:
            - Type: MetadataExtraction
              Parameters:
                - ParameterName: MetadataExtractionQuery
                  ParameterValue: '{EventType: .eventType}'
                - ParameterName: JsonParsingEngine
                  ParameterValue: JQ-1.6
      Tags:
        - Key: Cost
          Value: !Ref TagValue
    DependsOn:
      - S3BucketSesLogs
      - LogGroupFirehoseSesLogs

# ------------------------------------------------------------#
# Data Firehose LogGroup (CloudWatch Logs)
# ------------------------------------------------------------#
  LogGroupFirehoseSesLogs:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/firehose/ses-logs-${AWS::AccountId}-${AWS::Region}
      RetentionInDays: !Ref LogRetentionInDays
      Tags:
        - Key: Cost
          Value: !Ref TagValue

  LogStreamFirehoseSesLogs:
    Type: AWS::Logs::LogStream
    Properties:
      LogGroupName: !Ref LogGroupFirehoseSesLogs
      LogStreamName: S3Delivery
    DependsOn:
      - LogGroupFirehoseSesLogs

# ------------------------------------------------------------#
# SES Configuration Set
# ------------------------------------------------------------#
  ConfigurationSet:
    Type: AWS::SES::ConfigurationSet
    Properties:
      Name: !Sub ses-logs-${AWS::AccountId}-${AWS::Region}
      DeliveryOptions:
        TlsPolicy: REQUIRE

  ConfigurationSetEventDestination:
    Type: AWS::SES::ConfigurationSetEventDestination
    Properties:
      ConfigurationSetName: !Ref ConfigurationSet
      EventDestination:
        Name: !Ref FirehoseStreamSesLogs
        Enabled: True
        MatchingEventTypes:
          - send
          - delivery
          - reject
          - bounce
          - complaint
          - renderingFailure
          - deliveryDelay
          - subscription
        KinesisFirehoseDestination: 
          DeliveryStreamARN: !GetAtt FirehoseStreamSesLogs.Arn
          IAMRoleARN: !GetAtt SesFirehoseRole.Arn
    DependsOn:
      - ConfigurationSet
      - FirehoseStreamSesLogs
      - SesFirehoseRole

# ------------------------------------------------------------#
# Athena WorkGroup
# ------------------------------------------------------------#
  AthenaWorkgroup:
    Type: AWS::Athena::WorkGroup
    Properties:
      Description: !Sub Athena Workgroup for SES logs
      Name: !Sub ses-logs-${AWS::Region}
      RecursiveDeleteOption: true
      State: ENABLED
      Tags:
        - Key: Cost
          Value: !Ref TagValue
      WorkGroupConfiguration:
        EnforceWorkGroupConfiguration: false
        PublishCloudWatchMetricsEnabled: true
        RequesterPaysEnabled: false
        ResultConfiguration:
          OutputLocation: !Sub s3://${S3BucketSesLogs}/athenaAdhocQueries/

# ------------------------------------------------------------#
# Glue Database
# ------------------------------------------------------------#
  GlueDatabase:
    Type: AWS::Glue::Database
    Properties:
      CatalogId: !Ref AWS::AccountId
      DatabaseInput:
        Description: !Sub Glue database for SES logs
        Name: !Sub ses-logs-${AWS::Region}

# ------------------------------------------------------------#
# SNS Topic
# ------------------------------------------------------------#
  SNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      TracingConfig: PassThrough
      DisplayName: !Sub ses-logs-${AWS::Region}
      FifoTopic: false
      Tags:
        - Key: Cost
          Value: !Ref TagValue

手動作業

AWS CloudFormation テンプレートでデプロイした後、以下の作業をする必要があります。

  1. Amazon SES の対象 ID への設定セット割当
  2. Amazon SES の通知設定

Amazon SES の対象 ID への設定セット割当

AWS CloudFormation テンプレートにより、ses-logs-AWSアカウント番号-リージョン名 の名前で SES 設定セットが出来上がります。これを、ログを取りたい ID のデフォルト設定セットに割り当てます。これで Amazon SES から Amazon Data Firehose にログが送信されます。

Amazon SNS の通知設定

AWS CloudFormation テンプレートにより、おそらく ses-logs-から始まる名前の Amazon SNS トピックが作成されます。これを、ログを取りたい ID のフィードバック通知に設定します。これでバウンス、苦情があったときに Amazon SNS に通知されます。

ただし、この Amazon SNS トピックには通知先は設定されていませんので、メールアドレスなどでサブクスライブする作業をお忘れなく。

Amazon S3 ログの確認方法

Amazon S3 に送られたログは JSON 形式になっており、Amazon Data Firehose のコントロール下でバッファリングされたログデータが順次書き込まれていきます。ファイル名もランダムな名前になっており、直接ファイルを操作して検索するのは至難の業です。そのため、Amazon Athena で検索するのが最も効率的です。

検索を効率化するために、Amazon Athena External Table (Amazon S3 などのデータソースを検索しやすくするビューのようなもの) を AWS CloudFormation でデプロイしてあるので、そこに対して Amazon Athena から SQL でクエリします。

以下のように、Amazon Athena のクエリエディタでデータベース ses-logs-リージョン名、テーブル seslogs が作成されているのでそれに対してクエリを打ちます。

 

あまり出力結果を成型できていませんが、以下のサンプル SQL でとりあえずログは見られます。適宜、SQL は変えてみて欲しいです。

ある特定の日の配送ログ

SELECT * FROM seslogs
where event_type = 'Delivery'
and day = '2024/07/08';

event_type を Bounce や Complaint に変更すれば、バウンスや苦情のログを検索できます。すみませんが出力は成型できていません。

ある特定の期間のバウンスログ(成型済)

バウンスログについては若干成型したものがあります。

SELECT
  bounce.bounceType as bouncebounceType
, bounce.bouncesubtype as bouncebouncesubtype
, bounce.feedbackid as bouncefeedbackid
, bounce.timestamp as bouncetimestamp
, bounce.reportingMTA as bouncereportingmta
, mail.messageId as messageid
, mail.timestamp as timestamp
, mail.source as source
, mail.commonHeaders.subject as subject
, mail.commonHeaders.to as to
, element_at(mail.headers,2) as replyto
, mail.tags.ses_source_ip as ses_source_ip
, mail.tags.ses_outgoing_ip as ses_outgoing_ip
FROM seslogs
where event_type = 'Bounce'
and day between '2024/11/01' and '2024/11/30';

表形式で結果が見られます。

 

まとめ

いかがでしたでしょうか。

以前は Amazon OpenSearch Service でログ保管していたので、かなり課金額が安くなりました。AWS CloudFormation テンプレート化したことで他のアカウント、リージョンでもすぐに使用できるようになり、便利になりました。

正直、バウンスは「存在しないメールアドレス」宛てに送られたメールがほとんどです。通知が来たら送信者を突き止めて、入力したメールアドレスが間違ってますよ、と伝える運用をしています。

本記事が皆様のお役に立てれば幸いです。

著者について
広野 祐司

AWS サーバーレスアーキテクチャを駆使して社内クラウド人材育成アプリとコンテンツづくりに勤しんでいます。React で SPA を書き始めたら快適すぎて、他の言語には戻れなくなりました。サーバーレス & React 仲間を増やしたいです。AWSは好きですが、それよりもフロントエンド開発の方が好きでして、バックエンド構築を簡単にしてくれたAWSには感謝の気持ちの方が強いです。
取得資格:AWS 認定は13資格、ITサービスマネージャ、ITIL v3 Expert 等
2020 - 2024 Japan AWS Top Engineer 受賞
2022 - 2024 AWS Ambassador 受賞
2023 当社初代フルスタックエンジニア認定
好きなAWSサービス:AWS Amplify / AWS AppSync / Amazon Cognito / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする

クラウドに強いによるエンジニアブログです。

SCSKクラウドサービス(AWS)は、企業価値の向上につながるAWS 導入を全面支援するオールインワンサービスです。AWS最上位パートナーとして、多種多様な業界のシステム構築実績を持つSCSKが、お客様のDX推進を強力にサポートします。

AWSクラウドソリューション運用・監視
シェアする
タイトルとURLをコピーしました