Terraform

Terraformリファクタリング手法: moved / removed / import の実務パターン

2026-04-19
NicheeLab編集部

既存のリソースを壊さずに構成を整理するには、状態の整合性を保つための道具立てが不可欠です。本稿では Terraform の moved・removed・import を軸に、壊さないリファクタリングを段取り良く進める手法をまとめます。

HashiCorp 公式の安定機能を前提に、試験で狙われやすいポイントと現場での落とし穴を一体で押さえます。バージョン依存がある箇所は注意書きを付けています。

なぜ moved / removed / import なのか

Terraform は構成(コード)と状態(state)を突き合わせて差分を適用します。リファクタリング時にアドレス変更や撤去・取り込みがあると、状態の写像が崩れて誤消去や再作成が発生しがちです。moved・removed・import はこの『写像の修復』を公式に支援します。

特に、moved はアドレスの付け替え、removed は撤去の明示、import は既存資産の取り込みに対応します。正しく使えば計画はノーチェンジまたは最小差分になり、メンテナンスの安全性が飛躍的に上がります。

  • 状態の写像を壊さない原則: 先に写像を直し、後でコードを動かす
  • 計画(plan)は常に人が読む: 無関係な再作成が出たら中断
  • 試験では各機能の『状態への作用』『実インフラへの作用』の違いが頻出
機能主目的状態ファイルへの作用/性質
movedアドレスの変更(リネーム/移動)を状態に伝えるfrom→to の対応を記録し、再作成を防止。計画は通常ノーチェンジ
removedリソース撤去の明示と安全な切り離し状態からの除去を宣言的に行う(対応外は state rm を使用)。実体破壊は要計画確認
import(ブロック/CLI)既存リソースをTerraform管理下に取り込む状態に外部実体の対応を追加。構成と一致すれば計画はノーチェンジ

状態の写像を直してから適用する流れ

Before                Mapping                 After
aws_s3_bucket.web  -> moved{from web to app} -> aws_s3_bucket.app
(not imported)     -> import{id=...}         -> state has instance
(legacy)           -> removed/from           -> state forgets legacy

最小例: moved/removed/import を段取りに組み込む

# moved: リネームを安全に
moved {
  from = aws_s3_bucket.web
  to   = aws_s3_bucket.app
}

# removed: 撤去を明示 (サポート外/古い環境では state rm を使用)
# removed {
#   from = aws_s3_bucket.legacy
# }
# 代替: terraform state rm aws_s3_bucket.legacy  # 実体は削除しない

# import (Terraform 1.5+): 既存を取り込む
import {
  to = aws_s3_bucket.logs
  id = "my-logs-bucket"
}

moved ブロックの基本と定番パターン

moved ブロックは、リソースやモジュールのアドレスを変更した事実を Terraform に宣言します。これにより、状態の対応関係が保たれ、不要な destroy/create を避けられます。

よくあるのはリソース名のリネーム、モジュール階層の変更、for_each のキー変更です。いずれも『変更前の完全修飾アドレス』から『変更後の完全修飾アドレス』を正しく記述するのがコツです。

  • 配置場所: 変更を行うモジュール側のルートに置く(そのモジュール配下に効く)
  • 順序: 複数の moved は依存の浅いものから適用
  • for_each/ count の個別インスタンスは [key] / [index] を明示
  • モジュール跨ぎの移動は module.old.resource → module.new.resource の形で書く
パターン変更例注意点
単純リネームaws_s3_bucket.web aws_s3_bucket.app同一モジュール内でのアドレス変化のみ
モジュール移動module.old.aws_s3_bucket.b module.new.aws_s3_bucket.b両モジュールの境界を正確に指定
for_each のキー変更aws_instance.srv["a"] aws_instance.srv["blue"]全キー分の moved を用意

moved によるアドレス再マッピング

State (old)                  Mapping                        State (new)
module.web.aws_lb.app  --->  moved{from module.web...        module.edge.aws_lb.app
                               to   module.edge...}
resource.srv["a"]      --->  moved{from srv["a"] to srv["blue"]}  resource.srv["blue"]

moved の書き方いろいろ

# 1) 単純リネーム
moved {
  from = aws_s3_bucket.web
  to   = aws_s3_bucket.app
}

# 2) モジュール間の移動
moved {
  from = module.old.aws_security_group.sg
  to   = module.new.aws_security_group.sg
}

# 3) for_each のキー変更
moved {
  from = aws_instance.srv["a"]
  to   = aws_instance.srv["blue"]
}

# 実務Tip: moved を追加→plan でノーチェンジを確認→その後にリネーム/移動のコード編集をコミット

removed の安全撤去パターン(および state rm 併用)

不要になったリソースを撤去する際は、状態と実インフラ双方で『何を残し、何を消すか』をコントロールします。Terraform では撤去の明示に removed を使える場合があります。環境やバージョンで未対応の場合は terraform state rm で状態から切り離すのが代替です。

