既存EC2の情報からCloudFormationを自動生成してEC2を再構築する方法

記事タイトルとURLをコピーする

こんにちは!イーゴリです。

以前、以下の記事で既存EC2のENIを置き換えるためのCloudFormationテンプレート生成ツールを紹介しました。 この方法では、既存のENIをそのまま利用します。

ただし、AWSの仕様上、既存のENIを持つEC2を削除してから、新しいEC2にENIをアタッチするという手順になります。

blog.serverworks.co.jp

今回はそれとは少し異なり、既存EC2の情報をもとに「新しいEC2インスタンス」をCloudFormationで作成するためのテンプレートを自動生成するツールを紹介します。

この方法では新しいENIが作成されるため、新しいIPを使用する場合は既存のEC2を削除する必要はありません。

背景

AWS環境の運用をしていると、以下のようなケースがあります。

  • 既存EC2の設定をベースにして新しいEC2を構築したい
  • OSを更新したEC2を新しく作り直したい
  • Golden AMIを利用して再構築したい
  • 既存設定(タグ、SG、Subnetなど)をなるべくそのまま再現したい

このような場合、手動でCloudFormationテンプレートを書くのはかなり手間がかかります。

例えばEC2が1台だけの場合でも、以下のような情報を確認する必要があり、 お客様から一つ一つヒアリングしなければなりません。

  • InstanceType
  • Subnet
  • SecurityGroup
  • Tags
  • EBS設定
  • IMDS設定
  • IAMロール
  • Monitoring
  • SourceDestCheck

さらに複数台ある場合、これを毎回手作業で作成するのは現実的ではありません。

そこで、既存EC2の情報からCloudFormationテンプレートを自動生成するスクリプトを作成してみました。

前提条件

以下の環境が必要です。

  • Python 3

  • boto3 (boto3 は Python から AWS API を呼び出すための公式ライブラリです)

  • AWS CLI

  • 対象AWSアカウントへ接続済みのAWS CLIプロファイル

  • EC2情報を参照できる権限

やりたいこと

今回のツールで実現したいことはシンプルです。

既存EC2の情報
      ↓
ec2_parameters_to_csv.py(対象アカウントのすべてのEC2情報が出力される)
      ↓
出力結果:Parameter.csv、describe-instances.json、describe-network-interfaces.json、describe-volumes.json
      ↓
Parameter.csvの整理(不要なEC2を削除し、対象のEC2のみ残す)
      ↓
create_cfn_yaml.py
      ↓
CloudFormation YAML生成
      ↓
CloudFormationテンプレートを展開→新しいEC2作成

つまり、既存EC2の設定をテンプレート化して再利用できるようにするというものです。

注意事項

①このスクリプトについては、一切の責任を負えませんので、ご自身で十分にご検証した上で、このスクリプトを使ってもよいかご判断ください。

②下記のスクリプトは私にとって必要なEC2パラメーターのみ取得されているので、他のパラメーターが必要な場合、ご自身で修正してください。

EC2情報の取得

まず、既存EC2の情報を取得して Parameter.csv を作成します。

ROロール(AWS CLI)で対象AWSアカウントにログインする。
$ export AWS_DEFAULT_PROFILE=<AWSアカウントプロファイル名>

対象アカウントがROロールでログインされていることを確認する。
$ aws sts get-caller-identity

$ cd <スクリプトのディレクトリ>

以下のスクリプトを使用して、対象アカウント内のEC2情報をCSVとして出力します。

$ python3 ec2_parameters_to_csv.py
import boto3
import json
import csv
from datetime import datetime

# AWSクライアントの設定
ec2 = boto3.client('ec2')

# EC2インスタンス情報の取得
instances = ec2.describe_instances()
volumes = ec2.describe_volumes()
network_interfaces = ec2.describe_network_interfaces()

# カスタム関数: datetimeオブジェクトを文字列に変換
def custom_converter(o):
    if isinstance(o, datetime):
        return o.isoformat()

