EC2インスタンスが突然消えた — CloudTrail Event Historyで削除者を特定する方法

月曜の朝、本番EC2インスタンスが消えていた。Slackに通知が飛んで、誰もやっていないと言う。こういう状況で最初に開くべきツールがCloudTrail Event Historyだ。TerminateInstancesアクションは必ずCloudTrailに記録されており、どのIAMユーザーまたはロールが実行したかを数分で特定できる。

TL;DR — CloudTrailでEC2削除者を特定する

ステップ操作確認ポイント
1Event Historyを開くリージョンが正しいか確認
2イベント名でTerminateInstancesをフィルタ削除時刻の前後30分を絞り込む
3イベント詳細を展開userIdentityフィールドを確認
4CLIで同じクエリを実行インスタンスIDで絞り込み精度を上げる
5再発防止策を実施Termination ProtectionまたはSCP適用

CloudTrail Event Historyの仕組みを理解する

CloudTrailはAWSアカウント内のAPI呼び出しを記録するサービスだ。EC2のTerminateInstancesも例外ではなく、マネジメントコンソール、CLI、SDK、Auto Scalingなど、どの経路で実行されても同じイベントとして記録される。

Event Historyは追加設定なしで有効になっており、過去90日間のマネジメントイベントを保持する。S3バケットへの長期保存にはTrailの設定が必要だが、90日以内の調査であればEvent Historyだけで完結する。

graph LR A["コンソール操作"] --> CT["CloudTrail"] B["AWS CLI"] --> CT C["SDK / アプリ"] --> CT D["Auto Scaling"] --> CT CT --> EH["Event History 過去90日間"] CT --> S3["S3バケット Trail長期保存"] CT --> CWL["CloudWatch Logs リアルタイム監視"] EH --> INV["削除者の特定"]
  1. API呼び出し元 — コンソール、CLI、SDK、Auto Scalingなど複数の経路が存在する
  2. CloudTrail — すべての経路からのAPI呼び出しを記録し、イベントとして保存する
  3. Event History — 90日間のマネジメントイベントをコンソールまたはCLIで検索可能
  4. S3 / CloudWatch Logs — Trailを設定した場合の長期保存先。90日超の調査に必要

重要な点として、CloudTrailのイベントにはリージョン情報が含まれる。EC2インスタンスが存在していたリージョンのEvent Historyを確認しなければ、該当イベントは表示されない。これで見つからないと焦るケースが実際に多い。

ステップ1 — Event Historyでイベントを検索する(コンソール)

まず、削除されたEC2インスタンスが存在していたリージョンに切り替える。リージョンを間違えると何も見つからない。

  1. AWSマネジメントコンソールでCloudTrailを開く
  2. 左メニューから「イベント履歴 (Event History)」を選択する
  3. フィルターのドロップダウンで「イベント名 (Event name)」を選択し、TerminateInstancesと入力する
  4. 時間範囲を、インスタンスが最後に確認できた時刻から現在までに絞り込む
  5. イベントの行をクリックして詳細を展開し、イベントレコード (Event record)のJSONを確認する

JSONの中で最初に確認すべきフィールドはuserIdentityだ。ここに実行者の情報がすべて含まれている。

TerminateInstancesイベントのJSONを読む

イベントレコードのJSONは一見複雑に見えるが、削除者の特定に必要なフィールドは限られている。

🔽 イベントレコードJSONのサンプル(クリックして展開)
{
  "eventVersion": "1.08",
  "userIdentity": {
    "type": "IAMUser",
    "principalId": "AIDAEXAMPLEID",
    "arn": "arn:aws:iam::123456789012:user/taro.yamada",
    "accountId": "123456789012",
    "userName": "taro.yamada"
  },
  "eventTime": "2024-03-15T02:34:56Z",
  "eventSource": "ec2.amazonaws.com",
  "eventName": "TerminateInstances",
  "awsRegion": "ap-northeast-1",
  "sourceIPAddress": "203.0.113.10",
  "userAgent": "aws-cli/2.x Python/3.x",
  "requestParameters": {
    "instancesSet": {
      "items": [
        { "instanceId": "i-0abc123def456789" }
      ]
    }
  },
  "responseElements": {
    "instancesSet": {
      "items": [
        {
          "instanceId": "i-0abc123def456789",
          "currentState": { "code": 32, "name": "shutting-down" },
          "previousState": { "code": 16, "name": "running" }
        }
      ]
    }
  }
}

