0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OCIのアラートをZabbixに連携する

0
Last updated at Posted at 2026-09-13

概要

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してエラーがないことを確認します

func.py
## 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

func.yaml
## 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
requirements.txt
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"]
        • データ型:数値(整数)
        • 有効:チェック
  • 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
          
  • 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 重度の障害
  • tun1 Down (片系障害)
    OCI Site-to-Site VPN状態
    image.png

    OCI VPN監視状態
    image.png

    Zabbix障害画面
    image.png

    Zabbix tunnel state値
    image.png

  • tun2 Down (両系障害)
    OCI Site-to-Site VPN状態
    image.png

    OCI VPN監視状態
    image.png

    Zabbix障害画面
    image.png

    Zabbix tunnel state値
    image.png

  • tun1 Up (片系障害)
    OCI Site-to-Site VPN状態
    image.png

    OCI VPN監視状態
    image.png

    Zabbix障害画面
    image.png

    Zabbix tunnel state値
    image.png

  • tun2 Up (復旧)
    OCI Site-to-Site VPN状態
    image.png

    OCI VPN監視状態
    image.png

    Zabbix障害画面
    image.png

    Zabbix tunnel state値
    image.png

まとめ

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
    
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?