Serverless Framework + Lambda(Python) を使ってChatworkにCloudWatchのアラートを投稿

chatwork

どうも!AWS歴、数週間の西村祐二@大阪です。

AWSの勉強のために
EC2で発生した障害をCloudWatchで検知、監視結果をLambdaのトリガーに設定したSNSを経由して
Chatworkに投稿するというのをserverless frameworkを使ってやってみます。

あまりAWSに触れたことのない方の参考になれば幸いです。

概要図

スクリーンショット 2017-05-15 13.49.31

ゴール

障害検知したらChatworkにアラート通知が来るようにします。

スクリーンショット_2017-05-15_16_42_57

事前準備

Serverless Frameworkの環境構築

以下のページがとても参考になります。

今から始めるServerless Frameworkで簡単Lambda開発環境の構築

ChatworkのAPIトークン、ルームIDの取得

以下のページがとても参考になります。

Pythonを使ってChatWorkに発言してみた

環境

Serverless Framework 1.12.1
EC2 t2.micro Amazon Linux

Serverless Frameworkを用いたデプロイ

Serverless Frameworkを使って、外部モジュール等を含めたLambda関数のアップロードなど行います。

サービス(作業ディレクトリ)を作成

$ sls create -t aws-python3 -p chatwork-cloudwatch
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/nishimura.yuji/work/lambda/blog/chatwork-cloudwatch"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.12.1
 -------'

Serverless: Successfully generated boilerplate for template: "aws-python3"

サービス名は「chatwork-cloudwatch」として作成し、
「aws-python3」と指定することでpython3.6でサービスが作成されます。

完了後、以下ファイルが作成されます。

$ cd chatwork-cloudwatch
$ tree .
.
├── handler.py
└── serverless.yml

0 directories, 2 files

serverless.ymlの設定

デプロイ時に設定したい項目はserverless.ymlを編集することで設定できます。
今回は下記のように設定しました。
コメント部分は省いております。

service: chatwork-cloudwatch

provider:
  name: aws
  runtime: python3.6
  region: ap-northeast-1
  profile: sls

  environment:
    CHATWORK_API_KEY: xxxxxxxxxxxxxxxxxxxxxx
    CHATWORK_HEADER: X-ChatWorkToken
    CHATWORK_ROOM_ID: xxxxxxxxxxxxx
    CHATWORK_URL: https://api.chatwork.com/v2

plugins:
  - serverless-python-requirements


functions:
  dispatcher: # file_name
    handler: dispatcher.dispatch # file_name.functions_name
    events:
      - sns: sls-cloudwatch # topics_name

各ブロックごとに説明

サービ作成時に設定した名前が設定されます。

service: chatwork-cloudwatch


サービス作成時に 「aws-python3」とするとpython3.6が設定されます。
regionは初期設定だと「us-east-1」になっているので、「ap-northeast-1」の東京リージョンに変更しておきます。
使いたいprofileも設定できます。

provider:
  name: aws
  runtime: python3.6
  region: ap-northeast-1
  profile: sls


デプロイしたときに環境変数として設定できます。 APIキーなどはプログラムの中に直書きするのが嫌だったので、こちらに記載しました。 セキュリティを高めるのならKMSで暗号化して、Lambda実行時に復号化するのがよいかと思います。 KMSの設定については今回は割愛させていただきます。

  environment:
    CHATWORK_API_KEY: xxxxxxxxxxxxxxxxxxxxxx
    CHATWORK_HEADER: X-ChatWorkToken
    CHATWORK_ROOM_ID: xxxxxxxxxxxxx
    CHATWORK_URL: https://api.chatwork.com/v2


外部モジュールを含めてデプロイするときの設定です。

plugins:
  - serverless-python-requirements

以下のページがとても参考になります。

Serverless Frameworkのプラグインを利用した外部モジュールの管理

デプロイすると自動的にSNSをトリガーとして設定してくれます。
今回トピックス名は「sls-cloudwatch」として設定しております。
公式のドキュメントを参考に設定したので、はじめに作成されたものから、
ファイル名を「handler.py」→「dispatcher.py」に、 関数名を「hello」→「dispatch」に変更する必要があります。

公式ドキュメント https://serverless.com/framework/docs/providers/aws/events/sns/

functions:
  dispatcher: # file_name
    handler: dispatcher.dispatch # file_name.functions_name
    events:
      - sns: sls-cloudwatch # topics_name

PythonでLambda関数作成

$ cp -ip handler.py dispatcher.py 
$ vi dispatcher.py

import json
import logging
import requirements
import requests
import urllib
import os


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

message_header  = "============ Alart ===========\n"

def dispatch(event, context):
    logger.info("Event: " + str(event))
    message = json.loads(event['Records'][0]['Sns']['Message'])
    logger.info("Message: " + str(message))

    # 環境変数から情報取得
    CHATWORK_API_KEY = os.environ['CHATWORK_API_KEY']
    CHATWORK_HEADER = os.environ['CHATWORK_HEADER']
    CHATWORK_ROOM_ID = os.environ['CHATWORK_ROOM_ID']
    CHATWORK_URL = os.environ['CHATWORK_URL']

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


    # メッセージ取得
    chatwork_message = get_cloudwatch_message(message)
    payload = {'body': chatwork_message}
    headers = {CHATWORK_HEADER: CHATWORK_API_KEY}

    try:
        # Chatworkに投稿
        requests.post(URL, headers=headers, params=payload)
    except:
        logger.info("Failed! Message : \n" + str(chatwork_message))
    else:
        logger.info("Success! Message posted to: \n" + str(chatwork_message))


