AWS SES サンドボックス解除とドメイン認証(DMARC)2026年版
AWS SESサンドボックス解除からDKIM・SPF・DMARC設定まで、2026年版の完全手順。Terraformによる自動化とRoute53でのDNS設定を含む実践的なメール認証ガイド。
はじめに:2024年以降、メール送信の「前提条件」が変わった
2024年2月、Google と Yahoo が送信者ガイドラインを改定した。1日500通以上を送るドメインには DMARC レコードの設定が必須となり、未対応ドメインからのメールは迷惑メールフォルダへ振り分けられるか、受信拒否される。
問題は、これが「設定を追加すれば良い」だけの話に見えて、実態はそれなりに複雑な点だ。
メール認証の3層構造:
SPF(Sender Policy Framework)
└─ 「このドメインからの送信を許可するIPアドレス」をDNSに宣言
→ 偽装元ドメインからの直接送信を防ぐ
DKIM(DomainKeys Identified Mail)
└─ メール本文と送信元をデジタル署名で保証
→ 転送時のなりすましや改ざんを検出する
DMARC(Domain-based Message Authentication)
└─ SPF/DKIM が両方失敗したときの「処理ポリシー」をDNSに宣言
→ none(監視のみ)/ quarantine(隔離)/ reject(拒否)
┌────────────┐ 署名検証 ┌─────────────┐
│ SES 送信 │ ────────────→ │ 受信サーバー │
│ (DKIM付き) │ │ SPF/DKIM確認 │
└────────────┘ │ DMARC判定 │
└─────────────┘
AWS SES でメールを送信するには、この3層をすべて設定した上でサンドボックスを解除する必要がある。この記事では、Terraform による自動化を前提とした完全手順を解説する。
Part 1:AWS SES のサンドボックスとは何か
SES を新規に有効化すると、デフォルトで「サンドボックスモード」になる。
サンドボックスモードの制限:
✗ 送信先: 検証済みメールアドレス宛のみ(任意の宛先に送れない)
✗ 送信量: 1日200通、1秒1通まで
✗ 用途: 開発・テスト専用
本番モード(サンドボックス解除後):
✓ 送信先: 任意のメールアドレス宛に送信可能
✓ 送信量: デフォルト1日50,000通(上限申請可能)
✓ 用途: 本番メール送信(トランザクションメール、通知メール等)
サンドボックス解除は AWS への申請が必要だが、申請前に SPF・DKIM・DMARC を設定しておかないと審査が通りにくい。順番が重要だ。
推奨手順:
1. ドメインを SES に登録(DKIM 自動生成)
2. Route53 に SPF・DKIM・DMARC レコードを設定
3. SES コンソールで検証完了を確認
4. サンドボックス解除申請
5. 申請通過後、テスト送信で動作確認
Part 2:Terraform によるリソース構築
手動でのコンソール操作は、DNS レコードの設定漏れや環境間の差異を生む。Terraform で全リソースをコード管理する。
ディレクトリ構成
ses-setup/
├── main.tf
├── variables.tf
├── outputs.tf
└── terraform.tfvars
variables.tf
variable "domain_name" {
description = "SESに登録するドメイン名(例: example.com)"
type = string
}
variable "aws_region" {
description = "SESを設定するAWSリージョン"
type = string
default = "ap-northeast-1"
}
variable "dmarc_policy" {
description = "DMARCポリシー(none / quarantine / reject)"
type = string
default = "quarantine"
validation {
condition = contains(["none", "quarantine", "reject"], var.dmarc_policy)
error_message = "dmarc_policy は none, quarantine, reject のいずれかを指定してください。"
}
}
variable "dmarc_rua" {
description = "DMARCレポートの送信先メールアドレス"
type = string
# 例: "mailto:dmarc-reports@example.com"
}
main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# ── Route53 ホストゾーン(既存を参照)────────────────
data "aws_route53_zone" "main" {
name = var.domain_name
private_zone = false
}
# ── SES ドメイン ID(DKIM自動生成)─────────────────
resource "aws_sesv2_email_identity" "domain" {
email_identity = var.domain_name
dkim_signing_attributes {
next_signing_key_length = "RSA_2048_BIT"
}
}
# ── DKIM レコード(SESが生成した3件をRoute53に登録)──
resource "aws_route53_record" "dkim" {
count = 3
zone_id = data.aws_route53_zone.main.zone_id
name = "${aws_sesv2_email_identity.domain.dkim_signing_attributes[0].tokens[count.index]}._domainkey.${var.domain_name}"
type = "CNAME"
ttl = 300
records = ["${aws_sesv2_email_identity.domain.dkim_signing_attributes[0].tokens[count.index]}.dkim.amazonses.com"]
}
# ── SPF レコード ──────────────────────────────────
resource "aws_route53_record" "spf" {
zone_id = data.aws_route53_zone.main.zone_id
name = var.domain_name
type = "TXT"
ttl = 300
records = [
# SES の送信IPを許可、その他は ~all(ソフトフェイル)
# 本番で厳格にするなら -all(ハードフェイル)に変更
"v=spf1 include:amazonses.com ~all"
]
}
# ── DMARC レコード ────────────────────────────────
resource "aws_route53_record" "dmarc" {
zone_id = data.aws_route53_zone.main.zone_id
name = "_dmarc.${var.domain_name}"
type = "TXT"
ttl = 300
records = [
# pct=100: 全メールにポリシーを適用
# rua: 集約レポートの送信先(週次で届く)
"v=DMARC1; p=${var.dmarc_policy}; pct=100; rua=${var.dmarc_rua}; adkim=s; aspf=s"
]
}
# ── MAIL FROM ドメイン(バウンス処理用)─────────────
# 独自の MAIL FROM を設定することで、SPF アライメントが向上する
resource "aws_sesv2_email_identity_mail_from_attributes" "main" {
email_identity = aws_sesv2_email_identity.domain.email_identity
mail_from_domain = "mail.${var.domain_name}"
behavior_on_mx_failure = "USE_DEFAULT_VALUE"
}
# MAIL FROM ドメインの MX レコード
resource "aws_route53_record" "mail_from_mx" {
zone_id = data.aws_route53_zone.main.zone_id
name = "mail.${var.domain_name}"
type = "MX"
ttl = 300
records = ["10 feedback-smtp.${var.aws_region}.amazonses.com"]
}
# MAIL FROM ドメインの SPF レコード
resource "aws_route53_record" "mail_from_spf" {
zone_id = data.aws_route53_zone.main.zone_id
name = "mail.${var.domain_name}"
type = "TXT"
ttl = 300
records = ["v=spf1 include:amazonses.com ~all"]
}
outputs.tf
output "ses_identity_arn" {
description = "SES Email Identity の ARN(IAMポリシーで使用)"
value = aws_sesv2_email_identity.domain.arn
}
output "dkim_tokens" {
description = "DKIMトークン(Route53への登録確認用)"
value = aws_sesv2_email_identity.domain.dkim_signing_attributes[0].tokens
}
output "dmarc_record" {
description = "設定されたDMARCレコードの内容"
value = "v=DMARC1; p=${var.dmarc_policy}; pct=100; rua=${var.dmarc_rua}; adkim=s; aspf=s"
}
terraform.tfvars
domain_name = "example.com"
aws_region = "ap-northeast-1"
dmarc_policy = "quarantine"
dmarc_rua = "mailto:dmarc-reports@example.com"
Part 3:DMARC ポリシーの段階的な適用戦略
DMARC を最初から reject に設定するのはリスクがある。送信設定に不備があると正規のメールまで拒否される。段階的なロールアウトが実務上の推奨だ。
DMARCポリシーの3段階ロールアウト:
フェーズ1:監視(none)
─────────────────────────────────────
設定: p=none; pct=100; rua=mailto:...
期間: 2〜4週間
目的: レポートを収集し、正規のメール送信経路を把握
確認: rua に届く集約レポートで SPF/DKIM の通過率を確認
フェーズ2:隔離(quarantine)
─────────────────────────────────────
設定: p=quarantine; pct=10; rua=mailto:...
期間: 2〜4週間
目的: 10% のみ quarantine に適用し影響を確認
確認: 正規メールが迷惑メールに入っていないか確認後、pct=100に上げる
フェーズ3:拒否(reject)
─────────────────────────────────────
設定: p=reject; pct=100; rua=mailto:...
期間: 恒久運用
目的: なりすましメールを完全拒否
注意: 正規の送信経路が全て SPF/DKIM を通過していることを事前確認
Terraform では var.dmarc_policy を変えるだけでポリシーを切り替えられる。フェーズ移行のたびにコンソールを操作する必要はない。
# フェーズ2(quarantine)へ移行
# terraform.tfvars を編集
dmarc_policy = "quarantine"
terraform plan # 変更差分を確認(TXTレコードが1件 change)
terraform apply # 適用
Part 4:SES の送信設定と IAM ポリシー
Lambda / アプリケーションからの送信に必要な IAM
# SES 送信専用のIAMポリシー(最小権限)
resource "aws_iam_policy" "ses_send" {
name = "ses-send-email-policy"
description = "Allow sending emails via SES for specific identity"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ses:SendEmail",
"ses:SendRawEmail"
]
Resource = aws_sesv2_email_identity.domain.arn
Condition = {
StringLike = {
# 送信元アドレスをドメイン単位で制限
"ses:FromAddress" = "*@${var.domain_name}"
}
}
}
]
})
}
Node.js / TypeScript からの送信例(AWS SDK v3)
import {
SESv2Client,
SendEmailCommand,
} from '@aws-sdk/client-sesv2'
const sesClient = new SESv2Client({ region: 'ap-northeast-1' })
interface SendMailOptions {
to: string
subject: string
html: string
text: string
}
export async function sendMail(opts: SendMailOptions): Promise<void> {
const command = new SendEmailCommand({
FromEmailAddress: `noreply@${process.env.DOMAIN_NAME}`,
Destination: {
ToAddresses: [opts.to],
},
Content: {
Simple: {
Subject: {
Data: opts.subject,
Charset: 'UTF-8',
},
Body: {
Html: { Data: opts.html, Charset: 'UTF-8' },
Text: { Data: opts.text, Charset: 'UTF-8' },
},
},
},
})
await sesClient.send(command)
}
Cloudflare Workers / Hono からの送信
Cloudflare Workers 環境では AWS SDK は使えない。代わりに SES の HTTP エンドポイントを直接呼ぶか、AWS Signature Version 4 で署名したリクエストを送る。
// Hono ルート内での SES 送信(Cloudflare Workers 対応)
// AWS SDK の代わりに @aws-sdk/signature-v4 ベースの署名を使う
import { AwsClient } from 'aws4fetch' // npm: aws4fetch
const aws = new AwsClient({
accessKeyId: env.AWS_ACCESS_KEY_ID,
secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
region: 'ap-northeast-1',
service: 'ses',
})
async function sendMailFromEdge(
to: string,
subject: string,
body: string
): Promise<void> {
const payload = {
Content: {
Simple: {
Subject: { Data: subject, Charset: 'UTF-8' },
Body: { Text: { Data: body, Charset: 'UTF-8' } },
},
},
Destination: { ToAddresses: [to] },
FromEmailAddress: 'noreply@example.com',
}
const response = await aws.fetch(
'https://email.ap-northeast-1.amazonaws.com/v2/email/outbound-emails',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}
)
if (!response.ok) {
const error = await response.text()
throw new Error(`SES send failed: ${error}`)
}
}
Part 5:サンドボックス解除申請
DNS 設定が完了したら、AWS コンソールから申請する。
申請の流れ:
1. SES コンソール → Account Dashboard
2. "Request production access" をクリック
3. 以下を入力:
─ Mail type: Transactional(トランザクション)or Marketing
─ Website URL: サービスのURL
─ Use case description: 送信目的の説明(詳細に書くほど通りやすい)
─ Additional contacts: 連絡先
申請の記載例(通過率が上がるポイント):
─────────────────────────────────────
"We send transactional emails (contact form confirmations, account
notifications) for [service name]. All recipients have explicitly
requested our emails. We have implemented SPF, DKIM, and DMARC
with p=quarantine policy. Unsubscribe links are included in all
marketing emails. Bounce and complaint handling is configured via
SNS notifications."
審査期間は通常 24〜72時間。申請が却下された場合は理由が通知され、再申請できる。
バウンス・苦情の自動処理(必須)
サンドボックス解除後も、バウンス率と苦情率が高いと SES アカウントが停止される。SNS 経由で通知を受け取り、自動処理する仕組みが必要だ。
# バウンス・苦情通知の SNS トピック
resource "aws_sns_topic" "ses_notifications" {
name = "ses-bounce-complaint-notifications"
}
# SES → SNS の通知設定(バウンス)
resource "aws_sesv2_configuration_set" "main" {
configuration_set_name = "default"
}
resource "aws_sesv2_configuration_set_event_destination" "bounces" {
configuration_set_name = aws_sesv2_configuration_set.main.configuration_set_name
event_destination_name = "bounces-and-complaints"
event_destination {
sns_destination {
topic_arn = aws_sns_topic.ses_notifications.arn
}
matching_event_types = [
"BOUNCE",
"COMPLAINT",
"DELIVERY_DELAY",
]
enabled = true
}
}
Part 6:検証コマンドと運用チェック
DNS レコードの確認
# SPF レコードの確認
dig TXT example.com +short
# → "v=spf1 include:amazonses.com ~all"
# DKIM レコードの確認(SESが生成したトークンを使う)
DKIM_TOKEN="abcdef1234567890abcdef1234567890abcdef12"
dig CNAME "${DKIM_TOKEN}._domainkey.example.com" +short
# → "abcdef1234567890abcdef1234567890abcdef12.dkim.amazonses.com."
# DMARC レコードの確認
dig TXT _dmarc.example.com +short
# → "v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc-reports@example.com; adkim=s; aspf=s"
# MAIL FROM MX レコードの確認
dig MX mail.example.com +short
# → "10 feedback-smtp.ap-northeast-1.amazonses.com."
SES コンソールでの検証ステータス確認
# AWS CLI でドメイン検証ステータスを確認
aws sesv2 get-email-identity \
--email-identity example.com \
--query '{
VerifiedForSendingStatus: VerifiedForSendingStatus,
DkimStatus: DkimAttributes.Status,
MailFromStatus: MailFromAttributes.MailFromDomainStatus
}' \
--output json
# 期待する出力:
# {
# "VerifiedForSendingStatus": true,
# "DkimStatus": "SUCCESS",
# "MailFromStatus": "SUCCESS"
# }
テスト送信と配信確認
# CLI からテストメール送信
aws sesv2 send-email \
--from-email-address "noreply@example.com" \
--destination '{"ToAddresses":["your-test@gmail.com"]}' \
--content '{
"Simple": {
"Subject": {"Data": "SES テスト送信"},
"Body": {"Text": {"Data": "テスト送信です。DKIM・SPF・DMARC の確認用。"}}
}
}'
# 送信統計の確認(過去14日間)
aws sesv2 get-account \
--query 'SendQuota' \
--output json
Conclusion:「設定」ではなく「設計」としてメール認証を捉える
SPF・DKIM・DMARC の設定は一度やれば終わりに見えるが、実際には継続的な管理が必要だ。
| 項目 | 設定のみの管理 | 設計(Terraform)による管理 |
|---|---|---|
| 環境の再現性 | コンソール操作の記録が残らない | terraform plan で差分が見える |
| ポリシー変更 | DNS コンソールを手動編集 | var.dmarc_policy を変えて apply |
| バウンス処理 | 手動確認・手動削除 | SNS → Lambda で自動処理 |
| 審査への説明 | 設定根拠が残らない | コード+コメントが証跡になる |
2026年時点でのメール送信は、認証なし = 届かない という前提で設計する必要がある。SES のサンドボックス解除は手段であって、その先にある DMARC reject への段階的移行が本当のゴールだ。
DNS レコードを Terraform で管理することで、設定変更の意図と経緯が git log に残る。それはセキュリティ設定における「変更の説明責任」を担保する最も実用的な方法だ。
この記事をシェア