Terraform でパスと IP を指定して AWS WAF のルールグループを動的に生成する

2024.04.10

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

ALB に紐づける AWS WAF のルールグループを動的に生成する Terraform コードを作成したので紹介します。
このコードの使い方は 使い方注意 を先に参照ください。

  • 想定読者
    • 1 つの .tf ファイルで VPC や EC2 を apply するまでを実施したことがある
    • モジュールの概念を知っている
    • AWS WAF を構築したことがあり、Web ACL、ルールグループ、ルールなどの概念を知っている

追記:2023/3/30、Terraform 公式からスタイルガイドが公開されました。本記事で案内するコード例とは方針が異なっている部分もあります。コーディングの際は公式スタイルガイドやそれぞれのプロジェクトの方針に従ってください。

やることのイメージ

  1. Terraform コードの中で、名前、優先度、パス、IP の組み合わせを追記
  2. Terraform で更新
  3. 指定した内容で IP Sets とルールグループを動的に生成

「指定したパスへのアクセス」かつ「指定した IP アドレスからのアクセス」以外はブロックする、というルールグループを動的に生成します。

具体的に値を入れたときのイメージは以下です。

ディレクトリ構造

Cloud9 で「terraform-test2」という名前の Terraform 実行環境を作成しています。環境の準備方法は以下ブログを参照ください。

/home/ec2-user/environment 配下のディレクトリ構造は以下のようになっています。

test-terraform
├envs
|  └dev-waf
|    ├backend.tf
|    ├main.tf
│    └provider.tf
├modules
|  └waf
│    ├locals.tf
│    ├main.tf
│    ├outputs.tf
│    └variables.tf
└shared
   └common_values
     ├outputs.tf
     └waf_rules.tf

それぞれ以下のように呼びます。

  • envs 配下:ルートモジュール
  • modules 配下:子モジュール
  • shared 配下:共有モジュール

モジュールに関する補足(クリックで展開)

ルートモジュール(envs)

ルートモジュールは Terraform 設定の最上位レベルを表し、インフラストラクチャ全体の構成を定義する場所です。今回は子モジュールである modules.waf を呼び出して AWS アカウント上に WAF リソースを作成します。

子モジュール(modules)

子モジュールは再利用可能な Terraform 設定の単位を表し、特定の機能や構成を実装するために使用されます。今回は WAF モジュール 1 つしか用意しませんが、例えば VPC モジュール、container モジュールなどを用意しておくことで、複数の環境に同じモジュールを使っていくつも同じ設定の VPC やコンテナを作成することができます。

共有モジュール(shared/common_values)

共有モジュールは複数の環境や設定で共有される共通の値を保持するために使用されます。例えば dev 環境、stg 環境、prd 環境の 3 環境ある場合、この 3 つの環境に共通の値をここで定義することで、設定箇所が一つになります。

shared/common_values

waf_rules.tf

ここに追加したいルールグループの要素を追記します。

shared/common_values/waf_rules.tf(クリックで展開)

waf_rules.tf

locals {
  # アクセス制限をかける path と IP アドレスのリスト
  access_ristricted_paths_ips = [
    # {
    #   name = "access-ristrict-sample1"
    #   priority = 1 # <- 例。 priority は 1~999 で設定する。WAF モジュール側で +1000
    #   allow_path     = "/ristrict/sample1/"
    #   allow_ip_list = [
    #     "xx.xx.xx.xx/32", // AWS WAF の制約で末尾 /0 は指定できない(0.0.0.0/0 は指定できない)。すべてのアクセスを許可する場合、["0.0.0.0/1","128.0.0.0/1"] とする
    #     "yy.yy.yy.yy/32",
    #     "zz.zz.zz.zz/32",
    #   ]
    # },
    {
      name           = "access-ristrict-sample2"
      priority       = 1
      allow_path     = "/ristrict/sample2/"
      allow_ip_list  = [
        "0.0.0.0/1",
        "128.0.0.0/1",
      ]
    },
        {
      name           = "access-ristrict-sample3"
      priority       = 2
      allow_path     = "/ristrict/sample3/"
      allow_ip_list  = [
        "0.0.0.0/1",
        "128.0.0.0/1",
      ]
    },
  ]
}

outputs.tf

shared/common_values/outputs.tf(クリックで展開)

outputs.tf

# waf_rules
output "access_ristricted_paths_ips" {
  value = local.access_ristricted_paths_ips
}

# value フィールドに指定したデータの型(list, map など)はそのまま出力に反映される。
# local.access_ristricted_paths_ips が list 型で定義されていれば出力も list 型。
# output で以下のようなリストが出力される。

# access_ristricted_paths_ips = [
#   {
#     name           = "access-ristrict-sample2"
#     priority       = 1
#     allow_path     = "/ristrict/sample2/"
#     allow_ip_list  = [
#       "0.0.0.0/1",
#       "128.0.0.0/1",
#     ]
#   },
#   {
#     name           = "access-ristrict-sample3"
#     priority       = 2
#     allow_path     = "/ristrict/sample3/"
#     allow_ip_list  = [
#       "0.0.0.0/1",
#       "128.0.0.0/1",
#     ]
#   },
# ]

modules/waf

variables.tf

variables.tf は Terraform モジュールの入力値(変数)を定義するファイルです。モジュールを使用する側(呼び出し元)から値を受け取り、モジュール内で使用することができます。

modules/waf/variables.tf(クリックで展開)

variables.tf

variable "access_ristricted_paths_ips" {
  type = list(object({
    name          = string
    priority      = number
    allow_path    = string
    allow_ip_list = list(string)
  }))
  default     = []
  description = "アクセス拒否パスとIPリスト"
}

variable "alb_arn" {
  type        = string
  description = "ALB の ARN 設定情報"
}
  • access_ristricted_paths_ips 変数
    • typelist(object({...})) と定義しました。これで、オブジェクトのリストを受け取ります。各オブジェクトには namepriorityallow_pathallow_ip_list というフィールドがあります。
    • default は空のリスト [] を設定しました。呼び出し元がこの変数に値を渡さない場合、デフォルト値として空のリストが使用されます。
  • alb_arn 変数
    • type は文字列値を受け取るよう string で定義しました。

locals.tf

locals.tf はモジュール内で使用するローカル変数を定義するためのファイルです。ローカル変数はモジュール内で繰り返し使用される値や、複雑な式の結果を保持するために使用されます。ローカル変数は locals ブロックを使用して定義され、名前と値を指定します。

modules/waf/locals.tf(クリックで展開)

locals.tf

locals{
  access_ristricted_paths_ips = {
    for paths_ips in var.access_ristricted_paths_ips : 
    paths_ips.name => paths_ips
  }
}

# for で paths_ips を繰り返し処理する。
# 1 回目の繰り返し処理で var.access_ristricted_paths_ips から
# shared/common_values/waf_rules.tf で定義し
# shared/common_values/outputs.tf で出力した
# 以下のような list(paths_ips)を得る。

# {
#   name           = "access-ristrict-sample2"
#   priority       = 1
#   allow_path     = "/ristrict/sample2/"
#   allow_ip_list  = [
#     "0.0.0.0/1",
#     "128.0.0.0/1",
#   ]
# },

# paths_ips.name => paths_ips の指示では、
# paths_ips の name("access-ristrict-sample2")を
# 新しい map のトップレベルキーとして設定し、
# この list(paths_ips)全体を、新しい map の値として使う。
# 新しい map 型ができあがると以下のようになる。

# {
#   "access-ristrict-sample2" = {
#     name           = "access-ristrict-sample2"
#     priority       = 1
#     allow_path     = "/ristrict/sample2/"
#     allow_ip_list  = [
#       "0.0.0.0/1",
#       "128.0.0.0/1",
#     ]
#   }
# },

以下の処理について補足します。

 {
   for <ITEM> in <LIST>:
   <KEY> => <VALUE>
 }
  • <LIST>:反復処理の対象となる list または map
  • <ITEM>:list または map の各要素を表す変数
  • <KEY>:新しい map のキーを指定する式。
  • <VALUE>:新しい map の値を指定する式。

map 型について補足します。

map language=型

"キー" = { 値 }

map 型はキーと値のペアを持つデータ構造です。

  • map 型は一意のキーと対応する値のペアを持ちます。
  • キーは文字列で指定され、値には任意の型(文字列、数値、ブール値、リスト、他の map など)を指定できます。
  • 宣言方法
    • map は {} を使用して宣言します。
    • キーは「トップレベルキー」「全体のキー」「変数名」と言ったりします。{} 内部にも key = value のような形式が続きますが、{} の中のキーは「内部のキー」「ネストされたキー」「map 型の中の子 キー」と呼んだりするようです。
    • map language=型

      "key" = { # トップレベルキー、全体のキー
        key1 = value1 # 内部のキー、ネストされたキー
        key2 = value2
        key3 = ["xxx", "yyy",]
      }

main.tf

ではいよいよ AWS WAF のリソースを作成する main.tf です。今回は以下のリソースを作成します。

  1. IP Sets(動的生成)
  2. カスタムルールグループ(動的生成)
  3. Web ACL
  4. WAF のログを出力する CloudWatch Logs
  5. WAF のログフィルタ設定
  6. ALB への WAF の割り当て

長いです。トグルを展開して確認ください。

modules/waf/main.tf(クリックで展開)

main.tf

