既存のリソースを壊さずに構成を整理するには、状態の整合性を保つための道具立てが不可欠です。本稿では Terraform の moved・removed・import を軸に、壊さないリファクタリングを段取り良く進める手法をまとめます。
HashiCorp 公式の安定機能を前提に、試験で狙われやすいポイントと現場での落とし穴を一体で押さえます。バージョン依存がある箇所は注意書きを付けています。
Terraform は構成(コード)と状態(state)を突き合わせて差分を適用します。リファクタリング時にアドレス変更や撤去・取り込みがあると、状態の写像が崩れて誤消去や再作成が発生しがちです。moved・removed・import はこの『写像の修復』を公式に支援します。
特に、moved はアドレスの付け替え、removed は撤去の明示、import は既存資産の取り込みに対応します。正しく使えば計画はノーチェンジまたは最小差分になり、メンテナンスの安全性が飛躍的に上がります。
| 機能 | 主目的 | 状態ファイルへの作用/性質 |
|---|---|---|
| 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 ブロックは、リソースやモジュールのアドレスを変更した事実を Terraform に宣言します。これにより、状態の対応関係が保たれ、不要な destroy/create を避けられます。
よくあるのはリソース名のリネーム、モジュール階層の変更、for_each のキー変更です。いずれも『変更前の完全修飾アドレス』から『変更後の完全修飾アドレス』を正しく記述するのがコツです。
| パターン | 変更例 | 注意点 |
|---|---|---|
| 単純リネーム | 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 でノーチェンジを確認→その後にリネーム/移動のコード編集をコミット不要になったリソースを撤去する際は、状態と実インフラ双方で『何を残し、何を消すか』をコントロールします。Terraform では撤去の明示に removed を使える場合があります。環境やバージョンで未対応の場合は terraform state rm で状態から切り離すのが代替です。
重要なのは、state rm は状態からの除去のみで、実インフラは削除しない点です。逆に、構成からリソースを消すだけだと『destroy 計画』が出ます。どちらを選ぶかは運用方針(実体を残す/消す)で決めます。
| 目的 | 推奨手段 | 副作用/注意 |
|---|---|---|
| 実体も削除 | 構成は残し 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
# }Terraform 1.5+ では import ブロックで『どの実体(ID)をどのアドレスに取り込むか』を宣言できます。構成が実体の属性と一致すれば、計画はノーチェンジになり、既存資産の取り込みが安全に行えます。従来の terraform import CLI も利用可能です。
インポートに必要な ID は各リソースの公式ドキュメントに記載があります。複合 ID のケース(例: arn, project/region/name 形式)では正しいフォーマットで指定してください。
| 対象 | インポート ID 例 | 補足 |
|---|---|---|
| aws_s3_bucket.logs | my-logs-bucket | 一意バケット名 |
| aws_iam_role.app | app-role | アカウント/パスに依存する場合あり |
| google_compute_instance.vm["blue"] | projects/P/ zones/Z/ instances/NAME | フォーマット要確認 |
宣言的インポートの流れ
Existing Infra ----id----> import { to=addr, id=... } ----> State
^ attributes must match resource configimport ブロックと 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 で取り込みます。段階的に進めると計画の安定性が上がります。
推奨は『段階的リリース』です。1) 新モジュールを空で導入、2) moved で状態の写像を準備、3) 構成を移す、4) 足りない実体は import、5) テスト、という順。
| ステップ | 手段 | 目的/確認ポイント |
|---|---|---|
| 1. 新モジュール導入 | module ブロックを追加 | 依存と変数 I/O の整合性確認 |
| 2. 状態の写像 | moved from old → new | plan がノーチェンジであること |
| 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"
}リファクタリングは『手順の再現性』が品質です。計画の保存、レビュー、適用の分離、状態バックアップは定型にしましょう。特に moved/import を含む PR は、plan のノーチェンジ(または最小差分)をスクリーンショットやアーティファクトで必ず共有します。
ドリフトが疑われる場合は、適用前に状態の健全性チェック(state list/show)と、必要なら refresh を実行します。ターゲット適用(-target)は救済策であり恒常運用には使いません。
| 失敗パターン | 兆候 | 回避策 |
|---|---|---|
| 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 へ移動する。再作成を発生させずに進めたい。もっとも適切な手順はどれか。
正解: 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 を用意。モジュール跨ぎも同様に完全修飾アドレスで指定します。
NicheeLab編集部
データエンジニアリング・クラウド資格の専門家。Databricks・Snowflake等の認定資格を保有し、実務経験に基づいた問題作成・解説を行っています。NicheeLab運営。
Terraform HCL 構文の基礎:Block / Attribute / Expression を正しく使い分ける
Terraform Associate で頻出の HCL 構文を、ブロック・属性・式の3視点で整理。実務で迷いがちな書き...
Terraform Authoring & Ops Pro: 上位資格の範囲と対策
上位レベルを想定したTerraformの設計・運用ドメインを整理し、実務で通用する対策を提示。モジュール設計、ステート運...
Terraform Providers の基本: プラグイン型アーキテクチャを正しく使いこなす
Associate レベルで押さえるべき Provider の基礎、インストール、バージョニング、認証、エイリアス運用を...
Terraform Resourceブロック徹底ガイド: 最小単位のリソース定義
Associateレベルで押さえるべきResourceブロックの構造、依存関係、メタ引数、ライフサイクル制御を実務目線で...
Terraform Data Source徹底理解:既存リソースの参照で壊さず足す
Terraform Associate向けに、Data Sourceを用いた既存リソース参照の基本、選択基準、評価順序、...