Terraform 1.7 の新機能: AWS CLI でリストしたバケットを Import ブロックと for_each で一括インポートする

## はじめに

先日公開された Terraform 1.7 で、import ブロックが for_each をサポートしました。

https://www.hashicorp.com/blog/terraform-1-7-adds-test-mocking-and-config-driven-remove

> Terraform 1.7 also includes an enhancement for config-driven import: the ability to expand import blocks using for_each loops.

import ブロックは従来からサポートされていましたが、for_each をサポートしていなかったため、個々のリソースごとに import ブロックを記述する必要がありました。

この記事では、import ブロックを利用し、複数のリソースを一括でインポートする方法を解説します。Terraform 管理外のリソースを Terraform に取り込み、リソースの変更、破棄までのライフサイクルを検証します。

for_each のサポートにより、1 つの import ブロックで複数のリソースの import が可能になります。また、対象のリソースをコマンドラインで指定することにより、任意のリソースを import できることを検証します。

検証に使ったコードは、こちらにあります。

https://github.com/JHashimoto0518/terraform-exercise/tree/main/bulk-import-with-block

## Terraform 管理外のバケットを用意する

まず、Terraform 管理外のバケットを 2 つ用意します。

- tf-bulk-import-test-1
- tf-bulk-import-test-2

構成の異なるバケットで検証するため、tf-bulk-import-test-2 のみ、バージョニングを有効化しておきます。

まず、AWS CLI に渡す credential を設定します。今回は環境変数を利用します。Terraform もこの環境変数から credential を取得します。

```bash
export AWS_PROFILE="<replace your profile>"
```

tf-bulk-import-test-1 のバージョニングを停止します。

```bash
bucket_name=tf-bulk-import-test-1
aws s3api put-bucket-versioning --bucket ${bucket_name} --versioning-configuration Status=Suspended && aws s3api get-bucket-versioning --bucket ${bucket_name}
# {
#     "Status": "Suspended"
# }
```

tf-bulk-import-test-2 のバージョニングを有効にします。

```bash
bucket_name=tf-bulk-import-test-2
aws s3api put-bucket-versioning --bucket ${bucket_name} --versioning-configuration Status=Enabled && aws s3api get-bucket-versioning --bucket ${bucket_name}
# {
#     "Status": "Enabled"
# }
```

## バケットをインポートする

main.tf を記述します。今回は、簡易的にローカルで state を管理することにします。

for_each で import ブロックを展開します。また、バケット名を渡すための list 型の variable も宣言します。

list から for_each に渡す map を生成する方法については、@minamijoyo さんの記事を参照してください。

https://qiita.com/minamijoyo/items/3785cad0283e4eb5a188

```hcl:main.tf
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }

  backend "local" {
    path = "terraform.tfstate"
  }
}

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

variable "buckets" {
  type        = list(string)
  description = "buckets to import"
}

import {
  for_each = { for b in var.buckets : b => b }
  to       = aws_s3_bucket.this[each.key]
  id       = each.value
}

resource "aws_s3_bucket" "this" {
  for_each = { for b in var.buckets : b => b }
  bucket   = each.value
}
```

初期化します。

```bash
terraform init
```

インポートするバケット名のリストを変数に格納します。

今回は、バケット名でフィルタリングします。具体的には、プリフィックスが `tf-bulk-import-test`のバケットをインポートの対象とします。

```bash
buckets=$(aws s3api list-buckets --query "Buckets[?starts_with(Name, 'tf-bulk-import-test')].Name" --output json | jq -c .)
echo ${buckets} 
# ["tf-bulk-import-test-1","tf-bulk-import-test-2"]
```

plan を確認します。期待通り 2 つのバケットを import する plan が出力されました。

```bash
terraform plan -var="buckets=${buckets}"
# ...
# Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.
# ...
```

<details><summary>all output:</summary>

