Architect's Log

I'm a Cloud Architect. I'm highly motivated to reduce toils with driving DevOps.

【Terraform】movedブロックでmoduleをリファクタリングする

movedブロックはTerraform1.1で導入された新機能です。moduleのリファクタリングに使えるか検証しました。

実行環境

ubuntu-20.04 on Windows 10 (WSL2)

$ terraform --version
Terraform v1.1.9
on linux_amd64
+ provider registry.terraform.io/hashicorp/aws v3.55.0

コード

準備したコードです。事前にapplyまで済ませてあります。

modules/vpc/variables.tf

variable "vpc_name" {
  type = string
}

variable "vpc_cidr" {
  type = string
}

variable "subnet" {
  type = list(object({
    name = string
    cidr = string
  }))
}

modules/vpc/main.tf

resource "aws_vpc" "this" {
  cidr_block  = var.vpc_cidr
  tags        = { Name = var.vpc_name }
}

resource "aws_subnet" "pub" {
  for_each = { for i in var.subnet : i.name => i }
  vpc_id      = aws_vpc.this.id
  cidr_block  = each.value.cidr
  tags        = { Name = each.value.name }
}

output "vpc_id" {
  value = aws_vpc.this.id
}

main.tf

module "vpc" {
  source = "../modules/vpc"

  vpc_name = "vpc-sample"
  vpc_cidr = "172.16.0.0/16"
  subnet = [
    {
      name = "subnet-sample1"
      cidr = "172.16.0.0/20"
    },
    {
      name = "subnet-sample2"
      cidr = "172.16.16.0/20"
    }
  ]
}

// moduleを使わない
resource "aws_security_group" "this" {
  name        = "securitygroup-sample1"
  vpc_id      = module.vpc.vpc_id
}

moduleを使わないリソースのリネーム

まずmoduleを使わず定義したリソースで試します。

セキュリティグループのリソース名をdefaultに変更します。

diff --git a/vpc_sample/main.tf b/vpc_sample/main.tf
index 6b09809..bfa4756 100755
--- a/vpc_sample/main.tf
+++ b/vpc_sample/main.tf
@@ -15,7 +15,7 @@ module "vpc" {
   ]
 }

-resource "aws_security_group" "this" {
+resource "aws_security_group" "default" {
   name        = "securitygroup-sample1"
   vpc_id      = module.vpc.vpc_id
 }

当然ですが、リソースを作り直すplanが出力されます。

$ terraform plan
aws_security_group.this: Refreshing state... [id=sg-02a601a6be35932e4]
module.vpc.aws_vpc.this: Refreshing state... [id=vpc-0c91c8c5673d89ee5]
module.vpc.aws_subnet.pub["subnet-sample1"]: Refreshing state... [id=subnet-01629b6f8717a5523]
module.vpc.aws_subnet.pub["subnet-sample2"]: Refreshing state... [id=subnet-0edf4b9618cf8f38f]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
  - destroy

Terraform will perform the following actions:

  # aws_security_group.default will be created
  + resource "aws_security_group" "default" {
      + arn                    = (known after apply)
      + description            = "Managed by Terraform"
      + egress                 = (known after apply)
      + id                     = (known after apply)
      + ingress                = (known after apply)
      + name                   = "securitygroup-sample1"
      + name_prefix            = (known after apply)
      + owner_id               = (known after apply)
      + revoke_rules_on_delete = false
      + tags_all               = (known after apply)
      + vpc_id                 = "vpc-0c91c8c5673d89ee5"
    }

  # aws_security_group.this will be destroyed
  # (because aws_security_group.this is not in configuration)
  - resource "aws_security_group" "this" {
      - arn                    = "arn:aws:ec2:ap-northeast-1:601660378746:security-group/sg-02a601a6be35932e4" -> null
      - description            = "Managed by Terraform" -> null
      - egress                 = [] -> null
      - id                     = "sg-02a601a6be35932e4" -> null
      - ingress                = [] -> null
      - name                   = "securitygroup-sample1" -> null
      - owner_id               = "601660378746" -> null
      - revoke_rules_on_delete = false -> null
      - tags                   = {} -> null
      - tags_all               = {} -> null
      - vpc_id                 = "vpc-0c91c8c5673d89ee5" -> null
    }

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

これを避けるには、従来はterraform state mvでリソース名をリネームする必要がありました。Terraform 1.1からはmovedブロックが利用できます。

dev.classmethod.jp

movedブロックを追加して、再度planを実行してみます。

$ git diff
diff --git a/vpc_sample/main.tf b/vpc_sample/main.tf
index 6b09809..2cb6d0b 100755
--- a/vpc_sample/main.tf
+++ b/vpc_sample/main.tf
@@ -15,7 +15,13 @@ module "vpc" {
   ]
 }

