2022年 AWSで構築したアーキテクチャ

media thumbnail

GizTechPro事業部所属エンジニアの伊藤太樹です。
普段は参画先の現場にて、AWSクラウドを使用したインフラストラクチャの設計・構築や運用保守を担当しています。

本記事では2022年に、私が業務の中で構築したAWSの構築パターンの中から一つをピックアップして紹介します。

前提・対象とする読者

下記の方をターゲットとして記事を執筆しています。

  • AWSの下記サービスについて基本的な知識を持っている方 (それぞれどのようなサービス・機能であるか理解している程度で良いと思います)
    • Simple Email Service (SES)
    • Simple Notification Service (SNS)
    • Lambda
    • CloudWatchLogs
  • AWSの構築パターンに興味がある方

当てはまらない方もぜひぜひお気軽にご一読いただき、構築のご参考にしていただければ幸いです。
尚、各サービスに関連する用語の説明等は割愛させていただきます。ご了承ください。

SESイベントデータパターン

今回紹介するのは SESイベントデータパターン です。
このSESイベントデータパターンとは独自の命名で、公式に存在しているわけではありませんのでご注意ください。

私が構築を担当したシステムの一つでは、数千件のメールの一括配信をメインの機能としていました。

AWS SESを使用してメール送信をする場合
メールの送信件数や、バウンスの発生件数といった各結果ごとの件数を時間別に集計することはデフォルトの機能で可能です。

しかし、下記のような、送信されたメール1件1件の結果情報を個別に取得する方法は残念ながらデフォルトで用意されていません。

  • メールが送信されたか
  • 送信されたメールがバウンスとして処理されたか
  • 送信されたメールが受信側のメールサーバに届いたか
  • 送信されたメールが受信側でスパムとして処理されたか
  • メール送信時に配信遅延が発生したか

メール配信がメインの機能となっているシステムにおいて
どのメールの送信が成功したのか、送信が失敗したのか結果情報の追跡ができないというのは、致命的です。

そこで、メール結果情報を取得するために構築した構成が、SESイベントデータパターン になります。

SESでは何の設定もしていない場合、メールの送信結果を一切取得できませんが
SES設定セットという機能を活用することで、メールの送信結果をCloudWatch, Kinesis Data Firehose, SNSのいずれかに流すことができます。

Amazon SES イベントデータの使用

設定セット

設定セットとは、送信元IPプールや追跡オプションといった様々な設定をひとまとめにして管理する単位です。
メール送信結果の通知先も設定セットの中の設定の一つになります。

SESを使用してメールを送信する際には、オプションで何の設定セットを使用するか選択することができます。
状況や用途によって使用する設定セットを使い分けて、メール送信時の振る舞いを変えることができます。
https://docs.aws.amazon.com/ja_jp/ses/latest/dg/using-configuration-sets.html

複数あるメール送信先の中から今回は、SNSを使用することにしました。

実は本パターンの構築の背景には、アプリケーション側の運用保守チームが手軽に素早くメール送信結果情報を確認したいという依頼がありました。

CloudWatchを使用する場合はイベントデータをメトリクスに取り込むことになり、メトリクス上では送信結果ごとの件数は確認できるものの送信結果情報の詳細は確認できません。
Kinesisを使用する場合はイベントデータの最終出力先がS3となり、詳細の確認にはS3に保存されたオブジェクトをローカルにダウンロードしたり、Athenaサービスの使用が必要になり手軽ではありません。

どちらも依頼内容の要件を満たさなかったため、SNSを送信先とした上で他サービスと連携するのが最適と判断しました。

下記が使用するSESイベントデータパターンで使用するサービス一覧と、それぞれの用途になります。

AWSサービス用途
SES設定セットを使用してメールを送信
SNSSESのメール送信結果を受信
Lambda受信したメール送信結果をCloudWatchLogsに保存
CloudWatch (Logs)SESでのメール送信イベント1件1件の保存先

簡易的にはなりますが、構成図は下記になります。

SESでのメール送信結果情報をSNSに流すように設定した場合
メールの送信結果1件ごとにJSON形式のイベントデータがSNSに送信されます。

