😺

AWS CloudTrailイベント履歴を使ってサービスの消し忘れを探すアイデアの検証

2023/12/12に公開

はじめに

こんにちは!みゃっちーです。
現在大学4年生でクラウドエンジニアを目指し、日々AWSの勉強に取り組んでいます。

自分が独学で1からAWSを勉強していく中で一番怖いのが、サービスの消し忘れで多額の料金が発生することでした。

  • 一定の時間になったらチェックする方法
  • 料金が発生した場合にAWS budgetで通知する方法

などの方法が対策としてはあるものの、すぐに今のユーザでどのサービスが立ち上がったままなのか簡単に見る方法はなかなか紹介されておらず、AWSを始める足かせになってると感じました。その問題をできる限り簡単に、料金が発生しない方法で解決できないかと少し考えてみました。

考えた解決策

まず初めにリソースの消し忘れを確認するアプローチとしての2つが考え付きました。

  • AWS上の全リソースを取得する
  • ユーザの操作履歴を確認する

前者のAWS上の全リソースを取得するアプローチはConfigのbatch-get-resource-configを使って実現している先駆者の方がいらっしゃいました。
https://zenn.dev/osprey/articles/get-all-aws-resources

こちらでもいいのですが、今回は後者のアプローチでCloudTrailのユーザ操作履歴を解析して、一定期間の間に作成操作をしているけど削除操作をしていないリソースを特定する。という手法を検証してみました。成果物はシェルスクリプトとしてAWS CLI上で実行できるものを目指します。

検証結果

試しにEC2を2つ、RDSを1つ起動。
※EC2は3つ立ち上げて、1つ削除
※現段階で対応サービスはEC2インスタンスとRDSのみ

スクリプト実行結果

CLI上で実行し、このような出力が得られました。

----------------EC2instance----------------
Instance ID: i-09cb3fb50d2910206 Instance Type: t2.micro
URL : https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#InstanceDetails:instanceId=i-09cb3fb50d2910206
Instance ID: i-0cbfba7d810383e2b Instance Type: t2.micro
URL : https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#InstanceDetails:instanceId=i-0cbfba7d810383e2b
----------------RDScluster----------------
Instance ID: database-1-instance-1 Instance Type: db.r5.large
URL : https://us-east-1.console.aws.amazon.com/rds/home?region=us-east-1#database:id=database-1-instance-1

作成したスクリプト

今回作成したスクリプトはこちらです。
https://github.com/FujiiHirokl/AWS_resource_check/blob/main/aws_resource_check.sh

使い方

  1. AWS CloudShellを開きます
  2. シェルスクリプトをアップロードします
  3. 実行します
. aws_resource_check.sh {取得分数} {ユーザ名}
  1. 検出できます
----------------EC2instance----------------
Instance ID: i-09cb3fb50d2910206 Instance Type: t2.micro
URL : https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#InstanceDetails:instanceId=i-09cb3fb50d2910206
Instance ID: i-0cbfba7d810383e2b Instance Type: t2.micro
URL : https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#InstanceDetails:instanceId=i-0cbfba7d810383e2b
----------------RDScluster----------------
Instance ID: database-1-instance-1 Instance Type: db.r5.large
URL : https://us-east-1.console.aws.amazon.com/rds/home?region=us-east-1#database:id=database-1-instance-1
  1. URLを開くとそのサービスのコンソール画面に行くことができます

スクリプト解説

今回のスクリプトの処理の流れは大まかにこのようになっています。

  1. CloudTrailイベント履歴の取得
  2. 消し忘れリソースの特定
  3. リソースの表示

1.CloudTrailイベント履歴の取得

https://github.com/FujiiHirokl/AWS_resource_check/blob/main/aws_resource_check.sh#L1-L25

aws cloudtrail lookup-events APIを使ってイベント履歴の取得を行います。取得できるjsonフォーマット、オプション指定などの詳細はこちらを見てください。

https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/lookup-events.html#output

こちらが実際に取得できるデータサンプルの1つです。