確認すべきフィールドの意味を整理する。

フィールド意味注目ポイント
userIdentity.type実行者の種別IAMUserAssumedRoleAWSServiceなど
userIdentity.arn実行者のARNユーザー名またはロール名が含まれる
userIdentity.userNameIAMユーザー名typeがIAMUserの場合のみ存在
sourceIPAddress呼び出し元IPアドレスコンソール操作の場合はAWSのIPになることがある
userAgent使用したクライアントCLIかコンソールかSDKかを判別できる
requestParameters.instancesSet対象インスタンスID複数インスタンスの一括削除も記録される

userIdentity.typeAssumedRoleの場合、sessionContext.sessionIssuerフィールドにロールのARNが記録される。EC2インスタンスプロファイルやLambdaからの実行はこのパターンになる。

IAMロールの一時認証情報で実行された操作は、userIdentity.typeAssumedRoleになる。この場合、誰がそのロールをAssumeしたかはsessionContextを辿る必要がある。ロール名だけ見て「サービスが消した」と結論づけると、実際には人間がそのロールを使って実行していたケースを見逃す。

ステップ2 — CLIでTerminateInstancesイベントを検索する

コンソールのEvent Historyはフィルタリングが限定的で、大量のイベントがある環境では目的のレコードを見つけにくい。CLIを使えばインスタンスIDで直接絞り込める。これがコンソールで見つからないときの次の手だ。

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=TerminateInstances \
  --start-time "2024-03-14T00:00:00Z" \
  --end-time "2024-03-16T00:00:00Z" \
  --region ap-northeast-1 \
  --output json

特定のインスタンスIDに絞り込む場合は、ResourceName属性を使う。

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue=i-0abc123def456789 \
  --region ap-northeast-1 \
  --output json

lookup-eventsコマンドは1回のリクエストで最大50件を返す。--max-results--next-tokenでページネーションを制御できる。また、このコマンドはEvent Historyと同じ90日制限が適用される。

出力から実行者情報だけを抽出したい場合は、jqを組み合わせると効率的だ。

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=TerminateInstances \
  --region ap-northeast-1 \
  --output json | jq '.Events[].CloudTrailEvent | fromjson | {eventTime, userIdentity, sourceIPAddress}'

ステップ3 — AssumedRoleの場合に実行者を特定する

Auto ScalingやECSなどのAWSサービスがインスタンスを終了させた場合、または誰かがIAMロールをAssumeして実行した場合、userIdentity.typeAssumedRoleになる。このケースで実行者を特定するにはsessionContextを確認する。

graph TD START["TerminateInstancesイベント検出"] --> CHECK{"userIdentity.type"} CHECK -->|"IAMUser"| USER["userIdentity.userName で即座に特定"] CHECK -->|"AssumedRole"| ROLE["sessionContext.sessionIssuer でロールARNを確認"] CHECK -->|"AWSService"| SVC["invokedByフィールドで サービス名を確認"] ROLE --> INVOKED{"invokedByが 存在するか"} INVOKED -->|"あり"| AWSSVC["AWSサービスによる 自動実行と判断"] INVOKED -->|"なし"| HUMAN["ロールをAssumeした ユーザーをさらに調査"]
  1. typeの確認IAMUserであればuserNameフィールドで即座に特定できる
  2. AssumedRoleの場合sessionContext.sessionIssuerにロールのARNが記録されている
  3. AWSServiceの場合 — Auto ScalingやEC2 Fleet等のサービスが自動的に実行した可能性がある。invokedByフィールドにサービス名が記録される
  4. ロール名の確認 — ロール名から用途を判断し、そのロールを誰がAssumeしたかをさらに調査する

Auto Scalingによる終了の場合、userIdentity.invokedByautoscaling.amazonaws.comが記録される。これは意図した動作である可能性が高いが、スケールインポリシーの設定ミスで本番インスタンスが消えるケースも実際にある。

実際の障害パターン — 「誰も消していない」が嘘ではなかった件

症状: 深夜2時にEC2インスタンスが1台消えた。チームメンバー全員が「操作していない」と言う。CloudTrailを確認するとTerminateInstancesのイベントが存在し、userIdentity.typeAssumedRole、ARNにはrole/developer-roleが含まれていた。

最初の誤診: 誰かがdeveloper-roleを使って手動で削除したと判断し、チームメンバーへの聞き込みを続けた。

