【CircleCI】AWS WAFのIPsetをリポジトリで管理する
はじめに
皆さんはAWS WAFに設定するIPセットをどのように管理していますか?
プロジェクトが大きくなるにつれJoinするメンバーのIPアドレスを多数管理する場合、管理方法についてその最適解に悩むことも少なくないかと思います。
背景として、AWSコンソールから確認できる設定済みのIPアドレスは、アドレスのみのリストですので、実際に何のIPアドレスなのかはこのIPセットだけでは管理出来ません。

本記事では、IPアドレスとその用途をセットでテキストベースで管理し、そのファイルをもとにAWS WAFのIPセットの設定を行う流れについて記載いたします。
本記事の留意点
- タイトルにCircleCIと入れていますが、シェルスクリプトをベースとした手法ですのでGitHub Actionsにも流用可能な方法となっております。
- AWS WAFのACLの更新に関しては内容触れておりません。
- 記事で使用しているIPアドレスや名前はサンプルデータですので、実在するものとは一切関係ございません。
- 既に用意済みのIPセットに対して、IPアドレスリストを更新する方法について記載しています。
IPアドレスをテキストファイルで管理する
以下のように、IPアドレスとカンマ区切りで用途もしくは利用者について記載したテキストファイルで管理します。
このファイルは、AIの力で作成した疑似データです。
余談ですが最初かなり実在しそうな日本人名や会社名が作成されてしまったので、外国人名に作り直しました。
この形式で後述するCI/CDを設定すれば、非エンジニアの方でも追記してIPセットを更新できそうかと思います。
45.76.123.10/32, Aetherix LLC - Lars Öberg(DevOps)
13.227.44.101/32, Novara Systems - Amira Khaled(セールス)
34.201.72.59/32, BlueCedar Labs - Diego Márquez(研究)
51.15.201.7/32, Solenium Inc. - Freja Lindström(Remote)
88.198.134.22/32, Vintara Holdings - Marcus V. Hargreaves(CFO)
103.86.55.121/32, Orbio Networks - Anika Müller(ネットワーク)
104.244.42.129/32, Crestlineworks - Pavel Ivanov(エンジニア)
162.159.24.45/32, Nyxoria GmbH - Sofia Rossi(マーケ)
185.199.110.77/32, Halcyon Forge - Kaito Nakamura(コントリビュータ)
143.244.56.12/32, Meridian Arc - Oleg Petrov(サポート)
206.189.34.88/32, Skylane Partners - Amélie Dubois(PM)
174.129.23.5/32, Tesseract Solutions - Hugo Fernandes(顧問)
80.82.77.64/32, Velvet Harbor - Ingrid Sørensen(QA)
54.38.12.201/32, Quivira Tech - Benoît Moreau(バックエンド)
109.74.32.11/32, LumenField Ltd. - Zara Khan(HR)
213.136.89.3/32, AstraVale Corporation - Henrik Jansson(財務)
199.115.31.200/32, Parallax Works - Lucia Romano(デザイナー)
147.75.65.18/32, Emberline Co. - Nikhil Rao(データサイエンティスト)
52.58.23.144/32, Silica Ridge - Marta Kowalska(法務)
...(以下全部で60個)
IPセットの作成
今回は既存のIPセットに対しての更新を行う内容ですので、説明用として先に手動でIPセットを作成しておきます。
名前はipset-auto-update-testとして東京リージョンにしました。
以下の表示は新しいWAFコンソールで表示しています。

ちなみに古いWAFコンソールは以下の表示になります。

