
TerraformでIAM系リソースの一括インポート
Terraformで、多数のIAM系リソースのインポートを行う機会がありましたので、その際の手順などを記載します。
経緯
あるAWSアカウントで、全てWeb Consoleで設定作業がされていたため、IaC化を進めていくこととなりました。今回は、IAM系のリソースをTerraformでIaC化を行うこととしましたが、多数の設定が存在し、それらのimport定義を作成するのはとても大変なため、スクリプトを生成AIで準備して行いました。
(以下のブログで紹介したRoute53ホストゾーンのTerraformでの一括インポートと、手順は共通する部分があります)
Terraformで多数のDNSレコードを含むRoute53ホストゾーンを一括インポート
対象リソース
今回の具体的な対象リソースは以下の通りです。(IAM Userは運用上の課題がまだあり、今回は対象外です。)
IAM Role, IAM Policy, IAM Group, インスタンスプロファイル
但し、一部除外するものなどがあり、条件は以下の通りです。
- IAM Roleの中で、AWSが自動作成するサービスリンクロールは対象外
- IAM Policyは、カスタマー管理ポリシーのみが対象
- IAM Roleの、IAM Policyアタッチ設定においては、カスタマー管理ポリシー以外がアタッチされていた場合でも、そのアタッチ情報は管理対象とする
基本的には、AWSが自動作成するような部分は除外して、ユーザ側で管理する部分のみをIaC化するため、上記のような条件としました。
Terraform側で定義するリソースは、以下の通りとしました。
リソース名 | 説明 |
---|---|
aws_iam_role | IAM Roleの定義 |
aws_iam_policy | IAM Policyの定義 |
aws_iam_role_policy_attachment | IAM RoleにIAM Policyをアタッチする定義 |
aws_iam_role_policy | IAM Roleにインラインポリシーを設定する定義 |
aws_iam_instance_profile | インスタンスプロファイルの定義 |
aws_iam_group | IAM Groupの定義 |
aws_iam_group_policy_attachment | IAM GroupにIAM Policyをアタッチする定義 |
aws_iam_group_policy | IAM Groupにインラインポリシーを設定する定義 |
手順の概要
以下のような手順としました。
- 生成AIで、importブロックを作成するスクリプトを生成
- 生成したスクリプトで、importブロックのtfファイルを作成
- 「terraform plan -generate-config-out=xxx.tf」で、import定義を基に、resource定義のtfファイルを作成
- resource定義のtfファイルを修正
- 「terraform plan」で確認
- 「terraform apply」でインポート
それぞれの手順の詳細を以下に記載します
生成AIで、importブロックを作成するスクリプトを生成
以下のようなプロンプトでスクリプトを生成しました。
terraformとAWSに関連するpythonスクリプトの作成依頼です。
あるAWSアカウントで、全ての設定がWeb Consoleで作成されています。ここで、そのアカウントに存在している以下のIAM 関連リソースの設定をTerraformでコード化したいです。
<Terraform化の対象リソース>
IAM Role
IAM Policy
IAM Group
インスタンスプロファイル
このため、上記リソースを、terraformのimportブロックを作成して、importしたいです。
importブロックを作成するterraformの対象リソースは以下の通りです。
<terraformの対象リソース>
aws_iam_role
aws_iam_policy
aws_iam_role_policy_attachment
aws_iam_role_policy
aws_iam_group
aws_iam_group_policy_attachment
aws_iam_group_policy
aws_iam_instance_profile
ただし、対象リソースは、以下のようにしてください。
・aws_iam_role は、AWS自動作成のサービスリンクロールは対象外とします。
・aws_iam_policy は、カスタマー管理ポリシーのみを対象とします。
・aws_iam_policy は、カスタマー管理ポリシーのみを対象です。ただし、"aws_iam_role_policy_attachment"では、I AM Role (サービスリンクロールは除外されている)に対して、もしもカスタマー管理ポリシー以外がアタッチされていた場合、"aws_iam_role_policy_attachment"にそのアタッチの定義を含めて下さい。
<importブロックの記載について>
それぞれのimportブロックの例は以下の通りです。toの右側のコメントは、リソース名の指定方法です。idの右側のコメントは、idの指定方法です。
import {
to = aws_iam_role.developer_name #I AM Role名と同じにする
id = "developer_name" #I AM Role名
}
import {
to = aws_iam_policy.UsersManageOwnCredentials # I AM Policy名と同じとする
id = "arn:aws:iam::123456789012:policy/UsersManageOwnCredentials" # I AM PolicyのARN
}
import {
to = aws_iam_role_policy_attachment.test_role_test_policy # I AM Role名_I AM Policy名 の形式とする
id = "test-role/arn:aws:iam::xxxxxxxxxxxx:policy/test-policy" # ロール名とポリシーARNを / で区切る。
}
import {
to = aws_iam_role_policy.role_of_mypolicy_name_mypolicy_name # I AM Role名_I AM Policy名 の形式とする
id = "role_of_mypolicy_name:mypolicy_name" # ロール名とポリシー名を : で区切る。
}
import {
to = aws_iam_instance_profile.app_instance_profile_1 # インスタンスプロファイル名と同じとする
id = "app-instance-profile-1" # インスタンスプロファイル名
}
import {
to = aws_iam_group.developers # I AM Group名と同じにする
id = "developers" # I AM Group名
}
import {
to = aws_iam_group_policy_attachment.test_group_test_policy # I AM Group名_I AM Policy名 の形式とする
id = "test-group/arn:aws:iam::xxxxxxxxxxxx:policy/test-policy" # グループ名とポリシーARNを / で区切る
}
import {
to = aws_iam_group_policy.group_of_mypolicy_name_mypolicy_name # I AM Group名_I AM Policy名 の形式とする
id = "group_of_mypolicy_name:mypolicy_name" # グループ名とポリシー名を : で区切る。
}
ただし、上記の to で指定するリソース名は、全て以下の共通ルールに従うものとします。
・大文字は小文字にする
・ -(ハイフン)は_(アンダーバー)にする。
・先頭が数字だった場合は、その前に_(アンダーバー)を付与する。
・ドット(.)は、(アンダーバー)に変える
・ /(スラッシュ)は(アンダーバー)に変える
上記のようなimportブロックのファイルを作りたいですが、Web Consoleで多数作成されているため、手動での作成は手間がかかります。importブロックのファイル作成を自動化するPythonスクリプトを作成して下さい。
スクリプト内には、適宜日本語のコメントをつけて下さい。
その結果、生成されたスクリプトは以下のようなものです。
(今回、スクリプトの詳細までは検証していませんが、今回の目的は問題なく達成できました)
#!/usr/bin/env python3
"""
AWS IAMリソースをTerraformのimportブロックに自動変換するスクリプト
このスクリプトは以下のIAMリソースを対象とします:
- IAM Role (サービスリンクロールは除外)
- IAM Policy (カスタマー管理ポリシーのみ)
- IAM Group
- インスタンスプロファイル
- ロール・グループとポリシーのアタッチメント関係
"""
import boto3
import json
import re
from typing import List, Dict, Any
import argparse
import sys
class TerraformImportBlockGenerator:
def __init__(self, profile_name: str = None, region: str = 'ap-northeast-1'):
"""
初期化
Args:
profile_name: AWSプロファイル名(指定なしの場合はデフォルトプロファイルを使用)
region: AWSリージョン
"""
try:
if profile_name:
session = boto3.Session(profile_name=profile_name)
else:
session = boto3.Session()
self.iam_client = session.client('iam', region_name=region)
self.account_id = session.client('sts').get_caller_identity()['Account']
print(f"アカウントID: {self.account_id}")
except Exception as e:
print(f"AWSセッションの初期化に失敗しました: {e}")
sys.exit(1)
def normalize_resource_name(self, name: str) -> str:
"""
リソース名をTerraformの命名規則に合わせて正規化
Args:
name: 元のリソース名
Returns:
正規化されたリソース名
"""
# 大文字を小文字に変換
normalized = name.lower()
# ハイフンをアンダースコアに変換
normalized = normalized.replace('-', '_')
# ドットをアンダースコアに変換
normalized = normalized.replace('.', '_')
# スラッシュをアンダースコアに変換
normalized = normalized.replace('/', '_')
# 先頭が数字の場合はアンダースコアを付与
if normalized[0].isdigit():
normalized = '_' + normalized
return normalized
def get_customer_managed_policies(self) -> List[Dict[str, Any]]:
"""
カスタマー管理ポリシーの一覧を取得
Returns:
カスタマー管理ポリシーのリスト
"""
print("カスタマー管理ポリシーを取得中...")
policies = []
try:
paginator = self.iam_client.get_paginator('list_policies')
for page in paginator.paginate(Scope='Local'):
for policy in page['Policies']:
policies.append({
'PolicyName': policy['PolicyName'],
'Arn': policy['Arn']
})
except Exception as e:
print(f"カスタマー管理ポリシーの取得に失敗: {e}")
print(f"カスタマー管理ポリシー {len(policies)}個を取得しました")
return policies
def get_iam_roles(self) -> List[Dict[str, Any]]:
"""
IAMロール一覧を取得(サービスリンクロールは除外)
Returns:
IAMロールのリスト
"""
print("IAMロールを取得中...")
roles = []
try:
paginator = self.iam_client.get_paginator('list_roles')
for page in paginator.paginate():
for role in page['Roles']:
# サービスリンクロールを除外
if '/aws-service-role/' not in role['Path']:
roles.append({
'RoleName': role['RoleName'],
'Arn': role['Arn']
})
except Exception as e:
print(f"IAMロールの取得に失敗: {e}")
print(f"IAMロール {len(roles)}個を取得しました(サービスリンクロール除く)")
return roles
def get_iam_groups(self) -> List[Dict[str, Any]]:
"""
IAMグループ一覧を取得
Returns:
IAMグループのリスト
"""
print("IAMグループを取得中...")
groups = []
try:
paginator = self.iam_client.get_paginator('list_groups')
for page in paginator.paginate():
for group in page['Groups']:
groups.append({
'GroupName': group['GroupName'],
'Arn': group['Arn']
})
except Exception as e:
print(f"IAMグループの取得に失敗: {e}")
print(f"IAMグループ {len(groups)}個を取得しました")
return groups
def get_instance_profiles(self) -> List[Dict[str, Any]]:
"""
インスタンスプロファイル一覧を取得
Returns:
インスタンスプロファイルのリスト
"""
print("インスタンスプロファイルを取得中...")
profiles = []
try:
paginator = self.iam_client.get_paginator('list_instance_profiles')
for page in paginator.paginate():
for profile in page['InstanceProfiles']:
profiles.append({
'InstanceProfileName': profile['InstanceProfileName'],
'Arn': profile['Arn']
})
except Exception as e:
print(f"インスタンスプロファイルの取得に失敗: {e}")
print(f"インスタンスプロファイル {len(profiles)}個を取得しました")
return profiles
def get_role_policies(self, role_name: str) -> List[Dict[str, Any]]:
"""
特定のロールにアタッチされたポリシーを取得
Args:
role_name: ロール名
Returns:
アタッチされたポリシーのリスト(管理ポリシーとインラインポリシー)
"""
attached_policies = []
inline_policies = []
try:
# 管理ポリシーを取得
paginator = self.iam_client.get_paginator('list_attached_role_policies')
for page in paginator.paginate(RoleName=role_name):
for policy in page['AttachedPolicies']:
attached_policies.append({
'PolicyName': policy['PolicyName'],
'PolicyArn': policy['PolicyArn'],
'Type': 'attached'
})
# インラインポリシーを取得
paginator = self.iam_client.get_paginator('list_role_policies')
for page in paginator.paginate(RoleName=role_name):
for policy_name in page['PolicyNames']:
inline_policies.append({
'PolicyName': policy_name,
'PolicyArn': None,
'Type': 'inline'
})
except Exception as e:
print(f"ロール {role_name} のポリシー取得に失敗: {e}")
return attached_policies + inline_policies
def get_group_policies(self, group_name: str) -> List[Dict[str, Any]]:
"""
特定のグループにアタッチされたポリシーを取得
Args:
group_name: グループ名
Returns:
アタッチされたポリシーのリスト(管理ポリシーとインラインポリシー)
"""
attached_policies = []
inline_policies = []
try:
# 管理ポリシーを取得
paginator = self.iam_client.get_paginator('list_attached_group_policies')
for page in paginator.paginate(GroupName=group_name):
for policy in page['AttachedPolicies']:
attached_policies.append({
'PolicyName': policy['PolicyName'],
'PolicyArn': policy['PolicyArn'],
'Type': 'attached'
})
# インラインポリシーを取得
paginator = self.iam_client.get_paginator('list_group_policies')
for page in paginator.paginate(GroupName=group_name):
for policy_name in page['PolicyNames']:
inline_policies.append({
'PolicyName': policy_name,
'PolicyArn': None,
'Type': 'inline'
})
except Exception as e:
print(f"グループ {group_name} のポリシー取得に失敗: {e}")
return attached_policies + inline_policies
def generate_import_blocks(self, output_file: str = 'import_blocks.tf'):
"""
すべてのリソースのimportブロックを生成
Args:
output_file: 出力ファイル名
"""
print("Terraformのimportブロックを生成中...")
import_blocks = []
import_blocks.append("# Terraformインポートブロック")
import_blocks.append("# このファイルはgenerate_import_blocks.pyにより自動生成されました\n")
# カスタマー管理ポリシー
policies = self.get_customer_managed_policies()
if policies:
import_blocks.append("# カスタマー管理ポリシー")
for policy in policies:
resource_name = self.normalize_resource_name(policy['PolicyName'])
import_blocks.append(f"import {{")
import_blocks.append(f" to = aws_iam_policy.{resource_name}")
import_blocks.append(f" id = \"{policy['Arn']}\"")
import_blocks.append(f"}}\n")
# IAMロール
roles = self.get_iam_roles()
if roles:
import_blocks.append("# IAMロール")
for role in roles:
resource_name = self.normalize_resource_name(role['RoleName'])
import_blocks.append(f"import {{")
import_blocks.append(f" to = aws_iam_role.{resource_name}")
import_blocks.append(f" id = \"{role['RoleName']}\"")
import_blocks.append(f"}}\n")
# IAMグループ
groups = self.get_iam_groups()
if groups:
import_blocks.append("# IAMグループ")
for group in groups:
resource_name = self.normalize_resource_name(group['GroupName'])
import_blocks.append(f"import {{")
import_blocks.append(f" to = aws_iam_group.{resource_name}")
import_blocks.append(f" id = \"{group['GroupName']}\"")
import_blocks.append(f"}}\n")
# インスタンスプロファイル
instance_profiles = self.get_instance_profiles()
if instance_profiles:
import_blocks.append("# インスタンスプロファイル")
for profile in instance_profiles:
resource_name = self.normalize_resource_name(profile['InstanceProfileName'])
import_blocks.append(f"import {{")
import_blocks.append(f" to = aws_iam_instance_profile.{resource_name}")
import_blocks.append(f" id = \"{profile['InstanceProfileName']}\"")
import_blocks.append(f"}}\n")
# ロールポリシーアタッチメント
if roles:
import_blocks.append("# ロールポリシーアタッチメント")
for role in roles:
role_policies = self.get_role_policies(role['RoleName'])
for policy in role_policies:
if policy['Type'] == 'attached':
# 管理ポリシーのアタッチメント
role_name_normalized = self.normalize_resource_name(role['RoleName'])
policy_name_normalized = self.normalize_resource_name(policy['PolicyName'])
resource_name = f"{role_name_normalized}_{policy_name_normalized}"
import_blocks.append(f"import {{")
import_blocks.append(f" to = aws_iam_role_policy_attachment.{resource_name}")
import_blocks.append(f" id = \"{role['RoleName']}/{policy['PolicyArn']}\"")
import_blocks.append(f"}}\n")
elif policy['Type'] == 'inline':
# インラインポリシー
role_name_normalized = self.normalize_resource_name(role['RoleName'])
policy_name_normalized = self.normalize_resource_name(policy['PolicyName'])
resource_name = f"{role_name_normalized}_{policy_name_normalized}"
import_blocks.append(f"import {{")
import_blocks.append(f" to = aws_iam_role_policy.{resource_name}")
import_blocks.append(f" id = \"{role['RoleName']}:{policy['PolicyName']}\"")
import_blocks.append(f"}}\n")
# グループポリシーアタッチメント
if groups:
import_blocks.append("# グループポリシーアタッチメント")
for group in groups:
group_policies = self.get_group_policies(group['GroupName'])
for policy in group_policies:
if policy['Type'] == 'attached':
# 管理ポリシーのアタッチメント
group_name_normalized = self.normalize_resource_name(group['GroupName'])
policy_name_normalized = self.normalize_resource_name(policy['PolicyName'])
resource_name = f"{group_name_normalized}_{policy_name_normalized}"
import_blocks.append(f"import {{")
import_blocks.append(f" to = aws_iam_group_policy_attachment.{resource_name}")
import_blocks.append(f" id = \"{group['GroupName']}/{policy['PolicyArn']}\"")
import_blocks.append(f"}}\n")
elif policy['Type'] == 'inline':
# インラインポリシー
group_name_normalized = self.normalize_resource_name(group['GroupName'])
policy_name_normalized = self.normalize_resource_name(policy['PolicyName'])
resource_name = f"{group_name_normalized}_{policy_name_normalized}"
import_blocks.append(f"import {{")
import_blocks.append(f" to = aws_iam_group_policy.{resource_name}")
import_blocks.append(f" id = \"{group['GroupName']}:{policy['PolicyName']}\"")
import_blocks.append(f"}}\n")
# ファイルに出力
try:
with open(output_file, 'w', encoding='utf-8') as f:
f.write('\n'.join(import_blocks))
print(f"importブロックを {output_file} に出力しました")
except Exception as e:
print(f"ファイル出力に失敗: {e}")
sys.exit(1)
def main():
"""
メイン関数
"""
parser = argparse.ArgumentParser(
description='AWS IAMリソースのTerraformインポートブロック生成ツール',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用例:
python generate_import_blocks.py # デフォルトプロファイルを使用
python generate_import_blocks.py --profile myprofile # 指定プロファイルを使用
python generate_import_blocks.py --output my_imports.tf # 出力ファイル名を指定
"""
)
parser.add_argument(
'--profile',
help='使用するAWSプロファイル名',
default=None
)
parser.add_argument(
'--output',
help='出力ファイル名(デフォルト: import_blocks.tf)',
default='import_blocks.tf'
)
parser.add_argument(
'--region',
help='AWSリージョン(デフォルト: ap-northeast-1)',
default='ap-northeast-1'
)
args = parser.parse_args()
# インポートブロック生成器を初期化
generator = TerraformImportBlockGenerator(
profile_name=args.profile,
region=args.region
)
# インポートブロックを生成
generator.generate_import_blocks(output_file=args.output)
print("処理が完了しました!")
print(f"生成されたファイル: {args.output}")
print("\n次の手順:")
print("1. terraform init を実行")
print("2. terraform plan を実行してインポート対象を確認")
print("3. terraform import コマンドを実行してリソースをインポート")
if __name__ == '__main__':
main()
生成したスクリプトで、importブロックのtfファイルを作成
上記のスクリプトを利用して、Terraformのimportブロックのtfファイルを作成します。
以下のようにして実行できます。スクリプトのファイル名を「generate_import_blocks.py」とした場合、カレントディレクトリに「import_blocks.tf」というファイル名で、接続先のAWSアカウントの、今回対象のIAMリソースのimportブロックが出力されます。
python generate_import_blocks.py
出力された結果は以下例のようになります。
# IAMロール
import {
to = aws_iam_role.hogerole
id = "hogerole"
}
# カスタマー管理ポリシー
import {
to = aws_iam_policy.hogepolicy
id = "arn:aws:iam::************:policy/hogepolicy"
}
# ロールポリシーアタッチメント
import {
to = aws_iam_role_policy_attachment.test_role_test_policy
id = "test-role/arn:aws:iam::aws:policy/test-policy"
}
「terraform plan -generate-config-out=xxx.tf」で、import定義を基に、resource定義のtfファイルを作成
次に以下のコマンドを実行します。(terraform initは完了している前提です)xxx.tfのファイル名は任意です。
terraform plan -generate-config-out=xxx.tf
xxx.tfに、importブロックで指定していたI AMのリソース定義が出力されます。
resource定義のtfファイルを修正
上記で出力したtfファイルでは、デフォルト値のパラメータなども明示的に出力されます。今回は、視認性の観点より、運用上不要そうなパラメータの削除を以下のように行いました。(削除するパラメータは、運用により検討下さい)
# aws_iam_role で削除したパラメータ
force_detach_policies = false
name_prefix = null
permissions_boundary = null
tags = {}
tags_all = {}
description = null
# aws_iam_role で残したパラメータ
assume_role_policy
max_session_duration
name
path
tags # 設定値が存在する場合のみ残した
tags_all # 設定値が存在する場合のみ残した
description # 設定値が存在する場合のみ残した
# aws_iam_policy で削除したパラメータ
name_prefix = null
tags = {}
tags_all = {}
description = null
# aws_iam_policy で残したパラメータ
name
path
policy
tags # 設定値が存在する場合のみ残した
tags_all # 設定値が存在する場合のみ残した
description # 設定値が存在する場合のみ残した
# aws_iam_group で削除したパラメータ
# 無し
# aws_iam_group で残したパラメータ
name
path
# aws_iam_role_policy_attachment で削除したパラメータ
# 無し
# aws_iam_role_policy_attachment で残したパラメータ
policy_arn
role
# aws_iam_role_policy で削除したパラメータ
name_prefix = null
# aws_iam_role_policy で残したパラメータ
name
policy
role
# aws_iam_instance_profile で削除したパラメータ
name_prefix = null
tags = {}
tags_all = {}
# aws_iam_instance_profile で残したパラメータ
name
path
role
# aws_iam_group_policy_attachment で削除したパラメータ
# 無し
# aws_iam_group_policy_attachment で残したパラメータ
group
policy_arn
# aws_iam_group_policy で削除したパラメータ
name_prefix = null
# aws_iam_group_policy で残したパラメータ
group
name
policy
「terraform plan」で確認
terraform planを行い、インポート対象リソースに間違いないことを確認しました。最後に出力されるPlan結果のimportの数が、importブロックで指定したリソース数と一致して、add, change , destroyの数が全て 0 であることを確認しました。
terraform plan
(実行結果例)
Plan: 123 to import, 0 to add, 0 to change, 0 to destroy
「terraform apply」でインポート
terraform applyを行い、インポートしました。実行結果は、直前のplanと同じになったことを確認しました。また、tfstateファイルが所定の保存場所(S3バケット)に作成・更新されたことを確認しました。
terraform apply
(実行結果例)
Apply complete! Resources: 123 imported, 0 added, 0 changed, 0 destroyed.
おわりに
Terraformで、多数のI AM系リソースのインポートを行う作業について記載しました。この記事が皆様のお役に立てば幸いです。
アノテーション株式会社について
アノテーション株式会社はクラスメソッドグループのオペレーション専門特化企業です。
サポート・運用・開発保守・情シス・バックオフィスの専門チームが、最新 IT テクノロジー、高い技術力、蓄積されたノウハウをフル活用し、お客様の課題解決を行っています。
当社は様々な職種でメンバーを募集しています。
「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、アノテーション株式会社 採用サイトをぜひご覧ください。