############################################
# IPsets
############################################
## (動的生成). 指定パスの IP 許可ルールグループ(許可IP以外をブロック)
resource "aws_wafv2_ip_set" "access_ristrict" {
  for_each = local.access_ristricted_paths_ips

  name               = "${each.value.name}-ristrict-ipsets"
  description        = "${each.value.name}-ristrict-ipsets"
  scope              = "REGIONAL"
  addresses          = each.value.allow_ip_list
  ip_address_version = "IPV4"

  tags = {
    Name = "${each.value.name}-ristrict-ipsets"
  }
}

############################################
# カスタムルールグループ
############################################
## (動的生成). 指定パスの IP 許可ルールグループ(許可IP以外をブロック)
resource "aws_wafv2_rule_group" "access_ristrict" {
  for_each = local.access_ristricted_paths_ips

  name        = "${each.value.name}-ristrict-waf-rulegp"
  description = "${each.value.name}-ristrict-waf-rulegp"
  scope       = "REGIONAL"
  capacity    = 3 # 1 つのルールグループに 1 つのルールのみ設定する

  visibility_config {
    cloudwatch_metrics_enabled = true # CloudWatch Metrics を無効にすると CloudWatch で確認できなくなるため、true で固定
    metric_name                = "${each.value.name}-ristrict-rulegp"
    sampled_requests_enabled   = true # Web ACL ルールに一致するリクエストを表示する。true で固定
  }

  rule {
    name     = "${each.value.name}-ristrict-rule"
    priority = 1

    action {
      block {}
    }

    statement {
      and_statement {
        statement {
          byte_match_statement {
            field_to_match {
              uri_path {}
            }
            positional_constraint = "STARTS_WITH"
            search_string         = each.value.allow_path
            text_transformation {
              priority = 0
              type     = "NONE"
            }
          }
        }

        statement {
          not_statement {
            statement {
              ip_set_reference_statement {
                arn = aws_wafv2_ip_set.access_ristrict[each.key].arn # IPsets の ARN を指定
              }
            }
          }
        }
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "${each.value.name}-ristrict-rule"
      sampled_requests_enabled   = true
    }
  }
}

############################################
# Web ACL
############################################
resource "aws_wafv2_web_acl" "web_acl" {

  name        = "alb-webacl"
  description = "alb-wabacl"
  scope       = "REGIONAL"

  tags = {
    Name = "alb-webacl"
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "alb-webacl"
    sampled_requests_enabled   = true
  }

  default_action {
    allow {}
  }

  ## (動的生成). 指定パスの IP 許可ルールグループ(許可IP以外をブロック)
  dynamic "rule" {
    for_each = local.access_ristricted_paths_ips

    content {
      name     = "${title(rule.value.name)}PathIpRestriction"
      priority = rule.value.priority + 1000 

      override_action {
        none {}
      }

      statement {
        rule_group_reference_statement {
          arn = aws_wafv2_rule_group.access_ristrict[rule.key].arn
        }
      }

      visibility_config {
        cloudwatch_metrics_enabled = true 
        metric_name                = "${title(rule.value.name)}PathIpRestriction"
        sampled_requests_enabled   = true 
      }
    }
  }

}

############################################
# WAF のログを出力する CloudWatch Logs
############################################
resource "aws_cloudwatch_log_group" "cwlogs" {

  name              = "aws-waf-logs-${aws_wafv2_web_acl.web_acl.name}"
  retention_in_days = 30

  tags = {
    Name     = "aws-waf-logs-${aws_wafv2_web_acl.web_acl.name}"
  }
}

############################################
# WAF のログフィルタ設定
############################################
resource "aws_wafv2_web_acl_logging_configuration" "waflog" {

  resource_arn = aws_wafv2_web_acl.web_acl.arn
  log_destination_configs = [
    # CloudWatch Logs
    aws_cloudwatch_log_group.cwlogs.arn,
  ]

  logging_filter {
    # デフォルトではログを記録しない
    default_behavior = "DROP"

    filter {
      behavior = "KEEP"
      # MEETS_ALL: AND条件、 MEETS_ANY: OR条件
      requirement = "MEETS_ANY"

      ## action_condition ブロックに CAPTCHA の記載はないが apply はできる模様 https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration#action-condition
      condition {
        action_condition {
          # CAPTCHAアクションのログを記録
          action = "CAPTCHA"
        }
      }

      condition {
        action_condition {
          # COUNTアクションのログを記録
          action = "COUNT"
        }
      }

      condition {
        action_condition {
          # BLOCKアクションのログを記録
          action = "BLOCK"
        }
      }
    }
  }

  depends_on = [
    aws_cloudwatch_log_group.cwlogs,
  ]
}

############################################
# ALB への WAF の割り当て
############################################
resource "aws_wafv2_web_acl_association" "waf_association" {

  resource_arn = var.alb_arn #ALBのARN
  web_acl_arn  = aws_wafv2_web_acl.web_acl.arn
}

ここからそれぞれのリソースについて解説します。

1. IP Sets(動的生成)

############################################
# IP Sets(動的生成)
############################################
## (動的生成). 指定パスの IP 許可ルールグループ(許可IP以外をブロック)
resource "aws_wafv2_ip_set" "access_ristrict" {
  for_each = local.access_ristricted_paths_ips

  name               = "${each.value.name}-ristrict-ipsets"
  description        = "${each.value.name}-ristrict-ipsets"
  scope              = "REGIONAL"
  addresses          = each.value.allow_ip_list
  ip_address_version = "IPV4"

  tags = {
    Name = "${each.value.name}-ristrict-ipsets"
  }
}
  • 6 行目の for_each で繰り返し処理を書いています。local.access_ristricted_paths_ips から以下のような map が複数回読み込まれます。
    "access-ristrict-sample2" = {
      name           = "access-ristrict-sample2"
      priority       = 1
      allow_path     = "/ristrict/sample2/"
      allow_ip_list  = [
        "0.0.0.0/1",
        "128.0.0.0/1",
      ]
    }
    "access-ristrict-sample3" = {
      name           = "access-ristrict-sample3"
      priority       = 2
      allow_path     = "/ristrict/sample3/"
      allow_ip_list  = [
        "0.0.0.0/1",
        "128.0.0.0/1",
      ]
    }


  • each.value.name では local.access_ristricted_paths_ips で繰り返し読み込まれた map の中の name オブジェクトの値(ここでは access-ristrict-sample2access-ristrict-sample3)が繰り返し入ってきます。
    • name = "${each.value.name}-ristrict-ipsets" では、1 回目の繰り返しで "access-ristrict-sample2-ristrict-ipsets" という値になります。
  • 同様に each.value.allow_ip_list には ["0.0.0.0/1", "128.0.0.0/1",] という IP のリストが入ります。
  • 繰り返し生成された IP Sets の ARN や ID は以下のような map で保持されることになります。
    aws_wafv2_ip_set.access_ristrict = {
      "access-ristrict-sample2" = {
         arn = "xxxxx"
         id  = "xxxxx"
      },
      "access-ristrict-sample3" = {
         arn = "yyyyy"
         id  = "xxxxx"
      },
    }

2. カスタムルールグループ(動的生成)

作成するカスタムルールグループのデフォルトアクションは block です。
カスタムルールグループも IP Sets 同様に動的に生成するので for_each = local.access_ristricted_paths_ips で繰り返し値を読み込んでいます。 ルールのステートメント部分を抜き出して補足します。

    statement {
      and_statement {
        statement {
          byte_match_statement {
            field_to_match {
              uri_path {}
            }
            positional_constraint = "STARTS_WITH"
            search_string         = each.value.allow_path
            text_transformation {
              priority = 0
              type     = "NONE"
            }
          }
        }

        statement {
          not_statement {
            statement {
              ip_set_reference_statement {
                arn = aws_wafv2_ip_set.access_ristrict[each.key].arn # IPsets の ARN を指定
              }
            }
          }
        }
      }
    }
  • 3-15 行目の statement と 17-25 行目の statement を and_statement(AND 条件)でつなげています。
  • 3-15 行目の statement
    • each.value.allow_path で始まる URI から始まる path へのアクセスである、という条件です。
    • each.value.allow_path には local.access_ristricted_paths_ips で読み込まれた map の中の allow_path オブジェクトの値が入ります。
      • 1 回目の繰り返しでは /ristrict/sample2/ が入ります。
  • 17-25 行目の statement
    • ip_set_reference_statement {} で指定した IP「ではない」IP アドレスからのアクセスである、という条件です。
    • arn = aws_wafv2_ip_set.access_ristrict[each.key].arn で、動的に生成した IP Sets の ARN を順番に読み込みます。
      • [each.key] には local.access_ristricted_paths_ips で読み込まれた map の中 key(トップレベルキー)が入ります。
      • 1 回目の繰り返しでは arn = aws_wafv2_ip_set.access_ristrict[access-ristrict-sample2].arn となります。つまり、1 回目の繰り返しで読み込まれた IP Sets の ARN がここに入ってくるというわけです。
  • 繰り返し生成されたカスタムルールグループの ARN や ID は以下のような map で保持されることになります。
    aws_wafv2_rule_group.access_ristrict = {
      "access-ristrict-sample2" = {
         arn = "xxxxx"
         id  = "xxxxx"
      },
      "access-ristrict-sample3" = {
         arn = "yyyyy"
         id  = "xxxxx"
      },
    }

Web ACL

今回は AWS WAF を ALB に紐づけるので、スコープは "REGIONAL" としています。Web ACL のデフォルトアクションは allow です。
ルールブロックを動的生成する部分を抜き出して解説します。

  ## (動的生成). 指定パスの IP 許可ルールグループ(許可IP以外をブロック)
  dynamic "rule" {
    for_each = local.access_ristricted_paths_ips

    content {
      name     = "${title(rule.value.name)}PathIpRestriction"
      priority = rule.value.priority + 1000 

      override_action {
        none {}
      }

      statement {
        rule_group_reference_statement {
          arn = aws_wafv2_rule_group.access_ristrict[rule.key].arn
        }
      }

      visibility_config {
        cloudwatch_metrics_enabled = true 
        metric_name                = "${title(rule.value.name)}PathIpRestriction"
        sampled_requests_enabled   = true 
      }
    }
  }
  • dynamic は Terraform のブロック({} で囲まれたかたまり)を動的に生成する際に使用します。
    • for_each で繰り返す回数や条件を指定します。
    • content {} で繰り返し構造の中身の要素を定義します。
      dynamic "xxx" {
        for_each = yyy # 繰り返す回数や条件を指定
        content { 
          # 繰り返す内容
        }
      }
  • for_each = local.access_ristricted_paths_ips で map を繰り返し読み込みます。
  • rule.value.name
    • dynamic ブロックによって rule ブロックが生成されます。
    • rule.valuefor_each で指定された map のオブジェクトの現在の要素の値を表します。
    • 1 回目の繰り返しで rule.value.name は、現在の要素の name 属性の値(access-ristrict-sample2)を参照します。
  • name = "${title(rule.value.name)}PathIpRestriction" では title 関数を使用して、文字列をキャメルケースに変換しています。
  • arn = aws_wafv2_rule_group.access_ristrict[rule.key].arn で、動的に生成したカスタムルールグループの ARN を順番に読み込みます。
    • [rule.key] には dynamic ブロックで生成された rule ブロック内で for_each で読み込まれた map の key(トップレベルキー)が入ります。
    • 1 回目の繰り返しでは arn = aws_wafv2_rule_group.access_ristrict[access-ristrict-sample2].arn となります。

WAF のログを出力する CloudWatch Logs

############################################
# WAF のログを出力する CloudWatch Logs
############################################
resource "aws_cloudwatch_log_group" "cwlogs" {

  name              = "aws-waf-logs-${aws_wafv2_web_acl.web_acl.name}"
  retention_in_days = 30

  tags = {
    Name     = "aws-waf-logs-${aws_wafv2_web_acl.web_acl.name}"
  }
}

名前と CloudWatch Logs の保持期間 retention_in_days を 30 日間に設定しています。WAF のログを保存する CloudWatch Logs の名前は aws-waf-logs- で始まる必要がありますので注意してください。

ロググループの命名 - AWS WAF、 AWS Firewall Manager、および AWS Shield Advanced

WAF のログフィルタ設定

WAF のログフィルタを設定しています。フィルタ部分のみ抜き出します。

  logging_filter {
    # デフォルトではログを記録しない
    default_behavior = "DROP"

    filter {
      behavior = "KEEP"
      # MEETS_ALL: AND条件、 MEETS_ANY: OR条件
      requirement = "MEETS_ANY"

      ## action_condition ブロックに CAPTCHA の記載はないが apply はできる模様 https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration#action-condition
      condition {
        action_condition {
          # CAPTCHAアクションのログを記録
          action = "CAPTCHA"
        }
      }

      condition {
        action_condition {
          # COUNTアクションのログを記録
          action = "COUNT"
        }
      }

      condition {
        action_condition {
          # BLOCKアクションのログを記録
          action = "BLOCK"
        }
      }
    }
  }

  depends_on = [
    aws_cloudwatch_log_group.cwlogs,
  ]

デフォルトではログを取得せず、CAPTCHACOUNTCOUNT アクションのログのみ取得するようにしています。

depends_on は Terraform のメタ引数の一つで、リソース間の依存関係を明示的に指定するために使用します。今回は aws_cloudwatch_log_group.cwlogs リソースが作成される前に aws_wafv2_web_acl_logging_configuration.waflog リソースが作成されることを防いでいます。

The depends_on Meta-Argument - Configuration Language | Terraform | HashiCorp Developer

ALB への WAF の割り当て

############################################
# ALB への WAF の割り当て
############################################
resource "aws_wafv2_web_acl_association" "waf_association" {

  resource_arn = var.alb_arn #ALBのARN
  web_acl_arn  = aws_wafv2_web_acl.web_acl.arn
}

Web ACL の ARN と ALB の ARN を指定して関連付けをおこなっています。ALB の ARN はこの後ルートモジュール側で渡します。

outputs.tf

modules/waf/outputs.tf(クリックで展開)

outputs.tf

##################################################
## CloudWatch Logs
##################################################
# CloudWatch Logs Group ARN
output "cloudwatch_log_group_arn" {
  value = aws_cloudwatch_log_group.cwlogs.arn
  description = "The ARN of the CloudWatch Log Group for AWS WAF logs."
}

##################################################
## Web ACL 
##################################################
# Web ACL ARN 
output "web_acl_arn" {
  value = aws_wafv2_web_acl.web_acl.arn
  description = "The ARN of the Web ACL."
}

# Web ACL ID
output "web_acl_id" {
  value = aws_wafv2_web_acl.web_acl.id
  description = "The ID of the Web ACL."
}

# Web ACL name
output "web_acl_name" {
description = "The name of the WAFv2 WebACL."
value       = aws_wafv2_web_acl.web_acl.name
}

# web acl capacity
output "web_acl_capacity" {
description = "The web ACL capacity units (WCUs) currently being used by the WAFv2 WebACL."
value       = aws_wafv2_web_acl.web_acl.capacity
}

# Web ACL Logging Configuration
# 通常 Web ACL自体のARNと同じだが、ログ設定のコンテキストでの使用に便利
output "web_acl_logging_configuration_resource_arn" {
  value = aws_wafv2_web_acl_logging_configuration.waflog.resource_arn
  description = "The resource ARN for the Web ACL Logging Configuration."
}

##################################################
## IP Sets
##################################################
output "ip_set" {
value       = aws_wafv2_ip_set.access_ristrict
description = "WAFv2 IP Sets created for access restriction."
}

##################################################
## Rule Group
##################################################
output "rule_group" {
value       = aws_wafv2_rule_group.access_ristrict
description = "WAFv2 Rule Groups created for access restriction."
}

IP Sets とルールグループは動的に生成されます。
for_each で繰り返し作成されるリソースは map 型で保持されるので、output では map に保持された複数リソースが返ってきます。
ちなみに count で複数作成されるリソースは list 型で保持されます。

追記:以前は私が上記仕様を理解していなかったため、for 文で繰り返し output していました。問題なく apply はできたのですが、上記の書き方の方がシンプルであるため修正しました。

参考:修正前のコード
modules/waf/outputs.tf(クリックで展開)

outputs.tf

##################################################
## CloudWatch Logs
##################################################
# CloudWatch Logs Group ARN
output "cloudwatch_log_group_arn" {
  value = aws_cloudwatch_log_group.cwlogs.arn
  description = "The ARN of the CloudWatch Log Group for AWS WAF logs."
}

##################################################
## Web ACL 
##################################################
# Web ACL ARN 
output "web_acl_arn" {
  value = aws_wafv2_web_acl.web_acl.arn
  description = "The ARN of the Web ACL."
}

# Web ACL ID
output "web_acl_id" {
  value = aws_wafv2_web_acl.web_acl.id
  description = "The ID of the Web ACL."
}

# Web ACL name
output "web_acl_name" {
description = "The name of the WAFv2 WebACL."
value       = aws_wafv2_web_acl.web_acl.name
}

# web acl capacity
output "web_acl_capacity" {
description = "The web ACL capacity units (WCUs) currently being used by the WAFv2 WebACL."
value       = aws_wafv2_web_acl.web_acl.capacity
}

# Web ACL Logging Configuration
# 通常 Web ACL自体のARNと同じだが、ログ設定のコンテキストでの使用に便利
output "web_acl_logging_configuration_resource_arn" {
  value = aws_wafv2_web_acl_logging_configuration.waflog.resource_arn
  description = "The resource ARN for the Web ACL Logging Configuration."
}

##################################################
## IP Sets
##################################################
# IP Sets の作成は動的であるため、それぞれの IP Set の ARN を map 型で出力
output "ip_set_arns" {
value       = { for k, v in aws_wafv2_ip_set.access_ristrict : k => v.arn }
description = "The ARNs of the WAFv2 IP Sets created for access restriction."
}

# IP Sets の作成は動的であるため、それぞれの IP Set の ID を map 型で出力
output "ip_set_ids" {
value       = { for k, v in aws_wafv2_ip_set.access_ristrict : k => v.id }
description = "The IDs of the WAFv2 IP Sets created for access restriction."
}

##################################################
## Rule Group
##################################################
# Rule Group の作成は動的であるため、それぞれの Rule Group の ARN を map 型で出力
output "rule_group_arns" {
description = "The ARNs of the WAFv2 Rule Groups created for access restriction."
value       = { for k, v in aws_wafv2_rule_group.access_ristrict : k => v.arn }
}

# Rule Group の作成は動的であるため、それぞれの Rule Group の ID を map 型で出力
output "rule_group_ids" {
description = "The IDs of the WAFv2 Rule Groups created for access restriction."
value       = { for k, v in aws_wafv2_rule_group.access_ristrict : k => v.id }
}

# k は "key" の頭文字、map の key(トップレベルキー)を表すために使用
# v は "value" の頭文字、map 内の値を表すために使用

以下の処理について補足します。

for k, v in aws_wafv2_ip_set.access_ristrict : k => v.arn
  • for k, v in aws_wafv2_ip_set.access_ristrict:
    • この部分で aws_wafv2_ip_set.access_ristrict で読み込まれる map の各要素に対してループ処理を行います。
  • k は map のキー(トップレベルキー)、v は map 内の値になります。
    • 繰り返し生成された IP Sets の ARN や ID は以下のような map で保持されています。キーは "access-ristrict-sample2""access-ristrict-sample2"、map 内の値が "arn = "xxxxx"id = "xxxxx" です。
      aws_wafv2_ip_set.access_ristrict = {
        "access-ristrict-sample2" = {
           arn = "xxxxx"
           id  = "xxxxx"
        },
        "access-ristrict-sample2" = {
           arn = "yyyyy"
           id  = "xxxxx"
        },
      }
  • : k => v.arn:
    • この部分で新しい map の各エントリを定義しています。map のキー(k)はそのまま保持され、値は v.arn(IP Sets の ARN)に置き換えられます。
    • 結果、以下のようなキーと IP Sets の ARN の新しい map が生成されます。
      ip_set_arns = {
        "access-ristrict-sample2" = "xxxxx"
        "access-ristrict-sample3" = "xxxxx"
      }

カスタムルールグループの output も同様です。

envs/dev-waf

backend.tf

backend.tf ファイルには Terraform の設定に関する情報を含む terraform ブロックを記載しています。terraform ブロックは Terraform を実行する際一番最初に読み込まれます。

envs/dev-waf/backend.tf(クリックで展開)

backend.tf

##################################################
# Terraform settings
##################################################
terraform { # https://developer.hashicorp.com/terraform/language/settings
  # Terraform バージョンの指定
  required_version = "~> 1.5"
  # AWS プロバイダーのバージョン指定 https://registry.terraform.io/providers/hashicorp/aws/latest
  required_providers {
      aws = {
          source  = "hashicorp/aws"
          version = "~> 5.1"
      }
  }
  # tfstate ファイルを S3 に配置する(配置先の S3 は事前に作成しておく)
  backend s3 {
      bucket = "<s3_bucket_name>"
      region = "ap-northeast-1"
      key    = "<tfstate_file_name>.tfstate"
  }
}

ここでは Terraform のバージョン、プロバイダーのバージョン、状態ファイル(tfstate ファイル)の保存場所を指定しています。

  • required_version
    • 使用する Terraform のバージョンを指定します。
    • "~> 1.5" は Terraform 1.5 系、1.6 系、1.7 系……のバージョンが利用できます。バージョン制約から外れる Terraform バージョンで実行しようとするとエラーとなり実行できません。
  • required_providers ブロック
    • 使用するプロバイダーとバージョンを指定します。ここでは AWS を指定しています。
    • source はプロバイダーの名前を指定します("hashicorp/aws")。
    • version は使用する AWS プロバイダーのバージョンを指定します("~> 5.1")。今回は 5.1 系、5.2 系、5.3 系……のバージョンを使用することを意味しています。
  • backend ブロック
    • Terraform の状態ファイル(tfstate ファイル)をリモートで保存するための設定を定義します。今回はあらかじめ作成しておいた S3 バケットを指定しています。
    • region は S3 バケットが存在するリージョンを指定します。
    • key は tfstate ファイルのパスとファイル名を指定します。

バージョン指定については以下ドキュメントを参照ください。

tfstate ファイルを S3 バケットで管理することで、複数メンバーが同じ Terraform コード管理でき、tfstate ファイルの損失や競合を防ぎます。

provider.tf

AWS で使用するリージョンを指定しています。

envs/dev-waf/provider.tf(クリックで展開)

provider.tf

##################################################
# Provider settings
##################################################
# AWS プロバイダーの定義
provider aws {
    region = "ap-northeast-1"
}

main.tf

モジュールを呼び出して AWS WAF を実際にデプロイするファイルです。

envs/dev-waf/main.tf(クリックで展開)

main.tf

############################################
# Common Values
############################################
module "common_values" {
  source = "../../shared/common_values"
}

############################################
# WAF
############################################
module "waf" {
source = "../../modules/waf"

access_ristricted_paths_ips = module.common_values.access_ristricted_paths_ips # 動的生成ルールグループのための変数呼び出し

# WAF を紐づける ALB の ARN
alb_arn = "arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/waf-test-alb/xxxxxxxxxxxxxxxx"

}
  • module "common_values" ブロックの呼び出し
    • source 引数を使用し ../../shared/common_values ディレクトリにある common_values モジュールを呼び出しています。
  • module "waf" ブロックの呼び出し
    • source 引数を使用し ../../modules/waf ディレクトリにある waf モジュールを呼び出しています。
    • access_ristricted_paths_ips 引数
      • common_values モジュールから動的に生成されるWAFのルールグループで使用されるパスとIPアドレスのリストである access_ristricted_paths_ips の値を取得します。
    • alb_arn 引数
      • WAF を関連付ける ALB の ARN を指定しています。

使い方

0. terraform init、terraform plan、terraform apply をして Web ACL を作成する

まずは一度 terraform init、terraform plan、terraform apply をして Web ACL を作成しておきます。

1. Terraform コードの中で、名前、優先度、パス、IP の組み合わせを追記します。

shared/common_values/waf_rules.tf で access_ristricted_paths_ips = [] 内に、以下のような名前、優先度、パス、IP の組み合わせのリストを追記します。

    {
      name = "access-ristrict-sample1"
      priority = 10
      allow_path     = "/ristrict/sample/"
      allow_ip_list = [
        "0.0.0.0/1",
        "128.0.0.0/1",
      ]
    },

2. Terraform で更新

~/environment/test-terraform/envs/dev-waf 配下で更新をかけます。

terrafprm plan、apply(クリックで展開)
user.emi:~/environment/test-terraform/envs/dev-waf $ terraform plan
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=261098a7-82f6-49bd-bb9d-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=1461c7eb-52e6-42cc-97bb-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=0dd59d52-a04a-4740-b93a-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=0c82ad12-a05a-4af4-814d-xxxxxxxxxx]
module.waf.aws_wafv2_web_acl.web_acl: Refreshing state... [id=xxxxxxxxxx]
module.waf.aws_wafv2_web_acl_association.waf_association: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx,arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/waf-test-alb/xxxxxxxxxx]
module.waf.aws_cloudwatch_log_group.cwlogs: Refreshing state... [id=aws-waf-logs-alb-webacl]
module.waf.aws_wafv2_web_acl_logging_configuration.waflog: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx]

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

