Amazon CloudWatch Logs の S3 テーブル統合機能により Lambda 関数ログを Athena でクエリ可能とする構成を CDK で実装してみた

Amazon CloudWatch Logs の S3 テーブル統合機能により Lambda 関数ログを Athena でクエリ可能とする構成を CDK で実装してみた

なお、S3 テーブル統合機能で作成されたテーブルには追加のストレージ・メンテナンス料金は発生しないが、データ保持はロググループの保持ポリシーに連動する。
2026.05.05

こんにちは、クラスメソッドの若槻です。

2025年12月、Amazon CloudWatch Logs の新機能として「AWS マネージドな S3 テーブル統合」(S3 Tables Integration)が発表されました(参考)。この機能により、CloudWatch Logs に取り込まれたログを Amazon S3 Tables(Apache Iceberg 形式)へ自動的に連携し、Amazon Athena など標準的な SQL エンジンでクエリできるようになります。

本記事では、この S3 Tables Integration を AWS CDK(TypeScript)で実装してみました。

実装した構成

今回実装した主なリソースは以下です。

CDK で作成するリソース

リソース 役割
Lambda 関数 ログの発生源。JSON 形式でログを出力する
S3 Tables Integration CloudWatch Logs → S3 Tables の自動連携設定(サービスロール含む)
Glue s3tablescatalog Athena から S3 Tables にアクセスするためのフェデレーテッドカタログ
Athena ワークグループ クエリ実行環境

AWS が自動で作成するリソース(CDK では管理しない)

リソース タイミング
S3 Table バケット(aws-cloudwatch Integration 作成時
名前空間(logs ログデータが初めて S3 Tables に書き込まれたとき
テーブル({関数名}__lambda 同上

アーキテクチャは以下の通りです。

Lambda 関数
  ↓ 自動(JSON 形式)
CloudWatch Logs(/aws/lambda/xxx)
  ↓ S3 Tables Integration(自動連携)
S3 Table Bucket(aws-cloudwatch / AWS マネージド)
  ↓ Apache Iceberg
Athena(s3tablescatalog 経由)

ここで、aws-cloudwatch S3 Tables バケットは、Integration を作成したときに AWS が自動的に作成するマネージドリソースです。AWS ブログでは次のように説明されています。

With this integration, logs from CloudWatch are available in a read-only aws-cloudwatch S3 Tables bucket.

https://aws.amazon.com/jp/blogs/aws/amazon-cloudwatch-introduces-unified-data-management-and-analytics-for-operations-security-and-compliance/

コンストラクトは lib/constructs/ 配下にサブディレクトリとして分割し、複合的なコンストラクトはさらにネストしています。

lib/
├── constructs/
│   ├── lambda/
│   │   ├── index.ts                       # LambdaFunctionConstruct
│   │   └── log-group/index.ts             # LogGroupConstruct
│   ├── s3-tables-integration/
│   │   ├── index.ts                       # S3TablesIntegrationConstruct
│   │   └── service-role/index.ts          # ServiceRoleConstruct
│   ├── s3-tables-catalog/index.ts         # S3TablesCatalogConstruct
│   └── athena-workgroup/
│       ├── index.ts                       # AthenaWorkGroupConstruct
│       └── result-bucket/index.ts         # ResultBucketConstruct
└── lambda-logs-s3-tables-stack.ts         # スタック(4つの construct を組み合わせる)

やってみた

0. 前提

CDK バージョン

aws-cdk-lib/aws-observabilityadminCfnS3TableIntegration が含まれるモジュール)は v2.211.0(2025年8月13日リリース)で追加されました。それ以前のバージョンでは利用できないため、必要に応じて更新してください。

npm install aws-cdk-lib@latest constructs@latest

CDK デプロイ用 IAM ロールの権限

デプロイする IAM ロールに以下の権限が必要です。S3TablesCatalogConstruct 内の CfnCatalog リソース作成時に使用します。

  • glue:CreateCatalog – Glue Data Catalog に s3tablescatalog フェデレーテッドカタログを作成するために必要
  • glue:PassConnection – Amazon S3 サービスへの aws:s3tables 接続作成を委譲するために必要

参考: Integrating Amazon S3 Tables with AWS analytics services – Prerequisites

CD ワークフローからデプロイする場合など、最小権限を厳密に指定している場合はこれらの権限が不足していないか確認しましょう。

1. construct の設計

スタック本体は 4 つの construct を組み合わせる構成です。

lib/lambda-logs-s3-tables-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

import { AthenaWorkGroupConstruct } from "./constructs/athena-workgroup/index";
import { LambdaFunctionConstruct } from "./constructs/lambda/index";
import { S3TablesCatalogConstruct } from "./constructs/s3-tables-catalog/index";
import { S3TablesIntegrationConstruct } from "./constructs/s3-tables-integration/index";

export class LambdaLogsS3TablesStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    /** ログ発生源となる Lambda 関数 */
    const lambdaFunction = new LambdaFunctionConstruct(this, "Lambda");

    /** S3 Tables Integration */
    new S3TablesIntegrationConstruct(this, "S3TablesIntegration", {
      lambdaFunction: lambdaFunction.function,
    });

    /** Glue s3tablescatalog フェデレーテッドカタログ(データパイプライン自体には不要) */
    new S3TablesCatalogConstruct(this, "S3TablesCatalog");

    /** Athena ワークグループ(データパイプライン自体には不要) */
    new AthenaWorkGroupConstruct(this, "AthenaWorkGroup");
  }
}