```bash
aws_s3_bucket.this["tf-bulk-import-test-2"]: Preparing import... [id=tf-bulk-import-test-2]
aws_s3_bucket.this["tf-bulk-import-test-1"]: Preparing import... [id=tf-bulk-import-test-1]
aws_s3_bucket.this["tf-bulk-import-test-1"]: Refreshing state... [id=tf-bulk-import-test-1]
aws_s3_bucket.this["tf-bulk-import-test-2"]: Refreshing state... [id=tf-bulk-import-test-2]

Terraform will perform the following actions:

  # aws_s3_bucket.this["tf-bulk-import-test-1"] will be imported
    resource "aws_s3_bucket" "this" {
        arn                         = "arn:aws:s3:::tf-bulk-import-test-1"
        bucket                      = "tf-bulk-import-test-1"
        bucket_domain_name          = "tf-bulk-import-test-1.s3.amazonaws.com"
        bucket_regional_domain_name = "tf-bulk-import-test-1.s3.ap-northeast-1.amazonaws.com"
        hosted_zone_id              = "Z2M4EHUR26P7ZW"
        id                          = "tf-bulk-import-test-1"
        object_lock_enabled         = false
        region                      = "ap-northeast-1"
        request_payer               = "BucketOwner"
        tags                        = {}
        tags_all                    = {}

        grant {
            id          = "d03a..."
            permissions = [
                "FULL_CONTROL",
            ]
            type        = "CanonicalUser"
        }

        server_side_encryption_configuration {
            rule {
                bucket_key_enabled = true

                apply_server_side_encryption_by_default {
                    sse_algorithm = "AES256"
                }
            }
        }

        versioning {
            enabled    = false
            mfa_delete = false
        }
    }

  # aws_s3_bucket.this["tf-bulk-import-test-2"] will be imported
    resource "aws_s3_bucket" "this" {
        arn                         = "arn:aws:s3:::tf-bulk-import-test-2"
        bucket                      = "tf-bulk-import-test-2"
        bucket_domain_name          = "tf-bulk-import-test-2.s3.amazonaws.com"
        bucket_regional_domain_name = "tf-bulk-import-test-2.s3.ap-northeast-1.amazonaws.com"
        hosted_zone_id              = "Z2M4EHUR26P7ZW"
        id                          = "tf-bulk-import-test-2"
        object_lock_enabled         = false
        region                      = "ap-northeast-1"
        request_payer               = "BucketOwner"
        tags                        = {}
        tags_all                    = {}

        grant {
            id          = "d03a..."
            permissions = [
                "FULL_CONTROL",
            ]
            type        = "CanonicalUser"
        }

        server_side_encryption_configuration {
            rule {
                bucket_key_enabled = true

                apply_server_side_encryption_by_default {
                    sse_algorithm = "AES256"
                }
            }
        }

        versioning {
            enabled    = true
            mfa_delete = false
        }
    }

Plan: 2 to import, 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.
```

</details>

apply で import を実行します。

```bash
terraform apply -var="buckets=${buckets}"
# ...
# Apply complete! Resources: 2 imported, 0 added, 0 changed, 0 destroyed.
```

<details><summary>all output:</summary>