Terraform will perform the following actions:

  # module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"] will be created
  + resource "aws_wafv2_ip_set" "access_ristrict" {
      + addresses          = [
          + "0.0.0.0/1",
          + "128.0.0.0/1",
        ]
      + arn                = (known after apply)
      + description        = "access-ristrict-sample1-ristrict-ipsets"
      + id                 = (known after apply)
      + ip_address_version = "IPV4"
      + lock_token         = (known after apply)
      + name               = "access-ristrict-sample1-ristrict-ipsets"
      + scope              = "REGIONAL"
      + tags               = {
          + "Name" = "access-ristrict-sample1-ristrict-ipsets"
        }
      + tags_all           = {
          + "Name" = "access-ristrict-sample1-ristrict-ipsets"
        }
    }

  # module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"] will be created
  + resource "aws_wafv2_rule_group" "access_ristrict" {
      + arn         = (known after apply)
      + capacity    = 3
      + description = "access-ristrict-sample1-ristrict-waf-rulegp"
      + id          = (known after apply)
      + lock_token  = (known after apply)
      + name        = "access-ristrict-sample1-ristrict-waf-rulegp"
      + name_prefix = (known after apply)
      + scope       = "REGIONAL"
      + tags_all    = (known after apply)

      + rule {
          + name     = "access-ristrict-sample1-ristrict-rule"
          + priority = 1

          + action {
              + block {
                }
            }

          + statement {
              + and_statement {
                  + statement {
                      + byte_match_statement {
                          + positional_constraint = "STARTS_WITH"
                          + search_string         = "/ristrict/sample/"

                          + 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                = "access-ristrict-sample1-ristrict-rule"
              + sampled_requests_enabled   = true
            }
        }

      + visibility_config {
          + cloudwatch_metrics_enabled = true
          + metric_name                = "access-ristrict-sample1-ristrict-rulegp"
          + sampled_requests_enabled   = true
        }
    }

  # module.waf.aws_wafv2_web_acl.web_acl will be updated in-place
  ~ resource "aws_wafv2_web_acl" "web_acl" {
        id            = "xxxxxxxxxx"
        name          = "alb-webacl"
        tags          = {
            "Name" = "alb-webacl"
        }
        # (7 unchanged attributes hidden)

      + rule {
          + name     = "Access-Ristrict-Sample1PathIpRestriction"
          + priority = 1010

          + override_action {
              + none {}
            }

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

          + visibility_config {
              + cloudwatch_metrics_enabled = true
              + metric_name                = "Access-Ristrict-Sample1PathIpRestriction"
              + sampled_requests_enabled   = true
            }
        }

        # (4 unchanged blocks hidden)
    }

Plan: 2 to add, 1 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
user.emi:~/environment/test-terraform/envs/dev-waf $ 
user.emi:~/environment/test-terraform/envs/dev-waf $ terraform apply
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=1461c7eb-52e6-42cc-97bb-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=261098a7-82f6-49bd-bb9d-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=0c82ad12-a05a-4af4-814d-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=0dd59d52-a04a-4740-b93a-xxxxxxxxxx]
module.waf.aws_wafv2_web_acl.web_acl: Refreshing state... [id=xxxxxxxxxx]
module.waf.aws_wafv2_web_acl_association.waf_association: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx,arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/waf-test-alb/xxxxxxxxxx]
module.waf.aws_cloudwatch_log_group.cwlogs: Refreshing state... [id=aws-waf-logs-alb-webacl]
module.waf.aws_wafv2_web_acl_logging_configuration.waflog: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx]

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