# JSONファイルに保存
with open('describe-instances.json', 'w') as f:
    json.dump(instances, f, indent=4, default=custom_converter)

with open('describe-volumes.json', 'w') as f:
    json.dump(volumes, f, indent=4, default=custom_converter)

with open('describe-network-interfaces.json', 'w') as f:
    json.dump(network_interfaces, f, indent=4, default=custom_converter)

# CSVフォーマットの定義
csv_data = []
csv_headers = [
    'InstanceId', 'InstanceName', 'AvailabilityZone', 'PrivateIpAddress', 'InstanceType',
    'AMI_ID', 'IamInstanceProfile', 'IMDSv2', 'EBS_Volume_Type', 'EBS_Volume_Size', 'IOPS', 'Throughput', 'EBS_Encrypted',
    'KeyName', 'SubnetId', 'SecurityGroupIds', 'Monitoring', 'SourceDestCheck'
]

# Max counts
max_instance_tags = 0
max_ebs_tags = 0
max_eni_tags = 0

# Step 1: Calculate max tags
for reservation in instances['Reservations']:
    for instance in reservation['Instances']:
        max_instance_tags = max(max_instance_tags, len(instance.get('Tags', [])))

        for bd in instance.get('BlockDeviceMappings', []):
            if 'Ebs' in bd:
                volume_id = bd['Ebs']['VolumeId']
                for vol in volumes['Volumes']:
                    if vol['VolumeId'] == volume_id:
                        max_ebs_tags = max(max_ebs_tags, len(vol.get('Tags', [])))

        for eni in instance.get('NetworkInterfaces', []):
            for net_if in network_interfaces['NetworkInterfaces']:
                if net_if['NetworkInterfaceId'] == eni['NetworkInterfaceId']:
                    max_eni_tags = max(max_eni_tags, len(net_if.get('TagSet', [])))

# Step 2: Populate data rows
for reservation in instances['Reservations']:
    for instance in reservation['Instances']:
        instance_id = instance['InstanceId']
        instance_type = instance['InstanceType']
        ami_id = instance['ImageId']
        iam_instance_profile_arn = instance.get('IamInstanceProfile', {}).get('Arn', 'N/A')
        iam_instance_profile = iam_instance_profile_arn.split('/')[-1] if iam_instance_profile_arn != 'N/A' else 'N/A'
        imdsv2 = 'Enabled' if instance.get('MetadataOptions', {}).get('HttpTokens') == 'required' else 'Disabled'
        availability_zone = instance['Placement']['AvailabilityZone']
        private_ip = instance.get('PrivateIpAddress', 'N/A')
        monitoring_state = instance.get('Monitoring', {}).get('State', 'disabled')
        source_dest_check = instance.get('SourceDestCheck', True)
        instance_name = next((tag['Value'] for tag in instance.get('Tags', []) if tag['Key'] == 'Name'), '')

        ebs_volume_type = ''
        ebs_volume_size = ''
        ebs_encrypted = ''
        iops = 3000
        throughput = 125

        if 'BlockDeviceMappings' in instance:
            for bd in instance['BlockDeviceMappings']:
                if 'Ebs' in bd:
                    volume_id = bd['Ebs']['VolumeId']
                    for vol in volumes['Volumes']:
                        if vol['VolumeId'] == volume_id:
                            ebs_volume_type = vol['VolumeType']
                            ebs_volume_size = vol['Size']
                            ebs_encrypted = 'true' if vol.get('Encrypted') else 'false'

                            ebs_volume_type = 'gp3'

                            # スループットを計算:
                            # ボリュームサイズが170 GiB未満の場合、スループットは125 MiB/s
                            # ボリュームサイズが170 GiBから300 GiBの間では、スループットはボリュームサイズ * 0.75 (最大250 MiB/s)
                            # ボリュームサイズが300 GiB以上の場合、スループットは250 MiB/s
                            if ebs_volume_size <= 170:
                                throughput = 125
                            elif ebs_volume_size <= 300:
                                throughput = min(250, ebs_volume_size * 0.75)
                            else:
                                throughput = 250

                            # IOPS計算
                            if ebs_volume_size <= 1000:
                                iops = 3000
                            else:
                                iops = min(16000, 3000 + (ebs_volume_size - 1000) * 3)

        instance_tags = instance.get('Tags', [])
        ebs_tags = []
        eni_tags = []

        for bd in instance.get('BlockDeviceMappings', []):
            if 'Ebs' in bd:
                volume_id = bd['Ebs']['VolumeId']
                for vol in volumes['Volumes']:
                    if vol['VolumeId'] == volume_id:
                        ebs_tags.extend(vol.get('Tags', []))

        for eni in instance.get('NetworkInterfaces', []):
            for net_if in network_interfaces['NetworkInterfaces']:
                if net_if['NetworkInterfaceId'] == eni['NetworkInterfaceId']:
                    eni_tags.extend(net_if.get('TagSet', []))

        key_name = instance.get('KeyName', 'N/A')
        subnet_id = instance.get('SubnetId', 'N/A')
        security_groups = [sg['GroupId'] for sg in instance.get('SecurityGroups', [])]
        security_group_ids = ','.join(security_groups)

        row = [
            instance_id, instance_name, availability_zone, private_ip, instance_type,
            ami_id, iam_instance_profile, imdsv2, ebs_volume_type, ebs_volume_size, iops, throughput, ebs_encrypted,
            key_name, subnet_id, security_group_ids, monitoring_state, source_dest_check
        ]

        def extend_with_tags(row, tags, max_tags):
            data = []
            for tag in tags:
                data.append(tag['Key'])
                data.append(tag['Value'])
            data.extend(['', ''] * (max_tags - len(tags)))
            row.extend(data)

        extend_with_tags(row, instance_tags, max_instance_tags)
        extend_with_tags(row, ebs_tags, max_ebs_tags)
        extend_with_tags(row, eni_tags, max_eni_tags)

        csv_data.append(row)