2. LambdaFunctionConstruct

Lambda 関数とロググループを管理するコンストラクトです。関数名は、S3 Tables のログソース名の制約(小文字・数字・アンダースコアのみ)を満たすため、cdk.Names.uniqueId から変換して自動導出します。

lib/constructs/lambda/index.ts
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { Construct } from "constructs";

import { LogGroupConstruct } from "./log-group";

/** Lambda 関数とそのロググループを管理するコンストラクト。 */
export class LambdaFunctionConstruct extends Construct {
  readonly function: lambda.Function;

  constructor(scope: Construct, id: string) {
    super(scope, id);

    /**
     * 関数名を construct パスから自動導出。
     * dataSource.name(= cw:datasource:name タグ値)は小文字・数字・アンダースコアのみ有効。
     * CDK のデフォルト自動生成名(ハイフン・大文字を含む)はそのままでは使用不可。
     * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/data-source-discovery-management.html#application-logs
     */
    const functionName = cdk.Names.uniqueId(this)
      .toLowerCase()
      .replace(/[^a-z0-9]/g, "_");

    const { logGroup } = new LogGroupConstruct(this, "LogGroup", {
      functionName,
    });

    this.function = new lambda.Function(this, "Default", {
      functionName,
      runtime: lambda.Runtime.NODEJS_24_X,
      handler: "index.handler",
      code: lambda.Code.fromInline(`
exports.handler = async (event) => {
  console.log(JSON.stringify({ level: 'INFO', message: 'invoked' }));
  return { statusCode: 200 };
};`),
      logGroup,
      loggingFormat: lambda.LoggingFormat.JSON, // cwl__message を JSON 形式にするために必要
    });
  }
}
lib/constructs/lambda/log-group/index.ts
import * as cdk from "aws-cdk-lib";
import * as logs from "aws-cdk-lib/aws-logs";
import { Construct } from "constructs";

/**
 * Lambda 関数のロググループと CloudWatch Data Sources タグを管理するコンストラクト。
 */
export class LogGroupConstruct extends Construct {
  readonly logGroup: logs.LogGroup;

