🛡️

[AWS CDK] WAFのログをS3 or CloudWatchに直接出力する構成のCDKコード

2022/08/22に公開

[アップデート] AWS WAFのログを直接CloudWatch LogsおよびS3に出力可能になりました | DevelopersIO

こちらをCDKで構築する日本語情報があまり見当たらなかったので記事にしてみます。

注意点として、ログ出力先のS3とCloudWatchのリソース名にはPrefixとして aws-waf-logs- を必ず付けないといけないので押さえておきましょう(自分は当初、これを認識しておらずやや時間を使ってしまいました)。

環境

  • aws-cdk-lib 2.30.0
  • constructs 10.1.43

APIGW+WebACLを定義

まずはAPIGWのログをWAF WebACLと紐付けるCDKコードを準備しました。

import { Construct } from "constructs"
import * as cdk from "aws-cdk-lib"

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

    // APIGW+Lambda
    const restApi = new cdk.aws_apigateway.RestApi(this, "restApi", {
      restApiName: "restApi",
      deployOptions: {
        stageName: "dev",
      },
    })
    const pingFn = new cdk.aws_lambda.Function(this, "pingFn", {
      code: cdk.aws_lambda.Code.fromInline(`
        exports.handler = (event, context, callback) => {
          callback(null, { statusCode: 200, body: JSON.stringify({ message: "pong" }) });
        };
      `),
      handler: "index.handler",
      runtime: cdk.aws_lambda.Runtime.NODEJS_16_X,
    })
    restApi.root.addMethod(
      "GET",
      new cdk.aws_apigateway.LambdaIntegration(pingFn)
    )

    // WAF Web ACL
    const webAcl = new cdk.aws_wafv2.CfnWebACL(this, "wafV2WebAcl", {
      defaultAction: { allow: {} },
      scope: "REGIONAL",
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        sampledRequestsEnabled: true,
        metricName: "wafV2WebAcl",
      },
      rules: [
        {
          name: "AWSManagedRulesCommonRuleSet",
          priority: 1,
          statement: {
            managedRuleGroupStatement: {
              vendorName: "AWS",
              name: "AWSManagedRulesCommonRuleSet",
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: "AWSManagedRulesCommonRuleSet",
          },
        },
      ],
    })

    // APIGWとWebACLを紐付ける
    const webAclAssociation = new cdk.aws_wafv2.CfnWebACLAssociation(
      this,
      "webAclAssociation",
      {
        resourceArn: `arn:aws:apigateway:${this.region}:${this.account}:/restapis/${restApi.restApiId}/stages/dev`,
        webAclArn: webAcl.attrArn,
      }
    )
    webAclAssociation.addDependsOn(webAcl)
    webAclAssociation.addDependsOn(
      restApi.deploymentStage.node.defaultChild as cdk.CfnResource
    )
  }
}

S3にログ出力

まずはS3バケットにログ出力する場合。

    webAclAssociation.addDependsOn(webAcl)
    webAclAssociation.addDependsOn(
      restApi.deploymentStage.node.defaultChild as cdk.CfnResource
    )
+
+   // WAFログ用S3バケット
+   const bucket = new cdk.aws_s3.Bucket(this, "awsWafLogsBucket", {
+     bucketName: `aws-waf-logs-${this.account}-bucket`,
+     removalPolicy: cdk.RemovalPolicy.DESTROY,
+     blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL,
+     encryption: cdk.aws_s3.BucketEncryption.S3_MANAGED,
+   })
+
+   // WAFログ出力設定
+   const logConfig = new cdk.aws_wafv2.CfnLoggingConfiguration(
+     this,
+     "wafV2LoggingConfiguration",
+     {
+       logDestinationConfigs: [bucket.bucketArn],
+       resourceArn: webAcl.attrArn,
+     }
+   )
+   logConfig.addDependsOn(webAcl)
+   logConfig.addDependsOn(bucket.node.defaultChild as cdk.CfnResource)
  }
}

デプロイし、AWSマネコンにて設定できていることを確認しました。

CloudWatchにログ出力

続いてCloudWatch Logsにログ出力する場合。

    webAclAssociation.addDependsOn(webAcl)
    webAclAssociation.addDependsOn(
      restApi.deploymentStage.node.defaultChild as cdk.CfnResource
    )
+
+   // WAFログ用CloudWatchロググループ
+   const logGroup = new cdk.aws_logs.LogGroup(this, "awsWafLogsLogGroup", {
+     logGroupName: "aws-waf-logs-log-group",
+     removalPolicy: cdk.RemovalPolicy.DESTROY,
+   })
+
+   // WAFログ出力設定
+   const logConfig = new cdk.aws_wafv2.CfnLoggingConfiguration(
+     this,
+     "wafV2LoggingConfiguration",
+     {
+       logDestinationConfigs: [
+         `arn:aws:logs:${this.region}:${this.account}:log-group:${logGroup.logGroupName}`,
+       ],
+       resourceArn: webAcl.attrArn,
+     }
+   )
+   logConfig.addDependsOn(webAcl)
+   logConfig.addDependsOn(logGroup.node.defaultChild as cdk.CfnResource)
  }
}

こちらもデプロイし、AWSマネコンにて設定できていることを確認しました。

ちなみに、 logDestinationConfigs: の値は [logGroup.logGroupArn] でも良さそうに見えますが、この値は arn:aws:logs:ap-northeast-1:xxxxxxxxxxxx:log-group:aws-waf-logs-log-group:* といったように末尾に :* が付いているためエラーになってしまいます。

 ❌  WafStack failed: Error: The stack named WafStack failed to deploy: UPDATE_ROLLBACK_COMPLETE: Resource handler returned message: "Error reason: The ARN isn't valid. A valid ARN begins with arn: and includes other information separated by colons or slashes., field: LOG_DESTINATION, parameter: arn:aws:logs:ap-northeast-1:xxxxxxxxxxxx:log-group:aws-waf-logs-log-group:* (Service: Wafv2, Status Code: 400, Request ID: xxxxxxxxxxxxxxxxx, Extended Request ID: null)" (RequestToken: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, HandlerErrorCode: InvalidRequest)
    at prepareAndExecuteChangeSet

そのためロググループARNは自前で文字列結合しています。

参考

Discussion