重要なのは、state rm は状態からの除去のみで、実インフラは削除しない点です。逆に、構成からリソースを消すだけだと『destroy 計画』が出ます。どちらを選ぶかは運用方針(実体を残す/消す)で決めます。

  • 実体を残す: state rm で Terraform の管理対象から外す
  • 実体も消す: 構成を残したまま destroy させる(タグ付け等で最終確認)
  • 順序を明記した PR と Release Note を用意し、適用タイミングを分ける
  • バージョン差異に注意: removed ブロックが使えない場合の代替手順を準備
目的推奨手段副作用/注意
実体も削除構成は残し plan/apply で destroy を実行→その後コード撤去誤消去防止に plan 出力のレビュー必須
実体は維持(管理解除)terraform state rm <addr>以後 Terraform は追跡しない。ドリフト検知も不可
宣言的撤去removed ブロックが使える場合に from を宣言環境互換性を確認(未対応なら state rm)

撤去時の分岐

Config remove?   State action         Infra action
Yes              plan shows -destroy  Destroy on apply
No (state rm)    state forgets        Infra remains
removed block    state forgets (decl) Infra: per plan/policy

撤去の具体手順

# A) 実体も削除: 計画で -destroy を確認してから適用
# (構成は一旦残す)
resource "aws_s3_bucket" "tmp" { /* ... */ }
# → plan に -/+ が出たら中断。- のみ(削除)を確認。

# B) 実体は残し管理解除: state rm
# 実体を別チームへ引き渡す等
terraform state list | grep aws_s3_bucket.tmp
terraform state rm aws_s3_bucket.tmp

# C) 宣言的撤去 (環境が対応している場合)
# removed {
#   from = aws_s3_bucket.legacy
# }

import ブロックによる宣言的インポート

Terraform 1.5+ では import ブロックで『どの実体(ID)をどのアドレスに取り込むか』を宣言できます。構成が実体の属性と一致すれば、計画はノーチェンジになり、既存資産の取り込みが安全に行えます。従来の terraform import CLI も利用可能です。

インポートに必要な ID は各リソースの公式ドキュメントに記載があります。複合 ID のケース(例: arn, project/region/name 形式)では正しいフォーマットで指定してください。

  • import ブロックは一時的に追加し、取り込み後に残しておいても無害
  • 構成と実体が一致しないと plan に差分が出る(原因切り分けの助け)
  • 複数インスタンスは複数の import ブロックで列挙
  • CLI import は即時だが宣言が残らないため、履歴性はブロックが優位
対象インポート ID 例補足
aws_s3_bucket.logsmy-logs-bucket一意バケット名
aws_iam_role.appapp-roleアカウント/パスに依存する場合あり
google_compute_instance.vm["blue"]projects/P/ zones/Z/ instances/NAMEフォーマット要確認

宣言的インポートの流れ

Existing Infra ----id----> import { to=addr, id=... } ----> State
                          ^ attributes must match resource config

import ブロックと CLI の併用例

# 1) 宣言的インポート (推奨: 1.5+)
resource "aws_s3_bucket" "logs" {
  bucket = "my-logs-bucket"
  force_destroy = false
}
import {
  to = aws_s3_bucket.logs
  id = "my-logs-bucket"
}

# 2) 旧来の CLI でも可
# terraform import aws_s3_bucket.logs my-logs-bucket

モジュール分割・統合での合わせ技(moved × import)

既存モノリシックモジュールの分割や、逆に統合するケースでは、moved でアドレスを付け替えつつ、外部に既存だったリソースは import で取り込みます。段階的に進めると計画の安定性が上がります。

推奨は『段階的リリース』です。1) 新モジュールを空で導入、2) moved で状態の写像を準備、3) 構成を移す、4) 足りない実体は import、5) テスト、という順。

  • 写像の準備(moved)→構成移動→不足分の import の順を守る
  • 分割時は module.path の完全表記で moved を網羅
  • テスト環境で plan のノーチェンジを確認し、段階適用
  • 統合時は命名衝突を先に解消(プレフィックスなど)
ステップ手段目的/確認ポイント
1. 新モジュール導入module ブロックを追加依存と変数 I/O の整合性確認
2. 状態の写像moved from old → newplan がノーチェンジであること
3. 構成移動リソース定義を新モジュールへ差分が出たら戻って moved を見直す
4. 既存取り込みimport ブロック/CLI未管理資産を取り込みノーチェンジへ収束

分割の段階適用イメージ

module.monolith.aws_*  --moved-->  module.network.aws_* / module.app.aws_*
        ^                                   ^ import missing pieces here
        |                                   |
     initial                          after staged refactor

モジュール分割のスケルトン

# 旧: module.monolith 内の SG を新: module.network へ
moved {
  from = module.monolith.aws_security_group.web
  to   = module.network.aws_security_group.web
}

module "network" {
  source = "./modules/network"
  # ... vars
}

