TerraformでSecurity GroupやNetwork ACL等の一括インポート

TerraformでSecurity GroupやNetwork ACL等の一括インポート

TerraformでSecurity GroupやNetwork ACL等の一括インポート
2025.10.09

Terraformで、多数のSecurity GroupやNetwork ACL等のリソースのインポートを行う機会がありましたので、その際の手順などを記載します。

経緯

あるAWSアカウントで、全てWeb Consoleで設定作業がされていたため、IaC化を進めていくこととなりました。以前、以下でRoute53やI AM系リソースのインポートについて記載しましたが、今回は、Security GroupやNetwork ACL等のリソースが対象になります。
Terraformで多数のDNSレコードを含むRoute53ホストゾーンを一括インポート
TerraformでIAM系リソースの一括インポート

Security GroupやNetwork ACL等の対象リソースも、多数の設定が存在し、それらのimport定義を手動で作成するのはとても大変なため、上記同様にスクリプトを生成AIで準備して行いました。(手順の流れは、上記のI AM系リソースの一括インポートと同じです)

対象リソース

今回の具体的な対象リソースは以下の通りです。
Security Group, Network ACL, Subnet, VPC

以下、補足点です。

  • Security Group, Network ACLともに、所属するVPCをパラメータに設定する必要があるため、VPC本体の定義も行います。また、 Network ACLで、パラメータにSubnetを設定する必要があるため、Subnetも定義しました。(AWSアカウント内のVPCと、Subnetを全て定義しました)

  • VPC作成時に自動生成されるデフォルトのNetwork ACLは、対象外としました。こちらは、Terraformでも通常のNetwork ACL定義(aws_network_acl)ではなく、専用の定義(aws_default_network_acl)が用意された特別なもので、通常のNetwork ACLとは異なる管理が必要です。さらに、デフォルトのNetwork ACLは変更しない運用としています。これをTerraformで管理した場合、メリットよりも、異なる管理によるトラブルのリスクのデメリットの方が大きいと考え、対象外としました。

  • Terraform側で定義したリソースは、以下の通りです。

リソース名 説明
aws_security_group セキュリティグループの定義
aws_vpc_security_group_ingress_rule セキュリティグループのインバウンドルール定義
aws_vpc_security_group_egress_rule セキュリティグループのアウトバウンドルール定義
aws_vpc VPCの定義
aws_network_acl ネットワーク ACLの定義
aws_network_acl_rule ネットワーク ACLルールの定義
aws_network_acl_association ネットワーク ACLと関連付けるサブネットの定義
aws_subnet サブネットの定義

手順の概要

以下のような手順としました。

  1. 生成AIで、importブロックを作成するスクリプトを生成
  2. 生成したスクリプトで、importブロックのtfファイルを作成
  3. 「terraform plan -generate-config-out=xxx.tf」で、import定義を基に、resource定義のtfファイルを作成
  4. resource定義のtfファイルを修正
  5. 「terraform plan」で確認
  6. 「terraform apply」でインポート

それぞれの手順の詳細を以下に記載します

生成AIで、importブロックを作成するスクリプトを生成

以下のようなプロンプトでスクリプトを生成しました。

			
			terraformとAWSの質問です。あるAWSアカウントで、全ての設定がWeb Consoleで作成されています。ここで、そのアカウントに存在しているセキュリティグループと、Network ACLの設定を全てTerraformでコード化したいです。
また、セキュリティグループと、Network ACLは、VPCとサブネットにも紐づいています。このため、そのアカウント内に存在するVPCとサブネットも全てTerraform化したいです。

<Terraform化の対象リソース>
セキュリティグループ
Network ACL
VPC
サブネット

このため、上記リソースを、terraformのimportブロックを作成して、importしたいです。
importブロックを作成するterraformの対象リソースは以下の通りです。

<terraformの対象リソース>
aws_security_group
aws_vpc_security_group_ingress_rule
aws_vpc_security_group_egress_rule
aws_vpc
aws_network_acl
aws_network_acl_rule
aws_network_acl_association
aws_subnet

