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移行を検討されているようであれば早めに移行してしまったほうがいいかもしれません。

OpenTofuでネットワークリソースを作成する

先日入れたOpenTofuでAWSのネットワークリソースを作成してみます。

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用

tofu initの実行

Terraformと同様に、初回実行前にinitを実行。

$ tofu init

Initializing the backend...

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

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 created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that OpenTofu can guarantee to make the same selections by default when
you run "tofu init" in the future.

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.

Terraformで作成したコードそのままでinitは通りました。

tofu planの実行

ではplanを実行してみます。

$ tofu plan

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

OpenTofu will perform the following actions:

~~~中略~~~

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

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.

問題なく通りました。
特にコード変更なしで行けるんだなぁと感心。特にprovider周りはエラーが起きるかなと思っていたので、tofu用にコード変更なしで通るのは意外でした。

tofu applyの実行

ではapplyの実行です。
正直、planがすんなり通っているのでapplyは普通に通ると思いました。

$ tofu apply
~~~中略~~~
Do you want to perform these actions?
  OpenTofu will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: 
~~~中略~~~

あっけなくネットワークのリソースが作成できました。

.terraformディレクトリと.terraform.lock.hcl、terraform.tfstateファイル

tofu initを実行するとTerraformのproviderのプラグインなどがダウンロードされてくる.terraformディレクトリとprovider情報とかが記録される.terraform.lock.hclファイルが作成されます。
中を見てみると、.terraformディレクトリにはopentofu.orgから落としてきたproviderが入っています。
terraform initした際に作成されるディレクトリ構成とほぼ一緒でした。

$ ls .terraform/providers/registry.opentofu.org/hashicorp/aws/5.35.0/linux_amd64/
CHANGELOG.md  LICENSE  README.md  terraform-provider-aws

.terraform.lock.hclファイルは以下の内容。terraformでinitした際に作成される.terraform.lock.hclファイルと内容はほぼ一緒。異なるのはterraformがtofuになっているのと、providerのレジストリがopentofuのURLになっている。

# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.

provider "registry.opentofu.org/hashicorp/aws" {
  version     = "5.35.0"
  constraints = "~> 5.0"
  hashes = [
~~~中略~~~
  ]
}

tofu apply時に作成されるterraform.tfstateファイルですが、中をざっと見たところterraform applyした際に作成されるterraform.tfstateファイルとほぼ同じ構成でした。
これなら今までTerraformで構成管理していたものをOpenTofuに変更するのは簡単に出来そうな気がします。

OpenTofuを動かしてみて

これは思っていたよりも楽にOpenTofuが使えますね。正直、TerraformからOpenTofuに移行するとなった場合にTerraformで作成したcodeに何かしらの変更が入ると思っていましたが、
特にそんなことはなく何も修正しなくても新規でリソースが作成できました。
動かしてみてちょっと思ったのは、tofu initなどのコマンドを実行した直後にちょっと待ちが発生するなぁくらいです。
そのほかは特に違和感などもありませんでした。

今回は新規リソースの作成でしたが、既存をterraformで作ってそれをtofuでリソース追加・削除するみたいなのも確認してみようと思います。

OpenTofuを使えるようにする

OpenTofuとは

Terraformを管理しているHashiCorpがTerraformを含む一部サービスをBSL(Business Source License)に変更すると2023年に発表がありました。
このライセンスに変更することで本番利用についてはライセンスが必要となる可能性が出ています。
※詳しいことはわからない…。

それに反発した人たちがコミュニティを立ち上げたのがOpenTofuというプロジェクトです。※Linux Foundation
今後Terraformがどこまで無料で使えるかわからないこともあり、新しくできたOpenTofuを勉強してみようと思います。

OpenTofuのインストール

公式サイトの手順通りに実行

まずはOpenTofuをインストールします。
公式サイトにインストール手順があるのでそれに従います。
opentofu.org

自分はWSL2でUbuntu22.04を使用しているので、Ubuntuのインストール手順を利用します。
opentofu.org

$ which snap
/usr/bin/snap
$ snap install --classic opentofu
error: cannot communicate with server: Post http://localhost/v2/snaps/opentofu: dial unix /run/snapd.socket: connect: no such file or directory

早速うまくいきません。
エラーメッセージを見ると、「dial unix /run/snapd.socket: connect: no such file or directory」とあるので、snapdのソケットが見つからないと。
snapdが動いてない?

snapdプロセス確認

ということで、プロセスを確認する。

$ ps -ef | grep snap
***+  5347  5312  0 17:11 pts/8    00:00:00 grep --color=auto snap
$ ps auxwww | grep snap
***+  5349  0.0  0.0   8168   716 pts/8    S+   17:11   0:00 grep --color=auto snap

居ない…。
なので、snapdを起動させる必要がありそうです。

Webで調べていると、WSLではsystemdでsnapがdisableになっているようです。
ということで、ちゃちゃっとenableにします。

sudo systemctl start snapd.service
System has not been booted with systemd as init system (PID 1). Can't operate.
Failed to connect to bus: Host is down

おっふ、systemdが動いてない…。

WSL起動時にsystemdが動くようにする

boot時にsystemdが動くようwsl.confに設定を入れます。

$ sudo vi /etc/wsl.conf
$ sudo cat /etc/wsl.conf
#[network]
#generateResolvConf = false
[boot]
systemd=true

WSLの再起動が必要になるので、一度Ubuntuから抜けてpowershellを起動、WSLの停止をします。

PS C:\Users\***> wsl --shutdown

コマンド実行後、WSLを起動。Ubuntuにログインしsystemdの起動確認。

$ ps -ef | grep systemd
root          53       1  0 17:36 ?        00:00:00 /lib/systemd/systemd-journald
root          77       1  0 17:36 ?        00:00:00 /lib/systemd/systemd-udevd
systemd+     107       1  0 17:36 ?        00:00:00 /lib/systemd/systemd-networkd
systemd+     255       1  0 17:36 ?        00:00:00 /lib/systemd/systemd-resolved
message+     259       1  0 17:36 ?        00:00:00 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only
root         268       1  0 17:36 ?        00:00:00 /lib/systemd/systemd-logind
***+    1045       1  0 17:36 ?        00:00:00 /lib/systemd/systemd --user
root        1138      77  0 17:37 ?        00:00:00 /lib/systemd/systemd-udevd
root        1139      77  0 17:37 ?        00:00:00 /lib/systemd/systemd-udevd
root        1140      77  0 17:37 ?        00:00:00 /lib/systemd/systemd-udevd
root        1141      77  0 17:37 ?        00:00:00 /lib/systemd/systemd-udevd

systemdの起動が確認できました。

OpenTofuインストールの再実行

これでもう一度OpenTofuのインストールを実行。

$ snap install --classic opentofu
error: access denied (try with sudo)

sudoつけろと怒られたので、sudoつけて実行。

$ sudo snap install --classic opentofu
opentofu 1.6.1 from OpenTofu Core Team installed

うまくいきました。
ちょっと時間かかりましたが、ようやくOpenTofuをインストールできました。

OpenTofuを使ってみる

今まではterraform xxxでterraformを実行していましたが、これがtofu xxxに変更になるようです。
opentofu.org

ここのStep2にあるtofu --versionを実行してみます。

$ tofu --version
OpenTofu v1.6.1
on linux_amd64

コマンドが通ることが確認できました。
これでtofuを使う準備は完了ですかね。

次回からinitやplan、applyなどを試してみようと思います。

AmazonLinux2からAmazonLinux2023に移行したらEC2のメタデータが取れなくなった

お題の通りで、AmazonLinux2からAmazonLinux2023に移行したらEC2のメタデータが取れなくなった件です。

EC2の移行

AmazonLinux2はサポート終了日が 2025年6月30日までとなっており、利用者はそれまでにAmazonLinux2023などに移行が必要です。
AWSの公式サイトにサポート終了日の記載があります。
aws.amazon.com

ということで、AmazonLinux2の移行を検討していく必要があります。
AWSではAmazonLinux2の後継としてAmazonLinux2023がリリースされております。
大きな違いはOSがRedhat/CentOSベースからFedoraベースに変更されたことでしょうか。パッケージ管理系のコマンドがyumからdnfになっています。
その他、大きな変更点などは他の先人たちのサイトを見ていただくのがよろしいかとおもいます。

AmazonLinux2023に移行

ということで、AmazonLinux2をAmazonLinux2023に移行してみました。
移行といっても既存で入っているMWなどを新たに構築したAmazonLinux2023で再構築する形です。
先述した通り、Redhat/CentOSベースからFedoraベースに変更されていますので、パッケージインストールはdnfに変更。
その他一般的なLinuxコマンドは変わらず利用できそうです。デーモン管理などもsystemctlで実行可能です。

MWを一通り再設定してみて稼働確認して問題なし、OSは異なれどLinux同士なのでそこまで難しくはないです。
※細かいところで違いはきっとあると思いますが、稼働については今のところ問題なさそう。

EC2のメタデータが取れない

一通り構築が終わり、EC2のメタデータを利用してホスト名をつけようと思った時に問題が発生。
想定した通りにホスト名が設定できませんでした。
実行コマンドを1個1個確認していったところ、メタデータの取得でデータが何もとれていないことが判明。
AmazonLinux2の時にメタデータ取得に利用していたコマンド

curl http://169.254.169.254/latest/meta-data/

AmazonLinux2023ではIMDSv2がデフォルトで動作

調べてみたところ、AmazonLinux2023ではIMDSv2がデフォルトで動作するようです。
IMDSv2で動作する場合、メタデータ取得する際にTokenを取得し、そのTokenをHeaderにセットして利用する必要がありました。
docs.aws.amazon.com

ということで、AmazonLinux2の時にメタデータ取得に利用していたコマンドを以下の通り変更

TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"` \
&& curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/

メタデータを複数取得して変数に入れたいみたいなときはこんな感じです。

TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
AMIId=`curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/ami-id`
InstanceId=`curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/instance-id`
region=`curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/placement/availability-zone`

WSL2でterraform実行時にSTSのエラー

Terraform実行時に以下のエラーが出力されました。

Error: retrieving AWS account details: validating provider credentials: retrieving caller identity from STS: operation error STS: GetCallerIdentity, https response error StatusCode: 403, RequestID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, api error SignatureDoesNotMatch: Signature expired: 20231027T055458Z is now earlier than 20231027T055839Z (20231027T061339Z - 15 min.)

この対応についてのメモです。

エラーメッセージ

エラーメッセージを読むと、以下のような意味になります。

Error: retrieving AWS account details:
エラー:AWSアカウントの詳細を取得

validating provider credentials:
プロバイダー認証の検証

retrieving caller identity from STS
STSから実行者のIDを取得

operation error STS:
STSの操作エラー

GetCallerIdentity, https response error StatusCode: 403,
実行者のIDをGet、httpsのレスポンスエラー、ステータスコード403

RequestID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx,
リスエストID

api error SignatureDoesNotMatch:
APIのエラー、署名がマッチしない

Signature expired: 20231027T055458Z is now earlier than 20231027T055839Z (20231027T061339Z - 15 min.)
署名の有効期限切れ、20231027T055458Zが20231027T055839Zより早い

意味を考える

うーん、ざっと訳した感じ、最後の時間がポイントっぽいのでWSL上の時刻を取得する。

$ date
Fri Oct 27 15:19:34 JST 2023

※ちなみに、この時PC上の時刻は15:35あたりでした。

WSL2の時刻がだいぶずれていることが判明。
ちょっとGoogle先生で調べただけでも結構WSLの時刻がずれるという記事が出てきます。先人に感謝感謝。
ということで、原因は時刻ずれじゃないかなと想定、(20231027T061339Z - 15 min.)と出ているので、おそらく15分以上時刻がずれるとこのエラーが出るのかなと考えました。

WSL2の時刻同期をする

いろんな方法があるみたいですが、自分的には一番なじみのあるntpを実行することに。
ntpdateコマンドを実行しようとしたところ、ntpdateはWSL2のUbuntuには入っていなさそうだったのでインストールから

$ ntpdate

Command 'ntpdate' not found, but can be installed with:

sudo apt install ntpdate         # version 1:4.2.8p12+dfsg-3ubuntu4.20.04.1, or
sudo apt install ntpsec-ntpdate  # version 1.1.8+dfsg1-4build1

ntpdateのインストール

上で出ているようにaptでntpdateをインストール

$ sudo apt install ntpdate 
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following NEW packages will be installed:
  ntpdate
0 upgraded, 1 newly installed, 0 to remove and 257 not upgraded.
Need to get 48.8 kB of archives.
After this operation, 178 kB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu focal-updates/universe amd64 ntpdate amd64 1:4.2.8p12+dfsg-3ubuntu4.20.04.1 [48.8 kB]
Fetched 48.8 kB in 1s (44.8 kB/s)  
Selecting previously unselected package ntpdate.
(Reading database ... 51086 files and directories currently installed.)
Preparing to unpack .../ntpdate_1%3a4.2.8p12+dfsg-3ubuntu4.20.04.1_amd64.deb ...
Unpacking ntpdate (1:4.2.8p12+dfsg-3ubuntu4.20.04.1) ...
Setting up ntpdate (1:4.2.8p12+dfsg-3ubuntu4.20.04.1) ...
Processing triggers for man-db (2.9.1-1) ...

時刻同期

時刻同期先のNTPサーバはNICTさんにしました。
NICTのNTPサーバにntpdateで時刻同期を実施

$ sudo ntpdate ntp.nict.jp
27 Oct 15:37:57 ntpdate[9999]: step time server 133.243.238.163 offset 999.980640 sec

エラー解消

当初予想した通り、WSL2の時刻がかなりずれていたことでSTSのSignatureとのタイムラグが問題となりエラーとなっていたようです。
時刻同期を実施後にterraform planを実行したところ問題なく動作しました。

AKSにkafkaを入れる

Azure上のKubernetesにkafkaを入れてみます。

Kafka

Kafkaについては、素晴らしいサイトがいくつもあるので、そちらを見ていただいたほうがいいと思います。
個人的にはちょっと前のブログですが、以下のサイトを元に勉強をさせていただきました。
qiita.com
qiita.com


kafka導入

AKS上にkafkaを入れる方法ですが、今回はhelmを使ってbitnamiにあるkafkaをインストールしようと思います。
まずは以下のブログに書いた方法でCLIAKSに接続します。
gokigenmaru.hatenablog.com

ここでhelmを使ってインストールするのですが、まずはhelmコマンドをインストールするところからですね。

helmコマンドのインストール

helmのサイトにインストール方法が載っています。
自分はUbuntuなので、Aptで入れる方法を実施しました。
※サイトはバイナリを持ってきていれる方法がページ上部にあり、パッケージマネージャを使う方法はバイナリで入れる方法の下にあるので注意です。
helm.sh

サイトにある手順通りにコマンドを実行すればhelmのインストール完了です。

$ type helm
helm is hashed (/usr/sbin/helm)
$ helm version
version.BuildInfo{Version:"v3.12.3", GitCommit:"3a31588ad33fe3b89af5a2a54ee1d25bfe6eaa5e", GitTreeState:"clean", GoVersion:"go1.20.7"}
bitnami

bitnami上のkafkaを使うのですが、サイトは以下にあります。
github.com
こちらにあるvalues.yamlがkafkaのインストール内容になります。

まずはデフォルトで入れてみる

helmコマンドを使ってまずはbitnami上にあるデフォルトの値でインストールしてみます。
まずはbitnamiのリポジトリをhelmで追加します。

