gokigenmaruのブログ

40から始めるクラウドエンジニア

Terraformで作成したリソースをOpenTofuで追加・変更してみる

今の環境はTerraformで作成されていることが多いと思います。
今後、TerraformからOpenTofuに移行するには、やはり既存で利用していたものをOpenTofuで追加・削除・変更が出来ることが必要です。だって再作成もimportもしたくないし…。

ということで、Terraformで作成したリソースをOpenTofuで追加・変更できるか試してみます。

注意点

Terraformは最新が1.7系になります。
OpenTofuはTerraformのV1.5.6のソースをフォークして開発されているとのことなので、Terraform1.5.6以降にTerraformで実装された機能を使っている場合はおそらくそのまま移行は出来ないと思います。
最新を利用していた場合は一度OpenTofuで実行してみてエラーなり想定通りに構築されないなどの個所を修正してから本番運用に載せる必要がありそうです。


Terraformでリソース作成

作成するリソース

作成するリソースは以下の通りとします。
ここで作成するリソースのコードは先日OpenTofuで作成してみた既存のTerraformのコードと同じです。

 - PublicSubnet(2つ)
 - PrivateSubnet(2つ)
 - DBSubnet(2つ)

  • InternetGateway
  • NatGateway(2つ)

 - EIP(2つ) 

  • Route Table

 - Public用
 - Private用(2つ)
 - DB用

  • SecurityGroup
  • VPC Endpoint

 - Dynamo
 - Kinesis
 - S3用

環境作成

まずはTerraformで作成します。

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.38.0...
- Installed hashicorp/aws v5.38.0 (signed by HashiCorp)

Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

$ terraform plan

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:

~~中略~~

Plan: 36 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 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:

~~中略~~

Plan: 36 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

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:

~~中略~~

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

無事作成が完了しました。

一部リソースの削除を実施

追加・修正を行うため、いったん手動で作成したリソースを削除します。
今回はNATGWを削除します。NATGWを削除後、OpenTofuでNATGWの追加を行います。
NATGW追加すると、以下のリソースに作成・変更が入る想定です。

追加:

  • NatGateway(2つ)

 - EIP(2つ) 

変更:

  • Route Table

 - Public用
 - Private用(2つ)
 - DB用

RouteTableはTerraformで構築した際のNATGWのIDが入っています。NATGWを手動で削除後、OpenTofuでNATGWを追加すれば、新しいNATGWのIDが振られるので、RouteTableは新しくOpenTofuで追加したNATGWのIDに変更になるはずです。
ではやってみます。

NATGWを手動で削除する


上記NATGWをコンソールから手動削除します。



まずは1つ



2つ目も削除。


これでTerraformで作成したNATGWが削除されました。
Elastic IPもついでに開放しました。(残すとお金かかるから…)

Terraformで作成したリソースをOpenTofuで追加・削除・変更が出来るか確認

OpenTofuでNATGWの再作成をする

それではNATGWをOpenTofuで作成します。
Terraformのコード実行したところでTerraformのコード、環境そのままにOpenTofuで再実行します。
手順としてはtofu initを実施してからplan/applyと実行します。
initを実施しないとproviderのレジストリがTerraform側を向いてしまうのでエラーとなります。
試しにやってみるとこんな感じ。

$ tofu plan
╷
│ Error: Inconsistent dependency lock file
│ 
│ The following dependency selections recorded in the lock file are inconsistent with the current configuration:
│   - provider registry.opentofu.org/hashicorp/aws: required by this configuration but no version is selected
│ 
│ To update the locked dependency selections to match a changed configuration, run:
│   tofu init -upgrade