  constructor(scope: Construct, id: string, props: { functionName: string }) {
    super(scope, id);

    const { functionName } = props;

    this.logGroup = new logs.LogGroup(this, "Default", {
      logGroupName: `/aws/lambda/${functionName}`,
      removalPolicy: cdk.RemovalPolicy.DESTROY, // 検証用スタックのため。本番では RETAIN を推奨
    });

    /**
     * CloudWatch Data Sources として認識させるためのタグ。
     * S3 Tables Integration がログを正しくルーティングするために必要。
     * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/data-source-discovery-management.html#application-logs
     */
    cdk.Tags.of(this.logGroup).add("cw:datasource:name", functionName);
    cdk.Tags.of(this.logGroup).add("cw:datasource:type", "lambda");
  }
}

3. S3TablesIntegrationConstruct

CloudWatch Logs S3 Tables Integration の設定(サービスロール・Integration)をまとめたコンストラクトです。logSources に Lambda 関数名を指定することでログソースの関連付けも行います。

lib/constructs/s3-tables-integration/index.ts
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as observabilityadmin from "aws-cdk-lib/aws-observabilityadmin";
import { Construct } from "constructs";

import { ServiceRoleConstruct } from "./service-role";

interface S3TablesIntegrationProps {
  lambdaFunction: lambda.Function;
}

/**
 * CloudWatch Logs の S3 Tables Integration を管理するコンストラクト。
 *
 * - S3 Tables サービスロール(CloudWatch Logs が assume して S3 Tables に書き込む)
 * - S3 Tables Integration(CfnS3TableIntegration)
 */
export class S3TablesIntegrationConstruct extends Construct {
  constructor(scope: Construct, id: string, props: S3TablesIntegrationProps) {
    super(scope, id);

    const { lambdaFunction } = props;

    const { serviceRole } = new ServiceRoleConstruct(this, "ServiceRole", {
      lambdaFunction,
    });

    /** S3 Tables Integration */
    new observabilityadmin.CfnS3TableIntegration(
      this,
      "Default",
      {
        roleArn: serviceRole.roleArn,
        encryption: {
          sseAlgorithm: "AES256",
        },
        logSources: [
          {
            name: lambdaFunction.functionName,
            type: "lambda",
          },
        ],
      },
    );
  }
}
lib/constructs/s3-tables-integration/service-role/index.ts
import * as cdk from "aws-cdk-lib";
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { Construct } from "constructs";

/**
 * CloudWatch Logs が S3 Tables に書き込む際に assume するサービスロール。
 * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/s3-tables-integration.html#service-role-iam-policies
 */
export class ServiceRoleConstruct extends Construct {
  readonly serviceRole: iam.Role;

  constructor(scope: Construct, id: string, props: { lambdaFunction: lambda.Function }) {
    super(scope, id);

    const { lambdaFunction } = props;

    const stack = cdk.Stack.of(this);
    const accountId = stack.account;

    /**
     * ロググループ ARN の組み立て
     *
     * MEMO: lambdaFunction.logGroup.logGroupArn(末尾に :* 付き)は使えない。
     *       CloudWatch が STS に送る aws:SourceArn は :* なしのため、
     *       ArnLike 条件がミスマッチして S3TableIntegrationUnauthorized が発生する。
     *       logGroupName から cdk.Arn.format で組み立てることで :* を除去する。
     */
    const logGroupArn = cdk.Arn.format(
      {
        service: "logs",
        resource: "log-group",
        resourceName: lambdaFunction.logGroup.logGroupName,
        arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME, // log-group ARN はコロン区切り(arn:...:log-group:name)
      },
      stack,
    );

    this.serviceRole = new iam.Role(this, "Default", {
      assumedBy: new iam.ServicePrincipal("logs.amazonaws.com", {
        conditions: {
          StringEquals: { "aws:SourceAccount": accountId },
          ArnLike: { "aws:SourceArn": logGroupArn },
        },
      }),
    });
    this.serviceRole.addToPolicy(
      new iam.PolicyStatement({
        actions: ["logs:integrateWithS3Table"],
        resources: [logGroupArn],
        conditions: {
          StringEquals: { "aws:ResourceAccount": accountId },
        },
      }),
    );
  }
}

