Terraform Queryコマンドで既存リソースを楽に一括インポート

Terraform Queryコマンドで既存リソースを楽に一括インポート

2026.04.30

もうTerraformのversion 1.15がGAしましたが、今回は前version 1.14で登場した terraform queryコマンドを触ってみたのでレポートします。

terraform queryコマンドの概要

既存リソースをTerraformに一括インポートする際の前処理に使います。

「Terraformにインポート」というと import ブロックが使えますが、このimportブロックと、インポート先resourceブロックの作成を手伝ってくれるコマンド、という位置づけです。

terraform queryを実行する前に、新設のlistブロックの中に欲しい既存リソースを取得する(≒クエリする)処理を、これまた新設の.tfquery.hcl 拡張子ファイルに書く必要があります。

さて、以降は実際に触りながら解説します。

想定状況

「マネコン(マネジメントコンソール、要はAWSのWebページ)で作ったAWS環境を、今後はTerraformで管理していきたいぜー」という状況だとします。各種VPCリソース、複数のEC2インスタンス、セキュリティグループ、ALB、IAM RoleなどのたくさんのAWSリソースが作成済です。

既存リソースをTerraformに移植=Terraform管理下に置くためには、前述のimportブロックを1リソース毎に書いていく必要があります。リソース数が多い場合は大変です。

ここでterraform queryコマンドを使って少し楽をしましょう。

Providerの設定

これはterraform queryを使わない場合でも必要ですが、AWS providerの設定をします。terraform.required_providers以下にAWS Providerの設定と、providerブロックでそのAWS Providerの設定、ここではリージョン指定のみ、を行ないます。これは.tfquery.hcl 拡張子ファイルではなく通常の .tfファイルで構いません。

main.tf
terraform {
  required_version = ">= 1.14.9"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.42"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

Listブロック

以下は既存EC2インスタンスを取得する例です。

queries.tfquery.hcl
list "aws_instance" "dev" {
  provider = aws

  config {
    filter {
      name   = "tag:Env"
      values = ["Dev"]
    }
  }
}
  • provider引数は必須項目です。どのprovider経由でリソースを取得するか設定します。
  • config 引数以下でどういうインスタンスを取得するか設定します。config 引数以下の設定方法は各ProviderのListリソースの定義に拠ります。上記例の場合はEC2インスタンスのListリソースで、定義はこちらです。filter 以下はaws ec2 describe-instancesコマンドの --filters オプションと同じ項目が使えます。今回はEnvタグ値がDevのインスタンスのみ取得する、という設定になっています。

terraform queryコマンドを実行してみる

1インスタンスがヒットしました!

% terraform query
list.aws_instance.dev   account_id=123456789012,id=i-096a4d5e24c8fa433,region=us-east-1   query-test (i-096a4d5e24c8fa433)

表示されている情報=account_id/id/region は、リソースインポート時に使える Identity Schema です。

複数個ヒットする場合は以下のよう単純に行が増えます。

% terraform query
list.aws_instance.dev   account_id=123456789012,id=i-096a4d5e24c8fa433,region=us-east-1   query-test (i-096a4d5e24c8fa433)
list.aws_instance.dev   account_id=123456789012,id=i-01190cb94a67f0dbr,region=us-east-1   query-test2 (i-01190cb94a67f0dbr)

terraform queryコマンドからimportブロックとインポート先resourceブロックを作成する

terraform query -generate-config-out=generated.tf といった感じで、generate-config-out オプションを指定して実行します。するとimportブロックとインポート先の resourceブロックを自動作成してくれました!

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

# __generated__ by Terraform
resource "aws_instance" "dev_0" {
  provider                             = aws
  ami                                  = "ami-098e39bafa7e7303d"
  associate_public_ip_address          = true
  availability_zone                    = "us-east-1b"
  disable_api_stop                     = false
  disable_api_termination              = false
  ebs_optimized                        = true
  force_destroy                        = null
  get_password_data                    = false
  hibernation                          = false
  instance_initiated_shutdown_behavior = "stop"
  instance_type                        = "t3.micro"
  ipv6_address_count                   = 0
  ipv6_addresses                       = []
  monitoring                           = false
  placement_partition_number           = 0
  private_ip                           = "172.31.47.245"
  region                               = "us-east-1"
  secondary_private_ips                = []
  security_groups                      = ["default"]
  source_dest_check                    = true
  subnet_id                            = "subnet-56a9370a"
  tags = {
    Env  = "Dev"
    Name = "query-test"
  }
  tags_all = {
    Env  = "Dev"
    Name = "query-test"
  }
  tenancy                     = "default"
  user_data                   = null
  user_data_replace_on_change = null
  volume_tags                 = null
  vpc_security_group_ids      = ["sg-368fa676"]
  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_protocol_ipv6          = "disabled"
    http_put_response_hop_limit = 2
    http_tokens                 = "required"
    instance_metadata_tags      = "disabled"
  }
  primary_network_interface {
    network_interface_id = "eni-0512b0d94ce3f8aea"
  }
  private_dns_name_options {
    enable_resource_name_dns_a_record    = true
    enable_resource_name_dns_aaaa_record = false
    hostname_type                        = "ip-name"
  }
  root_ブロック_device {
    delete_on_termination = true
    encrypted             = false
    iops                  = 3000
    tags                  = {}
    tags_all              = {}
    throughput            = 125
    volume_size           = 8
    volume_type           = "gp3"
  }
  timeouts {
    create = null
    delete = null
    read   = null
    update = null
  }
}

