Terraform Queryコマンドで既存リソースを楽に一括インポート
もう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ファイルで構いません。
terraform {
required_version = ">= 1.14.9"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.42"
}
}
}
provider "aws" {
region = "us-east-1"
}
Listブロック
以下は既存EC2インスタンスを取得する例です。
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__ 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ブロックでリソースアドレスを変更する- リソース名をわかりやすいものに変える、グループ毎にchild module化するなど
- 参考: Refactor modules | Terraform | HashiCorp Developer
落穂拾い
listブロックとData Source(dataブロック)はなにが違うの?
「既存リソースの情報をTerraformの中で使う」という意味だと、Data Source(dataブロック)があります。これとlistブロックの違いは何でしょうか? → 目的が異なります。
- listブロック → 既存リソースをTerraform管理下に置きたいときに使う
- 今後Terraform上で更新(設定値の変更や削除など)が可能になる
- Data Source(dataブロック) → 既存リソースの情報を参照したいときに使う
- そのリソース自体の更新は不可。情報参照するだけ
また、使われるタイミングが異なります。 listブロックは terraform queryを実行した際に読まれますが、通常のTerraformワークフローつまり terraform planやterraform apply時には無関係です。一方Data Sourceは逆で、terraform queryでは使われない(.tfquery.hcl拡張子のファイルにData Sourceを書くとエラーになりました)ですが、 terraform planやterraform 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ブロックを一撃で書いてくれるとさらに嬉しいですが…
参考情報
- Import existing resources in bulk | Terraform | HashiCorp Developer
- Query files | Terraform | HashiCorp Developer
- list block reference for `.tfquery.hcl` files | Terraform | HashiCorp Developer
- Use search to find and import existing resources to state | Terraform | HashiCorp Developer
- aws_instance | List Resources | hashicorp/aws | Terraform | Terraform Registry
- aws_lb | List Resources | hashicorp/aws | Terraform | Terraform Registry
- aws_iam_role | List Resources | hashicorp/aws | Terraform | Terraform Registry
- aws_s3_bucket | List Resources | hashicorp/aws | Terraform | Terraform Registry
- describe-instances — AWS CLI 2.34.39 Command Reference
- Terraform version 1.5の新機能達を使ってみた | DevelopersIO
- import block reference | Terraform | HashiCorp Developer
- Refactor modules | Terraform | HashiCorp Developer