ただし、aws_network_aclは、Nameが付与されているものを対象にします。Nameが付与されていないもの(VPC作成時にデフォルトで作成されたACL)は、importブロックを作成しません。
このため、aws_network_acl_ruleと、aws_network_acl_associationも、Nameが付与されたaws_network_aclに紐づくものだけが、作成対象となります。

それぞれのimportブロックの例は以下の通りです。toの右側のコメントは、リソース名の指定方法です。idの右側のコメントは、idの指定方法です。

import {
to = aws_security_group.example # "セキュリティグループ名"にする。もしも default という名称の場合は、「default_sg_vpc_紐づくVPNのName」の形式にする
id = "sg-12345678" # security group id
}

import {
to = aws_vpc_security_group_ingress_rule.example # "ルールが紐づくセキュリティグループ名_ingress_タイプ_NN"  (タイプは、HTTPなど、そのルールより取得。タイプで、「すべてのトラフィック」の場合はALLとする。タイプで、「すべての ICMP - IPv4」の場合はICMPとする。NNは、ルールが紐づくセキュリティグループの中で、inboundルールの中での、同じタイプのルールの中での連番を付与)
id = "sgr-1234567890abcdefg"              # security_group_rule_id
}

import {
to = aws_vpc_security_group_egress_rule.example # "ルールが紐づくセキュリティグループ名_egress_タイプ_NN"  (タイプは、HTTPなど、そのルールより取得。タイプで、「すべてのトラフィック」の場合はALLとする。タイプで、「すべての ICMP - IPv4」の場合はICMPとする。NNは、ルールが紐づくセキュリティグループの中で、outboundルールの中での、同じタイプのルールの中での連番を付与)
id = "sgr-abcdefg1234567890"            # security_group_rule_id
}

import {
to = aws_vpc.test_vpc # Nameにする
id = "vpc-12345abc" # VPC ID
}

import {
to = aws_network_acl.main # Nameにする。Nameがないものは、"該当するVPCのName_default_acl" にする
id = "acl-12345abc" # ACL ID
}

import {
to = aws_network_acl_rule.my_rule # 対応するACL_ingress(egress)_ルール番号 (ルール番号が * の場合は、defalut とう文字列にする。Inboundルールの場合は ingress、Outboundルールの場合はegressとうい文字列を付与)
id = "acl-12345abc:100:tcp:false" # NETWORK_ACL_ID:RULE_NUMBER:PROTOCOL:EGRESS (EGRESSの部分は、Inboundルールの場合はfalse。outboundルールの場合はtrue)
}

import {
to = aws_network_acl_association.main # 対応するACLのリソース名_サブネットのリソース名
id = "aclassoc-1234567890abcdefg" # Network ACL Association ID
}

import {
to = aws_subnet.example # Nameにする。Nameが付与されてない場合は、紐づくVPNのNameとする
id = "subnet-12345abc" # サブネットID
}

ただし、上記の to で指定するリソース名は、全て以下の共通ルールに従うものとします。
・大文字は小文字にする
・ -(ハイフン)は_(アンダーバー)にする。
・先頭が数字だった場合は、その前に_(アンダーバー)を付与する。
・ドット(.)は、(アンダーバー)に変える
・ /(スラッシュ)は(アンダーバー)に変える

上記のようなimportブロックのファイルを作りたいですが、Web Consoleで多数作成されているため、手動での作成は手間がかかります。importブロックのファイル作成を自動化するPythonスクリプトを作成して下さい。
なお、スクリプト内には、適宜日本語のコメントをつけて下さい。

		

その結果、生成されたスクリプトは以下のようなものです。
(今回、スクリプトの詳細までは検証していませんが、今回の目的は問題なく達成できました)

			
			#!/usr/bin/env python3
"""
AWS リソースを Terraform import ブロック形式で出力するスクリプト
セキュリティグループ、VPC、サブネット、Network ACL の情報を AWS から取得し、
Terraform の import ブロックを自動生成する
"""

import boto3
import re
import json
from typing import Dict, List, Any
import argparse