EC2作成イベントサンプルjsonデータ
{
    "EventId": "xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx",
    "EventName": "RunInstances",
    "ReadOnly": "false",
    "AccessKeyId": "xxxxxxxxxxxxxxxx",
    "EventTime": "2023-12-06T02:06:07+00:00",
    "EventSource": "ec2.amazonaws.com",
    "Username": "root",
    "Resources": [
        {
            "ResourceType": "AWS::EC2::VPC",
            "ResourceName": "vpc-xxxxxxxxxxxxxx"
        },
        {
            "ResourceType": "AWS::EC2::Ami",
            "ResourceName": "ami-xxxxxxxxxxxxxxx"
        },
        {
            "ResourceType": "AWS::EC2::NetworkInterface",
            "ResourceName": "eni-xxxxxxxxxxxxxxx"
        },
        {
            "ResourceType": "AWS::EC2::Instance",
            "ResourceName": "i-xxxxxxxxxxxxxxx"
        },
        {
            "ResourceType": "AWS::EC2::SecurityGroup",
            "ResourceName": "launch-wizard-10"
        },
        {
            "ResourceType": "AWS::EC2::SecurityGroup",
            "ResourceName": "sg-xxxxxxxxxxxxxxx"
        },
        {
            "ResourceType": "AWS::EC2::Subnet",
            "ResourceName": "subnet-xxxxxxxxxxxxxxx"
        }
    ],
    "CloudTrailEvent": "{
    "eventVersion": "1.08",
    "userIdentity": {
        "type": "Root",
        "principalId": "xxxxxxxxxxxxxxx",
        "arn": "arn:aws:iam::xxxxxxxxxxxxxxx:root",
        "accountId": "xxxxxxxxxxxxxxx",
        "accessKeyId": "xxxxxxxxxxxxxxx",
        "sessionContext": {
            "sessionIssuer": {},
            "webIdFederationData": {},
            "attributes": {
                "creationDate": "2023-12-06T01:46:28Z",
                "mfaAuthenticated": "true"
            }
        }
    },
    "eventTime": "2023-12-06T02:06:07Z",
    "eventSource": "ec2.amazonaws.com",
    "eventName": "RunInstances",
    "awsRegion": "us-east-1",
    "sourceIPAddress": "xxxxxxxxxxxxxxx",
    "userAgent": "AWS Internal",
    "requestParameters": {
        "instancesSet": {
            "items": [
                {
                    "imageId": "ami-xxxxxxxxxxxxxxx",
                    "minCount": 1,
                    "maxCount": 1
                }
            ]
        },
        "instanceType": "t2.micro",
        "blockDeviceMapping": {},
        "monitoring": {
            "enabled": false
        },
        "disableApiTermination": false,
        "disableApiStop": false,
        "clientToken": "xxxxxxxxxxxxxxx",
        "networkInterfaceSet": {
            "items": [
                {
                    "deviceIndex": 0,
                    "associatePublicIpAddress": true,
                    "groupSet": {
                        "items": [
                            {
                                "groupId": "sg-xxxxxxxxxxxxxxx"
                            }
                        ]
                    }
                }
            ]
        },
        "ebsOptimized": false,
        "tagSpecificationSet": {
            "items": [
                {
                    "resourceType": "instance",
                    "tags": [
                        {
                            "key": "Name",
                            "value": "dfs"
                        }
                    ]
                }
            ]
        },
        "metadataOptions": {
            "httpTokens": "required",
            "httpPutResponseHopLimit": 2,
            "httpEndpoint": "enabled"
        },
        "privateDnsNameOptions": {
            "hostnameType": "ip-name",
            "enableResourceNameDnsARecord": true,
            "enableResourceNameDnsAAAARecord": false
        }
    },
    "responseElements": {
        "requestId": "xxxxxxxxxxxxxxx",
        "reservationId": "xxxxxxxxxxxxxxx",
        "ownerId": "xxxxxxxxxxxxxxx",
        "groupSet": {},
        "instancesSet": {
            "items": [
                {
                    "instanceId": "i-xxxxxxxxxxxxxxx",
                    "imageId": "ami-xxxxxxxxxxxxxxx",
                    "bootMode": "uefi-preferred",
                    "currentInstanceBootMode": "legacy-bios",
                    "instanceState": {
                        "code": 0,
                        "name": "pending"
                    },
                    "privateDnsName": "ip-xxxxxxxxxxxxxxx.ec2.internal",
                    "amiLaunchIndex": 0,
                    "productCodes": {},
                    "instanceType": "t2.micro",
                    "launchTime": 1701828367000,
                    "placement": {
                        "availabilityZone": "us-east-1c",
                        "tenancy": "default"
                    },
                    "monitoring": {
                        "state": "disabled"
                    },
                    "subnetId": "subnet-xxxxxxxxxxxxxxx",
                    "vpcId": "vpc-xxxxxxxxxxxxxxx",
                    "privateIpAddress": "172.31.18.137",
                    "stateReason": {
                        "code": "pending",
                        "message": "pending"
                    },
                    "architecture": "x86_64",
                    "rootDeviceType": "ebs",
                    "rootDeviceName": "/dev/xvda",
                    "blockDeviceMapping": {},
                    "virtualizationType": "hvm",
                    "hypervisor": "xen",
                    "tagSet": {
                        "items": [
                            {
                                "key": "Name",
                                "value": "dfs"
                            }
                        ]
                    },
                    "clientToken": "xxxxxxxxxxxxxxx",
                    "groupSet": {
                        "items": [
                            {
                                "groupId": "sg-xxxxxxxxxxxxxxx",
                                "groupName": "launch-wizard-10"
                            }
                        ]
                    },
                    "sourceDestCheck": true,
                    "networkInterfaceSet": {
                        "items": [
                            {
                                "networkInterfaceId": "eni-xxxxxxxxxxxxxxx",
                                "subnetId": "subnet-xxxxxxxxxxxxxxx",
                                "vpcId": "vpc-xxxxxxxxxxxxxxx",
                                "ownerId": "xxxxxxxxxxxxxxx",
                                "status": "in-use",
                                "macAddress": "xxxxxxxxxxxxxxx",
                                "privateIpAddress": "xxxxxxxxxxxxxxx",
                                "privateDnsName": "xxxxxxxxxxxxxxx",
                                "sourceDestCheck": true,
                                "interfaceType": "interface",
                                "groupSet": {
                                    "items": [
                                        {
                                            "groupId": "sg-xxxxxxxxxxxxxxx",
                                            "groupName": "launch-wizard-10"
                                        }
                                    ]
                                },
                                "attachment": {
                                    "attachmentId": "eni-attach-xxxxxxxxxxxxxxx",
                                    "deviceIndex": 0,
                                    "networkCardIndex": 0,
                                    "status": "attaching",
                                    "attachTime": 1701828367000,
                                    "deleteOnTermination": true
                                },
                                "privateIpAddressesSet": {
                                    "item": [
                                        {
                                            "privateIpAddress": "xxxxxxxxxxxxxxx",
                                            "privateDnsName": "ip-xxxxxxxxxxxxxxx.ec2.internal",
                                            "primary": true
                                        }
                                    ]
                                },
                                "ipv6AddressesSet": {},
                                "tagSet": {}
                            }
                        ]
                    }
                }
            ]
        },
        "ebsOptimized": false,
        "enaSupport": true,
        "cpuOptions": {
            "coreCount": 1,
            "threadsPerCore": 1
        },
        "capacityReservationSpecification": {
            "capacityReservationPreference": "open"
        },
        "enclaveOptions": {
            "enabled": false
        },
        "metadataOptions": {
            "state": "pending",
            "httpTokens": "required",
            "httpPutResponseHopLimit": 2,
            "httpEndpoint": "enabled",
            "httpProtocolIpv4": "enabled",
            "httpProtocolIpv6": "disabled",
            "instanceMetadataTags": "disabled"
        },
        "maintenanceOptions": {
            "autoRecovery": "default"
        },
        "privateDnsNameOptions": {
            "hostnameType": "ip-name",
            "enableResourceNameDnsARecord": true,
            "enableResourceNameDnsAAAARecord": false
        }
    },
    "requestID": "xxxxxxxxxxxxxxx",
    "eventID": "xxxxxxxxxxxxxxx",
    "readOnly": false,
    "eventType": "AwsApiCall",
    "managementEvent": true,
    "recipientAccountId": "xxxxxxxxxxxxxxx",
    "eventCategory": "Management",
    "sessionCredentialFromConsole": "true"
}"
}

