Terraform

Terraform state mvコマンド,movedブロックでリファクタリング

Terraformには元々 state mv という状態情報を更新できるコマンドがありました。

一度決めたリソース名のリネームやリソースを別のモジュールへ変更(移動)できて、リファクタリングにはとても便利でしたが、チーム運用の観点から考えるとレビューがやりづらいという弱点がありました。

しかしTerraform v1.1 にてmovedブロックなるものが登場して、この弱点が解消されたようです。

僕も最近Terraformをリファクタリングをする機会がありmovedブロックを使ったのでメモっておきます。

スポンサーリンク

state mvコマンドでリソースを別モジュールへ移動

まず基本的な挙動を理解するためにstate mvコマンドでリソースを移動します。

aws_vpcリソースとaws_subnetリソースの2つのリソースをvpcモジュールにまとめて定義していたものを、aws_subnetリソースのみsubnetモジュールとして分割(移動)したいというシチュエーションで進めます。

今回使用したサンプルコードはこちらのGitHubにコミットしました。

変更前vpcモジュールでvpcとサブネットを作成

まずinitコマンドで初期化してからapplyでvpcとsubnetを作成します。

$ terraform init
Initializing modules...
- vpc in modules/vpc
...
$
$ terraform apply
...
$

state listコマンドで状態情報を確認するとmodule.vpcとしてvpcとsubnetが作成されたことが確認できます。

$ terraform state list
module.vpc.aws_subnet.demo
module.vpc.aws_vpc.demo
$

tfファイルの編集

main.tfとmodules/vpc/main.tfを編集します。

今回はsubnetモジュールをあらかじめ用意したのでコメントアウトの解除のみでモジュールを有効にできますが、実際はsubnetモジュールのファイル群を新規作成するイメージです。

main.tf

main.tfのmodule “subnet”の部分のコメントアウトを解除してsubnetモジュールを有効にします。

$ vi main.tf
...
## リファクタリングのタイミングでコメントアウト解除
module "subnet" {
  source = "./modules/subnet"
  vpc-id = module.vpc.vpc-id
}
...

modules/vpc/main.tf

modules/vpc/main.tfの resource “aws_subnet” “demo” を削除します。

$ vi modules/vpc/main.tf
...
## リファクタリングのタイミングで削除
resource "aws_subnet" "demo" {
  vpc_id     = aws_vpc.demo.id
  cidr_block = "10.1.0.0/24"

  tags = {
    Name = "demo"
  }
}
...
$

これがこのまま残っていると、移行元のvpcモジュールのaws_subnetリソースと移行先のsubnetモジュールのaws_subnetリソースでの2重定義エラーとなってしまいます。

state mvコマンドでリソースを移動

subnetモジュールを新規に定義したのでinitで再度初期化します。

$ terraform init
Initializing modules...
- subnet in modules/subnet
...
$

この状態でplanコマンドを実行するとmodule.vpcからaws_subnetリソースが削除されて、module.subnetでaws_subnetが新規作成される挙動になることがわかります。本来やりたいことは削除→新規作成ではなく、ただの移動なのでこの挙動はまずいです。