実際の原因: sessionContext.sessionIssuerをさらに確認すると、invokedByフィールドにspotfleet.amazonaws.comが記録されていた。Spot Fleetがスポットインスタンスの中断に伴い自動的に終了処理を実行していたのだ。developer-roleはSpot Fleetのサービスリンクロールに付与されていた権限の一部だった。

修正: Spot Fleetの設定を確認し、中断通知の処理とインスタンス置き換えポリシーを見直した。CloudTrailのイベントを最後まで読まずにuserIdentity.arnだけで判断したのが失敗だった。イベントレコードは必ずinvokedByまで確認する。

ステップ4 — 90日を超えた調査にはS3のTrailログを使う

Event Historyの保持期間は90日だ。それ以前の削除を調査する場合、またはより詳細なデータイベントが必要な場合は、S3に保存されたTrailログを直接検索する必要がある。

TrailがS3に保存するログはGzip圧縮されたJSONファイルだ。aws s3 cpでダウンロードしてjqで検索するか、Amazon Athenaを使ってSQLクエリで検索できる。

aws s3 ls s3://your-cloudtrail-bucket/AWSLogs/123456789012/CloudTrail/ap-northeast-1/2024/03/15/ \
  --region ap-northeast-1

Athenaを使う場合は、AWSが提供するCloudTrail用のテーブル定義を使ってデータベースを作成し、以下のようなクエリで検索できる。

SELECT
  eventtime,
  useridentity.arn,
  useridentity.type,
  sourceipaddress,
  useragent,
  requestparameters
FROM cloudtrail_logs
WHERE eventname = 'TerminateInstances'
  AND awsregion = 'ap-northeast-1'
  AND eventtime BETWEEN '2024-03-14' AND '2024-03-16'
ORDER BY eventtime DESC;

再発防止 — Termination Protectionと予防的コントロール

削除者を特定した後は、同じ事故が起きない仕組みを入れる。調査で終わらせてはいけない。

EC2 Termination Protectionを有効にする

aws ec2 modify-instance-attribute \
  --instance-id i-0abc123def456789 \
  --disable-api-termination \
  --region ap-northeast-1

Termination Protectionが有効かどうかを確認する

aws ec2 describe-instance-attribute \
  --instance-id i-0abc123def456789 \
  --attribute disableApiTermination \
  --region ap-northeast-1

Termination Protectionは有効な防御だが、ec2:ModifyInstanceAttribute権限を持つユーザーが無効化してから削除できる点に注意が必要だ。より強固な制御が必要な場合は、AWS OrganizationsのSCP(Service Control Policy)でec2:TerminateInstancesを特定のロールのみに制限するアプローチが有効だ。

また、CloudTrailのイベントをCloudWatch Logsに転送し、TerminateInstancesイベントに対してメトリクスフィルターとアラームを設定することで、次回からはリアルタイムで通知を受け取れる。

CloudTrail Event Historyで削除者を特定するまとめ

EC2インスタンスのCloudTrail Event History調査は、正しいリージョンでTerminateInstancesイベントを検索し、userIdentityフィールドを読むだけで完結する。ただし、AssumedRoleの場合はsessionContextinvokedByまで確認しないと実行者を誤認するリスクがある。90日以内であればEvent Historyで十分だが、長期保存にはS3へのTrail設定が必要だ。

調査後は必ずTermination Protectionの有効化と、必要に応じたSCPによる予防的コントロールを実施する。インシデント対応は特定で終わりではなく、再発防止まで含めて完了だ。

用語集

用語説明
Event HistoryCloudTrailが提供する過去90日間のマネジメントイベント検索機能。追加設定不要で利用できる。
userIdentityCloudTrailイベントレコード内のフィールド。API呼び出しを実行したIAMエンティティの情報を含む。
AssumedRoleIAMロールの一時認証情報を使用してAPI呼び出しが行われた場合のuserIdentity.type値。
Termination ProtectionEC2インスタンスに設定できる保護機能。有効にするとAPIまたはコンソールからの終了操作をブロックする。
TrailCloudTrailのイベントをS3バケットやCloudWatch Logsに継続的に配信する設定。90日超の保存に必要。

コメント

このブログの人気の投稿

EC2 SSH接続タイムアウトの原因と修正方法 — セキュリティグループのインバウンドルール完全ガイド

S3パブリックアクセス拒否の原因と解決策:バケットレベルの「Block Public Access」が優先される仕組み

EC2インスタンスIDをメタデータから取得する方法 — IMDSv2が安全な理由