こんにちは、マネージサービス部でMSP事業を担当している宮坂です。
本エントリでは監視システムである Zabbix において、トリガーアクションによるアラート通知・リモートコマンド実行が失敗してしまった際に AWS Lambda を使って検知・通知を行う仕組みを考えてみたのでご紹介します。
Zabbix アクションの失敗はハンドリングがつらい
Zabbix ではアクション実行が失敗すると指定された通りにリトライを行い、それでも成功しなかった場合はアラートのステータスを "失敗" にマークし処理を終了します。このとき例えば別のアクションを実行するといったエラーハンドリングができず、また内部チェックアイテムなどで失敗回数を確認することもできないため Zabbix 単体では気づくことが難しく、人知れず失敗したままになり障害が起きていたのに気づけなかった!!、リモートコマンドによる自動処理が止まっていた!! といったトラブルを引き起こし、多くの Zabbix ユーザーを悩ませてきたのではないかと思います。
失敗した Zabbix アクションの確認方法
というわけで何らかの仕組みを用意してアクション失敗をハンドリングしたいという欲求が出てくるわけですが、そもそも失敗した Zabbix アクションを機械的に検索・取得する方法は2つあります。
方法1. Zabbix API を使う
Zabbix API の alert.get を使いログを取得します。
取得の際はフィルターで status
が 2
("failed after a number of retries" もしくは "tried to run the command on the Zabbix agent but it was unavailable") であるアラートに絞りつつ、さらに検索時の負荷を抑えるために time_from
や time_till
で期間を指定すると良いでしょう。
過去1日で失敗したアラートを取得するサンプルコードを示します。(pyzabbixライブラリを使用した Python 3.9 コード)
from datetime import datetime, timedelta from zoneinfo import ZoneInfo from pyzabbix import ZabbixAPI # PyPI: pyzabbix time_from = datetime.now(tz=ZoneInfo('Asia/Tokyo')) - timedelta(days=1) zapi = ZabbixAPI('https://xxxxxxxxxx/') zapi.login('xxxxxxxxxx', 'xxxxxxxxxx') zapi.alert.get( time_from=time_from.timestamp(), filter={ 'status': 2 }, output=['alertid', 'eventid', 'clock'] )
[{'alertid': '16', 'eventid': '458', 'clock': '1671435353'}]
方法2. DBを直接参照する
他にもDBの中身を直接取得する方法もあります。 過去1日で失敗したアラートを取得するサンプルクエリを示します。
SELECT alertid, eventid, clock FROM alerts WHERE status=2 AND clock > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 1 DAY));
+---------+---------+------------+ | alertid | eventid | clock | +---------+---------+------------+ | 16 | 458 | 1671435353 | +---------+---------+------------+ 1 row in set (0.00 sec)
Lambda で失敗を検知&通知する
さて、検知の仕組みを用意するとなるとどちらかと言えば Zabbix API を定期的に叩いて失敗しているアクションが無いかをポーリングするのが一般的だと思います。 しかしながら今回、対象の Zabbix server が AWS 上に構築されていることから、せっかくなので AWS ならではの機能を使って実装してみたいと思います。
具体的には Amazon Aurora MySQL クラスターから Lambda 関数を呼び出す機能を使って、アラートのレコードが更新されたときに Lambda 関数で判定を行い、ステータスが失敗だったら通知等の何らかの対応を行うというものです。 全体の構成・処理の流れは次の通りです。
また詳細なスペックは以下の通りです。
- Zabbix: Zabbix server 6.0.12 LTS
- RDB: Amazon Aurora MySQL 3.02.2 (MySQL 8.0.23 互換)
- Lambda runtime: Python 3.9
それでは実際に設定してみましょう。
Step1. Aurora が Lambda API に到達するためのルートを確保
Aurora インスタンスはプライベートサブネットに構築するのが一般的だと思いますが、それだと Aurora インスタンスが Lambda API を呼び出すことができません。そのため以下のうちいずれかの対応を行います。
- Aurora インスタンスをパブリックサブネットに構築し、パブリックアクセスを「あり」に設定する
- Aurora インスタンスが稼働しているVPCに NAT ゲートウェイを構築し、Aurora インスタンスからのルーティングを設定する
- Aurora インスタンスが稼働しているVPCに Lambda サービスの VPC エンドポイントを構築する、Aurora インスタンスからの通信を許可する
さらに Aurora インスタンスが Lambda API に対して通信ができるよう、Aurora インスタンスのセキュリティーグループでアウトバウンド通信を許可します。詳しくは以下の公式ドキュメントを参照してください。
Step2. Lambda 関数を作成
送られたレコードの中身を見て判定・失敗時の処理を行う Lambda 関数を作成します。 以下は SNS トピックに通知を送信する Python 3.9 のコード例です。
from os import environ from logging import getLogger, INFO import json import boto3 STATUS_FAILED = 2 def lambda_handler(event, context): logger = getLogger('zabbix-actionlog-checker.check') logger.setLevel(INFO) sns = boto3.client('sns') arn = environ['NOTIFY_SNS_ARN'] instance = event.get('instance', 'Unknown') alertid = int(event['alertid']) old_status = event['old_status'] or 'N/A' status = int(event['status']) logger.info(f'Zabbix alert {instance}#{alertid}: {old_status} => {status}') if status == STATUS_FAILED: logger.warning(f'Zabbix alert {instance}#{alertid} was failed') response = sns.publish( TopicArn=arn, Subject=f'Zabbix alert {instance}#{alertid} was failed', Message=json.dumps(event, ensure_ascii=False, indent=4) ) logger.debug(response)
Step3. Aurora 用IAMロール・IAMポリシーを作成
公式ドキュメントに従い、前項で作成した Lambda 関数に対しての lambda:InvokeFunction
アクションを許可したIAMロールを作成します。
Step4. Aurora クラスターに IAM ロールをアタッチ
AWSマネジメントコンソールにて対象の Aurora クラスターを開き、接続とセキュリティ > IAM ロールの管理 にて前項で作成した IAM ロールをアタッチします。
Step5. Aurora のクラスターパラメータを設定
クラスターパラメータにて以下2つのパラメーターを設定します。
aws_default_lambda_role
: 先ほど作成した IAM ロールのARNを指定するactivate_all_roles_on_login
:1
にセットする (ここでは設定せずクエリで毎度 SET ROLE してもよい)
Step6. Zabbix 用DBユーザーに Lambda 呼び出し権限を付与
DBにログインし、以下のクエリを実行して権限を付与します。
- Aurora 3.x (MySQL 8.0) で
zabbix
ユーザーに権限を付与する場合:GRANT AWS_LAMBDA_ACCESS TO zabbix;
- Aurora 2.x (MySQL 5.7) で
zabbix
ユーザーに権限を付与する場合:GRANT INVOKE LAMBDA ON *.* TO zabbix;
Step7. Zabbix DB の alert テーブルにトリガーを設定
DBのログインし、alert
テーブルがアップデートされた際に実行されるトリガーを設定します。
そのトリガーでは Aurora MySQL ネイティブ関数である lambda_async
を使用して Lambda 関数を呼び出します。
以下にサンプルを示します。
USE zabbix; DROP TRIGGER IF EXISTS zabbix_alert_updated; DELIMITER $$ CREATE TRIGGER zabbix_alert_updated AFTER UPDATE ON alerts FOR EACH ROW BEGIN DECLARE _result TEXT; SELECT lambda_async( '★★ Lambda function のARN ★★', JSON_OBJECT( 'instance', '★★ Zabbixサーバーを判別するユニークな識別子 ★★', 'alertid', NEW.alertid, 'actionid', NEW.actionid, 'alerttype', NEW.alerttype, 'clock', NEW.clock, 'error', NEW.error, 'esc_step', NEW.esc_step, 'eventid', NEW.eventid, 'mediatypeid', NEW.mediatypeid, 'message', NEW.message, 'retries', NEW.retries, 'sendto', NEW.sendto, 'status', NEW.status, 'subject', NEW.subject, 'userid', NEW.userid, 'p_eventid', NEW.p_eventid, 'acknowledgeid', NEW.acknowledgeid, 'old_status', OLD.status ) ) INTO _result; END $$ DELIMITER ;
動作テスト
意図的に成功しないメッセージ送信先を設定してアクションを失敗させてみます。
SNS 経由で通知が飛んできました。成功です。
おわりに
Amazon Aurora の機能を使ってレコードが UPDATE された際に Lambda 関数を呼び出す方法をご紹介しました。この方法には以下のようなメリットがあります。
- プッシュ式なのでほぼ即時で実行され検知・通知が早い
- Zabbix API によるポーリングだと取得する期間や取り扱いをうまく工夫しないと判定が漏れたり重複してしまうが、プッシュ式では比較的楽
なお注意点としては、変更頻度が非常に高いデータベースに対してトリガーを設定すると Lambda の invocations が急増する可能性があることにご注意ください。 また Lambda の呼び出し自体に失敗するとRDBのクエリが失敗します。そのため既存のデータベースに設定する場合は慎重に検証・適用作業を行ってください。また Amazon RDS 以外の AWS サービス障害の影響を受けてしまうリスクがあることも踏まえて導入可否を検討いただくのが良いかと思います。
今回のユースケース以外でもRDBのデータ更新をトリガーに何か別の処理をしたいというケースは様々なところにあると思いますのでぜひご活用ください。