🍣

Terraformの新機能movedを試してみた

2021/12/23に公開

この記事は??

従来のTerraformではリファクタリングした時やリソースの命名を変更した際は、
terrafrom state mv を実行または直接 tfstate を直接編集する必要があり、
それをバージョン管理外で実施しなければいけないのが課題でした。
(リソースの変更手順はこちらにまとめてあります。)

この記事ではTerraform version1.1系の新機能 moved を使って、既存のインフラの構造を
変更することなくリファクタリングとリソースの命名変更が出来ることを確認します。

この記事の目的

  • moved ブロックの使い方と挙動を理解すること

手順

ソースコードはこちら

resourceを作る

resource "aws_vpc" "example" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "example" {
  vpc_id            = aws_vpc.example.id
  availability_zone = "ap-northeast-1a"
  cidr_block        = "10.0.1.0/24"
}

resource "aws_security_group" "example" {
  name   = "http_sg"
  vpc_id = aws_vpc.example.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

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

moved を試すためにVPC、サブネット、セキュリティグループをresourceを使って作成します。
apply後、次はmoduleを使ってリファクタリングしてみます。

moduleでリファクタリング

// modules/network/main.tf

variable "cidr_block" {}

variable "availability_zone" {}

resource "aws_vpc" "example" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "example" {
  vpc_id            = aws_vpc.example.id
  availability_zone = var.availability_zone
  cidr_block        = var.cidr_block
}

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

// modules/security_group/main.tf

resource "aws_security_group" "example" {
  name   = "http_sg"
  vpc_id = var.vpc_id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

// main.tf

module "modified_network" {
  source = "./modules/network"

  availability_zone = "ap-northeast-1a"
  cidr_block        = "10.0.1.0/24"
}

module "sg" {
  source = "./modules/security_group"

  vpc_id = module.modified_network.vpc_id
}

module作成後、実行計画を確認します。

❯❯❯ terraform plan
aws_vpc.example: Refreshing state... [id=vpc-0c1962cc15d60eef0]
aws_subnet.example: Refreshing state... [id=subnet-018f681f7549f1134]
aws_security_group.example: Refreshing state... [id=sg-07f7564a362f56901]

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.example will be destroyed
  # (because aws_security_group.example is not in configuration)
  - resource "aws_security_group" "example" {
      - arn                    = "arn:aws:ec2:ap-northeast-1:762742784206:security-group/sg-07f7564a362f56901" -> null
      - description            = "Managed by Terraform" -> null
      - egress                 = [
          - {
              - cidr_blocks      = [
                  - "0.0.0.0/0",
                ]
              - description      = ""
              - from_port        = 0
              - ipv6_cidr_blocks = []
              - prefix_list_ids  = []
              - protocol         = "-1"
              - security_groups  = []
              - self             = false
              - to_port          = 0
            },
        ] -> null
      - id                     = "sg-07f7564a362f56901" -> null
      - ingress                = [
          - {
              - cidr_blocks      = [
                  - "0.0.0.0/0",
                ]
              - description      = ""
              - from_port        = 80
              - ipv6_cidr_blocks = []
              - prefix_list_ids  = []
              - protocol         = "tcp"
              - security_groups  = []
              - self             = false
              - to_port          = 80
            },
        ] -> null
      - name                   = "http_sg" -> null
      - owner_id               = "762742784206" -> null
      - revoke_rules_on_delete = false -> null
      - tags                   = {} -> null
      - tags_all               = {} -> null
      - vpc_id                 = "vpc-0c1962cc15d60eef0" -> null
    }

  # aws_subnet.example will be destroyed
  # (because aws_subnet.example is not in configuration)
  - resource "aws_subnet" "example" {
      - arn                             = "arn:aws:ec2:ap-northeast-1:762742784206:subnet/subnet-018f681f7549f1134" -> null
      - assign_ipv6_address_on_creation = false -> null
      - availability_zone               = "ap-northeast-1a" -> null
      - availability_zone_id            = "apne1-az4" -> null
      - cidr_block                      = "10.0.1.0/24" -> null
      - id                              = "subnet-018f681f7549f1134" -> null
      - map_customer_owned_ip_on_launch = false -> null
      - map_public_ip_on_launch         = false -> null
      - owner_id                        = "762742784206" -> null
      - tags                            = {} -> null
      - tags_all                        = {} -> null
      - vpc_id                          = "vpc-0c1962cc15d60eef0" -> null
    }

  # aws_vpc.example will be destroyed
  # (because aws_vpc.example is not in configuration)
  - resource "aws_vpc" "example" {
      - arn                              = "arn:aws:ec2:ap-northeast-1:762742784206:vpc/vpc-0c1962cc15d60eef0" -> null
      - assign_generated_ipv6_cidr_block = false -> null
      - cidr_block                       = "10.0.0.0/16" -> null
      - default_network_acl_id           = "acl-02735963094a4b45c" -> null
      - default_route_table_id           = "rtb-0fda05ae0d556db67" -> null
      - default_security_group_id        = "sg-08cd847daea28946d" -> null
      - dhcp_options_id                  = "dopt-3bb6fa5c" -> null
      - enable_classiclink               = false -> null
      - enable_classiclink_dns_support   = false -> null
      - enable_dns_hostnames             = false -> null
      - enable_dns_support               = true -> null
      - id                               = "vpc-0c1962cc15d60eef0" -> null
      - instance_tenancy                 = "default" -> null
      - main_route_table_id              = "rtb-0fda05ae0d556db67" -> null
      - owner_id                         = "762742784206" -> null
      - tags                             = {} -> null
      - tags_all                         = {} -> null
    }

  # module.http_sg.aws_security_group.example will be created
  + resource "aws_security_group" "example" {
      + arn                    = (known after apply)
      + description            = "Managed by Terraform"
      + egress                 = [
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + description      = ""
              + from_port        = 0
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "-1"
              + security_groups  = []
              + self             = false
              + to_port          = 0
            },
        ]
      + id                     = (known after apply)
      + ingress                = [
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + description      = ""
              + from_port        = 80
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "tcp"
              + security_groups  = []
              + self             = false
              + to_port          = 80
            },
        ]
      + name                   = "http_sg"
      + name_prefix            = (known after apply)
      + owner_id               = (known after apply)
      + revoke_rules_on_delete = false
      + tags_all               = (known after apply)
      + vpc_id                 = (known after apply)
    }

  # module.network.aws_subnet.example will be created
  + resource "aws_subnet" "example" {
      + arn                             = (known after apply)
      + assign_ipv6_address_on_creation = false
      + availability_zone               = "ap-northeast-1a"
      + availability_zone_id            = (known after apply)
      + cidr_block                      = "10.0.1.0/24"
      + id                              = (known after apply)
      + ipv6_cidr_block_association_id  = (known after apply)
      + map_public_ip_on_launch         = false
      + owner_id                        = (known after apply)
      + tags_all                        = (known after apply)
      + vpc_id                          = (known after apply)
    }

  # module.network.aws_vpc.example will be created
  + resource "aws_vpc" "example" {
      + arn                            = (known after apply)
      + cidr_block                     = "10.0.0.0/16"
      + default_network_acl_id         = (known after apply)
      + default_route_table_id         = (known after apply)
      + default_security_group_id      = (known after apply)
      + dhcp_options_id                = (known after apply)
      + enable_classiclink             = (known after apply)
      + enable_classiclink_dns_support = (known after apply)
      + enable_dns_hostnames           = (known after apply)
      + enable_dns_support             = true
      + id                             = (known after apply)
      + instance_tenancy               = "default"
      + ipv6_association_id            = (known after apply)
      + ipv6_cidr_block                = (known after apply)
      + main_route_table_id            = (known after apply)
      + owner_id                       = (known after apply)
      + tags_all                       = (known after apply)
    }

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

