こんにちは、貝塚です。
この6月にSCSKに入社しました。今後、AWSの技術情報を中心に記事を投稿していく予定です。よろしくお願いします。
入社後、早速、ハマったポイントをシェアしようと思います。
やりたかったこと
CloudFrontに設定したWAFv2のWeb ACLの、ルールにマッチしなかったときのデフォルトアクション(許可(Allow)/拒否(Block))を自動で切り替えたかったので、PythonからAWSを操作するライブラリboto3を使ってLambda関数を作成していました。
ハマったこと
Scopeの値が正しくない?
以下のコードを作成し、
import boto3 def lambda_handler(event, context): # WAFv2 web ACL名と web ACL IDをセット web_acl_name = 'SET_WEB_ACL_NAME' web_acl_id = 'SET_WEB_ACL_ID' # WAFv2 クライアント作成 wafv2 = boto3.client('wafv2') # 現在の web ACL 設定を取得 web_acl = wafv2.get_web_acl(Id=web_acl_id, Name=web_acl_name, Scope='CLOUDFRONT')
実行してみると、以下のエラーが出ました。
[ERROR] WAFInvalidParameterException: An error occurred (WAFInvalidParameterException) when calling the GetWebACL operation: Error reason: The scope is not valid., field: SCOPE_VALUE, parameter: CLOUDFRONT
Scopeの”CLOUDFRONT”という値が無効?
ですがboto3のドキュメントでは、Scopeは”CLOUDFRONT”または”REGIONAL”を指定できるように見受けられます。また、CloudFrontにアタッチされたWAFの場合は”CLOUDFRONT”を指定するのが正しいように思われます。
get_web_acl – Boto3 1.26.154 documentation (amazonaws.com)
…しばしあれもだめこれもだめとデバッグに時間を費やしてしまいますが、最終的には、boto3クライアント作成のところを以下のように変更することで、Scope=’CLOUDFRONT’を通すことができました。
# WAFv2 クライアント作成 wafv2 = boto3.client('wafv2', region_name='us-east-1')
ドキュメントをよく読むと、
To work with CloudFront, you must also specify the Region US East (N. Virginia) as follows: ... API and SDKs - For all calls, use the Region endpoint us-east-1.
Regionにus-east-1エンドポイントを指定しろと書かれています。最初はget_web_aclの引数としてRegion=を指定したり、見当違いのことをしていましたが、boto3.client()のところにregion_name=を渡せば良かったのですね…
Web ACL設定更新のやり方は?
さて、WAFの設定の更新なのになぜget_web_aclでweb ACLの情報を取得していたかと言いますと、WAFv2のweb ACLの更新では、楽観的ロック(Optimistic Locking)が採用されているからです。
まずはget_web_aclでLockTokenを取得し、更新(update_web_token)の時にはそのLockTokenを渡してやる必要があります。同時に発生した更新がなければ、更新は成功して終了です。
しかし、ほぼ同時に、かつ、この更新よりも早く、別のプログラムによって更新が行われていた場合、最初に行われた更新によりLockTokenの値が更新されるため、後から実行した更新はLockToken不一致となって失敗することになります。
LockTokenの指定を考慮した上で、Web ACL設定を更新するコードは以下のような形になります。
import boto3 def lambda_handler(event, context): # WAFv2 web ACL名と web ACL IDをセット web_acl_name = 'SET_WEB_ACL_NAME' web_acl_id = 'SET_WEB_ACL_ID' # 更新後のデフォルトアクションを指定 new_default_action = { 'Block': {} } # WAFv2 クライアント作成 wafv2 = boto3.client('wafv2', region_name='us-east-1') # 現在の web ACL 設定を取得 web_acl = wafv2.get_web_acl(Id=web_acl_id, Name=web_acl_name, Scope='CLOUDFRONT') # web ACL を新しい設定で更新 response = wafv2.update_web_acl( Name=web_acl['WebACL']['Name'], Scope='CLOUDFRONT', Id=web_acl_id, DefaultAction=new_default_action, LockToken=web_acl['LockToken'], VisibilityConfig=web_acl['WebACL']['VisibilityConfig'] )
実際に使用するコードでは、LockTokenが不一致のために更新に失敗した時の処理を考慮するべきでしょう。
また、AWSのAPIリファレンス UpdateWebACL – AWS WAFV2 (amazon.com) を確認すると、設定の部分更新はできず、設定が必要なパラメーターはすべて指定する必要があります。get_web_aclで取得した設定を、変更が必要な箇所だけ変更して残りをすべてupdate_web_aclに渡してあげてください。
(サンプルコードでは、変更したかったデフォルトアクションと、必須パラメータだったVisibilityConfigのみ指定していますが、一般的にはWAF Rulesなども設定してあると思いますので、ここで指定してあげてください)
AWS管理コンソール(AWS WAF → Web ACLs → Rulesタブ)から設定のダウンロード(下図、赤丸部分)ができますので、最初に渡すパラメータはこれをもとに作成してもよいでしょう。
まとめ
AWSのグローバルサービスをboto3で操作しようとしたことがなかったので、CloudFrontで使用しているときのリージョンの指定のしかたは完全に盲点でした。WAFをプログラムで操作しようとしている方の一助になれば幸いです。