AWS Lambda関数の実行エラーをZabbix Serverに通知してみた

#はじめに
運用監視系をZabbixで一元管理していることもあり、AWS Lambda関数の実行エラーもZabbix Serverへ通知したかったのでやってみました。

#環境

- Zabbix Server Version: 3.0.7
- AWS Lambda Runtime: Python2.7

#Zabbix ServerにHost/Item/Triggerを追加
##Host追加
[設定] > [ホスト] > [ホストの作成]
###「ホスト」タブ
- ホスト名: lambda-trapper
- 新規グループ作成: Lambda
- エージェントのインターフェース
    - IPアドレス: 127.0.0.1 (デフォルト)
    - 接続方法: IPアドレス (デフォルト)
    - ポート: 10050 (デフォルト)
- 有効: On
- その他項目: 空欄

##Item追加
[設定] > [ホスト] > ホスト一覧のlambda-trapperの[アイテム] > [アイテムの作成]

- 名前: ErrorLog
- タイプ: Zabbixトラッパー
- キー: errorlog
- データ型: 文字列
- ヒストリ保存期間(日): 90 (デフォルト)
- 値のマッピングの使用: なし (デフォルト)
- アプリケーションの作成: Logs
- ホストインベントリフィールドの自動設定: なし (デフォルト)
- 有効: On
- その他項目: 空欄

##Trigger追加
[設定] > [ホスト] > ホスト一覧のlambda-trapperの[トリガー] > [トリガーの作成]
###「トリガー」タブ
- 名前: Lambda function execution error
- 条件式: {lambda-trapper:errorlog.str(example)}=0 and {lambda-trapper:errorlog.nodata(30)}=0
- 障害イベントを継続して生成: On
- 深刻度: 重度の障害
- 有効: On
- その他項目: 空欄

#AWS Lambda関数にロギング処理追加
既存のAWS Lambda関数（実行エラーをトラップしたい関数）に、以下のようにロギング処理を追加します。

##コード
```python:lambda_function.py
# (省略)

import logging

# (省略)

#ロガー初期化
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    try:

        # (省略)

        logger.info("Execution succeeded.")
    except Exception, e:
        logger.error(e)
        raise(e)    #PythonのスタックトレースをCloudWatchログへ記録させたい場合に記載
```

ロギング処理を追加することで、AWS Lambda関数の実行結果がCloudWatchに出力されるようになります。
正常終了ログの例）

```
[INFO]	2017-03-01T08:18:31.149Z	a7afba37-fe57-11e6-a428-371557c5f4e7	Execution succeeded.
```

異常終了ログの例）

```
[ERROR]	2017-03-01T09:38:37.966Z	d8c7ede3-fe62-11e6-b58a-7dce3a95f14a	cannot concatenate 'str' and 'int' objects
```

