CDKでWAFのアクセスログをS3に保存する方法

CDKでWAFのアクセスログをS3に保存する方法

Clock Icon2025.05.18

こんにちは、リテールアプリ共創部の戸田駿太です。

この記事では、AWS CDK を使用して、WAF ログを S3 バケットに保存するための環境を構築する方法を解説します。

WAF のログを S3 に保存することで、後からログを分析してセキュリティの改善に役立てることができます。

実装するアーキテクチャ

今回実装するアーキテクチャは以下の通りです:

  1. WAF ログを S3 バケットに保存
  2. Kinesis Firehose を使用してログを S3 に配信
  3. 必要な IAM ロールとポリシーを作成
  4. API Gateway と Lambda を作成し、WAF で保護

ソースコード

https://github.com/ShuntaToda/athena-waf-block-cdk

S3 バケットの構造

WAF ログは、Kinesis Firehose を介して S3 バケットに出力されます。パス構造は以下のようになります:

my-project-waf-logs-{環境名}/
   └─ waf-logs/
         └─ {リージョン}/
               └─ {年}/
                     └─ {月}/
                           └─ {日}/
                                 └─ {時間}/
                                       └─ {AWSアカウントID}_{WAFログ}_{リージョン}_{年}_{月}_{日}_{時間}_{ランダム文字列}.gz

Firehose の設定では prefix: "waf-logs/" を指定し、その下に Firehose が自動的に日付ベースのパーティション(YYYY/MM/DD/HH/)を作成します。詳細なファイル名の命名規則は Firehose のデフォルトの挙動や WAF ログの仕様によって決まります。

CDK による実装

今回の実装では、WAF のログを S3 に保存するための基本的なリソースを CDK で定義します。

スタックの作成

まず、必要なリソースを含む CDK スタックを作成します。以下のコードでは、WAF ログを保存するための安全な設定の S3 バケットを作成しています。

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

    // WAFログを保存するS3バケットを作成
    const wafLogBucket = new s3.Bucket(this, "WafLogBucket", {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      encryption: s3.BucketEncryption.S3_MANAGED,
    });

    // 以下、各リソースの実装...
  }
}

WAF の設定

続いて、WebACL と IP セットを作成し、ブロックルールを定義します。このコードでは、特定の IP アドレスをブロックするためのルールを持つ WAF WebACL を設定します。

// WAFのIPセットを作成(ブロックするIPアドレスのリスト)
const blockedIpSet = new wafv2.CfnIPSet(this, "BlockedIpSet", {
  addresses: [], // ブロックするIPアドレスを指定
  ipAddressVersion: "IPV4",
  scope: "REGIONAL",
  name: "blocked-ip-set",
});

// WAF WebACLを作成
const webAcl = new wafv2.CfnWebACL(this, "WebAcl", {
  defaultAction: { allow: {} },
  scope: "REGIONAL",
  visibilityConfig: {
    cloudWatchMetricsEnabled: true,
    metricName: "WAFWebACL",
    sampledRequestsEnabled: true,
  },
  name: "waf-web-acl",
  rules: [
    {
      name: "BlockedIpRule",
      priority: 0,
      statement: {
        ipSetReferenceStatement: {
          arn: blockedIpSet.attrArn,
        },
      },
      action: { block: {} },
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        metricName: "BlockedIpRule",
        sampledRequestsEnabled: true,
      },
    },
  ],
});

デフォルトでは全てのトラフィックを許可し、IP セットに含まれるアドレスからのリクエストをブロックする設定です。CloudWatch 指標も有効にして、WAF の動作を監視できるようにしています。

ログ配信の設定

WAF のログを S3 バケットに配信するための Kinesis Firehose を設定します。このコードでは、WAF からのログを S3 に転送するためのストリームと必要な権限を設定しています。

// Firehose用のIAMロールを作成
const firehoseRole = new iam.Role(this, "FirehoseRole", {
  assumedBy: new iam.ServicePrincipal("firehose.amazonaws.com"),
});

// S3バケットへの書き込み権限を付与
wafLogBucket.grantWrite(firehoseRole);

// Firehoseデリバリーストリームを作成
const wafLogsDeliveryStream = new firehose.CfnDeliveryStream(this, "WafLogsDeliveryStream", {
  deliveryStreamType: "DirectPut",
  deliveryStreamName: "aws-waf-logs-delivery-stream",
  s3DestinationConfiguration: {
    bucketArn: wafLogBucket.bucketArn,
    roleArn: firehoseRole.roleArn,
    bufferingHints: {
      intervalInSeconds: 60,
      sizeInMBs: 1,
    },
    prefix: "waf-logs/",
    errorOutputPrefix: "waf-logs-errors/",
  },
});

// WAFのログ設定
const wafLogConfiguration = new wafv2.CfnLoggingConfiguration(this, "WafLoggingConfiguration", {
  logDestinationConfigs: [wafLogsDeliveryStream.attrArn],
  resourceArn: webAcl.attrArn,
});

API Gateway の作成と WAF との連携

API Gateway と Lambda 関数を作成し、WAF で保護する設定を追加します。このセクションでは、保護対象となる API のインフラを設定しています。

