𝕏 f B! L
案件・求人数 12,345
案件を探す(準備中) エージェントを探す(準備中) お役立ち情報 ログイン
案件・求人数 12,345
AWS SESでメール通知システムを構築|Lambda連携・バウンス処理・配信率最適化ガイド

AWS SESでメール通知システムを構築|Lambda連携・バウンス処理・配信率最適化ガイド

AWSSESメール配信Lambda通知システム
目次

「自社サービスのメール通知がスパムフォルダに入ってしまう」「バウンスメールの管理が手動で追いつかない」「送信数が増えるとAWSのSES制限に引っかかる」

メール配信は一見シンプルですが、配信率・バウンス管理・コンプライアンスを考慮すると、意外と奥が深い技術領域です。

Amazon Simple Email Service(SES)は、AWS上でスケーラブルなメール配信インフラを構築できるサービスです。本記事では、SESを使ったメール通知システムの設計・構築から、配信率の最適化・バウンス管理・Lambda連携まで実践的に解説します。

この記事を3秒でまとめると

  • Amazon SESは1通0.1ドル以下の低コストでスケーラブルなメール配信を実現
  • SPF/DKIM/DMARCの正しい設定で配信率を大幅に向上できる
  • SNS + Lambdaでバウンス・苦情の自動処理パイプラインを構築可能

Amazon SESの基礎知識

SESとは何か

Amazon Simple Email Service(SES)は、AWSが提供するクラウドベースのメール送受信サービスです。

特徴内容
料金EC2からの送信: 62,000通/月無料、以降$0.10/1,000通
スケーラビリティ1秒あたり数千通の送信が可能
統合性Lambda, SNS, S3, CloudWatchとネイティブ連携
リージョン東京(ap-northeast-1)を含む複数リージョンで利用可能

SESのアーキテクチャ概要

SESを中心としたメール通知システムのアーキテクチャは以下のようになります。

[アプリケーション] → [SES API/SMTP] → [SES送信エンジン]

                                   [受信者のメールサーバー]

                              [バウンス/苦情通知] → [SNSトピック]

                                                   [Lambda関数]

                                              [DynamoDB/CloudWatch]

Step 1: SESのセットアップ

ドメイン認証(DKIM・SPF)

メール配信率を最大化するために、まずドメインの認証を設定します。

# AWS CLIでドメイン認証を開始
aws ses verify-domain-identity \
  --domain example.com \
  --region ap-northeast-1

DKIM設定:

# DKIMの有効化
aws ses verify-domain-dkim \
  --domain example.com \
  --region ap-northeast-1

返されるDKIMトークンをDNSに設定します:

# Route 53 に追加するDKIMレコード
token1._domainkey.example.com  CNAME  token1.dkim.amazonses.com
token2._domainkey.example.com  CNAME  token2.dkim.amazonses.com
token3._domainkey.example.com  CNAME  token3.dkim.amazonses.com

SPFレコードの設定:

# TXTレコード
example.com  TXT  "v=spf1 include:amazonses.com ~all"

DMARCレコードの設定:

# DMARCポリシー
_dmarc.example.com  TXT  "v=DMARC1; p=quarantine; rua=mailto:[email protected]"

サンドボックスからの移行

SESは初期状態ではサンドボックスモードで、検証済みアドレスにしか送信できません。本番利用にはサンドボックスの解除申請が必要です。

# 送信クォータの確認
aws ses get-send-quota --region ap-northeast-1

AWS SESメール通知システムのアーキテクチャ

Step 2: Lambda連携でメール送信を自動化

SES + Lambdaの基本パターン

# Lambda関数: SESでメール送信
import boto3
import json
from botocore.exceptions import ClientError

ses = boto3.client('ses', region_name='ap-northeast-1')

