# AWS CloudTrail 完全ガイド（2026年版）：APIアクティビティ監査・証跡(Trail)・CloudTrail Lake・Athena分析・リアルタイム検知を本番品質で設計する

> AWS CloudTrailを公式ドキュメントに忠実に解説。4つのイベント種別（管理/データ/Insights/ネットワークアクティビティ）とイベント履歴 vs 証跡(Trail)の違い、マルチリージョン証跡のTerraform初期設定、SSE-KMS暗号化・ログ整合性検証、EventBridge/CloudWatch/Athenaでのリアルタイム検知と長期調査、CloudTrail Lake(Trino SQL)の現状、料金の落とし穴とコスト最適化、公式セキュリティ・ベストプラクティス13項目までを実コードで。

- 公開日: 2026-06-27
- 著者: 友田 陽大
- タグ: AWS, CloudTrail, セキュリティ, 監査ログ, Terraform, アーキテクチャ設計
- URL: https://tomodahinata.com/blog/aws-cloudtrail-audit-logging-governance-security-guide
- カテゴリ: AWS CloudTrail 監査・ガバナンス

## 要点

- CloudTrailは「誰が・いつ・どこから・どのAWS APIを呼んだか」を記録する台帳。イベント履歴は過去90日・管理イベントのみ・無料だが、恒久保存と全イベント種別の取得には証跡(Trail)が要る
- イベント種別は4つ（管理／データ／Insights／ネットワークアクティビティ）。管理イベントの1コピー目はリージョンごとに無料、データイベントは1コピー目から課金——ここがコストと設計の分かれ目
- 本番の初期設定は『マルチリージョン証跡＋SSE-KMS暗号化＋ログ整合性検証＋最小権限バケットポリシー』。Terraformで宣言的に固定する
- 検知はEventBridge→Lambda/SNSでニアリアルタイム、長期調査はAthena（パーティション射影でスキャン量を削減）、改ざん検出はログ整合性検証(SHA-256/digest)で担保する
- CloudTrail Lakeは2026年5月31日以降 新規受付終了（既存顧客は継続利用可）。新規はAthena＋S3が現実的な分析基盤になる

---

「**この本番設定、誰が・いつ変えたんですか？**」——インシデントの夜、最初に飛ぶこの問いに即答できるかどうかで、復旧までの時間が変わります。

決済基盤のように「お金が動く」システムでは、これは精神論ではありません。**いつ・誰が・どの権限で・どこから、どのAWS APIを呼んだか**を、改ざん不可能な形で残しておけるかが、コンプライアンス監査・不正調査・障害の原因究明のすべての起点になります。私はサーバーレス（Lambda + DynamoDB）の[決済プラットフォームの信頼性レイヤー](/case-studies/payment-platform-reliability)を設計・主導し、**本番稼働中の二重課金0件**を維持してきましたが、その土台にあるのは「正しさをコードと監査証跡で証明できる状態」を最初から作っておく、という規律です。その中核が **AWS CloudTrail** です。

この記事は、CloudTrail を**本番品質**で設計・運用するための実装ガイドです。「とりあえず有効化」で終わらせず、**マルチリージョン証跡・暗号化・整合性検証・リアルタイム検知・長期調査・コスト最適化**までを、Terraform / TypeScript / SQL の実コードで一気通貫に組みます。

