はじめに

Terraform でインフラを長期運用していると、リソース名の変更、モジュール構成の見直し、ディレクトリ構造の整理など、コードのリファクタリングが必要になる場面が必ず訪れます。従来はこうした変更を行うたびに terraform state mv コマンドを手動で実行し、state ファイル上のリソースアドレスを書き換える必要がありました。この方法はコマンドの実行履歴が Git に残らず、チームメンバーが変更の意図を追いにくいという課題がありました。

Terraform 1.1 から導入された moved ブロックは、この課題を解決するための宣言的な仕組みです。HCL コード内に移動元と移動先のアドレスを記述するだけで、terraform plan 実行時に Terraform が自動的にリソースの再関連付けを認識し、不要な削除・再作成を防いでくれます。本記事では moved ブロックの概念から実践的な使い方、注意点までを順に解説します。

moved ブロックの基本概念

Terraform は .tf ファイルに書かれたリソースのアドレス(例: aws_instance.web)と、state ファイルに記録されたアドレスが一致しているかどうかでリソースの同一性を判断します。コード上でリソース名を変更したり、モジュールの中に移動したりすると、Terraform はこれを「新しいリソースの追加」と「古いリソースの削除」として解釈してしまいます。これは特にデータベースや本番稼働中のコンピューティングリソースにとって致命的な問題です。

moved ブロックは、この「見かけ上は別物だが実体は同じリソースである」という情報を Terraform に明示的に伝えるための構文です。構文は非常にシンプルで、以下のように記述します。

moved {
  from = aws_instance.old_name
  to   = aws_instance.new_name
}

terraform plan を実行すると、Terraform は from に指定されたアドレスが state 内に存在するかを確認し、存在する場合は削除・再作成ではなく「アドレスの変更」として扱います。これにより、実際のクラウドリソースには何の変更も発生せず、state ファイル上のメタデータだけが更新されます。

基本的な HCL 例

リソース名の変更

最もシンプルな例として、EC2 インスタンスのリソース名を web から web_server に変更するケースを見てみましょう。

# 変更前
resource "aws_instance" "web" {
  ami           = "ami-0123456789abcdef0"
  instance_type = "t3.micro"
}
# 変更後
resource "aws_instance" "web_server" {
  ami           = "ami-0123456789abcdef0"
  instance_type = "t3.micro"
}

moved {
  from = aws_instance.web
  to   = aws_instance.web_server
}

この状態で terraform plan を実行すると、以下のような出力が得られます。

aws_instance.web has moved to aws_instance.web_server
  Plan: 0 to add, 0 to change, 0 to destroy.

リソースの再作成が発生しないことが確認でき、安心して apply を実行できます。

count や for_each を使ったリソースのアドレス変更

count を for_each に変更するような、インデックスの表現方法が変わるリファクタリングでも moved ブロックが活躍します。

moved {
  from = aws_instance.servers[0]
  to   = aws_instance.servers["primary"]
}

moved {
  from = aws_instance.servers[1]
  to   = aws_instance.servers["secondary"]
}

このように、count のインデックス番号と for_each のキー文字列をそれぞれ対応付けることで、個々のリソースごとに移行経路を明示できます。対象リソースが多い場合は、生成スクリプトで moved ブロックを自動生成するアプローチも実務ではよく使われます。

state とのかかわり

moved ブロックは state ファイルを直接編集するわけではありません。あくまで「plan の計算時にこのアドレス変更を考慮する」という指示であり、実際に state ファイルが更新されるのは terraform apply が成功したタイミングです。

ここで重要なポイントは、moved ブロックはコードとして残しておいても害がないという点です。from に指定されたアドレスが state 上に存在しない場合(すでに移行済み、もしくは元から存在しない場合)、Terraform はそのブロックを単に無視します。そのため、チームの他のメンバーが古い state を使って apply した場合でも、moved ブロックが存在していれば安全に追従できます。これは terraform state mv を各自が個別に実行する運用と比べて、チーム全体の一貫性を保ちやすいという大きな利点です。

なお、moved ブロックは state のインポート(import ブロックや terraform import)とは異なる機能です。moved は「すでに state 内に存在するリソースのアドレスを変える」ためのものであり、「state にまだ存在しないリソースを新たに取り込む」場合は import を使う必要があります。両者を混同しないよう注意してください。

モジュールをまたぐ移動

moved ブロックの真価は、モジュール構成を変更するような大規模なリファクタリングで発揮されます。例えば、ルートモジュールに直接書かれていたリソースを、新しく作成したモジュールの中に移動するケースです。

# ルートモジュール側
module "network" {
  source = "./modules/network"
  # ...
}

moved {
  from = aws_vpc.main
  to   = module.network.aws_vpc.main
}

モジュール内部で、さらにサブモジュールへ移動する場合は、モジュールパス全体を含めたアドレスを指定します。

moved {
  from = module.old_module.aws_subnet.private
  to   = module.new_module.aws_subnet.private
}

モジュールそのものを別名に変更する場合も同様に書けます。

moved {
  from = module.vpc_old
  to   = module.vpc_new
}