class TerraformImportGenerator:
    def __init__(self, region: str = 'ap-northeast-1'):
        """
        AWS クライアントを初期化

        Args:
            region (str): AWS リージョン
        """
        self.ec2 = boto3.client('ec2', region_name=region)
        self.region = region

    def sanitize_resource_name(self, name: str) -> str:
        """
        リソース名を Terraform リソース名として使用可能な形式に変換

        変換ルール:
        - 大文字は小文字にする
        - -(ハイフン)は_(アンダーバー)にする
        - 先頭が数字だった場合は、その前に_(アンダーバー)を付与する
        - ドット(.)は_(アンダーバー)に変える
        - /(スラッシュ)は_(アンダーバー)に変える

        Args:
            name (str): 元のリソース名

        Returns:
            str: 変換後のリソース名
        """
        if not name:
            return "unnamed_resource"

        # 小文字に変換
        name = name.lower()

        # 特殊文字を _ に変換
        name = re.sub(r'[-./]', '_', name)

        # 先頭が数字の場合、_ を追加
        if name and name[0].isdigit():
            name = '_' + name

        # 英数字とアンダースコア以外を削除
        name = re.sub(r'[^a-zA-Z0-9_]', '', name)

        return name if name else "unnamed_resource"

    def get_vpcs(self) -> List[Dict[str, Any]]:
        """
        VPC の情報を取得

        Returns:
            List[Dict]: VPC情報のリスト
        """
        try:
            response = self.ec2.describe_vpcs()
            vpcs = []

            for vpc in response['Vpcs']:
                vpc_name = next((tag['Value'] for tag in vpc.get('Tags', []) if tag['Key'] == 'Name'), None)
                vpcs.append({
                    'id': vpc['VpcId'],
                    'name': vpc_name,
                    'sanitized_name': self.sanitize_resource_name(vpc_name) if vpc_name else f"vpc_{vpc['VpcId'].replace('-', '_')}"
                })

            return vpcs

        except Exception as e:
            print(f"VPC取得エラー: {e}")
            return []

    def get_subnets(self) -> List[Dict[str, Any]]:
        """
        サブネットの情報を取得

        Returns:
            List[Dict]: サブネット情報のリスト
        """
        try:
            response = self.ec2.describe_subnets()
            subnets = []

            # VPC情報を取得してサブネット名に使用
            vpcs = {vpc['id']: vpc for vpc in self.get_vpcs()}

            for subnet in response['Subnets']:
                subnet_name = next((tag['Value'] for tag in subnet.get('Tags', []) if tag['Key'] == 'Name'), None)

                # 特殊ケース: "-" が "DefaultVPC-Public3" になる場合の処理
                if subnet_name == "-":
                    subnet_name = "DefaultVPC-Public3"

                # Nameが付与されていない場合は、紐づくVPCのNameを使用
                if not subnet_name:
                    vpc_info = vpcs.get(subnet['VpcId'])
                    if vpc_info and vpc_info['name']:
                        subnet_name = vpc_info['name']
                    else:
                        subnet_name = f"subnet_{subnet['SubnetId'].replace('-', '_')}"

                subnets.append({
                    'id': subnet['SubnetId'],
                    'name': subnet_name,
                    'vpc_id': subnet['VpcId'],
                    'sanitized_name': self.sanitize_resource_name(subnet_name)
                })

            return subnets

        except Exception as e:
            print(f"サブネット取得エラー: {e}")
            return []

    def get_security_groups(self) -> List[Dict[str, Any]]:
        """
        セキュリティグループの情報を取得

        Returns:
            List[Dict]: セキュリティグループ情報のリスト
        """
        try:
            response = self.ec2.describe_security_groups()
            security_groups = []

            # VPC情報を取得してセキュリティグループ名に使用
            vpcs = {vpc['id']: vpc for vpc in self.get_vpcs()}

            for sg in response['SecurityGroups']:
                sg_name = sg.get('GroupName', '')

                # defaultという名称の場合は、「default_sg_vpc_紐づくVPCのName」の形式にする
                if sg_name.lower() == 'default':
                    vpc_info = vpcs.get(sg['VpcId'])
                    if vpc_info and vpc_info['name']:
                        sg_name = f"default_sg_vpc_{vpc_info['name']}"
                    else:
                        sg_name = f"default_sg_vpc_{sg['VpcId'].replace('-', '_')}"

                security_groups.append({
                    'id': sg['GroupId'],
                    'name': sg_name,
                    'vpc_id': sg['VpcId'],
                    'sanitized_name': self.sanitize_resource_name(sg_name),
                    'ingress_rules': sg.get('IpPermissions', []),
                    'egress_rules': sg.get('IpPermissionsEgress', [])
                })

            return security_groups

        except Exception as e:
            print(f"セキュリティグループ取得エラー: {e}")
            return []

    def get_security_group_rules(self) -> List[Dict[str, Any]]:
        """
        セキュリティグループルールの詳細情報を取得

        Returns:
            List[Dict]: セキュリティグループルール情報のリスト
        """
        try:
            response = self.ec2.describe_security_group_rules()
            return response.get('SecurityGroupRules', [])

        except Exception as e:
            print(f"セキュリティグループルール取得エラー: {e}")
            return []

    def get_network_acls(self) -> List[Dict[str, Any]]:
        """
        Network ACL の情報を取得
        Nameタグが付与されているもののみを対象とする(デフォルトACLは除外)

        Returns:
            List[Dict]: Network ACL情報のリスト  
        """
        try:
            response = self.ec2.describe_network_acls()
            network_acls = []

            for acl in response['NetworkAcls']:
                acl_name = next((tag['Value'] for tag in acl.get('Tags', []) if tag['Key'] == 'Name'), None)

                # Nameタグが付与されているもののみを対象とする
                # (デフォルトで作成されたACLは除外)
                if acl_name:
                    network_acls.append({
                        'id': acl['NetworkAclId'],
                        'name': acl_name,
                        'vpc_id': acl['VpcId'],
                        'sanitized_name': self.sanitize_resource_name(acl_name),
                        'entries': acl.get('Entries', []),
                        'associations': acl.get('Associations', [])
                    })

            return network_acls

        except Exception as e:
            print(f"Network ACL取得エラー: {e}")
            return []

    def determine_rule_type(self, rule: Dict[str, Any]) -> str:
        """
        セキュリティグループルールのタイプを判定(HTTP、HTTPS、SSH等)

        Args:
            rule (Dict): ルール情報

        Returns:
            str: ルールタイプ
        """
        # プロトコル-1(すべてのトラフィック)の場合を最初にチェック
        if rule.get('IpProtocol') == '-1':
            return 'ALL'  # 「すべてのトラフィック」の場合はALLとする
        # ICMP - IPv4の場合をチェック
        elif rule.get('IpProtocol') == 'icmp':
            return 'ICMP'  # 「すべての ICMP - IPv4」の場合はICMPとする
        elif 'FromPort' in rule and 'ToPort' in rule:
            port = rule['FromPort']
            if port == 80:
                return 'http'
            elif port == 443:
                return 'https'
            elif port == 22:
                return 'ssh'
            elif port == 3389:
                return 'rdp'
            else:
                return f'port_{port}'
        else:
            return 'custom'

    def generate_vpc_imports(self, vpcs: List[Dict[str, Any]]) -> List[str]:
        """
        VPC の import ブロックを生成

        Args:
            vpcs (List[Dict]): VPC情報のリスト

        Returns:
            List[str]: import ブロックのリスト
        """
        imports = []
        for vpc in vpcs:
            import_block = f"""import {{
  to = aws_vpc.{vpc['sanitized_name']}
  id = "{vpc['id']}"
}}"""
            imports.append(import_block)

        return imports

    def generate_subnet_imports(self, subnets: List[Dict[str, Any]]) -> List[str]:
        """
        サブネットの import ブロックを生成

        Args:
            subnets (List[Dict]): サブネット情報のリスト

        Returns:
            List[str]: import ブロックのリスト
        """
        imports = []
        for subnet in subnets:
            import_block = f"""import {{
  to = aws_subnet.{subnet['sanitized_name']}
  id = "{subnet['id']}"
}}"""
            imports.append(import_block)

        return imports

    def generate_security_group_imports(self, security_groups: List[Dict[str, Any]]) -> List[str]:
        """
        セキュリティグループの import ブロックを生成

        Args:
            security_groups (List[Dict]): セキュリティグループ情報のリスト

        Returns:
            List[str]: import ブロックのリスト
        """
        imports = []
        for sg in security_groups:
            import_block = f"""import {{
  to = aws_security_group.{sg['sanitized_name']}
  id = "{sg['id']}"
}}"""
            imports.append(import_block)

        return imports

    def generate_security_group_rule_imports(self, security_groups: List[Dict[str, Any]], 
                                           sg_rules: List[Dict[str, Any]]) -> List[str]:
        """
        セキュリティグループルールの import ブロックを生成

        Args:
            security_groups (List[Dict]): セキュリティグループ情報
            sg_rules (List[Dict]): セキュリティグループルール情報

        Returns:
            List[str]: import ブロックのリスト
        """
        imports = []

        # セキュリティグループIDと名前のマッピングを作成
        sg_mapping = {sg['id']: sg for sg in security_groups}

        # セキュリティグループごと、方向ごと、タイプごとの連番管理
        # 構造: {sg_id: {direction: {rule_type: counter}}}
        type_counters = {}

        for rule in sg_rules:
            sg_id = rule['GroupId']
            if sg_id not in sg_mapping:
                continue

            sg_info = sg_mapping[sg_id]
            is_egress = rule['IsEgress']
            direction = 'egress' if is_egress else 'ingress'

            # プロトコルとポート情報からタイプを判定
            rule_type = self.determine_rule_type(rule)

            # セキュリティグループごと、方向ごと、タイプごとの連番を管理
            if sg_id not in type_counters:
                type_counters[sg_id] = {}
            if direction not in type_counters[sg_id]:
                type_counters[sg_id][direction] = {}
            if rule_type not in type_counters[sg_id][direction]:
                type_counters[sg_id][direction][rule_type] = 1

            counter = type_counters[sg_id][direction][rule_type]
            type_counters[sg_id][direction][rule_type] += 1

            resource_name = f"{sg_info['sanitized_name']}_{direction}_{rule_type}_{counter:02d}"

            import_block = f"""import {{
  to = aws_vpc_security_group_{direction}_rule.{resource_name}
  id = "{rule['SecurityGroupRuleId']}"
}}"""
            imports.append(import_block)

        return imports

    def generate_network_acl_imports(self, network_acls: List[Dict[str, Any]]) -> List[str]:
        """
        Network ACL の import ブロックを生成

        Args:
            network_acls (List[Dict]): Network ACL情報のリスト

        Returns:
            List[str]: import ブロックのリスト
        """
        imports = []
        for acl in network_acls:
            import_block = f"""import {{
  to = aws_network_acl.{acl['sanitized_name']}
  id = "{acl['id']}"
}}"""
            imports.append(import_block)

        return imports

    def generate_network_acl_rule_imports(self, network_acls: List[Dict[str, Any]]) -> List[str]:
        """
        Network ACL ルールの import ブロックを生成
        Nameタグが付与されたNetwork ACLに紐づくルールのみを対象とする

        Args:
            network_acls (List[Dict]): Network ACL情報のリスト(Nameタグありのもののみ)

        Returns:
            List[str]: import ブロックのリスト
        """
        imports = []

        # 既にget_network_acls()でフィルタリング済みのため、全て対象
        for acl in network_acls:
            for entry in acl['entries']:
                rule_number = entry['RuleNumber']
                protocol = entry['Protocol']
                is_egress = entry['Egress']

                # ルール番号が * の場合は default にする
                if rule_number == 32767:  # AWS の default rule number
                    rule_num_str = 'default'
                else:
                    rule_num_str = str(rule_number)

                direction = 'egress' if is_egress else 'ingress'
                resource_name = f"{acl['sanitized_name']}_{direction}_{rule_num_str}"

                # プロトコル番号を文字列に変換
                protocol_str = protocol if isinstance(protocol, str) else str(protocol)

                import_id = f"{acl['id']}:{rule_number}:{protocol_str}:{str(is_egress).lower()}"

                import_block = f"""import {{
  to = aws_network_acl_rule.{resource_name}
  id = "{import_id}"
}}"""
                imports.append(import_block)

        return imports

    def generate_network_acl_association_imports(self, network_acls: List[Dict[str, Any]], 
                                               subnets: List[Dict[str, Any]]) -> List[str]:
        """
        Network ACL Association の import ブロックを生成
        Nameタグが付与されたNetwork ACLに紐づくAssociationのみを対象とする

        Args:
            network_acls (List[Dict]): Network ACL情報のリスト(Nameタグありのもののみ)
            subnets (List[Dict]): サブネット情報のリスト

        Returns:
            List[str]: import ブロックのリスト
        """
        imports = []

        # サブネットIDと名前のマッピングを作成
        subnet_mapping = {subnet['id']: subnet for subnet in subnets}

        # 既にget_network_acls()でフィルタリング済みのため、全て対象
        for acl in network_acls:
            for association in acl['associations']:
                subnet_id = association['SubnetId']
                if subnet_id in subnet_mapping:
                    subnet_info = subnet_mapping[subnet_id]
                    resource_name = f"{acl['sanitized_name']}_{subnet_info['sanitized_name']}"

                    import_block = f"""import {{
  to = aws_network_acl_association.{resource_name}
  id = "{association['NetworkAclAssociationId']}"
}}"""
                    imports.append(import_block)

        return imports

    def generate_all_imports(self) -> str:
        """
        全ての AWS リソースの import ブロックを生成

        Returns:
            str: 全ての import ブロックを含む文字列
        """
        print("AWS リソース情報を取得中...")

        # 各リソースの情報を取得
        vpcs = self.get_vpcs()
        subnets = self.get_subnets()
        security_groups = self.get_security_groups()
        sg_rules = self.get_security_group_rules()
        network_acls = self.get_network_acls()

        print(f"取得完了: VPC={len(vpcs)}, サブネット={len(subnets)}, "
              f"セキュリティグループ={len(security_groups)}, "
              f"SGルール={len(sg_rules)}, ACL={len(network_acls)}")

        # import ブロックを生成
        all_imports = []

        # VPC
        all_imports.append("# VPC Import Blocks")
        all_imports.extend(self.generate_vpc_imports(vpcs))
        all_imports.append("")

        # サブネット
        all_imports.append("# Subnet Import Blocks")
        all_imports.extend(self.generate_subnet_imports(subnets))
        all_imports.append("")

        # セキュリティグループ
        all_imports.append("# Security Group Import Blocks")
        all_imports.extend(self.generate_security_group_imports(security_groups))
        all_imports.append("")

        # セキュリティグループルール
        all_imports.append("# Security Group Rule Import Blocks")
        all_imports.extend(self.generate_security_group_rule_imports(security_groups, sg_rules))
        all_imports.append("")

        # Network ACL
        all_imports.append("# Network ACL Import Blocks")
        all_imports.extend(self.generate_network_acl_imports(network_acls))
        all_imports.append("")

        # Network ACL ルール
        all_imports.append("# Network ACL Rule Import Blocks")
        all_imports.extend(self.generate_network_acl_rule_imports(network_acls))
        all_imports.append("")

        # Network ACL Association
        all_imports.append("# Network ACL Association Import Blocks")
        all_imports.extend(self.generate_network_acl_association_imports(network_acls, subnets))

        return "\n".join(all_imports)

    def save_to_file(self, content: str, filename: str = "terraform_imports.tf"):
        """
        import ブロックをファイルに保存

        Args:
            content (str): 保存するコンテンツ
            filename (str): 保存するファイル名
        """
        try:
            with open(filename, 'w', encoding='utf-8') as f:
                f.write(content)
            print(f"import ブロックを {filename} に保存しました。")
        except Exception as e:
            print(f"ファイル保存エラー: {e}")

