[小ネタ] コンソール上で確認出来ないCloudWatchメトリクスにCFnでアラームを設定したい
はじめに
コンサルティング部のぐっさんです。
最近WAFのBlockedRequestsについてアラーム発報したいけれどコンソールでメトリクスが出力されていない!といった事象に出会いましたので対処方法について確認した記録です。
原因
以下ドキュメントのレポート条件が原因です。
レポート条件: ゼロ以外の値がある。
対象のメトリクスで何らかの値が検知されないと、コンソール上で確認できないわけです。
WAFのBlockedRequestsの場合は、何らかのアクセスブロックが発生しない限りは見えない状態です。
CLIやマネジメントコンソールでの設定方法
これらについては既にわかりやすい記事がありますので、以下参照ください。
- CLI編
- マネジメントコンソール編
今回はIaC(CFn)からもメトリクスを直接指定してアラーム設定できるのか?にフォーカスして検証した記事となります。
結果
IaCを使用しても問題なくアラームの作成が可能です。
対象の名前空間とメトリクスをプロパティで直指定します。
今回の検証で使用したCFnで指定する場合の書き方の一例は以下です。
Resources:
WAFBlockedRequestsAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Ref AlarmName
AlarmDescription: !Sub 'Alarm when WAF BlockedRequests exceeds ${Threshold} in ${Period} seconds. WebACL: ${WebACLName} (ID: ${WebACLId}), Region: ${Region}, Rule: ${Rule}'
MetricName: BlockedRequests
Namespace: AWS/WAFV2
Statistic: !Ref Statistic
Period: !Ref Period
EvaluationPeriods: !Ref EvaluationPeriods
DatapointsToAlarm: !Ref DatapointsToAlarm
Threshold: !Ref Threshold
ComparisonOperator: !Ref ComparisonOperator
TreatMissingData: !Ref TreatMissingData
Dimensions:
- Name: WebACL
Value: !Ref WebACLName
- Name: Region
Value: !Ref Region
- Name: Rule
Value: !Ref Rule
AlarmActions:
- !Ref SNSTopicArn
以下は実際に検証した手順を記載します。
検証内容
前提
- 既存のALBに対してWAFを作成しアタッチ
- ALBのデフォルトアクションに固定レスポンスを返す設定を入れアクセス成功時はメンテナンス画面を表示する想定
- WAFによりアクセスがブロックされた際にはWAFのカスタムレスポンスを返す
- WAFのルールはIPセットを使用しホワイトリスト以外のIPからの接続は拒否する
- アラーム通知先: 既存のSNS・Chatbot経由でSlackへ通知
手順
- WAFのIPセット(ホワイトリスト用)を作成し自身のIPを入れる
- WAF作成
- ALBのDNS名でアクセス実行
- WAFのCloudWatchメトリクスでAllowedRequestsは更新されるがBlockedRequestsは見えないことを確認
- CFnでBlockedRequestsのアラーム作成
- WAFのホワイトリストから自身のIPを削除しアクセスがブロックされる(WAFのカスタムレスポンスが返される)ことを確認
- アラーム通知がくることを確認
- BlockedRequestsがメトリクス上で見えるようになることを確認
内容
WAFのIPセット(ホワイトリスト用)を作成し自身のIPを入れる
WAF作成
名前を適当につけてコンソールから作成します
リソースに既存のALBを設定
ルールに最初に作成したIPセットを指定
ホワイトリスト想定のためルールに合致した時のアクションは「Allow」を選択
WAFのデフォルトアクションは「Block」としてついでにカスタムレスポンスを設定
ブロックされた時に表示するHTMLを設定
ちなみにHTMLはAIにサッと作ってもらったものです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>アクセス制限</title>
<style>
body{margin:0;font-family:sans-serif;background:linear-gradient(45deg,#ff6b6b,#feca57,#48dbfb,#ff9ff3);background-size:400% 400%;animation:bg 15s ease infinite;min-height:100vh;display:flex;justify-content:center;align-items:center}
@keyframes bg{0%,100%{background-position:0 50%}50%{background-position:100% 50%}}
.card{background:#fff;border-radius:40px;padding:40px;max-width:500px;box-shadow:0 30px 60px rgba(0,0,0,.2);text-align:center}
.emoji{font-size:60px;margin-bottom:20px;animation:wiggle 1s ease-in-out infinite}
@keyframes wiggle{0%,100%{transform:rotate(-5deg)}50%{transform:rotate(5deg)}}
h1{background:linear-gradient(45deg,#ff6b6b,#feca57,#48dbfb);-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-size:32px;margin-bottom:15px}
p{color:#555;font-size:16px;line-height:1.6;margin-bottom:20px}
.cards{display:flex;gap:10px;margin:20px 0;flex-wrap:wrap;justify-content:center}
.mini{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:15px;border-radius:15px;font-size:12px;flex:1;min-width:100px;transition:transform .3s}
.mini:hover{transform:scale(1.05)}
.mini .i{font-size:24px;margin-bottom:5px}
.btn{display:inline-block;background:linear-gradient(135deg,#ff6b6b,#feca57);color:#fff;padding:15px 40px;border-radius:30px;text-decoration:none;font-weight:bold;box-shadow:0 10px 30px rgba(255,107,107,.4);transition:transform .3s}
.btn:hover{transform:translateY(-3px)}
.link{margin-top:20px;color:#999;font-size:12px}
.link a{color:#667eea;text-decoration:none}
</style>
</head>
<body>
<div class="card">
<div class="emoji">🚫✨</div>
<h1>ちょっと待って!</h1>
<p>セキュリティシステムがリクエストをブロックしました</p>
<div class="cards">
<div class="mini"><div class="i">🤖</div>Bot検出</div>
<div class="mini"><div class="i">🌍</div>地域制限</div>
<div class="mini"><div class="i">⚡</div>レート超過</div>
</div>
<a href="/" class="btn">🏠 ホームへ</a>
<div class="link">正当なアクセスですか?<br><a href="mailto:support@example.com">サポートに連絡</a></div>
</div>
</body>
</html>
その他の設定はすべてデフォルトでWAFを作成します。
ALBのDNS名でアクセス実行
ホワイトリストで自身のIPが許可されている状態なので、アクセスするとALBに設定した固定レスポンスの画面が表示されます。
これもAI産です。最大1024文字までなのでシンプルなコードを入れています。
<!DOCTYPE html><html><head><meta charset=UTF-8><meta name=viewport content=width=device-width,initial-scale=1><style>body{margin:0;font-family:sans-serif;background:#667eea;min-height:100vh;display:flex;justify-content:center;align-items:center}.box{background:white;border-radius:20px;padding:40px;max-width:500px;text-align:center;box-shadow:0 10px 40px black}.icon{font-size:60px;margin-bottom:15px}h1{color:#667eea;font-size:28px;margin:10px 0}p{color:gray;font-size:16px;margin:15px 0}.btn{display:inline-block;background:#667eea;color:white;padding:12px 30px;border-radius:20px;text-decoration:none;font-weight:bold;margin-top:10px}</style></head><body><div class=box><div class=icon>🛠</div><h1>メンテナンス中</h1><p>システムメンテナンスを実施中です</p><p>しばらくお待ちください</p><a href=/ class=btn>🔄 再読み込み</a></div></body></html>
WAFのCloudWatchメトリクスでAllowedRequestsは更新されるがBlockedRequestsは見えないことを確認
アクセスを行うまではメトリクスが見えないかと思いますが、アクセス後少し経過するとコンソールから対象のWAF名で検索すると確認できます。
メトリクスの検索窓にWAF名を入れて確認
赤枠の場所でAllowedRequestsを確認できます。
AllowedRequestsが確認できるが、BlockedRequestsのメトリクスが表示されていないことを確認
今回はこの状態で直接見えないBlockedRequestsを指定してアラームを作成できるか実験します。
CFnでBlockedRequestsのアラーム作成
全体のコードは以下の通りです。
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CloudWatch Alarm for WAF BlockedRequests'
Parameters:
WebACLName:
Type: String
Description: 'Name of the WAF WebACL to monitor'
WebACLId:
Type: String
Description: 'ID of the WAF WebACL'
Region:
Type: String
Description: 'Region of the WAF WebACL'
Default: 'ap-northeast-1'
Rule:
Type: String
Description: 'Rule name to monitor'
Default: 'ALL'
SNSTopicArn:
Type: String
Description: 'ARN of the SNS topic for alarm notifications'
AlarmName:
Type: String
Description: 'Name of the CloudWatch alarm'
Default: 'WAF-BlockedRequests'
Threshold:
Type: Number
Description: 'Number of blocked requests to trigger alarm'
Default: 1
EvaluationPeriods:
Type: Number
Description: 'Number of periods to evaluate'
Default: 1
MinValue: 1
DatapointsToAlarm:
Type: Number
Description: 'Number of datapoints that must be breaching to trigger alarm'
Default: 1
MinValue: 1
Period:
Type: Number
Description: 'Period in seconds for evaluation'
Default: 60
AllowedValues:
- 60
- 300
- 900
- 3600
Statistic:
Type: String
Description: 'Statistic to apply to the metric'
Default: 'Sum'
AllowedValues:
- Sum
- Average
- Maximum
- Minimum
- SampleCount
ComparisonOperator:
Type: String
Description: 'Comparison operator for the alarm'
Default: 'GreaterThanOrEqualToThreshold'
AllowedValues:
- GreaterThanThreshold
- GreaterThanOrEqualToThreshold
- LessThanThreshold
- LessThanOrEqualToThreshold
TreatMissingData:
Type: String
Description: 'How to treat missing data'
Default: 'notBreaching'
AllowedValues:
- notBreaching
- breaching
- ignore
- missing
Resources:
WAFBlockedRequestsAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Ref AlarmName
AlarmDescription: !Sub 'Alarm when WAF BlockedRequests exceeds ${Threshold} in ${Period} seconds. WebACL: ${WebACLName} (ID: ${WebACLId}), Region: ${Region}, Rule: ${Rule}'
MetricName: BlockedRequests
Namespace: AWS/WAFV2
Statistic: !Ref Statistic
Period: !Ref Period
EvaluationPeriods: !Ref EvaluationPeriods
DatapointsToAlarm: !Ref DatapointsToAlarm
Threshold: !Ref Threshold
ComparisonOperator: !Ref ComparisonOperator
TreatMissingData: !Ref TreatMissingData
Dimensions:
- Name: WebACL
Value: !Ref WebACLName
- Name: Region
Value: !Ref Region
- Name: Rule
Value: !Ref Rule
AlarmActions:
- !Ref SNSTopicArn
Outputs:
AlarmName:
Description: 'Name of the created alarm'
Value: !Ref WAFBlockedRequestsAlarm
Export:
Name: !Sub '${AWS::StackName}-AlarmName'
AlarmArn:
Description: 'ARN of the created alarm'
Value: !GetAtt WAFBlockedRequestsAlarm.Arn
Export:
Name: !Sub '${AWS::StackName}-AlarmArn'
コンソール上は見えませんが、取得したいメトリクス名を直接指定します。
パラメータで送信先のトピック名等を指定します。
リソースが作成されたことを確認
特にエラーなくアラームが作成されました!
ではアクセスを失敗させてアラームが本当に発報されるか確認します。
WAFのホワイトリストから自身のIPを削除しアクセスがブロックされる(WAFのカスタムレスポンスが返される)ことを確認
ホワイトリストからIPを削除
再度ALBのDNS名でアクセスを実行
すると先ほどの画面とは異なる表示になりました。
これはWAF側に設定したHTMLなので、きちんとアクセスがブロックされていることがわかります。
アラーム通知がくることを確認
Slackを見ると、以下の通知が来ていました。
ちゃんとアラームアクションが動いていますね!
CloudWatchのコンソールでもアラーム状態に遷移していました。
BlockedRequestsがメトリクス上で見えるようになることを確認
さきほどは見えなかったメトリクスを確認します。
BlockedRequestsがひょこっと現れました。さらにこのメトリクスがアラーム状態であることがわかります。
終わりに
今回はCFnで直指定してアラームの設定ができることを確認したくて検証しました。
ついでにカスタムレスポンス等も設定してこんなにフロント画面を簡単に設定できるようになったことに感動しました。
このブログが何かの役に立てたら幸いです。
最後までお読みいただきありがとうございました!