-resource "aws_security_group" "this" {
+resource "aws_security_group" "default" {
   name        = "securitygroup-sample1"
   vpc_id      = module.vpc.vpc_id
-}
\ No newline at end of file
+}
+
+moved {
+  from = aws_security_group.this
+  to = aws_security_group.default
+}
$ terraform plan
module.vpc.aws_vpc.this: Refreshing state... [id=vpc-0c91c8c5673d89ee5]
aws_security_group.default: Refreshing state... [id=sg-02a601a6be35932e4]
module.vpc.aws_subnet.pub["subnet-sample1"]: Refreshing state... [id=subnet-01629b6f8717a5523]
module.vpc.aws_subnet.pub["subnet-sample2"]: Refreshing state... [id=subnet-0edf4b9618cf8f38f]

Terraform will perform the following actions:

  # aws_security_group.this has moved to aws_security_group.default
    resource "aws_security_group" "default" {
        id                     = "sg-02a601a6be35932e4"
        name                   = "securitygroup-sample1"
        tags                   = {}
        # (8 unchanged attributes hidden)
    }

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

今度はリソース名をリネームするplanが出力されました。add/destroyが0であることを確認して、applyします。

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

無事にリソースを再作成することなくapplyできました。念のため再度planします。

$ terraform plan
module.vpc.aws_vpc.this: Refreshing state... [id=vpc-0c91c8c5673d89ee5]
aws_security_group.default: Refreshing state... [id=sg-02a601a6be35932e4]
module.vpc.aws_subnet.pub["subnet-sample2"]: Refreshing state... [id=subnet-0edf4b9618cf8f38f]
module.vpc.aws_subnet.pub["subnet-sample1"]: Refreshing state... [id=subnet-01629b6f8717a5523]

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.

コードとstateが同期されていることを確認できました。

module内リソースのリネーム

ここからが本題のmoduleのリファクタリングです。

vpcのリソース名を変更するので、先に確認しておきます。

$ terraform state list | grep aws_vpc
module.vpc.aws_vpc.this

リソース名を変更し、movedを定義します。

$ git diff
diff --git a/modules/vpc/main.tf b/modules/vpc/main.tf
index 15d46e6..b6848a6 100755
--- a/modules/vpc/main.tf
+++ b/modules/vpc/main.tf
@@ -1,15 +1,20 @@
-resource "aws_vpc" "this" {
+resource "aws_vpc" "default"
   cidr_block  = var.vpc_cidr
   tags        = { Name = var.vpc_name }
 }

+moved {
+  from = aws_vpc.this
+  to = aws_vpc.default
+}

 resource "aws_subnet" "pub" {
   for_each = { for i in var.subnet : i.name => i }
-  vpc_id      = aws_vpc.this.id
+  vpc_id      = aws_vpc.default.id
   cidr_block  = each.value.cidr
   tags        = { Name = each.value.name }
 }

 output "vpc_id" {
-  value = aws_vpc.this.id
+  value = aws_vpc.default.id
 }

planを実行します。

$ terraform plan
module.vpc.aws_vpc.default: Refreshing state... [id=vpc-0c91c8c5673d89ee5]
aws_security_group.default: Refreshing state... [id=sg-02a601a6be35932e4]
module.vpc.aws_subnet.pub["subnet-sample1"]: Refreshing state... [id=subnet-01629b6f8717a5523]
module.vpc.aws_subnet.pub["subnet-sample2"]: Refreshing state... [id=subnet-0edf4b9618cf8f38f]

Terraform will perform the following actions:

  # module.vpc.aws_vpc.this has moved to module.vpc.aws_vpc.default
    resource "aws_vpc" "default" {
        id                               = "vpc-0c91c8c5673d89ee5"
        tags                             = {
            "Name" = "vpc-sample"
        }
        # (15 unchanged attributes hidden)
    }

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

期待通りに、リネームするplanが出力されました。applyします(ログは省略)。

foreachを使ったリソースのリネーム

最後にforeachを使って生成したリソースをリネームできるか検証します。

サブネットのリソース名を確認しておきます。

$ terraform state list | grep aws_subnet
module.vpc.aws_subnet.pub["subnet-sample1"]
module.vpc.aws_subnet.pub["subnet-sample2"]

サブネットのリソース名を変更し、movedを定義しました。

$ git diff
diff --git a/modules/vpc/main.tf b/modules/vpc/main.tf
index b6848a6..9fd0774 100755
--- a/modules/vpc/main.tf
+++ b/modules/vpc/main.tf
@@ -15,6 +15,15 @@ resource "aws_subnet" "pub" {
   tags        = { Name = each.value.name }
 }

+moved {
+  from = aws_subnet.pub["subnet-sample1"]
+  to = aws_subnet.pub["pri-subnet-sample1"]
+}
+moved {
+  from = aws_subnet.pub["subnet-sample2"]
+  to = aws_subnet.pub["pri-subnet-sample2"]
+}
+
 output "vpc_id" {
   value = aws_vpc.default.id
 }