def main():
    """
    メイン実行関数
    """
    parser = argparse.ArgumentParser(description='AWS リソースの Terraform import ブロック生成')
    parser.add_argument('--region', default='ap-northeast-1', help='AWS リージョン (デフォルト: ap-northeast-1)')
    parser.add_argument('--output', default='terraform_imports.tf', help='出力ファイル名 (デフォルト: terraform_imports.tf)')

    args = parser.parse_args()

    # AWS認証情報の確認
    try:
        sts = boto3.client('sts')
        identity = sts.get_caller_identity()
        print(f"AWS アカウント: {identity['Account']}")
        print(f"使用リージョン: {args.region}")
        print("-" * 50)
    except Exception as e:
        print(f"AWS認証エラー: {e}")
        print("AWS認証情報を確認してください。")
        return

    # import ブロック生成器を初期化
    generator = TerraformImportGenerator(region=args.region)

    # import ブロックを生成
    import_blocks = generator.generate_all_imports()

    # ファイルに出力
    generator.save_to_file(import_blocks, args.output)

    print("\n生成完了!")
    print(f"次のステップ:")
    print(f"1. {args.output} の内容を確認")
    print(f"2. terraform plan を実行して import 対象を確認")
    print(f"3. terraform apply を実行して import を実行")

if __name__ == "__main__":
    main()

		