Amazon SESがAmazon SNSに発行するイベントデータのコンテンツ

イベントデータの送信先として指定したSNSトピックのサブスクリプションにLambda関数を登録し
Lambda関数内の処理で CloudWatchLogsにイベントを書き込みします。
(Lambdaのログ関数を使用して、CloudWatchLogsにログを出力することもできますが、内容が全く異なるログ出力と混在して視認性が低くなってしまいます。SNSから送られてきたJSONから、SESイベントデータJSONの部分だけを抽出して、CloudWatchLogsのロググループに書き込むことを推奨します。)

最終的に下記のようなJSONが、送信されたメール1件ごとにCloudWatchLogsに出力されてくるようにします。

{
  "eventType": "Send",
  "mail": {
    "timestamp": "2016-10-14T05:02:16.645Z",
    "source": "sender@example.com",
    "sourceArn": "arn:aws:ses:us-east-1:123456789012:identity/sender@example.com",
    "sendingAccountId": "123456789012",
    "messageId": "EXAMPLE7c191be45-e9aedb9a-02f9-4d12-a87d-dd0099a07f8a-000000",
    "destination": [
      "recipient@example.com"
    ],
    "headersTruncated": false,
    "headers": [
      {
        "name": "From",
        "value": "sender@example.com"
      },
      {
        "name": "To",
        "value": "recipient@example.com"
      },
      {
        "name": "Subject",
        "value": "Message sent from Amazon SES"
      },
      {
        "name": "MIME-Version",
        "value": "1.0"
      },
      {
        "name": "Content-Type",
        "value": "multipart/mixed;  boundary=\"----=_Part_0_716996660.1476421336341\""
      },
      {
        "name": "X-SES-MESSAGE-TAGS",
        "value": "myCustomTag1=myCustomTagValue1, myCustomTag2=myCustomTagValue2"
      }
    ],
    "commonHeaders": {
      "from": [
        "sender@example.com"
      ],
      "to": [
        "recipient@example.com"
      ],
      "messageId": "EXAMPLE7c191be45-e9aedb9a-02f9-4d12-a87d-dd0099a07f8a-000000",
      "subject": "Message sent from Amazon SES"
    },
    "tags": {
      "ses:configuration-set": [
        "ConfigSet"
      ],
      "ses:source-ip": [
        "192.0.2.0"
      ],
      "ses:from-domain": [
        "example.com"
      ],      
      "ses:caller-identity": [
        "ses_user"
      ],
      "myCustomTag1": [
        "myCustomTagValue1"
      ],
      "myCustomTag2": [
        "myCustomTagValue2"
      ]      
    }
  },
  "send": {}
}

メール送信が行われたことを意味する Send イベントのイベントデータJSONをサンプルとして掲載しましたが
それ以外のイベントのイベントデータJSONについては、下記デベロッパーガイドをご確認ください。

Amazon SESが Amazon SNSに発行するイベントデータの例

最後に、SESイベントデータパターン構築のterraformテンプレートを記載します。

terraform {
  required_version = ">=1.1.2"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.72.0"
    }
  }
  backend "s3" {
    bucket = "ses-event-data-pattern-tfstate"
    key    = "terraform.tfstate"
    region = "ap-northeast-1"
  }
}

provider "aws" {
  region  = "ap-northeast-1"
}


resource "aws_sns_topic" "event_data_notification" {
  name = "event-data-notification"
}

resource "aws_sns_topic_subscription" "subscription_lambda" {
  topic_arn = aws_sns_topic.event_data_notification.arn
  protocol  = "lambda"
  endpoint  = aws_lambda_function.ses_event_data_function.arn
}


resource "aws_ses_configuration_set" "event_data" {
  name = "event-data-pattern"

  delivery_options {
    tls_policy = "Require"
  }

  reputation_metrics_enabled = true
  sending_enabled            = true
}


resource "aws_ses_event_data_destination" "event_destination" {
  name                   = "event-data-destination"
  configuration_set_name = aws_ses_configuration_set.event_data.name
  enabled                = true
  matching_types = [
    "send",
    "bounce",
    "reject",
    "complaint",
    "delivery",
    "renderingFailure"
  ]
  sns_destination {
    topic_arn = aws_sns_topic.event_data_notification.arn
  }
}