この実装により CloudWatch に S3 テーブル統合が作成され、データソースとして Lambda 関数が追加されます。


CloudWatch コンソールの S3 テーブル統合画面。Integration が Active になり、Lambda 関数がデータソースとして関連付けられている

さらに CloudWatch のログ管理画面で、データソースの詳細も確認できます。


データソースの詳細画面。S3 Tables association が Active になっている

4. S3TablesCatalogConstruct

Glue の s3tablescatalog フェデレーテッドカタログを作成するコンストラクトです。Athena から S3 Tables をクエリするために使用しますが、データパイプライン自体には不要なため、必要に応じて作成してください。

lib/constructs/s3-tables-catalog/index.ts
import * as cdk from "aws-cdk-lib";
import * as glue from "aws-cdk-lib/aws-glue";
import { Construct } from "constructs";

/**
 * Glue s3tablescatalog フェデレーテッドカタログ。
 * @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-integrating-aws.html#table-integration-procedures
 */
export class S3TablesCatalogConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    /** IAM アクセス制御モードで統合する設定 */
    const iamAllowedPrincipalsAll: glue.CfnCatalog.PrincipalPermissionsProperty = {
      principal: { dataLakePrincipalIdentifier: "IAM_ALLOWED_PRINCIPALS" },
      permissions: ["ALL"],
    };

    const stack = cdk.Stack.of(this);

    new glue.CfnCatalog(this, "Default", {
      name: "s3tablescatalog",
      federatedCatalog: {
        identifier: cdk.Arn.format(
          {
            service: "s3tables",
            resource: "bucket",
            resourceName: "*",
          },
          stack,
        ),
        connectionName: "aws:s3tables",
      },
      createDatabaseDefaultPermissions: [iamAllowedPrincipalsAll], // IAM アクセス制御モード:新規 DB に IAM_ALLOWED_PRINCIPALS:ALL を自動付与
      createTableDefaultPermissions: [iamAllowedPrincipalsAll],    // IAM アクセス制御モード:新規テーブルに IAM_ALLOWED_PRINCIPALS:ALL を自動付与
      allowFullTableExternalDataAccess: "True",                    // 外部クエリエンジン(Athena 等)からのフルアクセスを許可
    });
  }
}

createDatabaseDefaultPermissions / createTableDefaultPermissionsIAM_ALLOWED_PRINCIPALS: ALL を設定することで IAM アクセス制御モードになります。これにより Lake Formation の明示的な権限付与は不要となり、IAM ポリシーのみでアクセスが制御されます。

5. AthenaWorkGroupConstruct

Athena ワークグループとクエリ結果バケットを管理するコンストラクトです。こちらも必要に応じて作成してください。

lib/constructs/athena-workgroup/index.ts
import * as athena from "aws-cdk-lib/aws-athena";
import { Construct } from "constructs";

import { ResultBucketConstruct } from "./result-bucket";

/**
 * Athena ワークグループとクエリ結果バケットを管理するコンストラクト。
 */
export class AthenaWorkGroupConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    const { bucket } = new ResultBucketConstruct(this, "ResultBucket");

    new athena.CfnWorkGroup(this, "Default", {
      name: "lambda-logs-s3tables",
      workGroupConfiguration: {
        resultConfiguration: {
          outputLocation: `s3://${bucket.bucketName}/results/`,
        },
      },
      recursiveDeleteOption: true, // スタック削除時にクエリ結果バケットも削除する(検証用スタックのため。本番では false を推奨)
    });
  }
}
lib/constructs/athena-workgroup/result-bucket/index.ts
import * as cdk from "aws-cdk-lib";
import * as s3 from "aws-cdk-lib/aws-s3";
import { Construct } from "constructs";

/**
 * Athena クエリ結果を格納する S3 バケット。
 */
export class ResultBucketConstruct extends Construct {
  readonly bucket: s3.Bucket;