生成したスクリプトで、importブロックのtfファイルを作成

上記のスクリプトを利用して、Terraformのimportブロックのtfファイルを作成します。
以下のようにして実行できます。スクリプトのファイル名を「terraform_import_generator.py」とした場合、カレントディレクトリに「terraform_imports.tf」というファイル名で、接続先のAWSアカウントの、今回対象リソースのimportブロックが出力されます。

			
			python terraform_import_generator.py

		

出力された結果は以下例のようになります。

			
			# Security Group
import {
  to = aws_security_group.hogehoge_sg
  id = "sg-12345abc"
}

# Security Group Inbound Rule
import {
  to = aws_vpc_security_group_ingress_rule.hogehoge_sg_ingress_port_12345_01
  id = "sgr-1234567890abcdefg"
}

# Security Group Outbound Rule
import {
  to = aws_vpc_security_group_egress_rule.hogehoge_sg_egress_ALL_01
  id = "sgr-abcdefg1234567890"
}

# Network ACL
import {
  to = aws_network_acl.hogehoge_acl
  id = "acl-1234567890abcdefg"
}

# Network ACL Rule
import {
  to = aws_network_acl_rule.hogehoge_acl_ingress_100
  id = "acl-1234567890abcdefg:100:-1:false"
}
import {
  to = aws_network_acl_rule.hogehoge_acl_ingress_default
  id = "acl-1234567890abcdefg:32767:-1:false"
}

		

