はじめに
業務でTerraformを使っていると、「Terraform管理外のリソースたち」に遭遇することがあります。
PoCで作成したものがそのまま残っていたり
、やむを得ず手動で作成したリソースが放置されたり
…。
Terraformで管理したい気持ちはあっても、気づけば放置されている可哀そうなサーバだったり
…。
このような正体不明なリソースは、
リリース済みのシステムにアサインされると意外によく見かけます。
こういったリソースに対して運用改善やコスト削減といった名目で発生するタスクは、
プロジェクトの新規参画者に割り振られる事が往々にしてあります。
そして全く知らないシステムのリソースをimportする羽目になり困惑することもしばしば...。
勉強にはなるので、悪い事ばかりではないですが...。
そんな困った状況に陥ったTerraformユーザーに向けて、
Terraform管理外のリソースをどのように扱うべきかについて解説します。
本記事はあくまでリソースの大まかな調査方法や、一般的な対処方法をまとめた記事です。
最終的な判断は必ず現場で確認してください。
事前確認
初めに調査対象のリソースの要否と用途を確認しましょう。
要否レベルで不明なリソースはまずないと思いますが、
設計書などのドキュメントでの確認
、先輩たちへの聞き込み調査
、構築時のログなどでの履歴確認
…etc…。
どんな方法でもいいので、最低限以下の事項を確認してリソースの正体を解き明かしましょう。
リソースは現在のシステムで必要か?
➤ YES(例:重要な役割を持っている、システムの一部として利用されている)
➤ NO (例:不要、または役割が不明)
リソースは現在どのように管理されているか?
➤ 手動で管理 (例:手動で作成した、または外部ツールから管理されている)
➤ 他ツールで管理 (例:別のIacツール、カスタムスクリプトなど)
リソースをTerraformで管理する必要があるか?
➤ YES(例:Terraformで統一的に管理したい、運用の効率化を図りたい)
➤ NO (例:現在の管理方法で問題ない、または運用に影響がない)
最終的な判断は現場によりますが、
上記の確認を踏まえて、リソースの処遇は大まかに以下の3パターンに分かれるでしょう。
1.リソースを削除する
リソースに以下のような経緯がある場合、削除することが多いでしょう。
- PoC検証時の残骸
- 障害復旧などで一時的に必要になったリソースの残骸
- 移行完了後の旧リソース
言うまでもないですが、削除間違い
にだけは気をつけてください。
また他のリソースとの依存関係
の有無をよく確認した上で削除しましょう。
なお大抵の場合はCLIツールが用意されているので、確実性を上げるためにも利用を検討してください。
2.DataSourcesとして定義する
リソースが以下のような用途の場合、DataSources(データソース)として定義することが多いでしょう。
ただ基本的に構築段階で対応しているべき内容なので、このパターンはまずないと思います。
- パスワード、SSH鍵などのシークレット情報
- 共通の設定を維持するために、既存のリソースの情報を参照
- 既存の設定や状態を確認のために情報を取得する
以下は一例ですが、
AWS CLI でシークレットを作成してdataブロックにて指定し、
resourceブロックにてRDSのAdminユーザー名/パスワードとして参照しています。
$ aws secretsmanager create-secret \
--name db-secret \
--secret-string '{"username":"dbuser","password":"dbpassword"}'
provider "aws" {
region = "ap-northeast-1"
}
data "aws_secretsmanager_secret" "main" {
name = "db-secret"
}
data "aws_secretsmanager_secret_version" "main" {
secret_id = data.aws_secretsmanager_secret.main.id
}
locals {
secret_data = jsondecode(data.aws_secretsmanager_secret_version.main.secret_string)
db_username = local.secret_data.username
db_password = local.secret_data.password
}
resource "aws_db_instance" "main" {
# シークレットマネージャーの値を参照
username = local.db_username
password = local.db_password
identifier = "postgres-rds-instance"
instance_class = "db.t3.micro"
engine = "postgres"
engine_version = "16.4"
allocated_storage = 20
db_name = "testdatabase"
final_snapshot_identifier = false
}
この状態でterraform applyすることでDataSourcesとしてtfstateに追加され、
以下コマンドで一覧を確認できます。
$ terraform state list
data.aws_secretsmanager_secret.main
data.aws_secretsmanager_secret_version.main
aws_db_instance.main
なお逆にDataSourcesを削除する際に、
dataブロックを削除してterraform applyしてもtfstateから削除されないことがあります。
そういった場合はterraform state rm
で指定することでtfstateから削除できます。
ただし削除前に必ず他のresourceから参照されていないことを確認してください。
$ terraform state rm data.aws_secretsmanager_secret.main
DataSourcesの定義方法は公式ドキュメントに記載があるので、
詳細は以下で確認してください。
3.Terraform管理下にする
以下のようなリソースの場合、importしてTerraform管理下にすることになるでしょう。
何だかんだこのパターンが一番遭遇率が高いと思います。
- 初期開発時の手動作成リソース
- terraform導入前に作成したリソース
またいくつか方法があるので、それぞれのメリット/デメリットを合わせて解説します。
例として、AWS CLIで作成したEC2インスタンスをimportする流れを記載します。
(1).importコマンド
importコマンドは指定したリソースの定義情報をtfstateに追加します。
初期のTerraformから実装されている機能のためバージョン問わず利用できますが、
事前の差分確認ができず、またresourceブロックの手動修正が必要となるため可能であれば後述のimportブロックの利用を推奨します。
・Terraform標準の方法
Terraformに組み込まれている公式の手段であり、追加のツールが不要
・手動での精密な制御
インポートするリソースの詳細を指定でき、ターゲットを明確に制御できる
・実行前に差分確認ができない
コマンド実行前にtfstateの差分が確認できない
・resourceブロックの自動生成機能がない
resourceブロックの生成機能はないため、コマンド実行後は差分が消えるまで修正し続ける必要がある
・複数リソースの一括インポートに不向き
リソースを一つずつインポートするため、インポート対象が多い場合はコマンドを個別に実行する必要があり手間がかかる
① インポート用リソース作成
AWS CLI でEC2インスタンスを作成します。
# AmazonLinux2023の最新AMIを取得
$ TARGET_AMI=$(aws ssm get-parameters \
--names "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64" \
--region ap-northeast-1 \
--query "Parameters[*].Value" \
--output text
)
$ aws ec2 run-instances \
--image-id $TARGET_AMI \
--instance-type t3.micro
② resourceブロック作成
インポート対象リソース用のresourceブロックを定義します、
なおimportコマンド後に手動修正するため、この時点では設定値の記載は不要です。
resource "aws_instance" "main" {
}
③ importコマンド実行
importコマンドは以下の引数を指定します。
$ terraform import [resource type].[resource name] [import対象リソースのID]
EC2の場合はインスタンスIDを指定します。
importコマンド実行後にImport successful!
と出力されればtfstateにインポートできています。
$ terraform import aws_instance.main i-123456789123456789
```console:実行結果
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
tfstateを見るとインポート対象が追加されていることが確認できます。
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "main",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
④ resourceブロック修正
terraform planで差分を確認してresourceブロックを修正します。
以下の実行結果から、aws_instanceはami
、instance_type
が必須パラメータのため定義する必要があることがわかります。
$ terraform plan
╷
│ Error: Missing required argument
│
│ with aws_instance.main,
│ on main.tf line 5, in resource "aws_instance" "main":
│ 5: resource "aws_instance" "main" {
│
│ "launch_template": one of `ami,instance_type,launch_template` must be specified
╵
╷
│ Error: Missing required argument
│
│ with aws_instance.main,
│ on main.tf line 5, in resource "aws_instance" "main":
│ 5: resource "aws_instance" "main" {
│
│ "ami": one of `ami,launch_template` must be specified
╵
╷
│ Error: Missing required argument
│
│ with aws_instance.main,
│ on main.tf line 5, in resource "aws_instance" "main":
│ 5: resource "aws_instance" "main" {
│
│ "instance_type": one of `instance_type,launch_template` must be specified
╵
以下のようにresourceブロックに定義を追加します。
resource "aws_instance" "main" {
ami = "ami-00c79d83cf718a893"
instance_type = "t3.micro"
}
再度差分を確認をして、No changes.
と出力されればインポート完了です。
ここで差分が発生する場合は、差分が無くなるまでresourceブロックを修正します。
$ terraform plan
aws_instance.main: Refreshing state... [id=i-00c5ef73e12b18394]
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.
(2).importブロック
importブロックはTerraform v1.5.0 で実装された機能であり、
importコマンドに比べメリットが多いため利用できる場合はこちらをおすすめします。
・事前の差分確認
事前にどのようなリソースがインポートされるか差分を確認できる
・複数のリソースのインポート
importブロックにて複数のリソースを定義できるため、一括でインポートができる
・Terraform標準の方法
Terraformに組み込まれている公式の手段であり、追加のツールが不要
・利用できるバージョンの制約
Terraform v1.5.0 から利用可能であるため、プロジェクトによっては利用できない場合がある
importコマンドの場合はリソースの定義情報がtfstate上にしか追加されないため、resourceブロックは手動で修正する必要がありますが、
importブロックはresourceブロックも自動で生成できます。
またimportコマンドはリソースごとに個別にインポートする必要がありますが、importブロックは複数のリソースをまとめてインポートすることができます。
なおimportブロックの使用方法として以下の二通りあるので、それぞれ解説します。
1.resourceブロックを自動で生成する
2.resourceブロックを事前に作成する
1.resourceブロックを自動で生成する
① インポート用リソース作成
importコマンドの例と同様に、AWS CLIでEC2インスタンスを作成します。
また複数リソースのインポートの例として、二つ作成します。
② importブロック作成
importブロックは以下のようにインポートするリソースごとに定義します。
またid
とto
は必須となるため対象に合わせて記載してください、
値自体はimportコマンドの引数で指定したものと同様です。
import {
id = "i-123456789123456789"
to = aws_instance.web
}
import {
id = "i-987654321987654321"
to = aws_instance.ap
}
なおこの際に必須になる引数の記載例は公式ドキュメントで確認できます。
EC2インスタンスの場合は以下に記載があります。
③ リソース定義 インポート
terraform planに-generate-config-out=[ファイル名].tf
という引数を付けて実行することで、
resourceブロックが記述されたファイルが生成されます。
$ terraform plan -generate-config-out=main.tf
aws_instance.web: Preparing import... [id=i-123456789123456789]
aws_instance.ap: Preparing import... [id=i-987654321987654321]
aws_instance.web: Refreshing state... [id=i-123456789123456789]
aws_instance.ap: Refreshing state... [id=i-987654321987654321]
なおファイル生成後に自動的にterraform planが実施されます。
EC2インスタンスの場合はresourceブロック内のipv6_addresses
とipv6_address_count
が競合してしまうためエラーが発生します。
Planning failed. Terraform encountered an error while generating this plan.
╷
│ Warning: Config generation is experimental
│
│ Generating configuration during import is currently experimental, and the generated configuration format may change in future versions.
╵
╷
│ Error: Conflicting configuration arguments
│
│ with aws_instance.ap,
│ on main.tf line 14:
│ (source code not available)
│
│ "ipv6_address_count": conflicts with ipv6_addresses
╵
╷
│ Error: Conflicting configuration arguments
│
│ with aws_instance.ap,
│ on main.tf line 15:
│ (source code not available)
│
│ "ipv6_addresses": conflicts with ipv6_address_count
╵
そのためどちらかをコメントアウトします、
ipv6をそもそも利用していないようであれば両方コメントアウトして問題ありません。
# ipv6_address_count = 0
# ipv6_addresses = []
その後再度terraform planを実行し、import
以外の差分が発生しないことを確認します。
$ terraform plan
Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.
④ tfstate インポート
terraform applyコマンドを実行してtfstateにリソースの定義情報を追加します。
$ terraform apply
aws_instance.web: Importing... [id=i-123456789123456789]
aws_instance.web: Import complete [id=i-123456789123456789]
aws_instance.ap: Importing... [id=i-987654321987654321]
aws_instance.ap: Import complete [id=i-987654321987654321]
Apply complete! Resources: 2 imported, 0 added, 0 changed, 0 destroyed.
コマンド実行後、tfstateにリソースの定義情報が追加されていることが確認できます。
またこの段階でimportブロックは不要となるため削除して問題ありません。
- import {
- id = "i-123456789123456789"
- to = aws_instance.web
- }
- import {
- id = "i-987654321987654321"
- to = aws_instance.ap
- }
2.resourceブロックを事前に作成する
importブロック内のto
に指定したresourceブロックが記述されたtfファイルが存在する場合、
tfファイルの生成を行わず、既存のresourceブロックとid
で指定したリソースを紐づけることが出来ます。
① インポート用リソース作成
importコマンドの例と同様に、AWS CLIでEC2インスタンスを作成します。
また複数リソースのインポートの例として、二つ作成します。
② importブロック、resourceブロックの作成
importブロック、resourceブロックをそれぞれ以下のように定義します。
import {
id = "i-123456789123456789"
to = aws_instance.web
}
import {
id = "i-987654321987654321"
to = aws_instance.ap
}
resource "aws_instance" "web" {
ami = "ami-00c79d83cf718a893"
instance_type = "t3.micro"
}
resource "aws_instance" "ap" {
ami = "ami-00c79d83cf718a893"
instance_type = "t3.micro"
}
③ インポートの実行
terraform applyコマンドを実行してtfstateにリソースの定義情報を追加します。
$ terraform apply
aws_instance.web: Importing... [id=i-123456789123456789]
aws_instance.web: Import complete [id=i-123456789123456789]
aws_instance.ap: Importing... [id=i-987654321987654321]
aws_instance.ap: Import complete [id=i-987654321987654321]
Apply complete! Resources: 2 imported, 0 added, 0 changed, 0 destroyed.
コマンド実行後、tfstateにリソースの定義情報が追加されていることが確認できます。
またこの段階でimportブロックは不要となるため削除して問題ありません。
- import {
- id = "i-123456789123456789"
- to = aws_instance.web
- }
- import {
- id = "i-987654321987654321"
- to = aws_instance.ap
- }
(3).terraformer
terraformerはGoogle Cloud PlatformがOSSとして提供しているツールであり、
指定した環境内のリソースを読み取って定義情報をtfファイルとして出力します。
アカウント内の広い範囲をインポートできるため、手動で構築したシステムを丸ごとterraform管理化したい場合などに有用です。
・tfファイルの自動生成
tfファイルを自動で生成できるため、手動で設定を書く手間が省ける
・リソースの一括インポート
複数のリソースを一度にインポートできるため、大規模なインフラの移行や管理が効率化される
・インポート対象の指定
環境内のリソースを一括でインポートするため、指定を誤るとデフォルトのリソースなどもインポートされてしまう場合がある
・外部ツールの依存
Terraform本体ではなく外部ツールを使用するため、ツールのバージョンや互換性に注意が必要
① terraformer インストール
以下を参照して、実行環境に合わせてインストールしてください。
個人的にはmiseでインストールするのが楽なのでおすすめします。
$ mise use terraformer@latest
② terraform init
プロバイダー情報を記載したtfファイルを作成し、terraform initで初期化します。
provider "aws" {
region = "ap-northeast-1"
}
$ terraform init
③ terraformer import
terraformer importでtfファイル、tfstateを生成します。
コマンドは以下のように引数を指定します。
$ terraformer import [プロバイダー] --resources=[インポート対象リソース]
また--resources=
にて指定できるリソースは以下コマンドで確認できます。
$ terraformer import [プロバイダー] list
$ terraformer import aws list
例として環境内のEC2インスタンス、VPC、サブネットをすべて指定する場合は以下の通りです。
$ terraformer import aws --resources=ec2_instance,vpc,subnet
2024/09/01 22:08:32 aws importing default region
2024/09/01 22:08:33 aws importing... ec2_instance
2024/09/01 22:08:35 aws done importing ec2_instance
2024/09/01 22:08:35 aws importing... vpc
2024/09/01 22:08:35 aws done importing vpc
2024/09/01 22:08:35 aws importing... subnet
2024/09/01 22:08:35 aws done importing subnet
2024/09/01 22:08:35 Number of resources for service ec2_instance: 2
2024/09/01 22:08:35 Number of resources for service vpc: 1
2024/09/01 22:08:35 Number of resources for service subnet: 3
2024/09/01 22:08:35 Refreshing state... aws_subnet.tfer--subnet-XXXXXXXXXXXXXXXXX
2024/09/01 22:08:35 Refreshing state... aws_subnet.tfer--subnet-XXXXXXXXXXXXXXXXX
2024/09/01 22:08:35 Refreshing state... aws_instance.tfer--i-XXXXXXXXXXXXXXXXX
2024/09/01 22:08:35 Refreshing state... aws_instance.tfer--i-XXXXXXXXXXXXXXXXX
2024/09/01 22:08:35 Refreshing state... aws_subnet.tfer--subnet-XXXXXXXXXXXXXXXXX
2024/09/01 22:08:35 Refreshing state... aws_vpc.tfer--vpc-XXXXXXXXXXXXXXXXX
2024/09/01 22:08:37 Filtered number of resources for service vpc: 1
2024/09/01 22:08:37 Filtered number of resources for service subnet: 3
2024/09/01 22:08:37 Filtered number of resources for service ec2_instance: 2
2024/09/01 22:08:37 aws Connecting....
2024/09/01 22:08:37 aws save subnet
2024/09/01 22:08:37 aws save tfstate for subnet
2024/09/01 22:08:37 aws save ec2_instance
2024/09/01 22:08:37 aws save tfstate for ec2_instance
2024/09/01 22:08:37 aws save vpc
2024/09/01 22:08:37 aws save tfstate for vpc
コマンドの実行に成功するとgeneratedディレクトリが生成されます。
generated
>プロバイダー
>リソース
という構造になっており、リソースの種別ごとにtfファイル、tfstateファイルが作成されます。
なお--resources=
引数の指定によっては想定外のリソースがインポートされることがあるので、
具体的にどのリソースがインポートされたか各リソースのtfファイル、tfstateを確認することをおすすめします。
.
├── generated
│ └── aws
│ ├── ec2_instance
│ │ ├── instance.tf
│ │ ├── outputs.tf
│ │ ├── provider.tf
│ │ ├── terraform.tfstate
│ │ └── variables.tf
│ ├── subnet
│ │ ├── outputs.tf
│ │ ├── provider.tf
│ │ ├── subnet.tf
│ │ ├── terraform.tfstate
│ │ └── variables.tf
│ └── vpc
│ ├── outputs.tf
│ ├── provider.tf
│ ├── terraform.tfstate
│ └── vpc.tf
└── provider.tf
その他リソースの指定方法については、
公式ドキュメントに使用例が記載されているのでご確認ください。
参考ドキュメント
本記事の作成に際して、以下のドキュメントを参照させていただきました。
この場を借りてお礼申し上げます。