diff --git a/vpc_sample/main.tf b/vpc_sample/main.tf
index 2cb6d0b..009f1d2 100755
--- a/vpc_sample/main.tf
+++ b/vpc_sample/main.tf
@@ -5,11 +5,11 @@ module "vpc" {
   vpc_cidr = "172.16.0.0/16"
   subnet = [
     {
-      name = "subnet-sample1"
+      name = "pri-subnet-sample1"
       cidr = "172.16.0.0/20"
     },
     {
-      name = "subnet-sample2"
+      name = "pri-subnet-sample2"
       cidr = "172.16.16.0/20"
     }
   ]

planを実行します。

$ terraform plan
module.vpc.aws_vpc.default: Refreshing state... [id=vpc-0c91c8c5673d89ee5]
module.vpc.aws_subnet.pub["pri-subnet-sample1"]: Refreshing state... [id=subnet-01629b6f8717a5523]
aws_security_group.default: Refreshing state... [id=sg-02a601a6be35932e4]
module.vpc.aws_subnet.pub["pri-subnet-sample2"]: Refreshing state... [id=subnet-0edf4b9618cf8f38f]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.vpc.aws_subnet.pub["pri-subnet-sample1"] will be updated in-place
  # (moved from module.vpc.aws_subnet.pub["subnet-sample1"])
  ~ resource "aws_subnet" "pub" {
        id                              = "subnet-01629b6f8717a5523"
      ~ tags                            = {
          ~ "Name" = "subnet-sample1" -> "pri-subnet-sample1"
        }
      ~ tags_all                        = {
          ~ "Name" = "subnet-sample1" -> "pri-subnet-sample1"
        }
        # (9 unchanged attributes hidden)
    }

  # module.vpc.aws_subnet.pub["pri-subnet-sample2"] will be updated in-place
  # (moved from module.vpc.aws_subnet.pub["subnet-sample2"])
  ~ resource "aws_subnet" "pub" {
        id                              = "subnet-0edf4b9618cf8f38f"
      ~ tags                            = {
          ~ "Name" = "subnet-sample2" -> "pri-subnet-sample2"
        }
      ~ tags_all                        = {
          ~ "Name" = "subnet-sample2" -> "pri-subnet-sample2"
        }
        # (9 unchanged attributes hidden)
    }

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

期待通りにリネームするplanが出力されました。changeが2件出力されているのはtagsを変更したためです。論点ではないので、このままapplyします。

$ terraform apply
...
module.vpc.aws_subnet.pub["pri-subnet-sample1"]: Modifying... [id=subnet-01629b6f8717a5523]
module.vpc.aws_subnet.pub["pri-subnet-sample2"]: Modifying... [id=subnet-0edf4b9618cf8f38f]
module.vpc.aws_subnet.pub["pri-subnet-sample2"]: Modifications complete after 0s [id=subnet-0edf4b9618cf8f38f]
module.vpc.aws_subnet.pub["pri-subnet-sample1"]: Modifications complete after 0s [id=subnet-01629b6f8717a5523]

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

無事applyできました。

変更後のコード

modules/vpc/variables.tfは変更なし。

modules/vpc/main.tf

resource "aws_vpc" "default" {
  cidr_block  = var.vpc_cidr
  tags        = { Name = var.vpc_name }
}

moved {
  from = aws_vpc.this
  to = aws_vpc.default
}

resource "aws_subnet" "pub" {
  for_each = { for i in var.subnet : i.name => i }
  vpc_id      = aws_vpc.default.id
  cidr_block  = each.value.cidr
  tags        = { Name = each.value.name }
}

moved {
  from = aws_subnet.pub["subnet-sample1"]
  to = aws_subnet.pub["pri-subnet-sample1"]
}
moved {
  from = aws_subnet.pub["subnet-sample2"]
  to = aws_subnet.pub["pri-subnet-sample2"]
}

output "vpc_id" {
  value = aws_vpc.default.id
}

main.tf

module "vpc" {
  source = "../modules/vpc"

  vpc_name = "vpc-sample"
  vpc_cidr = "172.16.0.0/16"
  subnet = [
    {
      name = "pri-subnet-sample1"
      cidr = "172.16.0.0/20"
    },
    {
      name = "pri-subnet-sample2"
      cidr = "172.16.16.0/20"
    }
  ]
}

// moduleを使わない
resource "aws_security_group" "default" {
  name        = "securitygroup-sample1"
  vpc_id      = module.vpc.vpc_id
}

moved {
  from = aws_security_group.this
  to = aws_security_group.default
}

所感

moduleをリファクタリングするにはstateの移動がかなり面倒で、リファクタリングを後回しにする要因になっていました。movedブロックはリファクタリングのトイルを減らす待望の機能追加です。