Claude Codeカスタムコマンドで乗り切るWAF設定のつらみ
はじめに
SREエンジニアをしているmutaoです。
今回はClaude Codeのスラッシュコマンドを利用した、Terraformコードを書きやすくする取り組みの一環を紹介します。
弊社では、TerraformでAWS WAFを管理していますが、特定パスの除外設定を追加するたびに複雑なネスト構造のHCLを数十行書く必要があり、作業が非常に複雑になっています。
また、WAFはアプリケーションの実装と密接に結びついていて、開発者自身が素早く除外を足せる仕組みが求められていました。
そんな中で、Claude Codeのスラッシュコマンドを活用して、Terraform初心者でもWAFの設定を簡単に変更できるようにするチャレンジを行いました。
WAF設定はなぜここまでしんどいのか
AWS WAFをTerraformで管理していると、「このパスだけ除外したい」という要件が発生した瞬間に作業が一気に複雑化します。
rule_action_override
を count
にするだけなら済むのですが、特定パスの例外を実現するには rule_action_override
→ regex_pattern_set
→ カスタムルールという三層のHCLを十数〜数十行単位で積み上げる必要があります。
現場で見ているTerraformの密度
ある環境では Web ACL 定義だけで数百行に達していました。
複数のマネージドルールに個別の除外を設けたり、プロダクト固有のパス例外を積み重ねたりすると、and_statement
や not_statement
を多段に組み合わせる必要があるからです。
結果として rule_action_override
→ regex_pattern_set
→ カスタムルールという三層構造が繰り返し登場し、深いネストが避けられません。以下に Terraform の一部を匿名化した例を示します。
# 例: 管理ルールをCOUNT化し、そのラベルを使って特定パスを除外してブロック
rule {
name = "example-managed-rule"
priority = 900
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
dynamic "rule_action_override" {
for_each = [
"CrossSiteScripting_BODY",
]
content {
action_to_use {
count {}
}
name = rule_action_override.value
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "example-managed-rule"
sampled_requests_enabled = true
}
}
rule {
name = "example-block-xss-body"
priority = 1000
action {
block {}
}
statement {
and_statement {
# マネージドルールが付けたラベルを確認
statement {
label_match_statement {
key = "awswaf:managed:aws:core-rule-set:CrossSiteScripting_Body"
scope = "LABEL"
}
}
# 除外対象パスをregexで判定
statement {
not_statement {
statement {
regex_pattern_set_reference_statement {
arn = aws_wafv2_regex_pattern_set.ignore_example_xss.arn
field_to_match { uri_path {} }
text_transformation {
priority = 0
type = "NONE"
}
}
}
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "example-block-xss-body"
sampled_requests_enabled = true
}
}
resource "aws_wafv2_regex_pattern_set" "ignore_example_xss" {
name = "example-app-sandbox-ignore-xss"
scope = "REGIONAL"
dynamic "regular_expression" {
for_each = [
"^/example/preview/.+$",
"^/example/admin/editor/.+$",
]
content {
regex_string = regular_expression.value
}
}
}
こんな入れ子構造を、例外が出るたびに手書きする。
想像だけでも気が遠くなります。
学びの土台になった本
突破口を探していた頃、オライリーの『生成AIのプロンプトエンジニアリング』を読み込み、特に意識したのは次のポイントです。
- タスクを小さく分解してゴールをはっきり書く
- 期待する出力形式を先に伝える
- フューショット(良い例を見せてから頼む)で挙動を安定させる
- 生成後にどう検証するかをプロンプト内で指示する
この“プロンプト設計の型”を、後述するスラッシュコマンド設計にそのまま流用しています。
スラッシュコマンド活用の前に試した工夫
最初は Terraform の dynamic
ブロックで共通化したり、除外設定をモジュール化して可読性を保とうと試みました。
しかし、パラメータの受け渡しや優先度の調整が複雑になり、結局どこで何をしているか追いづらくなってしまい惜敗が続きました。
そこで「プロンプト設計の知見を生かし、スラッシュコマンドで直接コードを生成してしまおう」と方向転換したのが今回の取り組みです。
PATH除外が複雑な理由(改めて)
除外を1件追加するだけで、以下の3ステップをすべて書かないといけません。
それぞれが別リソース・別ブロックになり、ちょっとした修正でもネストが雪だるま式に増えていきます。
ステップ① マネージドルールを COUNT モードに変更
マネージドルールを rule_action_override
で count {}
に書き換え、ブロックの代わりにラベルだけを付与します。
この時点で override_action
や visibility_config
も維持しながら設定しなければなりません。
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
rule_action_override {
action_to_use {
count {}
}
name = "CrossSiteScripting_BODY"
}
}
}
ステップ② 除外パターンセットの作成
除外したいパスごとに aws_wafv2_regex_pattern_set
を定義し、dynamic "regular_expression"
で正規表現を列挙します。
パターンが増えるたびにここもメンテナンス対象になります。
resource "aws_wafv2_regex_pattern_set" "ignore_example_xss" {
name = "example-app-sandbox-ignore-xss"
scope = "REGIONAL"
dynamic "regular_expression" {
for_each = [
"^/example/preview/.+$",
"^/example/admin/editor/.+$",
]
content {
regex_string = regular_expression.value
}
}
}
ステップ③ カスタムルールでラベルとパスを突き合わせる
最後に label_match_statement
と not_statement
を駆使して、ステップ1が付与したラベルとステップ2の除外パターンを突き合わせます。
優先度や可視化設定もここで個別に指定しなければなりません。
rule {
name = "example-block-xss-body"
priority = 1000
action {
block {}
}
statement {
and_statement {
statement {
label_match_statement {
key = "awswaf:managed:aws:core-rule-set:CrossSiteScripting_Body"
scope = "LABEL"
}
}
statement {
not_statement {
statement {
regex_pattern_set_reference_statement {
arn = aws_wafv2_regex_pattern_set.ignore_example_xss.arn
field_to_match { uri_path {} }
text_transformation {
priority = 0
type = "NONE"
}
}
}
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "example-block-xss-body"
sampled_requests_enabled = true
}
}
この3つをセットで書いてようやく「特定パスだけ除外する」という要件が満たせます。
除外パターンが増えるほどコピペとネストが増殖し、ケアレスミスが生まれやすくなるのが地獄たる所以です。
Claude Codeスラッシュコマンドで自動化
そこで /waf
というスラッシュコマンドを開発し、Terraform ファイルの解析〜生成を自動化しました。表に見える操作は極めてシンプルです。
# ルールをCOUNTモードに切り替え
/waf disable example-app/stg NoUserAgent_HEADER
# 特定パスの除外をまとめて追加
/waf exclude example-app/prod CrossSiteScripting_BODY /example/preview/,/example/admin/editor/
# 現在の設定サマリを確認
/waf status example-app/prod
プロンプト設計で気をつけたこと
- タスク分解:設定解析 → Terraform 生成 → PR 用差分の整理を順番に指示
- 出力形式指定:欲しい HCL の構造を箇条書きで説明
- フューショット:上記のようなサンプル断片を提示し、「これに倣って追加して」と明示
- 検証フロー:差分の確認や Lint の実行をプロンプト内に含め、最後は人間のレビューを前提化
この型でプロンプトを組み立てたところ、LLM が安定して狙い通りの Terraform 断片を吐き出すようになり、ネスト地獄から 1 行のコマンド入力へ置き換えられました。
Slash Command仕様をどうまとめたか
このスラッシュコマンドの仕様を waf.md
というドキュメントに整理し、Claude Code から参照させています。
中心となる部分を一部抜粋すると、次のような記述です。
---
argument-hint: enable [product/env] [rule] | disable [product/env] [rule]
| exclude [product/env] [rule] [path1,path2,...]
| status [product/env] | rules [product/env] | exclusions [product/env]
description: "AWS WAF v2の設定を管理するためのコマンドです"
---
## 利用可能なコマンド
- /waf enable [プロダクト/環境] [ルール名] — ルール単位でBLOCKに戻す(PATH指定なし)
- /waf disable [プロダクト/環境] [ルール名] — ルール単位でCOUNTモードにする(PATH指定なし)
- /waf exclude [プロダクト/環境] [ルール名] [パス1,パス2,...] — 特定パスだけ例外にする
- /waf status [プロダクト/環境]
- /waf rules [プロダクト/環境]
### 対象ファイル指定
- [プロダクト/環境] プレースホルダは `product/env` 形式。例: `example-app/prod`。
1. 入力形式: {プロダクト名}/{環境名}
2. 探索パス: terraform/{プロダクト名}/{環境名}/waf.tf
3. ファイル存在確認: 指定されたパスのwaf.tfをチェック
## PATH単位の例外設定の実装方法
### ステップ1: マネージドルールを COUNT モードに変更
- `rule_action_override` で対象ルールを `count {}` に切り替え、ブロックではなくラベル付与だけ行う。
- 既存の `override_action` と `visibility_config` はそのまま維持。
```hcl
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
rule_action_override {
action_to_use {
count {}
}
name = "CrossSiteScripting_BODY"
}
}
}
```
### ステップ2: 除外パターンセットの作成
- `aws_wafv2_regex_pattern_set` に許容したいパスの正規表現を列挙し、`dynamic "regular_expression"` で柔軟に展開する。
- パターン名は `${product_name}-${env}-ignore-<rule>` など統一した命名を利用。
```hcl
resource "aws_wafv2_regex_pattern_set" "ignore_example_xss" {
name = "example-app-sandbox-ignore-xss"
scope = "REGIONAL"
dynamic "regular_expression" {
for_each = [
"^/example/preview/.+$",
"^/example/admin/editor/.+$",
]
content {
regex_string = regular_expression.value
}
}
}
```
### ステップ3: カスタムルールでラベルとパスを突き合わせる
- `label_match_statement` でステップ1が付与したラベルを検出し、`not_statement` と `regex_pattern_set_reference_statement` で除外パターンを除く。
- それ以外を `action { block {} }` で制御し、`priority` を十分下げて既存ルールと競合しないようにする。
```hcl
rule {
name = "example-block-xss-body"
priority = 1000
action {
block {}
}
statement {
and_statement {
statement {
label_match_statement {
key = "awswaf:managed:aws:core-rule-set:CrossSiteScripting_Body"
scope = "LABEL"
}
}
statement {
not_statement {
statement {
regex_pattern_set_reference_statement {
arn = aws_wafv2_regex_pattern_set.ignore_example_xss.arn
field_to_match { uri_path {} }
text_transformation {
priority = 0
type = "NONE"
}
}
}
}
}
}
}
}
```
### ステップ4: モニタリングと後片付け
- `visibility_config` で CloudWatch メトリクスとサンプルログを有効にし、検証期間中のトラフィックを監視。
- 調査が完了したら一時的な `rule_label` や `count` アクションを削除し、恒常運用の設定へ移行。
```hcl
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "example-block-xss-body"
sampled_requests_enabled = true
}
```
### 動作原理
1. **マネージドルール実行**: `rule_action_override`で`COUNT`モードに設定されたルールが実行される
- 検知条件に一致した場合:ラベルが付与される(ブロックはされない)
- 検知条件に一致しない場合:何も起こらない
2. **カスタムルール実行**: `label_match_statement`でラベルの存在を確認
- ラベルが存在する場合:例外パターンをチェック
- ラベルが存在しない場合:カスタムルールは実行されない
3. **例外判定とアクション**:
- ラベルあり + 例外パターンに一致:何もしない(通過)
- ラベルあり + 例外パターンに不一致:BLOCK実行
**重要**: マネージドルールを`COUNT`にすることで、検知機能は維持しつつ、カスタムルールで柔軟な制御を実現します。ルールを削除や無効化すると、そもそも検知されなくなってしまいます。
各コマンドがどのステップを踏むか、生成するTerraform断片の例、実行後に確認してほしいポイントまで段階的に書いています。
ステップを明示し、毎回アウトプットのサンプルを添え、動作原理を記載することで精度を向上させています。
やってみて分かったこと
-
手書きの重労働から解放
例外が増えるたびに数十行規模のネストを手書きする苦労から解放され、心理的な負担が大幅に減りました。 -
開発者へのEnablingの可能性
Terraformを書き慣れていない開発者でも、スラッシュコマンドを利用してインフラ(WAF)の設定変更が可能になりました。 -
保守コストはゼロにならない
Terraform AWS Provider や WAF の仕様が変わるとコマンド側のメンテが必要です。便利さと保守負担のバランスを常に意識しています。
まとめ
- AWS WAF の PATH 除外は、ネスト構造と記述量が容赦なく、ミスも致命的になりがち
- プロンプト設計の基本(分解・出力形式・フューショット)が Claude Code のコマンド設計に直結した
- Slash Command の仕様をドキュメント化し、ステップと出力例を明示したことで、利用者の不安を減らしつつ安全に自動化できた
これからも、つらみの大きい運用タスクこそ AI とタッグを組んで改善していきたいところです。
Discussion