def lambda_handler(event, context):
    """イベント駆動でメールを送信するLambda関数"""
    
    try:
        response = ses.send_email(
            Source='[email protected]',
            Destination={
                'ToAddresses': [event['to']],
            },
            Message={
                'Subject': {
                    'Data': event['subject'],
                    'Charset': 'UTF-8',
                },
                'Body': {
                    'Html': {
                        'Data': event['html_body'],
                        'Charset': 'UTF-8',
                    },
                    'Text': {
                        'Data': event['text_body'],
                        'Charset': 'UTF-8',
                    },
                },
            },
            Tags=[
                {'Name': 'campaign', 'Value': event.get('campaign', 'transactional')},
                {'Name': 'type', 'Value': event.get('type', 'notification')},
            ],
        )
        
        return {
            'statusCode': 200,
            'body': json.dumps({
                'messageId': response['MessageId'],
                'status': 'sent'
            })
        }
        
    except ClientError as e:
        error_code = e.response['Error']['Code']
        if error_code == 'MessageRejected':
            return {
                'statusCode': 400,
                'body': json.dumps({'error': 'Message rejected', 'detail': str(e)})
            }
        elif error_code == 'Throttling':
            # リトライを示すステータスコードを返す
            return {
                'statusCode': 429,
                'body': json.dumps({'error': 'Throttled', 'retry': True})
            }
        raise

テンプレートメールの活用

SESのテンプレート機能を使えば、動的なメールコンテンツを効率的に管理できます。

# テンプレートの作成
ses.create_template(
    Template={
        'TemplateName': 'WelcomeEmail',
        'SubjectPart': '{{name}}さん、ようこそ!',
        'HtmlPart': '''
        <html>
        <body>
            <h1>{{name}}さん、登録ありがとうございます!</h1>
            <p>アカウントが正常に作成されました。</p>
            <p>プラン: <strong>{{plan}}</strong></p>
            <a href="{{loginUrl}}">ログインする</a>
        </body>
        </html>
        ''',
        'TextPart': '{{name}}さん、登録ありがとうございます!プラン: {{plan}}'
    }
)

# テンプレートを使ったメール送信
ses.send_templated_email(
    Source='[email protected]',
    Destination={'ToAddresses': ['[email protected]']},
    Template='WelcomeEmail',
    TemplateData=json.dumps({
        'name': '田中太郎',
        'plan': 'Professional',
        'loginUrl': 'https://example.com/login'
    })
)

一括送信(Bulk Email)

大量のメールを効率的に送信する場合は、send_bulk_templated_emailを使います。

# 一括テンプレート送信(最大50件/回)
response = ses.send_bulk_templated_email(
    Source='[email protected]',
    Template='WeeklyDigest',
    DefaultTemplateData=json.dumps({'fallback_name': 'ユーザー'}),
    Destinations=[
        {
            'Destination': {'ToAddresses': ['[email protected]']},
            'ReplacementTemplateData': json.dumps({'name': '田中', 'article_count': '5'})
        },
        {
            'Destination': {'ToAddresses': ['[email protected]']},
            'ReplacementTemplateData': json.dumps({'name': '鈴木', 'article_count': '3'})
        },
    ]
)

Step 3: バウンス・苦情の自動処理

SNS + Lambdaによるフィードバック処理

メール配信の健全性を保つには、**バウンス(配達不能)と苦情(スパム報告)**を適切に処理する必要があります。

# バウンス・苦情処理Lambda
import json
import boto3
from datetime import datetime

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('EmailSuppressionList')
cloudwatch = boto3.client('cloudwatch')

def lambda_handler(event, context):
    """SNS経由でSESのバウンス・苦情通知を処理"""
    
    for record in event['Records']:
        message = json.loads(record['Sns']['Message'])
        notification_type = message['notificationType']
        
        if notification_type == 'Bounce':
            handle_bounce(message['bounce'])
        elif notification_type == 'Complaint':
            handle_complaint(message['complaint'])
        elif notification_type == 'Delivery':
            handle_delivery(message['delivery'])

