2
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?

ちょっとだけLocal LLMとn8nでログ確認らくにしたかった

2
Last updated at Posted at 2026-02-22

ログ確認に(精神的な)限界を感じ、Mac上のローカルLLMに少しだけ助けてもらう仕組みを作ってみた話

(AIに記事をかいてもらいました :)

image.png

はじめに

日々の業務でFivetranやdbtなどのデータパイプラインを運用しているのですが、トラブルが起きるたびに管理画面を開いたり、ログを追うためにクエリを叩き続けたりする作業に、少しばかり疲れを感じていました。

「自分のスキル不足を補うために、もう少しだけログ確認を楽にできないだろうか…」

そう思い悩み、昨今話題のAIの力を借りてみることにしました。外部のクラウドAPIに機密データを投げるのは不安だったため、勉強も兼ねて手元のMacBook Air内で完結する小さな「ローカルAIOps環境」を構築してみました。

まだまだアーキテクチャとして至らない点も多々あるかと思いますが、個人の備忘録として、また同じようにログ運用に悩む方の少しでもお役に立てればと思い、構築手順を共有させていただきます。

今回構築したささやかな構成

全体像としては以下のような構成になります。外部通信を行わず、Macのローカルリソースのみで稼働しています。

処理の流れ:

  1. K3s 上のコンテナ群がログを出力する。
  2. OpenTelemetry (OTel) Collector がログを収集し、ClickHouse へ蓄積する。
  3. ワークフローエンジンの n8n が定期的に ClickHouse を確認し、新しいエラーログを抽出する。
  4. n8n から、Macのホスト側で待機させているローカルLLM(Liquid AI LFM2.5)へログを渡し、内容の意訳と要約をお願いする。
  5. LLMが生成してくれた要約レポートを、n8n経由で Slack に通知する。

前提となる環境

  • Apple Silicon Mac (M1/M2/M3)
  • Podman Desktop および k3d がインストール済みであること
  • kubectl コマンドが実行可能なこと
  • Slack の Incoming Webhook URL が発行済みであること
  • ローカルLLMの実行環境(llama.cpp / llama-server)が構築済みであること
  • ※今回は、軽量かつ精度が高いと感じた LiquidAI/LFM2.5-1.2B-Instruct のGGUFモデルを利用させていただきました。

構築手順

ここからは、実際に手元で構築した際の手順を記載します。

Step 1: ローカルLLMの起動

まずは、解析を担ってくれるLLMを起動します。K3s(コンテナ内部)からアクセスできるように、--host 0.0.0.0 を指定して起動しておくのがポイントでした。

# Macのターミナルで実行(モデルのパスはご自身の環境に合わせて変更してください)
llama-server -hf LiquidAI/LFM2.5-1.2B-Instruct-GGUF:Q4_K_M --host 0.0.0.0 --port 8080

Step 2: K3sクラスターの作成

テスト用の小さなKubernetesクラスターを k3d で作成します。

k3d cluster create aiops-cluster

Step 3: マニフェストの適用(基盤構築)

必要なコンポーネントを順番にデプロイしていきます。管理がしやすいよう、一度YAMLファイルに書き出してから apply する形をとっています。

3-1. ClickHouse(ログ蓄積)

OTelからのログ受信ポート(9000)と、n8nからのHTTPアクセス用ポート(8123)を開放しています。

cat <<EOF > clickhouse.yaml
apiVersion: v1
kind: Service
metadata:
  name: clickhouse
spec:
  selector:
    app: clickhouse
  ports:
    - name: tcp
      protocol: TCP
      port: 9000
      targetPort: 9000
    - name: http
      protocol: TCP
      port: 8123
      targetPort: 8123
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: clickhouse
spec:
  replicas: 1
  selector:
    matchLabels:
      app: clickhouse
  template:
    metadata:
      labels:
        app: clickhouse
    spec:
      containers:
        - name: clickhouse
          image: clickhouse/clickhouse-server:23.8
          ports:
            - containerPort: 9000
            - containerPort: 8123
          env:
            - name: CLICKHOUSE_USER
              value: "admin"
            - name: CLICKHOUSE_PASSWORD
              value: "password123"
