EventBridge、CloudWatch AlarmのアラートをChatworkへ通知するLambdaFunction

EventBridge、CloudWatch Alarmからのイベントを、SNSを介しLambdaFunctionでChatworkに通知してみました
2020.12.14

AWSで発生したアラートをチャットツールに通知するシステムは多いかと思います。 チャットツールがSlackであれば、AWS Chatbotを利用し、作り込みは不要で連携することが可能ですが、他のチャットツールでは少々作りこみが必要です。 今回はLambdaFunctionを利用し、AWSで発生したアラートをChatworkに通知してみたいと思います。

構成図

EventBridge、CloudWatch Alarmにてアラートが発生した際に、SNSを介してLambdaFunctionにてChatworkにメッセージを投稿するような構成です。

00

EventBridgeからは、直接LambdaFunctionを起動することもできますが、SNSを介しています。

SNSサブスクリプションのプロトコルがLambdaFunctionであれば、リージョンをまたいで利用できるので、 GuardDuty等リージョン毎に通知設定が必要なサービスでも、オプトインしているリージョン毎にLambdaFunctionのデプロイが不要になります。(SNSトピック、サブスクリプションは利用する各リージョン毎に必要です。) また、LambdaFunctionからみれば、呼び出し元がSNSだけになるので、ハンドラーに渡されるイベントも統一されコードもシンプルにできると思います。

ちなみに、今回は構成図の赤枠部分を構築するSAMテンプレートを用意しています。

このテンプレートで構築される環境の説明をしつつ、動作検証の様子をお届けしたいと思います。

前提

本エントリでは以下の環境を利用しています。

構築

SAMテンプレート/デプロイ

冒頭での説明の通り、SAMのテンプレートファイルはこちらに格納しています。今回のSAMプロジェクトディレクトリは以下のような構成となります。

notify-chatwork/
  ├── functions/
  │   ├── lambda_function.py   #LambdaFunctionのコード
  │   └── requirements.txt     #sam buildで利用
  └──template.yml              #AWSリソースを定義するSAMテンプレート

今回のLambdaFunctionでは、標準ライブラリには含まれていない、 HTTPライブラリrequestsを利用していますので、requirements.txtにパッケージ名を設定しています。

まずは、SAMプロジェクトディレクトリのトップで、sam buildを実行します。 利用するパッケージの依存関係を処理し、デプロイ用のアーティファクトを作成しています。

notify-chatwork $ sam build
Building codeuri: functions/ runtime: python3.8 metadata: {} functions: ['LambdaFunction']
Running PythonPipBuilder:ResolveDependencies
Running PythonPipBuilder:CopySource

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided

notify-chatwork $

sam deployコマンドで環境にデプロイします。 初回実行の際はSAM CLI構成ファイル(samconfig.toml)が存在しないので、sam deploy --guidedでデプロイを実施してください。詳細については、以下を参考にしてください。

コマンドを実行すると、パラメータの入力が求められますので以下を参考に指定ください。

  • Stack Name … AWS SAMによって作成されるCFnスタック名(任意の名称)
  • AWS Region … CFnスタックを作成するリージョン
  • Parameter … SAMテンプレートで定義しているパラメータ
    • SysName … リソースに付与されるプレフィックス
    • Env … 環境名
    • SubscriptionEmailEndpoint … LambdaFunctionのエラーを通知するメールアドレス
    • ChatworkApikey … Chatwork APIサービスを利用するためのアクセスキー
    • ChatworkRoomid … メッセージを投稿するルームID
    • ChatworkUserID … メッセージの宛先(To)となるユーザID
    • ChatworkEndpoint … 現時点ではhttps://api.chatwork.com/v2を指定
    • ChatworkHeader … 現時点ではX-ChatWorkTokenを指定

以下、sam deployコマンドを実行した画面です。少々長いので畳んでいます。