def handle_bounce(bounce):
    """バウンス処理: ハードバウンスはサプレッションリストに追加"""
    bounce_type = bounce['bounceType']
    
    for recipient in bounce['bouncedRecipients']:
        email = recipient['emailAddress']
        
        if bounce_type == 'Permanent':
            # ハードバウンス: サプレッションリストに追加
            table.put_item(Item={
                'email': email,
                'type': 'hard_bounce',
                'reason': recipient.get('diagnosticCode', 'unknown'),
                'timestamp': datetime.utcnow().isoformat(),
                'ttl': int((datetime.utcnow()).timestamp()) + 86400 * 365
            })
            
        # CloudWatchメトリクス
        put_metric('BounceCount', 1, bounce_type)

def handle_complaint(complaint):
    """苦情処理: サプレッションリストに追加して今後の送信を停止"""
    for recipient in complaint['complainedRecipients']:
        email = recipient['emailAddress']
        table.put_item(Item={
            'email': email,
            'type': 'complaint',
            'feedback_type': complaint.get('complaintFeedbackType', 'unknown'),
            'timestamp': datetime.utcnow().isoformat(),
        })
    
    put_metric('ComplaintCount', 1, 'Complaint')

def handle_delivery(delivery):
    """配信成功のメトリクス記録"""
    put_metric('DeliveryCount', len(delivery['recipients']), 'Success')

def put_metric(name, value, dimension_value):
    cloudwatch.put_metric_data(
        Namespace='SES/EmailMetrics',
        MetricData=[{
            'MetricName': name,
            'Value': value,
            'Unit': 'Count',
            'Dimensions': [{'Name': 'Type', 'Value': dimension_value}]
        }]
    )

サプレッションリストの管理

送信前にサプレッションリストをチェックし、バウンス・苦情のあるアドレスへの送信を自動的にスキップします。

def is_suppressed(email: str) -> bool:
    """メールアドレスがサプレッションリストに含まれるかチェック"""
    try:
        response = table.get_item(Key={'email': email})
        return 'Item' in response
    except Exception:
        return False  # エラー時は送信を許可(安全側に倒さない)

def send_email_safe(to: str, subject: str, body: str):
    """サプレッションチェック付きメール送信"""
    if is_suppressed(to):
        print(f"Skipped suppressed email: {to}")
        return None
    
    return ses.send_email(
        Source='[email protected]',
        Destination={'ToAddresses': [to]},
        Message={
            'Subject': {'Data': subject, 'Charset': 'UTF-8'},
            'Body': {'Text': {'Data': body, 'Charset': 'UTF-8'}}
        }
    )

Step 4: 配信率の最適化

配信率を高める5つの施策

施策効果実装難易度
DKIM + SPF + DMARC設定配信率+30〜50%★★☆
サプレッションリスト管理バウンス率低減★★☆
ウォームアップ(段階的増量)IP評価の向上★☆☆
エンゲージメント管理非アクティブユーザーの除外★★★
フィードバックループ登録苦情率の監視★☆☆

専用IPとレピュテーション管理

大量送信(月10万通以上)の場合は、専用IPアドレスの利用を検討します。

# 専用IPプールの作成
aws ses create-dedicated-ip-pool \
  --pool-name production-pool \
  --scaling-mode MANAGED

専用IPのウォームアップスケジュール:

日数推奨送信量/日
1-3日100通
4-7日500通
8-14日2,000通
15-21日10,000通
22-30日50,000通
31日以降フル稼働

CloudWatchダッシュボード

SESの配信メトリクスをCloudWatchで可視化します。

# 重要メトリクス
aws cloudwatch get-metric-statistics \
  --namespace AWS/SES \
  --metric-name Reputation.BounceRate \
  --start-time 2026-03-01T00:00:00Z \
  --end-time 2026-03-22T00:00:00Z \
  --period 86400 \
  --statistics Average

監視すべきメトリクス:

  • バウンス率: 5%以上でアカウント停止リスク(目標: 2%未満)
  • 苦情率: 0.1%以上で危険(目標: 0.05%未満)
  • 配信率: 95%以上を維持

CloudWatch監視の詳細はAWS CloudWatch監視ガイドを参照してください。

Step 5: IaCによるインフラ構築

Terraformでの構築例

# SESドメイン認証
resource "aws_ses_domain_identity" "main" {
  domain = "example.com"
}