「terraform plan -generate-config-out=xxx.tf」で、import定義を基に、resource定義のtfファイルを作成

次に以下のコマンドを実行します。(terraform initは完了している前提です)xxx.tfのファイル名は任意です。

			
			terraform plan -generate-config-out=xxx.tf

		

xxx.tfに、importブロックで指定していたリソース定義が出力されます。

resource定義のtfファイルを修正

上記で出力したtfファイルでは、デフォルト値のパラメータなども明示的に出力されます。今回は、視認性の観点より、運用上不要そうなパラメータの削除を以下のように行いました。また、非推奨のパラメータも削除しました。(削除するパラメータは、運用により検討下さい)

			
			# aws_security_group で削除したパラメータ
egress     # インラインのルール定義は非推奨(別途 aws_vpc_security_group_egress_rule で定義)
ingress    # インラインのルール定義は非推奨(別途 aws_vpc_security_group_ingress_rule で定義)
region                 = "ap-northeast-1”
name_prefix            = null
revoke_rules_on_delete = null
tags        = {}
tags_all    = {}

# aws_security_group で残したパラメータ
description
name
vpc_id
tags        # 設定値が存在する場合のみ残した
tags_all    # 設定値が存在する場合のみ残した

		
			
			# aws_vpc_security_group_ingress_rule で削除したパラメータ