Terraform will perform the following actions:

  # module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"] will be created
  + resource "aws_wafv2_ip_set" "access_ristrict" {
      + addresses          = [
          + "0.0.0.0/1",
          + "128.0.0.0/1",
        ]
      + arn                = (known after apply)
      + description        = "access-ristrict-sample1-ristrict-ipsets"
      + id                 = (known after apply)
      + ip_address_version = "IPV4"
      + lock_token         = (known after apply)
      + name               = "access-ristrict-sample1-ristrict-ipsets"
      + scope              = "REGIONAL"
      + tags               = {
          + "Name" = "access-ristrict-sample1-ristrict-ipsets"
        }
      + tags_all           = {
          + "Name" = "access-ristrict-sample1-ristrict-ipsets"
        }
    }

  # module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"] will be created
  + resource "aws_wafv2_rule_group" "access_ristrict" {
      + arn         = (known after apply)
      + capacity    = 3
      + description = "access-ristrict-sample1-ristrict-waf-rulegp"
      + id          = (known after apply)
      + lock_token  = (known after apply)
      + name        = "access-ristrict-sample1-ristrict-waf-rulegp"
      + name_prefix = (known after apply)
      + scope       = "REGIONAL"
      + tags_all    = (known after apply)

      + rule {
          + name     = "access-ristrict-sample1-ristrict-rule"
          + priority = 1

          + action {
              + block {
                }
            }

          + statement {
              + and_statement {
                  + statement {
                      + byte_match_statement {
                          + positional_constraint = "STARTS_WITH"
                          + search_string         = "/ristrict/sample/"

                          + 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                = "access-ristrict-sample1-ristrict-rule"
              + sampled_requests_enabled   = true
            }
        }

      + visibility_config {
          + cloudwatch_metrics_enabled = true
          + metric_name                = "access-ristrict-sample1-ristrict-rulegp"
          + sampled_requests_enabled   = true
        }
    }

  # module.waf.aws_wafv2_web_acl.web_acl will be updated in-place
  ~ resource "aws_wafv2_web_acl" "web_acl" {
        id            = "xxxxxxxxxx"
        name          = "alb-webacl"
        tags          = {
            "Name" = "alb-webacl"
        }
        # (7 unchanged attributes hidden)

      + rule {
          + name     = "Access-Ristrict-Sample1PathIpRestriction"
          + priority = 1010

          + override_action {
              + none {}
            }

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

          + visibility_config {
              + cloudwatch_metrics_enabled = true
              + metric_name                = "Access-Ristrict-Sample1PathIpRestriction"
              + sampled_requests_enabled   = true
            }
        }

        # (4 unchanged blocks hidden)
    }

Plan: 2 to add, 1 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_wafv2_ip_set.access_ristrict["access-ristrict-sample1"]: Creating...
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"]: Creation complete after 0s [id=b18a8319-eaa3-4643-8853-74f1740d2af2]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Creating...
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Creation complete after 0s [id=befd7e4f-24de-4c68-9695-f3c80b9143ae]
module.waf.aws_wafv2_web_acl.web_acl: Modifying... [id=xxxxxxxxxx]
module.waf.aws_wafv2_web_acl.web_acl: Modifications complete after 2s [id=xxxxxxxxxx]

Apply complete! Resources: 2 added, 1 changed, 0 destroyed.
user.emi:~/environment/test-terraform/envs/dev-waf $

3. 指定した内容で IP Sets とルールグループを動的に生成される

以下のような IP Sets、ルールグループが作成されます。

{
  "Name": "access-ristrict-sample1-ristrict-rule",
  "Priority": 1,
  "Statement": {
    "AndStatement": {
      "Statements": [
        {
          "ByteMatchStatement": {
            "SearchString": "/ristrict/sample/",
            "FieldToMatch": {
              "UriPath": {}
            },
            "TextTransformations": [
              {
                "Priority": 0,
                "Type": "NONE"
              }
            ],
            "PositionalConstraint": "STARTS_WITH"
          }
        },
        {
          "NotStatement": {
            "Statement": {
              "IPSetReferenceStatement": {
                "ARN": "arn:aws:wafv2:ap-northeast-1:123456789012:regional/ipset/access-ristrict-sample1-ristrict-ipsets/xxxxxxxxxx"
              }
            }
          }
        }
      ]
    }
  },
  "Action": {
    "Block": {}
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "access-ristrict-sample1-ristrict-rule"
  }
}

Web ACL には以下の名前でルールグループが紐づきます。

4. priority、path、IP を変更する

prioryty 変更

terraform plan、apply(クリックで展開)
cm-kitani.emi:~/environment/test-terraform/envs/dev-waf $ terraform plan
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=261098a7-82f6-49bd-bb9d-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=b18a8319-eaa3-4643-8853-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=1461c7eb-52e6-42cc-97bb-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=0dd59d52-a04a-4740-b93a-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=0c82ad12-a05a-4af4-814d-xxxxxxxxxx]
module.waf.aws_wafv2_web_acl.web_acl: Refreshing state... [id=xxxxxxxxxx]
module.waf.aws_wafv2_web_acl_association.waf_association: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx,arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/waf-test-alb/xxxxxxxxxx]
module.waf.aws_cloudwatch_log_group.cwlogs: Refreshing state... [id=aws-waf-logs-alb-webacl]
module.waf.aws_wafv2_web_acl_logging_configuration.waflog: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.waf.aws_wafv2_web_acl.web_acl will be updated in-place
  ~ resource "aws_wafv2_web_acl" "web_acl" {
        id            = "xxxxxxxxxx"
        name          = "alb-webacl"
        tags          = {
            "Name" = "alb-webacl"
        }
        # (7 unchanged attributes hidden)

      - rule {
          - name     = "Access-Ristrict-Sample1PathIpRestriction" -> null
          - priority = 1010 -> null

          - override_action {
              - none {}
            }

          - statement {
              - rule_group_reference_statement {
                  - arn = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/rulegroup/access-ristrict-sample1-ristrict-waf-rulegp/befd7e4f-24de-4c68-9695-xxxxxxxxxx" -> null
                }
            }

          - visibility_config {
              - cloudwatch_metrics_enabled = true -> null
              - metric_name                = "Access-Ristrict-Sample1PathIpRestriction" -> null
              - sampled_requests_enabled   = true -> null
            }
        }
      + rule {
          + name     = "Access-Ristrict-Sample1PathIpRestriction"
          + priority = 1005

          + override_action {
              + none {}
            }

          + statement {
              + rule_group_reference_statement {
                  + arn = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/rulegroup/access-ristrict-sample1-ristrict-waf-rulegp/befd7e4f-24de-4c68-9695-xxxxxxxxxx"
                }
            }

          + visibility_config {
              + cloudwatch_metrics_enabled = true
              + metric_name                = "Access-Ristrict-Sample1PathIpRestriction"
              + sampled_requests_enabled   = true
            }
        }

        # (4 unchanged blocks hidden)
    }

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

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
cm-kitani.emi:~/environment/test-terraform/envs/dev-waf $ terraform apply
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=261098a7-82f6-49bd-bb9d-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=b18a8319-eaa3-4643-8853-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=1461c7eb-52e6-42cc-97bb-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=0dd59d52-a04a-4740-b93a-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=0c82ad12-a05a-4af4-814d-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx]
module.waf.aws_wafv2_web_acl.web_acl: Refreshing state... [id=xxxxxxxxxx]
module.waf.aws_cloudwatch_log_group.cwlogs: Refreshing state... [id=aws-waf-logs-alb-webacl]
module.waf.aws_wafv2_web_acl_association.waf_association: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx,arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/waf-test-alb/xxxxxxxxxx]
module.waf.aws_wafv2_web_acl_logging_configuration.waflog: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.waf.aws_wafv2_web_acl.web_acl will be updated in-place
  ~ resource "aws_wafv2_web_acl" "web_acl" {
        id            = "xxxxxxxxxx"
        name          = "alb-webacl"
        tags          = {
            "Name" = "alb-webacl"
        }
        # (7 unchanged attributes hidden)

      - rule {
          - name     = "Access-Ristrict-Sample1PathIpRestriction" -> null
          - priority = 1010 -> null

          - override_action {
              - none {}
            }

          - statement {
              - rule_group_reference_statement {
                  - arn = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/rulegroup/access-ristrict-sample1-ristrict-waf-rulegp/befd7e4f-24de-4c68-9695-xxxxxxxxxx" -> null
                }
            }

          - visibility_config {
              - cloudwatch_metrics_enabled = true -> null
              - metric_name                = "Access-Ristrict-Sample1PathIpRestriction" -> null
              - sampled_requests_enabled   = true -> null
            }
        }
      + rule {
          + name     = "Access-Ristrict-Sample1PathIpRestriction"
          + priority = 1005

          + override_action {
              + none {}
            }

          + statement {
              + rule_group_reference_statement {
                  + arn = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/rulegroup/access-ristrict-sample1-ristrict-waf-rulegp/befd7e4f-24de-4c68-9695-xxxxxxxxxx"
                }
            }

          + visibility_config {
              + cloudwatch_metrics_enabled = true
              + metric_name                = "Access-Ristrict-Sample1PathIpRestriction"
              + sampled_requests_enabled   = true
            }
        }

        # (4 unchanged blocks hidden)
    }