resource "aws_ses_domain_dkim" "main" {
  domain = aws_ses_domain_identity.main.domain
}

# Route53 DNSレコード
resource "aws_route53_record" "ses_dkim" {
  count   = 3
  zone_id = data.aws_route53_zone.main.zone_id
  name    = "${aws_ses_domain_dkim.main.dkim_tokens[count.index]}._domainkey"
  type    = "CNAME"
  ttl     = 600
  records = ["${aws_ses_domain_dkim.main.dkim_tokens[count.index]}.dkim.amazonses.com"]
}

# SNSトピック(バウンス・苦情通知用)
resource "aws_sns_topic" "ses_bounces" {
  name = "ses-bounces"
}

resource "aws_sns_topic" "ses_complaints" {
  name = "ses-complaints"
}

# SES通知設定
resource "aws_ses_identity_notification_topic" "bounce" {
  topic_arn                = aws_sns_topic.ses_bounces.arn
  notification_type        = "Bounce"
  identity                 = aws_ses_domain_identity.main.domain
  include_original_headers = true
}

resource "aws_ses_identity_notification_topic" "complaint" {
  topic_arn                = aws_sns_topic.ses_complaints.arn
  notification_type        = "Complaint"
  identity                 = aws_ses_domain_identity.main.domain
  include_original_headers = true
}

# Lambda関数(バウンス処理)
resource "aws_lambda_function" "bounce_handler" {
  filename         = "bounce_handler.zip"
  function_name    = "ses-bounce-handler"
  role             = aws_iam_role.lambda_ses.arn
  handler          = "index.lambda_handler"
  runtime          = "python3.12"
  timeout          = 30
}

# SNS → Lambda サブスクリプション
resource "aws_sns_topic_subscription" "bounce_lambda" {
  topic_arn = aws_sns_topic.ses_bounces.arn
  protocol  = "lambda"
  endpoint  = aws_lambda_function.bounce_handler.arn
}

IaCの詳細はAWS IaC/Terraform/CDKガイドを参照してください。

コスト見積もり

SESのコストは非常に低く、スタートアップ企業にも適しています。

送信規模月額コスト目安
10,000通/月$1(EC2無料枠内なら$0)
100,000通/月$10
1,000,000通/月$100
10,000,000通/月$1,000

※ 上記はSES送信料のみ。Lambda・SNS・DynamoDBの料金は別途。

コスト最適化についてはAWS FinOpsコスト最適化ガイドも参照してください。

SES案件でのメール配信スキルの需要

メール配信インフラの構築・運用スキルは、以下のようなSES案件で需要があります。

  • ECサイト: 注文確認・発送通知・カート離脱リマインド
  • SaaS: ウェルカムメール・パスワードリセット・利用状況レポート
  • マーケティング: ニュースレター・キャンペーン通知・A/Bテスト
  • 業務システム: ワークフロー通知・承認依頼・日次レポート

まとめ — SESでスケーラブルなメール配信を実現

Amazon SESを使えば、低コストで信頼性の高いメール配信インフラを構築できます。

  • 認証設定: DKIM + SPF + DMARCで配信率を最大化
  • Lambda連携: イベント駆動でメール送信を自動化
  • バウンス管理: SNS + Lambdaで自動処理パイプラインを構築
  • 配信率最適化: ウォームアップ・エンゲージメント管理で安定運用
  • IaC: Terraformで再現可能なインフラ構築

これらのスキルは、SES案件のバックエンドエンジニアとして大きな武器になります。


SES BASEでは、AWSスキルを活かせる開発案件を多数掲載しています。

👉 SES BASEで案件を探す

関連記事:

SES案件をお探しですか?

SES記事をもっと読む →
🏗️

SES BASE 編集長

SES業界歴10年以上のメンバーが在籍する編集チーム。SES企業での営業・エンジニア経験、フリーランス独立経験を持つメンバーが、業界のリアルな情報をお届けします。

📊 業界データに基づく記事制作 🔍 IPA・経済産業省データ参照 💼 SES実務経験者が執筆・監修