「自社サービスのメール通知がスパムフォルダに入ってしまう」「バウンスメールの管理が手動で追いつかない」「送信数が増えると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

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スキルを活かせる開発案件を多数掲載しています。
関連記事: