Terraform v1.5.0 で追加された import ブロックと check ブロックを試してみた

2023.06.14

はじめに

こんにちは、アノテーション構築チームの荒川です。

2023-06-12 に正式リリースされた Terraform v1.5.0 の新機能である import ブロックと check ブロックを早速試しましたので紹介します。

試した内容は HashiCorp 社公式のブログ Terraform 1.5 brings config-driven import and checks と同じものになります。

環境情報

  • tfenv 3.0.0-18-g1ccfddb
  • Terraform v1.5.0
  • 事前に IAM ユーザー、IAM ロールを作成済み

導入

Terraform 1.5.0 のインストール

tfutils/tfenv: Terraform version manager を使って、Terraform バージョン 1.5.0 をインストールします。

$ tfenv install 1.5.0
Installing Terraform v1.5.0
# ...(省略)

$ tfenv use 1.5.0
Switching default version to v1.5.0
Default version (when not overridden by .terraform-version or TFENV_TERRAFORM_VERSION) is now: 1.5.0

$ terraform -v
Terraform v1.5.0
on darwin_arm64

import ブロック

従来のインポート制限

従来は terraform import コマンドで実行していた既存リソースのインポートでは以下のような制限がありました。

  • Resources are imported one at a time.
  • State is immediately modified, with no opportunity to preview the results. This can lead to accidental resource modifications or deletions if an apply operation is executed on the shared state by another team member before the corresponding configuration has been added.
  • The matching resource code has to be manually written, which often means a multi-step process of running plans to identify the required attribute values to achieve a clean run.

機械翻訳をかけた内容も記載します。

  • リソースは一度に 1 つずつインポートされます。
  • 状態はすぐに変更され、結果をプレビューする機会はありません。_ これにより、対応する構成が追加される前に、別のチーム メンバーによって共有状態に対して適用操作が実行された場合、リソースが誤って変更または削除される可能性があります。
  • 一致するリソース コードは手動で記述する必要があります。これは多くの場合、クリーンな実行を達成するために必要な属性値を特定するための計画を実行する複数のステップのプロセスを意味します。

それでは Terraform バージョン 1.5.0 でインポートがどうなったかを見ていきましょう。

インポート対象の作成

あらかじめ、インポート対象の EC2 インスタンスをマネジメントコンソールなどで作成します。対象リソースが停止済みの状態でもインポート可能です。

インポート結果をファイルへ出力する

import ブロックでは idto が必須パラメータとなっています。その他にはオプションで provider もあります。provider を指定しなければデフォルトのプロバイダーが選択されます。

id にはインポート対象のリソース ID をリテラル文字列(変数は使用不可)で、to には対象となる Terraform のリソース参照を指定します。

main.tf

terraform {
  required_version = "~> 1.5.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.2"
    }
  }
}

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

import {
  id = "i-08a63997b6546193c"
  to = aws_instance.example
}

その後、terraform plan コマンドに -generate-config-out=generated.tf オプションを指定して実行します。 generated の箇所はインポート後の出力ファイル名となりますので、別の名前にも変更できます。同じファイル名で 2 回インポートするといった上書き(重複ファイルの)保存はできません。

$ terraform init
# ...(省略)
$ terraform plan -generate-config-out=generated.tf
aws_instance.example: Preparing import... [id=i-08a63997b6546193c]
aws_instance.example: Refreshing state... [id=i-08a63997b6546193c]

Planning failed. Terraform encountered an error while generating this plan.

╷
│ Warning: Config generation is experimental
│ 
│ Generating configuration during import is currently experimental, and the generated configuration format
│ may change in future versions.
╵
╷
│ Error: Conflicting configuration arguments
│ 
│   with aws_instance.example,
│   on generater.tf line 14:
│   (source code not available)
│ 
│ "ipv6_address_count": conflicts with ipv6_addresses
╵
╷
│ Error: Conflicting configuration arguments
│ 
│   with aws_instance.example,
│   on generater.tf line 15:
│   (source code not available)
│ 
│ "ipv6_addresses": conflicts with ipv6_address_count