Plan: 0 to add, 1 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_wafv2_web_acl.web_acl: Modifying... [id=xxxxxxxxxx]
module.waf.aws_wafv2_web_acl.web_acl: Modifications complete after 2s [id=xxxxxxxxxx]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
cm-kitani.emi:~/environment/test-terraform/envs/dev-waf $

path 変更

terraform plan、terraform apply(クリックで展開)
user.emi:~/environment/test-terraform/envs/dev-waf $ terraform plan
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=1461c7eb-52e6-42cc-97bb-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=b18a8319-eaa3-4643-8853-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=261098a7-82f6-49bd-bb9d-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=0dd59d52-a04a-4740-b93a-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=0c82ad12-a05a-4af4-814d-xxxxxxxxxx]
module.waf.aws_wafv2_web_acl.web_acl: Refreshing state... [id=xxxxxxxxxx]
module.waf.aws_wafv2_web_acl_association.waf_association: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx,arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/waf-test-alb/xxxxxxxxxx]
module.waf.aws_cloudwatch_log_group.cwlogs: Refreshing state... [id=aws-waf-logs-alb-webacl]
module.waf.aws_wafv2_web_acl_logging_configuration.waflog: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"] will be updated in-place
  ~ resource "aws_wafv2_rule_group" "access_ristrict" {
        id          = "befd7e4f-24de-4c68-9695-xxxxxxxxxx"
        name        = "access-ristrict-sample1-ristrict-waf-rulegp"
        tags        = {}
        # (6 unchanged attributes hidden)

      - rule {
          - name     = "access-ristrict-sample1-ristrict-rule" -> null
          - priority = 1 -> null

          - action {
              - block {
                }
            }

          - statement {
              - and_statement {
                  - statement {
                      - byte_match_statement {
                          - positional_constraint = "STARTS_WITH" -> null
                          - search_string         = "/ristrict/sample/" -> null

                          - field_to_match {
                              - uri_path {}
                            }

                          - text_transformation {
                              - priority = 0 -> null
                              - type     = "NONE" -> null
                            }
                        }
                    }
                  - statement {
                      - not_statement {
                          - statement {
                              - ip_set_reference_statement {
                                  - arn = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/ipset/access-ristrict-sample1-ristrict-ipsets/b18a8319-eaa3-4643-8853-xxxxxxxxxx" -> null
                                }
                            }
                        }
                    }
                }
            }

          - visibility_config {
              - cloudwatch_metrics_enabled = true -> null
              - metric_name                = "access-ristrict-sample1-ristrict-rule" -> null
              - sampled_requests_enabled   = true -> null
            }
        }
      + rule {
          + name     = "access-ristrict-sample1-ristrict-rule"
          + priority = 1

          + action {
              + block {
                }
            }

          + statement {
              + and_statement {
                  + statement {
                      + byte_match_statement {
                          + positional_constraint = "STARTS_WITH"
                          + search_string         = "/ristrict/sample1/"

                          + field_to_match {
                              + uri_path {}
                            }

                          + text_transformation {
                              + priority = 0
                              + type     = "NONE"
                            }
                        }
                    }
                  + statement {
                      + not_statement {
                          + statement {
                              + ip_set_reference_statement {
                                  + arn = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/ipset/access-ristrict-sample1-ristrict-ipsets/b18a8319-eaa3-4643-8853-xxxxxxxxxx"
                                }
                            }
                        }
                    }
                }
            }

          + visibility_config {
              + cloudwatch_metrics_enabled = true
              + metric_name                = "access-ristrict-sample1-ristrict-rule"
              + sampled_requests_enabled   = true
            }
        }

        # (1 unchanged block hidden)
    }

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

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
user.emi:~/environment/test-terraform/envs/dev-waf $ terraform apply
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=261098a7-82f6-49bd-bb9d-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=b18a8319-eaa3-4643-8853-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=1461c7eb-52e6-42cc-97bb-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=0dd59d52-a04a-4740-b93a-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=0c82ad12-a05a-4af4-814d-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx]
module.waf.aws_wafv2_web_acl.web_acl: Refreshing state... [id=xxxxxxxxxx]
module.waf.aws_cloudwatch_log_group.cwlogs: Refreshing state... [id=aws-waf-logs-alb-webacl]
module.waf.aws_wafv2_web_acl_association.waf_association: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx,arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/waf-test-alb/xxxxxxxxxx]
module.waf.aws_wafv2_web_acl_logging_configuration.waflog: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"] will be updated in-place
  ~ resource "aws_wafv2_rule_group" "access_ristrict" {
        id          = "befd7e4f-24de-4c68-9695-xxxxxxxxxx"
        name        = "access-ristrict-sample1-ristrict-waf-rulegp"
        tags        = {}
        # (6 unchanged attributes hidden)

      - rule {
          - name     = "access-ristrict-sample1-ristrict-rule" -> null
          - priority = 1 -> null

          - action {
              - block {
                }
            }

          - statement {
              - and_statement {
                  - statement {
                      - byte_match_statement {
                          - positional_constraint = "STARTS_WITH" -> null
                          - search_string         = "/ristrict/sample/" -> null

                          - field_to_match {
                              - uri_path {}
                            }

                          - text_transformation {
                              - priority = 0 -> null
                              - type     = "NONE" -> null
                            }
                        }
                    }
                  - statement {
                      - not_statement {
                          - statement {
                              - ip_set_reference_statement {
                                  - arn = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/ipset/access-ristrict-sample1-ristrict-ipsets/b18a8319-eaa3-4643-8853-xxxxxxxxxx" -> null
                                }
                            }
                        }
                    }
                }
            }

          - visibility_config {
              - cloudwatch_metrics_enabled = true -> null
              - metric_name                = "access-ristrict-sample1-ristrict-rule" -> null
              - sampled_requests_enabled   = true -> null
            }
        }
      + rule {
          + name     = "access-ristrict-sample1-ristrict-rule"
          + priority = 1

          + action {
              + block {
                }
            }

          + statement {
              + and_statement {
                  + statement {
                      + byte_match_statement {
                          + positional_constraint = "STARTS_WITH"
                          + search_string         = "/ristrict/sample1/"

                          + field_to_match {
                              + uri_path {}
                            }

                          + text_transformation {
                              + priority = 0
                              + type     = "NONE"
                            }
                        }
                    }
                  + statement {
                      + not_statement {
                          + statement {
                              + ip_set_reference_statement {
                                  + arn = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/ipset/access-ristrict-sample1-ristrict-ipsets/b18a8319-eaa3-4643-8853-xxxxxxxxxx"
                                }
                            }
                        }
                    }
                }
            }

          + visibility_config {
              + cloudwatch_metrics_enabled = true
              + metric_name                = "access-ristrict-sample1-ristrict-rule"
              + sampled_requests_enabled   = true
            }
        }

        # (1 unchanged block hidden)
    }

