movedブロックを使ってリファクタリングしてみた

2022.04.28

こんにちは!AWS事業本部コンサルティング部のたかくにです。

今回は、movedブロックを使用してリソースのリファクタリングをやってみようと思います。

movedブロックとは

Terraform バージョン 1.1で現れたリファクタリングを補助してくれるブロックです。

使い方イメージとしては、「もしもリソースAがリソースBという名前だったら...」のような雰囲気で、リソースのリファクタリングを補助してくれるブロックとなっています。

今までどうやっていたの?

今までのリソースのリファクタリングでは、terraform stateコマンドを駆使して、リファクタリングを行っていました。

例えば以下のtfファイルがあり、リファクタリングを+/-に従い行いたかったとします。

main.tf

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~> 4.11.0"
    }
  }
  required_version = ">= 1.1.0"
}

provider "aws" {
  region = "ap-northeast-1"
}

+ resource "aws_vpc" "default" {
- resource "aws_vpc" "example" {
    cidr_block       = "10.0.0.0/16"
    instance_tenancy = "default"
    enable_dns_hostnames = true
    enable_dns_support = true
}

resource "aws_subnet" "default" {
  vpc_id     = aws_vpc.example.id
  cidr_block = cidrsubnet(aws_vpc.example.cidr_block, 8, 0)
}

aws_vpc.exampleからaws_vpc.defaultに変更する際は、以下の手順でリファクタリングを行います。

  1. コードの書き換え
  2. terraform state mvの実行
  3. terraform planで差分確認

コードの書き換え

aws_vpcのローカル名の変更及び、aws_subnet.defaultのVPC参照先を変更していきます。

main.tf

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~> 4.11.0"
    }
  }
  required_version = ">= 1.1.0"
}

provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_vpc" "default" {
# resource "aws_vpc" "example" {
  cidr_block       = "10.0.0.0/16"
  instance_tenancy = "default"
  enable_dns_hostnames = true
  enable_dns_support = true
}

resource "aws_subnet" "default" {
  # vpc_id     = aws_vpc.example.id
  vpc_id     = aws_vpc.default.id
  # cidr_block = cidrsubnet(aws_vpc.example.cidr_block, 8, 0)
  cidr_block = cidrsubnet(aws_vpc.default.cidr_block, 8, 0)
}

terraform state mvの実行

aws_vpcのローカル名を変更するため、terraform state mvを実行してきます。

takakuni@moved_blocks % terraform state mv aws_vpc.example aws_vpc.default
Move "aws_vpc.example" to "aws_vpc.default"
Successfully moved 1 object(s).

terraform planで差分確認

ローカル名の変更前後で差分が出ていないかterraform planで確認します。

takakuni@moved_blocks % terraform plan
aws_vpc.default: Refreshing state... [id=vpc-XXXXXXXXXXXXXXXXX]
aws_subnet.default: Refreshing state... [id=subnet-XXXXXXXXXXXXXXXXX]

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.

movedブロック使ってみた

movedブロックを使用して、aws_vpc.defaultからaws_vpc.exampleに戻してみようと思います。

movedブロックでは、以下の手順でリファクタリングを行います。

  1. コードの書き換え
  2. terraform planで差分確認
  3. terraform applyでリファクタリング
  4. terraform planで差分確認

コードの書き換え

aws_vpcのローカル名の変更及び、aws_subnet.defaultのVPC参照先を再度変更していきます。

main.tf

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~> 4.11.0"
    }
  }
  required_version = ">= 1.1.0"
}

provider "aws" {
  region = "ap-northeast-1"
}

# resource "aws_vpc" "default" {
resource "aws_vpc" "example" {
  cidr_block       = "10.0.0.0/16"
  instance_tenancy = "default"
  enable_dns_hostnames = true
  enable_dns_support = true
}

resource "aws_subnet" "default" {
  vpc_id     = aws_vpc.example.id
  # vpc_id     = aws_vpc.default.id
  cidr_block = cidrsubnet(aws_vpc.example.cidr_block, 8, 0)
  # cidr_block = cidrsubnet(aws_vpc.default.cidr_block, 8, 0)
}

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

terraform planで差分確認

terraform planで差分を確認します。

movedブロックを使用した場合、aws_vpc.defaultに対してリネームが行われることを検知してくれます。

takakuni@moved_blocks % aws-vault exec takakuni -- terraform plan 
aws_vpc.example: Refreshing state... [id=vpc-XXXXXXXXXXXXXXXXX]
aws_subnet.default: Refreshing state... [id=subnet-XXXXXXXXXXXXXXXXX]

