概要
Oracle Cloud Infrastructure(OCI)のリソース監視で発生するアラートをZabbixに連携してZabbix Dashboardで通知する構成を検証しました
既存環境でZabbixを利用している場合、現在の監視基盤を活用しながらOCIの監視を統合できます
連携の流れ
本検証では、以下の流れでOCIのアラートをZabbixへ連携します
なお、今回の監視対象はSite-to-Site VPNとし、ZabbixでVPNのUp/Down状況から重要度を判定するようにしています
-
OCI MonitoringにてSite-to-Site VPNのTunnel Stateを監視します
- 監視VPN名: ICN-VM-04-vpn-vpn
- 監視名: SYD-Alarm-ICN-VM-04-vpn-vpn
構築時に監視するVPNや監視名は環境に応じて読み替えてください
-
MonitoringでTunnel Downを検知するとNotificationからFunctionを呼び出します
-
Functionはアラート情報を加工してZabbixに送信します
-
Zabbixは受け取ったTunnel State情報を判断してメッセージを表示します
- Tunnel1のみDownの場合は片系通信可能のため警告
- Tunnel2のみDownの場合は片系通信可能のため警告
- Tunnel1およびTunnel2両方Downの場合は通信不可のため重度の障害
構成
設定
OCI Function
Zabbixに送信するFunctionを作成します
- アラート情報を取り込み
- ホストにPrefix OCI-VPN_ を追加
- Tunnel stateがDown状態の場合は0, Up状態の場合は1を設定
- ホスト名、Tunnel名およびTunnel StateをZabbixに送信
以下はFunction作成に必要なfunc.py, func.yamlおよびrequirements.txtです
Function作成手順に沿って作業を行いDeployしてエラーがないことを確認します
## Replace << >> with the appropriate value
import io
import json
import logging
import os
import socket
import struct
from fdk import response
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# ============================================================
# Configuration
# ============================================================
ZABBIX_SERVER = os.environ.get("ZABBIX_SERVER", "<< IP Address >>")
ZABBIX_PORT = int(os.environ.get("ZABBIX_PORT", "10051"))
ZABBIX_TIMEOUT = float(os.environ.get("ZABBIX_TIMEOUT", "10"))
# OCI VPN prefix
ZABBIX_HOST_PREFIX = os.environ.get("ZABBIX_HOST_PREFIX", "OCI-VPN_")
# ============================================================
# Utility
# ============================================================
def recv_exact(sock, length):
data = b""
while len(data) < length:
chunk = sock.recv(length - len(data))
if not chunk:
raise ConnectionError("Connection closed by Zabbix")
data += chunk
return data
def escape_key_param(value):
return str(value).replace("\\", "\\\\").replace('"', '\\"')
# ============================================================
# Zabbix sender protocol
# ============================================================
def send_to_zabbix(zabbix_host, item_key, value):
request = {
"request": "sender data",
"data": [
{
"host": zabbix_host,
"key": item_key,
"value": str(value),
}
],
}
body = json.dumps(
request,
separators=(",", ":"),
ensure_ascii=False,
).encode("utf-8")
packet = b"ZBXD\x01" + struct.pack("<Q", len(body)) + body
logger.info(
"Sending to Zabbix: server=%s:%s host=%s key=%s value=%s",
ZABBIX_SERVER,
ZABBIX_PORT,
zabbix_host,
item_key,
value,
)
with socket.create_connection(
(ZABBIX_SERVER, ZABBIX_PORT),
timeout=ZABBIX_TIMEOUT,
) as sock:
sock.settimeout(ZABBIX_TIMEOUT)
sock.sendall(packet)
header = recv_exact(sock, 13)
if header[:5] != b"ZBXD\x01":
raise RuntimeError("Invalid Zabbix response header")
response_length = struct.unpack("<Q", header[5:13])[0]
response_body = recv_exact(sock, response_length)
result = json.loads(response_body.decode("utf-8"))
logger.info("Zabbix response=%s", result)
if result.get("response") != "success":
raise RuntimeError("Zabbix sender failed: " + str(result))
info = str(result.get("info", ""))
if info and "failed: 0" not in info:
raise RuntimeError("Zabbix item rejected: " + info)
return result
# ============================================================
# OCI alarm parsing
# ============================================================
def process_alarm(payload):
alarm_title = str(payload.get("title", "")).strip()
if not alarm_title:
raise ValueError("Alarm title is missing")
message_type = str(payload.get("type", "")).upper()
metadata_list = payload.get("alarmMetaData") or []
if not metadata_list:
raise ValueError("alarmMetaData is missing")
meta = metadata_list[0]
namespace = str(meta.get("namespace", ""))
if namespace != "oci_vpn":
raise ValueError("Unexpected namespace: " + namespace)
dimensions = meta.get("dimensions") or []
if not dimensions:
raise ValueError("dimensions is missing")
dimension = dimensions[0]
resource_name = str(
dimension.get("resourceName", "")
).strip()
public_ip = str(
dimension.get("publicIp", "")
).strip()
parent_resource_id = str(
dimension.get("parentResourceId", "")
).strip()
if not resource_name:
raise ValueError("resourceName is missing")
# ----------------------------------------
# OCI -> Tunnel state
#
# 0 = DOWN
# 1 = UP
# ----------------------------------------
if message_type in ("OK_TO_FIRING", "REPEAT"):
state = 0
elif message_type in ("FIRING_TO_OK", "RESET"):
state = 1
else:
raise ValueError(
"Unsupported alarm type: " + message_type
)
# ----------------------------------------
# Dynamic Zabbix Host
# ----------------------------------------
zabbix_host = ZABBIX_HOST_PREFIX + alarm_title
# ----------------------------------------
# Dynamic Zabbix Item
# ----------------------------------------
resource_name_escaped = escape_key_param(resource_name)
item_key = (
'oci.vpn.tunnel.state'
f'["{resource_name_escaped}"]'
)
logger.info(
"Parsed OCI VPN alarm: "
"title=%s "
"resourceName=%s "
"publicIp=%s "
"parentResourceId=%s "
"type=%s "
"state=%s",
alarm_title,
resource_name,
public_ip,
parent_resource_id,
message_type,
state,
)
zabbix_result = send_to_zabbix(
zabbix_host,
item_key,
state,
)
return {
"zabbix_host": zabbix_host,
"item_key": item_key,
"resource_name": resource_name,
"public_ip": public_ip,
"parent_resource_id": parent_resource_id,
"message_type": message_type,
"state": state,
"zabbix": zabbix_result,
}
# ============================================================
# OCI Functions handler
# ============================================================
def handler(ctx, data: io.BytesIO = None):
try:
if data is None:
raise ValueError("No request body")
raw_body = data.getvalue()
if not raw_body:
raise ValueError("Empty request body")
logger.info(
"Received OCI notification: %s",
raw_body.decode("utf-8", errors="replace"),
)
payload = json.loads(raw_body.decode("utf-8"))
result = process_alarm(payload)
return response.Response(
ctx,
response_data=json.dumps(
{
"result": "success",
**result,
},
ensure_ascii=False,
),
headers={
"Content-Type": "application/json",
},
)
except Exception:
logger.exception("OCI VPN to Zabbix failed")
raise
## Replace << >> with the appropriate value
schema_version: 20180708
name: <<fn function名>>
version: 0.0.1
runtime: python
build_image: fnproject/python:3.12-dev
run_image: fnproject/python:3.12
entrypoint: /python/bin/fdk /function/func.py handler
memory: 256
fdk>=0.1.117
Zabbix
以下の手順で登録を行います
-
Zabbix Host作成
- データ収集 >> ホストを選択してホストの作成を押下
- ホスト名: OCI-VPN_SYD-Alarm-ICN-VM-04-vpn-vpn
- 表示名: SYD-Alarm-ICN-VM-04-vpn-vpn
Functionが指定する host はZabbix側のホスト名と一致する必要があります。
- データ収集 >> ホストを選択してホストの作成を押下
-
tun1/tun2のアイテム作成
- データ収集 >> ホスト >> 対象ホストのアイテムを選択してアイテムの作成を押下
- 以下を入力して追加を押下
- tun1 State
- 名前:OCI VPN tun1 State
- タイプ:Zabbixトラッパー
- キー:oci.vpn.tunnel.state["ICN-VM-04-vpn-vpn-tun1"]
- データ型:数値(整数)
- 有効:チェック
- tun2 State
- 名前:OCI VPN tun2 State
- キー:oci.vpn.tunnel.state["ICN-VM-04-vpn-vpn-tun2"]
- データ型:数値(整数)
- 有効:チェック
- tun1 State
-
Value mapping
- データ収集 >> ホスト >> 対象ホスト >> 値のマッピングを選択して追加を選択
- 値のマッピングを値を入力して追加を押下
- 名前:OCI VPN Tunnel State
- マッピング:
0 → DOWN
1 → UP
-
アイテム両方を 1 に初期化
Triggerはtun1/tun2両方の値を必要とするので、両TunnelがUPしていることを確認した上で初期値として1を投入します## Replace << >> with the appropriate value zabbix_sender \ -z << IP Address >> \ -s "OCI-VPN_SYD-Alarm-ICN-VM-04-vpn-vpn" \ -k 'oci.vpn.tunnel.state["ICN-VM-04-vpn-vpn-tun1"]' \ -o 1 zabbix_sender \ -z << IP Address >> \ -s "OCI-VPN_SYD-Alarm-ICN-VM-04-vpn-vpn" \ -k 'oci.vpn.tunnel.state["ICN-VM-04-vpn-vpn-tun2"]' \ -o 1- 監視データ >> 最新データで最新の値がUP(1)になることを確認します
-
Trigger作成
- データ収集 >> ホスト >> 対象ホストのトリガーを選択してトリガーの作成を押下
- 以下を入力して追加を押下
- Tunnel1 Down
- 名前:[OCI VPN] tun1 DOWN - SYD-Alarm-ICN-VM-04-vpn-vpn
- 深刻度:警告
- 条件式:
last(/OCI-VPN_SYD-Alarm-ICN-VM-04-vpn-vpn/oci.vpn.tunnel.state["ICN-VM-04-vpn-vpn-tun1"])=0 and last(/OCI-VPN_SYD-Alarm-ICN-VM-04-vpn-vpn/oci.vpn.tunnel.state["ICN-VM-04-vpn-vpn-tun2"])=1
- Tunnel2 Down
- 名前:[OCI VPN] tun2 DOWN - SYD-Alarm-ICN-VM-04-vpn-vpn
- 深刻度:警告
- 条件式:
last(/OCI-VPN_SYD-Alarm-ICN-VM-04-vpn-vpn/oci.vpn.tunnel.state["ICN-VM-04-vpn-vpn-tun1"])=1 and last(/OCI-VPN_SYD-Alarm-ICN-VM-04-vpn-vpn/oci.vpn.tunnel.state["ICN-VM-04-vpn-vpn-tun2"])=0
- Both Tunnels Down
- 名前:[OCI VPN] BOTH TUNNELS DOWN - SYD-Alarm-ICN-VM-04-vpn-vpn
- 深刻度:重度の障害
- 条件式:
last(/OCI-VPN_SYD-Alarm-ICN-VM-04-vpn-vpn/oci.vpn.tunnel.state["ICN-VM-04-vpn-vpn-tun1"])=0 and last(/OCI-VPN_SYD-Alarm-ICN-VM-04-vpn-vpn/oci.vpn.tunnel.state["ICN-VM-04-vpn-vpn-tun2"])=0
- Tunnel1 Down
-
Trigger機能テスト
-
fn invoke でtun1 DOWN送信
## Replace << >> with the appropriate value $ cat > test-down.json <<'EOF' { "dedupeKey": "test-tun1-down", "title": "SYD-Alarm-ICN-VM-04-vpn-vpn", "type": "OK_TO_FIRING", "severity": "WARNING", "timestampEpochMillis": 1789099000000, "timestamp": "2026-09-11T04:03:20Z", "alarmMetaData": [ { "id": "ocid1.alarm.oc1.ap-sydney-1.test", "status": "FIRING", "severity": "WARNING", "namespace": "oci_vpn", "query": "TunnelState[1m]{parentResourceId = \"<<OCI VPN Parent OCID>>\"}.mean() < 1", "totalMetricsFiring": 1, "dimensions": [ { "publicIp": "<<OCI VPN IP address>>", "resourceName": "ICN-VM-04-vpn-vpn-tun1", "parentResourceId": "<<OCI VPN Parent OCID>>" } ] } ], "notificationType": "Split messages per metric stream", "isStreamNotification": true, "version": 1.7 } EOF $ fn -v invoke <<fn app名>> <<fn function名>> < test-down.json -
監視データ >> 障害を選択して表示されることを確認
警告 [OCI VPN] tun1 DOWN - SYD-Alarm-ICN-VM-04-vpn-vpn -
fn invoke でtun1 UP送信
## Replace << >> with the appropriate value $ cat > test-up.json <<'EOF' { "dedupeKey": "test-tun1-up", "title": "SYD-Alarm-ICN-VM-04-vpn-vpn", "type": "FIRING_TO_OK", "severity": "WARNING", "timestampEpochMillis": 1789099300000, "timestamp": "2026-09-11T04:08:20Z", "alarmMetaData": [ { "id": "ocid1.alarm.oc1.ap-sydney-1.test", "status": "OK", "severity": "WARNING", "namespace": "oci_vpn", "query": "TunnelState[1m]{parentResourceId = \"<<OCI VPN Parent OCID>>\"}.mean() < 1", "totalMetricsFiring": 0, "dimensions": [ { "publicIp": "<<OCI VPN IP address>>", "resourceName": "ICN-VM-04-vpn-vpn-tun1", "parentResourceId": "<<OCI VPN Parent OCID>>" } ] } ], "notificationType": "Split messages per metric stream", "isStreamNotification": true, "version": 1.7 } EOF $ fn -v invoke <<fn app名>> <<fn function名>> < test-up.json -
監視データ >> 障害を選択して表示が解消されることを確認
-
OCI監視
- オブザーバビリティおよび管理>>アラーム定義からアラームの作成押下
- 以下値を設定して作成
- アラーム名:SYD-Alarm-ICN-VM-04-vpn-vpn
- メトリックネームスペース:oci_vpn
- メトリック名:TunnelState
- 集計メトリック・ストリーム:off
- ディメンション名:parentResourceId
- ディメンション値:<<対象VPNのOCID>>
- トリガー・ルール:1より小さい
- トピックの作成を押下して先ほど作成したFunctionをサブスクリプションとしたトピックを作成
- メッセージのグループ化:メトリック・ストリームごとに通知を分割
- メッセージの書式:RAWメッセージの送信
動作確認
Site-to-Site VPNを実際に障害発生させ、検証結果として以下の状態遷移をZabbixの障害画面へ反映できることを確認しました。
| tun1 | tun2 | Zabbixでの判定 |
|---|---|---|
| UP | UP | 正常 |
| DOWN | UP | 警告 |
| UP | DOWN | 警告 |
| DOWN | DOWN | 重度の障害 |
まとめ
OCI Monitoringで検知したSite-to-Site VPNのTunnel状態を、OCI NotificationsおよびOCI Functionsを経由してZabbixへ連携する構成を検証しました
OCI側では TunnelState をTunnel単位のメトリック・ストリームとして監視し、障害発生・復旧時のAlarm通知をFunctionへ送信します。FunctionではAlarmに含まれるAlarm名、Tunnel名および状態遷移をもとにZabbix Sender Protocolのデータへ変換し、Zabbix Trapper Itemへ送信します
Zabbix側ではtun1/tun2の状態をそれぞれ 0=DOWN、1=UP として保持し、Triggerで2つのTunnel状態を組み合わせることで、片系障害と両系障害を異なる深刻度として表示できました
今回の構成ではFunctionに特定VPNのOCIDやPublic IPを固定値として持たせず、OCI Alarm通知に含まれる title と resourceName からZabbixのHost名とItem Keyを動的に生成しています。そのため、同じ命名規則でZabbix側にHostとItemを用意することで、他のSite-to-Site VPNにもFunctionを共通利用できます
OCI Monitoring単体でもSite-to-Site VPN状態の監視は可能ですが、既存環境でZabbixを監視基盤として利用している場合、このようにOCIのAlarmをZabbixへ連携することで、オンプレミスや他システムを含めた監視をZabbix Dashboardへ集約が可能です
追記
OCI監視からインターネット経由でZabbixに連携する場合TLSで通信を保護することによって実現が可能です
Zabbix側
前段にTLS終端する仕組みを導入することによってZabbix自体の変更は不要です
例えば以下のような構成となります
Function側
func.pyから以下の点を変更します
-
変更点1: ssl モジュールを追加
import ssl -
変更点2: 接続先をLoad BalancerのFQDNに変更
パブリックFQDNを使用します
ポートは既にActive監視で10051を使用していて混在させたくない場合、区別のため別ポートにすることも可能ですZABBIX_SERVER = os.environ.get("ZABBIX_SERVER", "zabbix.example.com") ZABBIX_PORT = int(os.environ.get("ZABBIX_PORT", "10061")) -
変更点3: TLS接続用関数を追加
def create_tls_connection(): """ Connect to the TLS-terminating Load Balancer. The server certificate is verified using the system CA store. The certificate hostname must match ZABBIX_SERVER. """ logger.info( "Connecting to Zabbix TLS endpoint: %s:%s", ZABBIX_SERVER, ZABBIX_PORT, ) ssl_context = ssl.create_default_context() raw_sock = socket.create_connection( (ZABBIX_SERVER, ZABBIX_PORT), timeout=ZABBIX_TIMEOUT, ) raw_sock.settimeout( ZABBIX_TIMEOUT ) try: tls_sock = ssl_context.wrap_socket( raw_sock, server_hostname=ZABBIX_SERVER, ) logger.info( "TLS connection established: " "server=%s " "tls_version=%s " "cipher=%s", ZABBIX_SERVER, tls_sock.version(), tls_sock.cipher(), ) return tls_sock except Exception: raw_sock.close() raise -
変更点4: Zabbix接続部分をTLSソケットに変更
変更前with socket.create_connection( (ZABBIX_SERVER, ZABBIX_PORT), timeout=ZABBIX_TIMEOUT, ) as sock: sock.settimeout(ZABBIX_TIMEOUT)変更後
with create_tls_connection() as sock:sock.sendall(packet)以降は変更ありません
変更前後の通信経路
-
変更前
OCI Function ↓ TCP/10051 ↓ Zabbix Server -
変更後
OCI Function ↓ TLS/10061 ↓ Load Balancer ↓ TCP/10051 ↓ Zabbix Server