  constructor(scope: Construct, id: string) {
    super(scope, id);

    this.bucket = new s3.Bucket(this, "Default", {
      removalPolicy: cdk.RemovalPolicy.DESTROY, // 検証用スタックのため。本番では RETAIN を推奨
      autoDeleteObjects: true,
    });
  }
}

6. 動作確認

デプロイ

npx cdk deploy LambdaLogsS3Tables

CloudFormation のリソース一覧で S3TablesIntegration などの各種リソースが作成されたことが確認できます。


CloudFormation リソース一覧。S3TablesIntegration・ServiceRole・S3TablesCatalog など各リソースが作成されている

この時点では aws-cloudwatch テーブルバケットは作成されていますが、名前空間(logs)とテーブルはまだ存在しません。


デプロイ直後の aws-cloudwatch テーブルバケット。テーブルはまだ作成されていない

Lambda を実行してログを発生させる

aws lambda invoke \
  --function-name <関数> \
  --payload '{}' \
  --cli-binary-format raw-in-base64-out \
  /dev/null

初回の Lambda 実行でログが S3 Tables に書き込まれ、aws-cloudwatch バケット内に logs 名前空間とテーブルが自動作成されます。 データが反映されるまで数分かかります。

数分後に S3 コンソールを確認すると、logs 名前空間配下にテーブル({関数名}__lambda)が自動作成されていることを確認できます。


数分後に aws-cloudwatch テーブルバケットを確認するとテーブルが作成されている

Athena でクエリする

S3 コンソールでテーブルの「スキーマ」タブを確認すると、Lambda ソースのため cwl__ プレフィックスの 13 カラムが定義されています。右上の「クエリ」→「Athena でテーブルのクエリを実行」から Athena コンソールに直接遷移してクエリを実行できます。


テーブルのスキーマ。右上「クエリ」ボタンから Athena に遷移できる

Athena コンソールで実行すると、Lambda 関数のログが S3 Tables から取得できていることを確認できます。右上のワークグループを lambda-logs-s3tables に切り替えてから実行してください。


ワークグループを lambda-logs-s3tables に切り替えてクエリを実行すると結果が取得できる

AWS CLI でも確認できます。

EXEC_ID=$(aws athena start-query-execution \
  --query-string 'SELECT * FROM "s3tablescatalog/aws-cloudwatch"."logs"."<テーブル名>" ORDER BY cwl__timestamp DESC LIMIT 1' \
  --work-group "lambda-logs-s3tables" \
  --result-configuration "OutputLocation=s3://<結果バケット>/results/" \
  --query 'QueryExecutionId' --output text)

aws athena get-query-results \
  --query-execution-id "$EXEC_ID" \
  --output json
cwl__aws_account:      XXXXXXXXXXXX
cwl__ingestiontime:    2026-05-04 15:49:27.964000 UTC
cwl__logstreamid:      99022dc4-...
cwl__log:              XXXXXXXXXXXX:/aws/lambda/lambdalogss3tableslambdad926ab1c
cwl__aws_region:       ap-northeast-1
cwl__data_source_name: lambdalogss3tableslambdad926ab1c
cwl__logstream:        2026/05/04/lambdalogss3tableslambdad926ab1c[$LATEST]...
cwl__data_format:      Custom
cwl__source_log:
cwl__message:          {"timestamp":"2026-05-04T15:49:27.860Z","level":"INFO","requestId":"ebc052ad-...","message":"{\"level\":\"INFO\",\"message\":\"invoked\"}"}
cwl__timestamp:        2026-05-04 15:49:27.860000 UTC
cwl__data_source_type: lambda
cwl__loggroupid:       99022dc4-4c7a-4349-9a27-57cc3b3d1704

cwl__message を整形すると次の通りです。

{
  "timestamp": "2026-05-04T15:49:27.860Z",
  "level": "INFO",
  "requestId": "ebc052ad-5b4e-4d43-b90e-a530bb4b06fa",
  "message": "{\"level\":\"INFO\",\"message\":\"invoked\"}"
}

