4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Terraform v1.14 で GA した Terraform search を使ってみよう

Last updated at Posted at 2025-12-28

この記事は、HashiCorp Japan Advent Calendar 2025 7 日目の記事です。

Terraform search とは?

  • Terraform v1.14 で一般利用可能になった新機能
  • Terraform で管理されていないアンマネージドなリソースの検出およびインポートを支援
  • Terraform CLI と HCP Terraform どちらも利用可能

何が課題だったか?

特にクラウドへの移行の初期段階などでは、IaC を導入せずに CLI やコンソールを使って手動でリソースが作成・管理されることがあります (ClickOps と呼ばれる)。

その結果として起こるのが

  • インフラの利用状況がトラッキングできないことによる無駄なクラウド支出の増加
  • 誤設定や脆弱な設定によるセキュリティリスク
  • 変更履歴を追えない・予期せぬ環境差分によるデプロイの失敗、インシデントの発生
  • コンプライアンス、組織のポリシー、規制要件に違反するリソースの出現

などです。

どうやって解決するか?

これらの課題に対して、理想的なあるべき姿は、すべてのリソースが最初から完全に Terraform の管理下に置かれ、宣言的な定義に基づいて自動化されたワークフローによって変更管理されることですが、現実問題それは難しいでしょう。

そこで必要となるのは、管理されていないリソースを自動的に発見し、Terraform の管理下に置くようなツールです。Terraform search がまさにこれで、実際のクラウド環境をスキャンして管理されていないリソースを検出し、Terraform state にインポートするリソース定義の記述を支援してくれます。

実際に使ってみよう

前提条件

v1.14 以上の Terraform を使用してください。HCP Terraform の場合は、ワークスペースの Settings > General から変更してください。

スクリーンショット 2025-12-24 23.49.21.png

クエリの実行

まずは、探索したいリソースのプロバイダーを定義します。これは従来の Terraform と変わりありません。また、以下では HCP Terraform の設定も記載していますが、ローカルで実行する場合はなくても OK です。

terraform.tf
terraform {
  cloud {
    organization = "xxxxx"
    workspaces {
      name = "terraform-search-test"
    }
  }
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "6.26.0"
    }
  }
}

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

次に、探索するリソースを定義するための *.tfquery.hcl という拡張子を持つファイルを作成します。最小限の定義は以下です。

search.tfquery.hcl
list "aws_vpc" "example" {
  provider = aws
}

Terraform search では、対象のリソースを list ブロックで定義します。

list "<TYPE>" "<LABEL>"

  • <TYPE>: 検索対象のリソース種別
  • <LABEL>: 検出したリソースのラベル名

Terraform search をサポートしているリソース種別は、各プロバイダーのドキュメントで確認できます (例: aws_vpc)。ラベルは従来の Terraform の使い方と同じで、参照に使用するためユニークな値を指定する必要があります。

ファイルを作成したら、以下のコマンドで検索を実行します。

terraform query

結果は以下のような出力になります。この例では、1つの VPC を検出しました。

% terraform query
Running query in HCP Terraform. Output will stream here. Pressing Ctrl-C
will stop streaming the logs, but will not stop the query running remotely.

Preparing the remote query run...

To view this query run in a browser, visit:
https://app.terraform.io/app/xxxxx/terraform-search-test/search/qry-DgxrByW2mbEjHYFo

Waiting for the query run to start...

Terraform v1.14.3
on linux_amd64
Initializing plugins and modules...
list.aws_vpc.example   account_id=9092xxxxxxxx,id=vpc-0b67d6a8a7ec55e6c,region=ap-northeast-1   project-vpc (vpc-0b67d6a8a7ec55e6c)

検索対象のリソースは、config パラメータを指定することで細かく絞り込むことができます。次に、VPC サブネットを検索してみましょう。

search.tfquery.hcl
list "aws_subnet" "example" {
  provider = aws

  config {
    filter {
      name   = "availability-zone"
      values = ["ap-northeast-1a"]
    }
  }
}

filter 条件で ap-norhteast-1a のサブネットのみを検索します。この VPC には合計 6 つのサブネットがありますが、1a のサブネットのみが検出されていることが以下の出力から分かります。

Waiting for the query run to start...