# Dynamic headers
for i in range(1, max_instance_tags + 1):
    csv_headers.append(f'Instance_Tag{i}_Key')
    csv_headers.append(f'Instance_Tag{i}_Value')

for i in range(1, max_ebs_tags + 1):
    csv_headers.append(f'EBS_Tag{i}_Key')
    csv_headers.append(f'EBS_Tag{i}_Value')

for i in range(1, max_eni_tags + 1):
    csv_headers.append(f'ENI_Tag{i}_Key')
    csv_headers.append(f'ENI_Tag{i}_Value')

# Save to CSV
with open('Parameter.csv', 'w', newline='', encoding='latin-1') as f:
    writer = csv.writer(f)
    writer.writerow(csv_headers)
    writer.writerows(csv_data)

このスクリプトを実行すると、アカウント内のEC2インスタンスの情報が Parameter.csv として出力されます。

その後、不要なEC2の行を削除し、対象となるEC2のみを残します。

次は下記の流れになります。

create_cfn_yaml.py
        ↓
Parameter.csvを参照
        ↓
CloudFormationテンプレートの出力

Parameter.csv

Parameter.csv には EC2構築に必要な情報が入っています。 基本的には、AWS CLI の describe コマンドで取得した情報がそのまま出力されます。 設定を変更したい場合は、CSVの内容を手動で編集することで調整できます。

主な項目は以下です。