```bash
aws_s3_bucket.this["tf-bulk-import-test-1"]: Preparing import... [id=tf-bulk-import-test-1]
aws_s3_bucket.this["tf-bulk-import-test-2"]: Preparing import... [id=tf-bulk-import-test-2]
aws_s3_bucket.this["tf-bulk-import-test-1"]: Refreshing state... [id=tf-bulk-import-test-1]
aws_s3_bucket.this["tf-bulk-import-test-2"]: Refreshing state... [id=tf-bulk-import-test-2]

Terraform will perform the following actions:

  # aws_s3_bucket.this["tf-bulk-import-test-1"] will be imported
    resource "aws_s3_bucket" "this" {
        arn                         = "arn:aws:s3:::tf-bulk-import-test-1"
        bucket                      = "tf-bulk-import-test-1"
        bucket_domain_name          = "tf-bulk-import-test-1.s3.amazonaws.com"
        bucket_regional_domain_name = "tf-bulk-import-test-1.s3.ap-northeast-1.amazonaws.com"
        hosted_zone_id              = "Z2M4EHUR26P7ZW"
        id                          = "tf-bulk-import-test-1"
        object_lock_enabled         = false
        region                      = "ap-northeast-1"
        request_payer               = "BucketOwner"
        tags                        = {}
        tags_all                    = {}

        grant {
            id          = "d03a..."
            permissions = [
                "FULL_CONTROL",
            ]
            type        = "CanonicalUser"
        }

        server_side_encryption_configuration {
            rule {
                bucket_key_enabled = true

                apply_server_side_encryption_by_default {
                    sse_algorithm = "AES256"
                }
            }
        }

        versioning {
            enabled    = false
            mfa_delete = false
        }
    }

  # aws_s3_bucket.this["tf-bulk-import-test-2"] will be imported
    resource "aws_s3_bucket" "this" {
        arn                         = "arn:aws:s3:::tf-bulk-import-test-2"
        bucket                      = "tf-bulk-import-test-2"
        bucket_domain_name          = "tf-bulk-import-test-2.s3.amazonaws.com"
        bucket_regional_domain_name = "tf-bulk-import-test-2.s3.ap-northeast-1.amazonaws.com"
        hosted_zone_id              = "Z2M4EHUR26P7ZW"
        id                          = "tf-bulk-import-test-2"
        object_lock_enabled         = false
        region                      = "ap-northeast-1"
        request_payer               = "BucketOwner"
        tags                        = {}
        tags_all                    = {}

        grant {
            id          = "d03a..."
            permissions = [
                "FULL_CONTROL",
            ]
            type        = "CanonicalUser"
        }

        server_side_encryption_configuration {
            rule {
                bucket_key_enabled = true

                apply_server_side_encryption_by_default {
                    sse_algorithm = "AES256"
                }
            }
        }

        versioning {
            enabled    = true
            mfa_delete = false
        }
    }

Plan: 2 to import, 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

aws_s3_bucket.this["tf-bulk-import-test-1"]: Importing... [id=tf-bulk-import-test-1]
aws_s3_bucket.this["tf-bulk-import-test-1"]: Import complete [id=tf-bulk-import-test-1]
aws_s3_bucket.this["tf-bulk-import-test-2"]: Importing... [id=tf-bulk-import-test-2]
aws_s3_bucket.this["tf-bulk-import-test-2"]: Import complete [id=tf-bulk-import-test-2]

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

</details>

念のため、再度 plan を出力し、main.tf と実際のバケットとの差分がないことを確認します。

```bash
terraform plan -var="buckets=${buckets}"
# aws_s3_bucket.this["tf-bulk-import-test-2"]: Refreshing state... [id=tf-bulk-import-test-2]
# aws_s3_bucket.this["tf-bulk-import-test-1"]: Refreshing state... [id=tf-bulk-import-test-1]
# 
# 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 ファイルを参照すると、tf-bulk-import-test-2 のみ versioning が有効になっており、バケット間の構成差異も期待通りに import されていることが確認できます。

```json:terraform.tfstate
{
  ...
  "resources": [
    {
      ...
      "instances": [
        {
          "index_key": "tf-bulk-import-test-1",
          "schema_version": 0,
          "attributes": {
            ...
            "versioning": [
              {
                "enabled": false,
                "mfa_delete": false
              }
            ],
            ...
          },
          ...
        },
        {
          "index_key": "tf-bulk-import-test-2",
          "schema_version": 0,
          "attributes": {
            ...
            "versioning": [
              {
                "enabled": true,
                "mfa_delete": false
              }
            ],
            ...
          },
          ...
        }
      ]
    }
  ],
  ...
}
```

## インポートブロックの削除

不要になった import ブロックを削除します。

```diff_hcl:main.tf
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }

  backend "local" {
    path = "terraform.tfstate"
  }
}

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

variable "buckets" {
  type        = list(string)
  description = "buckets to import"
}

- import {
-   for_each = { for b in var.buckets : b => b }
-   to       = aws_s3_bucket.this[each.key]
-   id       = each.value
- }

resource "aws_s3_bucket" "this" {
  for_each = { for b in var.buckets : b => b }
  bucket   = each.value
}
```

```bash
terraform plan -var="buckets=${buckets}"
# aws_s3_bucket.this["tf-bulk-import-test-2"]: Refreshing state... [id=tf-bulk-import-test-2]
# aws_s3_bucket.this["tf-bulk-import-test-1"]: Refreshing state... [id=tf-bulk-import-test-1]
# 
# 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 します。

```bash
terraform apply -var="buckets=${buckets}"
# aws_s3_bucket.this["tf-bulk-import-test-1"]: Refreshing state... [id=tf-bulk-import-test-1]
# aws_s3_bucket.this["tf-bulk-import-test-2"]: Refreshing state... [id=tf-bulk-import-test-2]
# 
# 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.
```

## リファクタリング

インポートにより、バケットを Terraform 管理に移行できたので、構成の変更を試してみます。tf-bulk-import-test-2 のみ変更します。

このままでは、個別に構成を変更できないので、リファクタリングします。具体的には、for_each をやめて、resource ブロックを個別に記述するように変更します。ついでに、不要になった variable を削除します。

https://developer.hashicorp.com/terraform/language/modules/develop/refactoring

```diff_hcl:main.tf
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }

  backend "local" {
    path = "terraform.tfstate"
  }
}

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