def get_cloudwatch_message(data):
    logger.info("get data: " + str(data))

    # 渡ってきたデータからほしい情報を抽出
    alarm_name = data['AlarmName']
    new_state = data['NewStateValue']
    reason = data['NewStateReason']
    metric_name = data['Trigger']['MetricName']
    target_name = data['Trigger']['Dimensions'][0]['name']
    target_value = data['Trigger']['Dimensions'][0]['value']

    # Chatworkに投稿したいメッセージ
    chatwork_message = '{0}\n\
    アラーム名:{1} \n\
    new_state:{2} \n\
    reason:{3} \n\
    metric_name:{4} \n\
    target_name:{5} \n\
    target_value:{6}'.format(message_header,alarm_name,new_state,reason,metric_name,target_name,target_value)
    return chatwork_message

参考:SNSからLambdaに渡ってくるデータの例(eventに入ってくるデータ)

{
  'Records': [
    {
      'EventSource': 'aws:sns',
      'EventVersion': '1.0',
      'EventSubscriptionArn': 'arn:aws:sns:ap-northeast-1:xxxxxxxxxx',
      'Sns': {
        'Type': 'Notification',
        'MessageId': 'xxxxxxxxxx',
        'TopicArn': 'arn:aws:sns:ap-northeast-1:xxxxxxxxxx',
        'Subject': 'INSUFFICIENT_DATA: "xxxxxxxxxx" in Asia Pacific - Tokyo',
        'Message': '
        {
          "AlarmName":"xxxxxxxxxx",
          "AlarmDescription":"Created from EC2 Console",
          "AWSAccountId":"xxxxxxxxxx",
          "NewStateValue":"INSUFFICIENT_DATA",
          "NewStateReason":"Insufficient Data: 1 datapoint was unknown.",
          "StateChangeTime":"2017-05-12Txx:xx:xx.573+0000",
          "Region":"Asia Pacific - Tokyo",
          "OldStateValue":"OK",
          "Trigger":
          {
            "MetricName":"xxxxxxxxxx",
            "Namespace":"AWS/EC2",
            "StatisticType":"Statistic",
            "Statistic":"MAXIMUM",
            "Unit":null,
            "Dimensions":[
              {
                "name":"InstanceId",
                "value":"i-xxxxxxxxxx"
              }],
                "Period":60,
                "EvaluationPeriods":1,
                "ComparisonOperator":"GreaterThanThreshold","Threshold":5.0,
                "TreatMissingData":"","EvaluateLowSampleCountPercentile":""
          }

        }',
        'Timestamp': '2017-05-12Txx:xx:xx.612Z',
        'SignatureVersion': '1',
        'Signature': 
           ・・・・

外部モジュールを含めてデプロイするための準備

外部モジュールを含めてデプロイするために
「requirements.txt」「requirements.py」を作成します。

以下のページがとても参考になります。

Serverless Frameworkのプラグインを利用した外部モジュールの管理

$ vi requirements.txt
requests
$ vi requirements.py
import os
import sys


requirements = os.path.join(
    os.path.split(__file__)[0],
    '.requirements',
)

if requirements  not in sys.path:
    sys.path.append(requirements)

デプロイ

下記コマンドにて外部モジュール含めたデプロイができます。

$ sls deploy -v

Lambdaの実行ロール

Lambdaから他AWSリソースにアクセスしないため既存のロールで問題ないかと思います。

マネージメントコンソールにてデプロイ状況確認

Lambda関数の箇所に「chatwork-cloudwatch-dev-dispatcher」というLambda関数が作成されています。

スクリーンショット 2017-05-15 15.49.37

「chatwork-cloudwatch-dev-dispatcher」を選択し環境変数を確認すると
設定したとおりの値が入っていることがわかります。

スクリーンショット_2017-05-15_15_51_09

トリガー設定もSNSが設定されており、トピックス名が「sls-cloudwatch」とserverless.ymlで設定したものが作成・設定されていることがわかります。

スクリーンショット_2017-05-15_15_50_34

EC2にてインスタンスステータスのCloudWatchアラーム設定

EC2の「モニタリング」タブからアラームの作成ボタンを
クリックするとアラームの編集ウインドウが表示されます。
そこで、通知の送信先を「sls-cloudwatch」、
アラーム検知の条件を「ステータスチェックに失敗(インスタンス)」、
アラーム名を「status_check」として設定し保存します。

スクリーンショット 2017-05-15 16.26.09

意図的に障害発生

下記、ページを参考にEC2にてインターフェイスをダウンさせる

EC2にログイン
$ sudo ifconfig eth0 down

【AWS】EC2のヘルスチェックを意図的に失敗させる

しばらく待つと
アラームのステータスが「OK」→「アラーム」に変化します。

スクリーンショット 2017-05-15 16.47.23

Chatworkに通知

 うまく動作すると下記のようにLambdaによって、アラート内容をChatworkに通知してくれます。   スクリーンショット_2017-05-15_16_42_57

まとめ

いかがだったでしょうか。
Serverless Framework + Lambda(Python) を使ってCloudWatchのアラートをChatworkに投稿する仕組みを作ってみました。

Lambdaを使って何かをやろうと思うと付随して他のAWSサービスを利用することになるので
とても勉強になりました。
あまりAWSに触れたことがない方のAWSに触れるきっかけや参考になれば幸いです。

また、SNSからどのようなデータが渡ってくるか分かれば、安価で様々な監視ができる仕組みが作れるかと思います。

今回は予め用意されている監視設定を利用しましたが、
次はカスタムメトリクスを使って検知した内容を通知する仕組みを作成したいと思います。