// API GatewayのアクセスログをCloudWatchに保存するためのロググループを作成
const apiAccessLogs = new logs.LogGroup(this, "ApiGatewayAccessLogs", {
  retention: logs.RetentionDays.ONE_WEEK,
  removalPolicy: cdk.RemovalPolicy.DESTROY,
});

// API Gateway用のロギングロールを作成
const apiGatewayLoggingRole = new iam.Role(this, "ApiGatewayLoggingRole", {
  assumedBy: new iam.ServicePrincipal("apigateway.amazonaws.com"),
  managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonAPIGatewayPushToCloudWatchLogs")],
});

// API Gatewayのアカウントレベル設定を構成
// この設定はCloudWatchログを有効にするためにアカウントレベルで必要
const apiGatewayAccount = new apigateway.CfnAccount(this, "ApiGatewayAccount", {
  cloudWatchRoleArn: apiGatewayLoggingRole.roleArn,
});

// サンプルのLambda関数を作成
const apiLambda = new lambda.Function(this, "ApiFunction", {
  runtime: lambda.Runtime.NODEJS_18_X,
  code: lambda.Code.fromInline(`
    exports.handler = async function(event, context) {
      console.log('Event: ', JSON.stringify(event, null, 2));
      return {
        statusCode: 200,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          message: 'Hello from Lambda!',
          timestamp: new Date().toISOString(),
          requestContext: event.requestContext
        })
      };
    };
  `),
  handler: "index.handler",
  logRetention: logs.RetentionDays.ONE_WEEK,
});

// API Gatewayを作成
const api = new apigateway.RestApi(this, "SampleApi", {
  deployOptions: {
    stageName: "prod",
    // アクセスログの設定
    accessLogDestination: new apigateway.LogGroupLogDestination(apiAccessLogs),
    accessLogFormat: apigateway.AccessLogFormat.jsonWithStandardFields({
      caller: true,
      httpMethod: true,
      ip: true,
      protocol: true,
      requestTime: true,
      resourcePath: true,
      responseLength: true,
      status: true,
      user: true,
    }),
    // スロットリング設定
    methodOptions: {
      "/*/*": {
        // すべてのリソースとメソッドに対して適用
        throttlingRateLimit: 100, // 1秒あたりのリクエスト数
        throttlingBurstLimit: 50, // バーストリクエスト数
      },
    },
  },
  // CORS設定
  defaultCorsPreflightOptions: {
    allowOrigins: apigateway.Cors.ALL_ORIGINS,
    allowMethods: apigateway.Cors.ALL_METHODS,
  },
});

// API GatewayのデプロイメントがアカウントのCloudWatch設定に依存するように設定
const deployment = api.node.findChild("Deployment") as apigateway.CfnDeployment;
deployment.node.addDependency(apiGatewayAccount);

// エンドポイントの設定
api.root.addMethod("GET", new apigateway.LambdaIntegration(apiLambda));
const samples = api.root.addResource("samples");
samples.addMethod("GET", new apigateway.LambdaIntegration(apiLambda));
const sampleItem = samples.addResource("{id}");
sampleItem.addMethod("GET", new apigateway.LambdaIntegration(apiLambda));

// WAF WebACLをAPI Gatewayに関連付け
const webAclAssociation = new wafv2.CfnWebACLAssociation(this, "ApiWafAssociation", {
  resourceArn: `arn:aws:apigateway:${this.region}::/restapis/${api.restApiId}/stages/${api.deploymentStage.stageName}`,
  webAclArn: webAcl.attrArn,
});

シンプルな Lambda 関数とそれを呼び出す API エンドポイントを作成し、WAF WebACL と関連付けています。API Gateway には CORS の設定やスロットリング、詳細なアクセスログ記録も設定されています。

リソース情報の出力

最後に、作成したリソースの情報を出力として定義します。これによりデプロイ後にリソース情報を簡単に参照できるようになります。

// リソース情報を出力
new cdk.CfnOutput(this, "WebAclId", {
  value: webAcl.attrId,
  description: "The ID of the WAF Web ACL",
});

new cdk.CfnOutput(this, "WafLogBucketName", {
  value: wafLogBucket.bucketName,
  description: "The name of the S3 bucket storing WAF logs",
});

new cdk.CfnOutput(this, "ApiGatewayUrl", {
  value: api.url,
  description: "The URL of the API Gateway",
});

new cdk.CfnOutput(this, "ApiGatewayAccessLogsArn", {
  value: apiAccessLogs.logGroupArn,
  description: "The ARN of the API Gateway access logs",
});

出力には、作成した WAF WebACL の ID、ログを保存する S3 バケット名、API Gateway の URL とアクセスログの ARN が含まれています。これらの情報はコンソールで確認したり、別のスタックから参照したりするのに役立ちます。

まとめ

このブログでは、AWS CDK を使用して WAF ログを S3 に保存する方法について解説しました。WAF ログを S3 に保存することで、後からログを分析してセキュリティの改善に役立てることができます。

こちらの記事では、WAF ログを Athena を使用して分析する方法について解説しています。
https://dev.classmethod.jp/articles/cdk-athena-waf-log-analysis/

ぜひ参考にしてみてください!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.