- variable "buckets" {
-   type        = list(string)
-   description = "buckets to import"
- }
- 
- resource "aws_s3_bucket" "this" {
-   for_each = { for b in var.buckets : b => b }
-   bucket   = each.value
- }
+ resource "aws_s3_bucket" "test-1" {
+   bucket = "tf-bulk-import-test-1"
+ }
+ 
+ resource "aws_s3_bucket" "test-2" {
+   bucket = "tf-bulk-import-test-2"
+ }
```

`terraform plan` を実行すると、期待に反して、バケットを destroy し、新たに create する plan が出力されます。

```bash
terraform plan
# ...
# Plan: 2 to add, 0 to change, 2 to destroy.
# ...
```

<details><summary>all output:</summary>

```bash
aws_s3_bucket.this["tf-bulk-import-test-2"]: Refreshing state... [id=tf-bulk-import-test-2]
aws_s3_bucket.this["tf-bulk-import-test-1"]: Refreshing state... [id=tf-bulk-import-test-1]

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_s3_bucket.test-1 will be created
  + resource "aws_s3_bucket" "test-1" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "tf-bulk-import-test-1"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)
    }

  # aws_s3_bucket.test-2 will be created
  + resource "aws_s3_bucket" "test-2" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "tf-bulk-import-test-2"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)
    }

  # aws_s3_bucket.this["tf-bulk-import-test-1"] will be destroyed
  # (because aws_s3_bucket.this is not in configuration)
  - resource "aws_s3_bucket" "this" {
      - arn                         = "arn:aws:s3:::tf-bulk-import-test-1" -> null
      - bucket                      = "tf-bulk-import-test-1" -> null
      - bucket_domain_name          = "tf-bulk-import-test-1.s3.amazonaws.com" -> null
      - bucket_regional_domain_name = "tf-bulk-import-test-1.s3.ap-northeast-1.amazonaws.com" -> null
      - hosted_zone_id              = "Z2M4EHUR26P7ZW" -> null
      - id                          = "tf-bulk-import-test-1" -> null
      - object_lock_enabled         = false -> null
      - region                      = "ap-northeast-1" -> null
      - request_payer               = "BucketOwner" -> null
      - tags                        = {} -> null
      - tags_all                    = {} -> null

      - grant {
          - id          = "d03a..." -> null
          - permissions = [
              - "FULL_CONTROL",
            ] -> null
          - type        = "CanonicalUser" -> null
        }

      - server_side_encryption_configuration {
          - rule {
              - bucket_key_enabled = true -> null

              - apply_server_side_encryption_by_default {
                  - sse_algorithm = "AES256" -> null
                }
            }
        }

      - versioning {
          - enabled    = false -> null
          - mfa_delete = false -> null
        }
    }

  # aws_s3_bucket.this["tf-bulk-import-test-2"] will be destroyed
  # (because aws_s3_bucket.this is not in configuration)
  - resource "aws_s3_bucket" "this" {
      - arn                         = "arn:aws:s3:::tf-bulk-import-test-2" -> null
      - bucket                      = "tf-bulk-import-test-2" -> null
      - bucket_domain_name          = "tf-bulk-import-test-2.s3.amazonaws.com" -> null
      - bucket_regional_domain_name = "tf-bulk-import-test-2.s3.ap-northeast-1.amazonaws.com" -> null
      - hosted_zone_id              = "Z2M4EHUR26P7ZW" -> null
      - id                          = "tf-bulk-import-test-2" -> null
      - object_lock_enabled         = false -> null
      - region                      = "ap-northeast-1" -> null
      - request_payer               = "BucketOwner" -> null
      - tags                        = {} -> null
      - tags_all                    = {} -> null

      - grant {
          - id          = "d03a..." -> null
          - permissions = [
              - "FULL_CONTROL",
            ] -> null
          - type        = "CanonicalUser" -> null
        }

      - server_side_encryption_configuration {
          - rule {
              - bucket_key_enabled = true -> null

              - apply_server_side_encryption_by_default {
                  - sse_algorithm = "AES256" -> null
                }
            }
        }

      - versioning {
          - enabled    = true -> null
          - mfa_delete = false -> null
        }
    }