項目 内容 説明
InstanceId 元となるEC2 構築元となるEC2インスタンスのIDです。
InstanceName インスタンス名 新規EC2の Name タグの値です。既存インスタンスと同じ名前になるため、必要に応じて新しいインスタンス名を指定してください。
AvailabilityZone アベイラビリティゾーン 既存EC2が配置されているAZです。
PrivateIpAddress 固定IP 既存のプライベートIPです。変更も可能です。値を削除した場合は、新しいプライベートIPが自動で割り当てられます。※既存のIPから変更しない場合は、そのIPを保持しているENIを削除する必要があります。
InstanceType インスタンスタイプ 新しく作成するEC2のインスタンスタイプです。必要に応じて変更できます。
AMI_ID 使用するAMI 新しいEC2で使用するAMIです。ゴールデンイメージや希望するAMI IDを指定できます。
IamInstanceProfile IAMプロファイルの指定 EC2に関連付けるIAMインスタンスプロファイルです。不要な場合は環境に応じて調整します。
IMDSv2 IMDS設定 IMDSv2を有効化する場合は Enabled を指定します。不要な場合は Disabled を指定します。
EBS_Volume_Type EBSタイプ ルートボリュームのタイプです。本スクリプトでは gp2 / gp3 を想定しています。
EBS_Volume_Size EBSサイズ ルートボリュームのサイズ(GiB)です。必要に応じて変更できます。
IOPS EBS IOPS EBSのIOPS値です。ボリュームサイズをもとにスクリプト内で計算されます。必要に応じて調整してください。
Throughput EBSスループット gp3利用時のスループット値です。ボリュームサイズをもとにスクリプト内で計算されます。
EBS_Encrypted EBS暗号化設定 EBSの暗号化する場合は true を指定します。
KeyName EC2キーペア EC2に設定するキーペア名です。
SubnetId サブネット 新しいEC2を配置するサブネットです。
SecurityGroupIds セキュリティグループ EC2に関連付けるセキュリティグループIDです。複数ある場合はカンマ区切りで指定されます。
Monitoring 詳細モニタリング設定 EC2の詳細モニタリング設定です。enabled の場合は CloudWatch の詳細モニタリング(1分間隔)が有効になります。disabled の場合は標準モニタリング(5分間隔)になります。
SourceDestCheck 送信元/送信先チェック EC2の Source/Destination Check の設定です。NATインスタンスなどでは無効化が必要な場合があります。
Instance_TagX_Key EC2タグキー EC2インスタンスに設定されているタグのキーです。
Instance_TagX_Value EC2タグ値 EC2インスタンスに設定されているタグの値です。
EBS_TagX_Key EBSタグキー EBSボリュームに設定されているタグのキーです。
EBS_TagX_Value EBSタグ値 EBSボリュームに設定されているタグの値です。
ENI_TagX_Key ENIタグキー ENIに設定されているタグのキーです。
ENI_TagX_Value ENIタグ値 ENIに設定されているタグの値です。

上記のCSVでは、InstanceType の変更や AMI の指定、既存の Private IP をそのまま利用するか、新しい Private IP を割り当てるかといった調整が可能です。
必要に応じてCSVの内容を修正し、不要なEC2の行を削除した上で、次のステップに進みます。

実行方法

スクリプトは以下のように実行します。

$ python3 create_cfn_yaml.py
import csv
import os

def read_userdata_csv(file_path):
    userdata = []
    with open(file_path, 'r', encoding='latin-1') as csvfile:
        reader = csv.DictReader(csvfile)
        for row in reader:
            userdata.append({
                'InstanceID': row['Instance ID'],
                # 'Username': row['Username'],
                # 'Sudo': row['sudo'].strip().upper() == 'TRUE',
                'PublicKey': row['public-key']
            })
    return userdata

def read_parameter_csv(file_path):
    parameters = []
    with open(file_path, 'r', encoding='latin-1') as csvfile:
        reader = csv.DictReader(csvfile)
        for row in reader:
                    # データを不正な文字からクリーニングします
            cleaned_row = {key: value.replace('Â', '').strip() if value else None for key, value in row.items()}
            parameters.append(row)
    return parameters

def generate_userdata_section(userdata, instance_id):
    useradd_section = """
timedatectl set-timezone Asia/Tokyo

"""

    useradd_commands = []
    settingsudo_commands = []

    for user in userdata:
        if user['InstanceID'] == instance_id:
            # username = user['Username']
            ssh_key = user['PublicKey']
            # useradd_commands.append(f'create_user {username} "{ssh_key}"')
            useradd_commands.append(f'register_publickey_ec2-user "{ssh_key}"')
            # if user['Sudo']:
            #     settingsudo_commands.append(f'settingsudo "{username}"')

    return useradd_section\
    + "\n".join(useradd_commands)
    # + "\n" + "\n".join(settingsudo_commands)