cidr_ipv6                    = null
region                       = "ap-northeast-1”
prefix_list_id               = null
tags                         = null
description                  = null

# aws_vpc_security_group_ingress_rule で残したパラメータ
cidr_ipv4
description      # 設定値が存在する場合のみ残した
from_port
ip_protocol
referenced_security_group_id
security_group_id
to_port

		
			
			# aws_vpc_security_group_egress_rule で削除したパラメータ
cidr_ipv6                    = null
region                       = "ap-northeast-1”
prefix_list_id               = null
tags                         = null
description                  = null

# aws_vpc_security_group_egress_rule で残したパラメータ
cidr_ipv4
description      # 設定値が存在する場合のみ残した
from_port
ip_protocol
referenced_security_group_id
security_group_id
to_port

		
			
			# aws_vpc で削除したパラメータ
assign_generated_ipv6_cidr_block     = false
enable_network_address_usage_metrics = false
ipv4_ipam_pool_id                    = null
ipv4_netmask_length                  = null
instance_tenancy                     = "default”
ipv6_cidr_block                      = null
ipv6_cidr_block_network_border_group = null
ipv6_ipam_pool_id    = null
ipv6_netmask_length  = 0
region               = "ap-northeast-1”

# aws_vpc で残したパラメータ
cidr_block
enable_dns_hostnames
enable_dns_support
tags
tags_all

		
			
			# aws_network_acl で削除したパラメータ
