Terraform連載 第7回:importブロックはmoduleのリソースも取り込めるか?
Terraform連載企画!今回はimportブロックを用いたモジュールへのインポートについて検証してみます
Terraform連載
- 2024年01月23日公開
はじめに
こんにちは、NTTテクノクロスの岩瀨です。
今回は、自分が業務上必要になって調べたことを紹介する形で記事を書いてみようと思います。
※なお、本記事はv1.7リリース(2024/1/17)以前に執筆した記事です。
関連記事:
「Terraform連載 第1回:いまさら聞けない、IaCってなに?~Terraform、IaSQLの紹介~」
「Terraform連載 第2回:Terraform v1.5の紹介&活用方法について考えてみた」
「Terraform連載 第3回:Terraform v1.6のtestコマンドについてご紹介」
「Terraform連載 第4回:for_eachの使い方」
「Terraform連載 第5回:module(モジュール)の紹介」
「Terraform連載 第6回:Terraform v1.7 removedブロックの紹介」
「Terraform連載 第7回:importブロックはmoduleのリソースも取り込めるか?」
「Terraform連載 第8回:Terraformの管理スコープとしてiDRACをimportしてみた」
連載第2回で取り上げたimportブロックは既存のリソースをTerraform管理に取り込むことができる(コードも自動生成可能な)機能、第5回で取り上げたmoduleは複数のリソースをまとめる事ができる機能です。
Terraformで作成できるAWSリソースは非常に多くありますが、全部には対応できていません。また、新しいAWSサービスが登場した直後も未対応の場合が多く、最初のうちはマネジメントコンソールから手動で作成する必要がある、といったケースもあります。
そこで「手動で作成したAWSリソースを後からTerraformへ取り込む際に、モジュールのリソースとして取り込むことができるか?」が気になり、検証を行いました。コンポーネントや機能などの単位でモジュールを活用することは往々にしてありますので、これができると自由度もかなり上がってくると思います。
なお検証ではTerraform v1.7.0-rc1を利用しています(主題から少し外れますが、1.7で追加される機能もついでに検証しているためです)。
$ terraform --version
Terraform v1.7.0-rc1
on linux_amd64
+ provider registry.terraform.io/hashicorp/aws v5.31.0
-generate-config-outはmoduleのコード生成には対応していない模様
Terraform公式サイト:https://developer.hashicorp.com/terraform/language/import/generating-configuration
まず-generate-config-out
オプションを使ったコードの生成で、モジュール側のコードを作ることはできないようでした。
検証用に、下記のようなディレクトリ構成を用意しました。
.
|-- modules/
| `-- network-from-import/
`-- environments/
|-- import_to_module_with_config_out.tf
`-- providers.tf
import_to_module_with_config_out.tf
にはimport
ブロックのコードを実装しています。
# import_to_module_with_config_out.tf
module "network" {
source = "../../modules/network-from-import"
}
import {
to = module.network.aws_vpc.main
id = "vpc-0cb81db209d65b802"
}
この状態でルートモジュール側(environmentsディレクトリ)からterraform plan -generate-config-out
を実行すると、以下のエラーが発生してしまいました。
$ terraform init
(略)
$ terraform plan -generate-config-out=../../modules/network-from-import/vpc.tf
╷
│ Error: Configuration for import target does not exist
│
│ on import_to_module_with_config_out.tf line 8, in import:
│ 8: to = module.network.aws_vpc.main
│
│ The configuration for the given import target module.network.aws_vpc.main does not exist. All target instances must have an associated configuration to be imported.
╵
モジュール側にインポートのターゲットが無いといったエラーメッセージが出ており、未対応のようです。 -generate-config-out
は、まだexperimental feature(実験的な機能)の位置づけのようです。今後対応してくれるとうれしいですね。
モジュールのコードを用意すればモジュールへインポートできる
-generate-config-out
オプションでのコードの生成はモジュールには対応していませんが、import
ブロック自体はモジュールへの取り込みに対応している模様です。 つまりモジュールのコードを用意してやればOKみたいなので、実機検証をしてみます。
前提条件
今回の実機検証では、連載第6回で紹介したremovedブロックの検証直後の状態から開始してみます。 第6回では、networkモジュールで作成したリソース(VPCとサブネット)をTerraform管理外にしました。今回は「Terraform管理から外したリソースを、再度Terraformに取り込む」という事をやってみたいと思います。
環境準備:コード作成
検証環境を以下のように変更しました。
.
|-- modules/
| `-- network/
| |-- outputs.tf
| |-- providers.tf
| |-- variables.tf
| `-- vpc-subnet.tf
`-- environments/
|-- import_to_nw_module.tf
|-- main_tokyo.tf
`-- providers.tf
networkモジュール、およびルートモジュールのmain_tokyo.tf
は、連載第5回で利用したものと同一です。
import_to_nw_module.tf
にはimport
ブロックのコードが書かれています。 書き方にはいくつかパターンがあるので紹介してみます。
パターン1:べた書き
import {
to = module.network_tokyo.aws_subnet.main["public-1a"]
id = "subnet-0775ae40a9cfbb5ed"
}
import {
to = module.network_tokyo.aws_subnet.main["public-1c"]
id = "subnet-016a525e2dad4e550"
}
import {
to = module.network_tokyo.aws_subnet.main["private-1a"]
id = "subnet-0be701229eb362c3e"
}
import {
to = module.network_tokyo.aws_subnet.main["private-1c"]
id = "subnet-0533d0ef9c35db070"
}
import {
to = module.network_tokyo.aws_vpc.main
id = "vpc-0d2c354e13eb5ce19"
}
最も単純な書き方で、リソースとimport
ブロックの数が1:1の関係になります。 インポート対象のVPC ID、サブネットIDは、マネジメントコンソールなどから値を転記する必要もあります。少数のリソースをインポートする場合はあまり問題にはなりませんが、多数インポートする場合は書くのがとても大変になり、あまり推奨できません。 またIDが異なる環境(別のAWSアカウントやリージョン)に持っていっても使えない=再利用性が低いという欠点もあります(が、Terraform v1.5ではそもそもid
の値は文字列で指定する必要があったので、「以前はこういう書き方しかできなかった」のも事実です)。
パターン2:dataソースの活用+for_eachでサブネットをまとめてインポート
# import_to_nw_module.tf
data "aws_vpc" "tokyo-dev" {
filter {
name = "tag:Name"
values = ["tokyo-dev"]
}
}
data "aws_subnets" "tokyo-dev" {
filter {
name = "vpc-id"
values = [data.aws_vpc.tokyo-dev.id]
}
}
data "aws_subnet" "main" {
for_each = toset(data.aws_subnets.tokyo-dev.ids)
id = each.value
}
locals {
subnets = { for id, tag_key in data.aws_subnet.main : id => replace(tag_key.tags.Name, "${data.aws_vpc.tokyo-dev.tags.Name}-", "") }
}
import {
for_each = local.subnets
id = each.key
to = module.network_tokyo.aws_subnet.main[each.value]
}
import {
to = module.network_tokyo.aws_vpc.main
id = data.aws_vpc.tokyo-dev.id
}
import
ブロックが登場したv1.5の時点ではid
は文字列リテラルで指定する必要がありました(=つまり変数等の値を使うことができなかった)。しかしv1.6から参照式を使用可能になりました(※GitHub)ので、これを利用してdataソースで参照したVPCおよびサブネットの情報からID値を参照するように実装しています。上記例では、VPCはNameタグが一致するもの、サブネットはVPC IDに紐づいたものをそれぞれ取得しています。このように一意に対象が特定できる、かつ環境に左右されない属性値を持っている場合は、dataソースを使うことで再利用性の高いコードにすることができます。
またv1.7ではimportブロックでのfor_eachサポートが追加される予定(※GitHub)です。今回はこれを使って、サブネット4つを1つのimportブロックでまとめて取り込んでいます。このためサブネット数が増えてもコードはこのまま利用できるため、拡張性も確保しています。
localsのsubnets
MAPは、サブネットのNameタグからnetworkモジュール用のキー値を生成し、サブネットIDと紐づけるために用意しています。networkモジュールではサブネットをfor_each
で作成しており、キー値としてpublic-1a
のような値を用いています(実装は連載第5回の記事を参照)。一方で作成したサブネットのNameタグは[VPCのNameタグ]-public-1a
の命名のため、import
ブロックのfor_each
のキー値として利用できません。そのためreplace()
関数を使ってVPCのNameタグ部分の文字列を除去し、subnets
MAPに格納することで対処しています。 最終的にsubnets
MAPには以下のような値が格納されます(import
ブロックのfor_each
でループされ、それぞれeach.key
, each.value
の値として使われます)。
+ subnets = {
+ subnet-016a525e2dad4e550 = "public-1c"
+ subnet-0533d0ef9c35db070 = "private-1c"
+ subnet-0775ae40a9cfbb5ed = "public-1a"
+ subnet-0be701229eb362c3e = "private-1a"
}
インポート実行
terraform plan
を実行して実行計画を表示します。
$ terraform init
(略)
$ terraform plan
module.network_tokyo.aws_vpc.main: Preparing import... [id=vpc-0d2c354e13eb5ce19]
module.network_tokyo.aws_vpc.main: Refreshing state... [id=vpc-0d2c354e13eb5ce19]
module.network_tokyo.aws_subnet.main["private-1c"]: Preparing import... [id=subnet-0533d0ef9c35db070]
module.network_tokyo.aws_subnet.main["private-1a"]: Preparing import... [id=subnet-0be701229eb362c3e]
module.network_tokyo.aws_subnet.main["public-1a"]: Preparing import... [id=subnet-0775ae40a9cfbb5ed]
module.network_tokyo.aws_subnet.main["public-1c"]: Preparing import... [id=subnet-016a525e2dad4e550]
module.network_tokyo.aws_subnet.main["public-1c"]: Refreshing state... [id=subnet-016a525e2dad4e550]
module.network_tokyo.aws_subnet.main["public-1a"]: Refreshing state... [id=subnet-0775ae40a9cfbb5ed]
module.network_tokyo.aws_subnet.main["private-1a"]: Refreshing state... [id=subnet-0be701229eb362c3e]
module.network_tokyo.aws_subnet.main["private-1c"]: Refreshing state... [id=subnet-0533d0ef9c35db070]
Terraform will perform the following actions:
# module.network_tokyo.aws_subnet.main["private-1a"] will be imported
resource "aws_subnet" "main" {
arn = "arn:aws:ec2:ap-northeast-1:378493308328:subnet/subnet-0be701229eb362c3e"
assign_ipv6_address_on_creation = false
availability_zone = "ap-northeast-1a"
availability_zone_id = "apne1-az4"
cidr_block = "172.16.0.64/26"
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-0be701229eb362c3e"
ipv6_native = false
map_customer_owned_ip_on_launch = false
map_public_ip_on_launch = false
owner_id = "378493308328"
private_dns_hostname_type_on_launch = "ip-name"
tags = {
"Name" = "tokyo-dev-private-1a"
}
tags_all = {
"Name" = "tokyo-dev-private-1a"
}
vpc_id = "vpc-0d2c354e13eb5ce19"
}
# module.network_tokyo.aws_subnet.main["private-1c"] will be imported
resource "aws_subnet" "main" {
arn = "arn:aws:ec2:ap-northeast-1:378493308328:subnet/subnet-0533d0ef9c35db070"
assign_ipv6_address_on_creation = false
availability_zone = "ap-northeast-1c"
availability_zone_id = "apne1-az1"
cidr_block = "172.16.0.192/26"
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-0533d0ef9c35db070"
ipv6_native = false
map_customer_owned_ip_on_launch = false
map_public_ip_on_launch = false
owner_id = "378493308328"
private_dns_hostname_type_on_launch = "ip-name"
tags = {
"Name" = "tokyo-dev-private-1c"
}
tags_all = {
"Name" = "tokyo-dev-private-1c"
}
vpc_id = "vpc-0d2c354e13eb5ce19"
}
# module.network_tokyo.aws_subnet.main["public-1a"] will be imported
resource "aws_subnet" "main" {
arn = "arn:aws:ec2:ap-northeast-1:378493308328:subnet/subnet-0775ae40a9cfbb5ed"
assign_ipv6_address_on_creation = false
availability_zone = "ap-northeast-1a"
availability_zone_id = "apne1-az4"
cidr_block = "172.16.0.0/26"
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-0775ae40a9cfbb5ed"
ipv6_native = false
map_customer_owned_ip_on_launch = false
map_public_ip_on_launch = false
owner_id = "378493308328"
private_dns_hostname_type_on_launch = "ip-name"
tags = {
"Name" = "tokyo-dev-public-1a"
}
tags_all = {
"Name" = "tokyo-dev-public-1a"
}
vpc_id = "vpc-0d2c354e13eb5ce19"
}
# module.network_tokyo.aws_subnet.main["public-1c"] will be imported
resource "aws_subnet" "main" {
arn = "arn:aws:ec2:ap-northeast-1:378493308328:subnet/subnet-016a525e2dad4e550"
assign_ipv6_address_on_creation = false
availability_zone = "ap-northeast-1c"
availability_zone_id = "apne1-az1"
cidr_block = "172.16.0.128/26"
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-016a525e2dad4e550"
ipv6_native = false
map_customer_owned_ip_on_launch = false
map_public_ip_on_launch = false
owner_id = "378493308328"
private_dns_hostname_type_on_launch = "ip-name"
tags = {
"Name" = "tokyo-dev-public-1c"
}
tags_all = {
"Name" = "tokyo-dev-public-1c"
}
vpc_id = "vpc-0d2c354e13eb5ce19"
}
# module.network_tokyo.aws_vpc.main will be imported
resource "aws_vpc" "main" {
arn = "arn:aws:ec2:ap-northeast-1:378493308328:vpc/vpc-0d2c354e13eb5ce19"
assign_generated_ipv6_cidr_block = false
cidr_block = "172.16.0.0/16"
default_network_acl_id = "acl-077dc4cc4dbab14e7"
default_route_table_id = "rtb-08b07b0493bf5d59f"
default_security_group_id = "sg-05f0ec3fc619da72e"
dhcp_options_id = "dopt-0a60d61f51584c104"
enable_dns_hostnames = false
enable_dns_support = true
enable_network_address_usage_metrics = false
id = "vpc-0d2c354e13eb5ce19"
instance_tenancy = "default"
ipv6_netmask_length = 0
main_route_table_id = "rtb-08b07b0493bf5d59f"
owner_id = "378493308328"
tags = {
"Name" = "tokyo-dev"
}
tags_all = {
"Name" = "tokyo-dev"
}
}
Plan: 5 to import, 0 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.
結果が5 to import
となっており、5つのリソースをインポートすることを示しています。問題ないので、このままterraform apply
でインポートします。
$ terraform apply
(略)
Plan: 5 to import, 0 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
module.network_tokyo.aws_vpc.main: Importing... [id=vpc-0d2c354e13eb5ce19]
module.network_tokyo.aws_vpc.main: Import complete [id=vpc-0d2c354e13eb5ce19]
module.network_tokyo.aws_subnet.main["private-1a"]: Importing... [id=subnet-0be701229eb362c3e]
module.network_tokyo.aws_subnet.main["private-1a"]: Import complete [id=subnet-0be701229eb362c3e]
module.network_tokyo.aws_subnet.main["private-1c"]: Importing... [id=subnet-0533d0ef9c35db070]
module.network_tokyo.aws_subnet.main["private-1c"]: Import complete [id=subnet-0533d0ef9c35db070]
module.network_tokyo.aws_subnet.main["public-1c"]: Importing... [id=subnet-016a525e2dad4e550]
module.network_tokyo.aws_subnet.main["public-1c"]: Import complete [id=subnet-016a525e2dad4e550]
module.network_tokyo.aws_subnet.main["public-1a"]: Importing... [id=subnet-0775ae40a9cfbb5ed]
module.network_tokyo.aws_subnet.main["public-1a"]: Import complete [id=subnet-0775ae40a9cfbb5ed]
Apply complete! Resources: 5 imported, 0 added, 0 changed, 0 destroyed.
正常終了しました。terraform state list
からも、対象リソースがモジュールのリソースとなっていることがわかります。
$ terraform state list
module.network_tokyo.aws_subnet.main["private-1a"]
module.network_tokyo.aws_subnet.main["private-1c"]
module.network_tokyo.aws_subnet.main["public-1a"]
module.network_tokyo.aws_subnet.main["public-1c"]
module.network_tokyo.aws_vpc.main
まとめ
今回はimport
ブロックを用いたモジュールへのインポートについて検証しました。検証のスタート地点は、自分の業務で「こういう使い方できるんだっけ??」と疑問に思ったことでしたが、期待通りの結果が得られたことに個人的にも満足です。 ついでにv1.6で追加されたid
の参照式対応と、v1.7で追加されるimport
ブロックのfor_each
について検証しました。import
ブロックが登場したv1.5と比べると、非常に実用的になってきたなと感じます。-generate-config-out
オプションはまだまだ発展途上な印象ですが、今後どのように発展していくのか、楽しみです。
お読みいただきありがとうございました。
IOWNデジタルツイン事業部では、Terraform EnterpriseおよびTerraform Cloud Plusに関する、日本語による保守サポートをご提供しております。Terraformの導入にあたってお悩みの方は、ぜひお気軽にご相談ください。
Terraformの導入にあたってお悩みの方へ
NTTテクノクロス株式会社
ⅠOWNデジタルツイン事業部
第三ビジネスユニット