import {
  to       = aws_instance.dev_0
  provider = aws
  identity = {
    account_id = "123456789012"
    id         = "i-096a4d5e24c8fa433"
    region     = "us-east-1"
  }
}

resource "aws_instance" "dev_1" {
  provider                             = aws
  ami                                  = "ami-098e39bafa7e7303d"
  associate_public_ip_address          = true
  availability_zone                    = "us-east-1b"
  disable_api_stop                     = false
  disable_api_termination              = false
  ebs_optimized                        = true
  force_destroy                        = null
  get_password_data                    = false
  hibernation                          = false
  instance_initiated_shutdown_behavior = "stop"
  instance_type                        = "t3.micro"
  ipv6_address_count                   = 0
  ipv6_addresses                       = []
  monitoring                           = false
  placement_partition_number           = 0
  private_ip                           = "172.31.47.77"
  region                               = "us-east-1"
  secondary_private_ips                = []
  security_groups                      = ["default"]
  source_dest_check                    = true
  subnet_id                            = "subnet-56a9370a"
  tags = {
    Env  = "Dev"
    Name = "query-test2"
  }
  tags_all = {
    Env  = "Dev"
    Name = "query-test2"
  }
  tenancy                     = "default"
  user_data                   = null
  user_data_replace_on_change = null
  volume_tags                 = null
  vpc_security_group_ids      = ["sg-368fa676"]
  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_protocol_ipv6          = "disabled"
    http_put_response_hop_limit = 2
    http_tokens                 = "required"
    instance_metadata_tags      = "disabled"
  }
  primary_network_interface {
    network_interface_id = "eni-053eb97a55b000c0b"
  }
  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                  = 3000
    tags                  = {}
    tags_all              = {}
    throughput            = 125
    volume_size           = 8
    volume_type           = "gp3"
  }
  timeouts {
    create = null
    delete = null
    read   = null
    update = null
  }
}

import {
  to       = aws_instance.dev_1
  provider = aws
  identity = {
    account_id = "123456789012"
    id         = "i-01190cb94a67f0dbr"
    region     = "us-east-1"
  }
}

terraform apply

以下の通りエラーになりました。機械的にコードを生成してくれただけなので仕方ないですね。

% terraform apply

│ Error: Conflicting configuration arguments