> **この記事のルール**：仕様・料金・条件・機能のステータスは、すべて **AWS 公式ドキュメント（2026年6月時点）** に照合しています。とりわけ料金とマネージド機能は改定が速いので、本番投入前に必ず[公式の料金ページ](https://aws.amazon.com/cloudtrail/pricing/)と最新ドキュメントを確認してください。アカウントID（`111122223333`）・バケット名・リージョンは例示です。

---

## 0. メンタルモデル：CloudTrail は「AWSアカウントのAPI台帳」

設計を始める前に、CloudTrail が何で・何でないかを一行で固定します。

> **CloudTrail ＝ AWSアカウント内で行われた操作を「イベント」として記録する、ガバナンス・コンプライアンス・運用監査・リスク監査のためのサービス。** コンソール・CLI・SDK・API、どこ経由の操作でも記録される。

公式の定義もこの通りです。

> Actions taken by a user, role, or an AWS service are recorded as events in CloudTrail. Events include actions taken in the AWS Management Console, AWS Command Line Interface, and AWS SDKs and APIs.

ここから、現場で効く3つの帰結が出ます。

1. **CloudTrail はアプリの可観測性（observability）ではない。** OpenTelemetry が「アプリ内部で何が起きたか（トレース・メトリクス・ログ）」を見るのに対し、CloudTrail は「**AWS の管理面・データ面で誰が何のAPIを叩いたか**」を見ます。両者は補完関係で、別物です（アプリ側の三本柱は[OpenTelemetryによる可観測性](/blog/aws-observability-opentelemetry-sre-ecs)へ）。
2. **CloudTrail はログを「順番に」並べない。** 公式が明言する通り、ログはスタックトレースではなく、イベントは特定の順序で現れません。時系列の追跡は `eventTime` で自分でソートします。
3. **「有効化されている」と「証拠として使える」は違う。** 後述しますが、デフォルトの**イベント履歴は90日・管理イベントのみ**。恒久保存・改ざん検出・全イベント種別の取得には、**証跡(Trail)を自分で作る**必要があります。

---

## 1. 全体地図：4つのイベント種別と「イベント履歴 vs 証跡(Trail)」

CloudTrail を理解する最短ルートは、**「何が記録されるか（イベント種別）」**と**「どこに溜まるか（イベント履歴／証跡／Lake）」**の2軸を分けて捉えることです。

### 1-1. 記録される4つのイベント種別

公式は4種類を定義しています。**デフォルトでは管理イベントだけが記録され、データ／Insights／ネットワークアクティビティは記録されません。**

| イベント種別 | 何を記録するか | 既定 | 課金 |
| --- | --- | --- | --- |
| **管理イベント**（Management） | コントロールプレーン操作（`RunInstances`・`CreateUser`・`ConsoleLogin` 等）。読み取り/書き込みを分けて選択可 | **記録ON** | 1コピー目はリージョンごと無料 |
| **データイベント**（Data） | データプレーン操作（S3オブジェクトの `GetObject`、Lambda の `Invoke`、DynamoDB の `PutItem` 等）。**高ボリューム** | OFF | 1コピー目から課金 |
| **Insightsイベント**（Insights） | API呼び出し率／エラー率の**異常検知**。管理・データの両方を継続分析 | OFF | 分析対象数に課金 |
| **ネットワークアクティビティ**（Network activity） | **VPCエンドポイント**経由のAPIアクティビティ。組織外の認証情報による接近を検出 | OFF | 課金 |

> ネットワークアクティビティイベントは比較的新しい機能で、**2025年2月にGA**になりました（S3・EC2・KMS・Secrets Manager・CloudTrail の5サービスでスタートし、対応サービスは継続拡大中）。「VPCエンドポイントを誰が・どこから使っているか」が見えるため、データ境界（data perimeter）の検出的コントロールに効きます。

### 1-2. 溜まる場所：イベント履歴・証跡・CloudTrail Lake

**ここが最大の誤解ポイント**です。「CloudTrail はデフォルトでオン」は半分正しく、半分危険です。

- **イベント履歴（Event history）** — アカウント作成時から**自動でオン・無料**。ただし制約が強い。

  > The Event history provides a viewable, searchable, downloadable, and immutable record of the **past 90 days of management events** in an AWS Region.

  つまり「**過去90日・管理イベントのみ・単一リージョン**」。データイベントは含まれず、90日で消えます。**証拠としての恒久記録ではありません。**

- **証跡（Trail）** — 「イベントを **S3 に継続配信**する設定（オプションで CloudWatch Logs・EventBridge にも）」。90日を超える保存・データイベント・整合性検証・全リージョン集約は、すべて証跡が前提です。**本番で最初にやるべきはこれ**です。

- **CloudTrail Lake** — イベントを**Trino SQL** でクエリできるマネージドな監査データレイク。後述しますが、**2026年5月31日以降、新規顧客の受付を終了**しています（既存顧客は継続利用可）。新規構築なら **Athena + S3** が現実的な選択肢になります。

> **設計の出発点**：イベント履歴に頼らない。**マルチリージョン証跡を1本** 作り、S3 に恒久配信する。これが土台です。次章で「壊れない初期設定」を Terraform で固めます。

---

## 2. 最初の一歩：マルチリージョン証跡を Terraform で本番品質に初期設定

証跡は「作る」のは一瞬ですが、**本番品質**にするには次の5点を最初から満たす必要があります。これは公式のセキュリティ・ベストプラクティスそのものです（§8で全項目を整理）。

1. **マルチリージョン**（全リージョンのイベントを取りこぼさない）
2. **SSE-KMS 暗号化**（保存時の機密性）
3. **ログファイル整合性検証**（改ざん・削除の検出）
4. **最小権限のS3バケットポリシー**（`aws:SourceArn` で証跡を限定）
5. **CloudWatch Logs 連携**（リアルタイム監視の土台）

> **コンソールとCLI/APIで既定が違う**という罠があります。公式は「**All trails created using the CloudTrail console are multi-Region trails**」と明言する一方、**CLI/API・Terraform で作ると既定は単一リージョン**です。だから IaC では `is_multi_region_trail = true` を**明示**しなければなりません。

### 2-1. 配信先S3バケット（バージョニング・公開遮断・暗号化）

```hcl
# 監査ログ専用バケット。本来は「ログアーカイブ専用アカウント」に隔離するのが理想（§8）。
resource "aws_s3_bucket" "trail" {
  bucket = "prod-audit-trail-111122223333"
}

# 改ざん・誤削除に備えてバージョニングは必須
resource "aws_s3_bucket_versioning" "trail" {
  bucket = aws_s3_bucket.trail.id
  versioning_configuration { status = "Enabled" }
}

# 監査ログが公開されることは絶対にあってはならない
resource "aws_s3_bucket_public_access_block" "trail" {
  bucket                  = aws_s3_bucket.trail.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# S3側の既定暗号化（CloudTrail自身もKMSで暗号化するが、多層で固める）
resource "aws_s3_bucket_server_side_encryption_configuration" "trail" {
  bucket = aws_s3_bucket.trail.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.trail.arn
    }
    bucket_key_enabled = true # KMSリクエストを集約してコスト削減
  }
}
```

### 2-2. 最小権限のバケットポリシー（`aws:SourceArn` で証跡を縛る）

CloudTrail がバケットに書き込むには、ACLチェックと書き込みの2つの許可が要ります。**`aws:SourceArn` 条件で「この証跡からの書き込みだけ」に限定する**のがベストプラクティスです（混乱した代理人問題＝confused deputy の防止）。

```hcl
data "aws_caller_identity" "current" {}

locals {
  trail_arn = "arn:aws:cloudtrail:us-east-1:${data.aws_caller_identity.current.account_id}:trail/org-audit-trail"
}

data "aws_iam_policy_document" "trail_bucket" {
  # ① CloudTrail がバケットACLを確認する許可
  statement {
    sid       = "AWSCloudTrailAclCheck"
    actions   = ["s3:GetBucketAcl"]
    resources = [aws_s3_bucket.trail.arn]
    principals {
      type        = "Service"
      identifiers = ["cloudtrail.amazonaws.com"]
    }
    condition {
      test     = "StringEquals"
      variable = "aws:SourceArn"
      values   = [local.trail_arn]
    }
  }

  # ② ログオブジェクトの書き込み許可（bucket-owner-full-control 必須）
  statement {
    sid       = "AWSCloudTrailWrite"
    actions   = ["s3:PutObject"]
    resources = ["${aws_s3_bucket.trail.arn}/AWSLogs/${data.aws_caller_identity.current.account_id}/*"]
    principals {
      type        = "Service"
      identifiers = ["cloudtrail.amazonaws.com"]
    }
    condition {
      test     = "StringEquals"
      variable = "s3:x-amz-acl"
      values   = ["bucket-owner-full-control"]
    }
    condition {
      test     = "StringEquals"
      variable = "aws:SourceArn"
      values   = [local.trail_arn]
    }
  }
}

resource "aws_s3_bucket_policy" "trail" {
  bucket = aws_s3_bucket.trail.id
  policy = data.aws_iam_policy_document.trail_bucket.json
}
```

> 組織（AWS Organizations）の証跡にする場合、`②` のリソースパスはアカウントIDではなく**組織IDのパス**（`AWSLogs/o-xxxxxxxxxx/<account>/...`）になります。`is_organization_trail = true` を使うときはここを忘れがちです。

### 2-3. KMSキー（CloudTrail に暗号化を許可する）

SSE-KMS にするなら、**キーポリシーで `cloudtrail.amazonaws.com` に暗号化を許可**します。`kms:EncryptionContext` と `aws:SourceArn` の二重条件で、無関係なサービスにキーを使わせません。

```hcl
data "aws_iam_policy_document" "trail_kms" {
  # アカウント管理者（鍵の管理権限）
  statement {
    sid       = "EnableRoot"
    actions   = ["kms:*"]
    resources = ["*"]
    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"]
    }
  }

  # CloudTrail にデータキー生成を許可（証跡ARNと暗号化コンテキストで限定）
  statement {
    sid       = "AllowCloudTrailEncrypt"
    actions   = ["kms:GenerateDataKey*"]
    resources = ["*"]
    principals {
      type        = "Service"
      identifiers = ["cloudtrail.amazonaws.com"]
    }
    condition {
      test     = "StringEquals"
      variable = "aws:SourceArn"
      values   = [local.trail_arn]
    }
    condition {
      test     = "StringLike"
      variable = "kms:EncryptionContext:aws:cloudtrail:arn"
      values   = ["arn:aws:cloudtrail:*:${data.aws_caller_identity.current.account_id}:trail/*"]
    }
  }

  # ログ読者がKMSで復号できるように（最小権限で）
  statement {
    sid       = "AllowDecryptForReaders"
    actions   = ["kms:Decrypt", "kms:DescribeKey"]
    resources = ["*"]
    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/SecurityAuditor"]
    }
  }
}

resource "aws_kms_key" "trail" {
  description             = "CloudTrail log encryption key"
  enable_key_rotation     = true # 年次自動ローテーション
  deletion_window_in_days = 30
  policy                  = data.aws_iam_policy_document.trail_kms.json
}
```

### 2-4. 証跡本体（マルチリージョン・整合性検証・CloudWatch Logs連携）

```hcl
resource "aws_cloudwatch_log_group" "trail" {
  name              = "/aws/cloudtrail/org-audit"
  retention_in_days = 365 # CloudWatch Logs側の保持（S3とは別管理）
}

# CloudTrail が CloudWatch Logs に書き込むためのロール（最小権限・割愛気味に提示）
resource "aws_iam_role" "cloudtrail_cw" {
  name = "CloudTrail_CloudWatchLogs_Role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "cloudtrail.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy" "cloudtrail_cw" {
  role = aws_iam_role.cloudtrail_cw.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["logs:CreateLogStream", "logs:PutLogEvents"]
      Resource = "${aws_cloudwatch_log_group.trail.arn}:*"
    }]
  })
}

resource "aws_cloudtrail" "org_audit" {
  name           = "org-audit-trail"
  s3_bucket_name = aws_s3_bucket.trail.id
  kms_key_id     = aws_kms_key.trail.arn

  is_multi_region_trail         = true # CLI/Terraform既定は単一リージョン。必ず明示！
  include_global_service_events = true # IAM等グローバルサービスのイベントも取得
  enable_log_file_validation    = true # ★整合性検証（digest生成）をON
  enable_logging                = true

  cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.trail.arn}:*"
  cloud_watch_logs_role_arn  = aws_iam_role.cloudtrail_cw.arn

  # is_organization_trail = true # AWS Organizations管理アカウントで全メンバーに適用

  # バケットポリシーが先に存在しないと作成が失敗するため明示依存
  depends_on = [aws_s3_bucket_policy.trail]
}
```

これで「**全リージョンの管理イベントを、暗号化＋改ざん検出付きで、S3 とCloudWatch Logs に恒久配信する**」土台ができました。証跡からS3への配信は、公式によれば**平均で約5分**（保証値ではない）です。

---

## 3. ログの読み方：レコードJSONと `userIdentity`（フォレンジックの起点）

検知も調査も、結局は**1件のイベントJSONを正しく読めるか**に尽きます。下は「**ルートユーザーが、MFAなしで、見知らぬIPからコンソールにログインした**」という、本来あってはならないイベントの例です（現フォーマットは `eventVersion` 1.11）。

```json
{
  "eventVersion": "1.11",
  "userIdentity": {
    "type": "Root",
    "principalId": "111122223333",
    "arn": "arn:aws:iam::111122223333:root",
    "accountId": "111122223333"
  },
  "eventTime": "2026-06-27T02:14:51Z",
  "eventSource": "signin.amazonaws.com",
  "eventName": "ConsoleLogin",
  "awsRegion": "us-east-1",
  "sourceIPAddress": "203.0.113.42",
  "userAgent": "Mozilla/5.0 ...",
  "requestParameters": null,
  "responseElements": { "ConsoleLogin": "Success" },
  "additionalEventData": { "MFAUsed": "No" },
  "eventID": "8a9b0c1d-2e3f-4a5b-6c7d-8e9f0a1b2c3d",
  "eventType": "AwsConsoleSignIn",
  "recipientAccountId": "111122223333"
}
```

読むべきフィールドを公式定義とともに押さえます。

| フィールド | 意味（公式準拠） | 調査での使いどころ |
| --- | --- | --- |
| `userIdentity` | 「**誰が**」呼んだか。IAMアイデンティティ情報 | 主役。下表で深掘り |
| `eventSource` / `eventName` | 「**どのサービスの・どの操作**」（`iam.amazonaws.com` / `CreateUser` 等） | 操作の特定・フィルタの軸 |
| `eventTime` | リクエスト完了時刻（UTC） | 時系列の再構成（ソートキー） |
| `sourceIPAddress` | リクエスト元IP（AWS内部発は `AWS Internal`） | 不審な発信元の特定 |
| `errorCode` / `errorMessage` | エラー時のコードと説明（`AccessDenied` 等） | 攻撃・権限不足の兆候 |
| `readOnly` | 読み取り専用操作か（true/false） | 「変更系」だけ抽出する |
| `eventCategory` | `Management` / `Data` / `NetworkActivity` | 種別での切り分け |
| `recipientAccountId` | このイベントを受け取ったアカウント | クロスアカウント操作の検出 |
| `tlsDetails` | TLSバージョン・暗号スイート・FQDN | 古いTLS接続の棚卸し |
| `sessionCredentialFromConsole` | コンソールセッション由来か（trueのときのみ表示） | 人手 vs 自動化の区別 |

### `userIdentity.type`：正しい綴りで覚える

「誰が」を表す `type` の値は、調査クエリのキーになります。公式の値（現行）を正確に：

| type | 何者か |
| --- | --- |
| `Root` | ルートユーザー。**通常運用で出てはいけない** |
| `IAMUser` | IAMユーザー |
| `AssumedRole` | ロールを引き受けたセッション（`sessionContext` が付く） |
| `Role` | サービスロール等 |
| `FederatedUser` | STSフェデレーション |
| `AWSService` | AWSサービスが代理実行 |
| `AWSAccount` | 別アカウント |
| `IdentityCenterUser` | IAM Identity Center ユーザー（**`IAMIdentityCenter` ではない**） |
| `SAMLUser` / `WebIdentityUser` | SAML / Webアイデンティティ連携 |

`AssumedRole` のときは `userIdentity.sessionContext.sessionIssuer`（どのロールから）と `sessionContext.attributes.mfaAuthenticated`（MFA有無）が決定的に重要です。「**誰のロールを、MFA付きで引き受けたセッションか**」がここで分かります。

---

## 4. 場面別の実戦：検知・監視・調査・データイベント

土台ができたら、CloudTrail を「**置いてあるだけ**」から「**働く監査基盤**」に変えます。用途別に4パターンを実装します。

### 4.1 リアルタイム検知：EventBridge → Lambda/SNS

**最も価値が高い検知は「攻撃者が証跡そのものを止めにくる」瞬間**です。侵入後にまずやるのは証跡停止（証拠隠滅）。これを `StopLogging` / `DeleteTrail` / `UpdateTrail` / `PutEventSelectors` で即検知します。

EventBridge は、CloudTrail が記録したAPI呼び出しに**ニアリアルタイム**で反応できます（detail-type は `AWS API Call via CloudTrail`、コンソールサインインは `AWS Console Sign In via CloudTrail`）。

```hcl
resource "aws_cloudwatch_event_rule" "trail_tampering" {
  name        = "detect-cloudtrail-tampering"
  description = "CloudTrail証跡の停止・削除・改変を即検知する"
  event_pattern = jsonencode({
    "detail-type" = ["AWS API Call via CloudTrail"]
    detail = {
      eventSource = ["cloudtrail.amazonaws.com"]
      eventName   = ["StopLogging", "DeleteTrail", "UpdateTrail", "PutEventSelectors"]
    }
  })
}

resource "aws_cloudwatch_event_target" "to_lambda" {
  rule = aws_cloudwatch_event_rule.trail_tampering.name
  arn  = aws_lambda_function.audit_alert.arn
}

resource "aws_lambda_permission" "allow_eventbridge" {
  statement_id  = "AllowExecutionFromEventBridge"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.audit_alert.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.trail_tampering.arn
}
```

ルートログインを検知したいなら、パターンを差し替えるだけです。

```json
{
  "detail-type": ["AWS Console Sign In via CloudTrail"],
  "detail": { "userIdentity": { "type": ["Root"] }, "eventName": ["ConsoleLogin"] }
}
```

受け側の Lambda は、**「AWS発のイベントでも境界では信用しない」**を徹底し、使うフィールドだけ Zod で厳格に検証してから整形・通知します。`eventID`（イベントごとに一意なGUID）を重複排除キーに使い、再送に強くします。

```ts
import { SNSClient, PublishCommand } from "@aws-sdk/client-sns";
import { z } from "zod";
import type { EventBridgeEvent } from "aws-lambda";

const sns = new SNSClient({});
const TOPIC_ARN = process.env.ALERT_TOPIC_ARN;
if (!TOPIC_ARN) throw new Error("ALERT_TOPIC_ARN is not set"); // 起動時に落とす

// CloudTrailレコードのうち、通知に使うフィールドだけを境界で検証する
const CloudTrailDetail = z.object({
  eventID: z.string().uuid(),
  eventName: z.string(),
  eventSource: z.string(),
  awsRegion: z.string(),
  sourceIPAddress: z.string().optional(),
  errorCode: z.string().optional(),
  userIdentity: z.object({
    type: z.string(),
    arn: z.string().optional(),
  }),
});

export const handler = async (
  event: EventBridgeEvent<"AWS API Call via CloudTrail", unknown>,
): Promise<void> => {
  const detail = CloudTrailDetail.parse(event.detail); // 不正形状なら即例外

  const actor = detail.userIdentity.arn ?? detail.userIdentity.type;
  const subject = `🚨 [監査] ${detail.eventName} by ${detail.userIdentity.type}`;
  const message = [
    `操作: ${detail.eventName} (${detail.eventSource})`,
    `実行者: ${actor}`,
    `リージョン: ${detail.awsRegion}`,
    `送信元IP: ${detail.sourceIPAddress ?? "不明"}`,
    detail.errorCode ? `結果: 失敗 (${detail.errorCode})` : "結果: 成功",
    `eventID: ${detail.eventID}`,
  ].join("\n");

  await sns.send(
    new PublishCommand({
      TopicArn: TOPIC_ARN,
      Subject: subject.slice(0, 100), // SNS Subjectは最大100文字
      Message: message,
      MessageDeduplicationId: detail.eventID, // FIFOトピック使用時の冪等キー
    }),
  );
};
```

> **検知の前提**：EventBridge で確実に拾うには、**そのリージョンでロギングしている証跡が必要**です（とくにデータイベントの検知は証跡必須）。だから §2 の「マルチリージョン証跡」を先に作りました。公式の EventBridge チュートリアルも、Step 1 は証跡の作成から始まります。

### 4.2 CloudWatch Logs メトリクスフィルタ＋アラーム

「個別イベントの通知」ではなく「**一定時間に何回起きたか**」で鳴らしたいなら、CloudWatch Logs のメトリクスフィルタ＋アラームが向きます。証跡を CloudWatch Logs に流す設定（§2-4で完了済み）が前提です。

公式ドキュメントが**例として明示しているフィルタは3つ**——「セキュリティグループ変更」「コンソールサインイン失敗」「IAMポリシー変更」です。下は IAMポリシー変更の例（メトリクス名 `IAMPolicyEventCount`、5分で1回でも発火）。

```hcl
resource "aws_cloudwatch_log_metric_filter" "iam_policy_changes" {
  name           = "IAMPolicyChanges"
  log_group_name = aws_cloudwatch_log_group.trail.name
  pattern        = "{ ($.eventName = DeleteGroupPolicy) || ($.eventName = DeleteRolePolicy) || ($.eventName = DeleteUserPolicy) || ($.eventName = PutGroupPolicy) || ($.eventName = PutRolePolicy) || ($.eventName = PutUserPolicy) || ($.eventName = CreatePolicy) || ($.eventName = DeletePolicy) || ($.eventName = CreatePolicyVersion) || ($.eventName = DeletePolicyVersion) || ($.eventName = AttachRolePolicy) || ($.eventName = DetachRolePolicy) || ($.eventName = AttachUserPolicy) || ($.eventName = DetachUserPolicy) || ($.eventName = AttachGroupPolicy) || ($.eventName = DetachGroupPolicy) }"

  metric_transformation {
    name      = "IAMPolicyEventCount"
    namespace = "CloudTrailMetrics"
    value     = "1"
  }
}

resource "aws_cloudwatch_metric_alarm" "iam_policy_changes" {
  alarm_name          = "IAMPolicyChanges"
  namespace           = "CloudTrailMetrics"
  metric_name         = "IAMPolicyEventCount"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  threshold           = 1
  evaluation_periods  = 1
  period              = 300
  statistic           = "Sum"
  alarm_actions       = [aws_sns_topic.security_alerts.arn]
}
```

> **正直な注記**：ネットでよく見る「ルートアカウント利用」「未認可API呼び出し」「MFAなしサインイン」「NACL変更」などの定番フィルタ群は、**CloudTrail公式の当該ページには載っていません**。出典は **CIS AWS Foundations Benchmark** や Security Hub のコントロールです。実装する価値は高い（私も入れます）ですが、「公式が例示している3つ」と「ベンチマーク由来で自分で組むもの」を混同しないのが、信頼できる設計者の作法です。GuardDuty・Security Hub と組み合わせれば、これらの多くはマネージドに検出できます（§8）。

### 4.3 Athena で90日超を調査する

インシデント調査は「**S3 に溜めた生ログを、必要なときに横断クエリする**」のが王道です。CloudTrail コンソールから Athena テーブルを自動作成もできますが、本番では**パーティション射影（partition projection）**を効かせて、スキャン量＝コストと実行時間を抑えます。

```sql
CREATE EXTERNAL TABLE cloudtrail_logs (
  eventVersion STRING,
  userIdentity STRUCT<
    type: STRING, principalId: STRING, arn: STRING, accountId: STRING, userName: STRING,
    sessionContext: STRUCT<attributes: STRUCT<mfaAuthenticated: STRING, creationDate: STRING>>
  >,
  eventTime STRING, eventSource STRING, eventName STRING, awsRegion STRING,
  sourceIPAddress STRING, userAgent STRING, errorCode STRING, errorMessage STRING,
  requestParameters STRING, responseElements STRING, eventID STRING,
  readOnly BOOLEAN, eventType STRING, recipientAccountId STRING
)
PARTITIONED BY (`account` STRING, `region` STRING, `date` STRING)
ROW FORMAT SERDE 'com.amazon.emr.hive.serde.CloudTrailSerde'
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://prod-audit-trail-111122223333/AWSLogs/111122223333/CloudTrail/'
TBLPROPERTIES (
  'projection.enabled' = 'true',
  'projection.account.type' = 'enum',
  'projection.account.values' = '111122223333',
  'projection.region.type' = 'enum',
  'projection.region.values' = 'us-east-1,ap-northeast-1',
  'projection.date.type' = 'date',
  'projection.date.range' = '2024/01/01,NOW',
  'projection.date.format' = 'yyyy/MM/dd',
  'projection.date.interval' = '1',
  'projection.date.interval.unit' = 'DAYS',
  'storage.location.template' =
    's3://prod-audit-trail-111122223333/AWSLogs/${account}/CloudTrail/${region}/${date}'
);
```

調査でよく使う3本：

```sql
-- ① 「誰がこのセキュリティグループを開けたか」（直近1週間・パーティションで絞る）
SELECT eventTime, userIdentity.arn AS who, sourceIPAddress, eventName, requestParameters
FROM cloudtrail_logs
WHERE region = 'ap-northeast-1' AND date >= '2026/06/20'
  AND eventSource = 'ec2.amazonaws.com'
  AND eventName IN ('AuthorizeSecurityGroupIngress', 'RevokeSecurityGroupIngress')
ORDER BY eventTime DESC;

-- ② AccessDenied が急増しているプリンシパル（攻撃 or 権限設計ミスの兆候）
SELECT userIdentity.arn AS who, count(*) AS denied
FROM cloudtrail_logs
WHERE date >= '2026/06/01' AND errorCode = 'AccessDenied'
GROUP BY 1 ORDER BY denied DESC LIMIT 20;

-- ③ ルートアカウントの利用（本番では原則ゼロであるべき）
SELECT eventTime, eventName, sourceIPAddress
FROM cloudtrail_logs
WHERE date >= '2026/06/01' AND userIdentity.type = 'Root'
ORDER BY eventTime DESC;
```

> **コストの鍵はパーティション**です。Athena は**スキャンしたデータ量に課金**されます。`account` / `region` / `date` の射影列を `WHERE` に必ず入れて、無関係なパーティションを読ませない。これだけで数十GBのスキャンが数百MBに縮みます。

### 4.4 データイベントを「高度なイベントセレクタ」で外科的に絞る

データイベント（S3オブジェクト・Lambda Invoke・DynamoDB項目操作）は**高ボリュームかつ1コピー目から課金**なので、「全部ON」は事故です。**高度なイベントセレクタ（advanced event selectors）**で、**監査対象のリソースだけ・書き込みだけ**に外科的に絞ります。

```hcl
resource "aws_cloudtrail" "data_events" {
  name           = "payments-evidence-data-trail"
  s3_bucket_name = aws_s3_bucket.trail.id
  kms_key_id     = aws_kms_key.trail.arn
  is_multi_region_trail      = true
  enable_log_file_validation = true

  # 高度なセレクタを使うと既定の「管理イベント記録」が上書きされる。
  # 管理イベントを残したいなら、明示的にManagementセレクタを足す（重要な罠）。
  advanced_event_selector {
    name = "Log all management events"
    field_selector {
      field  = "eventCategory"
      equals = ["Management"]
    }
  }

  # 証拠保管バケットへの「書き込み系オブジェクト操作」だけを記録
  advanced_event_selector {
    name = "Audit writes on the payment evidence bucket only"
    field_selector {
      field  = "eventCategory"
      equals = ["Data"]
    }
    field_selector {
      field  = "resources.type"
      equals = ["AWS::S3::Object"]
    }
    field_selector {
      field       = "resources.ARN"
      starts_with = ["arn:aws:s3:::prod-payments-evidence/"]
    }
    field_selector {
      field  = "readOnly"
      equals = ["false"] # GetObjectのような読み取りは除外してコストを抑える
    }
  }

  depends_on = [aws_s3_bucket_policy.trail]
}
```

> 基本のイベントセレクタ（basic）は S3オブジェクト・Lambda・DynamoDB の3種だけ。**それ以外の多数のリソース種別（RDS・SQS・SNS・Bedrock 等、継続拡大中）とフィールド単位の絞り込みは、高度なセレクタ専用**です。CloudTrail Lake のイベントデータストアでも高度なセレクタのみが使えます。

---

## 5. CloudTrail Lake：現状を正直に（2026年5月31日 新規受付終了）

CloudTrail Lake は、イベントを**Trino SQL** でクエリできるマネージドな監査データレイクです。証跡が「S3にファイルを置く」のに対し、Lake は「**SQLで横断分析できる不変のデータストア**」を提供します。

ただし——**ここは記事の信頼性に関わるので正直に書きます**。公式ドキュメントは2026年6月時点で次のように明記しています。

> AWS CloudTrail Lake will no longer be open to new customers starting May 31, 2026. If you would like to use CloudTrail Lake, sign up prior to that date. Existing customers can continue to use the service as normal.

つまり、**2026年5月31日以降は新規受付を終了**。**既存顧客は従来どおり利用可能**ですが、これから新規に監査分析基盤を組むなら、現実的な選択は **§4.3 の Athena + S3** になります。「CloudTrail Lake を使え」と無条件に勧める記事は、この変更を踏まえていません。

既存顧客向けに、Lake の要点だけ押さえておきます。

- **イベントデータストア（EDS）** — 高度なセレクタで選んだイベントの**不変（immutable）**なコレクション。デフォルトで CloudTrail により暗号化。
- **保持期間** — 「1年延長可（One-year extendable）」で**既定366日・最大3,653日（約10年）**、「7年（Seven-year）」で**約2,557日（約7年）**。長期コンプライアンス保管に対応。
- **SQL** — Trino の `SELECT` 構文・関数をフル活用。複数 EDS の `JOIN` も可能。
- **生成AIによる自然言語クエリ（query generator）** — 英語のプロンプトから即使えるSQLを生成（**GA**）。一方、クエリ結果の要約機能は**プレビュー**——この2つはステータスが違うので混同しない。

```sql
-- Lake（Trino）で「証跡が止められた瞬間」を横断検索する例
SELECT eventTime, userIdentity.arn, eventName, sourceIPAddress
FROM <event_data_store_id>
WHERE eventName IN ('StopLogging', 'DeleteTrail')
  AND eventTime > '2026-06-01 00:00:00'
ORDER BY eventTime DESC;
```

---

## 6. ログファイル整合性検証：否認防止を担保する

監査ログは「**残っている**」だけでは不十分で、「**改ざんも削除もされていないと証明できる**」必要があります。これを担保するのが**ログファイル整合性検証**（§2-4 で `enable_log_file_validation = true` 済み）です。

仕組みは堅実です。

- CloudTrail は配信する各ログファイルの**ハッシュ**を計算し、**1時間ごと**に、その時間帯のログを参照する **digest ファイル**を生成・配信する。
- digest ファイルは CloudTrail の**秘密鍵で署名**され、**1つ前の digest の署名**も含む——これが**鎖（チェーン）**になり、digest ファイル自体の削除も検出できる。
- 使用アルゴリズムは公式の通り、**ハッシュ＝SHA-256、署名＝SHA-256 with RSA**。これにより「**検出されずにログを改ざん・削除・偽造することは計算上不可能**」になります。

検証はCLI一発です。

```bash
aws cloudtrail validate-logs \
  --trail-arn arn:aws:cloudtrail:us-east-1:111122223333:trail/org-audit-trail \
  --start-time 2026-06-25T00:00:00Z \
  --region us-east-1
```

> **なぜ重要か**。公式が言う通り、検証済みログは「**そのログファイルが改変されていないこと**」「**特定の認証情報が特定のAPIアクティビティを実行したこと**」を**積極的に主張**できる——フォレンジックと否認防止（non-repudiation）の核心です。決済・個人情報を扱うシステムで「監査ログがあります」と言うとき、本当に意味があるのは「**検証可能な**監査ログがあります」です。

---

## 7. 料金の現実とコスト最適化

CloudTrail は「ほぼ無料」にも「気づいたら高い」にもなります。境界を正確に押さえます（**us-east-1・2026年時点**。改定があるため[公式料金ページ](https://aws.amazon.com/cloudtrail/pricing/)で要確認）。

| 対象 | 料金 | 既定 |
| --- | --- | --- |
| 管理イベント | **1コピー目はリージョンごと無料** ／ 2コピー目以降 **$2.00 / 10万件** | 記録ON |
| データイベント | **$0.10 / 10万件（1コピー目から課金）** ／ 集計 +$0.03 / 10万件 | OFF |
| Insightsイベント | 管理 **$0.35 / 10万件**・データ **$0.03 / 10万件**（インサイトタイプごと・分析対象数） | OFF |
| ネットワークアクティビティ | **$0.10 / 10万件** | OFF |
| CloudTrail Lake 取り込み | 1年延長可 **$0.75/GB**（CloudTrailイベント）／ 7年は階層型（5TBまで$2.5・25TBまで$1・超過$0.50 /GB） | — |
| CloudTrail Lake クエリ | **$0.005 / スキャンGB** | — |
| S3・CloudWatch Logs・KMS・SNS・Athena | 各サービスで**別途従量課金** | — |

公式の文言を正確に。

> The first copy of management events within each region is delivered free of charge. ... For data events, all deliveries incur CloudTrail costs, including the first.

ここから、現場で効く3つのコスト則：

1. **単一リージョン（or マルチリージョン1本）の管理イベント証跡は実質ほぼ無料。** 課金されるのは S3 保管料（通常 月数セント〜数ドル）と、KMS を使うなら少量の KMS リクエスト料くらい。**だから「まず証跡を1本」は、コストを理由に渋るべきではありません。**
2. **「2コピー目」の罠。** 公式の例の通り、**マルチリージョン証跡がある状態で同じ管理イベントを拾う単一リージョン証跡を足すと、後者は課金**されます。組織トレイルとメンバーアカウントの重複も同じ。「監査用」「開発者用」と証跡を増やすと、知らぬ間に2コピー目が積み上がります。
3. **KMSイベントの爆発に注意。** これは公式も警告しています——S3 で SSE-KMS を多用すると、**大量の KMS 管理イベント**が CloudTrail に乗ってコストを押し上げる。証跡作成時の **「Exclude AWS KMS events」「Exclude Amazon RDS Data API events」**でノイズを落とせます（高度なセレクタなら管理・データ両方を絞れる）。

> データイベントと Insights は「効果」と「金額」を天秤にかけ、**§4.4 のように外科的に**有効化する。全リソース・読み取り込みで全開にした瞬間、請求が桁で変わります。

---

## 8. セキュリティ・ベストプラクティス チェックリスト（公式）

公式の「Security best practices in AWS CloudTrail」は、**検出的（Detective）**と**予防的（Preventative）**の2系統で構成されています。本番投入前の最終チェックリストとしてそのまま使えます。

**検出的（Detective）**

1. **証跡を作る** — イベント履歴（90日・管理イベントのみ）は恒久記録ではない。証跡が前提。
2. **マルチリージョン証跡にする** — 全リージョン＋グローバルサービスイベントを捕捉。AWS Config ルール `multi-region-cloud-trail-enabled` で継続監視。
3. **ログファイル整合性検証を有効化** — SHA-256 / SHA-256 with RSA で改ざん・削除・配信欠落を検出（§6）。
4. **CloudWatch Logs と連携** — 特定イベントの監視・アラート（§4.2）。`cloud-trail-cloud-watch-logs-enabled` で監視。
5. **GuardDuty を使う** — MLによる脅威検出。CloudTrail を含む複数ログを継続分析。
6. **Security Hub（CSPM）を使う** — 構成を検出的コントロールで評価。

**予防的（Preventative）**

7. **専用・集中S3バケットに集約** — ログアーカイブ専用アカウント＋集中バケット。Organizations なら組織トレイル。
8. **SSE-KMS で暗号化** — 既定でも CloudTrail は暗号化するが、CMK で鍵を統制（§2-3）。`cloud-trail-encryption-enabled` で監視。
9. **SNSトピックポリシーに条件キー** — `aws:SourceArn`（任意で `aws:SourceAccount`）を追加し不正アクセスを防止。
10. **ログ保管バケットに最小権限** — バケットポリシーを見直し、`aws:SourceArn` 条件で限定（§2-2）。
11. **S3 の MFA Delete を有効化** — バージョン削除・バージョニング変更に追加認証（ライフサイクルとは併用不可）。
12. **オブジェクトのライフサイクル管理** — 保持ポリシーをライフサイクルルールで（例：1年でアーカイブ階層へ）。
13. **`AWSCloudTrail_FullAccess` の付与を絞る** — このポリシー保持者は監査を無効化・再構成できる。管理者最小人数に限定。

> CloudTrail は[WAFによる多層防御](/blog/waf-defense-in-depth-aws-waf-cloud-armor-owasp-guide)や[DynamoDBのIAM最小権限](/blog/dynamodb-security-iam-fine-grained-access-control-encryption-vpc-endpoint-guide)、[OIDCによる鍵レスCI/CD](/blog/github-actions-oidc-keyless-cicd-aws-gcp-guide)と同じ思想——**「クライアントを信じない／破れない層で守る」**——の上に立ちます。CloudTrail はその中で「**起きたことを後から否認不能に証明する**」検出的コントロールの最後の砦です。

---

## 9. まとめ：CloudTrail 設計チートシート

| 問い | 結論 |
| --- | --- |
| まず何をする？ | **マルチリージョン証跡を1本**。S3恒久配信＋SSE-KMS＋整合性検証＋最小権限バケットポリシー |
| イベント履歴で足りる？ | NO。90日・管理イベントのみ・単一リージョン。**恒久記録ではない** |
| コストの境界は？ | 管理イベント1コピー目はリージョンごと無料／**データイベントは1コピー目から課金**。2コピー目の重複とKMSイベント爆発に注意 |
| リアルタイム検知は？ | **EventBridge → Lambda/SNS**。最優先は「証跡停止（証跡隠滅）」の検知 |
| 集計で鳴らすには？ | **CloudWatch Logs メトリクスフィルタ＋アラーム**（公式例：SG変更・サインイン失敗・IAMポリシー変更） |
| 90日超の調査は？ | **Athena ＋ パーティション射影**。スキャン量（=コスト）を `WHERE` の射影列で抑える |
| 改ざんされてない証明は？ | **ログファイル整合性検証**（digest／SHA-256／`validate-logs`）。否認防止の核心 |
| CloudTrail Lake は？ | **2026/5/31以降 新規受付終了**（既存は継続可）。新規は Athena+S3 が現実解 |
| データイベントの付け方は？ | **高度なセレクタで外科的に**。対象リソース・書き込みのみに絞る |

CloudTrail は「有効化して終わり」のサービスではありません。**証跡の設計・暗号化・整合性・検知・調査・コスト**を一つの監査基盤として組んで初めて、「誰が・いつ・何をしたか」にインシデントの夜でも即答でき、コンプライアンス監査にも耐えます。

私は、サーバーレス決済基盤で「**正しさをコードと監査証跡で証明できる状態**」を最初から設計し、本番二重課金0件を維持してきました。**一人 × 生成AI（Claude Code）**で、こうした本番品質の AWS 監査・セキュリティ基盤を、速く・安全に、検証可能な形で構築します。AWS の監査・ガバナンス設計でお困りなら、[お問い合わせ](/contact)からお気軽にご相談ください。
