AWS WAF のログを CloudWatch Logs ロググループに出力する際、ロググループ名は aws-waf-logs- で始まる必要がある

AWS WAF のログを CloudWatch Logs ロググループに出力する際、ロググループ名は aws-waf-logs- で始まる必要があります。
2024.03.06

コーヒーが好きな emi です。

タイトルの通りなのですが、AWS WAF のログを CloudWatch Logs ロググループに出力する際、ロググループ名は aws-waf-logs- で始まる必要があるというのを知らずにかなり悩んでしまったので、二度と間違えないためにブログにします。

Terraform で WAF を作成しようとしていた

Terraform で WAF を作成しようと Cloud9 上で作業していました。
以下のようなディレクトリ構成で WAF モジュールを作成し、マルチリージョン構成でも展開可能な形で準備しています。
一旦セカンダリリージョンは null として作成しないようにし、テストしました。

~/environment/emiki-terraform

├enves
│  └dev
│    ├backend.tf
│    └main.tf
└modules
  └waf
    ├global.tf
    ├main.tf
    ├outputs.tf
    ├provider.tf
    └varuables.tf

エラー

terraform initterraform plan と問題無く完了したので、リソースをデプロイするため terraform apply を実行します。

terraform apply 実行結果全体(クリックで展開)
ユーザー名:~/environment/emiki-terraform/envs/dev $ terraform apply
data.aws_caller_identity.current: Reading...
data.aws_caller_identity.current: Read complete after 0s [id=123456789012]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.waf.aws_cloudwatch_log_group.primary will be created
  + resource "aws_cloudwatch_log_group" "primary" {
      + arn               = (known after apply)
      + id                = (known after apply)
      + log_group_class   = (known after apply)
      + name              = "emiki-dev-pri-waf-loggroup"
      + name_prefix       = (known after apply)
      + retention_in_days = 30
      + skip_destroy      = false
      + tags_all          = (known after apply)
    }

  # module.waf.aws_wafv2_ip_set.primary will be created
  + resource "aws_wafv2_ip_set" "primary" {
      + addresses          = [
          + "104.28.206.29/32",
        ]
      + arn                = (known after apply)
      + description        = "emiki-dev-pri-waf-ipsets"
      + id                 = (known after apply)
      + ip_address_version = "IPV4"
      + lock_token         = (known after apply)
      + name               = "emiki-dev-pri-waf-ipsets"
      + scope              = "REGIONAL"
      + tags               = {
          + "Name" = "emiki-dev-pri-webacl"
        }
      + tags_all           = {
          + "Name" = "emiki-dev-pri-webacl"
        }
    }

  # module.waf.aws_wafv2_rule_group.primary will be created
  + resource "aws_wafv2_rule_group" "primary" {
      + arn         = (known after apply)
      + capacity    = 10
      + description = "emiki-dev-pri-waf-rulegp1"
      + id          = (known after apply)
      + lock_token  = (known after apply)
      + name        = "emiki-dev-pri-waf-rulegp1"
      + name_prefix = (known after apply)
      + scope       = "REGIONAL"
      + tags_all    = (known after apply)

      + rule {
          + name     = "emiki-dev-pri-waf-rule1"
          + priority = 1

          + action {
              + block {
                }
            }

          + statement {
              + and_statement {
                  + statement {
                      + byte_match_statement {
                          + positional_constraint = "EXACTLY"
                          + search_string         = "/path/emiki-admin/"

                          + field_to_match {
                              + uri_path {}
                            }

                          + text_transformation {
                              + priority = 0
                              + type     = "NONE"
                            }
                        }
                    }
                  + statement {
                      + not_statement {
                          + statement {
                              + ip_set_reference_statement {
                                  + arn = (known after apply)
                                }
                            }
                        }
                    }
                }
            }

          + visibility_config {
              + cloudwatch_metrics_enabled = true
              + metric_name                = "emiki-dev-pri-waf-rule1"
              + sampled_requests_enabled   = true
            }
        }

      + visibility_config {
          + cloudwatch_metrics_enabled = true
          + metric_name                = "emiki-dev-pri-waf-rulegp1"
          + sampled_requests_enabled   = true
        }
    }

  # module.waf.aws_wafv2_web_acl.primary will be created
  + resource "aws_wafv2_web_acl" "primary" {
      + application_integration_url = (known after apply)
      + arn                         = (known after apply)
      + capacity                    = (known after apply)
      + description                 = "emiki-dev-pri-webacl"
      + id                          = (known after apply)
      + lock_token                  = (known after apply)
      + name                        = "emiki-dev-pri-webacl"
      + scope                       = "REGIONAL"
      + tags                        = {
          + "Name" = "emiki-dev-pri-webacl"
        }
      + tags_all                    = {
          + "Name" = "emiki-dev-pri-webacl"
        }

      + default_action {
          + allow {
            }
        }

      + rule {
          + name     = "AWS-AWSManagedRulesATPRuleSet"
          + priority = 6

          + override_action {
              + count {}
            }

          + statement {
              + managed_rule_group_statement {
                  + name        = "AWSManagedRulesATPRuleSet"
                  + vendor_name = "AWS"

                  + managed_rule_group_configs {
                      + aws_managed_rules_atp_rule_set {
                          + login_path = "var.primary.login_path"

                          + request_inspection {
                              + payload_type = "JSON"

                              + password_field {
                                  + identifier = "var.primary.identifier_password"
                                }

                              + username_field {
                                  + identifier = "var.primary.identifier_username"
                                }
                            }
                        }
                    }
                }
            }

          + visibility_config {
              + cloudwatch_metrics_enabled = true
              + metric_name                = "AWS-AWSManagedRulesATPRuleSet"
              + sampled_requests_enabled   = false
            }
        }
      + rule {
          + name     = "AWS-AWSManagedRulesAmazonIpReputationList"
          + priority = 4

          + override_action {
              + count {}
            }

          + statement {
              + managed_rule_group_statement {
                  + name        = "AWSManagedRulesAmazonIpReputationList"
                  + vendor_name = "AWS"
                }
            }

          + visibility_config {
              + cloudwatch_metrics_enabled = true
              + metric_name                = "AWS-AWSManagedRulesAmazonIpReputationList"
              + sampled_requests_enabled   = true
            }
        }
      + rule {
          + name     = "AWS-AWSManagedRulesAnonymousIpList"
          + priority = 5

          + override_action {
              + count {}
            }

          + statement {
              + managed_rule_group_statement {
                  + name        = "AWSManagedRulesAnonymousIpList"
                  + vendor_name = "AWS"
                }
            }

          + visibility_config {
              + cloudwatch_metrics_enabled = true
              + metric_name                = "AWS-AWSManagedRulesAnonymousIpList"
              + sampled_requests_enabled   = true
            }
        }
      + rule {
          + name     = "AWS-AWSManagedRulesCommonRuleSet"
          + priority = 0

          + override_action {
              + count {}
            }

          + statement {
              + managed_rule_group_statement {
                  + name        = "AWSManagedRulesCommonRuleSet"
                  + vendor_name = "AWS"
                }
            }

          + visibility_config {
              + cloudwatch_metrics_enabled = true
              + metric_name                = "AWS-AWSManagedRulesCommonRuleSet"
              + sampled_requests_enabled   = true
            }
        }
      + rule {
          + name     = "AWS-AWSManagedRulesKnownBadInputsRuleSet"
          + priority = 1

          + override_action {
              + count {}
            }

          + statement {
              + managed_rule_group_statement {
                  + name        = "AWSManagedRulesKnownBadInputsRuleSet"
                  + vendor_name = "AWS"
                }
            }

          + visibility_config {
              + cloudwatch_metrics_enabled = true
              + metric_name                = "AWS-AWSManagedRulesKnownBadInputsRuleSet"
              + sampled_requests_enabled   = true
            }
        }
      + rule {
          + name     = "AWS-AWSManagedRulesLinuxRuleSet"
          + priority = 2

          + override_action {
              + count {}
            }

          + statement {
              + managed_rule_group_statement {
                  + name        = "AWSManagedRulesLinuxRuleSet"
                  + vendor_name = "AWS"
                }
            }

          + visibility_config {
              + cloudwatch_metrics_enabled = true
              + metric_name                = "AWS-AWSManagedRulesLinuxRuleSet"
              + sampled_requests_enabled   = true
            }
        }
      + rule {
          + name     = "AWS-AWSManagedRulesSQLiRuleSet"
          + priority = 3

          + override_action {
              + count {}
            }

          + statement {
              + managed_rule_group_statement {
                  + name        = "AWSManagedRulesSQLiRuleSet"
                  + vendor_name = "AWS"
                }
            }

          + visibility_config {
              + cloudwatch_metrics_enabled = true
              + metric_name                = "AWS-AWSManagedRulesSQLiRuleSet"
              + sampled_requests_enabled   = true
            }
        }
      + rule {
          + name     = "emiki-dev-pri-waf-rulegp1"
          + priority = 7

          + override_action {
              + count {}
            }

          + statement {
              + rule_group_reference_statement {
                  + arn = (known after apply)
                }
            }

          + visibility_config {
              + cloudwatch_metrics_enabled = true
              + metric_name                = "emiki-dev-pri-waf-rulegp1"
              + sampled_requests_enabled   = true
            }
        }

      + visibility_config {
          + cloudwatch_metrics_enabled = true
          + metric_name                = "emiki-dev-pri-webacl"
          + sampled_requests_enabled   = true
        }
    }

  # module.waf.aws_wafv2_web_acl_association.primary will be created
  + resource "aws_wafv2_web_acl_association" "primary" {
      + id           = (known after apply)
      + resource_arn = "arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/test-alb/93f466xxxxxxxxxx"
      + web_acl_arn  = (known after apply)
    }

  # module.waf.aws_wafv2_web_acl_logging_configuration.primary will be created
  + resource "aws_wafv2_web_acl_logging_configuration" "primary" {
      + id                      = (known after apply)
      + log_destination_configs = (known after apply)
      + resource_arn            = (known after apply)

      + logging_filter {
          + default_behavior = "DROP"

          + filter {
              + behavior    = "KEEP"
              + requirement = "MEETS_ANY"

              + condition {
                  + action_condition {
                      + action = "COUNT"
                    }
                }
            }
        }
    }

