情報畑でつかまえてロゴ
本サイトは NTTテクノクロスが旬の IT をキーワードに
IT 部門が今知っておきたい最新テクノロジーに関する情報をお届けするサイトです

Terraform連載 第7回:importブロックはmoduleのリソースも取り込めるか?

Terraform連載企画!今回はimportブロックを用いたモジュールへのインポートについて検証してみます

terraform_banner.png

はじめに

こんにちは、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のリソースも取り込めるか?」

連載第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の導入にあたってお悩みの方は、ぜひお気軽にご相談ください。

HashiCorp製品サポートサービス(弊社ホームページ)

連載シリーズ
著者プロフィール
岩瀬 正太郎
岩瀬 正太郎

NTTテクノクロス株式会社
ⅠOWNデジタルツイン事業部
第三ビジネスユニット