Warning メッセージに関しては import のドキュメント にも注意書きされている通り、将来のリリースで変更が入る可能性を示唆しています。

また、ipv6_address_countipv6_addresses が(自動で)両方とも定義されたため、エラーメッセージが出ますが、どちらかをコメントアウトすれば問題ありません。 生成された generated.tf は自由に編集できますので、ipv6_addresses はコメントアウトします。

generated.tf

# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.

# __generated__ by Terraform
resource "aws_instance" "example" {
  ami                                  = "ami-0bba69335379e17f8"
  associate_public_ip_address          = false
  availability_zone                    = "ap-northeast-1a"
  disable_api_stop                     = false
  disable_api_termination              = false
  ebs_optimized                        = true
  get_password_data                    = false
  hibernation                          = false
  host_id                              = null
  host_resource_group_arn              = null
  iam_instance_profile                 = "network-stack-Ec2IamRoleInstanceProfile-mbweK8XgLiSz"
  instance_initiated_shutdown_behavior = "stop"
  instance_type                        = "t3.micro"
  ipv6_address_count                   = 0
  # ipv6_address_count と共存できないのでコメントアウトする
  # ipv6_addresses                       = []
  key_name                             = "arakawa-keypair"
  monitoring                           = false
  placement_group                      = null
  placement_partition_number           = 0
  private_ip                           = "10.0.1.58"
  secondary_private_ips                = []
  security_groups                      = []
  source_dest_check                    = true
  subnet_id                            = "subnet-081f6e0739afaf5f2"
  tags = {
    Name = "sample-ec2"
  }
  tags_all = {
    Name = "sample-ec2"
  }
  tenancy                     = "default"
  user_data                   = null
  user_data_base64            = null
  user_data_replace_on_change = null
  volume_tags                 = null
  vpc_security_group_ids      = ["sg-0544ec1bad40b2a64"]
  capacity_reservation_specification {
    capacity_reservation_preference = "open"
  }
  cpu_options {
    amd_sev_snp      = null
    core_count       = 1
    threads_per_core = 2
  }
  credit_specification {
    cpu_credits = "unlimited"
  }
  enclave_options {
    enabled = false
  }
  maintenance_options {
    auto_recovery = "default"
  }
  metadata_options {
    http_endpoint               = "enabled"
    http_put_response_hop_limit = 1
    http_tokens                 = "optional"
    instance_metadata_tags      = "disabled"
  }
  private_dns_name_options {
    enable_resource_name_dns_a_record    = true
    enable_resource_name_dns_aaaa_record = false
    hostname_type                        = "ip-name"
  }
  root_block_device {
    delete_on_termination = true
    encrypted             = false
    iops                  = 100
    kms_key_id            = null
    tags                  = {}
    throughput            = 0
    volume_size           = 8
    volume_type           = "gp2"
  }
}

コメントアウト後に再度 plan を実行します。

$ terraform plan
aws_instance.example: Preparing import... [id=i-08a63997b6546193c]
aws_instance.example: Refreshing state... [id=i-08a63997b6546193c]

