AWS SES サンドボックス解除とドメイン認証(DMARC)2026年版

|
AWS SES DKIM SPF DMARC メール Route53

AWS SESのサンドボックス解除とSPF・DKIM・DMARC設定まで、2026年版の手順。Route 53でのDNS設定。Terraformによる自動化メモ。

はじめに: 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:[email protected]"
}

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:[email protected]"

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}"
          }
        }
      }
    ]
  })
}

Lambda からの送信(問い合わせフォームバックエンド)

LP案件の問い合わせフォームは API Gateway → Lambda → SES の構成です。Lambda(Python)側での送信コードは以下になります。

# contact_form.py
import boto3
import json
import logging
import os

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

ses = boto3.client("sesv2", region_name=os.environ["AWS_REGION"])

FROM_ADDRESS  = os.environ["FROM_ADDRESS"]   # [email protected]
NOTIFY_ADDRESS = os.environ["NOTIFY_ADDRESS"] # 通知先(クライアントのメールアドレス等)


def lambda_handler(event, context):
    body = json.loads(event.get("body", "{}"))
    name    = body.get("name", "")
    email   = body.get("email", "")
    message = body.get("message", "")

    if not all([name, email, message]):
        return {"statusCode": 400, "body": json.dumps({"error": "Missing required fields"})}

    try:
        ses.send_email(
            FromEmailAddress=FROM_ADDRESS,
            Destination={"ToAddresses": [NOTIFY_ADDRESS]},
            Content={
                "Simple": {
                    "Subject": {"Data": f"【問い合わせ】{name} 様より", "Charset": "UTF-8"},
                    "Body": {
                        "Text": {
                            "Data": f"名前: {name}\nメール: {email}\n\n{message}",
                            "Charset": "UTF-8",
                        }
                    },
                }
            },
        )
        logger.info(f"Email sent to {NOTIFY_ADDRESS} from {email}")
        return {"statusCode": 200, "body": json.dumps({"message": "送信しました"})}

    except Exception as e:
        logger.error(f"SES send failed: {e}")
        return {"statusCode": 500, "body": json.dumps({"error": "送信に失敗しました"})}

環境変数は Lambda コンソールまたは Terraform の environment ブロックで設定します。

resource "aws_lambda_function" "contact_form" {
  function_name = "${var.project_name}-contact"
  # ...

  environment {
    variables = {
      FROM_ADDRESS   = "noreply@${var.domain_name}"
      NOTIFY_ADDRESS = var.notify_email
    }
  }
}

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:[email protected]; 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 "[email protected]" \
  --destination '{"ToAddresses":["[email protected]"]}' \
  --content '{
    "Simple": {
      "Subject": {"Data": "SES テスト送信"},
      "Body": {"Text": {"Data": "テスト送信です。DKIM・SPF・DMARC の確認用。"}}
    }
  }'

# 送信統計の確認(過去14日間)
aws sesv2 get-account \
  --query 'SendQuota' \
  --output json

まとめ: 設計としてメール認証を捉える

SPF / DKIM / DMARC の設定は一度やれば終わり、ではなく、実際には継続的な管理が必要です。

項目 設定のみの管理 設計(Terraform)による管理
環境の再現性 コンソール操作の記録が残らない terraform plan で差分が見える
ポリシー変更 DNS コンソールを手動編集 var.dmarc_policy を変えて apply
バウンス処理 手動確認・手動削除 SNS → Lambda で自動処理
審査への説明 設定根拠が残らない コード+コメントが証跡になる

2026年時点でのメール送信は、認証なし = 届かない という前提で設計する必要があります。SES のサンドボックス解除は手段であって、その先にある DMARC reject への段階的移行がゴールになります。

DNS レコードを Terraform で管理することで、設定変更の意図と経緯が git log に残ります。こうした部分も Terraform でインフラを管理する目的の一つです。

ご精読、ありがとうございました。

この記事をシェア

Twitter / X
記事一覧に戻る
🤖
Cloud Assistant
Llama 3.3 × AI Gateway

こんにちは!クラウドエンジニアのポートフォリオサイトへようこそ。AWS構成・副業サービス・お仕事のご相談など、何でも聞いてください 👋