Plan: 0 to add, 1 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_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Modifying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Modifications complete after 1s [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
user.emi:~/environment/test-terraform/envs/dev-waf $

IP 変更

terraform plan、terraform apply(クリックで展開)
user.emi:~/environment/test-terraform/envs/dev-waf $ terraform plan
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=261098a7-82f6-49bd-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=1461c7eb-52e6-42cc-97bb-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=b18a8319-eaa3-4643-8853-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=0dd59d52-a04a-4740-b93a-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=0c82ad12-a05a-4af4-814d-xxxxxxxxxx]
module.waf.aws_wafv2_web_acl.web_acl: Refreshing state... [id=xxxxxxxxxx]
module.waf.aws_wafv2_web_acl_association.waf_association: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx,arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/waf-test-alb/xxxxxxxxxx]
module.waf.aws_cloudwatch_log_group.cwlogs: Refreshing state... [id=aws-waf-logs-alb-webacl]
module.waf.aws_wafv2_web_acl_logging_configuration.waflog: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample2"] will be updated in-place
  ~ resource "aws_wafv2_ip_set" "access_ristrict" {
      ~ addresses          = [
          - "128.0.0.0/1",
            # (1 unchanged element hidden)
        ]
        id                 = "1461c7eb-52e6-42cc-97bb-xxxxxxxxxx"
        name               = "access-ristrict-sample2-ristrict-ipsets"
        tags               = {
            "Name" = "access-ristrict-sample2-ristrict-ipsets"
        }
        # (6 unchanged attributes hidden)
    }

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

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
user.emi:~/environment/test-terraform/envs/dev-waf $ terraform apply
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=b18a8319-eaa3-4643-8853-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=1461c7eb-52e6-42cc-97bb-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=261098a7-82f6-49bd-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=0c82ad12-a05a-4af4-814d-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=0dd59d52-a04a-4740-b93a-xxxxxxxxxx]
module.waf.aws_wafv2_web_acl.web_acl: Refreshing state... [id=xxxxxxxxxx]
module.waf.aws_wafv2_web_acl_association.waf_association: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx,arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/waf-test-alb/xxxxxxxxxx]
module.waf.aws_cloudwatch_log_group.cwlogs: Refreshing state... [id=aws-waf-logs-alb-webacl]
module.waf.aws_wafv2_web_acl_logging_configuration.waflog: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample2"] will be updated in-place
  ~ resource "aws_wafv2_ip_set" "access_ristrict" {
      ~ addresses          = [
          - "128.0.0.0/1",
            # (1 unchanged element hidden)
        ]
        id                 = "1461c7eb-52e6-42cc-97bb-xxxxxxxxxx"
        name               = "access-ristrict-sample2-ristrict-ipsets"
        tags               = {
            "Name" = "access-ristrict-sample2-ristrict-ipsets"
        }
        # (6 unchanged attributes hidden)
    }

