Serverless Zabbix Sender for AWS

監視について「AWSと言えばCloudWatch」と言いたいところですが、プロセスの生死監視や、アラート発報時のサービス再起動の自動化等がやりたくて、CloudWatchだけだと色々と不都合なことがあります。

そこで、CloudWatchのメトリクス値をZabbixに投げて何もかもZabbixで見るという方法があり、以前から様々な場所で語られています。メトリクス値をZabbixに投げさえすれば、アラートの発報は当然として、発報後の処理もアクション等を使って大概のことができます。

Zabbixにメトリクス値を投げるには**「Zabbix Sender」**を実行する方法が簡単ですが、AWSとの連携を考えると『CloudWatchのメトリクスを取得してZabbix Senderを実行する』というスクリプト等が必要です。すぐ思いつくのは、Zabbixマネージャーが動いているサーバのOS上でこのスクリプトを実行すること(リソースの相乗り)ですが、大規模になればなるほど、スクリプトがリソースを食い、Zabbixマネージャーの稼働に影響が出るなど、本末転倒な状況になりかねません。

Zabbixマネージャーを含む各サーバの稼働に影響を与えないように、Zabbix Senderを実行するにはどうしたら良いのか。悩む中で閃いたのが、**ServerlessでZabbix Senderを動かしたらいいんじゃないかというアイデア**で、このアイデアが**「Serverless Zabbix Sender」の開発**に繋がっていきました。

# 前置き: Zabbix Senderとは
OSのコマンドラインで実行する場合の解説は、以下の通りです。
>Zabbix sender は、パフォーマンスデータをZabbix サーバで処理するために送信するコマンドラインユーティリティです。
>このユーティリティは通常、稼働とパフォーマンスのデータを定期的に送信するために長時間動作するユーザースクリプトの中で使用されます。

>データを1つ送信する場合
>例)Zabbix senderを使用してZabbixサーバーに値を送信する場合:
>shell> zabbix_sender -z zabbix -s "Linux DB3" -k db.connections -o 43
>オプション:
>z - Zabbixサーバのホスト (IPアドレスでの指定でも可)
>s - 監視対象のホスト名 (Webインタフェースで登録されたホスト名)
>k - アイテムキー
>o - 送信する値
> https://www.zabbix.com/documentation/2.2/jp/manual/concepts/sender

通常はこの方法なのですが、**Serverless Zabbix Sender**では、サーバもOSも使えないので、別の方法を考えなくてはなりません。
**Serverless Zabbix Sender**では、PythonでZabbix Senderを実行することにしました。

# PythonでZabbix Sender
Pythonだし、きっと探せばあるだろう、と思って探してみました。
**ありました。Python版Zabbix Senderです。**
https://pypi.org/project/ZabbixSender/
![image.png](https://qiita-image-store.s3.amazonaws.com/0/211986/49553b29-6034-bcde-9d94-855c33d648c8.png)
使い方は超シンプルで、リンク先で書かれている通りにコードを書くだけです。

# 構成図
**Python版Zabbix SenderをAWS Lambdaで動かして、Zabbixマネージャーにメトリクス値を送る**ことを考えました。構成図は以下の通りです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/211986/2a3d7395-9607-36ca-f9d0-26284d6ba06f.png)
# コード
Lambda関数を組み立てていきます。
## `Event`
CloudWatch Eventsを使ってLambdaを起動しますので、Eventで監視対象やIAM Roleの値を与えます。おまけかもしれませんが、**しれっとクロスアカウントにも対応させてしまいます。**

```json:event
{
    "target" : {
        "awsAccountId" : "xxxxxxxxxxxx",
        "awsIamRoleName" : "getMetricsWithCAA",
        "awsServiceNameSpace" : "AWS/RDS",
        "awsMetricName" : "CPUUtilization",
        "awsMetricsDemensionName" : "DBInstanceIdentifier",
        "awsMetricsDemensionValue" : "Zabbix",
        "awsRegion" : "ap-northeast-1",
        "Period" : 300,
        "Statistics" : "Sum"
    },
    "zabbix": {
        "zabbixManagerIpAddress" : "xx.xxx.xxx.xx",
        "zabbixManagerPort" : 10051,
        "targetZabbixHostName" : "ZabbixDB",
        "targetZabbixItemKey" : "aws.rds.cpu"
    }
}
```
### `target` の定義
監視対象および、監視対象が存在する環境に関する情報を定義します。