data "aws_iam_policy_document" "assume_role" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}


resource "aws_iam_policy" "lambda_access_cwlogs_policy" {
  name = "lambda_access_cwlogs_policy"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "Logs:PutLogEvents"
        ]
        Effect   = "Allow"
        Resource = "*"
      }
    ]
  })
}

resource "aws_iam_role" "lambda_iam_role" {
  name               = "LambdaIAMRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
    aws_iam_policy.lambda_access_cwlogs_policy.arn
  ]
}

data "archive_file" "ses_event_data_function_file" {
  type        = "zip"
  source_file = "ses_event_data_function.py"
  output_path = "ses_event_data_function.zip"
}

resource "aws_lambda_function" "ses_event_data_function" {
  function_name = "OutputSESEventDataFunction"
  filename      = data.archive_file.ses_event_data_function_file.output_path
  role          = aws_iam_role.lambda_iam_role.arn
  handler       = "ses_event_data_function.lambda_handler"
  runtime       = "python3.9"

  environment {
    variables = {
      LOG_GROUP_NAME  = aws_cloudwatch_log_group.ses_event_data_log.name
      LOG_STREAM_NAME = aws_cloudwatch_log_stream.ses_event_data_log_stream.name
    }
  }
}


resource "aws_lambda_permission" "trigger_sns" {
  statement_id  = "AllowSnsExecutionLambda"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.ses_event_data_function.function_name
  principal     = "sns.amazonaws.com"
  source_arn    = aws_sns_topic.event_data_notification.arn

  depends_on = [aws_lambda_function.ses_event_data_function, aws_sns_topic.event_data_notification]
}

resource "aws_cloudwatch_log_group" "ses_event_data_log" {
  name              = "ses_event_data_log"
  retention_in_days = 7
}

resource "aws_cloudwatch_log_stream" "ses_event_data_log_stream" {
  name           = "ses_event_data_log_stream"
  log_group_name = aws_cloudwatch_log_group.ses_event_data_log.name
}

また、Lambda関数内の処理は下記のようになっています。
(タイムスタンプ等一部ハードコーディングしている箇所があるため、実運用する際には修正が必要ですが…)

import os
import boto3
from botocore.config import Config
import json
import logging

config = Config(
  retries = {
    'max_attempts': 10,
    'mode': 'standard'
  }
)

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

cwlogs = boto3.client('logs', config=config)

def lambda_handler(event, context):
    logger.info(json.dumps(event, ensure_ascii=False))


    for data in event['Records']:
        logger.info(data)

        try:
            response = cwlogs.put_log_events(
                logGroupName = os.environ['LOG_GROUP_NAME'],
                logStreamName = os.environ['LOG_STREAM_NAME'],
                logEvents = [
                    {
                        'timestamp': 1679214703556,
                        'message': data['Sns']['Message']
                    },
                ],
                sequenceToken = 'string'
            )

            logger.info(response)

        except:
            logger.exception('')

実際に構築後は、下記のように
送信されたメール結果情報をCloudWatchLogs上で確認することができるようになります。

LogInsightsを使って絞り込みをした上で集計したり、フィルターパターンで検索をかけたりということも容易にできます。 
本構成が良いと思った方は、ぜひ導入を検討してみてください。

まとめ

以上、2022年で構築したAWS構成パターンを紹介させていただきました。
開発チームをサポートし、安定したインフラストラクチャを構築していくことは、大変やりがいのある業務です。

Gizumoには、Webアプリケーション開発のエンジニアだけでなく、オンプレ・AWSのインフラエンジニアも多数在籍しています。
私たちと一緒にGizumoのインフラ技術レベルを発展させてくれる方の入社を、心からお待ちしています!

少しでも開発にお困りの方は
相談しやすいスペシャリストにお問い合わせください

お問い合わせ
  1. breadcrumb-logo
  2. メディア
  3. 2022年 AWSで構築したアーキテクチャ