sam deployの実行
notify-chatwork $ sam deploy --guided

Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Not found

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [sam-app]: test-dev-lambda-notify-chatwork
        AWS Region [us-east-1]: ap-northeast-1
        Parameter SysName [test]: test
        Parameter Env [prd]: dev
        Parameter SubscriptionEmailEndpoint []: xxxxx@gmail.com
        Parameter ChatworkApikey: 
        Parameter ChatworkRoomid: 
        Parameter ChatworkUserID []: 1111111
        Parameter ChatworkEndpoint [https://api.chatwork.com/v2]: 
        Parameter ChatworkHeader [X-ChatWorkToken]: 
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [y/N]: y
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: n
        Capabilities [['CAPABILITY_IAM']]: CAPABILITY_NAMED_IAM
        Save arguments to configuration file [Y/n]:  
        SAM configuration file [samconfig.toml]: 
        SAM configuration environment [default]: 

        Looking for resources needed for deployment: Found!

                Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-1serkyaxr8xb6
                A different default S3 bucket can be set in samconfig.toml

        Saved arguments to config file
        Running 'sam deploy' for future deployments will use the parameters saved above.
        The above parameters can be changed by modifying samconfig.toml
        Learn more about samconfig.toml syntax at 
        https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html
Uploading to test-dev-lambda-notify-chatwork/582217bf66e53f8eed410eed9128a517  539161 / 539161.0  (100.00%)

        Deploying with following values
        ===============================
        Stack name                   : test-dev-lambda-notify-chatwork
        Region                       : ap-northeast-1
        Confirm changeset            : True
        Deployment s3 bucket         : aws-sam-cli-managed-default-samclisourcebucket-1serkyaxr8xb6
        Capabilities                 : ["CAPABILITY_NAMED_IAM"]
        Parameter overrides          : {'SysName': 'test', 'Env': 'dev', 'SubscriptionEmailEndpoint': 'xxxxx@gmail.com', 'ChatworkApikey': 'xxxxxxxxxxxxxxxx', 'ChatworkRoomid': '1111111', 'ChatworkUserID': '1111111', 'ChatworkEndpoint': 'https://api.chatwork.com/v2', 'ChatworkHeader': 'X-ChatWorkToken'}
        Signing Profiles           : {}

Initiating deployment
=====================
Uploading to test-dev-lambda-notify-chatwork/2b0676c9bcb0bfca8d4f99cb96a98bb3.template  2787 / 2787.0  (100.00%)

Waiting for changeset to be created..

CloudFormation stack changeset
-----------------------------------------------------------------------------------------------------------------
Operation                    LogicalResourceId            ResourceType                 Replacement                
-----------------------------------------------------------------------------------------------------------------
+ Add                        LambdaFunctionErrorsAlarm    AWS::CloudWatch::Alarm       N/A                        
+ Add                        LambdaFunctionRole           AWS::IAM::Role               N/A                        
+ Add                        LambdaFunctionSnsTopic       AWS::SNS::Topic              N/A                        
+ Add                        LambdaFunction               AWS::Lambda::Function        N/A                        
-----------------------------------------------------------------------------------------------------------------

Changeset created successfully. arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:changeSet/samcli-deploy1607912002/c35a3e66-3cb3-4641-9204-d7313c5cbf48


Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y

2020-12-14 11:14:11 - Waiting for stack create/update to complete

CloudFormation events from changeset
-----------------------------------------------------------------------------------------------------------------
ResourceStatus               ResourceType                 LogicalResourceId            ResourceStatusReason       
-----------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS           AWS::SNS::Topic              LambdaFunctionSnsTopic       -                          
CREATE_IN_PROGRESS           AWS::IAM::Role               LambdaFunctionRole           -                          
CREATE_IN_PROGRESS           AWS::SNS::Topic              LambdaFunctionSnsTopic       Resource creation          
                                                                                       Initiated                  
CREATE_IN_PROGRESS           AWS::IAM::Role               LambdaFunctionRole           Resource creation          
                                                                                       Initiated                  
CREATE_COMPLETE              AWS::SNS::Topic              LambdaFunctionSnsTopic       -                          
CREATE_COMPLETE              AWS::IAM::Role               LambdaFunctionRole           -                          
CREATE_IN_PROGRESS           AWS::Lambda::Function        LambdaFunction               -                          
CREATE_COMPLETE              AWS::Lambda::Function        LambdaFunction               -                          
CREATE_IN_PROGRESS           AWS::Lambda::Function        LambdaFunction               Resource creation          
                                                                                       Initiated                  
CREATE_IN_PROGRESS           AWS::CloudWatch::Alarm       LambdaFunctionErrorsAlarm    -                          
CREATE_COMPLETE              AWS::CloudWatch::Alarm       LambdaFunctionErrorsAlarm    -                          
CREATE_IN_PROGRESS           AWS::CloudWatch::Alarm       LambdaFunctionErrorsAlarm    Resource creation          
                                                                                       Initiated                  
CREATE_COMPLETE              AWS::CloudFormation::Stack   test-dev-lambda-notify-      -                          
                                                          chatwork                                                
-----------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - test-dev-lambda-notify-chatwork in ap-northeast-1

デプロイが正常終了すると、CloudFormationにスタックが作成されていることが確認できます。

LambdaFunction

SAMテンプレートで構築されるLambdaFunctionについて簡単に説明します。

ChatworkのAPI呼び出し等に必要な情報については、変更がしやすいよう環境変数に設定しています。設定される値はsam deploy時に指定した値となります。

SNSからLambdaFunctionに渡されるイベントの形式は、以下ドキュメントに記載があります。今回はイベントデータをロギングしていますので、実際に渡された値についてはCloudWatch Logsから確認することが可能です。

ここでは、イベントデータよりSubjectMessageをピックアップし、Chatworkへの投稿メッセージを生成しています。 メッセージを投稿後は、レスポンスのステータスコードを判定し、異常が発生した際はLambdaFunctionで例外をスローし、Cloudwatch Alarmで検知できるようにしています。

その他、処理内容についてはコード内のコメントを参照ください。

import logging
import requests
import json
import os

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    chatwork_apikey = os.environ['CHATWORK_APIKEY']     # 環境変数よりChatoworkAPIキー取得
    chatwork_roomid = os.environ['CHATWORK_ROOMID']     # 環境変数よりChatoworkルームID取得
    chatwork_endpoint = os.environ['CHATWORK_ENDPOINT'] # 環境変数よりChatoworkエンドポイント取得
    chatwork_header = os.environ['CHATWORK_HEADER']     # 環境変数よりChatoworkリクエストヘッダ取得

    # ハンドラーに渡されたイベントデータをロギング
    logger.info("EVENT: " + json.dumps(event))

    # イベントデータの一部を取得(Chatworkのメッセージに利用)
    subject = event['Records'][0]['Sns']['Subject']
    message = event['Records'][0]['Sns']['Message']

    # Chatworkに改行含めてjsonデータを連携するため文字列を辞書型に変換
    dict_message=json.loads(message)
    # indentオプション指定し改行等を保った状態でJSON形式にエンコード
    json_message = json.dumps(dict_message, indent=2)

    # EventBridge経由のイベントはsubjectが設定されていないのでメッセージ部より取得
    if subject is None:
        subject = dict_message['detail-type']

    # Chatwork指定のリクエストヘッダにAPIトークンを設定(リクエスト仕様)
    headers = {chatwork_header: chatwork_apikey}

    # SNSイベントよりChatworkメッセージ生成(インフォメーション + タイトル、絵文字無変換)
    chatwork_message = '[info][title]{0}[/title][/info]'.format(subject,json_message)
    payload = {'body': chatwork_message}

    # Chatwork APIを利用するためのURL
    url = '{0}/rooms/{1}/messages'.format(chatwork_endpoint,chatwork_roomid)

    # Chatworkに投稿
    # paramsパラメータでのペイロードを指定するとクエリストリングとなり、Chatwork API(CloudFrontのURL最大長)の制限に抵触する恐れがあるのでdataパラメータを利用
    response = requests.post(url,headers=headers,data=payload)

    # ステータスコードを判定してロギング
    if response.status_code == requests.codes.ok:
        logger.info("Status Code :" + str(response.status_code) + " Response Header:" + json.dumps(dict(response.headers)))
        logger.info("Message posted to:" + str(chatwork_message))
        return {
            'statusCode': 200,
        }
    else:
        logger.error("Status Code :" + str(response.status_code) + " Response Header:" + json.dumps(dict(response.headers)))
        raise Exception

動作確認

Cloudwatch Alarm

Cloudwatch Alarmで発生したアラートを、Chatworkに通知してみたいと思います。

動作確認の前提として、構成図赤枠外にあるCloudwatch Alarmおよび、SNSが必要になります。

SNS

サブスクリプションに先程のSAMテンプレートで作成されたLambdaFunctionを指定した、SNSトピックです。

Cloudwatch Alarm

通知先を上述のSNS(LambdaFunctionサブスクリプション)に設定したCloudwatch Alarmです。動作確認のため、ここでは任意メトリクスをピックアップして設定しています。

意図的にCloudwatch Alarmのステータスが変わる状況をつくり、アラートを発生させました。

想定どおり、Chatworkにメッセージが投稿されました。

一部マスクしていますが、投稿されたメッセージをテキストでも貼っておきます。(畳んでいます)

Chatoworkへ投稿されたメッセージ
{
  "AlarmName": "test-dev-alarm-xxx",
  "AlarmDescription": null,
  "AWSAccountId": "XXXXXXXXXXXX",
  "NewStateValue": "ALARM",
  "NewStateReason": "Threshold Crossed: 1 out of the last 1 datapoints [1.0 (14/12/20 04:02:00)] was greater than the threshold (0.0) (minimum 1 datapoint for OK -> ALARM transition).",
  "StateChangeTime": "2020-12-14T04:07:20.050+0000",
  "Region": "Asia Pacific (Tokyo)",
  "AlarmArn": "arn:aws:cloudwatch:ap-northeast-1:XXXXXXXXXXXX:alarm:test-dev-alarm-xxx",
  "OldStateValue": "OK",
  "Trigger": {
    "MetricName": "ExecutionsFailed",
    "Namespace": "AWS/States",
    "StatisticType": "Statistic",
    "Statistic": "MAXIMUM",
    "Unit": null,
    "Dimensions": [
      {
        "value": "arn:aws:states:ap-northeast-1:XXXXXXXXXXXX:stateMachine:test",
        "name": "StateMachineArn"
      }
    ],
    "Period": 300,
    "EvaluationPeriods": 1,
    "ComparisonOperator": "GreaterThanThreshold",
    "Threshold": 0.0,
    "TreatMissingData": "- TreatMissingData:                    notBreaching",
    "EvaluateLowSampleCountPercentile": ""
  }
}

EventBridge

EventBridgeから発行されるイベントを、Chatworkに通知してみたいと思います。

SNS

先程のCloudwatch Alarmの動作確認で利用したものと、同様のSNSトピックを利用します。

EventBridge

今回はAccess Analyzerのチェック結果を検知するルールにしています。

意図的に、Access Analyzerのイベントを発生(ここでは、他のアカウントにS3バケットのアクセス許可を付与、Access Analyzerの即時チェック実施)させ、 ターゲット(SNS)にイベントが送信されると、想定どおりChatworkにメッセージが投稿されました。

一部マスクしていますが、投稿されたメッセージをテキストでも貼っておきます。(畳んでいます)

Chatoworkへ投稿されたメッセージ
{
  "version": "0",
  "id": "c661b441-fd5b-2c0c-05ba-39de40720621",
  "detail-type": "Access Analyzer Finding",
  "source": "aws.access-analyzer",
  "account": "XXXXXXXXXXXX",
  "time": "2020-12-14T05:46:52Z",
  "region": "ap-northeast-1",
  "resources": [
    "arn:aws:access-analyzer:ap-northeast-1:XXXXXXXXXXXX:analyzer/ConsoleAnalyzer-ecdeb962-2e85-4110-a84f-f0c7d05702e9"
  ],
  "detail": {
    "version": "1.0",
    "id": "085d9073-f563-4da5-8b81-85d2066059ab",
    "status": "ACTIVE",
    "resourceType": "AWS::S3::Bucket",
    "resource": "arn:aws:s3:::test-prd-artifacts",
    "createdAt": "2020-12-13T00:33:13.579Z",
    "analyzedAt": "2020-12-14T05:46:50.948Z",
    "updatedAt": "2020-12-14T05:46:50.948Z",
    "accountId": "XXXXXXXXXXXX",
    "region": "ap-northeast-1",
    "principal": {
      "AWS": "arn:aws:iam::xxxxxxxxxxxx:user/test-user"
    },
    "action": [
      "s3:AbortMultipartUpload",
      "s3:DeleteBucket",
      "s3:DeleteBucketWebsite",
      "s3:DeleteObject",
      "s3:DeleteObjectTagging",
      "s3:DeleteObjectVersion",
      "s3:DeleteObjectVersionTagging",
      "s3:GetAccelerateConfiguration",
      "s3:GetAnalyticsConfiguration",
      "s3:GetBucketAcl",
      "s3:GetBucketCORS",
      "s3:GetBucketLocation",
      "s3:GetBucketLogging",
      "s3:GetBucketNotification",
      "s3:GetBucketObjectLockConfiguration",
      "s3:GetBucketPolicyStatus",
      "s3:GetBucketPublicAccessBlock",
      "s3:GetBucketRequestPayment",
      "s3:GetBucketTagging",
      "s3:GetBucketVersioning",
      "s3:GetBucketWebsite",
      "s3:GetEncryptionConfiguration",
      "s3:GetInventoryConfiguration",
      "s3:GetLifecycleConfiguration",
      "s3:GetMetricsConfiguration",
      "s3:GetObject",
      "s3:GetObjectAcl",
      "s3:GetObjectLegalHold",
      "s3:GetObjectRetention",
      "s3:GetObjectTagging",
      "s3:GetObjectTorrent",
      "s3:GetObjectVersion",
      "s3:GetObjectVersionAcl",
      "s3:GetObjectVersionForReplication",
      "s3:GetObjectVersionTagging",
      "s3:GetObjectVersionTorrent",
      "s3:GetReplicationConfiguration",
      "s3:ListBucket",
      "s3:ListBucketByTags",
      "s3:ListBucketMultipartUploads",
      "s3:ListBucketVersions",
      "s3:ListMultipartUploadParts",
      "s3:ObjectOwnerOverrideToBucketOwner",
      "s3:PutAccelerateConfiguration",
      "s3:PutAnalyticsConfiguration",
      "s3:PutBucketAcl",
      "s3:PutBucketCORS",
      "s3:PutBucketLogging",
      "s3:PutBucketNotification",
      "s3:PutBucketObjectLockConfiguration",
      "s3:PutBucketPublicAccessBlock",
      "s3:PutBucketRequestPayment",
      "s3:PutBucketTagging",
      "s3:PutBucketVersioning",
      "s3:PutBucketWebsite",
      "s3:PutEncryptionConfiguration",
      "s3:PutInventoryConfiguration",
      "s3:PutLifecycleConfiguration",
      "s3:PutMetricsConfiguration",
      "s3:PutObject",
      "s3:PutObjectAcl",
      "s3:PutObjectLegalHold",
      "s3:PutObjectRetention",
      "s3:PutObjectTagging",
      "s3:PutObjectVersionAcl",
      "s3:PutObjectVersionTagging",
      "s3:PutReplicationConfiguration",
      "s3:ReplicateDelete",
      "s3:ReplicateObject",
      "s3:ReplicateTags",
      "s3:RestoreObject"
    ],
    "condition": {},
    "isDeleted": false,
    "isPublic": false
  }
}

LambdaFunctionエラー

Chatworkへのメッセージ投稿に失敗した場合の動作を確認してみます。

通知に利用するリソースは、SAMテンプレートで作成されたリソースとなります。

SNS

EメールをサブスクリプションとするSNSトピックです。LambdaFunction自体のエラーを通知するため、Chatwork通知のSNSは利用せずメール通知としています。

Cloudwatch Alarm

SAMテンプレートで作成したLambdaFunctionのErrorsメトリクスを監視、 通知先は上述のSNS(Eメールサブスクリプション)に設定したCloudwatch Alarmです。

動作確認のため、一時的にLambdaFunctionの環境変数(ChatworkHeader)を指定外のものに変更しメッセージ投稿を失敗させました。

Cloudwatch Alarmのステータスが変わり、メール通知が行われました。

上記のようなメールを受信した際は、Chatworkへのメッセージ投稿が失敗していることになりますので、LambdaFunctionが出力したCloudWatch Logs等を参考に原因調査が必要になります。

さいごに

Chatworkにメッセージを投稿するLambdaFunctionを作成し、動作を確認してみました。なお、ChatworkのAPIドキュメントにも記載されていますが、APIのバージョンアップにてURIが変更されるようなので、LambdaFunctionへの影響を確認し、改修をお願いいたします。

参考