Terraform will perform the following actions:

  # aws_instance.example will be imported
    resource "aws_instance" "example" {
        ami                                  = "ami-0bba69335379e17f8"
        arn                                  = "arn:aws:ec2:ap-northeast-1:123456789012:instance/i-08a63997b6546193c"
        associate_public_ip_address          = false
        availability_zone                    = "ap-northeast-1a"
        cpu_core_count                       = 1
        cpu_threads_per_core                 = 2
        disable_api_stop                     = false
        disable_api_termination              = false
        ebs_optimized                        = true
        get_password_data                    = false
        hibernation                          = false
        iam_instance_profile                 = "network-stack-Ec2IamRoleInstanceProfile-mbweK8XgLiSz"
        id                                   = "i-08a63997b6546193c"
        instance_initiated_shutdown_behavior = "stop"
        instance_state                       = "stopped"
        instance_type                        = "t3.micro"
        ipv6_address_count                   = 0
        ipv6_addresses                       = []
        key_name                             = "arakawa-keypair"
        monitoring                           = false
        placement_partition_number           = 0
        primary_network_interface_id         = "eni-07807f8e0c923e5d5"
        private_dns                          = "ip-10-0-1-58.ap-northeast-1.compute.internal"
        private_ip                           = "10.0.1.58"
        secondary_private_ips                = []
        security_groups                      = []
        source_dest_check                    = true
        subnet_id                            = "subnet-081f6e0739afaf5f2"
        tags                                 = {
            "Name" = "sample-ec2"
        }
        tags_all                             = {
            "Name" = "sample-ec2"
        }
        tenancy                              = "default"
        vpc_security_group_ids               = [
            "sg-0544ec1bad40b2a64",
        ]

        capacity_reservation_specification {
            capacity_reservation_preference = "open"
        }

        cpu_options {
            core_count       = 1
            threads_per_core = 2
        }

        credit_specification {
            cpu_credits = "unlimited"
        }

        enclave_options {
            enabled = false
        }

        maintenance_options {
            auto_recovery = "default"
        }

        metadata_options {
            http_endpoint               = "enabled"
            http_put_response_hop_limit = 1
            http_tokens                 = "optional"
            instance_metadata_tags      = "disabled"
        }

        private_dns_name_options {
            enable_resource_name_dns_a_record    = true
            enable_resource_name_dns_aaaa_record = false
            hostname_type                        = "ip-name"
        }

        root_block_device {
            delete_on_termination = true
            device_name           = "/dev/xvda"
            encrypted             = false
            iops                  = 100
            tags                  = {}
            throughput            = 0
            volume_id             = "vol-0139b9770d0e206d9"
            volume_size           = 8
            volume_type           = "gp2"
        }
    }

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

この状態でエラーなく plan が実行できましたので apply をします。

$ terraform apply
# ...(省略)
# 出力内容を確認して yes を入力
  Enter a value: yes
aws_instance.example: Importing... [id=i-08a63997b6546193c]
aws_instance.example: Import complete [id=i-08a63997b6546193c]

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

以上で、terraform.tfstate が作成されインポート対象の EC2 インスタンスが Terraform のステート管理対象となります。 以降の Terraform での操作は、あたかも初めから Terraform で作成したかのように振る舞えます。generated.tf を通常の Terraform のリソースを定義したファイルとして扱えます。

インポート後は import ブロックを消して問題ありません。消した後も plan 等の操作に影響はありません。

既存の resource ブロックで設定で構成を管理しつつ import する

事前にリソース構成を把握できている場合は、あらかじめ任意の .tf ファイルに対象の resource ブロックを作成して、値を入力しておくこともできます。

ec2.tf

# 事前に把握している構成を定義(※今回は aws_instance の必須パラメータのみ記載)
resource "aws_instance" "example" {
  instance_type = "t3.micro"
  ami           = "ami-0bba69335379e17f8"
}

これまでの操作を試し、対象 EC2 インスタンスがインポート済みの状態である場合は、generated.tf と terraform.tfstate ファイルを削除します。

# 既存の generated.tf, tfstate を削除
$ rm -f generated.tf terraform.tfstate

その後、再度 terraform plan コマンドを実行します。-generate-config-out=generated.tf オプションは不要です。

