GoogleカレンダーでCloudWatchアラームにダウンタイムを設定する

前置き・概要

計画メンテナンスなど、特定期間だけアラート発報を抑制したい場合があります。

監視ツールには大抵「ダウンタイム」というアラート発報抑制期間を設定する項目があります。
Zabbix の「メンテナンス期間」であれば「毎日xx時からxx時間」「隔週xx曜日」といった柔軟な指定が可能です。

CloudWatchアラームに「ダウンタイム」的な設定項目はありません。
CloudWatch Events + Lambda (or SSM Automation)を利用すれば似たようなことが出来ますが、
アラームアクションを有効化/無効化するタイミングをUTCタイムゾーンのcron形式で表現しなければならず、
前述の監視ツールに比べると分かりやすさ・柔軟性に欠けます。

今回は以下のようなシステムを構築することで、CloudWatchアラームに分かりやすいダウンタイムを設定します。

  1. Google カレンダーに予定を作成し、説明欄にCloudWatchアラーム名を記載
  2. 予定が開始される時、CloudWatchアラームアクションを無効化
  3. 予定が終了する時、CloudWatchアラームアクションを有効化・アラームステータスをOKに変更

ダウンタイム中にALARM状態に変化した場合、その後でアラームアクションを有効化してもSNSへの通知は行われません。
そこで、アラームアクション有効化後、アラームステータスを一旦「OK」の状態にします。
すると、再度ALARM状態に変化した際にSNSへの通知が行われることになります。

前準備

IFTTT サインアップ

ifttt.com にアクセスし、サインアップしておきます。

IFTTT(イフト)はサービス自動連係ツールです。
今回利用するGoogleカレンダー以外にも、Slack や Facebook も利用可能です。

CloudWatchアラーム 作成

ON/OFF の対象となるCloudWatchアラームを作成しておきましょう。

ちなみに、今回実施するのは「アラームの有効/無効化」ではなく「アラームアクションの有効/無効化」です。
アクションの有効/無効が分かるように項目を表示しておきましょう。
※画像は編集を加えています。

以下のように、有効かどうかが確認できます。

ダウンタイム設定システム 構築

AWS

SSMパラメータ

IFTTT の webhook で API Gateway にPOSTする際、HTTP BODYにAPIキー(的なもの)を含めます。
そのキーとSSMパラメータの値が一致すればCloudWatchアラームアクション無効化/有効化を行います。

認証に使う文字列なので、Secure String (安全な文字列) として作成し、暗号化します。
Secure String は CFn をサポートしていないので手動で作成します。

Lambda 周り

Lambda 関数やIAMロールなど、以下の CFnテンプレートで作成可能です。

---
AWSTemplateFormatVersion: 2010-09-09
Description: "Lambda Function and additinal resoureces for CloudWatch Alarm downtime system."

Metadata: 
  "AWS::CloudFormation::Interface": 
    ParameterGroups: 
      - Label: 
          default: "Lambda"
        Parameters: 
          - NameOfFunction
      - Label: 
          default: "SSM Parameter Store"
        Parameters: 
          - NameOfSsmParamForApiKey

    ParameterLabels: 
      NameOfFunction: 
        default: "Function Name"
      NameOfSsmParamForApiKey: 
        default: "Parameter Name"

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  NameOfFunction: 
    Type: String
    Default: set-alarm-downtime

  NameOfSsmParamForApiKey: 
    Type: AWS::SSM::Parameter::Name

Resources:
# ------------------------------------------------------------#
# Disable Alarm Function Resources
# ------------------------------------------------------------#
# IAM Role
  RoleForFunction: 
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      ManagedPolicyArns: 
          - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies: 
        - 
          PolicyName: !Sub "${NameOfFunction}-Policy"
          PolicyDocument: 
            Version: "2012-10-17"
            Statement: 
              - 
                Effect: "Allow"
                Action: 
                  - "ssm:GetParameters"
                  - "kms:Decrypt"
                  - "cloudwatch:DisableAlarmActions"
                  - "cloudwatch:EnableAlarmActions"
                  - "cloudwatch:SetAlarmState"
                Resource: "*"