│   with aws_instance.dev_0,
│   on generated.tf line 5, in resource "aws_instance" "dev_0":
│    5: resource "aws_instance" "dev_0" {

│ "primary_network_interface": conflicts with associate_public_ip_address


│ Error: Conflicting configuration arguments

│   with aws_instance.dev_0,
│   on generated.tf line 18, in resource "aws_instance" "dev_0":
│   18:   ipv6_address_count                   = 0

│ "ipv6_address_count": conflicts with ipv6_addresses


│ Error: Conflicting configuration arguments

│   with aws_instance.dev_0,
│   on generated.tf line 19, in resource "aws_instance" "dev_0":
│   19:   ipv6_addresses                       = []

│ "ipv6_addresses": conflicts with ipv6_address_count


│ Error: Conflicting configuration arguments

│   with aws_instance.dev_1,
│   on generated.tf line 100, in resource "aws_instance" "dev_1":
│  100: resource "aws_instance" "dev_1" {

│ "primary_network_interface": conflicts with associate_public_ip_address


│ Error: Conflicting configuration arguments

│   with aws_instance.dev_1,
│   on generated.tf line 113, in resource "aws_instance" "dev_1":
│  113:   ipv6_address_count                   = 0

│ "ipv6_address_count": conflicts with ipv6_addresses


│ Error: Conflicting configuration arguments

│   with aws_instance.dev_1,
│   on generated.tf line 114, in resource "aws_instance" "dev_1":
│  114:   ipv6_addresses                       = []

│ "ipv6_addresses": conflicts with ipv6_address_count

競合属性を削除しました。

  • ipv6_addresses = []ipv6_address_count と常時競合するため、ipv6_address_count のみ残しました
  • primary_network_interface ブロック — 多数の引数と競合するため

再度applyすると、 diffが2 to importとなり、想定どおりなので通し(yesを入力し)ます。

% terraform apply

(略)

Plan: 2 to import, 0 to add, 2 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を入力)

(略)

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

以下の通りTerraform管理下にEC2インスタンスを置くことができました!

% terraform state list
aws_instance.dev_0
aws_instance.dev_1

再度terraform applyを実行するとdiffは無くなりました。

% terraform apply     
aws_instance.dev_1: Refreshing state... [id=i-01190cb94a67f0da7]
aws_instance.dev_0: Refreshing state... [id=i-096a4d5e24c8fa452]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

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

他のリソース(VPC,IAM Roleなど)も「Listブロック」項から繰り返していけば、すべてTerraform管理下に置けるはずです。

リファクタリング

現状はリソースをTerraform管理下に置けただけの状態です。ここからさらに「可読性の高い」「変更に強い」「移植性の高い」「意図が伝わる」コードにして行くのがベターです。

  • ファイル名(generated.tf)変える・ファイル分割する
  • 意図の無いargumentを削除する
    • デフォルト値を指定しているだけのものなど
  • 環境固有値を削除する
    • リソースidをベタ書きしている箇所を、他のリソースのattributeを参照する形に変えたり、Data Sourceを活用したりなどする
  • 一部attribute値をvariablesを参照する形に変える
  • 公式スタイルガイドに準拠する
  • movedブロックでリソースアドレスを変更する

落穂拾い

listブロックとData Source(dataブロック)はなにが違うの?

「既存リソースの情報をTerraformの中で使う」という意味だと、Data Source(dataブロック)があります。これとlistブロックの違いは何でしょうか? → 目的が異なります。

  • listブロック → 既存リソースをTerraform管理下に置きたいときに使う
    • 今後Terraform上で更新(設定値の変更や削除など)が可能になる
  • Data Source(dataブロック) → 既存リソースの情報を参照したいときに使う
    • そのリソース自体の更新は不可。情報参照するだけ

また、使われるタイミングが異なります。 listブロックは terraform queryを実行した際に読まれますが、通常のTerraformワークフローつまり terraform planterraform apply時には無関係です。一方Data Sourceは逆で、terraform queryでは使われない(.tfquery.hcl拡張子のファイルにData Sourceを書くとエラーになりました)ですが、 terraform planterraform apply時に使われます。

terraform queryコマンドで取得したリソースの詳細を確認したい

include_resource 引数追加で可能です。

  list "aws_instance" "dev" {
    provider = aws
+
+   include_resource = true
+
    config {
      filter {
        name   = "tag:Env"
        values = ["Dev"]
      }
    }
  }

…が、ただ単に terraform queryを実行しただけでは出力内容は変わりませんでした。terraform query -json とした場合は詳細情報も出るようになりました。

listブロックの構文チェックがしたい

terraform validate-queryオプションを追加すると .tfquery.hcl拡張子のファイルつまりlistブロックの構文チェックも実施してくれます。

% terraform validate -query

│ Error: Unsupported block type

│   on queries.tfquery.hcl line 17:
│   17: data "aws_caller_identity" "current" {}

│ Blocks of type "data" are not expected here.

感想

「マネコンで作ったAWS環境を、今後はTerraformで管理していきたい」という話はよく聞くので、そういうシーンにハマるよいアップデートだと思います。欲を言えば、特定アカウントxリージョンの全リソースのimportとresourceブロックを一撃で書いてくれるとさらに嬉しいですが…

参考情報

この記事をシェアする

関連記事