message フィールドに {"level":"INFO","message":"invoked"} が入っており、Lambda 関数内の console.log 出力が S3 Tables に正しく格納されていることが確認できます。

ハマったポイント

CDK で実装を行う際に、以下のような点でハマりました。

その1:Lambda 関数名はアンダースコア・英数字・小文字のみ

S3 Tables のログソース名(AssociateSourceToS3TableIntegrationname)は 小文字・数字・アンダースコアのみ が有効です。ただしエラーメッセージは「英数字とアンダースコアのみ」と表示されるため、大文字が原因だと気付きにくいです。

ValidationException: Source name must only contain letters, numbers, and underscores

CDK がデフォルトで自動生成する Lambda 関数名(StackName-Resource-RandomSuffix)はハイフンと大文字を含むため NG です。functionName を明示的に指定する必要があります。

lib/constructs/lambda/index.ts
// ❌ CDK のデフォルト自動生成名(ハイフン・大文字を含む)
//    例: LambdaLogsS3Tables-LambdaDefault-u7KEHtRqDI4S
//    → AssociateSourceToS3TableIntegration の name に使うと ValidationException

// ✅ cdk.Names.uniqueId から小文字・アンダースコアのみに変換して使用
this.functionName = cdk.Names.uniqueId(this)
  .toLowerCase()
  .replace(/[^a-z0-9]/g, "_");
// 例: lambdalogss3tableslambdad926ab1c

その2:サービスロールの信頼ポリシーで logGroupArn:* を付けてはいけない

CDK の logGroup.logGroupArn は末尾に :* が付いた ARN を返します(例: arn:aws:logs:region:account:log-group:name:*)。しかし CloudWatch が STS に AssumeRole を要求する際に送る aws:SourceArn:* なし(例: arn:aws:logs:region:account:log-group:name)です。

ArnLike 条件でミスマッチが起き、S3TableIntegrationUnauthorized エラーが発生します。このエラーは CloudWatch Logs の CloudWatch メトリクス(AWS/Logs 名前空間 / S3TableIntegrationUnauthorized メトリクス)で検知できます。

lib/constructs/s3-tables-integration/index.ts
// ❌ 間違い:末尾 :* 付き
assumedBy: new iam.ServicePrincipal("logs.amazonaws.com", {
  conditions: {
    ArnLike: { "aws:SourceArn": fn.logGroup.logGroupArn }, // :* 付き → ミスマッチ
  },
}),

// ✅ 正しい:cdk.Arn.format で :* なし ARN を組み立て
const logGroupArn = cdk.Arn.format({
  service: "logs",
  resource: "log-group",
  resourceName: lambdaFunction.logGroup.logGroupName,
  arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
});

assumedBy: new iam.ServicePrincipal("logs.amazonaws.com", {
  conditions: {
    ArnLike: { "aws:SourceArn": logGroupArn }, // :* なし → 正しくマッチ
  },
}),

その3:ロググループへの cw:datasource タグ付けが必要

S3 Tables Integration を作成してログソースを関連付けても、ロググループに cw:datasource:name / cw:datasource:type タグがないと CloudWatch がデータソースとして認識せず、データが流れません。

このタグ付けは CDK で cdk.Tags.of(logGroup).add(...) を使って付与できます。LambdaFunctionConstruct でロググループを明示的に作成しているため、CDK のタグ API が直接使えます。

lib/constructs/lambda/index.ts
cdk.Tags.of(this.logGroup).add("cw:datasource:name", functionName);
cdk.Tags.of(this.logGroup).add("cw:datasource:type", "lambda");

その4:loggingFormat: JSON を指定しないと cwl__message がタブ区切りになる

loggingFormat を指定しないと Lambda のデフォルト(TEXT 形式)でログが出力されます。この場合 cwl__message の値は次のようにタブ区切りの文字列になります。

2026-05-04T15:03:34.946Z\t6929ead1-...\tINFO\t{"level":"INFO","message":"invoked"}