# Lambda Function
  Function: 
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import boto3
          import json
          import os
          
          ssm = boto3.client('ssm')
          cloudwatch = boto3.client('cloudwatch')
          
          def lambda_handler(event, context):
            http_body_json = event['body']
            http_body_dict = json.loads(http_body_json, strict=False)
            
            # get ssm parameter info, including api key
            ssm_param_info = ssm.get_parameters(
                Names = [
                    os.environ['ssm_param'],
                ],
                WithDecryption = True
            )
            
            # if provided api key matches ssm parameter secure string
            if (ssm_param_info['Parameters'][0]['Value'] == http_body_dict['ApiKey']):
              alarms = http_body_dict['Alarms'].replace(' ', '').split('\n')
              event_type = http_body_dict['EventType']
              # if provided event type is 'start', then disable alarm action(s)
              if (event_type == 'start'):
                response = cloudwatch.disable_alarm_actions(
                    AlarmNames=alarms
                )
                print(response)
              # if provided event type is 'end'
              # then enable alarm action(s) and change state of alarm(s) to 'OK'
              elif (event_type == 'end'):
                response = cloudwatch.enable_alarm_actions(
                    AlarmNames=alarms
                )
                print(response)
                
                for alarm in alarms:
                  response = cloudwatch.set_alarm_state(
                      AlarmName=alarm,
                      StateValue='OK',
                      StateReason='non-alart time ended'
                  )
                  print(response)
              # if provided event type is not 'start' and 'end'
              # then print error message
              else:
                print('Error: invalid event type.')
            # if provided api key does not matche ssm parameter secure string
            # then print error message
            else:
              print('Error: API Key missmatch.')
      Environment: 
        Variables: 
          ssm_param: !Ref NameOfSsmParamForApiKey
      FunctionName: !Ref NameOfFunction
      Handler: index.lambda_handler
      MemorySize: 128
      Role: !GetAtt RoleForFunction.Arn
      Runtime: python3.7
      Timeout: 120
    DependsOn: LogGroupForFunction

# CloudWatch Logs LogGroup
  LogGroupForFunction: 
    Type: AWS::Logs::LogGroup
    Properties: 
      RetentionInDays: 7
      LogGroupName: 
        !Sub "/aws/lambda/${NameOfFunction}"

API Gateway

API Gateway (HTTP API)を作成します。
HTTP API は CFn をサポートしていないので手動作成します。Lambda のコンソールから作成可能です。

API Gateway のエンドポイントを控えておきましょう。

Google カレンダー

新しいカレンダーを作成します。このカレンダーに作成した予定の期間内、CloudWatchアラームアクションが無効化されます。

IFTTT

Googleカレンダー イベント開始時

[Create] でアプレットを作成します。

[This] でトリガー(Google カレンダー)を設定します。

Google Calendar を選択します。

[Any event starts] (=カレンダーで予定が開始された時)を選択します。

先ほど作成したカレンダーを選択し [Create Trigger] します。

次に [That] でアクション(Webhook)を作成します。

Webhooks を選択します。

[Make a web request] を選択します。

次の画面で、いくつか項目を入力して [Create action] を選択します。

URL https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/default/set-alarm-downtime
Method POST
Content Type application/json
Body { “ApiKey”: “xxxxxxxxxx”, “EventType”: “start”, “Alarms”: “{{Description}}” }
  • URL は、API Gateway のURLです。
  • Body の xxxxxxxxxx はAPIキー(的なもの)です。先ほど作成したSSMパラメータの値を入力します。
  • Body の {{Description}} はそのままで大丈夫です。IFTTT で扱える変数で、Google カレンダーの予定の説明欄の内容を表します。

最後にアプレットに名前を付けて完了です。

これで、Google カレンダーで予定が開始された時に API Gateway に HTTP POST されます。
その BODY には json 形式で以下が含まれています。

APIキー(的なもの) この文字列をSSMパラメータの値と比較し、一致しない場合 Lambda はそこで止まります。
イベントタイプ 予定の「開始」または「終了」を表します。
開始の場合、対象のCloudWatchアラームアクションを無効化します。
終了の場合、アラームアクションを有効化しアラームの状態を「OK」に更新します。
予定の説明 Googleカレンダーの予定の説明欄に記載されている文字列です。
今回はここに、有効化/無効化するCloudWatchアラーム名を記載します。

 

Googleカレンダー イベント終了時

イベント開始時と同じ流れで IFTTT のアプレットを作成します。先ほどと異なるのは以下2点です。

  • トリガーとして [Any event ends] (=カレンダーで予定が終了した時) を選択する
  • Webhooks の Body は以下のようにする
{ "ApiKey": "xxxxxxxxxx", "EventType": "end", "Alarms": "{{Description}}" }

ダウンタイム設定システムを試す

試す

Googleカレンダーで、以下のような予定を作成します。設定した期間内、アラームが無効化されます。

それまで有効だったアラームアクションが…

カレンダーの予定開始時間になると、無効化されました。

予定終了時間になるとアクションが有効化され、復旧(OK)状態になります。
(実際に復旧したわけではなく、Lambda の set_alarm_state で一時的に状態を変更しています)

しばらくすると、アラームの状態が本来のものになります。
こうすることで、ダウンタイム期間が終了して尚 継続しているアラートも通知されるようになっています。

「ダウンタイム開始前からNG状態だったアラームが、ダウンタイム終了後に通知されてしまう」
というのが難点、というか、通常の監視ツールで利用できるダウンタイムとの違いです。

アラームアクションがなかなか有効化/無効化されない時

Googleカレンダーの予定が開始/終了されてから Webhooks がトリガーされるまで時間がかかる場合があります。
そんな時はアプレットの Settings を開き、

[Check now] を押すと即時 Webhooks がトリガーされます。