$ cat .terraform.lock.hcl 
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.38.0"
  constraints = "~> 5.0"
  hashes = [
~~省略~~

上記の通りlockfileでエラーになります。
lockfileを見ると中のproviderの指定がterraformのレジストリになっています。

ということで、OpenTofuでplanまで実行します。

$ tofu init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.38.0...
- Installed hashicorp/aws v5.38.0 (signed, key ID 0C0AF313E5FD9F80)

Providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://opentofu.org/docs/cli/plugins/signing/

OpenTofu has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

OpenTofu has been successfully initialized!

You may now begin working with OpenTofu. Try running "tofu plan" to see
any changes that are required for your infrastructure. All OpenTofu commands
should now work.

If you ever set or change modules or backend configuration for OpenTofu,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

$ tofu plan

~~中略~~

Note: Objects have changed outside of OpenTofu

OpenTofu detected the following changes made outside of OpenTofu since the last "tofu apply" which may have affected this plan:

  # aws_eip.ngw1a01eip01 has been deleted
  - resource "aws_eip" "ngw1a01eip01" {
      - id                   = "eipalloc-0beb36b7e810a1bef" -> null
        tags                 = {
            "Name"           = "nwngw1a01eip01"
        }
        # (8 unchanged attributes hidden)
    }

  # aws_eip.ngw1c01eip01 has been deleted
  - resource "aws_eip" "ngw1c01eip01" {
      - id                   = "eipalloc-0c6f192958e54a017" -> null
        tags                 = {
            "Name"           = "nwngw1c01eip01"
        }
        # (8 unchanged attributes hidden)
    }

  # aws_nat_gateway.ngw1a01 has been deleted
  - resource "aws_nat_gateway" "ngw1a01" {
      - id                                 = "nat-0806243b682d70571" -> null
      - network_interface_id               = "eni-0673c836d5ee5050c" -> null
      - private_ip                         = "10.128.34.96" -> null
      - public_ip                          = "13.231.73.35" -> null
        tags                               = {
            "Name"           = "nwngw1a01"
        }
        # (7 unchanged attributes hidden)
    }

  # aws_nat_gateway.ngw1c01 has been deleted
  - resource "aws_nat_gateway" "ngw1c01" {
      - id                                 = "nat-0a8b45bf6c738deea" -> null
      - network_interface_id               = "eni-084f2e6e41715ec88" -> null
      - private_ip                         = "10.128.38.73" -> null
      - public_ip                          = "3.115.195.71" -> null
        tags                               = {
            "Name"           = "nwngw1c01"
        }
        # (7 unchanged attributes hidden)
    }


Unless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include
actions to undo or respond to these changes.

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

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

OpenTofu will perform the following actions:

  # aws_eip.ngw1a01eip01 will be created
  + resource "aws_eip" "ngw1a01eip01" {
      + allocation_id        = (known after apply)
      + association_id       = (known after apply)
      + carrier_ip           = (known after apply)
      + customer_owned_ip    = (known after apply)
      + domain               = "vpc"
      + id                   = (known after apply)
      + instance             = (known after apply)
      + network_border_group = (known after apply)
      + network_interface    = (known after apply)
      + private_dns          = (known after apply)
      + private_ip           = (known after apply)
      + public_dns           = (known after apply)
      + public_ip            = (known after apply)
      + public_ipv4_pool     = (known after apply)
      + tags                 = {
          + "Name"           = "nwngw1a01eip01"
        }
      + tags_all             = (known after apply)
      + vpc                  = (known after apply)
    }

  # aws_eip.ngw1c01eip01 will be created
  + resource "aws_eip" "ngw1c01eip01" {
      + allocation_id        = (known after apply)
      + association_id       = (known after apply)
      + carrier_ip           = (known after apply)
      + customer_owned_ip    = (known after apply)
      + domain               = "vpc"
      + id                   = (known after apply)
      + instance             = (known after apply)
      + network_border_group = (known after apply)
      + network_interface    = (known after apply)
      + private_dns          = (known after apply)
      + private_ip           = (known after apply)
      + public_dns           = (known after apply)
      + public_ip            = (known after apply)
      + public_ipv4_pool     = (known after apply)
      + tags                 = {
          + "Name"           = "nwngw1c01eip01"
        }
      + tags_all             = (known after apply)
      + vpc                  = (known after apply)
    }

  # aws_nat_gateway.ngw1a01 will be created
  + resource "aws_nat_gateway" "ngw1a01" {
      + allocation_id                      = (known after apply)
      + association_id                     = (known after apply)
      + connectivity_type                  = "public"
      + id                                 = (known after apply)
      + network_interface_id               = (known after apply)
      + private_ip                         = (known after apply)
      + public_ip                          = (known after apply)
      + secondary_private_ip_address_count = (known after apply)
      + secondary_private_ip_addresses     = (known after apply)
      + subnet_id                          = "subnet-073d40cd0c084332c"
      + tags                               = {
          + "Name"           = "nwngw1a01"
        }
      + tags_all                           = (known after apply)
    }

  # aws_nat_gateway.ngw1c01 will be created
  + resource "aws_nat_gateway" "ngw1c01" {
      + allocation_id                      = (known after apply)
      + association_id                     = (known after apply)
      + connectivity_type                  = "public"
      + id                                 = (known after apply)
      + network_interface_id               = (known after apply)
      + private_ip                         = (known after apply)
      + public_ip                          = (known after apply)
      + secondary_private_ip_address_count = (known after apply)
      + secondary_private_ip_addresses     = (known after apply)
      + subnet_id                          = "subnet-03bed27699232e0e0"
      + tags                               = {
          + "Name"           = "nwngw1c01"
        }
      + tags_all                           = (known after apply)
    }

  # aws_route_table.rtbpvt1a01 will be updated in-place
  ~ resource "aws_route_table" "rtbpvt1a01" {
        id               = "rtb-081d7d52464ab2b98"
      ~ route            = [
          - {
              - carrier_gateway_id         = ""
              - cidr_block                 = "0.0.0.0/0"
              - core_network_arn           = ""
              - destination_prefix_list_id = ""
              - egress_only_gateway_id     = ""
              - gateway_id                 = ""
              - ipv6_cidr_block            = ""
              - local_gateway_id           = ""
              - nat_gateway_id             = "nat-0806243b682d70571"
              - network_interface_id       = ""
              - transit_gateway_id         = ""
              - vpc_endpoint_id            = ""
              - vpc_peering_connection_id  = ""
            },
          + {
              + carrier_gateway_id         = ""
              + cidr_block                 = "0.0.0.0/0"
              + core_network_arn           = ""
              + destination_prefix_list_id = ""
              + egress_only_gateway_id     = ""
              + gateway_id                 = ""
              + ipv6_cidr_block            = ""
              + local_gateway_id           = ""
              + nat_gateway_id             = (known after apply)
              + network_interface_id       = ""
              + transit_gateway_id         = ""
              + vpc_endpoint_id            = ""
              + vpc_peering_connection_id  = ""
            },
        ]
        tags             = {
            "Name"           = "nwrtbpvt1a01"
        }
        # (5 unchanged attributes hidden)
    }

  # aws_route_table.rtbpvt1c01 will be updated in-place
  ~ resource "aws_route_table" "rtbpvt1c01" {
        id               = "rtb-00ed0059281716688"
      ~ route            = [
          - {
              - carrier_gateway_id         = ""
              - cidr_block                 = "0.0.0.0/0"
              - core_network_arn           = ""
              - destination_prefix_list_id = ""
              - egress_only_gateway_id     = ""
              - gateway_id                 = ""
              - ipv6_cidr_block            = ""
              - local_gateway_id           = ""
              - nat_gateway_id             = "nat-0a8b45bf6c738deea"
              - network_interface_id       = ""
              - transit_gateway_id         = ""
              - vpc_endpoint_id            = ""
              - vpc_peering_connection_id  = ""
            },
          + {
              + carrier_gateway_id         = ""
              + cidr_block                 = "0.0.0.0/0"
              + core_network_arn           = ""
              + destination_prefix_list_id = ""
              + egress_only_gateway_id     = ""
              + gateway_id                 = ""
              + ipv6_cidr_block            = ""
              + local_gateway_id           = ""
              + nat_gateway_id             = (known after apply)
              + network_interface_id       = ""
              + transit_gateway_id         = ""
              + vpc_endpoint_id            = ""
              + vpc_peering_connection_id  = ""
            },
        ]
        tags             = {
            "Name"           = "nwrtbpvt1c01"
        }
        # (5 unchanged attributes hidden)
    }

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

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

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

想定通りです。
EIPも削除しているのでEIPも再作成されていますが、想定通りNATGWの再作成、RouteTableの変更が入っています。
ではapplyします。

$ tofu apply
~~中略~~
Note: Objects have changed outside of OpenTofu

OpenTofu detected the following changes made outside of OpenTofu since the last "tofu apply" which may have affected this plan:

  # aws_eip.ngw1a01eip01 has been deleted
  - resource "aws_eip" "ngw1a01eip01" {
      - id                   = "eipalloc-0beb36b7e810a1bef" -> null
        tags                 = {
            "Name"           = "nwngw1a01eip01"
        }
        # (8 unchanged attributes hidden)
    }

  # aws_eip.ngw1c01eip01 has been deleted
  - resource "aws_eip" "ngw1c01eip01" {
      - id                   = "eipalloc-0c6f192958e54a017" -> null
        tags                 = {
            "Name"           = "nwngw1c01eip01"
        }
        # (8 unchanged attributes hidden)
    }

  # aws_nat_gateway.ngw1a01 has been deleted
  - resource "aws_nat_gateway" "ngw1a01" {
      - id                                 = "nat-0806243b682d70571" -> null
      - network_interface_id               = "eni-0673c836d5ee5050c" -> null
      - private_ip                         = "10.128.34.96" -> null
      - public_ip                          = "13.231.73.35" -> null
        tags                               = {
            "Name"           = "nwngw1a01"
        }
        # (7 unchanged attributes hidden)
    }

  # aws_nat_gateway.ngw1c01 has been deleted
  - resource "aws_nat_gateway" "ngw1c01" {
      - id                                 = "nat-0a8b45bf6c738deea" -> null
      - network_interface_id               = "eni-084f2e6e41715ec88" -> null
      - private_ip                         = "10.128.38.73" -> null
      - public_ip                          = "3.115.195.71" -> null
        tags                               = {
            "Name"           = "nwngw1c01"
        }
        # (7 unchanged attributes hidden)
    }


Unless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include
actions to undo or respond to these changes.

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

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

OpenTofu will perform the following actions:

  # aws_eip.ngw1a01eip01 will be created
  + resource "aws_eip" "ngw1a01eip01" {
      + allocation_id        = (known after apply)
      + association_id       = (known after apply)
      + carrier_ip           = (known after apply)
      + customer_owned_ip    = (known after apply)
      + domain               = "vpc"
      + id                   = (known after apply)
      + instance             = (known after apply)
      + network_border_group = (known after apply)
      + network_interface    = (known after apply)
      + private_dns          = (known after apply)
      + private_ip           = (known after apply)
      + public_dns           = (known after apply)
      + public_ip            = (known after apply)
      + public_ipv4_pool     = (known after apply)
      + tags                 = {
          + "Name"           = "nwngw1a01eip01"
        }
      + tags_all             = (known after apply)
      + vpc                  = (known after apply)
    }

  # aws_eip.ngw1c01eip01 will be created
  + resource "aws_eip" "ngw1c01eip01" {
      + allocation_id        = (known after apply)
      + association_id       = (known after apply)
      + carrier_ip           = (known after apply)
      + customer_owned_ip    = (known after apply)
      + domain               = "vpc"
      + id                   = (known after apply)
      + instance             = (known after apply)
      + network_border_group = (known after apply)
      + network_interface    = (known after apply)
      + private_dns          = (known after apply)
      + private_ip           = (known after apply)
      + public_dns           = (known after apply)
      + public_ip            = (known after apply)
      + public_ipv4_pool     = (known after apply)
      + tags                 = {
          + "Name"           = "nwngw1c01eip01"
        }
      + tags_all             = (known after apply)
      + vpc                  = (known after apply)
    }

  # aws_nat_gateway.ngw1a01 will be created
  + resource "aws_nat_gateway" "ngw1a01" {
      + allocation_id                      = (known after apply)
      + association_id                     = (known after apply)
      + connectivity_type                  = "public"
      + id                                 = (known after apply)
      + network_interface_id               = (known after apply)
      + private_ip                         = (known after apply)
      + public_ip                          = (known after apply)
      + secondary_private_ip_address_count = (known after apply)
      + secondary_private_ip_addresses     = (known after apply)
      + subnet_id                          = "subnet-073d40cd0c084332c"
      + tags                               = {
          + "Name"           = "nwngw1a01"
        }
      + tags_all                           = (known after apply)
    }

  # aws_nat_gateway.ngw1c01 will be created
  + resource "aws_nat_gateway" "ngw1c01" {
      + allocation_id                      = (known after apply)
      + association_id                     = (known after apply)
      + connectivity_type                  = "public"
      + id                                 = (known after apply)
      + network_interface_id               = (known after apply)
      + private_ip                         = (known after apply)
      + public_ip                          = (known after apply)
      + secondary_private_ip_address_count = (known after apply)
      + secondary_private_ip_addresses     = (known after apply)
      + subnet_id                          = "subnet-03bed27699232e0e0"
      + tags                               = {
          + "Name"           = "nwngw1c01"
        }
      + tags_all                           = (known after apply)
    }

  # aws_route_table.rtbpvt1a01 will be updated in-place
  ~ resource "aws_route_table" "rtbpvt1a01" {
        id               = "rtb-081d7d52464ab2b98"
      ~ route            = [
          - {
              - carrier_gateway_id         = ""
              - cidr_block                 = "0.0.0.0/0"
              - core_network_arn           = ""
              - destination_prefix_list_id = ""
              - egress_only_gateway_id     = ""
              - gateway_id                 = ""
              - ipv6_cidr_block            = ""
              - local_gateway_id           = ""
              - nat_gateway_id             = "nat-0806243b682d70571"
              - network_interface_id       = ""
              - transit_gateway_id         = ""
              - vpc_endpoint_id            = ""
              - vpc_peering_connection_id  = ""
            },
          + {
              + carrier_gateway_id         = ""
              + cidr_block                 = "0.0.0.0/0"
              + core_network_arn           = ""
              + destination_prefix_list_id = ""
              + egress_only_gateway_id     = ""
              + gateway_id                 = ""
              + ipv6_cidr_block            = ""
              + local_gateway_id           = ""
              + nat_gateway_id             = (known after apply)
              + network_interface_id       = ""
              + transit_gateway_id         = ""
              + vpc_endpoint_id            = ""
              + vpc_peering_connection_id  = ""
            },
        ]
        tags             = {
            "Name"           = "nwrtbpvt1a01"
        }
        # (5 unchanged attributes hidden)
    }

  # aws_route_table.rtbpvt1c01 will be updated in-place
  ~ resource "aws_route_table" "rtbpvt1c01" {
        id               = "rtb-00ed0059281716688"
      ~ route            = [
          - {
              - carrier_gateway_id         = ""
              - cidr_block                 = "0.0.0.0/0"
              - core_network_arn           = ""
              - destination_prefix_list_id = ""
              - egress_only_gateway_id     = ""
              - gateway_id                 = ""
              - ipv6_cidr_block            = ""
              - local_gateway_id           = ""
              - nat_gateway_id             = "nat-0a8b45bf6c738deea"
              - network_interface_id       = ""
              - transit_gateway_id         = ""
              - vpc_endpoint_id            = ""
              - vpc_peering_connection_id  = ""
            },
          + {
              + carrier_gateway_id         = ""
              + cidr_block                 = "0.0.0.0/0"
              + core_network_arn           = ""
              + destination_prefix_list_id = ""
              + egress_only_gateway_id     = ""
              + gateway_id                 = ""
              + ipv6_cidr_block            = ""
              + local_gateway_id           = ""
              + nat_gateway_id             = (known after apply)
              + network_interface_id       = ""
              + transit_gateway_id         = ""
              + vpc_endpoint_id            = ""
              + vpc_peering_connection_id  = ""
            },
        ]
        tags             = {
            "Name"           = "nwrtbpvt1c01"
        }
        # (5 unchanged attributes hidden)
    }

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

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

  Enter a value: yes

~~中略~~

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

変更できました。
本当に簡単に移行できますね。初回にtofu initさえ実施すればTerraformからOpenTofuに移行し今まで通りリソースが管理できそうです。

OpenTofuでリソースを削除する

Terraformで作成し、OpenTofuで追加・修正したリソースを削除してみます。

$ tofu destroy

~~中略~~

Do you really want to destroy all resources?
  OpenTofu will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

~~中略~~

Destroy complete! Resources: 36 destroyed.

削除もできました。

あとがき

思っていた以上に簡単に移行が出来そうな手ごたえがありました。
ほんと注意点はinitをきちんとすることくらいかなと思います。
今回はAWSのリソースをいじるというよく使われるところで試したのもあると思います。Terraformで実装されているけどあまりメジャーではないprovider(例えばVMware Aria Operations for Applicationsとか)だと同じようにうまくいくとは限らないかもしれません。
OpenTofuは次期バージョンとなる1.7からTerraformでは利用できないがコミュニティなどでリクエストされた新しい機能などをupdateする方針のようです。
そのため、Terraformのライセンス変更でOpenTofu移行を検討されているようであれば早めに移行してしまったほうがいいかもしれません。