今回はデータ量を削減するためにユーザとログの取得範囲を指定し、解析に必要なイベントリソースごとに分けてjsonファイルを取得しています。全ログを取得してもいいのですが、例えばlookup-events の最大取得数は50件です。50件以降はどうするかというと、その次のページのトークンを発行し、再帰的に呼び出しています。そのためフィルターをかけずにこのAPIを複数回使用するとこのようにログが爆発的に増えます。また解析時間も減らすことができるので分割して保存することにしました。

2.消し忘れリソースの特定

次に取得したjsonファイルから作成イベントと削除イベントを取り出し、消し忘れているリソースを特定します。

少し気を付けないといけないのが取得形式です。作成イベントと削除イベントはEventNameを参照すればいいのですが、EC2インスタンスのIDやタイプなどの詳細情報はCloudTrailEventに文字列でjson形式で入っています。

EventName -> (string)
The name of the event returned.
返されたイベントの名前。

CloudTrailEvent -> (string)
A JSON string that contains a representation of the event returned.
#返されたイベントの表現を含む JSON 文字列。

処理手順

analyze_events関数にjsonファイルと解析情報を渡すと削除されていないリソース情報だけを取り出します。
https://github.com/FujiiHirokl/AWS_resource_check/blob/main/aws_resource_check.sh#L119-L131

