
Terraformで多数のDNSレコードを含むRoute53ホストゾーンを一括インポート
Terraformで、多数のDNSレコードを含むAmazon Route 53ホストゾーンのインポートを行う機会がありましたので、その際の手順などを記載します。
経緯
あるAWSアカウント内でRoute53ホストゾーンが管理されていましたが、全てWeb Consoleで設定作業がされていて、100件以上のDNSレコードが作成されていました。
TerraformでIaC化を行うこととしましたが、手動で多数のDNSレコードのimport定義を作成するのはとても大変なため、スクリプトを準備して行いました。
手順の概要
以下のような手順としました。
- AWS CLIで、Route53ホストゾーンの情報をjsonファイルに出力
- jsonファイルから、スクリプトで、importブロックのtfファイルを作成
- 「terraform plan -generate-config-out=xxx.tf」で、import定義を基に、resource定義のtfファイルを作成
- resource定義のtfファイルを修正
- 「terraform plan」で確認
- 「terraform apply」でインポート
それぞれの手順の詳細を以下に記載します
AWS CLIで、Route53ホストゾーンの情報をjsonファイルに出力
まず、以下のようなコマンドで、対象のホストゾーンのDNSレコード情報を取得しました。
<ホストゾーンID>の部分は、Web Consoleにて、Route53の画面から対象ホストゾーンのホストゾーンIDを確認して入力します。
出力するファイル名は任意ですが、ホストゾーン名.jsonのようにすると、複数のホストゾーンを処理する場合などに分かりやすいと思います。(以下は、ホストゾーン名が hogehoge.hoge の場合の例です)
aws route53 list-resource-record-sets \
--hosted-zone-id /hostedzone/<ホストゾーンID> \
--query "ResourceRecordSets[*].{HostedZoneId:'<ホストゾーンID>', Name:Name, Type:Type}" \
--output json > hogehoge.hoge.json
出力内容は以下のような形で、DNSレコードの数分だけ出力されます。
[
{
"HostedZoneId": "<ホストゾーンID>",
"Name": "hogehoge.hoge.",
"Type": "NS"
},
(省略)
jsonファイルから、スクリプトで、importブロックのtfファイルを作成
次に、上記のjsonファイルから、Terraformのimportブロックのtfファイルを作成します。これは、以下のようなPythonスクリプトを実行することにより、一回で作成できました。
(このスクリプトはAIで生成させました。コードの記述内容詳細の検証はしていませんが、今回の作業目的は達成できました。いくつか前提とした条件は別途下述します)
import json
import sys
import re
def sanitize_name(name):
# ドットや特殊文字をアンダースコアに変換
sanitized = re.sub(r'\W+', '_', name.strip('.'))
# 先頭が数字ならアンダースコアを追加
if re.match(r'^\d', sanitized):
sanitized = f"_{sanitized}"
return sanitized
def get_output_filename(zone_name):
base = zone_name.strip('.')
return f"{base}_import.tf"
def extract_subdomain(full_name, zone_name):
if full_name.endswith(zone_name):
return full_name[:-len(zone_name)].strip('.')
return full_name.strip('.')
def generate_import_blocks(records):
output_blocks = []
seen_zone_ids = set()
if not records:
return output_blocks
base_zone = records[0]["Name"]
base_zone_clean = sanitize_name(base_zone)
for record in records:
zone_id = record["HostedZoneId"]
name = record["Name"]
rtype = record["Type"]
# ゾーンの import ブロックは一度だけ
if zone_id not in seen_zone_ids:
block = f"""import {{
to = aws_route53_zone.{base_zone_clean}
id = "{zone_id}"
}}"""
output_blocks.append(block)
seen_zone_ids.add(zone_id)
# ID 部分の生成
record_id = f"{zone_id}_{name.strip('.')}_{rtype}"
# リソース名の生成
if name == base_zone:
resource_name = f"{sanitize_name(name)}_{rtype.lower()}"
else:
subdomain = extract_subdomain(name, base_zone)
resource_name = f"{sanitize_name(subdomain)}_{rtype.lower()}"
block = f"""import {{
to = aws_route53_record.{resource_name}
id = "{record_id}"
}}"""
output_blocks.append(block)
return output_blocks
def main():
if len(sys.argv) != 2:
print("使い方: python generate_tf_imports.py input.json")
sys.exit(1)
input_file = sys.argv[1]
with open(input_file, 'r') as f:
records = json.load(f)
if not records:
print("JSON にレコードがありません。")
sys.exit(1)
zone_name = records[0]["Name"]
output_file = get_output_filename(zone_name)
blocks = generate_import_blocks(records)
with open(output_file, 'w') as f:
f.write("\n\n".join(blocks))
print(f"{output_file} に Terraform import ブロックを書き出しました。")
if __name__ == "__main__":
main()
上記スクリプトは、以下のようにして実行できます。スクリプトのファイル名を「generate_tf_imports.py」とした場合、上記のjsonファイルを引数で指定して実行すると、カレントディレクトリに「ホストゾーン名_import.tf」というファイル名で、該当のホストゾーン(aws_route53_zone)と全てのレコード(aws_route53_record)のimportブロックが出力されたファイルが作成されます。
python3 generate_tf_imports.py hogehoge.hoge.json
出力された結果は以下例のようになります。
import {
to = aws_route53_zone.hogehoge_hoge
id = "<ホストゾーンID>"
}
import {
to = aws_route53_record.hogehoge_hoge_ns
id = "<ホストゾーンID>_hogehoge.hoge_NS"
}
import {
to = aws_route53_record.subdomain_a
id = "<ホストゾーンID>_subdomain.hogehoge.hoge_A"
}
上記スクリプトはAIで生成させましたが、いくつか前提となっている条件を記載します。
- import定義の中に、1つのaws_route53_zone(ホストゾーン)の定義を作成
こちらに記載されている通り、idにはホストゾーンIDを指定。 - aws_route53_zoneのtoで指定するリソース名は、ホストゾーン名とする。
- import定義の中に、レコード数の分だけaws_route53_recordの定義を作成
こちらに記載されている通り、idには、ホストゾーンID、レコード名、レコードタイプを_(アンダースコア)で連結して指定。 - aws_route53_recordのtoで指定するリソース名は、以下の通りとする
- レコード名=ホストゾーン名の場合・・・レコード名とレコードタイプを_(アンダースコア)で連結
- レコード名≠ホストゾーン名の場合・・・サブドメイン名とレコードタイプを_(アンダースコア)で連結
- リソース名は、ドット(.)やハイフン(-)などの文字が含まれる場合は、_ (アンダースコア)に変換する。先頭が数字だった場合は、その前に_(アンダースコア)を付与する
補足点
- もしも、レコード名に = や * などの文字が含まれている場合には、該当レコードの import定義のみは、正常に変換されていませんでした。AWS CLIでjsonに出力した時点で、エスケープ文字になっていたためです。(例:= は \75)
変換後に、\ のようなエスケープ文字が含まれていた場合は、変換が正常でないことを確認後、手動で修正しました。
以下、=の文字がレコード名に含まれていた場合の修正例です。idに\075と表記されたままでは後続の処理で正常に動作しなかったため、web consoleでの表記と同じように=に修正しました。toのリソース名では=文字は使用できないため、この例では削除しています。
# 修正前
import {
to = aws_route53_record.subdomain_075_a
id = "<ホストゾーンID>_subdomain\075.hogehoge.hoge_A"
}
# 修正後
import {
to = aws_route53_record.subdomain_a
id = "<ホストゾーンID>_subdomain=.hogehoge.hoge_A"
}
「terraform plan -generate-config-out=xxx.tf」で、import定義を基に、resource定義のtfファイルを作成
次に以下のコマンドを実行します。(terraform initは完了している前提です)xxx.tfのファイル名は任意です。
terraform plan -generate-config-out=xxx.tf
xxx.tfに、importブロックで指定していたaws_route53_zoneとaws_route53_recordのリソース定義が出力されます。
(実行時にエラーメッセージが出力されましたが、xxx.tfにimportブロックで指定していたリソースのデータは全て出力されていて、以下で修正も行うため、今回の用途では問題ありませんでした。今回のようにスクリプトでimport定義のファイルを作らず、手動でimport定義のファイルを作っても、同様のエラーは表示されます)
resource定義のtfファイルを修正
上記で出力したtfファイルでは、不要なパラメータ等が存在し、上記で記載したエラーの原因になっているものもあるようでした。今回は、以下例のように修正しました。
- aws_route53_zone
# 変更前
# __generated__ by Terraform from <ゾーンID>
resource "aws_route53_zone" "hogehoge_hoge" {
comment = "Sample"
delegation_set_id = null
force_destroy = null
name = "hogehoge.hoge"
tags = {}
tags_all = {}
}
# 変更後
resource "aws_route53_zone" "hogehoge_hoge" {
comment = "Sample"
name = "hogehoge.hoge"
}
- aws_route53_record(エイリアスレコード以外)
レコードの定義は多数ありましたが、変更前の出力されるパラメータのパターンは全て同一で、変更後のパターンも同一だっため、エディタの一括編集機能で簡単に修正できました。
# 変更前
# __generated__ by Terraform
resource "aws_route53_record" "subdomain_a" {
allow_overwrite = null
health_check_id = null
multivalue_answer_routing_policy = false
name = "subdomain.hogehoge.hoge"
records = ["x.x.x.x"]
set_identifier = null
ttl = 300
type = "A"
zone_id = "<ゾーンID>"
}
# 変更後
resource "aws_route53_record" "subdomain_a" {
name = "subdomain.hogehoge.hoge"
records = ["x.x.x.x"]
ttl = 300
type = "A"
zone_id = aws_route53_zone.hogehoge_hoge.zone_id
}
- aws_route53_record(エイリアスレコード)
エイリアスレコードの場合は、上記のそれ以外のレコードと少しパラメータの内容が異なりますが、こちらも、変更前の出力されるパラメータのパターンと、変更後のパターンは同一だっため、エディタの一括編集機能で簡単に修正できました。
尚、aliasのブロック内に出力されているzone_idは、ユーザ側で作成しているRoute53ホストゾーンのIDとは異なり、aliasで設定しているAWSサービスの固有のゾーンIDとなり、AWS側で定義されているものです。以下の例だとcloudfrontですが、cloudfrontの場合には、こちらに記載されているゾーンID(Z2FDTNDATAQYW2)となります。(この値はサービスやリージョンなどにより異なり、上記リンク先のようにAWS公式サイトに記載されています)今回は、aliasのブロック内に表記されているzone_id(AWS側で定義)は、variableにて設定し、aliasのブロックからは参照する形としました。
# 変更前
# __generated__ by Terraform
resource "aws_route53_record" "sub_a" {
allow_overwrite = null
health_check_id = null
multivalue_answer_routing_policy = false
name = "sub.hogehoge.hoge"
records = []
set_identifier = null
ttl = 0
type = "A"
zone_id = "<ゾーンID>"
alias {
evaluate_target_health = false
name = "xxx.cloudfront.net"
zone_id = "Z2FDTNDATAQYW2"
}
}
# 変更後
resource "aws_route53_record" "sub_a" {
name = "sub.hogehoge.hoge"
type = "A"
zone_id = aws_route53_zone.hogehoge_hoge.zone_id
alias {
evaluate_target_health = false
name = "xxx.cloudfront.net"
zone_id = var.cloudfront_zone_id
}
}
variable "cloudfront_zone_id" {
description = "Cloudfront Fixed Zone ID"
type = string
default = "Z2FDTNDATAQYW2"
}
「terraform plan」で確認
terraform planを行い、インポート対象リソースに間違いないことを確認しました。最後に出力されるPlan結果のimportの数が、importブロックで指定したリソース数(aws_route53_zoneの数+aws_route53_recordの数)と一致して、add, change , destroyの数が全て 0 であることを確認しました。
terraform plan
(実行結果例)
Plan: 123 to import, 0 to add, 0 to change, 0 to destroy
「terraform apply」でインポート
terraform applyを行い、インポートしました。実行結果は、直前のplanと同じになったことを確認しました。また、tfstateファイルが所定の保存場所(S3バケット)に作成・更新されたことを確認しました。
terraform apply
(実行結果例)
Apply complete! Resources: 123 imported, 0 added, 0 changed, 0 destroyed.
補足:AWS環境への接続設定について
今回、AWS CLIやterraformコマンドでAWS環境への接続を行っていますが、該当のAWSアカウントにはMFAで接続していたため、こちらの記事の設定内容で、接続を行いました。
おわりに
Terraformで、多数のDNSレコードを含むAmazon Route 53ホストゾーンをインポートする作業について記載しました。この記事が皆様のお役に立てば幸いです。
アノテーション株式会社について
アノテーション株式会社はクラスメソッドグループのオペレーション専門特化企業です。
サポート・運用・開発保守・情シス・バックオフィスの専門チームが、最新 IT テクノロジー、高い技術力、蓄積されたノウハウをフル活用し、お客様の課題解決を行っています。
当社は様々な職種でメンバーを募集しています。
「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、アノテーション株式会社 採用サイトをぜひご覧ください。