def format_tags(tags, indent=8):
    indent_space = " " * indent
    return "\n".join([f"{indent_space}- Key: {tag['Key']}\n{indent_space}  Value: {tag['Value']}" for tag in tags])


def generate_cloudformation_template(parameters, userdata, output_dir):
    templates = []
    for parameter in parameters:
        instance_id = parameter["InstanceId"]
        name_tag = parameter.get("InstanceName", "")
        private_ip = parameter.get("PrivateIpAddress", "").strip()

        # IMDSv2に基づいてHttpTokensの値を設定します
        http_tokens = "required" if parameter.get("IMDSv2", "").strip().lower() == "enabled" else "optional"

        userdata_script = generate_userdata_section(userdata, instance_id)
        userdata_script = userdata_script.replace("\n", "\n            ")

        security_group_ids = "\n".join([f"        - {sg.strip()}" for sg in parameter["SecurityGroupIds"].split(',')])

        # EC2インスタンスのタグを生成します
        instance_tags = []
        for i in range(1, 101):
            tag_key = parameter.get(f"Instance_Tag{i}_Key")
            tag_value = parameter.get(f"Instance_Tag{i}_Value")
            if tag_key and tag_value:
                # aws: から始まるタグと TestTag タグを除外
                if not tag_key.strip().startswith("aws:") and tag_key.strip() != "TestTag":
                    instance_tags.append({"Key": tag_key.strip(), "Value": tag_value.strip()})

        # InstanceNameが空でない場合は、Nameタグを追加します
        if name_tag and not any(tag['Key'] == 'Name' for tag in instance_tags):
            instance_tags.append({"Key": "Name", "Value": name_tag.strip()})

        # 起動テンプレート用のタグを生成します
        ebs_tags = []
        eni_tags = []
        for i in range(1, 101):
            ebs_tag_key = parameter.get(f"EBS_Tag{i}_Key")
            ebs_tag_value = parameter.get(f"EBS_Tag{i}_Value")
            if ebs_tag_key and ebs_tag_value:
                ebs_tags.append({"Key": ebs_tag_key.strip(), "Value": ebs_tag_value.strip()})

            eni_tag_key = parameter.get(f"ENI_Tag{i}_Key")
            eni_tag_value = parameter.get(f"ENI_Tag{i}_Value")
            if eni_tag_key and eni_tag_value:
                eni_tags.append({"Key": eni_tag_key.strip(), "Value": eni_tag_value.strip()})

        # 動的なタグを含むEC2インスタンスの基本テンプレートを生成します
        template = f"""AWSTemplateFormatVersion: "2010-09-09"
Description: "{name_tag}"
Resources:
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: {parameter.get("AMI_ID")}
      InstanceType: {parameter.get("InstanceType")}
      KeyName: {parameter.get("KeyName")}
      SubnetId: {parameter.get("SubnetId")}
"""

        # private_ipが存在するか確認し、値が空でない場合に行を追加します
        if private_ip:
            template += f"      PrivateIpAddress: {private_ip}\n"

        source_dest_check = parameter.get("SourceDestCheck", "true").strip().lower()
        monitoring = "true" if parameter.get("Monitoring", "disabled").strip().lower() == "enabled" else "false"

        # テンプレートの生成を続けます
        template += f"""      SecurityGroupIds:
{security_group_ids}
      EbsOptimized: true
      SourceDestCheck: {source_dest_check}
      DisableApiTermination: true
      UserData:
        Fn::Base64:
          Fn::Sub: |
            #!/bin/bash -v
            sleep 10
            exec > >(tee /var/log/user-data.log | logger -t user-data -s 2> /dev/console) 2>&1
            {userdata_script.strip()}
            reboot
      IamInstanceProfile: {parameter.get("IamInstanceProfile")}
      Monitoring: {monitoring}
      Tags:
{format_tags(instance_tags)}
      LaunchTemplate:
        LaunchTemplateId: !Ref LaunchTemplate01
        Version: 1
  LaunchTemplate01:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        MetadataOptions:
          HttpTokens: {http_tokens}
        TagSpecifications:
          - ResourceType: volume
            Tags:
{format_tags(ebs_tags, indent=12)}
          - ResourceType: network-interface
            Tags:
{format_tags(eni_tags, indent=12)}
        BlockDeviceMappings:
          - DeviceName: "/dev/xvda"
            Ebs:
              Encrypted: {str(parameter.get("EBS_Encrypted", "false")).lower()}
              VolumeSize: {parameter.get("EBS_Volume_Size", "30")}
              VolumeType: {parameter.get("EBS_Volume_Type", "gp3")}
              DeleteOnTermination: true
              Iops: {parameter.get("IOPS", "3000")}
              Throughput: {parameter.get("Throughput", "125")}
"""

        if name_tag:
            filename = f"{name_tag}({instance_id}).yaml"
        else:
            filename = f"({instance_id}).yaml"

        filename = os.path.join(output_dir, filename)
        templates.append((filename, template))

    return templates

