2026年6月8日
2026年6月8日
Route 53 を Terraform で管理する方法|既存ゾーンの取り込み、レコードの書き方、消してはいけないゾーンの守り方
Route 53 を Terraform で管理するときは、「既存のホストゾーンとレコードを import で取り込んでから管理を始める」「ホストゾーンは prevent_destroy で削除から守る」「同じ名前と種類のレコードは1つのリソースにまとめる」の三つを押さえておくと、事故の多くを防げます。この記事では、プロバイダーと state の準備、既存環境の取り込み、WordPress サイトでよく使うレコードの書き方、plan の読み方と運用のルールまでを順に説明します。
Terraform と AWS プロバイダーは更新が早く、引数や推奨される設定が変わることがあります(2026年10月現在の情報です)。コードは例として、手元のバージョンのドキュメントと照らし合わせて使ってください。本番の DNS に適用する前に、検証用のドメインで一通り試し、現在のレコードを aws route53 list-resource-record-sets で書き出してバックアップしておきます。
管理する範囲を先に決める
Route 53 まわりには、Terraform で扱える対象が三つあります。
- ホストゾーン(aws_route53_zone):ゾーンそのもの。作ると NS と SOA が自動で用意されます。
- レコード(aws_route53_record):A・CNAME・MX・TXT などのレコードセット。
- 登録ドメインのネームサーバー設定:ドメインを Route 53 Domains で登録している場合、AWS プロバイダーにはその設定を扱うリソースもあります。ただしドメインの登録情報は影響が大きいため、最初は管理の対象に含めず、画面や CLI で扱うほうが安全です。
最初の段階では「ホストゾーンとレコード」を対象にし、ゾーンが複数あるならドメインごとに state を分けると、plan の差分が読みやすく、誤った変更の影響も1つのドメインに収まります。
プロバイダーと state の準備
DNS の state には、どのレコードをどのリソースで管理しているかが記録されます。手元のファイルに置くと、作業者が複数いるときに食い違いが起きるため、S3 などのリモートに置き、同時実行を防ぐロックを有効にします。
terraform {
required_version = ">= 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0" # 手元で使う主要バージョンに合わせる
}
}
backend "s3" {
bucket = "your-tfstate-bucket"
key = "dns/example.com.tfstate"
region = "ap-northeast-1"
encrypt = true
use_lockfile = true # S3 のロックファイルでロックする(新しい Terraform で使える方式)
}
}
provider "aws" {
region = "ap-northeast-1"
}
Route 53 はリージョンを持たないグローバルなサービスですが、プロバイダーにはリージョンの指定が要ります。ロックの方式は Terraform のバージョンによって推奨が変わってきたため(以前は DynamoDB のテーブルを使う方式が一般的でした)、使うバージョンのドキュメントで確認してください。state のバケットはバージョニングを有効にし、読み書きできる人を絞っておきます。
既存のゾーンとレコードを取り込む
すでに Route 53 で運用しているゾーンを Terraform で「作り直す」と、新しいゾーンには別のネームサーバーが割り当てられ、レジストラの設定と食い違ってドメインが引けなくなります。既存のものは import で state に取り込みます。Terraform 1.5 以降は、import ブロックをコードに書いて plan で確認できます。
import {
to = aws_route53_zone.main
id = "Z0123456789EXAMPLE" # ホストゾーンID
}
import {
to = aws_route53_record.cname["www"] # 後述の for_each の例に合わせた宛先
id = "Z0123456789EXAMPLE_www.example.com_CNAME" # ゾーンID_名前_種類
}
レコードの ID は「ゾーンID_名前_種類」の形で、加重などのルーティングポリシーを使っているレコードは末尾に「_識別子」が付きます。terraform plan -generate-config-out=generated.tf を実行すると、対応するリソースの定義がまだないものについて、取り込む内容から設定のひな形を書き出せます。生成されたコードはそのまま使わず、不要な引数を整理し、名前を読みやすく付け直してから本来のファイルへ移します。
取り込んだ後の plan で「変更なし」になることを確認してから、次の変更に進みます。ここで差分が出る場合は、コードと実際の値が食い違っているので、適用せずにコード側を直します。
レコードの書き方
ゾーンと基本のレコード
resource "aws_route53_zone" "main" {
name = "example.com"
comment = "managed by terraform"
lifecycle {
prevent_destroy = true
}
}
resource "aws_route53_record" "apex" {
zone_id = aws_route53_zone.main.zone_id
name = "example.com"
type = "A"
ttl = 300
records = ["203.0.113.10"]
}
resource "aws_route53_record" "mx" {
zone_id = aws_route53_zone.main.zone_id
name = "example.com"
type = "MX"
ttl = 3600
records = ["10 mail.example.net"]
}
# 同じ名前の TXT は1つのリソースにまとめる
resource "aws_route53_record" "apex_txt" {
zone_id = aws_route53_zone.main.zone_id
name = "example.com"
type = "TXT"
ttl = 3600
records = [
"v=spf1 include:_spf.example.net ~all",
"google-site-verification=your-verification-code",
]
}
output "name_servers" {
value = aws_route53_zone.main.name_servers
}
Route 53 では「名前と種類」の組み合わせが1つのレコードセットです。SPF と所有権確認の TXT を別々のリソースで書くと、互いの値を上書きし合ったり、作成時に既存のレコードとぶつかったりします。同じ名前・同じ種類の値は、1つのリソースの records にまとめます。
255 文字を超える TXT(DKIM の公開鍵など)は、文字列の途中に \"\" を入れて区切ります(例:"v=DKIM1; k=rsa; p=前半部分\"\"後半部分")。Route 53 は1つの文字列を 255 文字までしか扱えないためです。
サブドメインをまとめて書く
同じ形のレコードが多い場合は、変数と for_each を使うと、追加や削除が一覧の編集だけで済みます。
variable "cname_records" {
type = map(string)
default = {
"www" = "example.com"
"shop" = "shops.example-ec.net"
}
}
resource "aws_route53_record" "cname" {
for_each = var.cname_records
zone_id = aws_route53_zone.main.zone_id
name = "${each.key}.example.com"
type = "CNAME"
ttl = 300
records = [each.value]
}
CloudFront などへの ALIAS
ゾーン頂点を CloudFront やロードバランサーへ向けるときは、alias ブロックを使います。ALIAS のレコードには ttl と records を書きません。上の apex の A レコードの代わりに使う例です。
resource "aws_route53_record" "apex_alias" {
zone_id = aws_route53_zone.main.zone_id
name = "example.com"
type = "A"
alias {
name = aws_cloudfront_distribution.site.domain_name
zone_id = aws_cloudfront_distribution.site.hosted_zone_id
evaluate_target_health = false
}
}
接続先の zone_id は、ID を手で書かず、リソースの属性から参照すると書き間違いを防げます。
ゾーンを削除から守る
ホストゾーンを削除して作り直すと、ネームサーバーが変わります。レジストラの NS を更新するまでドメインが引けなくなるため、DNS の Terraform では最も避けたい事故です。
- prevent_destroy:上の例のように lifecycle に書いておくと、ゾーンを削除・置き換えする plan はエラーで止まります。
- force_destroy:レコードが残っていてもゾーンを削除できるようにする引数です。本番のゾーンでは有効にしません。
- 再利用可能な委任セット:複数のゾーンで同じネームサーバーを使いたい、作り直してもネームサーバーを変えたくない、という場合は、aws_route53_delegation_set を作って zone の delegation_set_id に指定する方法があります。既存のゾーンに後から付けることはできないため、新しくゾーンを作るときに検討します。
plan の読み方と運用のルール
plan では、記号で変更の種類を見分けます。~(update in-place)は TTL や値の変更で、通常はそのまま反映されます。-/+(replace)はリソースの作り直しで、レコードの名前や種類を変えたときに出ます。作り直しでは、古いレコードの削除と新しいレコードの作成の間に、短時間レコードがない状態になりうるため、アクセスの少ない時間に適用します。ゾーンに -/+ や - が出たら、適用せずに原因を調べます。
運用では、次のルールを決めておきます。
- 画面からの変更を禁止する:コンソールで直した値は、次の apply で Terraform の値に戻されます。緊急で画面から直した場合は、すぐにコードへ反映します。食い違いは
terraform plan -refresh-onlyで確認できます。 - 既存レコードの上書きに頼らない:aws_route53_record には、既存のレコードを上書きして作成する allow_overwrite という引数があります。便利ですが、取り込み漏れに気付けなくなるため、既存のものは import で取り込みます。
- 権限を絞る:Terraform を実行する IAM ロールには、対象のホストゾーンに対するレコードの参照と変更(route53:ListResourceRecordSets、route53:ChangeResourceRecordSets、route53:GetChange など)を中心に付け、ゾーンの削除権限は通常の実行ロールから外すことを検討します。必要な権限の一覧は、使うリソースによって変わるため公式ドキュメントで確認してください。
- 変更はレビューを通す:plan の結果をプルリクエストに貼り、別の人が確認してから apply します。
まとめ
- 既存の Route 53 は作り直さず、import ブロックで取り込み、plan が「変更なし」になってから管理を始めます。
- state は S3 などに置いてロックを有効にし、ドメインごとに分けると差分が読みやすくなります。
- 同じ名前と種類の値は1つの aws_route53_record にまとめ、長い TXT は区切り、ALIAS には ttl と records を書きません。
- ホストゾーンには prevent_destroy を付け、force_destroy は使いません。作り直すとネームサーバーが変わります。
- plan の
-/+を見落とさず、画面からの変更の禁止、権限の絞り込み、レビューを運用のルールにします。