Plan: 6 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.waf.aws_cloudwatch_log_group.primary: Creating...
module.waf.aws_wafv2_ip_set.primary: Creating...
module.waf.aws_cloudwatch_log_group.primary: Creation complete after 1s [id=emiki-dev-pri-waf-loggroup]
module.waf.aws_wafv2_ip_set.primary: Creation complete after 0s [id=83c7113b-c8e0-423c-a341-d3xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.primary: Creating...
module.waf.aws_wafv2_rule_group.primary: Creation complete after 1s [id=9ee22a4b-1af4-4caf-95ed-afxxxxxxxxxx]
module.waf.aws_wafv2_web_acl.primary: Creating...
module.waf.aws_wafv2_web_acl.primary: Creation complete after 3s [id=9e639a03-4581-4cb7-8907-xxxxxxxxxxxx]
module.waf.aws_wafv2_web_acl_logging_configuration.primary: Creating...
module.waf.aws_wafv2_web_acl_association.primary: Creating...
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [10s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [20s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [30s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [40s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [50s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [1m0s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [1m10s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [1m20s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [1m30s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [1m40s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [1m50s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [2m0s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [2m10s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [2m20s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [2m30s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [2m40s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [2m50s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Still creating... [3m0s elapsed]
module.waf.aws_wafv2_web_acl_association.primary: Creation complete after 3m4s [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/emiki-dev-pri-webacl/9e639a03-4581-4cb7-8907-xxxxxxxxxxxx,arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/test-alb/93f466xxxxxxxxxx]
╷
│ Error: putting WAFv2 WebACL Logging Configuration (arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/emiki-dev-pri-webacl/9e639a03-4581-4cb7-8907-xxxxxxxxxxxx): WAFInvalidParameterException: 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:123456789012:log-group:emiki-dev-pri-waf-loggroup
│ {
│   RespMetadata: {
│     StatusCode: 400,
│     RequestID: "0ae3a6c4-5bd7-4d8c-a0be-6623e8c39d1a"
│   },
│   Field: "LOG_DESTINATION",
│   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:123456789012:log-group:emiki-dev-pri-waf-loggroup",
│   Parameter: "arn:aws:logs:ap-northeast-1:123456789012:log-group:emiki-dev-pri-waf-loggroup",
│   Reason: "The ARN isn't valid. A valid ARN begins with arn: and includes other information separated by colons or slashes."
│ }
│ 
│   with module.waf.aws_wafv2_web_acl_logging_configuration.primary,
│   on .terraform/modules/waf/modules/waf/main.tf line 310, in resource "aws_wafv2_web_acl_logging_configuration" "primary":
│  310: resource "aws_wafv2_web_acl_logging_configuration" "primary" {
│ 
╵
ユーザー名:~/environment/emiki-terraform/envs/dev $

▼エラー部分抜粋

╷
│ Error: putting WAFv2 WebACL Logging Configuration (arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/emiki-dev-pri-webacl/9e639a03-4581-4cb7-8907-xxxxxxxxxxxx): WAFInvalidParameterException: 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:123456789012:log-group:emiki-dev-pri-waf-loggroup
│ {
│   RespMetadata: {
│     StatusCode: 400,
│     RequestID: "0ae3a6c4-5bd7-4d8c-a0be-6623e8c39d1a"
│   },
│   Field: "LOG_DESTINATION",
│   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:123456789012:log-group:emiki-dev-pri-waf-loggroup",
│   Parameter: "arn:aws:logs:ap-northeast-1:123456789012:log-group:emiki-dev-pri-waf-loggroup",
│   Reason: "The ARN isn't valid. A valid ARN begins with arn: and includes other information separated by colons or slashes."
│ }
│ 
│   with module.waf.aws_wafv2_web_acl_logging_configuration.primary,
│   on .terraform/modules/waf/modules/waf/main.tf line 310, in resource "aws_wafv2_web_acl_logging_configuration" "primary":
│  310: resource "aws_wafv2_web_acl_logging_configuration" "primary" {
│ 
╵

The ARN isn't valid. A valid ARN begins with arn: and includes other information separated by colons or slashes. とエラーが出ています。
機械翻訳すると ARN が有効でない。有効な ARNはarn:で始まり、コロンまたはスラッシュで区切られたその他の情報を含む。 ということ…

CloudWatch Logs ロググループは同じモジュールの中で作成していて、ロググループの ARN はその作成したものを変数で呼んでいるので、ARN が間違っているとは考えにくいと思いました。

調査

エラーは出ていても Web ACL 自体は作成されていたので、マネジメントコンソールからログの設定を確認してみます。

ロギングは無効になっていました。Enable をクリックして有効化の設定を見てみます。

CloudWatch Logs log group をチェックした状態でロググループを選択しようとすると、候補が出てきません。

CloudWatch Logs には私が過去の検証で残してあるロググループがいくつかあります。Terraform から作成したロググループはちゃんと存在していました。

なぜロググループの候補が出てこないのだろうと思い WAF の画面に戻ると、気になる記述を発見しました。

Select a log group in your account that begins with 'aws-waf-logs-' or create one in the Amazon CloudWatch console. You must use a log group that's associated with your account.
(機械翻訳)アカウント内の「aws-waf-logs-」で始まるロググループを選択するか、Amazon CloudWatchコンソールで作成します。アカウントに関連付けられているロググループを使用する必要があります。

ここでやっと、aws-waf-logs- で始まるロググループ名でないといけないことに気づきました。試しに CloudWatch Logs コンソールで aws-waf-logs- から始まるロググループを作成してみます。

すると、WAF のロググループの候補に作成したロググループが出てきました。

原因

AWS WAF のログを CloudWatch Logs ロググループに出力する際、ロググループ名は aws-waf-logs- で始まる必要がありました。公式ドキュメントや re:Post にもばっちり記載されています。

Terraform のドキュメント log_destination_configs にも記載されていました。

Note: data firehose, log group, or bucket name must be prefixed with aws-waf-logs-, e.g. aws-waf-logs-example-firehose, aws-waf-logs-example-log-group, or aws-waf-logs-example-bucket.

出力エラー The ARN isn't valid. A valid ARN begins with arn: and includes other information separated by colons or slashes. で検索したらブログも引っ掛かりました。

先人の知見は偉大ですね。

おわりに

たったこれだけのために 1 時間近く費やしてしまい非常に無念です。最初に「エラー内容でググる」、「マネジメントコンソールからログ出力の設定を確認する」おけばここまで悩まずに済んだと思います。エラー内容から「ARN の指定に問題があるのでは…」と考え頑張ってしまいました。
どなた様も同じ轍を踏まないことを願っています。
どなたかのお役に立てば幸いです。

参考

aws_wafv2_ip_set | Resources | hashicorp/aws | Terraform | Terraform Registry

aws_wafv2_rule_group | Resources | hashicorp/aws | Terraform | Terraform Registry

aws_wafv2_web_acl | Resources | hashicorp/aws | Terraform | Terraform Registry

aws_cloudwatch_log_group | Resources | hashicorp/aws | Terraform | Terraform Registry

aws_wafv2_web_acl_logging_configuration | Resources | hashicorp/aws | Terraform | Terraform Registry