# 取りこぼしの既存実体をインポート
import {
  to = module.network.aws_network_acl.default
  id = "acl-123456"
}

ガードレールとCI運用: 壊さないための手順化

リファクタリングは『手順の再現性』が品質です。計画の保存、レビュー、適用の分離、状態バックアップは定型にしましょう。特に moved/import を含む PR は、plan のノーチェンジ(または最小差分)をスクリーンショットやアーティファクトで必ず共有します。

ドリフトが疑われる場合は、適用前に状態の健全性チェック(state list/show)と、必要なら refresh を実行します。ターゲット適用(-target)は救済策であり恒常運用には使いません。

  • terraform fmt/validate plan -out=plan.tfplan 人手レビュー apply plan.tfplan
  • state のバックアップを有効化(リモートバックエンド + バージョニング)
  • 重要変更は環境で段階適用(dev → staging → prod)
  • 失敗時のロールバック戦略(前バージョン state の復元)を準備
失敗パターン兆候回避策
moved 漏れで再作成plan に -/+ が出るアドレス一致を見直し moved を追加
import ID 不一致plan に変更が大量発生公式ドキュメントの ID 仕様を再確認
state rm の誤用実体が残存/ドリフト運用方針を明確化(管理解除か削除か)

CI パイプラインの最小構成

[fmt] -> [validate] -> [plan -out] -> [manual review] -> [apply plan] -> [post-check]
   |                                                        ^ artifacts(plan, logs)

CI 実行例(擬似コード)

terraform fmt -check
terraform validate
terraform init -input=false
terraform plan -input=false -out=plan.tfplan
# 人手承認後
terraform apply -input=false plan.tfplan
# 監査用に state バックアップ/plan 出力を保存

問題で確認

Pro

問題 1

既存の aws_instance.web を aws_instance.app に名称変更し、さらに module.old から module.new へ移動する。再作成を発生させずに進めたい。もっとも適切な手順はどれか。

  1. moved で from=module.old.aws_instance.web, to=module.new.aws_instance.app を宣言し、plan でノーチェンジを確認した後に構成を移す
  2. 構成を先に書き換えて plan を取り、差分が大きくなければ apply する
  3. terraform state rm で aws_instance.web を外してから新しい app を apply する
  4. import ブロックで既存 web を app のアドレスに取り込む

正解: A

アドレス変更(リネーム/モジュール移動)は moved で状態の写像を先に直すのが定石。構成先行や state rm は無用な再作成や管理解除を招く。import は既存を Terraform 管理下に置くための機能であり、すでに管理済みのリソースのリネーム/移動には使わない。

よくある質問

moved と import の違いは?いつどちらを使う?

moved は『すでに Terraform 管理下にあるリソースのアドレス変更』を状態へ伝えるもの。import は『まだ Terraform 管理外の既存リソース』を状態へ取り込むもの。管理下→アドレス変更は moved、管理外→取り込みは import。

removed ブロックが環境で使えません。どうすべき?

removed が未対応/制約下なら、目的で分岐します。実体も削除したい場合は構成を残して plan/apply の destroy を実行し、その後コードを消す。実体は残して管理解除したい場合は terraform state rm を使います。

for_each のキー変更や一部だけの移動はどう書く?

個別インスタンスを明示します。例: moved { from = aws_instance.srv["a"] to = aws_instance.srv["blue"] }。複数あればそれぞれ moved を用意。モジュール跨ぎも同様に完全修飾アドレスで指定します。

この記事で学んだ内容を問題で確認しましょう

16,000問以上の問題で実力チェック

無料で問題を解いてみる
この記事の著者

NicheeLab編集部

データエンジニアリング・クラウド資格の専門家。Databricks・Snowflake等の認定資格を保有し、実務経験に基づいた問題作成・解説を行っています。NicheeLab運営。


関連記事
Terraform

Terraform HCL 構文の基礎:Block / Attribute / Expression を正しく使い分ける

Terraform Associate で頻出の HCL 構文を、ブロック・属性・式の3視点で整理。実務で迷いがちな書き...

Terraform

Terraform Authoring & Ops Pro: 上位資格の範囲と対策

上位レベルを想定したTerraformの設計・運用ドメインを整理し、実務で通用する対策を提示。モジュール設計、ステート運...

Terraform

Terraform Providers の基本: プラグイン型アーキテクチャを正しく使いこなす

Associate レベルで押さえるべき Provider の基礎、インストール、バージョニング、認証、エイリアス運用を...

Terraform

Terraform Resourceブロック徹底ガイド: 最小単位のリソース定義

Associateレベルで押さえるべきResourceブロックの構造、依存関係、メタ引数、ライフサイクル制御を実務目線で...

Terraform

Terraform Data Source徹底理解:既存リソースの参照で壊さず足す

Terraform Associate向けに、Data Sourceを用いた既存リソース参照の基本、選択基準、評価順序、...

Terraformの記事一覧 (101件)
© 2026 NicheeLab All rights reserved.