Plan: 0 to add, 1 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_wafv2_ip_set.access_ristrict["access-ristrict-sample2"]: Modifying... [id=1461c7eb-52e6-42cc-97bb-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample2"]: Modifications complete after 0s [id=1461c7eb-52e6-42cc-97bb-xxxxxxxxxx]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
user.emi:~/environment/test-terraform/envs/dev-waf $

注意

shared/common_values/waf_rules.tf で access_ristricted_paths_ips = [] から以下のように名前、優先度、パス、IP の組み合わせのリストを削除して Terraform を更新するだけでは、ルールグループが Web ACL に紐づいている関係で削除が失敗します。

terraform apply(クリックで展開)
user.emi:~/environment/test-terraform/envs/dev-waf $ terraform apply
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=b18a8319-eaa3-4643-8853-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=261098a7-82f6-49bd-bb9d-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=1461c7eb-52e6-42cc-97bb-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=0dd59d52-a04a-4740-b93a-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=0c82ad12-a05a-4af4-814d-xxxxxxxxxx]
module.waf.aws_wafv2_web_acl.web_acl: Refreshing state... [id=xxxxxxxxxx]
module.waf.aws_cloudwatch_log_group.cwlogs: Refreshing state... [id=aws-waf-logs-alb-webacl]
module.waf.aws_wafv2_web_acl_association.waf_association: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx,arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/waf-test-alb/xxxxxxxxxx]
module.waf.aws_wafv2_web_acl_logging_configuration.waflog: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place
  - destroy

Terraform will perform the following actions:

  # module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"] will be destroyed
  # (because key ["access-ristrict-sample1"] is not in for_each map)
  - resource "aws_wafv2_ip_set" "access_ristrict" {
      - addresses          = [
          - "0.0.0.0/1",
          - "128.0.0.0/1",
        ] -> null
      - arn                = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/ipset/access-ristrict-sample1-ristrict-ipsets/b18a8319-eaa3-4643-8853-xxxxxxxxxx" -> null
      - description        = "access-ristrict-sample1-ristrict-ipsets" -> null
      - id                 = "b18a8319-eaa3-4643-8853-xxxxxxxxxx" -> null
      - ip_address_version = "IPV4" -> null
      - lock_token         = "aede225d-0e76-471d-9030-06be5230c6b3" -> null
      - name               = "access-ristrict-sample1-ristrict-ipsets" -> null
      - scope              = "REGIONAL" -> null
      - tags               = {
          - "Name" = "access-ristrict-sample1-ristrict-ipsets"
        } -> null
      - tags_all           = {
          - "Name" = "access-ristrict-sample1-ristrict-ipsets"
        } -> null
    }

  # module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"] will be destroyed
  # (because key ["access-ristrict-sample1"] is not in for_each map)
  - resource "aws_wafv2_rule_group" "access_ristrict" {
      - arn         = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/rulegroup/access-ristrict-sample1-ristrict-waf-rulegp/befd7e4f-24de-4c68-9695-xxxxxxxxxx" -> null
      - capacity    = 3 -> null
      - description = "access-ristrict-sample1-ristrict-waf-rulegp" -> null
      - id          = "befd7e4f-24de-4c68-9695-xxxxxxxxxx" -> null
      - lock_token  = "fb7a8d63-7dcf-4d2e-b650-3b8ef61e3eeb" -> null
      - name        = "access-ristrict-sample1-ristrict-waf-rulegp" -> null
      - scope       = "REGIONAL" -> null
      - tags        = {} -> null
      - tags_all    = {} -> null

      - rule {
          - name     = "access-ristrict-sample1-ristrict-rule" -> null
          - priority = 1 -> null

          - action {
              - block {
                }
            }

          - statement {
              - and_statement {
                  - statement {
                      - byte_match_statement {
                          - positional_constraint = "STARTS_WITH" -> null
                          - search_string         = "/ristrict/sample1/" -> null

                          - field_to_match {
                              - uri_path {}
                            }

                          - text_transformation {
                              - priority = 0 -> null
                              - type     = "NONE" -> null
                            }
                        }
                    }
                  - statement {
                      - not_statement {
                          - statement {
                              - ip_set_reference_statement {
                                  - arn = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/ipset/access-ristrict-sample1-ristrict-ipsets/b18a8319-eaa3-4643-8853-xxxxxxxxxx" -> null
                                }
                            }
                        }
                    }
                }
            }

          - visibility_config {
              - cloudwatch_metrics_enabled = true -> null
              - metric_name                = "access-ristrict-sample1-ristrict-rule" -> null
              - sampled_requests_enabled   = true -> null
            }
        }

      - visibility_config {
          - cloudwatch_metrics_enabled = true -> null
          - metric_name                = "access-ristrict-sample1-ristrict-rulegp" -> null
          - sampled_requests_enabled   = true -> null
        }
    }

  # module.waf.aws_wafv2_web_acl.web_acl will be updated in-place
  ~ resource "aws_wafv2_web_acl" "web_acl" {
        id            = "xxxxxxxxxx"
        name          = "alb-webacl"
        tags          = {
            "Name" = "alb-webacl"
        }
        # (7 unchanged attributes hidden)

      - rule {
          - name     = "Access-Ristrict-Sample1PathIpRestriction" -> null
          - priority = 1010 -> null

          - override_action {
              - none {}
            }

          - statement {
              - rule_group_reference_statement {
                  - arn = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/rulegroup/access-ristrict-sample1-ristrict-waf-rulegp/befd7e4f-24de-4c68-9695-xxxxxxxxxx" -> null
                }
            }

          - visibility_config {
              - cloudwatch_metrics_enabled = true -> null
              - metric_name                = "Access-Ristrict-Sample1PathIpRestriction" -> null
              - sampled_requests_enabled   = true -> null
            }
        }

        # (4 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 2 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_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 10s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 20s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 30s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 40s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 50s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 1m0s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 1m10s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 1m20s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 1m30s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 1m40s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 1m50s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 2m0s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 2m10s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 2m20s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 2m30s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 2m40s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 2m50s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 3m0s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 3m10s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 3m20s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 3m30s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 3m40s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 3m50s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 4m0s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 4m10s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 4m20s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 4m30s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 4m40s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 4m50s elapsed]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Still destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx, 5m0s elapsed]
╷
│ Error: deleting WAFv2 RuleGroup (befd7e4f-24de-4c68-9695-xxxxxxxxxx): WAFAssociatedItemException: AWS WAF couldn’t perform the operation because your resource is being used by another resource or it’s associated with another resource.
│ 
│ 
╵
user.emi:~/environment/test-terraform/envs/dev-waf $

エラー抜粋

╷
│ Error: deleting WAFv2 RuleGroup (befd7e4f-24de-4c68-9695-xxxxxxxxxx): WAFAssociatedItemException: AWS WAF couldn’t perform the operation because your resource is being used by another resource or it’s associated with another resource.
│ 
│ 
╵

追加したルールグループを削除したい場合、先に手動で Web ACL からルールグループを削除しておきます。

再度 Terraform を更新すると削除が完了します。

terraform plan、apply(クリックで展開)
user.emi:~/environment/test-terraform/envs/dev-waf $ terraform plan
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=1461c7eb-52e6-42cc-97bb-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=b18a8319-eaa3-4643-8853-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=261098a7-82f6-49bd-bb9d-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=0dd59d52-a04a-4740-b93a-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=0c82ad12-a05a-4af4-814d-xxxxxxxxxx]
module.waf.aws_wafv2_web_acl.web_acl: Refreshing state... [id=xxxxxxxxxx]
module.waf.aws_wafv2_web_acl_association.waf_association: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx,arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/waf-test-alb/xxxxxxxxxx]
module.waf.aws_cloudwatch_log_group.cwlogs: Refreshing state... [id=aws-waf-logs-alb-webacl]
module.waf.aws_wafv2_web_acl_logging_configuration.waflog: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx]

Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the last "terraform apply" which may have affected this plan:

  # module.waf.aws_wafv2_web_acl.web_acl has changed
  ~ resource "aws_wafv2_web_acl" "web_acl" {
      ~ capacity      = 9 -> 6
        id            = "xxxxxxxxxx"
        name          = "alb-webacl"
        tags          = {
            "Name" = "alb-webacl"
        }
        # (6 unchanged attributes hidden)

        # (5 unchanged blocks hidden)
    }