| Key       |       意味 |
|:-----------------|:------------------|
| awsAccountId             |              AWSのアカウントID (12桁) |
| awsIamRoleName           |            AssumeRoleするIAMロール名 |
| awsServiceNameSpace             |             サービスの名前空間 (AWS/RDS等) |
| awsMetricName               |                メトリクス名 (CPUUtilization等) |
| awsMetricsDemensionName             |             ディメンション名 (DBInstanceIdentifier等) |
| awsMetricsDemensionValue          |           ディメンションの値 (任意の値) |
| awsRegion               |                リージョン (ap-northeast-1等) |
| Period             |             期間 |
| Statistics          |          統計 [(詳しくはこちら)](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Statistic) |
### `zabbix` の定義
Zabbix Managerに関する情報を定義します。

| Key       |       意味 |
|:-----------------|:------------------|
| zabbixManagerIpAddress             |              ZabbixマネージャーのIPアドレス|
| zabbixManagerPort           |            Zabbixマネージャーの受信ポート |
| targetZabbixHostName             |              Zabbixマネージャーに登録されているホスト名|
| targetZabbixItemKey               |                Zabbixアイテムのキー |


## `lambda_handler.py`
AssumeRoleして、CloudWatchのメトリクス値を取得して、Zabbix Senderを実行します。

```python:lambda_handler
from ZabbixSender import ZabbixSender, ZabbixPacket
import boto3
import datetime

# AssumeRoleして、一時クレデンシャルを取得する関数
def stsAssumeRole(awsAccountId, awsIamRoleName, awsRegion):
    # 1. AssumeRoleするためのClient作成、基礎情報の定義
    awsIamRoleArn = "arn:aws:iam::" + awsAccountId + ":role/" + awsIamRoleName
    awsSessionName = "CrossAccountZabbixSender"
    awsStsClient = boto3.client('sts')
 
    # 2. AssumeRole
    response = awsStsClient.assume_role(
        RoleArn=awsIamRoleArn,
        RoleSessionName=awsSessionName
    )

    # 3. 一時クレデンシャルの取得
    awsSession = boto3.Session(
        aws_access_key_id=response['Credentials']['AccessKeyId'],
        aws_secret_access_key=response['Credentials']['SecretAccessKey'],
        aws_session_token=response['Credentials']['SessionToken'],
        region_name=awsRegion
    )
    
    # 4. 一時クレデンシャルを返す
    return awsSession

# CloudWatch Metricsを取得する関数
def getMetricStatistics(awsSession, target):
    # 1. AWS CloudWatch用Clientを生成
    awsClient = awsSession.client('cloudwatch')
    
    # 2. eventで指定した監視ターゲットのメトリクスを取得
    ## getMetricStatisticsはUTCで時間指定する必要があるため、UTCタイムゾーンを生成
    UTC = datetime.timezone(datetime.timedelta(hours=0), 'UTC')
    ## StartTimeとEndTimeで期間を決め、getMetricStatisticsを実行
    metricStatistics = awsClient.get_metric_statistics(
                            Namespace = target["awsServiceNameSpace"],
                            MetricName = target["awsMetricName"],
                            Dimensions=[
                                {
                                    'Name': target["awsMetricsDemensionName"],
                                    'Value': target["awsMetricsDemensionValue"]
                                }
                            ],
                            StartTime = datetime.datetime.now(UTC) - datetime.timedelta(seconds=target["Period"]),
                            EndTime = datetime.datetime.now(UTC),
                            Period = target["Period"],
                            Statistics = [target["Statistics"]]
                    )

    # 3. metricStatisticsをdictごと返す
    return metricStatistics

# Zabbixにメトリクス値を送信する関数
def zabbixSender(dataPoint, zabbix):
    # 1. Zabbix Managerを示すオブジェクトを取得
    zabbixManager = ZabbixSender(zabbix["zabbixManagerIpAddress"], zabbix["zabbixManagerPort"])
    
    # 2. Zabbix Senderで送るパケットを作成
    zabbixPacket = ZabbixPacket()
    zabbixPacket.add(zabbix["targetZabbixHostName"],zabbix["targetZabbixItemKey"], dataPoint)
    
    # 3. Zabbix Senderでパケット(データポイント)を送信
    zabbixManager.send(zabbixPacket)

# lambda_handler
def lambda_handler(event, context):
    # 1. stsAssumeRoleを呼び、eventで指定したAWSアカウントから一時アクセスキー(awsSession)を取得
    awsSession = stsAssumeRole(event["target"]["awsAccountId"],event["target"]["awsIamRoleName"],event["target"]["awsRegion"])
    
    # 2. 1.で得たawsSessionを利用し、CloudWatch Metricsを取得
    metricStatistics = getMetricStatistics(awsSession, event["target"])
    
    # 3. 2.で得たmetricStatisticsからデータポイントだけを抜き出す
    dataPoint = metricStatistics['Datapoints'][0][event["target"]["Statistics"]]
    
    # 4. 3.で得たデータポイントをZabbix SenderでZabbix Managerに送る
    zabbixSender(dataPoint, event["zabbix"])
```