EOF

kubectl apply -f clickhouse.yaml

3-2. OTel Collector(ログ収集)

クラスター内のコンテナログを拾い集め、ClickHouseへ転送する設定です。

cat <<EOF > otel.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: otel-config
data:
  config.yaml: |
    receivers:
      filelog:
        include: [ /var/log/pods/*/*/*.log ]
        operators:
          - type: container
    processors:
      batch:
    exporters:
      clickhouse:
        endpoint: tcp://clickhouse:9000
        username: admin
        password: password123
        ttl_days: 1
        logs_table_name: otel_logs
    service:
      pipelines:
        logs:
          receivers: [filelog]
          processors: [batch]
          exporters: [clickhouse]
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: otel-collector
spec:
  selector:
    matchLabels:
      app: otel-collector
  template:
    metadata:
      labels:
        app: otel-collector
    spec:
      containers:
        - name: otel-collector
          image: otel/opentelemetry-collector-contrib:0.88.0
          args: ["--config=/etc/otel/config.yaml"]
          volumeMounts:
            - name: config
              mountPath: /etc/otel
            - name: varlogpods
              mountPath: /var/log/pods
              readOnly: true
            - name: varlibdockercontainers
              mountPath: /var/lib/docker/containers
              readOnly: true
      volumes:
        - name: config
          configMap:
            name: otel-config
        - name: varlogpods
          hostPath:
            path: /var/log/pods
        - name: varlibdockercontainers
          hostPath:
            path: /var/lib/docker/containers
EOF

kubectl apply -f otel.yaml

3-3. n8n(自動化ワークフロー)

連携処理を担うn8nです。日本時間で動作するように環境変数を設定しています。

cat <<EOF > n8n.yaml
apiVersion: v1
kind: Service
metadata:
  name: n8n
spec:
  selector:
    app: n8n
  ports:
    - protocol: TCP
      port: 5678
      targetPort: 5678
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: n8n
spec:
  replicas: 1
  selector:
    matchLabels:
      app: n8n
  template:
    metadata:
      labels:
        app: n8n
    spec:
      containers:
        - name: n8n
          image: docker.io/n8nio/n8n:latest
          ports:
            - containerPort: 5678
          env:
            - name: GENERIC_TIMEZONE
              value: "Asia/Tokyo"
            - name: TZ
              value: "Asia/Tokyo"
EOF

kubectl apply -f n8n.yaml

3-4. テスト用アプリケーション(Chaos Logger)

動作確認のために、あえてエラーログを定期的に出力するPodも用意しました。

cat <<EOF > chaos.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: chaos-logger
spec:
  replicas: 1
  selector:
    matchLabels:
      app: chaos-logger
  template:
    metadata:
      labels:
        app: chaos-logger
    spec:
      containers:
        - name: logger
          image: busybox
          command:
            - /bin/sh
            - -c
            - |
              while true; do
                rand=\$((RANDOM % 4))
                case \$rand in
                  0) echo "[ERROR] Failed to connect to database 'user_db' at 10.43.2.5: Connection timed out. Retry attempt 3/3 failed.";;
                  1) echo "[CRITICAL] Payment gateway API returned HTTP 503 Service Unavailable. Transaction ID: txn_\$RANDOM dropped.";;
                  2) echo '{"level":"error","service":"auth-api","message":"NullPointerException in TokenValidator.java:45","trace":"java.lang.NullPointerException\n\tat com.app.auth.TokenValidator.validate(TokenValidator.java:45)"}';;
                  3) echo "[INFO] User login successful. UserID: 10024";;
                esac
                sleep \$((RANDOM % 20 + 10))
              done
EOF

kubectl apply -f chaos.yaml

Step 4: n8n ワークフローの構築

Pod が全て Running 状態になったら、n8n にアクセスするためにポートフォワードを設定します。

kubectl port-forward svc/n8n 5678:5678