$ terraform plan
aws_instance.example: Preparing import... [id=i-08a63997b6546193c]
aws_instance.example: Refreshing state... [id=i-08a63997b6546193c]

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_instance.example will be updated in-place
  # (imported from "i-08a63997b6546193c")
  ~ resource "aws_instance" "example" {
        ami                                  = "ami-0bba69335379e17f8"
        arn                                  = "arn:aws:ec2:ap-northeast-1:123456789012:instance/i-08a63997b6546193c"
        associate_public_ip_address          = false
        availability_zone                    = "ap-northeast-1a"
        cpu_core_count                       = 1
        cpu_threads_per_core                 = 2
        disable_api_stop                     = false
        disable_api_termination              = false
        ebs_optimized                        = true
        get_password_data                    = false
        hibernation                          = false
        iam_instance_profile                 = "network-stack-Ec2IamRoleInstanceProfile-mbweK8XgLiSz"
        id                                   = "i-08a63997b6546193c"
        instance_initiated_shutdown_behavior = "stop"
        instance_state                       = "stopped"
        instance_type                        = "t3.micro"
        ipv6_address_count                   = 0
        ipv6_addresses                       = []
        key_name                             = "arakawa-keypair"
        monitoring                           = false
        placement_partition_number           = 0
        primary_network_interface_id         = "eni-07807f8e0c923e5d5"
        private_dns                          = "ip-10-0-1-58.ap-northeast-1.compute.internal"
        private_ip                           = "10.0.1.58"
        secondary_private_ips                = []
        security_groups                      = []
        source_dest_check                    = true
        subnet_id                            = "subnet-081f6e0739afaf5f2"
      ~ tags                                 = {
          - "Name" = "sample-ec2" -> null
        }
      ~ tags_all                             = {
          - "Name" = "sample-ec2"
        } -> (known after apply)
        tenancy                              = "default"
      + user_data_replace_on_change          = false
        vpc_security_group_ids               = [
            "sg-0544ec1bad40b2a64",
        ]

        capacity_reservation_specification {
            capacity_reservation_preference = "open"
        }

        cpu_options {
            core_count       = 1
            threads_per_core = 2
        }

        credit_specification {
            cpu_credits = "unlimited"
        }

        enclave_options {
            enabled = false
        }

        maintenance_options {
            auto_recovery = "default"
        }

        metadata_options {
            http_endpoint               = "enabled"
            http_put_response_hop_limit = 1
            http_tokens                 = "optional"
            instance_metadata_tags      = "disabled"
        }

        private_dns_name_options {
            enable_resource_name_dns_a_record    = true
            enable_resource_name_dns_aaaa_record = false
            hostname_type                        = "ip-name"
        }

        root_block_device {
            delete_on_termination = true
            device_name           = "/dev/xvda"
            encrypted             = false
            iops                  = 100
            tags                  = {}
            throughput            = 0
            volume_id             = "vol-0139b9770d0e206d9"
            volume_size           = 8
            volume_type           = "gp2"
        }
    }

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

plan 実行後も 先程と同様に ipv6_address_countipv6_addresses が重複していますが、エラーとはならないので続けて apply します。

$ terraform apply
# ...(省略)
# 出力内容を確認して yes を入力
  Enter a value: yes

aws_instance.example: Importing... [id=i-08a63997b6546193c]
aws_instance.example: Import complete [id=i-08a63997b6546193c]

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

import が完了すると resource.aws_instance.example リソース定義に変更が入るわけではなく、terraform.tfstate 上でのみ存在するリソースになります。

インポート後は import ブロックを消して問題ありません。消した後も plan 等の操作に影響はありません。

この状態でインスタンスタイプを t3.micro -> t3.nano と変更してみます。さらに、停止済みの状態を続けたいので、aws_ec2_instance_state も追記します。

ec2.tf

resource "aws_instance" "example" {
  instance_type = "t3.nano" # t3.micro -> t3.nano
  ami           = "ami-0bba69335379e17f8"
}

# 停止済みを維持する
resource "aws_ec2_instance_state" "example" {
  instance_id = aws_instance.example.id
  state       = "stopped"
}

再度 plan を実行します。

$ terraform plan
aws_instance.example: Refreshing state... [id=i-08a63997b6546193c]

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