# トリガー
CloudWatch Eventsを設定して、一定間隔でこの**Serverless Zabbix Sender**を実行するようにします。
## イベントソース
画面の指示に従い作ればOKです。`スケジュール` と `Cron式` の組み合わせです。例えば、5分間隔で実行する場合、Cron式には以下を入力します。

```text:Cron式
*/5 * * * ? *
```
## ターゲット
もちろん `Lambda関数` を指定します。 `入力の設定` では `定数(JSONテキスト)` を選択し、先ほどのEventで記載したJSONを入力します。

ここまで実施すれば、**Serverless Zabbix Sender**が動き始めます。

# VPC Lambdaとして動かす
**VPC Lambda**で**Serverless Zabbix Sender**を実行し、プライベートIP間での通信でZabbixマネージャーにメトリクスを送信することもできます。Security Groupでしっかりアクセス制限もできるので現実的です。ただし、いくつか注意事項があります。まず、VPC Lambdaのコールドスタートを回避するために、実行間隔を長くしすぎないことです。次に、VPC Lambdaを実行するVPCのサイズを必ず大きめに確保し、VPC Lambda専用とすることです。VPC Lambdaに割り当てられるIPアドレスは自動で決まり、監視対象とメトリクスが増えれば増えるほどIPアドレスを消費しますから、他のVPCとは分け、リッチにIPアドレスを確保できるサイズにした方が良いでしょう。

VPC Lambdaとして**Serverless Zabbix Sender**を実行する場合の構成図は以下の通りです。LambdaとZabbixマネージャー間の通信が、VPC Peeringになりました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/211986/12c2ec6c-ffcd-827c-481d-bef6b4166c25.png)

# 最後に
**Serverless Zabbix Sender**が動き始めて、Zabbixマネージャーでメトリクス値が次々に更新される様子を眺めつつ、これが全てServerlessで動いて送られているんだと考えると、なかなかに爽快な気分です。しかも、Lambdaで実行していますから、よほど大規模かつ高頻度でない限りは、無料の範囲内でなんとかできてしまいます。もう、どこかのサーバのリソースに相乗りする必要も、お金を出してZabbix Sender専用サーバなどと頑張る必要もありません。**Serverless Zabbix Sender**の後ろには、AWSの強大なコンピューティングリソースが控えており、どれだけ大規模になろうとも、確かに支えてくれます。Serverlessのパワーを頼ることにより、Zabbixマネージャーは、自身に本来割り当てられるはずのリソースを存分に使えるようになります。Serverlessの確かなパワーが、Zabbixを使った監視と運用をリッチにしてくれます。Serverlessは素晴らしいです。これに限らず、色んな場面で活用していくべきでしょう。