loggingFormat: lambda.LoggingFormat.JSON を指定すると、cwl__message は JSON 文字列になり Athena の json_extract でパースしやすくなります。

lib/constructs/lambda/index.ts
this.function = new lambda.Function(this, "Default", {
  // ...
  loggingFormat: lambda.LoggingFormat.JSON, // cwl__message を JSON 形式にするために必要
});

ポイントとなる仕様

S3 テーブルの利用料は掛からない

Excerpt で軽く触れていますが、S3 テーブル統合で作成された S3 テーブルには、CloudWatch Logs の取り込みとストレージ料金以外に、追加のストレージやテーブルのメンテナンス料金はかかりません。

この統合によって作成された S3 テーブルには、既存の CloudWatch の取り込みとストレージ料金以外に、追加のストレージやテーブルのメンテナンス料金はかかりません。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/s3-tables-integration.html#3-tables-integration-when-to-use

自動作成されるテーブルを使う必要がある点や、次項の仕様のような制約がある代わりに、安価に Apache Iceberg 形式のテーブルが利用できる、という位置付けの機能のようです。

データ保持はロググループの保持ポリシーに連動する

こちらも Excerpt で触れていますが、最も把握しておくべき仕様だと思います。S3 テーブルバケットのデータ保持は、ロググループに設定されている保持ポリシーと連動し、またロググループやログストリームの削除に連動してデータも削除されます。

S3 テーブルバケットのデータ保持は、ロググループに設定されている保持ポリシーと一致します。たとえば、ロググループを 1 日間の保持に設定すると、CloudWatch Logs は 1 日後に CloudWatch Logs と S3 Table の両方からデータを削除します。ロググループまたはログストリームを削除すると、CloudWatch Logs は S3 テーブルバケットからデータも削除します。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/s3-tables-integration.html#data-flow

これにより、CloudWatch Logs の保管料金が高いからと言って S3 テーブルにだけデータを残す、という運用はできません。 CloudWatch Logs の保持期間を長く設定するか、S3 テーブルは短期間の分析用に使い、長期保存は別途 S3 にエクスポートするなどの運用が必要になります。

S3 Tables に格納されるカラム

すでに確認しましたが、S3 Tables 側のテーブルスキーマは CloudWatch が自動定義します。取得できるカラムは以下の通りです(実際に Athena でクエリして確認)。なお、カラム名のプレフィックス cwl__ は CloudWatch Logs を示す識別子です。

カラム 内容
cwl__timestamp ログのタイムスタンプ
cwl__message ログ本文(Lambda の場合は JSON 文字列)
cwl__logstream CloudWatch Logs のストリーム名
cwl__data_source_name データソース名(Lambda 関数名)
cwl__data_source_type データソースタイプ(lambda
cwl__aws_account AWS アカウント ID
cwl__aws_region AWS リージョン
cwl__log ロググループ識別子
cwl__ingestiontime CloudWatch への取り込み時刻

おわりに

Amazon CloudWatch Logs の S3 テーブル統合機能により Lambda 関数ログを Athena でクエリ可能とする構成を CDK で実装してみました。

S3 テーブル統合の概念やリソースの関係性を把握するのに苦労しましたが、最終的な実装としてはシンプルにできました。

初めは Lake Formation への入門が必要で、その部分の実装が CDK で完結できないのでは?と構えていましたが、今年になり IAM ベースの認可がサポートされることにより、粒度の細かい制御が不要なユースケースでは、Lake Formation 設定無しで完結できるようになりました。

また、「ポイントとなる仕様」で述べた通り、CloudWatch の S3 テーブル統合機能はデータ保持がロググループの保持ポリシーに連動することを理解した上で使う必要があります。短期間の分析用途として簡単に Apache Iceberg 形式のテーブルが利用できる機能、という位置付けで使うようにしましょう。

参考

https://deepcopy.pages.dev/blog/2026/03/2026-03-17_s3-tables-simplified-permissions/

以上

この記事をシェアする

関連記事