analyze_events関数内部の具体的な処理は次のような手順になっています。

  1. JSONファイルの読み込み (load_cloudtrail_data 関数)

    • 指定されたファイルパスからJSONデータを読み込み、Pythonの辞書として返します。
    • 読み込みに失敗した場合はNoneを返します。
  2. イベントデータのフィルタリング (filter_events_by_event_names 関数)

    • 引数で渡されたデータから、指定されたイベント名に基づいてイベントデータをフィルタリングします。
    • イベント名のグループに対して、指定されたイベント名を含むイベントのリストを返します。
  3. 特定の項目の抽出 (extract_specified_items 関数)

    • イベントデータから、指定されたキーのパスに基づいて特定の項目を抽出し、辞書形式で返します。
  4. 重複の除去 (remove_duplicates_from_first_list 関数)

    • 二つのリストを比較し、一つ目のリストから二つ目のリストに存在する要素を除去します。
    • 重複を判定するためのキーを使用し、一つ目のリストから重複を除去した新しいリストを返します。

3.リソースの表示

analyze_events関数の戻り値から取り出したいデータをextract_value_from_json関数で指定することで特定の情報を取り出すことができます。下記はリソースのリージョン情報を取り出したいときの例です。
https://github.com/FujiiHirokl/AWS_resource_check/blob/main/aws_resource_check.sh#L177
このほかにもEC2だと下記のような情報を取得することができます。

ユーザー情報
  • userIdentity.type: ユーザーのタイプ(例: Root)
  • userIdentity.principalId: プリンシパルID
  • userIdentity.arn: Amazon Resource Name
  • userIdentity.accountId: アカウントID
  • userIdentity.accessKeyId: アクセスキーID
  • userIdentity.sessionContext.attributes.creationDate: セッションの作成日
  • userIdentity.sessionContext.attributes.mfaAuthenticated: MFA認証状態
イベント情報
  • eventTime: イベントの発生時刻
  • eventSource: イベントのソース(例: ec2.amazonaws.com)
  • eventName: イベント名(例: RunInstances)
  • awsRegion: AWSリージョン
  • sourceIPAddress: ソースIPアドレス
  • userAgent: ユーザーエージェント
リクエストパラメータ
  • requestParameters.instancesSet.items: 起動したインスタンスのセット
  • requestParameters.instanceType: インスタンスタイプ
  • requestParameters.networkInterfaceSet.items: ネットワークインターフェイス設定
  • requestParameters.tagSpecificationSet.items: タグ設定
  • requestParameters.privateDnsNameOptions: プライベートDNS名のオプション
レスポンスエレメント
  • responseElements.requestId: リクエストID
  • responseElements.reservationId: 予約ID
  • responseElements.ownerId: オーナーID
  • responseElements.instancesSet.items: 起動したインスタンスの詳細情報(インスタンスID、画像ID、状態コードなど)
  • responseElements.networkInterfaceSet.items: ネットワークインターフェイスの詳細情報
その他の情報
  • eventID: イベントID
  • eventType: イベントタイプ
  • recipientAccountId: 受信者アカウントID
  • eventCategory: イベントカテゴリ

これらの情報から必要な情報を選択し表示します。今回はインスタンスIDとインスタンスタイプを表示。また、リージョン情報とインスタンスIDを使用して検知した対象サービスのマネジメントコンソール上のURLを表示するようにしました。

https://github.com/FujiiHirokl/AWS_resource_check/blob/main/aws_resource_check.sh#L170-L185

----------------EC2instance----------------
Instance ID: i-xxxxxxxxxxxxx Instance Type: t2.micro
URL : https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#InstanceDetails:instanceId=i-xxxxxxxxxxxxxx

まとめ

きちんと削除されていないサービスを検知することができました!

また、このCloudtrailイベント履歴は90日間分のデータを自動で記録してくれていて、無料で扱うことができるので安心して使えます。

現状では対応できているサービスが少ないので今後少しづつ増やしていきたいと思います!

ここまで読んでいただきありがとうございました!
これからも利用料に注意しながら楽しいAWSライフを過ごします!

デベキャン

Discussion