Changes to Outputs:
  - vpc_id = "vpc-0c1962cc15d60eef0" -> null

すると、リソースは削除および再作成されることがわかります。

movedを使う

既存のインフラの構造に変更を加えることなく、リファクタリングするために今までは、
terraform state mv を使うか tfstate を直接変更していましたが、今後は
moved を使って次の様に宣言的に記述することが出来ます。

// main.tf

moved {
  from = aws_vpc.example
  to   = module.network.aws_vpc.example
}

moved {
  from = aws_subnet.example
  to   = module.network.aws_subnet.example
}

moved {
  from = aws_security_group.example
  to   = module.sg.aws_security_group.example
}

実行計画を確認してみます。

❯❯❯ terraform plan
module.network.aws_vpc.example: Refreshing state... [id=vpc-0c1962cc15d60eef0]
module.network.aws_subnet.example: Refreshing state... [id=subnet-018f681f7549f1134]
module.sg.aws_security_group.example: Refreshing state... [id=sg-07f7564a362f56901]

Terraform will perform the following actions:

  # aws_subnet.example has moved to module.network.aws_subnet.example
    resource "aws_subnet" "example" {
        id                              = "subnet-018f681f7549f1134"
        tags                            = {}
        # (10 unchanged attributes hidden)
    }

  # aws_vpc.example has moved to module.network.aws_vpc.example
    resource "aws_vpc" "example" {
        id                               = "vpc-0c1962cc15d60eef0"
        tags                             = {}
        # (15 unchanged attributes hidden)
    }

  # aws_security_group.example has moved to module.sg.aws_security_group.example
    resource "aws_security_group" "example" {
        id                     = "sg-07f7564a362f56901"
        name                   = "http_sg"
        tags                   = {}
        # (8 unchanged attributes hidden)
    }

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

Changes to Outputs:
  - vpc_id = "vpc-0c1962cc15d60eef0" -> null

You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.

既存のインフラの構造に変更が発生しないことが確認出来ました!

movedでリソースの命名を変更する

さらに moved ブロックはリソースの命名の変更にも対応しています。

// main.tf

module "modified_network" {
  source = "./modules/network"

  availability_zone = "ap-northeast-1a"
  cidr_block        = "10.0.1.0/24"
}

module "sg" {
  source = "./modules/security_group"

  vpc_id = module.modified_network.vpc_id
}

moved {
  from = module.network
  to   = module.modified_network
}

命名の変更はこの様に記述することが出来ます。

さいごに

terraform state mvtfstate を手動で変更するよりも直感的で分かりやすいですね。
今後リファクタリングする際は moved ブロックで構成を変更 -> PRを送ってレビューしてもらう
-> 変更を適用 -> moved ブロックは不要なので削除という流れで進めるのが良さそうです。

GitHubで編集を提案

Discussion