Terraform will perform the following actions:

  # aws_ec2_instance_state.example will be created
  + resource "aws_ec2_instance_state" "example" {
      + force       = false
      + id          = (known after apply)
      + instance_id = "i-08a63997b6546193c"
      + state       = "stopped"
    }

  # aws_instance.example will be updated in-place
  ~ resource "aws_instance" "example" {
        id                                   = "i-08a63997b6546193c"
      ~ instance_type                        = "t3.micro" -> "t3.nano"
        tags                                 = {}
      + user_data_replace_on_change          = false
        # (29 unchanged attributes hidden)

        # (8 unchanged blocks hidden)
    }

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

この状態で apply をすると、先程と同様にあたかも初めから Terraform で管理していたかのように変更を加えられます。

注意点として、インスタンスタイプを変更したタイミングで EC2 インスタンスの状態は実行中となります。1〜2 分ほど待機すれば、停止済みとなります。

インスタンスタイプ変更直後

インスタンスタイプ変更から 1〜2 分後

複数のリソースを同時にインポートする

これまでの操作を試し、対象 EC2 インスタンスがインポート済みの状態である場合は、generated.tf と terraform.tfstate、ならびに terraform.tfstate.backup ファイルを削除します。

# 既存の generated.tf, tfstate, tfstate.backup, ec2.tf を削除
$ rm -f generated.tf terraform.tfstate terraform.tfstate.backup ec2.tf

その後、以下のようにインポート対象を 2 つ定義します。

main.tf

import {
  id = "i-08a63997b6546193c"
  to = aws_instance.example1
}

import {
  id = "i-0fadbf71562989414"
  to = aws_instance.example2
}

plan コマンドを実行します。

$ terraform plan -generate-config-out=generated.tf
aws_instance.example1: Preparing import... [id=i-08a63997b6546193c]
aws_instance.example2: Preparing import... [id=i-0fadbf71562989414]
aws_instance.example1: Refreshing state... [id=i-08a63997b6546193c]
aws_instance.example2: Refreshing state... [id=i-0fadbf71562989414]

Planning failed. Terraform encountered an error while generating this plan.

╷
│ Warning: Config generation is experimental
│ 
│ Generating configuration during import is currently experimental, and the generated configuration format
│ may change in future versions.
╵
╷
│ Error: Conflicting configuration arguments
│ 
│   with aws_instance.example2,
│   on generated.tf line 14:
│   (source code not available)
│ 
│ "ipv6_address_count": conflicts with ipv6_addresses
╵
╷
│ Error: Conflicting configuration arguments
│ 
│   with aws_instance.example1,
│   on generated.tf line 14:
│   (source code not available)
│ 
│ "ipv6_address_count": conflicts with ipv6_addresses
╵
╷
│ Error: Conflicting configuration arguments
│ 
│   with aws_instance.example2,
│   on generated.tf line 15:
│   (source code not available)
│ 
│ "ipv6_addresses": conflicts with ipv6_address_count
╵
╷
│ Error: Conflicting configuration arguments
│ 
│   with aws_instance.example1,
│   on generated.tf line 15:
│   (source code not available)
│ 
│ "ipv6_addresses": conflicts with ipv6_address_count

これまでと変わらず ipv6 関連のコンフリクトエラーは出ますが、generated.tf には 2 つのリソースがインポートされます。

generated.tf の 20 行目、98 行目(ipv6_addresses の箇所)を 2 箇所コメントアウトして保存し、再度 apply を実行します。

$ terraform apply
# ...(省略)
# yes を入力
Apply complete! Resources: 2 imported, 0 added, 0 changed, 0 destroyed.

複数のリソースが同時にインポートできました!

これまで同様、インポート後は import ブロックを消して問題ありません。消した後も plan 等の操作に影響はありません。

続いて check も見ていきましょう。

check ブロック

従来のカスタム条件

従来は カスタム条件 を使って、lifecycle ブロックで入力値の事前検証と事後検証ができました。