$ helm repo add bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories
$ helm search repo bitnami/kafka
NAME            CHART VERSION   APP VERSION     DESCRIPTION                                       
bitnami/kafka   25.1.10         3.5.1           Apache Kafka is a distributed streaming platfor...

見てみると、今のチャートのバージョンは25.1.10ですね。
では、インストールしてみます。

$ helm install kafka bitnami/kafka
NAME: kafka
LAST DEPLOYED: Sun Sep 10 10:35:44 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: kafka
CHART VERSION: 25.1.10
APP VERSION: 3.5.1

** Please be patient while the chart is being deployed **

Kafka can be accessed by consumers via port 9092 on the following DNS name from within your cluster:

    kafka.default.svc.cluster.local

Each Kafka broker can be accessed by producers via port 9092 on the following DNS name(s) from within your cluster:

    kafka-controller-0.kafka-controller-headless.default.svc.cluster.local:9092
    kafka-controller-1.kafka-controller-headless.default.svc.cluster.local:9092
    kafka-controller-2.kafka-controller-headless.default.svc.cluster.local:9092

The CLIENT listener for Kafka client connections from within your cluster have been configured with the following security settings:
    - SASL authentication

To connect a client to your Kafka, you need to create the 'client.properties' configuration files with the content below:

security.protocol=SASL_PLAINTEXT
sasl.mechanism=SCRAM-SHA-256
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required \
    username="user1" \
    password="$(kubectl get secret kafka-user-passwords --namespace default -o jsonpath='{.data.client-passwords}' | base64 -d | cut -d , -f 1)";

To create a pod that you can use as a Kafka client run the following commands:

    kubectl run kafka-client --restart='Never' --image docker.io/bitnami/kafka:3.5.1-debian-11-r44 --namespace default --command -- sleep infinity
    kubectl cp --namespace default /path/to/client.properties kafka-client:/tmp/client.properties
    kubectl exec --tty -i kafka-client --namespace default -- bash

    PRODUCER:
        kafka-console-producer.sh \
            --producer.config /tmp/client.properties \
            --broker-list kafka-controller-0.kafka-controller-headless.default.svc.cluster.local:9092,kafka-controller-1.kafka-controller-headless.default.svc.cluster.local:9092,kafka-controller-2.kafka-controller-headless.default.svc.cluster.local:9092 \
            --topic test

    CONSUMER:
        kafka-console-consumer.sh \
            --consumer.config /tmp/client.properties \
            --bootstrap-server kafka.default.svc.cluster.local:9092 \
            --topic test \
            --from-beginning
$ kubectl get pod
NAME                 READY   STATUS    RESTARTS   AGE
kafka-controller-0   1/1     Running   0          84s
kafka-controller-1   1/1     Running   0          84s
kafka-controller-2   1/1     Running   0          84s

簡単にインストールできました。
helm使うとインストールらくちんです。



備忘録:kafkaのシェルコマンド

このページは備忘録です。
たまにアップデートされていくと思います。

kafka brokerのconfigを見る

kafka-configs.sh --bootstrap-server kafka.default.svc.cluster.local:9092 --entity-type brokers --describe --all

topicの一覧をlistで出す

kafka-topics.sh --bootstrap-server kafka.default.svc.cluster.local:9092 --list

kafkaのtopicの情報を見る

kafka-topics.sh --bootstrap-server kafka.default.svc.cluster.local:9092 --topic [topic名] --describe

kafkaのProducerでメッセージを送信する

kafka-console-producer.sh --broker-list kafka-0.kafka-headless.default.svc.cluster.local:9092,kafka-1.kafka-headless.default.svc.cluster.local:9092,kafka-2.kafka-headless.default.svc.cluster.local:9092 --topic [topic名]

kafkaのConsumerでメッセージを受信する

kafka-console-consumer.sh --bootstrap-server kafka.default.svc.cluster.local:9092 --topic [topic名] --from-beginning