$ terraform plan
...
Terraform will perform the following actions:

  # module.subnet.aws_subnet.demo will be created
  + resource "aws_subnet" "demo" {
...
  # module.vpc.aws_subnet.demo will be destroyed
  # (because aws_subnet.demo is not in configuration)
  - resource "aws_subnet" "demo" {
...
Plan: 1 to add, 0 to change, 1 to destroy.
$

これはtf定義ファイルのみ更新されていて状態情報が更新されていないために発生する矛盾です。この矛盾をstate mvコマンドで解消します。

## terraform state mv 移動元 移動先
$ terraform state mv module.vpc.aws_subnet.demo module.subnet.aws_subnet.demo
Move "module.vpc.aws_subnet.demo" to "module.subnet.aws_subnet.demo"
Successfully moved 1 object(s).
$

state mv後の状態情報を確認するとaws_subnetリソースがmodule.vpcからmodule.subnetへ変更されたことが確認できます。

$ diff -u terraform.tfstate terraform.tfstate.1672748270.backup
--- terraform.tfstate	2023-01-03 21:17:50.000000000 +0900
+++ terraform.tfstate.1672748270.backup	2023-01-03 21:17:50.000000000 +0900
@@ -1,12 +1,12 @@
 {
   "version": 4,
   "terraform_version": "1.3.6",
-  "serial": 3,
+  "serial": 4,
   "lineage": "023xxx",
   "outputs": {},
   "resources": [
     {
-      "module": "module.vpc",
+      "module": "module.subnet",
       "mode": "managed",
       "type": "aws_subnet",
       "name": "demo",
$

ここでplanとapplyコマンドを確認すると差分がなくなった(ソースコードと状態情報の整合性がとれた)ことが確認できます。

## No changes と出力される
$ terraform plan
module.vpc.aws_vpc.demo: Refreshing state... [id=vpc-005xxx]
module.subnet.aws_subnet.demo: Refreshing state... [id=subnet-0cfxxx]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are
needed.
$
$
## No changes と出力される
$ terraform apply
module.vpc.aws_vpc.demo: Refreshing state... [id=vpc-005xxx]
module.subnet.aws_subnet.demo: Refreshing state... [id=subnet-0cfxxx]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are
needed.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
$

state listコマンドでもaws_subnetリソースがmodule.vpcからmodule.subnetへ移動されたことが確認できます。

$ terraform state list
module.subnet.aws_subnet.demo
module.vpc.aws_vpc.demo
$

以上がTerraform v1.1まで使用していた、state mvコマンドの基本的な使い方になります。

ちなみに公式ドキュメントには、複雑すぎてmovedブロックとして表現できないリファクタリングの場合はstate mvを使うこともありですよ、と注意書きがあったので今後もお世話になるタイミングがあるかもしれません。

movedブロックでリソースを別モジュールへ移動

基本的な流れはstate mvコマンドと一緒です。state mvコマンドで行っていたことをmovedブロックで対応します。

このmovedブロックを使うことによって、state mvで実際の状態情報を更新する前にplanコマンドでtfファイルの変更が正しいか(正常にリソースの移動ができるか)を確認できるようになります。

tfファイルの編集

main.tf

movedブロックで移動前後のリソースをfrom/toで定義します。

$ vi main.tf
...
## リファクタリングのタイミングでコメントアウト解除
module "subnet" {
  source = "./modules/subnet"
  vpc-id = module.vpc.vpc-id
}

moved {
  from = module.vpc.aws_subnet.demo
  to   = module.subnet.aws_subnet.demo
}
...
$

planコマンドで実行前確認

ここでplanコマンドを実行するとhas moved toとリソースを移動する旨が表示されます。

$ terraform plan
...
Terraform will perform the following actions:

  # module.vpc.aws_subnet.demo has moved to module.subnet.aws_subnet.demo
    resource "aws_subnet" "demo" {
        id                                             = "subnet-0f3xxx"
        tags                                           = {
            "Name" = "demo"
        }
        # (15 unchanged attributes hidden)
    }

Plan: 0 to add, 0 to change, 0 to destroy.
...
$

applyコマンドでリソースを移動

applyコマンドで状態情報を更新します。

$ terraform apply
...
Terraform will perform the following actions:

  # module.vpc.aws_subnet.demo has moved to module.subnet.aws_subnet.demo
    resource "aws_subnet" "demo" {
        id                                             = "subnet-0f3xxx"
        tags                                           = {
            "Name" = "demo"
        }
        # (15 unchanged attributes hidden)
    }

...
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
$

applyコマンド実行後に状態情報を確認するとstate mvコマンドで移動したときと同様にaws_subnetリソースがmodule.vpcからmodule.subnetへ移動されたことが確認できます。

$ diff -u terraform.tfstate.backup terraform.tfstate
--- terraform.tfstate.backup	2023-01-04 14:14:18.000000000 +0900
+++ terraform.tfstate	2023-01-04 14:14:18.000000000 +0900
@@ -1,12 +1,12 @@
 {
   "version": 4,
   "terraform_version": "1.3.6",
-  "serial": 3,
+  "serial": 4,
   "lineage": "011cd80f-60e3-86fb-cc54-cd8fbd5670a5",
   "outputs": {},
   "resources": [
     {
-      "module": "module.vpc",
+      "module": "module.subnet",
       "mode": "managed",
       "type": "aws_subnet",
       "name": "demo",
$

state listコマンドでも移動されたことが確認できます。(もちろんplan,applyコマンドでも差分は出力されません。)

$ terraform state list
module.subnet.aws_subnet.demo
module.vpc.aws_vpc.demo
$

以上がTerraform v1.1で追加された、movedブロックの基本的な使い方になります。

movedブロックのおかげで、状態情報を実際に更新する前にplanコマンドで確認できるので、レビューなどがやりやすくなったと思います。

Tips

countやfor_each構文を使ったリソースの個別指定

count構文の配列や、for_each構文を使ったMap、連想配列のリソースを個別に操作する場合は、公式ドキュメントこちらの記事のようにインデックス番号やキー名を指定します。(個別ではなくリソースをまるごと操作するときは指定しなくても大丈夫です。)

movedブロックの削除

削除せずに履歴としてそのまま残すことが強く推奨されています

個人で管理しているレベルのインフラなら変更後に削除してしまっても問題ないですが、複数人で管理しているインフラにおいて、movedの記述がないtfファイルをコミットして、それを別メンバーが使用したときに既存リソースの削除が起こる場合があります。(例えばローカルで状態情報管理していてそれが古いときなど)

今回は以上です〜ノシ

参考

(´・ω・`)ゞアリガトゴザイマス.。.・゚

公式ドキュメント state mvコマンド
公式ドキュメント movedブロック
movedブロックを使ってリファクタリングしてみた