📧

TerraformでSESv2の複数ドメイン検証とメール認証(SPF/DKIM/DMARC)対応

に公開

はじめに

  • 勉強と備忘録を兼ねてアウトプットしています
  • 本記事では複数ドメインのSES認証設定を扱います
  • 単一ドメインのみの場合は、より簡単な実装も可能です

前提条件

  • Route 53でホストゾーンが作成済みであること

構成

  • ドメイン認証に必要なリソースを ses モジュールとして作成します。
  • Route 53 のホストゾーンは、別途 route53 モジュールで作成されていることを前提とします。
.
├── main.tf            # 各モジュールを呼び出すファイル
└── modules/
    ├── route53/       # 事前準備: Route 53 ホストゾーンを作成するモジュール
    └── ses/           # 本記事で作成: SES 認証レコード用モジュール

1. モジュールの作成

variables.tf

# SESで設定するドメイン
variable "email_identities" {
  type = map(object({
    email_identity = string
  }))
}

# レコードを作成するRoute 53ゾーンのID
variable "route53_zone_id" {
  type = string
}

main.tf

# SESv2 Email Identity を作成し、Easy DKIM を有効化
resource "aws_sesv2_email_identity" "email_identities" {
  for_each = var.email_identities

  email_identity = each.value.email_identity
}

# ドメインを抽出(メールアドレスではなくドメインのみ)
locals {
  domain_identities = {
    for key, identity in var.email_identities : key => identity
    if !can(regex("@", identity.email_identity)) # Only for domains, not email addresses
  }
  
  # DKIMレコード用のデータ構造を作成
  dkim_records = flatten([
    for domain_key, domain_value in local.domain_identities : [
      for i in range(3) : {
        key     = "${domain_key}-${i}"
        domain_key = domain_key
        domain  = domain_value.email_identity
        index   = i
        token   = aws_sesv2_email_identity.email_identities[domain_key].dkim_signing_attributes[0].tokens[i]
      }
    ]
  ])
}

# DKIM Records using SESv2 Easy DKIM(複数ドメイン対応)
resource "aws_route53_record" "dkim_verification" {
  for_each = { for record in local.dkim_records : record.key => record }
  
  zone_id = var.route53_zone_id
  name    = "${each.value.token}._domainkey.${each.value.domain}"
  type    = "CNAME"
  ttl     = 600
  records = ["${each.value.token}.dkim.amazonses.com"]

  depends_on = [aws_sesv2_email_identity.email_identities]
}

# SPF Records (TXT record for each domain)
resource "aws_route53_record" "spf" {
  for_each = local.domain_identities

  zone_id = var.route53_zone_id
  name    = each.value.email_identity
  type    = "TXT"
  ttl     = 600
  records = ["v=spf1 include:amazonses.com ~all"]
}

# DMARC Records (TXT record for each domain)
# p=none: 監視モードでメール配信への影響なし。段階的にp=quarantine、p=rejectに変更可能
resource "aws_route53_record" "dmarc" {
  for_each = local.domain_identities

  zone_id = var.route53_zone_id
  name    = "_dmarc.${each.value.email_identity}"
  type    = "TXT"
  ttl     = 600
  records = ["v=DMARC1; p=none; rua=mailto:dmarc@${each.value.email_identity};"]
}

2. モジュールの利用

  • ルートの main.tf で route53 モジュールと ses モジュールを連携させます。
  • route53 モジュールの出力(zone_id)を ses モジュールへ渡します。
# 1. 事前に Route 53 ホストゾーンを作成
module "route53" {
  source = "./modules/route53"

  domain_name = "example.com"
  # ... その他必要な変数
}

# 2. SES モジュールを呼び出し(複数ドメイン対応)
module "ses_auth" {
  source = "./modules/ses"

  route53_zone_id = module.route53.zone_id

  email_identities = {
    "example-com" = {
      email_identity = "example.com"
    }
    "test-example-com" = {
      email_identity = "test.example.com"
    }
  }
}

3. 実行と確認

AWS コンソールで確認

サービス 確認項目
SES 各ドメインのIDステータス が「検証済み」
Route 53 各ドメインのDKIM(CNAME×3)、SPF(TXT)、DMARC(TXT)レコードが作成されている

メールヘッダーで確認

各ドメインからテストメールを送信し、ヘッダーに spf=pass, dkim=pass, dmarc=pass が含まれていることを確認。

Authentication-Results: spf=pass smtp.mailfrom=ap-northeast-1.amazonses.com;
  dkim=pass header.d=example.com;
  dkim=pass header.d=amazonses.com;
  dmarc=pass action=none header.from=example.com;
  compauth=pass reason=100

4. 複数ドメイン対応の実装ポイント

DKIMレコード作成の課題と解決

複数ドメインそれぞれに3つのDKIMレコードを作成する必要があるため、以下のような実装にしました:

# ポイント1: flatten関数でネストしたリストを平坦化
locals {
  dkim_records = flatten([
    for domain_key, domain_value in local.domain_identities : [
      for i in range(3) : {
        key        = "${domain_key}-${i}"  # 一意なキー生成
        domain_key = domain_key
        domain     = domain_value.email_identity
        index      = i
        token      = aws_sesv2_email_identity.email_identities[domain_key].dkim_signing_attributes[0].tokens[i]
      }
    ]
  ])
}

# ポイント2: for_eachで各レコードを作成
resource "aws_route53_record" "dkim_verification" {
  for_each = { for record in local.dkim_records : record.key => record }
  # ...
}

失敗例:countとfor_eachの混在

初期実装時に以下のような方法を試みましたが、Terraformではcountとfor_eachの混在ができないためエラーになりました:

# 失敗例
resource "aws_route53_record" "dkim_verification" {
  for_each = local.domain_identities
  count    = 3  # これはエラーになる
  
  # ...
}

MAIL FORM はまた次回にでも設定していきます...

Discussion