リポジトリに各ファイルを用意
リポジトリには、以下のようなファイル構成で用意します。
% tree -a . -I '.git'
.
├── .circleci
│ └── config.yml
├── ip-set-sample.txt
└── update-ip-set.sh
-
.circleci/config.yml: CircleCIの設定ファイル -
ip-set-sample.txt: 先ほど記載したIPアドレスとカンマ区切りで用途を書いたテキストファイル -
update-ip-set.sh: 後述するシェルスクリプト
このシェルスクリプトは単体でも実行できる内容なので、GitHub Actionsなどにも利用可能です。
早速その中身を見ていきましょう。
IPセットを更新するシェルスクリプトの作成
まずは全体のコードを先に掲載します。
#!/bin/bash
_ipset_id=$1
_ipset_name=$2
_scope=$3
_region=$4
_list_path=$5
_aws_profile=$6
# 0. 引数チェックとリストファイル存在チェック
if [[ -z "$_ipset_id" || -z "$_ipset_name" || -z "$_scope" || -z "$_region" || -z "$_list_path" || -z "$_aws_profile" ]]; then
echo "Usage: $0 <ipset_id> <ipset_name> <scope> <region> <list_path> <aws_profile>" >&2
exit 1
fi
if [[ ! -f "$_list_path" ]]; then
echo "Error: list file '$_list_path' not found." >&2
exit 1
fi
# 1. file読み込みIPアドレスのリストを作成
ip_addrs=""
while IFS= read -r line || [[ -n $line ]]; do
# 行から左側の IP/CIDR 部分のみを抽出(カンマ以降を切り捨てる)
ip="${line%%,*}"
# 前後の空白を削除
ip="$(echo "$ip" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
# 空行はskip
if [[ -n "$ip" ]]; then
ip_addrs+="$ip "
fi
done < "$_list_path"
# 2. LockTokenの取得
lock_token=$(
aws wafv2 list-ip-sets \
--scope ${_scope} \
--region ${_region} \
--query "IPSets[?Name=='${_ipset_name}'] | [0].LockToken" \
--output text \
--profile ${_aws_profile} \
--no-cli-pager
)
echo $lock_token
# 3. LockTokenを使用してIPセットを更新
aws wafv2 update-ip-set \
--scope ${_scope} \
--region ${_region} \
--id ${_ipset_id} \
--name ${_ipset_name} \
--addresses ${ip_addrs} \
--lock-token ${lock_token} \
--profile ${_aws_profile} \
--no-cli-pager
# 4. 更新後の確認: ファイルのリストと実際の設定の差分を確認
local_file=$(mktemp)
tmp_aws=$(mktemp)
trap 'rm -f "$local_file" "$tmp_aws"' EXIT
## 4-1. ファイル側(ip_addrs はスペース区切り)を改行にしてソート
echo "$ip_addrs" | tr ' ' '\n' | sed '/^\s*$/d' | sort -u > "$local_file"
## 4-2. WAF 側の設定を取得し、改行分割してソート
if ! {
set -o pipefail
aws wafv2 get-ip-set \
--scope "${_scope}" \
--region "${_region}" \
--id "${_ipset_id}" \
--name "${_ipset_name}" \
--query 'IPSet.Addresses' \
--output text \
--profile "${_aws_profile}" \
--no-cli-pager \
| tr -s '\t ' '\n' \
| sed '/^\s*$/d' \
| sort -u > "$tmp_aws"
}; then
echo "Error: failed to retrieve or process WAF IP set" >&2
exit 1
fi
## 4-3. 差分の確認
echo "----------------------------------------"
echo "入力ファイルとWAF設定の差分を確認します"
echo "----------------------------------------"
if diff -u "$local_file" "$tmp_aws" >/dev/null; then
echo "差分なし:更新成功。入力ファイルとWAF設定が一致します。"
exit 0
else
echo "差分検出:入力ファイルとWAF設定が一致しません。" >&2
diff -u "$local_file" "$tmp_aws" >&2
exit 2
fi
引数の説明
ではセクションごとに内容を確認していきます。
引数としては、以下の5つ〜6つになります。
-
_ipset_id: 更新するIPセットのIDです。コンソール画面のARNの右側に記載されています。 -
_ipset_name: 更新するIPセットの名前です。今回はipset-auto-update-testという名前で作成しました。 -
_scope: CloudFrontディストリビューションなどのグローバルリソースタイプ用かどうかを指定します。グローバルならCLOUDFRONT、そうではないリージョナルならREGIONALを指定します。今回は東京リージョンなのでREGIONALを指定することになります。 -
_region: リージョナルならそのリージョン名を、グローバルならus-east-1を指定します。 -
_list_path: カンマ区切りのIPアドレスを記載したテキストファイルのパスを指定します。 -
_aws_profile: AWSのプロファイル名。これはCI内でプロファイルを指定してsetupしている場合に必要ですが、必須ではありません。
そして必要に応じて一応チェック処理を入れています。
_ipset_id=$1
_ipset_name=$2
_scope=$3
_region=$4
_list_path=$5
_aws_profile=$6
# 0. 引数チェックとリストファイル存在チェック
if [[ -z "$_ipset_id" || -z "$_ipset_name" || -z "$_scope" || -z "$_region" || -z "$_list_path" || -z "$_aws_profile" ]]; then
echo "Usage: $0 <ipset_id> <ipset_name> <scope> <region> <list_path> <aws_profile>" >&2
exit 1
fi
if [[ ! -f "$_list_path" ]]; then
echo "Error: list file '$_list_path' not found." >&2
exit 1
fi
テキストファイルの読み込み
次にテキストファイルを読み込みます。
テキストファイルは以下の構成で記載し、実際にスクリプトが見るのはカンマまでとします。
ですので用途を自由に記載できます。
{IPアドレス}/{プレフィックス}, {用途}
空行や多少のスペースを許容するかどうかはお任せしますが、今回は多少は許容するパターンで作成してみました。
逆にIPのプレフィクスは必須なのでそのバリデーションを入れるのはありかもしれません。(※ここでは入れておりません)
# 1. file読み込みIPアドレスのリストを作成
ip_addrs=""
while IFS= read -r line || [[ -n $line ]]; do
# 行から左側の IP/CIDR 部分のみを抽出(カンマ以降を切り捨てる)
ip="${line%%,*}"
# 前後の空白を削除
ip="$(echo "$ip" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
# 空行はskip
if [[ -n "$ip" ]]; then
ip_addrs+="$ip "
fi
done < "$_list_path"
LockTokenを利用してIPのセットを更新
変数ip_addrsに更新するIPアドレスを用意できたら、実際にaws cliで更新をしていきます。
IPセットを更新する際、AWSはその内容の整合性を保つために、最後に内容を取得した状態に対してtokenを発行し、そのLockTokenが更新に必須となっています。
そのため、まず既存のIPセットをaws wafv2 list-ip-setsコマンドで取得し、LockTokenを取得してからaws wafv2 update-ip-setコマンドで更新する流れになります。
以下は、取得時にLockTokenをqueryし、変数lock_tokenに格納したあとにIPセットを更新しています。
# 2. LockTokenの取得
lock_token=$(
aws wafv2 list-ip-sets \
--scope ${_scope} \
--region ${_region} \
--query "IPSets[?Name=='${_ipset_name}'] | [0].LockToken" \
--output text \
--profile ${_aws_profile} \
--no-cli-pager
)
echo $lock_token
# 3. LockTokenを使用してIPセットを更新
aws wafv2 update-ip-set \
--scope ${_scope} \
--region ${_region} \
--id ${_ipset_id} \
--name ${_ipset_name} \
--addresses ${ip_addrs} \
--lock-token ${lock_token} \
--profile ${_aws_profile} \
--no-cli-pager
設定がきちんと出来たかを確認
ここまでで処理自体は終えて良いのですが、本当に正常にもれなく設定されたか気になる所ですよね。ただ、これを手動でやろうとするとかなり面倒です。
なぜならコンソールで表示されるIPセットの並びと設定元のテキストファイルの並びは違うからです。
そこで、スクリプト内でそれぞれソートしてdiffを取ることで、解決してみました。
# 4. 更新後の確認: ファイルのリストと実際の設定の差分を確認
local_file=$(mktemp)
tmp_aws=$(mktemp)
trap 'rm -f "$local_file" "$tmp_aws"' EXIT
## 4-1. ファイル側(ip_addrs はスペース区切り)を改行にしてソート
echo "$ip_addrs" | tr ' ' '\n' | sed '/^\s*$/d' | sort -u > "$local_file"
## 4-2. WAF 側の設定を取得し、改行分割してソート
if ! {
set -o pipefail
aws wafv2 get-ip-set \
--scope "${_scope}" \
--region "${_region}" \
--id "${_ipset_id}" \
--name "${_ipset_name}" \
--query 'IPSet.Addresses' \
--output text \
--profile "${_aws_profile}" \
--no-cli-pager \
| tr -s '\t ' '\n' \
| sed '/^\s*$/d' \
| sort -u > "$tmp_aws"
}; then
echo "Error: failed to retrieve or process WAF IP set" >&2
exit 1
fi
## 4-3. 差分の確認
echo "----------------------------------------"
echo "入力ファイルとWAF設定の差分を確認します"
echo "----------------------------------------"
if diff -u "$local_file" "$tmp_aws" >/dev/null; then
echo "差分なし:更新成功。入力ファイルとWAF設定が一致します。"
exit 0
else
echo "差分検出:入力ファイルとWAF設定が一致しません。" >&2
diff -u "$local_file" "$tmp_aws" >&2
exit 2
fi
ここまでで動作確認する場合
ここまで完了して動作確認したい場合は、引数が多いのでスクリプトを実行するためのスクリプトを作成してみましょう。
私は下記のような感じで実行しました。
#!/bin/bash
_ipset_id=107ea961-48a2-4838-b5a9-f4d0d83c02ae
_ipset_name=ipset-auto-update-test
_scope=REGIONAL
_region=ap-northeast-1
_file_path=./ip-set-sample.txt
./update-ip-set.sh "${_ipset_id}" \
"${_ipset_name}" \
"${_scope}" \
"${_region}" \
"${_file_path}" \
default
CircleCIの設定
ここからは、先程作成したスクリプトをもとに、CircleCIの設定ファイルを作成していきます。
長く見えるかもしれませんが、先程のスクリプトの引数をparametersとして設定するために行数を使っているだけで、スクリプトを理解すれば特に難しい設定ではないかなと思います。
aws-cli/setupの部分は、oidc認証を使用しています。
ここはkey/sec運用ならそれに合わせて書き方を変更して下さい。
version: 2.1
orbs:
aws-cli: circleci/aws-cli@5.4.1
jobs:
update-ip-set:
docker:
- image: cimg/base:stable
resource_class: small
parameters:
aws-profile:
type: string
default: OIDC-user
description: The AWS profile to use
ipset-id:
type: string
description: The WAF IP set ID
ipset-name:
type: string
description: The WAF IP set name
scope:
type: enum
enum: [CLOUDFRONT, REGIONAL]
default: CLOUDFRONT
description: The scope of the WAF IP set
region:
type: string
default: us-east-1
description: The AWS region
list-path:
type: string
description: Path to the file containing IP addresses
steps:
- checkout
- aws-cli/setup:
profile_name: << parameters.aws-profile >>
role_arn: $OIDC_ROLE_ARN
- run:
name: Update WAF IP Set
command: |
chmod +x ./update-ip-set.sh
./update-ip-set.sh \
<< parameters.ipset-id >> \
<< parameters.ipset-name >> \
<< parameters.scope >> \
<< parameters.region >> \
<< parameters.list-path >> \
<< parameters.aws-profile >>
workflows:
update-waf-ip-set-workflow:
jobs:
- update-ip-set:
name: update-ip-set-tokyo
ipset-id: 70ed853f-ffe0-4236-b0be-788ff9171d27
ipset-name: ipset-auto-update-test
scope: REGIONAL
region: ap-northeast-1
list-path: ./ip-set-sample.txt
context: waf-test
上記を実際に動作してみました。
トータル20秒程度で完了しています。

AWSコンソールからも設定が無事反映されていることを確認できました。

気になるのはドキュメントにあるmax 50の表記なのですが、
アドレスが50個を超えても問題なく設定出来るんですよね。
これは何の制限になるのでしょうか...?
分かる方いらっしゃいましたらコメントよろしくお願いいたします。
おわりに
AWS WAFのIPセットに焦点を当てて今回記事を書いてみました。
管理方法に正解はないと思いますので、そのひとつとして参考になれば幸いです。
ここまで読んでいただきありがとうございました。
参考
Discussion