今後は check ブロックで定義することでユニットテストのアサーションのように、より柔軟に検証を強化できます。HashiCorp 社のブログでより具体的な内容を見ていきましょう。

  • Because they exist at the top level, checks can reference all resources, data sources, and module outputs in the configuration. Checks are best suited for overall functional validation of the infrastructure, while postconditions guarantee the configuration of a single resource.
  • Checks occur as the last step in the plan or apply and do not halt execution. Failed checks emit a warning message instead of an error.
  • Checks can contain more than one assertion. Combined with the ability to reference all objects in the configuration and the power of the Terraform language, this allows for more complex conditional evaluations that make up an overall result.
  • A check block can optionally include one nested (“scoped”) data source. If a scoped data source fails to execute, the error is contained to the check block evaluation and does not halt overall execution of the Terraform run.

機械翻訳をかけた内容も記載します。

  • これらは最上位に存在するため、チェックでは構成内のすべてのリソース、データ ソース、およびモジュール出力を参照できます。チェックはインフラストラクチャの全体的な機能検証に最適ですが、事後条件は単一リソースの構成を保証します。
  • チェックはプランまたは適用の最後のステップとして発生し、実行は停止しません。チェックが失敗すると、エラーではなく警告メッセージが表示されます。
  • チェックには複数のアサーションを含めることができます。構成内のすべてのオブジェクトを参照する機能と Terraform 言語の機能を組み合わせることで、全体的な結果を構成するより複雑な条件付き評価が可能になります。
  • チェック ブロックには、オプションで 1 つのネストされた (「スコープ付き」) データ ソースを含めることができます。スコープ付きデータ ソースの実行に失敗した場合、エラーはチェック ブロックの評価に含まれ、Terraform 実行全体の実行は停止しません。

それでは Terraform バージョン 1.5.0 でチェックがどうなったかを見ていきましょう。 インポートと同様に EC2 に対して試してみます。

check ブロックの実装

check ブロックには assert ブロックの実装と、assert ブロック内の conditionerror_message が必須パラメータとなっています。今回は最小の構成で作ってみます。

check.tf

check "instance_type_check" {
  assert {
    condition     = aws_instance.example1.instance_type == "t3.nano"
    error_message = "インスタンスタイプが ${aws_instance.example1.instance_type} です。t3.nano を指定してください。"
  }
}

plan を実行してみます。

$ terraform plan
aws_instance.example2: Refreshing state... [id=i-0fadbf71562989414]
aws_instance.example1: Refreshing state... [id=i-08a63997b6546193c]

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.

generated.tfaws_instance.example1instance_type を変更してみます。

generated.tf

instance_type = "t3.micro" # t3.nano -> t3.micro

この状態で plan を再度実行します。