def main():
    parameter_file = 'Parameter.csv'
    userdata_file = 'userdata.csv'

    # ディレクトリ 'yaml' が存在しない場合は作成します
    output_dir = 'yaml'
    os.makedirs(output_dir, exist_ok=True)

    parameters = read_parameter_csv(parameter_file)
    userdata = read_userdata_csv(userdata_file)
    
    templates = generate_cloudformation_template(parameters, userdata, output_dir)
    
    if templates:
        for filename, template in templates:
            with open(filename, 'w') as yaml_file:
                yaml_file.write(template)
            print(f"CloudFormation template saved to '{filename}'.")
    else:
        print("Error generating CloudFormation templates.")

if __name__ == "__main__":
    main()

※1 上記のスクリプトでは、「TestTag」や「aws」から始まるタグ情報はコピーされないようになっています。必要に応じて修正してください。

※2 この記事では、下記のユーザーデータをスクリプトに設定しています。必要に応じて修正してください。

#!/bin/bash -v
sleep 10
exec > >(tee /var/log/user-data.log | logger -t user-data -s 2> /dev/console) 2>&1
timedatectl set-timezone Asia/Tokyo
reboot

なお、EC2構築と同時に Linux 内の ec2-user に公開鍵を追加したい場合は、以下の設定を追記することで対応できます。

以前のバージョンのスクリプトでは、userdata.csv にデータを追加することで、Public Key の登録だけでなく新しいユーザーの作成にも対応していました。 本記事では説明をシンプルにするため、該当する処理はコメントアウトしています。 必要に応じてスクリプトを修正することで、この機能を利用することも可能です。

create_user() {
    useradd $1
    mkdir /home/$1/.ssh
    chmod 700 /home/$1/.ssh
    chown $1:$1 /home/$1/.ssh
    echo $2 > /home/$1/.ssh/authorized_keys
    chmod 600 /home/$1/.ssh/authorized_keys
    chown $1:$1 /home/$1/.ssh/authorized_keys
}

settingsudo() {
    echo "$1 ALL = NOPASSWD: ALL" >> /etc/sudoers.d/$1
    chmod 440 /etc/sudoers.d/$1
    chown root:root /etc/sudoers.d/$1
}

実行すると「yaml/」ディレクトリに{インスタンス名(インスタンスID)}のファイルが生成されます。