ブラウザで http://localhost:5678 にアクセスし、以下の流れでワークフローを組みました。

  1. Schedule Trigger: 30秒間隔で定期実行する。
  2. HTTP Request (ClickHouse): 直近40秒間に発生したエラーログのみを抽出する(ログの重複検知を防ぐため、少しのりしろを持たせています)。
  3. If ノード: 抽出結果が0件の場合は、LLMへのリクエストを行わずに処理を終了する(不要なリソース消費を防ぐため)。
  4. HTTP Request (LLM): http://host.containers.internal:8080/v1/chat/completions 宛にログデータを送り、SREの視点で要約をお願いするプロンプトを設定。
  5. HTTP Request (Slack): LLMから返ってきた要約テキストをSlackへPOSTする。
🛠️ 手っ取り早く試したい方向け:インポート用JSON

こちらのJSONをn8nのキャンバスに貼り付けると、上記のフローが再現できます。(※SlackのURLやClickHouseの認証情報はご自身の環境に合わせて修正をお願いいたします)

{
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "seconds",
              "seconds": 30
            }
          ]
        }
      },
      "id": "schedule-trigger",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [460, 240]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://clickhouse:8123/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "text/plain",
        "body": "SELECT Timestamp, Body \nFROM default.otel_logs \nWHERE (Body LIKE '%WARNING%' OR Body LIKE '%ERROR%' OR Body LIKE '%CRITICAL%')\n  AND Timestamp > now() - interval 40 second\nORDER BY Timestamp DESC \nFORMAT JSON"
      },
      "id": "clickhouse-query",
      "name": "HTTP Request (ClickHouse)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [680, 240],
      "credentials": {
        "httpBasicAuth": {
          "id": "YOUR_CREDENTIAL_ID",
          "name": "ClickHouse Auth"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "number": [
            {
              "value1": "={{ $json.rows }}",
              "operation": "larger"
            }
          ]
        }
      },
      "id": "if-error-exists",
      "name": "If Error Exists",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [900, 240]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://host.containers.internal:8080/v1/chat/completions",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"model\": \"local-model\",\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"あなたは優秀なインフラエンジニア(SRE)です。提供されたシステムログを確認し、何が起きているのか日本語で簡潔に要約して報告してください。\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {{ JSON.stringify($json.data) }}\n    }\n  ],\n  \"temperature\": 0.1\n}"
      },
      "id": "llm-analyze",
      "name": "HTTP Request (LLM)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [1120, 220]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "YOUR_SLACK_WEBHOOK_URL",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"text\": \"🚨 *K3s AIアラート (LFM2.5)* 🚨\\n\\n\" + $json.choices[0].message.content\n}"
      },
      "id": "slack-notify",
      "name": "HTTP Request (Slack)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [1340, 220]
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "HTTP Request (ClickHouse)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request (ClickHouse)": {
      "main": [
        [
          {
            "node": "If Error Exists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Error Exists": {
      "main": [
        [
          {
            "node": "HTTP Request (LLM)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request (LLM)": {
      "main": [
        [
          {
            "node": "HTTP Request (Slack)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request (Slack)": {
      "main": [
        []
      ]
    }
  }
}

終わりに

まだまだ改善の余地は山ほどある簡素な仕組みですが、無味乾燥なログが少しだけ分かりやすい文章になって通知されるようになり、日々の監視に対する精神的なハードルが少し下がったように感じます。

テスト稼働中も「データベースへの接続がタイムアウトしています」など、LLMが的確に意訳して教えてくれるため、今まで見落としていた細かな事象にも気づきやすくなりました。

大層なシステムではありませんが、小さく手元で始めてみるのも学びが多くて面白いなと実感しています。今後は機密情報のマスキング(PII対策)を挟むなど、より実用的な構成にも挑戦してみたいです。

もし「もっとこうした方が良いよ!」といったアドバイスやご指摘がありましたら、ぜひお手柔らかに教えていただけますと幸いです。最後までお読みいただき、ありがとうございました。


*(クリーンアップ手順:検証が終わった後は k3d cluster delete aiops-cluster で簡単に破棄できます)

2
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
2
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?