egress        # インラインでのルール定義はせず、別途 aws_network_acl_rule で定義
ingress       # インラインでのルール定義はせず、別途 aws_network_acl_rule で定義
subnet_ids    # 関連づけるsubnetはここで定義せず、別途 aws_network_acl_association で定義
region        = "ap-northeast-1”

# aws_network_acl で残したパラメータ
vpc_id
tags
tags_all

		
			
			# aws_network_acl_rule で削除したパラメータ
icmp_code       = null
icmp_type       = null
ipv6_cidr_block = null
region         = "ap-northeast-1”

# aws_network_acl_rule で残したパラメータ
cidr_block
egress
from_port
network_acl_id
protocol
rule_action
rule_number
to_port

		
			
			# aws_network_acl_association で削除したパラメータ
region         = "ap-northeast-1”

# aws_network_acl_association で残したパラメータ
network_acl_id
subnet_id

		
			
			# aws_subnet で削除したパラメータ
availability_zone_id      # 「availability_zone」のパラメータと重複エラーとなるため。また「availability_zone」の方が、サポート対象リージョンが多いため、そちらで設定する
assign_ipv6_address_on_creation                = false
customer_owned_ipv4_pool                       = null
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
ipv6_cidr_block                     = null
ipv6_native                         = false
map_customer_owned_ip_on_launch     = false
outpost_arn                         = null
region                              = "ap-northeast-1”

# aws_subnet で残したパラメータ
availability_zone
cidr_block
map_public_ip_on_launch
private_dns_hostname_type_on_launch
tags
tags_all
vpc_id

		

「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で、多数のSecurity GroupやNetwork ACL等のリソースのインポートを行う作業について記載しました。この記事が皆様のお役に立てば幸いです。

アノテーション株式会社について

アノテーション株式会社はクラスメソッドグループのオペレーション専門特化企業です。

サポート・運用・開発保守・情シス・バックオフィスの専門チームが、最新 IT テクノロジー、高い技術力、蓄積されたノウハウをフル活用し、お客様の課題解決を行っています。

当社は様々な職種でメンバーを募集しています。

「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、アノテーション株式会社 採用サイトをぜひご覧ください。

この記事をシェアする

FacebookHatena blogX

関連記事

TerraformでSecurity GroupやNetwork ACL等の一括インポート | DevelopersIO