CloudFormation template saved to 'yaml/Bastion(i-0606f85f472df06ba).yaml'.
CloudFormation template saved to 'yaml/DC1.test.local(i-01c7fd724682ffe9a).yaml'.
CloudFormation template saved to 'yaml/DC2.test.local(i-0160d258bd82a9356).yaml'.
CloudFormation template saved to 'yaml/DC3.test.local(i-00f31a7ac8e70017f).yaml'.
CloudFormation template saved to 'yaml/onpremDNS(i-0ae18790cf80003f7).yaml'.
CloudFormation template saved to 'yaml/testwin(i-086610811b0ba8b87).yaml'.

つまり、EC2インスタンスごとに1つのCloudFormationテンプレートが作成されます。

CFnテンプレートの出力例を紹介します。

AWSTemplateFormatVersion: "2010-09-09"
Description: "Bastion"
Resources:
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-051cf04206
      InstanceType: t2.micro
      KeyName: ke
      SubnetId: subnet-09
      PrivateIpAddress: 10.123.10.15
      SecurityGroupIds:
        - sg-01f249b28e
        - sg-028744e32
      EbsOptimized: true
      SourceDestCheck: true
      DisableApiTermination: true
      UserData:
        Fn::Base64:
          Fn::Sub: |
            #!/bin/bash -v
            sleep 10
            exec > >(tee /var/log/user-data.log | logger -t user-data -s 2> /dev/console) 2>&1
            timedatectl set-timezone Asia/Tokyo
            reboot
      IamInstanceProfile: base
      Monitoring: false
      Tags:
        - Key: insttag7
          Value: 7
        - Key: insttag4
          Value: 4
        - Key: insttag5
          Value: 5
        - Key: Name
          Value: Bastion
        - Key: insttag2
          Value: 2
        - Key: insttag3
          Value: 3
        - Key: insttag8
          Value: 8
        - Key: insttag1
          Value: 1
        - Key: insttag6
          Value: 6
      LaunchTemplate:
        LaunchTemplateId: !Ref LaunchTemplate01
        Version: 1
  LaunchTemplate01:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        MetadataOptions:
          HttpTokens: optional
        TagSpecifications:
          - ResourceType: volume
            Tags:
              - Key: ebsttag5
                Value: 5
              - Key: ebsttag3
                Value: 3
              - Key: ebsttag6
                Value: 6
              - Key: ebsttag1
                Value: 1
              - Key: ebsttag4
                Value: 4
              - Key: Name
                Value: bastion-root
              - Key: TestEBSTags
                Value: EBSTags
              - Key: ebsttag2
                Value: 2
          - ResourceType: network-interface
            Tags:
              - Key: env
                Value: test
              - Key: enitag4
                Value: 4
              - Key: enitag1
                Value: 1
              - Key: enitag2
                Value: 2
              - Key: enitag5
                Value: 5
              - Key: enitag6
                Value: 6
              - Key: testenitag
                Value: testtag
              - Key: enitag3
                Value: 3
              - Key: name
                Value: bastion
        BlockDeviceMappings:
          - DeviceName: "/dev/xvda"
            Ebs:
              Encrypted: false
              VolumeSize: 8
              VolumeType: gp3
              DeleteOnTermination: true
              Iops: 3000
              Throughput: 125

ユーザーデータのログは /var/log/user-data.log に出力されます。
EC2展開後は /var/log/user-data.log を確認することでログを確認できます。

CloudFormationでEC2作成

生成されたテンプレートをCloudFormationでデプロイすることで新しいEC2インスタンスを作成できます。

EBS、ENIタグやEBSやIMDS等の詳細設定のために LaunchTemplate01 が作成されるため、不要なリソースを残さないようLaunchTemplate01を削除しておくことをおすすめします。

この方法を使うことで既存設定を再利用や再構築などが効率よく実施できます。

以上、御一読ありがとうございました。

本田 イーゴリ (記事一覧)

カスタマーサクセス部

・2024 Japan AWS Top Engineers (Security)
・AWS SAP, DOP, SCS, DBS, SAA, DVA, COA, CLF, ANS, AIF, MLS, MLA, DEA
・Azure AZ-900
・EC-Council CCSE

趣味:日本国内旅行(47都道府県制覇)・ドライブ・音楽