Plan: 2 to add, 0 to change, 2 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.
```

</details>

Terraform は、state にリソース名を付けて管理しています。既存バケットを destroy し、新しいバケットを create する plan を作成するのは、このリソース名の違いが原因です。

state のリソース名を確認してみます。

```bash
terraform state list --state ./terraform.tfstate
# aws_s3_bucket.this["tf-bulk-import-test-1"]
# aws_s3_bucket.this["tf-bulk-import-test-2"]
```

したがって、main.tf の記述に合わせて、リソース名を次のように変更します。

- aws_s3_bucket.this["tf-bulk-import-test-1"]-> aws_s3_bucket.test-1
- aws_s3_bucket.this["tf-bulk-import-test-2"]-> aws_s3_bucket.test-2

リソース名の変更には、`moved` ブロックを利用します。宣言的に変更できるため、`state mv` コマンドと比べて安全です。

https://www.hashicorp.com/blog/terraform-1-1-improves-refactoring-and-the-cloud-cli-experience#easier-and-safer-refactoring-with-moved-statements

```diff_hcl:main.tf
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }

  backend "local" {
    path = "terraform.tfstate"
  }
}

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

resource "aws_s3_bucket" "test-1" {
  bucket = "tf-bulk-import-test-1"
}

resource "aws_s3_bucket" "test-2" {
  bucket = "tf-bulk-import-test-2"
}

+ moved {
+   from = aws_s3_bucket.this["tf-bulk-import-test-1"]
+   to   = aws_s3_bucket.test-1
+ }
+ 
+ moved {
+   from = aws_s3_bucket.this["tf-bulk-import-test-2"]
+   to   = aws_s3_bucket.test-2
+ }
```

期待通りに move の plan が作成されました。

`terraform plan` の output:

```bash
aws_s3_bucket.test-1: Refreshing state... [id=tf-bulk-import-test-1]
aws_s3_bucket.test-2: Refreshing state... [id=tf-bulk-import-test-2]