#Zabbix送信用のAWS Lambda関数を作成
CloudWatchからKickされるZabbix送信用のAWS Lambda関数を新しく作成します。
ちなみにZabbixSenderモジュールは、「[ここ](http://blog.father.gedow.net/2015/12/08/aws-lambda-python-send-metric-value-to-zabbix/)」で公開されていたコードを流用させて頂きました。

##AWS Lambda関数名
cwl-to-zabbix

##コード
```python:handler.py
import os
import json
from StringIO import StringIO
import base64
import gzip
from ZabbixSender import ZabbixSender
import logging

ZBX_SERVER = os.environ['ZBX_SERVER']
ZBX_HOST = os.environ['ZBX_HOST']
ZBX_ITEM = os.environ['ZBX_ITEM']
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def sender(event, context):
    log_b64 = event['awslogs']['data']
    log_gz = StringIO(base64.b64decode(log_b64))
    jsonData = json.load(gzip.GzipFile(fileobj=log_gz, mode='rb'))
    status = jsonData['logEvents'][0]['extractedFields']['status']
    message = jsonData['logEvents'][0]['extractedFields']['event']
    timestamp = int(jsonData['logEvents'][0]['timestamp']) / 1000
    func = os.path.basename(jsonData['logGroup'])

    try:
        if status == "ERROR":
            errlog = func + ': ' + message
            sender = ZabbixSender(ZBX_SERVER)
            sender.add(ZBX_HOST, ZBX_ITEM, errlog, timestamp)
            sender.send()
        logger.info("Execution Succeeded.")
    except Exception, e:
        logger.error(e)
        raise(e)
```

```python:ZabbixSender.py
import socket
import struct
import time
import json

class ZabbixSender:

    log = True

    def __init__(self, host='127.0.0.1', port=10051):
        self.address = (host, port)
        self.data    = []

    def __log(self, log):
        if self.log: print log

    def __connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            self.sock.connect(self.address)
        except:
            raise Exception("Can't connect server.")

    def __close(self):
        self.sock.close()

    def __pack(self, request):
        string = json.dumps(request)
        header = struct.pack('<4sBQ', 'ZBXD', 1, len(string))
        return header + string

    def __unpack(self, response):
        header, version, length = struct.unpack('<4sBQ', response[:13])
        (data, ) = struct.unpack('<%ds'%length, response[13:13+length])
        return json.loads(data)

    def __request(self, request):
        self.__connect()
        try:
            self.sock.sendall(self.__pack(request))
        except Exception as e:
            raise Exception("Failed sending data.\nERROR: %s" % e)

        response = ''
        while True:
            data = self.sock.recv(4096)
            if not data:
                break
            response += data

        self.__close()
        return self.__unpack(response)

    def __active_checks(self):
        hosts = set()
        for d in self.data:
            hosts.add(d['host'])

        for h in hosts:
            request = {"request":"active checks", "host":h}
            self.__log("[active check] %s" % h)
            response = self.__request(request)
            if not response['response'] == 'success': self.__log("[host not found] %s" % h)

    def add(self, host, key, value, clock=None):
        if clock is None: clock = int(time.time())
        self.data.append({"host":host, "key":key, "value":value, "clock":clock})

    def send(self):
        if not self.data:
            self.__log("Not found sender data, end without sending.")
            return False

        self.__active_checks()
        request  = {"request":"sender data", "data":self.data}
        response = self.__request(request)
        result   = True if response['response'] == 'success' else False

        if result:
            for d in self.data:
                self.__log("[send data] %s" % d)
            self.__log("[send result] %s" % response['info'])
        else:
            raise Exception("Failed send data.")

        return result
```

##環境変数
- ZBX_SERVER = ＜ZabbixサーバのIPアドレス＞
- ZBX_HOST = lambda-trapper
- ZBX_ITEM = errorlog

##設定
- ランタイム: Python 2.7
- ハンドラ: handler.sender
- ロール: 既存のロールを選択
- 既存のロール: ※

※
予め以下のアクセス権を持つロールを作成し、そのロールを当てます。

- ENIへのDescribe、Create、Delete権限（Zabbixサーバと通信するので、VPC接続が必要）
- CloudWatchのLogGroup、LogStreamへのCreate権限（AWS Lambda関数からロギング出力するため）
- CloudWatchのLogEventへのPut権限

#サブスクリプションフィルタを作成
1. AWS Lambda関数の一覧からZabbix送信用の関数(cwl-to-zabbix)をクリック
1. [トリガー]タブをクリックし、[トリガーの追加]をクリック
1. イベントソース欄（破線の枠内）をクリックし、ソース一覧から[CloudWatch Logs]を選択
1. 次のように項目を埋めて[送信]をクリック
    - ロググループ: (既存AWS Lambda関数のロググループを選択) ※
    - フィルタの名前: LambdaStream_cwl-to-zabbix
    - フィルタパターン: ```[status="ERROR", timestamp=*Z, request_id="*-*", event]```

※予め前項のロギング処理を追加したAWS Lambda関数のロググループを選択してください。

#まとめ的なもの

- Zabbix送信用AWS Lambda関数自体がコケた場合については考慮していません。
- [ZabbixSender](http://blog.father.gedow.net/2015/12/08/aws-lambda-python-send-metric-value-to-zabbix/)モジュールを公開されている外道父さんに感謝！
- サーバレスは面白いな。