Unless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

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

Terraform will perform the following actions:

  # module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"] will be destroyed
  # (because key ["access-ristrict-sample1"] is not in for_each map)
  - resource "aws_wafv2_ip_set" "access_ristrict" {
      - addresses          = [
          - "0.0.0.0/1",
          - "128.0.0.0/1",
        ] -> null
      - arn                = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/ipset/access-ristrict-sample1-ristrict-ipsets/b18a8319-eaa3-4643-8853-xxxxxxxxxx" -> null
      - description        = "access-ristrict-sample1-ristrict-ipsets" -> null
      - id                 = "b18a8319-eaa3-4643-8853-xxxxxxxxxx" -> null
      - ip_address_version = "IPV4" -> null
      - lock_token         = "aede225d-0e76-471d-9030-06be5230c6b3" -> null
      - name               = "access-ristrict-sample1-ristrict-ipsets" -> null
      - scope              = "REGIONAL" -> null
      - tags               = {
          - "Name" = "access-ristrict-sample1-ristrict-ipsets"
        } -> null
      - tags_all           = {
          - "Name" = "access-ristrict-sample1-ristrict-ipsets"
        } -> null
    }

  # module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"] will be destroyed
  # (because key ["access-ristrict-sample1"] is not in for_each map)
  - resource "aws_wafv2_rule_group" "access_ristrict" {
      - arn         = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/rulegroup/access-ristrict-sample1-ristrict-waf-rulegp/befd7e4f-24de-4c68-9695-xxxxxxxxxx" -> null
      - capacity    = 3 -> null
      - description = "access-ristrict-sample1-ristrict-waf-rulegp" -> null
      - id          = "befd7e4f-24de-4c68-9695-xxxxxxxxxx" -> null
      - lock_token  = "fb7a8d63-7dcf-4d2e-b650-3b8ef61e3eeb" -> null
      - name        = "access-ristrict-sample1-ristrict-waf-rulegp" -> null
      - scope       = "REGIONAL" -> null
      - tags        = {} -> null
      - tags_all    = {} -> null

      - rule {
          - name     = "access-ristrict-sample1-ristrict-rule" -> null
          - priority = 1 -> null

          - action {
              - block {
                }
            }

          - statement {
              - and_statement {
                  - statement {
                      - byte_match_statement {
                          - positional_constraint = "STARTS_WITH" -> null
                          - search_string         = "/ristrict/sample1/" -> null

                          - field_to_match {
                              - uri_path {}
                            }

                          - text_transformation {
                              - priority = 0 -> null
                              - type     = "NONE" -> null
                            }
                        }
                    }
                  - statement {
                      - not_statement {
                          - statement {
                              - ip_set_reference_statement {
                                  - arn = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/ipset/access-ristrict-sample1-ristrict-ipsets/b18a8319-eaa3-4643-8853-xxxxxxxxxx" -> null
                                }
                            }
                        }
                    }
                }
            }

          - visibility_config {
              - cloudwatch_metrics_enabled = true -> null
              - metric_name                = "access-ristrict-sample1-ristrict-rule" -> null
              - sampled_requests_enabled   = true -> null
            }
        }

      - visibility_config {
          - cloudwatch_metrics_enabled = true -> null
          - metric_name                = "access-ristrict-sample1-ristrict-rulegp" -> null
          - sampled_requests_enabled   = true -> null
        }
    }

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

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
user.emi:~/environment/test-terraform/envs/dev-waf $ terraform apply
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=261098a7-82f6-49bd-bb9d-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=1461c7eb-52e6-42cc-97bb-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=b18a8319-eaa3-4643-8853-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample2"]: Refreshing state... [id=0dd59d52-a04a-4740-b93a-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample3"]: Refreshing state... [id=0c82ad12-a05a-4af4-814d-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Refreshing state... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx]
module.waf.aws_wafv2_web_acl.web_acl: Refreshing state... [id=xxxxxxxxxx]
module.waf.aws_wafv2_web_acl_association.waf_association: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx,arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/waf-test-alb/xxxxxxxxxx]
module.waf.aws_cloudwatch_log_group.cwlogs: Refreshing state... [id=aws-waf-logs-alb-webacl]
module.waf.aws_wafv2_web_acl_logging_configuration.waflog: Refreshing state... [id=arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/alb-webacl/xxxxxxxxxx]

Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the last "terraform apply" which may have affected this plan:

  # module.waf.aws_wafv2_web_acl.web_acl has changed
  ~ resource "aws_wafv2_web_acl" "web_acl" {
      ~ capacity      = 9 -> 6
        id            = "xxxxxxxxxx"
        name          = "alb-webacl"
        tags          = {
            "Name" = "alb-webacl"
        }
        # (6 unchanged attributes hidden)

        # (5 unchanged blocks hidden)
    }


Unless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

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

Terraform will perform the following actions:

  # module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"] will be destroyed
  # (because key ["access-ristrict-sample1"] is not in for_each map)
  - resource "aws_wafv2_ip_set" "access_ristrict" {
      - addresses          = [
          - "0.0.0.0/1",
          - "128.0.0.0/1",
        ] -> null
      - arn                = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/ipset/access-ristrict-sample1-ristrict-ipsets/b18a8319-eaa3-4643-8853-xxxxxxxxxx" -> null
      - description        = "access-ristrict-sample1-ristrict-ipsets" -> null
      - id                 = "b18a8319-eaa3-4643-8853-xxxxxxxxxx" -> null
      - ip_address_version = "IPV4" -> null
      - lock_token         = "aede225d-0e76-471d-9030-06be5230c6b3" -> null
      - name               = "access-ristrict-sample1-ristrict-ipsets" -> null
      - scope              = "REGIONAL" -> null
      - tags               = {
          - "Name" = "access-ristrict-sample1-ristrict-ipsets"
        } -> null
      - tags_all           = {
          - "Name" = "access-ristrict-sample1-ristrict-ipsets"
        } -> null
    }

  # module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"] will be destroyed
  # (because key ["access-ristrict-sample1"] is not in for_each map)
  - resource "aws_wafv2_rule_group" "access_ristrict" {
      - arn         = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/rulegroup/access-ristrict-sample1-ristrict-waf-rulegp/befd7e4f-24de-4c68-9695-xxxxxxxxxx" -> null
      - capacity    = 3 -> null
      - description = "access-ristrict-sample1-ristrict-waf-rulegp" -> null
      - id          = "befd7e4f-24de-4c68-9695-xxxxxxxxxx" -> null
      - lock_token  = "fb7a8d63-7dcf-4d2e-b650-3b8ef61e3eeb" -> null
      - name        = "access-ristrict-sample1-ristrict-waf-rulegp" -> null
      - scope       = "REGIONAL" -> null
      - tags        = {} -> null
      - tags_all    = {} -> null

      - rule {
          - name     = "access-ristrict-sample1-ristrict-rule" -> null
          - priority = 1 -> null

          - action {
              - block {
                }
            }

          - statement {
              - and_statement {
                  - statement {
                      - byte_match_statement {
                          - positional_constraint = "STARTS_WITH" -> null
                          - search_string         = "/ristrict/sample1/" -> null

                          - field_to_match {
                              - uri_path {}
                            }

                          - text_transformation {
                              - priority = 0 -> null
                              - type     = "NONE" -> null
                            }
                        }
                    }
                  - statement {
                      - not_statement {
                          - statement {
                              - ip_set_reference_statement {
                                  - arn = "arn:aws:wafv2:ap-northeast-1:123456789012:regional/ipset/access-ristrict-sample1-ristrict-ipsets/b18a8319-eaa3-4643-8853-xxxxxxxxxx" -> null
                                }
                            }
                        }
                    }
                }
            }

          - visibility_config {
              - cloudwatch_metrics_enabled = true -> null
              - metric_name                = "access-ristrict-sample1-ristrict-rule" -> null
              - sampled_requests_enabled   = true -> null
            }
        }

      - visibility_config {
          - cloudwatch_metrics_enabled = true -> null
          - metric_name                = "access-ristrict-sample1-ristrict-rulegp" -> null
          - sampled_requests_enabled   = true -> null
        }
    }

Plan: 0 to add, 0 to change, 2 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_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Destroying... [id=befd7e4f-24de-4c68-9695-xxxxxxxxxx]
module.waf.aws_wafv2_rule_group.access_ristrict["access-ristrict-sample1"]: Destruction complete after 1s
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"]: Destroying... [id=b18a8319-eaa3-4643-8853-xxxxxxxxxx]
module.waf.aws_wafv2_ip_set.access_ristrict["access-ristrict-sample1"]: Destruction complete after 0s

Apply complete! Resources: 0 added, 0 changed, 2 destroyed.
user.emi:~/environment/test-terraform/envs/dev-waf $

おわりに

ALB に紐づける AWS WAF のルールグループを動的に生成する Terraform コードを作成しました。
どなたかのお役に立てば幸いです。ご意見もお待ちしております。

参考