CloudWatchの複合アラームで、ELB(ALB)の5XXエラーの監視(検知・通知)を、「いい感じ(重複しないように)」にやろうとしたら、うまくいきませんでした。
ついでなので、複合アラームの作り方なども記載してみました。
目次
やりたいこと
- HTTPCode_ELB_[500|502|503|504]_Countを通知したい
- HTTPCode_ELB_5XX_Countは、[500|502|503|504]以外のときに通知したい
- 501,505,561などのときだけ通知する
- 500,502,503,504のときは通知しない
これをCloudWatch複合アラーム(CompositeAlarm)によって、上記のように500,502,503,504のときに、重複しない検知・通知の仕組みを構築します。
また、CloudFormationで構築を行います。
ただし、それが「うまくいかなかったお話」になります。
※実はある方法によって「いい感じ」を実現することができた話を混ぜた内容で登壇したので、本記事の一番下に資料を載せました。(2023/05/29)
前提
ELBにおける、ALB(Application Load Balancer)の5XXエラーを対象としています。
いい感じ? = 背景
ALBの5XX系メトリクス(グラフ)には、以下の2種類があります。
- HTTPCode_Target_5XX_Count
- ターゲットによって生成された HTTP 応答コードの数。
- HTTPCode_ELB_5XX_Count
- ロードバランサーから送信される HTTP 5XX サーバーエラーコードの数。
- ※ターゲット側が原因のこともある。
それぞれの詳細の説明は省略しますが、今回対象にするのは、後者のHTTPCode_ELB_5XX_Countです。
というのもこのELB(ALB)の5XXエラーには、HTTPCode_ELB_5XX_Countというメトリクス以外に、500,502,503,504のときはそれぞれのメトリクスが別で存在します。
- HTTPCode_ELB_500_Count
- HTTPCode_ELB_502_Count
- HTTPCode_ELB_503_Count
- HTTPCode_ELB_504_Count
実際に上記エラーが起きてslackなどに通知する際には、「5XX」よりもそれぞれの具体的な数値で通知してくれた方が便利ですよね。
しかし上記の具体値のメトリクスで通知の設定はするが、それ以外のエラーのときのためにも、「5XX」のメトリクスの方でも通知しておきたくなります(※)。
※上記ステータスコード以外の5XX系のエラーが発生することはあまりないかもしれませんが・・・
具体的には以下公式でALBにおける各エラーコードが記載されています。
ところが「5XX」も通知するようにすると、500,502,503,504を検知したときに、それぞれのメトリクスだけでなく5XXの方のメトリクスでも「重複」して通知が来てしまいます。
それはちょっと嫌だなあ、なんて思った時に、CloudWatchの複合アラーム(CompositeAlarm)という機能で、重複を排除した通知の仕組みを作ってみました。
そして実装してみたら、「想定通りの挙動になりませんでした」。
注意
上記手法では、うまくいった(想定通りの挙動になった)としても、500,2,3,4とそれ以外の5XXエラーが同時(同評価期間内)に発生した場合、5XXメトリクスの方の通知が来ません。(例えば500と501が同時に起きたとき、500の通知だけ来る)
それは実際にエラーが起きた時は結局AWSコンソールのグラフを見に行くと思うのでそれで判別すればいいかなという方向で、頑張らないことにしました。
コード
具体的には、CloudWatch Alarm(CloudWatchアラーム)と、CloudWatch CompositeAlarm(CloudWatch複合アラーム)を使います。
CloudFormationで構築するため、yamlテンプレートを記載します。
※GitHubにもあります。
yaml折り畳み
AWSTemplateFormatVersion: '2010-09-09'
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------#
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "LoadBalancerFullName"
Parameters:
- LoadBalancerFullName
- Label:
default: "SNSTopicArn"
Parameters:
- SNSTopicArn
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
LoadBalancerFullName:
Type: String
SNSTopicArn:
Type: String
# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#
Resources:
# ============================================== #
# HTTPCodeELBOther5XXCount (Composite Alarm)
# ============================================== #
### 5XXがアラーム状態で、500,502,503,504がOK状態のとき(それ以外の5系)に発火
HTTPCodeELBOther5XXCountAlarm:
Type: AWS::CloudWatch::CompositeAlarm
Properties:
AlarmName: ALB-HTTPCodeELBOther5XXCount-Alarm
ActionsEnabled: true
AlarmActions:
- !Ref SNSTopicArn
AlarmRule: !Sub "ALARM(${HTTPCodeELB5XXCountAlarm}) AND NOT (ALARM(${HTTPCodeELB500CountAlarm}) OR ALARM(${HTTPCodeELB502CountAlarm}) OR ALARM(${HTTPCodeELB503CountAlarm}) OR ALARM(${HTTPCodeELB504CountAlarm}))"
# ============================================== #
# HTTPCodeELB5XXCount from metrics
# ============================================== #
HTTPCodeELB5XXCountAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: ALB-HTTPCodeELB5XXCount-Alarm
ActionsEnabled: false ### 500,502,503,504との複合アラームでの通知をするのでこちらではAction=false
AlarmActions:
- !Ref SNSTopicArn
MetricName: HTTPCode_ELB_5XX_Count
Namespace: AWS/ApplicationELB
Dimensions:
- Name: LoadBalancer
Value: !Ref LoadBalancerFullName
Statistic: "Sum"
Period: 300
EvaluationPeriods: 1
DatapointsToAlarm: 1
Threshold: 0
ComparisonOperator: GreaterThanThreshold
TreatMissingData: notBreaching
# ============================================== #
# HTTPCodeELB500Count from metrics
# ============================================== #
HTTPCodeELB500CountAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: ALB-HTTPCodeELB500Count-Alarm
ActionsEnabled: true
AlarmActions:
- !Ref SNSTopicArn
MetricName: HTTPCode_ELB_500_Count
Namespace: AWS/ApplicationELB
Dimensions:
- Name: LoadBalancer
Value: !Ref LoadBalancerFullName
Statistic: "Sum"
Period: 300
EvaluationPeriods: 1
DatapointsToAlarm: 1
Threshold: 0
ComparisonOperator: GreaterThanThreshold
TreatMissingData: notBreaching
# ============================================== #
# HTTPCodeELB502Count from metrics
# ============================================== #
HTTPCodeELB502CountAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: ALB-HTTPCodeELB502Count-Alarm
ActionsEnabled: true
AlarmActions:
- !Ref SNSTopicArn
MetricName: HTTPCode_ELB_502_Count
Namespace: AWS/ApplicationELB
Dimensions:
- Name: LoadBalancer
Value: !Ref LoadBalancerFullName
Statistic: "Sum"
Period: 300
EvaluationPeriods: 1
DatapointsToAlarm: 1
Threshold: 0
ComparisonOperator: GreaterThanThreshold
TreatMissingData: notBreaching
# ============================================== #
# HTTPCodeELB503Count from metrics
# ============================================== #
HTTPCodeELB503CountAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: ALB-HTTPCodeELB503Count-Alarm
ActionsEnabled: true
AlarmActions:
- !Ref SNSTopicArn
MetricName: HTTPCode_ELB_503_Count
Namespace: AWS/ApplicationELB
Dimensions:
- Name: LoadBalancer
Value: !Ref LoadBalancerFullName
Statistic: "Sum"
Period: 300
EvaluationPeriods: 1
DatapointsToAlarm: 1
Threshold: 0
ComparisonOperator: GreaterThanThreshold
TreatMissingData: notBreaching
# ============================================== #
# HTTPCodeELB504Count from metrics
# ============================================== #
HTTPCodeELB504CountAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: ALB-HTTPCodeELB504Count-Alarm
ActionsEnabled: true
AlarmActions:
- !Ref SNSTopicArn
MetricName: HTTPCode_ELB_504_Count
Namespace: AWS/ApplicationELB
Dimensions:
- Name: LoadBalancer
Value: !Ref LoadBalancerFullName
Statistic: "Sum"
Period: 300
EvaluationPeriods: 1
DatapointsToAlarm: 1
Threshold: 0
ComparisonOperator: GreaterThanThreshold
TreatMissingData: notBreaching
解説
前提
パラメータで渡すSNSTopicArn、LoadBalancerFullNameに関してですが、
SNSTopicArnは、アラーム時にアクション(AlarmActions)として通知する、通知先のSNSトピックのARNになります。
LoadBalancerFullNameはロードバランサーのフルネームで、実際にALBの5XXメトリクスをターゲットにするCloudWatch AlarmのDimensions(Name: LoadBalancer)のValueに指定するものです。
CloudFormationでALB(AWS::ElasticLoadBalancingV2::LoadBalancer
)を作成する場合、ALBリソースを定義して、Outputsで!GetAtt (ALBの論理ID).LoadBalancerFullName
で出力できます。
CloudWatch CompositeAlarm
早速、一番の肝である、CloudWatch複合アラーム(AWS::CloudWatch::CompositeAlarm)になります。
複合アラームとは、他のアラームの状態(主に複数)を利用して計算し、その条件がTRUEになるときにアラーム状態になるものです。
### 5XXがアラーム状態で、500,502,503,504がOK状態のとき(それ以外の5系)に発火 HTTPCodeELBOther5XXCountAlarm: Type: AWS::CloudWatch::CompositeAlarm Properties: AlarmName: ALB-HTTPCodeELBOther5XXCount-Alarm ActionsEnabled: true AlarmActions: - !Ref SNSTopicArn AlarmRule: !Sub "ALARM(${HTTPCodeELB5XXCountAlarm}) AND NOT (ALARM(${HTTPCodeELB500CountAlarm}) OR ALARM(${HTTPCodeELB502CountAlarm}) OR ALARM(${HTTPCodeELB503CountAlarm}) OR ALARM(${HTTPCodeELB504CountAlarm}))"
大事なのが複合アラーム特有のパラメータであるAlarmRule
で、これが唯一のCloudWatch Alarmのパラメータとの違いです。
ここに、複数のアラームをもとに状態遷移する、複合アラームのルールを書きます。
ここでは、[500|502|503|504]以外の5XXを通知するアラームにします。 つまり、5XXが発火して、[500|502|503|504]が発火していない状態です。
具体的には以下のようになります。
ALARM(${HTTPCodeELB5XXCountAlarm}) AND NOT ( ALARM(${HTTPCodeELB500CountAlarm}) OR ALARM(${HTTPCodeELB502CountAlarm}) OR ALARM(${HTTPCodeELB503CountAlarm}) OR ALARM(${HTTPCodeELB504CountAlarm}) )
文章にすると、以下のようになります。
- 5XXが発火していて
- 以下のどれでもない(NOT + OR連結)とき
- 500発火
- 502発火
- 503発火
- 504発火
これがうまく動くと、5XXが発火して、[500|502|503|504]が発火していない状態のとき、つまり501などのステータスのときに通知が来ます。
複合アラームのもとの各アラーム
上記のように5XXと、500,2,3,4のアラームを作成します。
500,2,3,4の方は普通のアラームなので省きますが、5XXの方では、ActionsEnabled
をfalseにしています。
HTTPCodeELB5XXCountAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: ALB-HTTPCodeELB5XXCount-Alarm ActionsEnabled: false ### 500,502,503,504との複合アラームでの通知をするのでこちらではAction=false AlarmActions: - !Ref SNSTopicArn
これは、複合アラームであるHTTPCodeELBOther5XXCountAlarm
が発火するときに、もとの5XXのメトリクスのアラームは発火したくないので、falseにしておくことで発火の際のアクションが実行されません。
※AlarmActionsは指定する必要がないのですが、他に合わせて(また気軽にもとに戻せるように)SNSTopicArnを残しています。
しかし・・・
上記のコードでデプロイし、いざしばらく運用してみました。
すると実際にサーバ側(ELB)でエラーが発生し、「HTTPCodeELBOther5XXCountAlarm」の方、つまり「500,2,3,4でない5XX」の方のアラーム通知がslackに来ました。
「うおお、うまくいった」
「でも、500でも502でも503でも504でもないステータスなんてそんな簡単に起こるか・・・?」
という疑問が浮かびながらも、数秒後にさらにslackに通知が来ました。
「504・・・」
504エラーの通知が来てしまいました。
実際にAWSコンソールのCloudWatchのメトリクスを見に行くと、
- 5XXが1件
- 504が1件
でした。
つまり、501など、その他のエラーは起きていませんでした。
考察とその後
事象
いろいろ考えてみたのですが、まず事象としては以下になります。
- 5XX、504が1件ずつで、他の5XXは起きていなかった
- 先にOther5XX通知が来て、そのすぐ後に504通知が来た
考察
まず、Other5XX通知が来たということは、確かにその時は504は発火せず5XXのみ発火していたはずです。
そして、後から504通知が来たということは、「5XXが先にメトリクスにputされて、後から504がputされた(またはputが同時で取得(?)のタイミングがずれた)」ということになります。
評価期間や評価タイミングの問題もあるかもしれませんが、もし発火タイミングとして「5XX -> 504」という内部の仕組みだとすると、毎回この挙動が起きてしまいます。
たまたまだとしても、1回目から起きてしまうと、また起きるんじゃないかとちょっと不安になります。
公式ドキュメントにも
ALARM(CPUUtilizationTooHigh) AND ALARM(DiskReadOpsTooHigh)
ALARM(CPUUtilizationTooHigh) AND NOT ALARM(DeploymentInProgress)
などの例があったりして、似たような条件だしいけるだろうと思っていました。
常時(毎分)メトリクスにputされ続けるものだと判定タイミングが多少ずれても起きづらい・・・?
今回のALBの5XXエラーメトリクスのように、起きたときだけメトリクスにputされるものだと判定タイミングずれが起きやすい・・・?
それともたまたま・・・?
そもそもALBのメトリクスの仕組み上そうなる・・・?
その後
「1回、しかも最初に起きたのであれば、今後も起きる可能性があるかもな」
「回数が少なくても、たまに起きるようでは逆にノイズになるな」
ということで、その後挙動を再現させたり、深く調査してみたりはしませんでした。
評価期間や判定データポイントの数を変えたりするのも考えたのですが、結局タイミング次第で起きたりするんじゃないかなと思い、諦めました。
そして、そっと複合アラームや各具体値のアラームは削除して、5XXのみのアラームに戻してデプロイしました。。。
最後に
ほとんど考察や検証に時間をかけずに即断してしまった(あまり時間がなかった)ので、もしかしたら原因が違ったり、回避策があったり、何かが間違っていたり、実はほとんど起きなかったりするのかもしれません。
でもCloudWatchの複合アラームを使ってみるという経験が踏めたので、まあ良かったかなと思います。
時間があるときにでも再度調査・検証してみたり、もっとちゃんと考察しようかなと思います。
(追記)ついに実現!うまくいきました!
「いい感じ」を実現できた話を混ぜて登壇しました
2023/05/29開催「JAWS-UG SRE支部 #6」にて、実はある方法によって実現することができた話を混ぜて登壇させて頂きました。
Construct Hubに公開した(CDK)
そのソリューションをCDKで構築できるようにし、そのCDKコンストラクトライブラリをConstruct Hubに公開しました。
よかったらぜひご覧下さい。(実現方法の詳細や流れも多少書いています。)