$ terraform plan
aws_instance.example1: Refreshing state... [id=i-08a63997b6546193c]
aws_instance.example2: Refreshing state... [id=i-0fadbf71562989414]

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_instance.example1 will be updated in-place
  ~ resource "aws_instance" "example1" {
        id                                   = "i-08a63997b6546193c"
      ~ instance_type                        = "t3.nano" -> "t3.micro"
        tags                                 = {
            "Name" = "sample-ec2"
        }
      + user_data_replace_on_change          = false
        # (29 unchanged attributes hidden)

        # (8 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.
╷
│ Warning: Check block assertion failed
│ 
│   on check.tf line 3, in check "instance_type_check":
│    3:     condition     = aws_instance.example1.instance_type == "t3.nano"
│     ├────────────────
│     │ aws_instance.example1.instance_type is "t3.micro"
│ 
│ インスタンスタイプが t3.micro です。t3.nano を指定してください。
╵

plan の最後に警告がわかりやすく表示されます。apply 時も同様に警告は出ますが、yes を入力すると apply できてしまいます。

Plan: 0 to add, 1 to change, 0 to destroy.
╷
│ Warning: Check block assertion failed
│ 
│   on check.tf line 3, in check "instance_type_check":
│    3:     condition     = aws_instance.example1.instance_type == "t3.nano"
│     ├────────────────
│     │ aws_instance.example1.instance_type is "t3.micro"
│ 
│ インスタンスタイプが t3.micro です。t3.nano を指定してください。
╵

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_instance.example1: Modifying... [id=i-08a63997b6546193c]
aws_instance.example1: Still modifying... [id=i-08a63997b6546193c, 10s elapsed]
aws_instance.example1: Still modifying... [id=i-08a63997b6546193c, 20s elapsed]
aws_instance.example1: Modifications complete after 23s [id=i-08a63997b6546193c]
╷
│ Warning: Check block assertion failed
│ 
│   on check.tf line 3, in check "instance_type_check":
│    3:     condition     = aws_instance.example1.instance_type == "t3.nano"
│     ├────────────────
│     │ aws_instance.example1.instance_type is "t3.micro"
│ 
│ インスタンスタイプが t3.micro です。t3.nano を指定してください。
╵

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

apply 前の確認時、apply 完了後の 2 回警告メッセージが表示されます。

警告だけではなく強制力を持たせたい場合は、これまで同様 事前検証と事後検証 も併用して、各リソース内でも検証しましょう。

generated.tfaws_instance.example1 ブロック内に lifecycle を追記します。

generated.tf

  # インポートした aws_instance.example1 リソース内に記載
  lifecycle {
    postcondition {
      condition     = self.instance_type == "t3.nano"
      error_message = "インスタンスタイプが ${self.instance_type} です。t3.nano を指定してください。"
    }
  }

こうすることで、エラーで中断させられます。apply もできなくなります。

$ terraform plan
# ...(省略)
Plan: 0 to add, 1 to change, 0 to destroy.
╷
│ Error: Resource postcondition failed
│ 
│   on generated.tf line 83, in resource "aws_instance" "example1":
│   83:       condition     = self.instance_type == "t3.nano"
│     ├────────────────
│     │ self.instance_type is "t3.micro"
│ 
│ インスタンスタイプが t3.micro です。t3.nano を指定してください。
╵

まとめ

Terraform 1.5.0 のアップデート情報を紹介しました。

import ブロックは多少の不整合が発生する可能性はありますが、作成する際に指定可能なパラメータの多いリソースに対して便利そうでした。

EC2 や CloudFront、RDS などはパラメータが大量に指定できますので、先にマネジメントコンソールで作成してから、後からまとめてインポートするといった使い方が便利そうです。

check ブロックは最上位のブロックで定義できるようになったことで、condition の条件が resrouce ブロックの self 以外にも、柔軟に指定できるようになりました。

以下の HashiCorp 社公式のブログのサンプルコードのように、実際に HTTP GET リクエストを送信し、サービスの稼働状況なども含めた上で、より安全にデプロイが行えるようになります。

check "health_check" {
  data "http" "example" {
    url = "https://${aws_lb.example.dns_name}"
  }
 
  assert {
    condition     = data.http.example.status_code == 200
    error_message = "${data.http.example.url} returned an unhealthy status code"
  }
}

check は警告までですので、警告文も無視して apply してしまわないようにご注意ください。

補足: VSCode プラグイン HashiCorp Terraform について

VSCode の HashiCorp Terraform - Visual Studio Marketplace を使っていますが、プリリースバージョンでも import / check ブロックともに補完が効かないです。今後のアップデートを楽しみにしています。

リリースページ: Releases · hashicorp/vscode-terraform

terraform validate コマンドはすでにアップデートされているため、VSCode の設定を行っておけば、必須パラメータの不足等は判定してくれます。

2023-06-26 追記

拡張機能のバージョン2.27.0より check、import ブロックの導入されました。 現時点でもプリリリースバージョンであれば使用できます。

参考

アノテーション株式会社について

アノテーション株式会社は、クラスメソッド社のグループ企業として「オペレーション・エクセレンス」を担える企業を目指してチャレンジを続けています。「らしく働く、らしく生きる」のスローガンを掲げ、さまざまな背景をもつ多様なメンバーが自由度の高い働き方を通してお客様へサービスを提供し続けてきました。現在当社では一緒に会社を盛り上げていただけるメンバーを募集中です。少しでもご興味あれば、アノテーション株式会社WEBサイト をご覧ください。