CloudWatch複合アラームでELBの5XXをいい感じに検知しようとしたらうまくいかなかった話

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における各エラーコードが記載されています。

docs.aws.amazon.com


ところが「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にもあります。

github.com


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」にて、実はある方法によって実現することができたを混ぜて登壇させて頂きました。

speakerdeck.com


Construct Hubに公開した(CDK)

そのソリューションをCDKで構築できるようにし、そのCDKコンストラクトライブラリをConstruct Hubに公開しました。

よかったらぜひご覧下さい。(実現方法の詳細や流れも多少書いています。)

go-to-k.hatenablog.com