Terraform v1.14.3
on linux_amd64
Initializing plugins and modules...
list.aws_subnet.example   account_id=9092xxxxxxxx,id=subnet-007dbf7f675e3233a,region=ap-northeast-1   project-subnet-private1-ap-northeast-1a (subnet-007dbf7f675e3233a)
list.aws_subnet.example   account_id=9092xxxxxxxx,id=subnet-0a42f326856afb9d1,region=ap-northeast-1   project-subnet-public1-ap-northeast-1a (subnet-0a42f326856afb9d1)

config で指定可能なパラメータはプロバイダー/リソースごとに異なるのでドキュメントを参照してください。aws_subnet の場合は、主に filter で絞ることになります。filter で指定可能なパラメータはさらに CLI のリファレンスから確認します。

HCP Terraform を利用する場合

ワークスペースに、Search & Import というメニューが増えています。CLI から実行した terraform query の実行結果が閲覧でき、検出されたリソースの一覧が表示されています。

また、右上の + New query ボタンで UI からクエリを実行することもできます。
スクリーンショット 2025-12-27 18.56.06.png

リソースのインポート

terraform import コマンドを使用すると、手動で作成したリソースを Terraform の管理下に置くことができます。この機能自体は以前からあるものですが、Terraform search によってリソースの検出からインポートまでのワークフローをシームレスに実現することができるようになりました。

クエリによって検出されたリソースを選択して、右上の Generate Starter Configuration ボタンをクリックすると、import ブロックとリソース定義が自動生成されます。

スクリーンショット 2025-12-28 14.54.46.png

main.tf ファイルを作成して、Starter Configuration として生成されたコードをコピペします。

スクリーンショット 2025-12-28 14.54.53.png

main.tf
import {
  to       = aws_subnet.example_0
  provider = aws
  identity = {
    account_id = "9092xxxxxxxx"
    id         = "subnet-007dbf7f675e3233a"
    region     = "ap-northeast-1"
  }
}

resource "aws_subnet" "example_0" {
  provider                                       = aws
  assign_ipv6_address_on_creation                = false
  availability_zone                              = "ap-northeast-1a"
  availability_zone_id                           = "apne1-az4"
  cidr_block                                     = "10.0.128.0/20"
  customer_owned_ipv4_pool                       = null
  enable_dns64                                   = false
  enable_lni_at_device_index                     = 0
  enable_resource_name_dns_a_record_on_launch    = false
  enable_resource_name_dns_aaaa_record_on_launch = false
  ipv6_cidr_block                                = null
  ipv6_native                                    = false
  map_customer_owned_ip_on_launch                = false
  map_public_ip_on_launch                        = false
  outpost_arn                                    = null
  private_dns_hostname_type_on_launch            = "ip-name"
  region                                         = "ap-northeast-1"
  tags = {
    Name = "project-subnet-private1-ap-northeast-1a"
  }
  tags_all = {
    Name = "project-subnet-private1-ap-northeast-1a"
  }
  vpc_id = "vpc-0b67d6a8a7ec55e6c"
  timeouts {
    create = null
    delete = null
  }
}

import {
  to       = aws_subnet.example_1
  provider = aws
  identity = {
    account_id = "9092xxxxxxxx"
    id         = "subnet-0a42f326856afb9d1"
    region     = "ap-northeast-1"
  }
}

resource "aws_subnet" "example_1" {
  provider                                       = aws
  assign_ipv6_address_on_creation                = false
  availability_zone                              = "ap-northeast-1a"
  availability_zone_id                           = "apne1-az4"
  cidr_block                                     = "10.0.0.0/20"
  customer_owned_ipv4_pool                       = null
  enable_dns64                                   = false
  enable_lni_at_device_index                     = 0
  enable_resource_name_dns_a_record_on_launch    = false
  enable_resource_name_dns_aaaa_record_on_launch = false
  ipv6_cidr_block                                = null
  ipv6_native                                    = false
  map_customer_owned_ip_on_launch                = false
  map_public_ip_on_launch                        = false
  outpost_arn                                    = null
  private_dns_hostname_type_on_launch            = "ip-name"
  region                                         = "ap-northeast-1"
  tags = {
    Name = "project-subnet-public1-ap-northeast-1a"
  }
  tags_all = {
    Name = "project-subnet-public1-ap-northeast-1a"
  }
  vpc_id = "vpc-0b67d6a8a7ec55e6c"
  timeouts {
    create = null
    delete = null
  }
}