モジュール単位で moved を書けば、内部に含まれる個々のリソースに対して一つずつ moved ブロックを書く必要はありません。Terraform はモジュール配下のすべてのリソースアドレスを一括で読み替えてくれます。これは大規模なモジュール分割・統合作業において非常に有効です。

ただし、モジュールの入出力(input variables や output)の構造自体が変わる場合は、moved ブロックだけでは対応できません。変数の受け渡し方法やデフォルト値の違いによって、リソースの属性値が実質的に変わってしまう場合は、plan の差分を必ず確認し、意図しない更新が発生していないかをチェックする必要があります。

クラウドプロバイダーとの関係

moved ブロックは Terraform コア(HCL とステート管理のレイヤー)の機能であり、AWS・GCP・Azure のいずれのプロバイダーを使っていても同じ構文・同じ挙動で利用できます。プロバイダー側でサポートの有無を気にする必要はありません。

実務上の違いとしては、各プロバイダーのリソース命名規則やモジュール構成の慣習によって、リファクタリングの頻度や規模が変わってくる点が挙げられます。例えば AWS では aws_instance や aws_s3_bucket のようにリソースタイプが細分化されているため、命名変更のリファクタリングが比較的多く発生します。GCP では google_compute_instance のようにプロジェクト・リージョン単位でリソースをグルーピングするケースが多く、モジュール跨ぎの移動が起こりやすい傾向があります。Azure ではリソースグループ単位の設計変更に伴い、モジュール構成全体を見直すタイミングで moved ブロックが多用される傾向があります。いずれの場合も、移動の基本的な考え方と記述方法は共通している点を覚えておくとよいでしょう。

注意点

moved ブロックを使う際には、いくつか注意すべき点があります。

第一に、リソースの「移動」とリソースの「変更」を混同しないことです。moved ブロックはアドレスの変更のみを扱うものであり、リソース自体の属性(例えばインスタンスタイプや AMI)を同時に変更すると、Terraform はアドレス変更と属性変更を両方検出し、属性変更分については通常通り update または場合によっては destroy・create が計画されます。リファクタリングと機能的な変更は、可能な限り別々のコミット・別々の apply に分けることを推奨します。

第二に、moved ブロックを削除するタイミングです。すべてのチームメンバーの state が新しいアドレスに移行したことを確認してから moved ブロックを削除するのが安全です。CI/CD パイプラインで state を共有している場合は特に問題になりにくいですが、個々のエンジニアがローカルで apply を行う運用の場合、古い moved ブロックをしばらく残しておくことをお勧めします。

第三に、プロバイダーをまたぐ移動や、リソースタイプそのものが変わるような移動(例えば aws_instance から aws_instance 以外のリソースタイプへ)には moved ブロックは使えません。あくまで同一リソースタイプ内でのアドレス変更に限定されることを理解しておく必要があります。リソースタイプ自体を変更したい場合は、removed ブロックと import ブロックを組み合わせる、または一時的に terraform state rm と terraform import を使うアプローチが必要になります。

第四に、Terraform Cloud や Terraform Enterprise でリモート state を使っている場合、moved ブロックの効果は plan 実行時にリモート側でも正しく評価されます。特別な設定は不要ですが、plan の出力をチームで必ずレビューし、意図しない diff が出ていないか確認する習慣をつけることが重要です。

FAQ

Q1. moved ブロックを書いた後、古いリソース名はコードから完全に消しても大丈夫ですか?

A. 問題ありません。moved ブロックの from 側は、現在のコードに存在しないリソースアドレスを指していても構いません。Terraform は state 内に残っている古いアドレスと moved ブロックを照合するため、コード上にはもう存在しない旧リソース名を from に書いても正しく動作します。

Q2. terraform state mv コマンドと moved ブロックはどちらを使うべきですか?

A. 基本的には moved ブロックの使用を推奨します。moved ブロックはコードとして Git にコミットされるため、変更の意図や履歴が残り、チーム全体で再現可能です。また CI/CD パイプライン上で自動的に適用されるため、各メンバーが個別にコマンドを実行する必要もありません。terraform state mv は緊急対応や、moved ブロックが対応できない特殊なケース(プロバイダー間の移動など)で補助的に使う位置づけと考えるとよいでしょう。

Q3. モジュールの source を変更しただけ(内容は同じ)の場合も moved ブロックが必要ですか?

A. モジュールの呼び出し名(ローカル名、例えば module "network" の network 部分)を変更した場合は moved ブロックが必要です。一方で source の参照先(Git リポジトリのパスやバージョンタグなど)だけを変更し、モジュール呼び出し名自体は変えていない場合は、リソースアドレスに変化がないため moved ブロックは不要です。

まとめ

moved ブロックは、Terraform コードのリファクタリングを安全かつ宣言的に行うための重要な機能です。リソース名の変更、count から for_each への移行、モジュール構成の再編成など、様々なシーンでリソースの不要な削除・再作成を防ぎ、チーム全体で変更履歴を共有できるようになります。AWS・GCP・Azure を問わず同じ構文で利用できるため、マルチクラウド環境でも一貫した運用が可能です。大規模なリファクタリングに着手する前には、必ず terraform plan の出力を確認し、moved ブロックが意図通りに機能しているかをチェックする習慣を身につけましょう。