[Terraform リファクタリング]リソース内で定義していた設定を別リソースに移す

2024.05.07

Terraformを使っていると、実リソースへの影響なくリソース内で定義した設定を別リソースに移したくなることが有ると思います。

例: リソースaws_instance内でebs_block_deviceを定義していたが、EBSの定義はリソースaws_ebs_volumeに移したい等

以下の手順で実現可能です。

  1. リファクタリングする
  2. 新規に記述を追加したリソースをインポートする

本ブログでは、サンプルコードを交えつつ上記の作業をやってみます。

やってみた

Provider Updateの対応手順ですが、公式ドキュメントには以下の手順がありました。

Terraform AWS Provider Version 4 Upgrade Guide | Guides | hashicorp/aws | Terraform | Terraform Registry

リソースaws_s3_bucket内で定義したaclをリソースaws_s3_bucket_aclにリファクタリングするという内容です。

S3をリファクタリングしたい場合は、上記を参考にしてください。

全く同じでは面白くないので、今回はaws_instance内のebs_block_deviceをリソースaws_ebs_volumeに移す作業を例にやっていきます。

修正前のコード

EC2インスタンスを定義しています。

main.tf

resource "aws_instance" "this" {
  ami           = data.aws_ami.this.id
  instance_type = "t4g.nano"
  key_name      = "sato-test"
  root_block_device {
    volume_type = "gp3"
    volume_size = "10"
  }
  ebs_block_device {
    device_name = "/dev/sdf"
    volume_type = "gp3"
    volume_size = "20"
  }
  tags = {
    Name = "refactor-test"
  }
}

ハイライト箇所を置き換えます。

コードを修正する

ebs_block_deviceの記述を削除して、リソースaws_ebs_volumeaws_volume_attachmentを追加しました。

main.tf

resource "aws_instance" "this" {
  ami               = data.aws_ami.this.id
  availability_zone = "ap-northeast-1a"
  instance_type     = "t4g.nano"
  key_name          = "sato-test"
  root_block_device {
    volume_type = "gp3"
    volume_size = "10"
  }
-  ebs_block_device {
-    device_name = "/dev/sdf"
-    volume_type = "gp3"
-    volume_size = "20"
-  }
  tags = {
    Name = "refactor-test"
  }
}

+resource "aws_ebs_volume" "this" {
+  availability_zone = "ap-northeast-1a"
+  size              = "20"
+  type              = "gp3"
+}

+resource "aws_volume_attachment" "this" {
+  device_name = "/dev/sdf"
+  volume_id   = aws_ebs_volume.this.id
+  instance_id = aws_instance.this.id
+}

planを実行してみます。

$ terraform plan                                      
data.aws_ami.this: Reading...
data.aws_ami.this: Read complete after 0s [id=ami-055b7614d7fc0f105]
aws_instance.this: Refreshing state... [id=i-05aaeb83023a23766]

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

Terraform will perform the following actions:

  # aws_ebs_volume.this will be created
  + resource "aws_ebs_volume" "this" {
      + arn               = (known after apply)
      + availability_zone = "ap-northeast-1a"
      + encrypted         = (known after apply)
      + final_snapshot    = false
      + id                = (known after apply)
      + iops              = (known after apply)
      + kms_key_id        = (known after apply)
      + size              = 20
      + snapshot_id       = (known after apply)
      + tags_all          = (known after apply)
      + throughput        = (known after apply)
      + type              = "gp3"
    }

  # aws_volume_attachment.this will be created
  + resource "aws_volume_attachment" "this" {
      + device_name = "/dev/sdf"
      + id          = (known after apply)
      + instance_id = "i-05aaeb83023a23766"
      + volume_id   = (known after apply)
    }

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

追加したリソースaws_ebs_volumeaws_volume_attachmentが差分として表示されます。

Stateファイルを見てみると、resource aws_instance内でEBSボリュームの定義があります。

terraform.tfstate

    {
      "mode": "managed",
      "type": "aws_instance",
      "name": "this",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 1,
          "attributes": {
            # 省略
            "ebs_block_device": [
              {
                "delete_on_termination": true,
                "device_name": "/dev/sdf",
                "encrypted": false,
                "iops": 3000,
                "kms_key_id": "",
                "snapshot_id": "",
                "tags": {},
                "tags_all": {},
                "throughput": 125,
                "volume_id": "vol-XXXXXX",
                "volume_size": 20,
                "volume_type": "gp3"
              }
            ],

今回の変更内容は、resourceaws_ebs_volumeaws_volume_attachmentを追加します。

Terraformは追加したリソースが既存のリソースであることを知らないため、差分が表示されます。

この状態でapplyすると、EBSボリュームを新規に追加してアタッチしようします。

しかし、/dev/sdfには既存のEBSボリュームがアタッチされているので、エラーになります。

エラーや実リソースへの影響なくリファクタリングするには、追加した記述が既存リソースのものであることを、Terraformに教える必要があります。

Terraformはtfstateで状態を管理するため、tfstateに記述を追加します。

importを行うことで、既存リソースをtfstateに取り込むことができます。

新規に記述を追加したリソースをインポートする

インポート対象は、先程のPlanで差分が出たaws_ebs_volumeaws_volume_attachmentです。

インポートブロック又は、インポートコマンドでインポートを行います。(どちらか1つでOK)

インポートコマンド

$ terraform import aws_ebs_volume.this vol-XXXXXX # ボリュームID
$ terraform import aws_volume_attachment.this /dev/sdf:vol-XXXXXX:i-YYYYYYY # デバイス名:ボリュームID:インスタンスID

インポートブロック

main.tf

import {
  to = aws_ebs_volume.this
  id = "vol-XXXXXX" # ボリュームID
}
import {
  to = aws_volume_attachment.this
  id = "/dev/sdf:vol-XXXXXX:i-YYYYYYY" # デバイス名:ボリュームID:インスタンスID
}

tfstateを確認すると、aws_ebs_volumeaws_volume_attachmentが追加されたことがわかります。

terraform.tfstate

    {
      "mode": "managed",
      "type": "aws_ebs_volume",
      "name": "this",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
# 省略
    {
      "mode": "managed",
      "type": "aws_volume_attachment",
      "name": "this",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
# 省略
    }

Planで差分がでないことを確認

再度Planを実行します。

% terraform plan
aws_ebs_volume.this: Refreshing state... [id=vol-XXXXXX]
data.aws_ami.this: Reading...
data.aws_ami.this: Read complete after 0s [id=ami-055b7614d7fc0f105]
aws_instance.this: Refreshing state... [id=i-YYYYYYY]
aws_volume_attachment.this: Refreshing state... [id=vai-XXXXXX]

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.になっており、差分がでていません。実リソースへの影響なく、リファクタリングができました。

おわりに

リソース内で定義していた設定を別リソースに移す作業についてでした。

破壊的な変更を含むAWS Providerのバージョンアップや、Terraformの記述を統一したいときなどに使える方法です。

リファクタリングの参考になれば幸いです。

以上、AWS事業本部の佐藤(@chari7311)でした。