Terraform will perform the following actions:

  # aws_vpc.default has moved to aws_vpc.example
    resource "aws_vpc" "example" {
        id                               = "vpc-XXXXXXXXXXXXXXXXX"
        tags                             = {}
        # (16 unchanged attributes hidden)
    }

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

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

terraform applyでリファクタリング

terraform planの状態では、実際のリネームは行われていないためterraform applyで実行していきます。

takakuni@moved_blocks % terraform apply
aws_vpc.example: Refreshing state... [id=vpc-XXXXXXXXXXXXXXXXX]
aws_subnet.default: Refreshing state... [id=subnet-XXXXXXXXXXXXXXXXX]

Terraform will perform the following actions:

  # aws_vpc.default has moved to aws_vpc.example
    resource "aws_vpc" "example" {
        id                               = "vpc-XXXXXXXXXXXXXXXXX"
        tags                             = {}
        # (16 unchanged attributes hidden)
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes


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

terraform planで差分確認

terraform planで再度、リファクタリングが影響で差分が発生していないことを確認します。

takakuni@moved_blocks % terraform plan 
aws_vpc.example: Refreshing state... [id=vpc-XXXXXXXXXXXXXXXXX]
aws_subnet.default: Refreshing state... [id=subnet-XXXXXXXXXXXXXXXXX]

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.

terraform state mvよりも優れている点

弊社、かずえさんのお言葉をお借りすると以下のメリットがmovedブロックに存在します。

例えば自分が公開モジュールを作っていて、自分以外の人がそのモジュールを利用しているような状況だったらどうでしょうか?リファクタリングした後に利用者全員に「terraform state mvを使ってね」と伝えるのは無理があります。他のケースだと、Terraformの実行は基本的にCI/CDパイプラインで自動化していて、terraform state mvコマンドを気軽に実行できないとか、シングルテナント構成で大量に同じ構成のStateがあるのですべてでterraform state mvコマンドを打つのは煩雑だ、みたいなケースが考えられます。

引用元

加えて、私は「tfstateファイルに対しての変更は、terraform apply時である」ことが優れていると感じました。

tfstateファイルの変更がapply後であること

terraform state mvコマンド実行した場合に気になることは、コマンド実行時にtfstateファイルが書き変わってしまうことです。

そのため、リファクタリング前に影響範囲(参照先)を入念に調べ、コマンド実行後にリファクタリングの完遂または、エラー時は戻し作業の完遂が求められます。

個人的には、かなり緊張感のある作業のため、なるべくやりたくない作業の1つです。

しかし、movedブロックを使うことで、tfstateファイルの更新をかける前に、十分な検証が行えるためより安全なリファクタリングが見込めます。

terraform state mvよりも気をつけたいポイント

movedブロックにも気をつけたいポイントは以下の通りです。

  • movedブロックは削除非推奨
  • リファクタするリソースの近くで定義しよう

movedブロックは削除非推奨

movedブロックは、tfstateファイルに保存されないブロックです。そのため、削除してもterraform plan/applyで変更検知ができないブロックであることに注意してください。

movedブロックを削除すると、古いアドレスを参照している部分が、リネームではなく既存のオブジェクトの削除を計画するため破壊的な変更になります。

リファクタするリソースの近くで定義しよう

実は、以下のように複数movedを定義することも可能です。

参照可能な値は最終地点の、aws_vpc.example_vpcのみとなります。

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~> 4.11.0"
    }
  }
  required_version = ">= 1.1.0"
}

provider "aws" {
  region = "ap-northeast-1"
}

# resource "aws_vpc" "default" {
# resource "aws_vpc" "example" {
resource "aws_vpc" "example_vpc" {
  cidr_block       = "10.0.0.0/16"
  instance_tenancy = "default"
  enable_dns_hostnames = true
  enable_dns_support = true
}

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

moved {
  from = aws_vpc.example
  to = aws_vpc.example_vpc
}

resource "aws_subnet" "default" {
  # vpc_id     = aws_vpc.default.id
  # vpc_id     = aws_vpc.example.id
  vpc_id     = aws_vpc.example_vpc.id
  # cidr_block = cidrsubnet(aws_vpc.default.cidr_block, 8, 0)
  # cidr_block = cidrsubnet(aws_vpc.example.cidr_block, 8, 0)
  cidr_block = cidrsubnet(aws_vpc.example_vpc.cidr_block, 8, 0)
}

リソース名の変更履歴を追跡しやすいよう、リファクタするリソースの近くで定義するのがオススメです。

参考

まとめ

以上、「movedブロックを使用してリファクタリングをやってみた。」でした。

terraform state mvコマンドよりも個人的には使い勝手が良くリファクタリングに貢献できる機能だと思いました。

以上、AWS事業本部コンサルティング部のたかくにでした!