【Cloudflare】Terraformプロバイダーをv5にアップグレードしました
こんにちは。SREエンジニアのいせです。
弊社ではCloudflareをTerraformで管理しているのですが、先日プロバイダーのバージョンを5系にアップグレードしました。
v5へのアップグレードは思っていた以上に多くの課題がありました。この記事では、一定規模の本番環境でv4からv5への移行を行った際に遭遇した問題と、試行錯誤しながら見つけた対応方法をまとめています。同じような状況に直面される方々の参考になれば幸いです。
弊社の環境について
弊社ではCloudflareで10ほどのゾーンを運営しており、DNSレコード数は全部で数百におよびます。
サービスの規模としては珍しくないのではないかと思うのですが、Terraformのリソースも数百ということなので、アップグレードはなかなか骨の折れる作業でした。
アップグレード前は4.39.0を使用しており、5.9.0にアップグレードを行いました。
アップグレードの大変さ
Terraformプロバイダーの5系のバージョンは、それまでとは異なる設計アプローチで開発されています。
v4までは手作業で実装されていたプロバイダーが、v5ではCloudflareのOpenAPI仕様から自動生成されるようになりました。
この変更の結果、破壊的な変更が多数発生し複雑なマイグレーション作業が必要になります。
公式ドキュメントではGritというツールを用いたマイグレーションスクリプトが公開されていますが、対応しきれないケースも多々あります。
Terraformのプロバイダーアップグレードでは、tfstateのマイグレーションも課題になります。
今回は破壊的な変更が多いため、tfstateをなんとかしてv5に対応するように変更することは諦め、まっさらなtfstateを新たに作成して、既存リソースをすべてインポートし直すという手順を踏みました。
この作業は、Claude CodeなどAIコーディングエージェントが実用になった時代だからこそ、現実的なスケジュールで完成させることができました。
AIがなかったら、構文チェックが通るようになるまでひたすら手作業で修正していたと思うと、なかなかしんどかったなと思います。
修正内容の例
実際に行った修正内容をいくつかご紹介します。
1. リソース名の対応表
まずはシンプルな名前の変更から。例えば以下のような名前の変更が必要でした。
cloudflare_record → cloudflare_dns_record
これは一括置換できますね。定義箇所だけでなく、別のリソースのパラメータへ値を受け渡す際に参照として使う箇所も忘れずに修正が必要です。
2. パラメータ名の変更や廃止
パラメータ名が変わってしまったり、廃止されたりといったことも多々あります。
例えばゾーン定義では以下のような修正を行いました。
resource "cloudflare_zone" "example_com" {
- account_id = "123456789"
- zone = "example.com"
- plan = "free"
- jump_start = false
+ account = {
+ id = "123456789"
+ }
+ name = "example.com"
type = "full"
- paused = false
}
3. ブロック・リスト形式の構造変更パターン
v5では多くのリソースでパラメータの持ち方が大幅に変更されました。主にブロック形式とリスト形式、オブジェクト形式間での変更が発生しています。
一括置換などがやりにくく、手作業では骨の折れる作業ですが、AIコーディングエージェントとTerraformの構文チェック機構が威力を発揮します。
AIはへこたれることなく、何度も何度も成功するまで修正と構文チェック実行を繰り返してくれます。
Cloudflareプロバイダーは構文エラーのメッセージが比較的わかりやすく、planコマンドを実行しなくてもあらかたエラーが検出できることも幸いしました。
ブロック → オブジェクト形式
# Pages プロジェクト
resource "cloudflare_pages_project" "example" {
- build_config {
+ build_config = {
build_command = "npm run build"
}
}
ブロック → リスト形式
# ルールセット
resource "cloudflare_ruleset" "example" {
- rules {
+ rules = [
+ {
enabled = true
action = "redirect"
- }
+ }
+ ]
}
複雑なパターン
resource "cloudflare_zero_trust_access_policy" "example" {
- include {
- ip = ["192.168.0.0/24"]
- }
+ include = [{
+ ip = {
+ ip = "192.168.0.0/24"
+ }
+ }]
}
4. ゾーン設定の分割
v5では、cloudflare_zone_settings_overrideが廃止され、代わりにcloudflare_zone_settingを使用する必要があります。この新しいリソースは設定項目ごとに個別のリソースを作成する必要があるため、for_eachを使用して一括で作成するアプローチを取りました。
-resource "cloudflare_zone_settings_override" "example_com" {
- zone_id = cloudflare_zone.example_com.id
- settings {
- ssl = "full"
- always_use_https = "on"
- }
-}
+locals {
+ zone_settings = {
+ ssl = "full"
+ always_use_https = "on"
+ }
+}
+
+resource "cloudflare_zone_setting" "example_com" {
+ for_each = local.zone_settings
+ zone_id = cloudflare_zone.example_com.id
+ setting_id = each.key
+ value = each.value
+}
5. DNSレコードの変更
次のような変更に遭遇しました。
name属性の取る値が、サブドメイン部分のみからFQDNに変更ttlの省略が不可になり、1を指定するとautomaticに設定されるように変更- CNAMEなどのレコードタイプで
contentの値の末尾にドットがある場合、v4では自動的に省略されていたが、v5ではそれがなくなった
DNSレコードは大量に存在するため、直接Terraformで定義するのではなく、別ファイルにYAML形式で定義したものを読み込んでfor_eachで展開するという方法を取っていました。
そこでそのファイルはいじらずに、動的にFQDNに変換したり、未定義の場合にデフォルト値を与えるような修正を加えています。
ただし、contentの末尾のドットについてはYAMLファイルのほうを修正しています。
TXTなどレコードタイプによっては除去するべきではないなど条件が複雑になることと、修正対象が一部のCNAMEレコードだけで、手作業で修正したほうが手っ取り早かったためです。
-resource "cloudflare_record" "example_com" {
+resource "cloudflare_dns_record" "example_com" {
for_each = local.records
zone_id = cloudflare_zone.example_com.id
- name = each.value.name
+ name = each.value.name == "@" ? "example.com" : "${each.value.name}.example.com"
content = each.value.value
type = each.value.type
- ttl = lookup(each.value, "ttl", null)
+ ttl = lookup(each.value, "ttl", 1)
priority = lookup(each.value, "priority", null)
proxied = lookup(each.value, "proxied", null)
}
作業の流れ
最終的には以下の流れでアップグレード作業を行いました。
1. 構文修正
前述したような構文変更への対応を行います。
AIコーディングエージェントに構文エラーがなくなるまで、繰り返しtfファイルの修正とterraform validateの実行をしてもらいます。
構文エラーの多さにびっくりしてたまにv5へのアップグレードを放棄しようとしてしまうので、その都度、叱咤激励しましょう。
エラーメッセージ自体はわかりやすいので、少量の修正であればスムーズにこなしてくれます。
量が多いとまとめて効率的に処理しようとしてだいたい失敗するので、効率は重視せずにひとつひとつ修正と確認を繰り返すように念押しするのがコツです。
プロンプトは何度も修正しながら実行したため、この一発ですべての作業が完了するというものではありません。
実際に利用した正確なプロンプトは残していなかったのですが、どのような内容だったか、一例として記載します。
プロンプトの例
terraformのプロバイダーをアップグレードしたところ、数多くの構文エラーが発生しました。以下の手順をエラーがなくなるまで繰り返し実施してください。
1. terraform validateコマンドを実行
2. エラーメッセージをひとつだけ選んで、tfファイルを修正
3. terraform validateコマンドでエラーがなおったことを確認
まとめて修正したりせず、エラーをひとつずつ修正してその都度必ずチェックをしてください。
効率よりひとつずつ着実に修正することを重視してください。
2. tfstateの再作成・既存リソースのインポート
tfstateのマイグレーションは難しいため、v4のtfstateを放棄してまっさらな状態からすべてのリソースをインポートします。
1. IDフォーマットの特定
各リソースのインポートするには、Terraform上でのリソース名と実際のCloudflare上でのリソースの識別子をterraform impotコマンドの引数として渡す必要があります。
フォーマットはリファレンスを参照してください。
私が作業をした際に必要となったリソースのIDフォーマットを掲載しておきます。
どうやらゾーンIDをパラメータに持つリソースは先頭にゾーンIDがつき、アカウントIDを持つリソースはそれが先頭につくようです。
また、ゾーンにもアカウントにも作成できるリソースの場合は、プレフィックスとして、accountsもしくはzonesがつき、そのあとにアカウントIDやゾーンID、そしてリソースのIDという構成です。
cloudflare_zone: <zone_id>
cloudflare_dns_record: <zone_id>/<record_id>
cloudflare_zone_setting: <zone_id>/<setting_name>
cloudflare_pages_project: <account_id>/<project_name>
cloudflare_account_member: <account_id>/<member_id>
cloudflare_ruleset: accounts/<account_id>/<ruleset_id> or zones/<zone_id>/<ruleset_id>
2. 旧バージョンのtfstateからリソース名とIDを取り出してインポートコマンドを生成
リソースIDのフォーマットがわかったので、次に実際の値を特定してインポートコマンドを生成します。
これにはv4のtfstateファイルを使いました。
terraform state pull > terraform_v4.tfstate
リソース名が変更になったり、ひとつのリソースが複数に分割されたりしたものをうまく修正しながら、AIにインポートコマンドを生成してもらいます。
tfstateファイルの実態は巨大なjsonファイルです。そのままAIに読み込ませるのは得策ではなく、構造を把握してjqコマンドなどを駆使させましょう。
for_eachなどを使った部分はjsonの構造が少し違うので要注意です。
3. インポートを実行
現在のtfstateファイルを削除して、生成してもらったコマンドを実行しリソースをインポートします。
4. planの実行と差分の対応
インポート後にplanを実行し、差分を確認します。
正しくインポートできていて、構文修正も問題なければNo changesと出力されるはずですが、そう簡単にはいきません。
プロバイダーのマイナーバージョンにもよりますが、うまくインポートできないバグやreadonlyのパラメータで差分がでてしまう問題があります。
少しずつ修正されてはいるものの、まだ完璧ではないため、そういったリソースに出くわした場合は可能であればignore_changesに突っ込んだり、あるいは潔く諦めてリソースを再作成するなどの対応が必要なる場合があります。
私の場合は、メンバーのインポートがうまくできず、最終的に再作成しました。
おわりに
Terraformのcloudflareプロバイダーの5系へのアップグレードについてご紹介しました。
これからv5アップグレードに取り組まれる方々の参考になれば幸いです。多くの課題はありますが、AIと根気よく対応していけばアップグレードできるのではないかと思います。
弊社では各種エンジニアを大募集しています。興味を持っていただけましたら、ぜひご応募ください!