Terraform will perform the following actions:

  # aws_s3_bucket.this["tf-bulk-import-test-1"] has moved to aws_s3_bucket.test-1
    resource "aws_s3_bucket" "test-1" {
        id                          = "tf-bulk-import-test-1"
        tags                        = {}
        # (9 unchanged attributes hidden)

        # (3 unchanged blocks hidden)
    }

  # aws_s3_bucket.this["tf-bulk-import-test-2"] has moved to aws_s3_bucket.test-2
    resource "aws_s3_bucket" "test-2" {
        id                          = "tf-bulk-import-test-2"
        tags                        = {}
        # (9 unchanged attributes hidden)

        # (3 unchanged blocks 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.
```

apply で、move を実行します。

move 後に plan を確認します。リネームにより、Terraformは「resourceブロックに記述したバケットが、実際の tf-bulk-import-test-1, 2 と同一である」と認識したことがわかります。

```bash
terraform plan
aws_s3_bucket.test-1: Refreshing state... [id=tf-bulk-import-test-1]
aws_s3_bucket.test-2: Refreshing state... [id=tf-bulk-import-test-2]

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 ブロックを削除します。

```diff_hcl:main.tf
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }

  backend "local" {
    path = "terraform.tfstate"
  }
}

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

resource "aws_s3_bucket" "test-1" {
  bucket = "tf-bulk-import-test-1"
}

resource "aws_s3_bucket" "test-2" {
  bucket = "tf-bulk-import-test-2"
}

- moved {
-   from = aws_s3_bucket.this["tf-bulk-import-test-1"]
-   to   = aws_s3_bucket.test-1
- }
- 
- moved {
-   from = aws_s3_bucket.this["tf-bulk-import-test-2"]
-   to   = aws_s3_bucket.test-2
- }
```

これまでと同様に plan、apply を実行します。

なお、モジュールの場合は、以前のバージョンのユーザーのアップグレードパスを保持するために、moved ブロックを保持することが強く推奨されています。

https://developer.hashicorp.com/terraform/language/modules/develop/refactoring

> Removing a moved block is a generally breaking change because any configurations that refer to the old address will plan to delete that existing object instead of move it. We strongly recommend that you retain all historical moved blocks from earlier versions of your modules to preserve the upgrade path for users of any previous version.
>
> If you do decide to remove moved blocks, proceed with caution. It can be safe to remove moved blocks when you are maintaining private modules within an organization and you are certain that all users have successfully run terraform apply with your new module version.

今回は検証目的のため、削除しても問題はありません。

## バケットの構成を変更する

リファクタリングにより、個別に構成を変更できるようになったので、tf-bulk-import-test-2 を変更します。

tf-bulk-import-test-2 にタグを付けます。

```diff_hcl:main.tf
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }

  backend "local" {
    path = "terraform.tfstate"
  }
}

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

resource "aws_s3_bucket" "test-1" {
  bucket = "tf-bulk-import-test-1"
}

resource "aws_s3_bucket" "test-2" {
  bucket = "tf-bulk-import-test-2"
+   tags = {
+     "versioning" = "true"
+   }
}
```

plan を確認し、apply します。

`terraform plan` の output:

```bash
aws_s3_bucket.test-2: Refreshing state... [id=tf-bulk-import-test-2]
aws_s3_bucket.test-1: Refreshing state... [id=tf-bulk-import-test-1]

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:

  # aws_s3_bucket.test-2 will be updated in-place
  ~ resource "aws_s3_bucket" "test-2" {
      + force_destroy               = false
        id                          = "tf-bulk-import-test-2"
      ~ tags                        = {
          + "versioning" = "true"
        }
      ~ tags_all                    = {
          + "versioning" = "true"
        }
        # (8 unchanged attributes hidden)

        # (3 unchanged blocks hidden)
    }

Plan: 0 to add, 1 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.
```

これで、一括インポートしたリソースでも、リファクタリングにより、個別に構成変更できることを確認できました。

## バケットを削除する

最後に、tf-bulk-import-test-2 を削除します。

```diff_hcl:main.tf
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }

  backend "local" {
    path = "terraform.tfstate"
  }
}

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

resource "aws_s3_bucket" "test-1" {
  bucket = "tf-bulk-import-test-1"
}

- resource "aws_s3_bucket" "test-2" {
-   bucket = "tf-bulk-import-test-2"
-   tags = {
-     "versioning" = "true"
-   }
- }
```

plan を確認し、apply します。

```bash
terraform plan
# ...
# Plan: 0 to add, 0 to change, 1 to destroy.
# ...
```
<details><summary>all output:</summary>

```bash
aws_s3_bucket.test-2: Refreshing state... [id=tf-bulk-import-test-2]
aws_s3_bucket.test-1: Refreshing state... [id=tf-bulk-import-test-1]

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

Terraform will perform the following actions:

  # aws_s3_bucket.test-2 will be destroyed
  # (because aws_s3_bucket.test-2 is not in configuration)
  - resource "aws_s3_bucket" "test-2" {
      - arn                         = "arn:aws:s3:::tf-bulk-import-test-2" -> null
      - bucket                      = "tf-bulk-import-test-2" -> null
      - bucket_domain_name          = "tf-bulk-import-test-2.s3.amazonaws.com" -> null
      - bucket_regional_domain_name = "tf-bulk-import-test-2.s3.ap-northeast-1.amazonaws.com" -> null
      - force_destroy               = false -> null
      - hosted_zone_id              = "Z2M4EHUR26P7ZW" -> null
      - id                          = "tf-bulk-import-test-2" -> null
      - object_lock_enabled         = false -> null
      - region                      = "ap-northeast-1" -> null
      - request_payer               = "BucketOwner" -> null
      - tags                        = {
          - "versioning" = "true"
        } -> null
      - tags_all                    = {
          - "versioning" = "true"
        } -> null

      - grant {
          - id          = "d03a..." -> null
          - permissions = [
              - "FULL_CONTROL",
            ] -> null
          - type        = "CanonicalUser" -> null
        }

      - server_side_encryption_configuration {
          - rule {
              - bucket_key_enabled = true -> null

              - apply_server_side_encryption_by_default {
                  - sse_algorithm = "AES256" -> null
                }
            }
        }

      - versioning {
          - enabled    = true -> null
          - mfa_delete = false -> null
        }
    }

Plan: 0 to add, 0 to change, 1 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.
```

</details>

```bash
terraform apply
# ...
# Apply complete! Resources: 0 added, 0 changed, 1 destroyed.
```

# まとめ

for_each による import ブロックの展開により、既存リソースを一括でインポートできます。また、 variableにより、任意のリソースをインポートできます。 今回は、プリフィックスによりインポートするリソースをリストしましたが、要件に応じて柔軟に変更できます。例えば、タグによるフィルタリングなどが、考えられます。

また、インポート、リファクタリング、構成の変更、リソースの破棄までのライフサイクル全体を宣言的に記述できるため、plan によるプレビューおよび冪等性の担保により、インフラを扱う際の心理的な負担が軽減されることも、大きなメリットです。

Terraformは、最近のアップデートで、宣言的に実行できる機能を拡充しており、コマンドを利用していたユースケースの多くにおいて、安全性が高まってきています。Terraformer などのツールもありますが、Terraform 単体で多くをカバーできれば、既存リソースの IaC 移行において、有力な選択肢になると考えています。