CloudTrail の eventType を実際のログで理解する ── ログインと VPC 作成を例に
AWS CloudTrail には eventType というフィールドがあり、記録されたイベントの種類を分類する。
本記事では マネジメントコンソールにログインして VPC を作成する という一連の操作を実際に行い、取得したログをもとに eventType の違いを解説する。
eventType の種類
以下は公式ドキュメントの説明をそのまま引用したもの。
| eventType | 説明(公式ドキュメントより) |
|---|---|
AwsApiCall |
An API was called. |
AwsServiceEvent |
The service generated an event related to your trail. For example, this can occur when another account made a call with a resource that you own. |
AwsConsoleAction |
An action was taken in the console that was not an API call. |
AwsConsoleSignIn |
A user in your account (root, IAM, federated, SAML, or SwitchRole) signed in to the AWS Management Console. |
AwsVpceEvents |
CloudTrail network activity events enable VPC endpoint owners to record AWS API calls made using their VPC endpoints from a private VPC to the AWS service. To record network activity events, the VPC endpoint owner must enable network activity events for the event source. |
検証環境
- IAM ユーザー:
example-user - 操作内容:
- マネジメントコンソールにログイン(
2026-06-20T12:55:16Z) - ap-northeast-1 で VPC(名前:
hogehoge、CIDR:10.0.0.0/24)を作成(2026-06-20T12:55:39Z)
- マネジメントコンソールにログイン(
1. AwsConsoleSignIn ── コンソールへのログイン
コンソールにサインインすると eventType: AwsConsoleSignIn のイベントが記録される。
{
"eventVersion": "1.11",
"userIdentity": {
"type": "IAMUser",
"principalId": "AIDAXXXXXXXXXXXXXXXXX",
"arn": "arn:aws:iam::123456789012:user/example-user",
"accountId": "123456789012",
"userName": "example-user"
},
"eventTime": "2026-06-20T12:55:16Z",
"eventSource": "signin.amazonaws.com",
"eventName": "ConsoleLogin",
"awsRegion": "ap-southeast-2",
"sourceIPAddress": "xxx.xxx.xxx.xxx",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
"requestParameters": null,
"responseElements": {
"ConsoleLogin": "Success"
},
"additionalEventData": {
"LoginTo": "https://console.aws.amazon.com/console/home?...",
"MobileVersion": "No",
"MFAUsed": "No"
},
"eventID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"readOnly": false,
"eventType": "AwsConsoleSignIn",
"managementEvent": true,
"recipientAccountId": "123456789012",
"eventCategory": "Management",
"tlsDetails": {
"tlsVersion": "TLSv1.3",
"cipherSuite": "TLS_AES_128_GCM_SHA256",
"clientProvidedHostHeader": "ap-southeast-2.signin.aws.amazon.com"
}
}
ポイント
| フィールド | 値 | 意味 |
|---|---|---|
eventType |
AwsConsoleSignIn |
サインインイベント専用の分類 |
eventSource |
signin.amazonaws.com |
サインインサービスからの記録 |
eventName |
ConsoleLogin |
イベント名(固定) |
responseElements.ConsoleLogin |
Success |
ログイン成否(失敗時は Failure) |
additionalEventData.MFAUsed |
No |
MFA 未使用(有効なら Yes) |
additionalEventData.LoginTo |
URL | ログイン後のリダイレクト先 |
requestParameters |
null |
ログインにリクエストパラメータなし |
ログアウトは記録されない
コンソールからログアウトしても CloudTrail にイベントは記録されない。ログアウトはブラウザ側でセッション Cookie を破棄するだけで、AWS のサーバーへの API 呼び出しが発生しないため。
なぜ awsRegion が ap-southeast-2 なのか?
ConsoleLogin イベントの awsRegion はブラウザが接続した signin エンドポイントのリージョン が記録される。これはコンソールが最終的に操作するリージョンとは無関係。
今回の操作では ap-southeast-2.signin.aws.amazon.com に接続してログインし、その後 ap-northeast-1 で VPC を作成している。lookup-events コマンドのデフォルトリージョン(ap-northeast-1)では ConsoleLogin イベントが見つからないため、全リージョンを確認する必要がある。
# すべてのリージョンを確認する必要がある
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=ConsoleLogin \
--start-time "2026-06-20T00:00:00Z" \
--region ap-southeast-2
2. AwsApiCall ── VPC 作成(書き込み系)
コンソールの「VPC を作成」ボタンを押すと、内部的に EC2 API(CreateVpc)が呼び出される。コンソール操作であっても API 呼び出しは AwsApiCall に分類される。
{
"eventVersion": "1.11",
"userIdentity": {
"type": "IAMUser",
"principalId": "AIDAXXXXXXXXXXXXXXXXX",
"arn": "arn:aws:iam::123456789012:user/example-user",
"accountId": "123456789012",
"accessKeyId": "ASIAXXXXXXXXXXXXXXXXX",
"userName": "example-user",
"sessionContext": {
"attributes": {
"creationDate": "2026-06-20T12:55:16Z",
"mfaAuthenticated": "false"
}
}
},
"eventTime": "2026-06-20T12:55:39Z",
"eventSource": "ec2.amazonaws.com",
"eventName": "CreateVpc",
"awsRegion": "ap-northeast-1",
"sourceIPAddress": "xxx.xxx.xxx.xxx",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
"requestParameters": {
"cidrBlock": "10.0.0.0/24",
"instanceTenancy": "default",
"amazonProvidedIpv6CidrBlock": false,
"tagSpecificationSet": {
"items": [
{
"resourceType": "vpc",
"tags": [
{ "key": "Name", "value": "hogehoge" }
]
}
]
}
},
"responseElements": {
"requestId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"vpc": {
"vpcId": "vpc-xxxxxxxxxxxxxxxxx",
"state": "pending",
"ownerId": "123456789012",
"cidrBlock": "10.0.0.0/24",
"cidrBlockAssociationSet": {
"items": [
{
"cidrBlock": "10.0.0.0/24",
"associationId": "vpc-cidr-assoc-xxxxxxxxxxxxxxxxx",
"cidrBlockState": { "state": "associated" }
}
]
},
"dhcpOptionsId": "dopt-xxxxxxxxxxxxxxxxx",
"instanceTenancy": "default",
"tagSet": {
"items": [{ "key": "Name", "value": "hogehoge" }]
},
"isDefault": false
}
},
"requestID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"eventID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"readOnly": false,
"eventType": "AwsApiCall",
"managementEvent": true,
"recipientAccountId": "123456789012",
"eventCategory": "Management",
"tlsDetails": {
"tlsVersion": "TLSv1.3",
"cipherSuite": "TLS_AES_128_GCM_SHA256",
"clientProvidedHostHeader": "ec2.ap-northeast-1.amazonaws.com"
},
"sessionCredentialFromConsole": "true"
}
ポイント
| フィールド | 値 | 意味 |
|---|---|---|
eventType |
AwsApiCall |
API 呼び出しイベント |
eventSource |
ec2.amazonaws.com |
EC2 サービスが処理 |
eventName |
CreateVpc |
呼び出された API 名 |
readOnly |
false |
書き込み系 API(変更を伴う) |
requestParameters |
CIDR・タグなど | リクエスト内容が記録される |
responseElements |
VPC ID・状態など | 作成されたリソース情報が記録される |
sessionCredentialFromConsole |
"true" |
コンソール経由であることを示すフラグ |
sessionContext.creationDate |
2026-06-20T12:55:16Z |
ログインセッションの作成時刻 |
ログインイベントとの紐付け
CreateVpc イベントの sessionContext.creationDate(2026-06-20T12:55:16Z)が、ConsoleLogin イベントの eventTime(2026-06-20T12:55:16Z)と 完全一致 している。これにより、同一セッションで行われた操作であることが確認できる。
12:55:16Z ConsoleLogin (AwsConsoleSignIn) ─── セッション開始
↕ sessionContext.creationDate で紐付け可能
12:55:39Z CreateVpc (AwsApiCall) ─── 同セッションで操作
3. AwsApiCall ── 画面描画による読み取り系 API(readOnly)
コンソールで VPC ダッシュボードを開いたり、作成後に画面が更新されたりすると、大量の読み取り系 API が自動的に呼び出される。
{
"eventVersion": "1.11",
"userIdentity": {
"type": "IAMUser",
"userName": "example-user",
"sessionContext": {
"attributes": {
"creationDate": "2026-06-20T12:55:16Z",
"mfaAuthenticated": "false"
}
}
},
"eventTime": "2026-06-20T12:55:59Z",
"eventSource": "ec2.amazonaws.com",
"eventName": "DescribeVpcs",
"awsRegion": "ap-northeast-1",
"sourceIPAddress": "xxx.xxx.xxx.xxx",
"userAgent": "Mozilla/5.0 ...",
"requestParameters": {
"maxResults": 1000,
"vpcSet": {},
"filterSet": {}
},
"responseElements": null,
"readOnly": true,
"eventType": "AwsApiCall",
"managementEvent": true,
"eventCategory": "Management",
"sessionCredentialFromConsole": "true"
}
ポイント
| フィールド | 値 | 意味 |
|---|---|---|
readOnly |
true |
読み取り専用 API |
responseElements |
null |
読み取り系は null(結果は返さない) |
eventType |
AwsApiCall |
書き込み系と同じ分類 |
4. AwsApiCall ── VPC フィルタ操作(readOnly)
VPC 一覧画面でフィルタボックスに VPC ID を入力して絞り込むと、コンソールはブラウザ内でクライアントサイド処理するのではなく、特定の VPC ID を指定して DescribeVpcs を再度呼び出す。これも AwsApiCall(readOnly)として記録される。
初期表示とフィルタ後で requestParameters が変化している点に注目:
# 初期表示(全件取得)
"requestParameters": {
"maxResults": 1000,
"vpcSet": {},
"filterSet": {}
}
# フィルタ後(特定 VPC ID を指定)
"requestParameters": {
"vpcSet": {
"items": [{ "vpcId": "vpc-xxxxxxxxxxxxxxxxx" }]
},
"filterSet": {}
}
フィルタ後の完全なイベント:
{
"eventVersion": "1.11",
"userIdentity": {
"type": "IAMUser",
"principalId": "AIDAXXXXXXXXXXXXXXXXX",
"arn": "arn:aws:iam::123456789012:user/example-user",
"accountId": "123456789012",
"accessKeyId": "ASIAXXXXXXXXXXXXXXXXX",
"userName": "example-user",
"sessionContext": {
"attributes": {
"creationDate": "2026-06-20T13:12:16Z",
"mfaAuthenticated": "false"
}
}
},
"eventTime": "2026-06-20T13:12:28Z",
"eventSource": "ec2.amazonaws.com",
"eventName": "DescribeVpcs",
"awsRegion": "ap-northeast-1",
"sourceIPAddress": "xxx.xxx.xxx.xxx",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
"requestParameters": {
"vpcSet": {
"items": [
{ "vpcId": "vpc-xxxxxxxxxxxxxxxxx" }
]
},
"filterSet": {}
},
"responseElements": null,
"requestID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"eventID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"readOnly": true,
"eventType": "AwsApiCall",
"managementEvent": true,
"recipientAccountId": "123456789012",
"eventCategory": "Management",
"tlsDetails": {
"tlsVersion": "TLSv1.3",
"cipherSuite": "TLS_AES_128_GCM_SHA256",
"clientProvidedHostHeader": "ec2.ap-northeast-1.amazonaws.com"
},
"sessionCredentialFromConsole": "true"
}
ポイント
| フィールド | 値 | 意味 |
|---|---|---|
eventType |
AwsApiCall |
フィルタ操作も API 呼び出しとして記録 |
readOnly |
true |
読み取りのみ |
requestParameters.vpcSet.items |
VPC ID のリスト | フィルタで指定した VPC ID が記録される |
responseElements |
null |
読み取り系のため null
|
5. AwsConsoleAction ── 今回の操作では記録なし
AwsConsoleAction は「コンソール上で行われたが API 呼び出しではない操作」に分類される。今回のログイン〜VPC 作成・フィルタ操作を通じて AwsConsoleAction は 1 件も記録されなかった。
上記のとおり、VPC のフィルタ操作のようにコンソール上の UI 操作に見えるものも、実際には API 呼び出し(AwsApiCall)として記録される。現状、具体的にどのような操作が AwsConsoleAction に該当するのかは不明。
6. ログイン〜VPC 作成で記録されたイベントの全体像
ログインして VPC 作成画面を操作した際、以下のイベントが時系列で記録されていた。
[ap-southeast-2]
12:55:16Z ConsoleLogin (AwsConsoleSignIn) ← ★ ログイン
[ap-northeast-1]
12:55:20Z ListApplications (AwsApiCall) ← コンソールホーム描画
12:55:20Z DescribeRegions (AwsApiCall)
12:55:21Z ListNotificationHubs (AwsApiCall)
12:55:24Z DescribeVpcs (AwsApiCall) ← VPCダッシュボード描画開始
12:55:24Z DescribeSubnets (AwsApiCall) ↓ 以下、ダッシュボード描画に伴う
12:55:24Z DescribeRouteTables (AwsApiCall) Describe系APIが並列実行
12:55:24Z DescribeInternetGateways (AwsApiCall)
12:55:24Z DescribeSecurityGroups (AwsApiCall)
12:55:24Z DescribeNatGateways (AwsApiCall)
12:55:24Z DescribeNetworkAcls (AwsApiCall)
12:55:24Z DescribeAddresses (AwsApiCall)
12:55:24Z DescribeInstances (AwsApiCall)
12:55:24Z ... (多数)
12:55:26Z DescribeAvailabilityZones (AwsApiCall) ← VPC作成フォーム表示
12:55:26Z DescribeIpamPools (AwsApiCall)
12:55:26Z DescribeIpv6Pools (AwsApiCall)
12:55:39Z CreateVpc (AwsApiCall) ← ★ VPC 作成(readOnly: false)
12:55:40Z DescribeVpcs (AwsApiCall) ← 作成後の画面更新
12:55:40Z DescribeRouteTables (AwsApiCall)
12:55:40Z DescribeNatGateways (AwsApiCall)
12:55:40Z ... (多数)
12:55:57Z DescribeVpcEncryptionControls (AwsApiCall)
12:55:59Z DescribeVpcs (AwsApiCall)
コンソールで 1 回 VPC を作成しただけで、裏側では数十件の
AwsApiCallが発生している。
まとめ
| 操作 | eventType | readOnly | 記録リージョン | 主なフィールド |
|---|---|---|---|---|
| コンソールログイン | AwsConsoleSignIn |
- | signin エンドポイントのリージョン |
responseElements.ConsoleLogin, additionalEventData.MFAUsed
|
| VPC 作成 | AwsApiCall |
false |
操作したリージョン |
requestParameters, responseElements(VPC ID など) |
| 画面描画(Describe 系) | AwsApiCall |
true |
操作したリージョン | responseElements: null |
| VPC フィルタ操作 | AwsApiCall |
true |
操作したリージョン |
requestParameters.vpcSet.items(VPC ID) |
| コンソール固有操作 | AwsConsoleAction |
- | - | 今回の操作では記録なし(詳細不明) |
コンソール操作か CLI/SDK 操作かの判別方法
eventType だけではコンソール操作か CLI/SDK 操作かを区別できない。以下のフィールドを合わせて確認する。
| 判別材料 | コンソール操作 | CLI/SDK 操作 |
|---|---|---|
sessionCredentialFromConsole |
"true" |
存在しない |
userAgent |
ブラウザの UA(Mozilla/5.0...) |
aws-cli/... / aws-sdk-...
|
参考
付録: eventType 別に同一 S3 バケットへ振り分ける CDK 実装
AwsConsoleSignIn とそれ以外のイベントを同一 S3 バケットの別フォルダに振り分ける仕組みを AWS CDK(TypeScript)で実装する。
アーキテクチャ
マルチリージョン CloudTrail Trail(全リージョンのイベントを ap-northeast-1 に集約)
→ CloudWatch Logs(ap-northeast-1)
├── サブスクリプションフィルタ①: { $.eventType = "AwsConsoleSignIn" }
│ → Firehose①(console-signin-filter)
│ → S3: <bucket>/AWSConsoleSignIn/yyyy/MM/dd/
└── サブスクリプションフィルタ②: { $.eventType != "AwsConsoleSignIn" }
→ Firehose②(console-others-filter)
→ S3: <bucket>/Others/yyyy/MM/dd/
CWL は 1 ロググループあたりサブスクリプションフィルタを最大 2 本設定できる。この上限をちょうど使い切る構成になる。
なぜ EventBridge ではなく Trail + CloudWatch Logs を使うのか
EventBridge には source: aws.signin / detail-type: AWS Console Sign In via CloudTrail という条件でサインインイベントをキャッチするルールを作成できる。しかし "via CloudTrail" という名称が示す通り、このイベントは CloudTrail の Trail が有効になっていないと EventBridge に流れない。Trail なしの状態で EventBridge ルールを作成しても MatchedEvents が常にゼロになる。
さらに EventBridge 方式には構造的な問題がある。ConsoleLogin イベントは signin エンドポイントのリージョンに記録されるため、どのリージョンに記録されるかは環境依存であり、理論上はサインインエンドポイントが存在するすべてのリージョンに EventBridge ルールをデプロイする必要がある。
マルチリージョン Trail 方式ならこれらの問題を両方解決できる。
| 観点 | EventBridge 方式 | Trail + CWL 方式(本実装) |
|---|---|---|
| Trail の要否 | 実は必要 | 必要(明示的に管理) |
| デプロイリージョン | 全 signin リージョン分 | ap-northeast-1 のみ |
| 遅延 | 数秒〜数分 | 数分(CWL 配信遅延) |
| リアルタイム検知 | 可能 | 不要な用途に最適 |
プロジェクト構成
signin-filter-cdk/
├── bin/
│ └── app.ts # CDK アプリエントリーポイント(1リージョンのみ)
├── lib/
│ └── signin-filter-stack.ts # スタック定義(Trail + CWL + Firehose×2 + S3)
├── cdk.json
├── package.json
└── tsconfig.json
bin/app.ts ── エントリーポイント
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { SigninFilterStack } from '../lib/signin-filter-stack';
const app = new cdk.App();
// ap-northeast-1 の 1 リージョンのみ。マルチリージョン Trail が全リージョンを網羅する
new SigninFilterStack(app, 'SigninFilterStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: 'ap-northeast-1',
},
});
lib/signin-filter-stack.ts ── スタック定義
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as cloudtrail from 'aws-cdk-lib/aws-cloudtrail';
import * as firehose from 'aws-cdk-lib/aws-kinesisfirehose';
export class SigninFilterStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// 1. S3 バケット: CloudTrail 生ログ(Trail が全管理イベントを書き込む)
const trailBucket = new s3.Bucket(this, 'TrailBucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// 2. CloudWatch Logs グループ: Trail → CWL のストリーミング転送先
const trailLogGroup = new logs.LogGroup(this, 'TrailLogGroup', {
logGroupName: '/aws/cloudtrail/management-events',
retention: logs.RetentionDays.ONE_MONTH,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// 3. マルチリージョン Trail
// isMultiRegionTrail: true で全リージョンのイベントを ap-northeast-1 に集約する
new cloudtrail.Trail(this, 'ManagementTrail', {
trailName: 'management-events',
bucket: trailBucket,
sendToCloudWatchLogs: true,
cloudWatchLogGroup: trailLogGroup,
isMultiRegionTrail: true,
includeGlobalServiceEvents: true,
enableFileValidation: true,
managementEvents: cloudtrail.ReadWriteType.ALL,
});
// 4. S3 バケット: eventType 別フォルダで振り分けるバケット(Firehose の出力先)
const signinBucket = new s3.Bucket(this, 'SigninLogsBucket', {
bucketName: `signinfilterstack-${this.account}`,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// 5. IAM ロール: 2本の Firehose が同じロール・同じバケットを共有する
const firehoseRole = new iam.Role(this, 'FirehoseRole', {
assumedBy: new iam.ServicePrincipal('firehose.amazonaws.com'),
inlinePolicies: {
S3Access: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
's3:AbortMultipartUpload',
's3:GetBucketLocation',
's3:GetObject',
's3:ListBucket',
's3:ListBucketMultipartUploads',
's3:PutObject',
],
resources: [signinBucket.bucketArn, `${signinBucket.bucketArn}/*`],
}),
],
}),
},
});
// 6a. Firehose①: AwsConsoleSignIn → AWSConsoleSignIn/ プレフィックス
const signinStream = new firehose.CfnDeliveryStream(this, 'DeliveryStream', {
deliveryStreamName: 'console-signin-filter',
deliveryStreamType: 'DirectPut',
s3DestinationConfiguration: {
bucketArn: signinBucket.bucketArn,
roleArn: firehoseRole.roleArn,
prefix: 'AWSConsoleSignIn/!{timestamp:yyyy}/!{timestamp:MM}/!{timestamp:dd}/',
errorOutputPrefix: 'errors/signin/!{firehose:error-output-type}/!{timestamp:yyyy}/!{timestamp:MM}/!{timestamp:dd}/',
bufferingHints: { intervalInSeconds: 60, sizeInMBs: 1 },
compressionFormat: 'GZIP',
},
});
// 6b. Firehose②: AwsConsoleSignIn 以外 → Others/ プレフィックス
const othersStream = new firehose.CfnDeliveryStream(this, 'OthersDeliveryStream', {
deliveryStreamName: 'console-others-filter',
deliveryStreamType: 'DirectPut',
s3DestinationConfiguration: {
bucketArn: signinBucket.bucketArn,
roleArn: firehoseRole.roleArn,
prefix: 'Others/!{timestamp:yyyy}/!{timestamp:MM}/!{timestamp:dd}/',
errorOutputPrefix: 'errors/others/!{firehose:error-output-type}/!{timestamp:yyyy}/!{timestamp:MM}/!{timestamp:dd}/',
bufferingHints: { intervalInSeconds: 60, sizeInMBs: 1 },
compressionFormat: 'GZIP',
},
});
// 7. IAM ロール: CWL が両 Firehose にデータを送るための権限
const cwlToFirehoseRole = new iam.Role(this, 'CWLToFirehoseRole', {
assumedBy: new iam.ServicePrincipal(`logs.${this.region}.amazonaws.com`),
inlinePolicies: {
FirehoseAccess: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ['firehose:PutRecord', 'firehose:PutRecordBatch'],
resources: [signinStream.attrArn, othersStream.attrArn],
}),
],
}),
},
});
// 8a. サブスクリプションフィルタ①: AwsConsoleSignIn のみ → Firehose①
// ConsoleLogin / SwitchRole / RenewRole / ExitRole を一括キャプチャする
new logs.CfnSubscriptionFilter(this, 'ConsoleLoginFilter', {
logGroupName: trailLogGroup.logGroupName,
filterName: 'ConsoleLoginOnly',
filterPattern: '{ $.eventType = "AwsConsoleSignIn" }',
destinationArn: signinStream.attrArn,
roleArn: cwlToFirehoseRole.roleArn,
});
// 8b. サブスクリプションフィルタ②: AwsConsoleSignIn 以外 → Firehose②
// CWL は 1 ロググループあたり最大 2 本のサブスクリプションフィルタを設定できる
new logs.CfnSubscriptionFilter(this, 'OthersFilter', {
logGroupName: trailLogGroup.logGroupName,
filterName: 'OthersOnly',
filterPattern: '{ $.eventType != "AwsConsoleSignIn" }',
destinationArn: othersStream.attrArn,
roleArn: cwlToFirehoseRole.roleArn,
});
new cdk.CfnOutput(this, 'TrailBucketName', {
value: trailBucket.bucketName,
description: 'CloudTrail 全管理イベント S3 バケット',
});
new cdk.CfnOutput(this, 'SigninBucketName', {
value: signinBucket.bucketName,
description: 'eventType 別振り分け済みログ S3 バケット',
});
}
}
デプロイ手順
cd signin-filter-cdk
npm install
npx cdk bootstrap --profile default # 初回のみ
npx cdk deploy --profile default
動作確認
コンソールへログインすると、数分後に S3 バケットへ GZIP 圧縮ファイルが保存される。
s3://<bucket>/
├── AWSConsoleSignIn/2026/06/30/
│ └── console-signin-filter-1-2026-06-30-04-12-17-xxxx.gz
└── Others/2026/06/30/
└── console-others-filter-1-2026-06-30-04-11-26-xxxx.gz
S3 ファイルの構造と注意点
二重 GZIP 形式
S3 に保存されるファイルは 二重 GZIP になっている。
-
1層目: Firehose が付与する GZIP(
compressionFormat: 'GZIP'の設定による) - 2層目: CWL がサブスクリプションフィルタ経由で送信する際に付与する GZIP
コマンドラインで展開する場合は 2 段階必要になる。
# 1層目を展開(-k で元ファイルを保持)
gzip -d -k console-signin-filter-1-xxxx.gz
# 展開後もバイナリ(gzip)なので、.gz として再展開
mv console-signin-filter-1-xxxx console-signin-filter-1-xxxx.layer2.gz
gzip -d console-signin-filter-1-xxxx.layer2.gz
# → テキスト JSON が得られる
CWL ラッパー形式(DATA_MESSAGE / CONTROL_MESSAGE)
展開後の JSON は CloudTrail イベントそのものではなく、CWL ラッパーに包まれている。messageType フィールドで 2 種類に分かれる。
| messageType | 内容 | 発生タイミング |
|---|---|---|
DATA_MESSAGE |
実際の CloudTrail イベントを含む通常データ | ログが発生するたびに常時届く |
CONTROL_MESSAGE |
ヘルスチェック(CloudTrail イベントなし) | サブスクリプションフィルタの作成・更新時のみ |
DATA_MESSAGE の構造:
{
"messageType": "DATA_MESSAGE",
"owner": "123456789012",
"logGroup": "/aws/cloudtrail/management-events",
"logStream": "123456789012_CloudTrail_ap-northeast-1_4",
"subscriptionFilters": ["ConsoleLoginOnly"],
"logEvents": [
{
"id": "...",
"timestamp": 1782792736989,
"message": "{\"eventVersion\":\"1.11\",\"eventType\":\"AwsConsoleSignIn\",...}"
}
]
}
CONTROL_MESSAGE の構造(CloudTrail イベントは含まない):
{
"messageType": "CONTROL_MESSAGE",
"owner": "CloudwatchLogs",
"logGroup": "",
"logEvents": [
{ "message": "CWL CONTROL MESSAGE: Checking health of destination Firehose." }
]
}
⚠️ CONTROL_MESSAGE はフィルタをバイパスする
{ $.eventType = "AwsConsoleSignIn" } のサブスクリプションフィルタを設定していても、CONTROL_MESSAGE はフィルタパターンを無視して全宛先に強制転送される。これは AWS の仕様であり、回避できない。
-
AWSConsoleSignIn/とOthers/の両フォルダに同時に 1 件ずつ届く - 発生するのはサブスクリプションフィルタの作成・更新時のみなので、通常運用中は発生しない
-
messageType == "CONTROL_MESSAGE"で識別できるため、集計処理では除外できる
Firehose が複数レコードを区切り文字なしで連結する
Firehose は 60 秒(または 1MB)のバッファ中に受け取った複数の DATA_MESSAGE を、改行・区切り文字なしで 1 ファイルに連結して S3 に書き出す。
# ファイル内部のイメージ(実際は改行なし)
{"messageType":"DATA_MESSAGE","logEvents":[...]}{"messageType":"DATA_MESSAGE","logEvents":[...]}...
そのため json.loads() で一括パースするとエラーになる。JSONDecoder.raw_decode() を使って 1 オブジェクトずつ切り出す必要がある。
1 つの DATA_MESSAGE に複数の CloudTrail イベントが含まれることがある
CWL は Firehose へ転送するタイミングをバッチにまとめる。60 秒以内に複数の CloudTrail イベントが発生した場合、1 つの DATA_MESSAGE の logEvents 配列に複数イベントがまとめて入ることがある。
例: 10 秒差で 2 回ログインした場合
{
"messageType": "DATA_MESSAGE",
"logEvents": [
{ "message": "{...\"eventTime\":\"2026-06-30T04:32:34Z\"...}" },
{ "message": "{...\"eventTime\":\"2026-06-30T04:32:44Z\"...}" }
]
}
解析スクリプト
上記の構造を踏まえた解析スクリプト:
import gzip, io, json
from collections import Counter
def parse_firehose_file(path: str) -> list[dict]:
"""Firehose が出力した二重 GZIP ファイルから CloudTrail イベントを抽出する"""
# 二重 GZIP を展開
with gzip.open(path, 'rb') as f:
layer1 = f.read()
with gzip.GzipFile(fileobj=io.BytesIO(layer1)) as gz:
raw = gz.read().decode('utf-8')
# Firehose 連結(区切り文字なし)を 1 オブジェクトずつ切り出す
decoder = json.JSONDecoder()
events = []
pos = 0
while True:
chunk = raw[pos:].lstrip()
if not chunk:
break
obj, end = decoder.raw_decode(chunk)
pos += end
# CONTROL_MESSAGE はヘルスチェックのため除外する
if obj.get('messageType') != 'DATA_MESSAGE':
continue
for log_event in obj['logEvents']:
events.append(json.loads(log_event['message']))
return events
events = parse_firehose_file('console-signin-filter-1-xxxx.gz')
# eventType の集計
counter = Counter(e['eventType'] for e in events)
print(counter)
# → Counter({'AwsConsoleSignIn': 2})
# イベント一覧
for e in events:
print(e['eventType'], e['eventName'], e['eventTime'],
e.get('userIdentity', {}).get('userName'))
# → AwsConsoleSignIn ConsoleLogin 2026-06-30T04:32:34Z hijiri
# → AwsConsoleSignIn ConsoleLogin 2026-06-30T04:32:44Z hijiri