RDS Multi-AZのフェイルオーバーをZABBIXで検知できるようにする

# 概要
RDSのMulti-AZ構成で、DBのフェイルオーバーが発生したことを検知したいという要望があったので実装してみました。
仕組みとしては、RDSでフェイルオーバー時に発生するイベントサブスクリプションからAmazon SNSを呼び出して、Lambda関数を実行。Lambda関数でZABBIXに送信という形で考えてみました。

![flow.jpg](https://qiita-image-store.s3.amazonaws.com/0/290607/0bce45dc-fc3f-d5d7-a5c2-26dfdc409887.jpeg)

# 設定
## AWSの設定
### Lambda用ロールの作成
IAM => ロール => ロールの作成 からLambdaで利用するロールを作成します。

信頼されたエンティティの種類を選択
=> AWSサービス
このロールを使用するサービスを選択
=> Lambda
ポリシー
=> AWSLambdaBasicExecutionRole

### Lambda関数の作成
実行ロール
=>先ほど作成したロールを選択
ランタイム
=>Python2.7

lambda_function.py
ZabbixSender.py
という2つのファイルを作成します。

```lambda_function.py
import os
import json
import logging
from ZabbixSender import ZabbixSender

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 lambda_handler(event, context):
    message = json.loads(event['Records'][0]['Sns']['Message'])
    val = 0
    if message['Event Message'] == "Multi-AZ instance failover started":
        val = 1
    
    try:
        sender = ZabbixSender(ZBX_SERVER)
        sender.add(ZBX_HOST, ZBX_ITEM, val)
        sender.send()
        logger.info("Execution Succeeded.")
    except Exception, e:
        logger.error(e)
        raise(e)
```
フェイルオーバー発生時に、EventMessageに `Multi-AZ instance failover started` という文字列が含まれて送信されてくるので、この文字列を検知した場合に、変数に `1` をセットして送信します。
それ以外は全て `0` が入るので、フェイルオーバー完了時は `0` として送信されます。


```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
```
ZABBIXにデータを送信するためのライブラリを作ってらっしゃる方がいましたので使わせて頂きました！
ありがとうございます :pray: 
http://blog.father.gedow.net/2015/12/08/aws-lambda-python-send-metric-value-to-zabbix/

環境変数を3つ用意します

|環境変数|値|
|---|---|
|ZBX_HOST|ZABBIXに登録されているホスト名|
|ZBX_ITEM|RDS_failover.failover(監視アイテムとキー)|
|ZBX_SERVER|ZABBIXサーバーのホスト名|

### SNSの作成
#### トピックの作成
トピック名：detectFailover

#### サブスクリプションの作成
プロトコル => AWS Lambda
エンドポイント => 作成したLambda関数のエンドポイント
「サブスクリプションの作成」をクリック

### RDSの設定
RDSのダッシュボードから「イベントサブスクリプション」=>「イベントサブスクリプションの作成」をクリック
名前：サブスクリプションの名前
ターゲット：ARN
ARN：作成したトピックを選択
ソース
ソースタイプ：インスタンス
インスタンスを含める：個別に選択 インスタンス
指定インスタンスのプルダウンから、フェイルオーバーを検知したいRDSインスタンスを選択
含まれるイベントのカテゴリ：特定のイベントカテゴリを選択します。
特定のイベントのプルダウンから、「フェイルオーバー」を選択

「作成」をクリック

## ZABBIXの設定
監視対象のホスト(RDS)がすでに登録されていれば、そのホストの設定を開きます。
ない場合は新規に作成

### アイテムの作成
名前：RDS_failover
キー：RDS_failover.failover
タイプ：Zabbix トラッパー
データ型：数値(整数)
データの形式：10進数

### トリガーの作成
名前：RDS-Multi-AZ-failover
条件式：下記のように入力します。

```
{ホスト:RDS_failover.failover.last()}=1 & {ホスト:RDS_failover.failover.nodata(300)}=0
```

Lambdaで、フェイルオーバーを検知した時に `1` がZABBIXに送信されるので、1の値がセットされた時にアラートが発生するようにします。
また、フェイルオーバー発生時しか値が飛んでこないので、5分間何も更新が無い時に復旧させます。

## テスト
検証できる環境なら、実際にフェイルオーバーをさせてアラートが鳴るかテストします。
RDSから対象のインスタンスの詳細を開き、
アクション=>再起動 をクリック
「フェイルオーバーし再起動しますか?」にチェックを入れて「再起動」をクリックするとフェイルオーバーが走ります。
しばらく待つと、ZABBIXからアラートが発生するようになります。(多少時間差があるようです)
このまま5分待つと自動復旧になります。
ZABBIXの最新データを見ると、`RDS_failover.failover` のアイテムに `1` がセットされていることが分かります。