コードの中身を確認して、既存のリソース設定に合うように修正します。
ちなみにこのまま terraform plan を実行すると以下のエラーが発生しました。

│ Error: Missing required argument
│ 
│   with aws_subnet.example_0,
│   on main.tf line 24, in resource "aws_subnet" "example_0":
│   24:   map_customer_owned_ip_on_launch                = false
│ 
│ "map_customer_owned_ip_on_launch": all of
│ `customer_owned_ipv4_pool,map_customer_owned_ip_on_launch,outpost_arn` must
│ be specified
╵
╷
│ Error: Conflicting configuration arguments
│ 
│   with aws_subnet.example_0,
│   on main.tf line 14, in resource "aws_subnet" "example_0":
│   14:   availability_zone                              = "ap-northeast-1a"
│ 
│ "availability_zone": conflicts with availability_zone_id
╵
╷
│ Error: Conflicting configuration arguments
│ 
│   with aws_subnet.example_0,
│   on main.tf line 15, in resource "aws_subnet" "example_0":
│   15:   availability_zone_id                           = "apne1-az4"
│ 
│ "availability_zone_id": conflicts with availability_zone
╵
╷
│ Error: enable_lni_at_device_index must not be zero, got 0
│ 
│   with aws_subnet.example_0,
│   on main.tf line 19, in resource "aws_subnet" "example_0":
│   19:   enable_lni_at_device_index                     = 0
│ 
╵

以下の修正を行います。

  • map_customer_owned_ip_on_launch はデフォルト false なので消します
  • availability_zoneavailability_zone_id がコンフリクトしているので片方消します
  • enable_lni_at_device_index は 0 を指定することができないのですが、これも使わないので消します

再度 terraform plan を実行すると、エラーは消えましたがリソースの差分が出てしまっています。timeouts の部分は今回いらないのでこれも削除してしまいましょう。

Terraform will perform the following actions:

  # aws_subnet.example_0 will be updated in-place
  # (will be imported first)
  ~ resource "aws_subnet" "example_0" {
        arn                                            = "arn:aws:ec2:ap-northeast-1:9092xxxxxxxx:subnet/subnet-007dbf7f675e3233a"
        assign_ipv6_address_on_creation                = false
        availability_zone                              = "ap-northeast-1a"
        availability_zone_id                           = "apne1-az4"
        cidr_block                                     = "10.0.128.0/20"
        customer_owned_ipv4_pool                       = null
        enable_dns64                                   = false
        enable_lni_at_device_index                     = 0
        enable_resource_name_dns_a_record_on_launch    = false
        enable_resource_name_dns_aaaa_record_on_launch = false
        id                                             = "subnet-007dbf7f675e3233a"
        ipv6_cidr_block                                = null
        ipv6_cidr_block_association_id                 = null
        ipv6_native                                    = false
        map_customer_owned_ip_on_launch                = false
        map_public_ip_on_launch                        = false
        outpost_arn                                    = null
        owner_id                                       = "9092xxxxxxxx"
        private_dns_hostname_type_on_launch            = "ip-name"
        region                                         = "ap-northeast-1"
        tags                                           = {
            "Name" = "project-subnet-private1-ap-northeast-1a"
        }
        tags_all                                       = {
            "Name" = "project-subnet-private1-ap-northeast-1a"
        }
        vpc_id                                         = "vpc-0b67d6a8a7ec55e6c"

      + timeouts {}
    }

これで再度プランを実行すると、インポートだけになりました。terraform apply を実行すると、リソースがインポートされます。

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

この状態で再度クエリを実行すると、先ほどのリソースの IaC StatusManaged になっていることがわかります。

スクリーンショット 2025-12-28 18.09.14.png

今回は HCP Terraform の UI からコードを生成する例を紹介しましたが、ローカルで terraform query を実行する場合は、以下のオプションを付けることでファイルに出力することもできます。

terraform query -generate-config-out=subnets.tf

まとめ

Terraform search を利用することで、レガシーな IaC 管理されていない既存のリソースを簡単に Terraform 管理下に移行することができます。これは、大量のリソースが存在し、一からリソース定義を記述するには時間がかかるような大規模ワークロードで特に効果を発揮します。

プロバイダーによってはまだ対応していないリソースもありますが、今後どんどんサポートも増えていくと思うのでぜひ HCP Terraform と合わせて活用してみてください!

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?