2025年9月30日火曜日

LTX-Video の inference.py でメモリ使用量を抑えるテクニック

LTX-Video の inference.py でメモリ使用量を抑えるテクニック

概要

基本何もしないと30GBほどは消費します
VRAM の小さいマシンでも動作させるようにする方法を紹介します

環境

  • macOS 15.7
  • Python 3.10.12
  • LTX-Video (main revision: 53d263f31727a0021bf65e3e413d1cb139abb86b)

CPUオフロードをオンにする

  • source env/bin/activate
  • pip install accelerate
  • vim ltx_video/inference.py
     pipeline = LTXVideoPipeline(**submodel_dict)
     pipeline = pipeline.to(device)
+    pipeline.enable_attention_slicing()
+    pipeline.enable_model_cpu_offload()
     return pipeline

288 行目あたりに上記を追加します

distilled なモデルを使う

configs/ltxv-2b-0.9.8-distilled.yaml を使います

生成する動画の長さを短くする

  • num_frames を下げる (デフォルトは121)
  • frame_rate を下げる (デフォルトは30)

でデフォルトは4秒((121-1)/30)の動画になるのですがどちらかを下げて動画の長さを短くしてもメモリ消費量を抑えられます

入力する画像のサイズを小さくする

  • 512x512ではな384x384などにする
  • それでもダメなら256x256にする
  • SD-WebUI に合わせるなら896x1152から256x384に比率を抑えながら解像度を下げて入力画像にする

最後に

Windows なら更に xformers の対応や Q8-Kernels もできます

2025年9月29日月曜日

launchctl に登録した plist がいつ実行されるのか確認する方法

launchctl に登録した plist がいつ実行されるのか確認する方法

概要

直接 plist を見るしかないのでシェルスクリプトを使いましょう

環境

  • macOS 15.7

シェルスクリプト

#!/bin/bash

# launchctl list で "local.mac" を含むタスクのラベルを取得
labels=$(launchctl list | grep local.mac | awk '{print $3}')

echo "Label -> Schedule"

for label in $labels; do
    # plist の候補パス
    plist_path=""
    for dir in ~/Library/LaunchAgents /Library/LaunchAgents /Library/LaunchDaemons /System/Library/LaunchDaemons; do
        if [ -f "$dir/$label.plist" ]; then
            plist_path="$dir/$label.plist"
            break
        fi
    done

    if [ -z "$plist_path" ]; then
        echo "$label -> plist not found"
        continue
    fi

    # plist を JSON に変換して jq でパース
    json=$(plutil -convert json -o - "$plist_path" 2>/dev/null)
    if [ -z "$json" ]; then
        echo "$label -> failed to parse plist"
        continue
    fi

    echo "$label ->"

    # RunAtLoad
    run_at_load=$(echo "$json" | jq -r '.RunAtLoad // empty')
    if [ "$run_at_load" == "true" ]; then
        echo "  RunAtLoad: true"
    fi

    # StartInterval
    start_interval=$(echo "$json" | jq -r '.StartInterval // empty')
    if [ -n "$start_interval" ]; then
        echo "  StartInterval: every $start_interval seconds"
    fi

    # StartCalendarInterval (複数対応)
    intervals=$(echo "$json" | jq -c '.StartCalendarInterval // empty')
    if [ -n "$intervals" ]; then
        echo "  StartCalendarInterval:"
        echo "$intervals" | jq -r 'if type=="array" then .[] else . end | "    Hour: \(.Hour // "-"), Minute: \(.Minute // "0")"' 
    fi

    echo "-------------------------"
done

出力結果

Label -> Schedule
local.mac.1 ->
  StartCalendarInterval:
    Hour: 1, Minute: 0
    Hour: 4, Minute: 0
    Hour: 7, Minute: 0
    Hour: 10, Minute: 0
    Hour: 13, Minute: 0
    Hour: 16, Minute: 0
    Hour: 19, Minute: 0
    Hour: 22, Minute: 0
-------------------------
local.mac.2 ->
  StartCalendarInterval:
    Hour: 8, Minute: 0
-------------------------
local.mac.3 ->
  StartCalendarInterval:
    Hour: 8, Minute: 5
-------------------------
local.mac.4 ->
  StartCalendarInterval:
    Hour: 5, Minute: 30
    Hour: 11, Minute: 30
    Hour: 17, Minute: 30
    Hour: 23, Minute: 30
-------------------------
local.mac.5 ->
  StartCalendarInterval:
    Hour: 9, Minute: 0
    Hour: 12, Minute: 0
    Hour: 18, Minute: 0
    Hour: 21, Minute: 0
-------------------------
local.mac.6 ->
  StartCalendarInterval:
    Hour: 8, Minute: 5
-------------------------

最後に

もっと簡単に確認できる方法はないだろうか

2025年9月26日金曜日

LTX-Video を M2 mac mini で動かしてみた

LTX-Video を M2 mac mini で動かしてみた

概要

LTX-Video は軽量な動画生成モデルです
今回は M2Pro mac mini 上で動かしてみました

環境

  • macOS 15.7
  • Python 3.10.12
  • LTX-Video (main revision: 53d263f31727a0021bf65e3e413d1cb139abb86b)

環境準備

  • git clone https://github.com/Lightricks/LTX-Video.git
  • cd LTX-Video
  • pyenv local 3.10.12
  • python -m venv env
  • source env/bin/activate
  • python -m pip install -e .\[inference\]
  • pip install av imageio 'imageio[ffmpeg]'

なぜか av と imageio がインストールされていなかったので手動でインストールしました

実行

今回使用するモデルは ltxv-2b-0.9.8-distilled になります
13b もありますが M2 Pro mac mini では動作しないと判断し軽量な 2b を使っています

入力の画像のサイズは 512x512 にしています

プロンプトは画像に合わせてどういう動画にしたいのかを入力する感じです

PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.0 \
python inference.py \
--prompt "A cute domestic short-haired cat riding a skateboard quickly along the street, cinematic, smooth motion, highly detailed, realistic lighting" \
--conditioning_media_paths ./input.png \
--conditioning_start_frames 0 \
--height 512 \
--width 512 \
--num_frames 16 \
--pipeline_config configs/ltxv-2b-0.9.8-distilled.yaml

最初に各種モデルのダウンロードが開始されます
結構いろいろなモデルをダウンロードするので全体で40GBほどあるので注意してください

num_frames を大きくすれば動画の長さも長くなります
ただ消費するメモリと生成時間も長くなるので注意してください
上記で紹介している num_frames=6 で生成までにだいたい20分ほどかかりました
動画のサイズは1秒にも満たない長さです

消費メモリはスワップも含めて30GBほどです
CPUとGPUは70%ほどの使用量で推移していました

生成されたファイルは outputs/2025-09-26/video_output_0_a-cute-domestic-shorthaired-cat_171198_512x512x6_1.mp4 という感じで生成されます

生成する動画の長さ

num_frames / frame_rate オプションで決まります
inference.py だとデフォルトは num_frames=121、frame_rate=30 なので4秒の動画が生成されます

なので今回のサンプルコマンドだと 16/30 なので0.5秒の動画になります
なお num_frames は作成したフレーム数+1するほうがいいようです

ちなみに M2 Pro mac mini で生成した際にデフォルトの値でも生成できました
生成時間は20分ほどでした
使用するメモリやCPU/GPUは変わりませんでした (生成する動画の長さに応じて使用するリソースは変化しないのかも?)

4秒版の動画は今の通りです

ちなみに num_frames=241 にしたところ untimeError: Invalid buffer size: 9.16 GiB が発生したので M2 Pro mac mini では 8 秒の長さが限界かもしれません

追記: 画像のサイズを256x256にし動画のサイズも256x256にしたらnum_frames=241でも生成できました

pip freeze

念の為動作したバージョンをメモしておきます

av==15.1.0
certifi==2025.8.3
charset-normalizer==3.4.3
diffusers==0.35.1
einops==0.8.1
filelock==3.19.1
fsspec==2025.9.0
hf-xet==1.1.10
huggingface-hub==0.35.1
idna==3.10
imageio==2.37.0
imageio-ffmpeg==0.6.0
importlib_metadata==8.7.0
Jinja2==3.1.6
-e git+https://github.com/Lightricks/LTX-Video.git@53d263f31727a0021bf65e3e413d1cb139abb86b#egg=ltx_video
MarkupSafe==3.0.2
mpmath==1.3.0
networkx==3.4.2
numpy==2.2.6
packaging==25.0
pillow==11.3.0
psutil==7.1.0
PyYAML==6.0.3
regex==2025.9.18
requests==2.32.5
safetensors==0.6.2
sentencepiece==0.2.1
sympy==1.14.0
timm==1.0.20
tokenizers==0.21.4
torch==2.8.0
torchvision==0.23.0
tqdm==4.67.1
transformers==4.51.3
typing_extensions==4.15.0
urllib3==2.5.0
zipp==3.23.0

最後に

LTX-Video を M2 pro mac mini 上で動かしてみました
これまで Stable Video Diffusion を試してきましたがそれよりも精度や生成スピードなどいろいろな点が良かった気がします

今回は2bモデルを使いましたが13bモデルのほうが精度はいいはずなので機会があれば試してみたいです (ltxv-13b-0.9.8-distilled)

ちなみに種類としては

  • dev -> 高クオリティ、低スピード、VRAM大量消費
  • mix -> 中クオリティ、高スピード、VRAM中消費
  • distilled -> 低クオリティ、中スピード、VRAM低消費

という感じっぽいです

参考サイト

2025年9月25日木曜日

M2 Pro Mac mini で Stable Video Diffusion を動かす

M2 Pro Mac mini で Stable Video Diffusion を動かす

概要

過去に forge を使った方法を紹介しました
今回はモデルを Python スクリプトで直接動かし画像から動画を生成してみます

環境

  • macOS 15.6.1
  • Python 3.10.12
    • diffusers 0.35.1
    • torch 2.5.1

ライブラリインストール

  • pyenv local 3.10.12
  • python -m venv svd-env
  • source svd-env/bin/activate
  • pip install diffusers transformers accelerate

モデルダウンロード

  • git lfs install
  • git clone https://huggingface.co/stabilityai/stable-video-diffusion-img2vid-xt svd-model

Python スクリプト

  • vim app.py
import torch
from diffusers import StableVideoDiffusionPipeline
from diffusers.utils import export_to_video, load_image

device = "mps" if torch.backends.mps.is_available() else "cpu"

pipe = StableVideoDiffusionPipeline.from_pretrained(
    "./svd-model", torch_dtype=torch.float16, variant="fp16"
)
pipe.to(device)
pipe.enable_model_cpu_offload()
pipe.unet.enable_forward_chunking()
pipe.enable_attention_slicing()

# Load the conditioning image
image = load_image("./input.png")
image = image.resize((256, 256))

# generator = torch.manual_seed(42)
# frames = pipe(image, decode_chunk_size=8, generator=generator, num_frames=4).frames[0]
frames = pipe(image, decode_chunk_size=8, num_frames=2).frames[0]

export_to_video(frames, "generated.mp4", fps=7)

動かすためのポイント

基本的にはそのままでは動かせんません
メモリが足りずに「RuntimeError: Invalid buffer size:」のエラーになります
なのでメモリを節約するための設定をいれる必要があります

pipe.enable_model_cpu_offload()
pipe.unet.enable_forward_chunking()
pipe.enable_attention_slicing()

更にもっともメモリを使用するのは num_frames でこれは生成する動画のフレーム数を指定します
SVD num_frames で指定したフレームを一度に生成するのこの値が大きければ大きいほど動画はスムーズになりますが逆に生成速度とメモリ消費量は大きくなります
デフォルトだと25が指定されていますが25では80GBほどVRAMを使うので到底動きません

最低が2以上で1を指定すると以下のエラーとなります

RuntimeError: Sizes of tensors must match except in dimension 2. Expected size 1 but got size 2 for tensor number 1 in the list.

一応サンプルコードの数値は自分が M2 Pro mac mini で試した感じ生成スピード/消費メモリ/画質のトレーズオフで一番良かった数値になります

また画像のサイズは動画の生成速度にはほぼ影響しませんでした

最後に

M2 Pro mac mini で SVD を動かしてみました
num_frames=2 でしか動かなかったので正直かなり厳しいです
1フレームずつ生成して結合する方法もあるらしいのですがその場合はかなり動きが不自然になる可能性があるのでそれも微妙かなと思います

GPU は問題なさそうですがやはりメモリが明らかに足りていない感じです

参考サイト

2025年9月24日水曜日

RooCode で git mcp を使ってコードレビューしてもらう方法

RooCode で git mcp を使ってコードレビューしてもらう方法

概要

code-server にインストールした RooCode を使って MCP サーバを設定します
今回は https://github.com/modelcontextprotocol/servers/tree/main/src/git を使いコードレビューする方法を紹介します

環境

  • Ubuntu 24.04
  • code-server 4.103.2
  • RooCode 3.28.7
  • uv 0.8.22

Python の設定

  • pyenv global 3.12.11

uv のインストール

  • curl -LsSf https://astral.sh/uv/install.sh | sh

mcp サーバ追加

mcp-server-git で管理するパスを --repository オプションで指定します
code-server の場合 code-server で開いているプロジェクトに合わせるようにしましょう

  • mcp_settings.json
{
  "mcpServers": {
    "git": {
      "command": "uvx",
      "args": [
        "mcp-server-git",
        "--repository",
        "/home/devops/infra"
      ]
    }
  }
}

記載できたら mcp-server-git が起動していることを確認しましょう
うまく起動できていない場合は code-server のプロセスが uv コマンドを参照できていない可能性があるので PATH に通っているかなど確認しましょう

なお mcp_settings.json のフルパスは /home/username/.local/share/code-server/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json にあります

動作確認

RooCode で新規にタスクを作成して mcp-server-git が使えることを確認しましょう
例えば「直近10件のコミットログを参照してざっくり内容を説明してください」と指定するとちゃんと mcp-server-git を使うための確認ダイアログが表示されることが確認できます

レビューのために使えそうなプロンプト

  • xxxブランチの内容をレビューしてください
    • xxxブランチを手元に持ってくる必要があるがPRなどの内容をレビューはこれでできます
  • xxxリビジョンの説明をしてください
    • ピンポイントでリビジョンを指定してレビューしたい場合はこちら
  • 現在のdiffのレビューをしてください
    • まだコミットする前にレビューしてもらう場合にはgit diffを使ってレビューすることもできます

最後に

mcp-server-git を使って RooCode でレビューする方法を紹介しました
レビューしてもらったあとに大抵は提案もしてくれるので提案の修正をお願いすればファイルをそのまま修正してくれます

gitのunstageファイルがうまく取得できないケースがあったのでunstageなファイルのレビューは少し工夫がいるのかもしれません

あとは Github や Gitlab を使っている場合はそれ専用の MCP サーバがあり手元にブランチを持ってこないでも PR の内容を直接レビューしてくれるのでもし専用の MCP サーバがある場合はそちらを使ったほうが簡単な場合があります

今回の方法はどんな git リポジトリにも使える汎用的な方法になります

2025年9月22日月曜日

Blogger に次の記事と前の記事のリンクを設置する方法

Blogger に次の記事と前の記事のリンクを設置する方法

概要

リンクを追加する方法を紹介します

環境

  • Blogger 2025/09/22
    • テンプレート Contempo (light)

サンプルコード

data:olderPageUrldata:newerPageUrl を使います
今回は記事の下部に表示していますが好きな箇所に設置してもらっても OK です

ポイントは data:view.isPost が使えるスコープで行いましょう

<b:includable id='postFooter' var='post'>
   <div class='post-bottom'>
   <div class='post-footer float-container'>
      <b:include name='footerBylines'/>
      <b:include cond='data:widget.type == &quot;Blog&quot;' data='post' name='postFooterAuthorProfile'/>
      <!-- 独自追加2025/09/22 -->
      <b:if cond='data:view.isPost'>
         <!-- float解除で強制改行 -->
         <div style='clear: both;'/>
            <div class='post-nav'>
               <b:if cond='data:olderPageUrl'>
                  <a class='post-nav-btn older' expr:href='data:olderPageUrl'>前の記事</a>
               </b:if>
               <b:if cond='data:newerPageUrl'>
                  <a class='post-nav-btn newer' expr:href='data:newerPageUrl'>次の記事</a>
               </b:if>
            </div>
            <style>
               .post-nav {
               display: flex;
               justify-content: space-between; /* 左右端に配置 */
               margin: 20px 0;
               }
            </style>
      </b:if>
      <!-- 独自追加2025/09/22 -->
      </div>
      <b:if cond='data:view.isSingleItem'>
         <b:include data='{ shareButtonClass: &quot;post-share-buttons-bottom invisible&quot;, overridden: true }' name='maybeAddShareButtons'/>
         <b:else/>
         <b:include data='post' name='postFooterJumpLink'/>
      </b:if>
   </div>
</b:includable>

最後に

テンプレートによって使える変数などがことなるので注意しましょう
data:post.next と data:post.previous が使えるらしいのですが自分は使えませんでした

参考サイト

2025年9月21日日曜日

ElastAlert2 を使ってみる

ElastAlert2 を使ってみる

概要

前回ES9をdockerで構築しました
今回は ElastAlert2 を組み合わせて特定のログがES9に格納されたらアラートされる仕組みを作成してみます

環境

  • macOS 15.6.1
  • docker 28.4.0
    • ElasticSearch 9.0.7
    • Kibana 9.0.7
    • fluentd 1.19-1
    • ElastAlert 2.26.0

elastalert.yaml

EalstAlert2 共通の設定を作成します
接続する ElasticSearch9 の設定やルールを管理するディレクトリを指定します

  • vim elastalert.yaml
rules_folder: /opt/elastalert/rules

run_every:
  seconds: 10

buffer_time:
  minutes: 15

es_host: 192.168.1.152
es_port: 9200
use_ssl: true
verify_certs: false
es_username: elastic
es_password: xxx

writeback_index: elastalert_status

alert_time_limit:
  days: 2

rules/test_alert.yaml

とりあえずテスト用のアラートファイルを作成します
今回は必ずアラートが上がるように @timestmp フィールドを監視し5分間の間に2回レコードが格納された場合にアラートが上がるようにします

  • mkdir rules
  • rules/test_alert.yaml
name: "test_alert"
type: "frequency"
index: "fluentd*"
is_enabled: true
num_events: 2
realert:
  minutes: 5
terms_size: 50
timeframe:
  minutes: 5
timestamp_field: "@timestamp"
timestamp_type: "iso"
use_strftime_index: false
alert_subject: "Test {} 123 aa☃"
alert_subject_args:
  - "log"
alert_text: "Test {}  123 bb☃"
alert_text_args:
  - "source"
filter:
  - query:
      query_string:
        query: "@timestamp:*"
alert:
  - "slack"
slack_webhook_url: 'https://hooks.slack.com/services/xxx'
slack_channel_override: "#private"
slack_emoji_override: ":kissing_cat:"
slack_msg_color: "warning"
slack_parse_override: "none"
slack_username_override: "elastalert"

各種項目の説明

ルールの基本

name: "test_alert"
ルールの名前。Slack 通知などに出てくる。

type: "frequency"
「一定時間内に指定した件数以上のイベントがあるとアラートを出す」というタイプ。

index: "fluentd"
監視対象の Elasticsearch インデックス名。

is_enabled: true
このルールが有効になっている。

発火条件

num_events: 2
  イベント件数のしきい値。

timeframe: minutes: 5
  評価対象の時間範囲。
  → 「5分間に2件以上あればアラート」

realert: minutes: 5
  一度アラートが出た後、同じ条件で再度アラートを出すまでのクールダウン時間。
  → 5分間は抑止する。

terms_size: 50
  集計クエリのサイズ。frequency ルールでは多くの場合デフォルトでOK。

タイムスタンプ関連

timestamp_field: "@timestamp"
  イベントの時刻として使う Elasticsearch のフィールド。

timestamp_type: "iso"
  日時フォーマットの種類。ISO 8601 形式。

use_strftime_index: false
  インデックス名に日付を埋め込まない(例: fluentd-%Y.%m.%d ではなく fluentd 固定)。

通知メッセージ

alert_subject: "Test {} 123 aa☃"
  Slack の件名やタイトル部分に使うテンプレート。
  {} に alert_subject_args のフィールドが入る。

alert_subject_args: "log"
  → log フィールドの値が {} に挿入。

alert_text: "Test {} 123 bb☃"
  本文テンプレート。
  {} に alert_text_args のフィールドが入る。

alert_text_args: "log"
  → log フィールドの値が {} に挿入。

検索条件

filter:
  単純に @timestamp フィールドが存在する全ログを対象にする。
  実質「全件」になる。

ElastAlert2 コンテナの起動

ではコンテナを起動します
ホスト側に作成した各種設定ファイルがちゃんとコンテナにマウントされるようにしましょう

  • docker run -d --name elastalert --restart=always -v $(pwd)/elastalert.yaml:/opt/elastalert/config.yaml -v $(pwd)/rules:/opt/elastalert/rules jertel/elastalert2 --verbose

ログを見ると

Background alerts thread 0 pending alerts sent at 2025-09-17 23:35 UTCINFO:elastalert:1 rules loaded

でルールが有効になっていることが確認できます
また以下のログがあれば Slack に通知できているログになります

INFO:elastalert:Ran test_alert from 2025-09-17 23:54 UTC to 2025-09-17 23:55 UTC: 2 query hits (0 already seen), 1 matches, 1 alerts sent

動作確認

5分間待って Slack に通知が来ることを確認しましょう

以下のように変数部分が <MISSING VALUE> になる場合は ES9 上に指定のフィールドが存在するか確認してください

おまけ: 事前にルールファイルの確認をする

  • docker run --entrypoint "" --rm -v $(pwd)/elastalert.yaml:/opt/elastalert/config.yaml -v $(pwd)/rules:/opt/elastalert/rules jertel/elastalert2 elastalert-test-rule /opt/elastalert/rules/test_alert.yaml

最後に

ES9 と ElastAlert2 を連携させて特定のログが出た場合に Slack に通知する仕組みを試してみました
基本的には filter や timeframe などの監視条件をいろいろ変更してログ監視ルールを作成していく感じになります

また今回の構成だと rules ディレクトリにルールファイルをどんどん追加していく感じになりますが追加したルールファイルが1つでも壊れている(YAML構文エラーや必須のディレクティブが定義されていないなどがある)と ElastAlert2 自体が起動しないので注意してください

今回紹介した機能以外にもたくさんの機能があるので興味があれば参考サイトから公式のドキュメントを参照してみてください

参考サイト

2025年9月20日土曜日

docker で ElasticSearch9 を試す

docker で ElasticSearch9 を試す

概要

過去にES8を試しました
ES9が出たので久しぶりに試してみました

環境

  • macOS 15.6.1
  • docker 28.4.0
    • ElasticSearch 9.0.7
    • Kibana 9.0.7
    • fluentd 1.19-1

ElasticSearch の起動

  • docker run -d -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" --name es docker.elastic.co/elasticsearch/elasticsearch:9.0.7

パスワードなどの確認や CA 証明書の取得などは同じ流れでした

  • docker exec -it es /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic -a
  • docker cp es:/usr/share/elasticsearch/config/certs/http_ca.crt .
  • curl --cacert http_ca.crt -u elastic https://localhost:9200
Enter host password for user 'elastic':
{
  "name" : "528f80bd51ca",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "pBMuWf9vTBuFEj1PHoZiFA",
  "version" : {
    "number" : "9.0.7",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "c6d8fb31b39450a223671e79141dd1c4b2759b5f",
    "build_date" : "2025-09-10T22:06:39.784049935Z",
    "build_snapshot" : false,
    "lucene_version" : "10.1.0",
    "minimum_wire_compatibility_version" : "8.18.0",
    "minimum_index_compatibility_version" : "8.0.0"
  },
  "tagline" : "You Know, for Search"
}

エンロールメントトークンも取得しておきます

  • docker exec -it es /usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s kibana --url "https://localhost:9200"

Kibana 起動

  • docker run -d -p 5601:5601 --name kibana docker.elastic.co/kibana/kibana:9.0.7

Kibana ログイン時に認証コードが必要なので取得しておきます

  • docker exec kibana bin/kibana-verification-code

ログ送信

fluentd コンテナを使って送信します

  • vim Dockerfile
FROM fluent/fluentd:v1.19-1

USER root
RUN gem install fluent-plugin-elasticsearch
  • docker build -t my_fluentd .

fluent.conf を作成します
ES9 に接続するには認証情報などが必要になります
ssl_verify=false を設定しないと fluentd -> es で SSL のエラーが発生しました

  • vim fluent.conf
<source>
  @type forward
  port 24224
  bind 0.0.0.0
</source>

<match docker.**>
  @type copy
  <store>
    @type stdout
  </store>
  <store>
    @type elasticsearch
    user elastic
    password xxx
    ca_file /fluentd/etc/http_ca.crt
    ssl_verify false
    scheme https
    host 192.168.1.152
    port 9200

    logstash_format true
    logstash_prefix fluentd
  </store>
</match>

@timestamp フィールドを使うので logstash_format: true を設定しています

  • docker run --name fluentd -d -p 24224:24224 -p 24224:24224/udp -v $(pwd):/fluentd/etc -e FLUENTD_CONF=fluent.conf my_fluentd

あとは fluentd にログを投げるコンテナを起動すれば OK です

  • docker run --rm --log-driver=fluentd --log-opt fluentd-address=host.docker.internal:24224 --log-opt tag="docker.{{.Name}}" alpine /bin/sh -c "while :;do date; sleep 3; done;"

動作確認

Kibana を確認して fluentd インデックスにログがあることを確認します
Discover で棒グラフが表示されない場合は

最後に

ElasticSearch9 を docker で動かしてみました
認証方法などはほぼ ES8 と変わってませんでした

fluentd からログを送信する際にも認証情報は必要になるので注意しましょう

参考サイト

2025年9月19日金曜日

emacs の copilot-chat.el で MCP サーバと連携する

emacs の copilot-chat.el で MCP サーバと連携する

概要

まだテスト段階ですが使えるようなので使ってみました
モデルは gpt4-1 にしています

emacs はバージョン30以上が必要です

環境

  • Ubuntu 24.04
  • emacs 30.1
    • copilot-chat.el 20250909.1505
    • mcp.el 20250829.1241

設定

copilo-chat 起動後に設定が読み込まれその後 mcp-hub-start-all-server を実行すると mcp サーバが起動するようにします

MCP にアクセスを許可するディレクトリ (/home/devops) はシンボリックリンクだとうまく動作しないので実フォルダを指定しましょう

(use-package
 mcp
 :ensure t
 :after copilot-chat
 :custom
 (mcp-hub-servers
  '(("filesystem" .
     (:command
      "npx"
      :args
      ("-y"
       "@modelcontextprotocol/server-filesystem"
       "/home/devops")))))
 :config (require 'mcp-hub)
 :hook (after-init . mcp-hub-start-all-server))

MCPサーバ起動

  • copilot-chat
  • mcp-hub-all-start-server
Started server filesystem (/1/1)

となれば起動完了です
ps aux | grep npx などでプロセスを確認しても OK です

copilot-chat.el に MCP サーバ登録

  • copilot-chat-set-mcp-servers

上記を実行するとまずは追加する MCP サーバを選択するミニバッファが起動します
今回は1つしかないですが複数ある場合はその分を数字を教えて有効にします
All でも OK です

emacs 上にまだ copilot-chat インスタンスが起動していない場合は新規で作成するか既存のインスタンスを選択するミニバッファが表示されます

自分が試してみた感じ新規でインスタンスを作成しないと MCP サーバを使ってくれなかったので新規で作成しましょう

動作確認

copilot-chat のバッファで指示してみましょう   ちゃんと結果が返ってくることを確認します

MCP 初回実行時はツールを実行する確認があるので yes で許可します

最後に

emacs + copilot-chat.el + mcp を試してみました
ちゃんと Copilot Chat バッファで命令した際に MCP を使うことが確認できました
公式だとモデルは Claude のほうが良いようなのですが gpt4-1 でもちゃんと動作しました

今回は copilot-chat 起動 -> MCP サーバ起動 -> MCP サーバを copilot-chat を連携という感じの3段階で設定したので面倒な感じはしますが .emacs に自動起動する設定を書けばすべて一発で起動することもできます

MCP と関係はないですがまだ vscode の Github Copilot Chat の拡張にあるエージェントモードには対応していないようです (参考サイトの issues 参照)

参考サイト

2025年9月18日木曜日

Gitlab で LDAP のログインテストをスクリプトから行う

Gitlab で LDAP のログインテストをスクリプトから行う

概要

過去に CLI を使った方法を紹介しました
CLI の場合 Gitlab インスタンスに ssh が必要なのと接続チェックしか行えないので実際のログインまでのテストはできません

今回は実際にログインするテストを ssh なしで実装してみました
UI と同じリクエストを Python スクリプトから送信すれば OK です

環境

  • Gitlab 18.1.6
  • Python 3.12.11
    • requests 2.32.5
    • beautifulsoup4 4.13.5

サンプルコード

import requests
from bs4 import BeautifulSoup

# GitLab の URL
GITLAB_URL = "https://your-gitlab-fqdn"

# LDAP ユーザーの資格情報
USERNAME = "hawk"
PASSWORD = "xxxxxxxx"
LDAP_NAME = "ldapserver01"


def test_ldap_login():
    # セッションを作成
    session = requests.Session()

    # ログインページにアクセスして CSRF トークンを取得
    login_page = session.get(f"{GITLAB_URL}/users/sign_in")
    if login_page.status_code != 200:
        print("ログインページへのアクセスに失敗しました。")
        return

    # CSRF トークンを取得
    soup = BeautifulSoup(login_page.text, "html.parser")
    element = soup.find("input", {"name": "authenticity_token"})
    if element is None:
        print("authenticity_tokenが見つかりませんでした。")
        return
    csrf_token = element["value"]  # type: ignore

    # ログインデータを準備
    login_data = {
        "username": USERNAME,
        "password": PASSWORD,
        "authenticity_token": csrf_token,
    }

    # ログインリクエストを送信
    response = session.post(
        f"{GITLAB_URL}/users/auth/{LDAP_NAME}/callback",
        data=login_data,
    )

    # レスポンスを確認
    if response.status_code == 200 and "Invalid Login or password" not in response.text:
        if "Welcome to GitLab" in response.text:
            print("LDAP ログインテストが成功しました。")
        else:
            print("LDAP ログインテストに失敗しました。")
            print("レスポンスに 'Welcome to GitLab' が含まれていません。")
    else:
        print("LDAP ログインテストに失敗しました。")
        print("ステータスコード:", response.status_code)
        print("レスポンス:", response.text)


if __name__ == "__main__":
    test_ldap_login()

少し解説

LDAP_NAME の部分は gitlab.rb に設定した ldap_name を設定してください
クロスサイト対策として CSRF トークンが必要になるのでそれをページから取得しリクエストに含める必要があります

Gitlab の API はなく直接 UI ページにアクセスして UI のフォームと同じリクエストを送信している感じです

最後に

これで LDAP のログインテストも ssh せずにテストできます

2025年9月17日水曜日

nifcloud の Gitlab インスタンスにリモートアクセスVPNゲートウェイ経由でプライベートIPアドレスでアクセスする方法

nifcloud の Gitlab インスタンスにリモートアクセスVPNゲートウェイ経由でプライベートIPアドレスでアクセスする方法

概要

前回EasyRSAを使って nifcloud のリモートアクセスVPNゲートウェイを接続しました
今回は nifcloud 側に Gitlab インスタンスにプライベートIPを割り当て割り当てたプライベートIPを使ってリモートアクセスVPNゲートウェイ経由でアクセスしてみます

環境

  • macOS 15.6.1
    • OpenVPN Connect-3.7.1

Gitlab インスタンスにプライベートIPを割り振る

前回リモートアクセスVPNゲートウェイ用に作成したPVLANを使って Gitlab インスタンスにプライベートIPを割り振ります

Gitlab インスタンスのファイアウォールに macOS からのアクセスを許可する

macOS 側の IP 帯は 192.168.1.0/24 なのでこれを追加します
また VPN 経由の場合 192.168 から 172.16 に変換されてアクセスも来るので PVLAN の 172.16.0.0/16 も許可します

確認用で ICMP を追加していますがこれはなくても OK です

macOS 側の hosts ファイルを編集する

プライベートIPでアクセスできるようにします

  • sudo vim /private/etc/hosts
172.16.0.100    xxx.jp-east-1.gitlab.devops.nifcloud.com

OpenVPN Connect を使って VPN 接続する

前回登録したプロフィールを使って接続します

アクセスできるか確認する

hosts に登録した FQDN でアクセスできるか確認しましょう
問題なくアクセスできれば Gitlab ログインの画面が表示されます

あくせすできない場合は ping を打ってみたり Gitlab 側のファイアウォールの設定などを確認しましょう

FQDN でうまくアクセスできない場合は

macOS の場合 /private/etc/hosts に書いた情報を優先的に使ってくれるのですがキャッシュがあるとそれが更に優先されます

一度キャッシュを削除してから再度アクセスしてみてください

  • sudo dscacheutil -flushcache
  • sudo killall -HUP mDNSResponde

以下でプライベートIPになることを確認しましょう

  • dscacheutil -q host -a name xxx.jp-east-1.gitlab.devops.nifcloud.com
name: xxx.jp-east-1.gitlab.devops.nifcloud.com
ip_address: 172.16.0.100

最後に

nifcloud のリモートアクセスVPNゲートウェイを使って Gitlab インスタンスにプライベート IP でアクセスしてみました

hosts ファイルを編集する必要があるのが面倒ですそればっかりはどうしようもないかなと思います

2025年9月16日火曜日

nifcloud のリモートアクセスVPNゲートウェイを使って macOS とニフクラをVPN接続する

nifcloud のリモートアクセスVPNゲートウェイを使って macOS とニフクラをVPN接続する

概要

試してみたのでメモです
macOS 側は OpenVPN Connect を使います

環境

  • macOS 15.6.1
    • OpenVPN Connect-3.7.1

事前準備

こちらの記事を参考にサーバ証明書とクライアント証明書を作成しておきましょう
また作成したサーバ証明書は nifcloud 側にアップロードしておきます

PVLAN 作成

作成時の設定ポイントは以下です
この CIDR は接続元の macOS 側で払い出されているプライベート IP 帯と被らないようにしましょう
なお今回 macOS 側は 192.168.1.0/24 がプライベート IP 帯になります

  • CIDR -> 172.16.0.0/16

リモートアクセスVPNゲートウェイの作成

作成時の設定ポイントは以下です
IP アドレスは nifcloud 側に作成した PVLAN 上の CIDR 範囲から指定します
ネットワークプールのCIDR は macOS 側のプライベート IP 帯を指定します
サーバ証明書は Let’s Encrypt で取得した fullchain.pem を指定します
なので CA 証明書は fullchain.pem から分割した CA 証明書を nifcloud にアップロードして指定します

  • IPアドレス -> 172.16.0.1
  • ネットワークプールのCIDR -> 192.168.1.0/24
  • サーバー証明書 -> アップロードしたサーバ証明書
  • CA 証明書 -> アップロードしたCA証明書

ユーザの追加

リモートアクセスVPNゲートウェイにユーザを追加します
作成したリモートアクセスVPNゲートウェイユーザを選択しプルダウンから「ユーザ作成」で追加します

好きなユーザ名パスワードで作成しましょう
なおこのユーザ名とパスワードは macOS から接続時に使います

クライアント設定ファイルのダウンロード

作成したリモートアクセスVPNゲートウェイユーザを選択し「クライアント設定ファイル->ダウンロード」でダウンロードできます

これを使って OpenVPN Connect から接続します

ovpn の編集

nifcloud からダウンロードした ovpn ファイルはそのままでは使えないので編集します
事前に作成したクライアント証明書、クライアント証明書キー、CA証明書の情報をダウンロードした ovpn ファイルを開いて編集します

  • <cert></cert> タグの追加
  • <key></key> タグの追加
  • <ca></ca> タグの追加
  • 各種 [inline] のある行の削除

OpenVPN Connect インストール

  • brew install openvpn-connect

OpenVPN Connect を使って接続テスト

ダウンロードした ovpn ファイルを編集後にダブルクリックすると勝手に設定を取り込んでくれます
あとは接続し先ほど作成したユーザ名とパスワードで接続できるか確認しましょう

上記のようになれば接続完了です

最後に

nifcloud のリモートアクセスVPNゲートウェイを使って macOS から VPN 接続してみました
あとは nifcloud 上に作成した PVLAN に接続するインスタンスを作成しプライベート IP アドレスを付与すれば自宅の Mac からプライベート IP で nifcloud 上のインスタンスにアクセスできるようになります

参考サイト

2025年9月15日月曜日

macOS で EasyRSA を使ってサーバ証明書とクライアント証明書を作成する

macOS で EasyRSA を使ってサーバ証明書とクライアント証明書を作成する

概要

OpenVPN で接続するために使います
ちゃんとした認証局(CA)からサーバ証明書もクライアント証明書も発行できる場合はそちらを使いましょう

環境

  • macOS 15.6.1
  • EasyRSA 3.2.4

インストール

  • brew install easy-rsa

初期化

  • easyrsa init-pki
Notice
------
'init-pki' complete; you may now create a CA or requests.

Your newly created PKI dir is:
* /opt/homebrew/etc/easy-rsa/pki

Using Easy-RSA configuration:
* undefined

認証局の作成

  • easyrsa build-ca

パスワードと Common Name を入力します

Enter New CA Key Passphrase: 
Passphrase must be at least 4 characters!

Enter New CA Key Passphrase: 

Confirm New CA Key Passphrase: 

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [Easy-RSA CA]:ytest

Notice
------
CA creation complete. Your new CA certificate is at:
* /opt/homebrew/etc/easy-rsa/pki/ca.crt

Build-ca completed successfully.

サーバ証明書

  • easyrsa gen-req server nopass

Common Name を入力します

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [server]:vpn.ytest

Notice
------
Private-Key and Public-Certificate-Request files created.
Your files are:
* req: /opt/homebrew/etc/easy-rsa/pki/reqs/server.req
* key: /opt/homebrew/etc/easy-rsa/pki/private/server.key

このままだと使えないのでCAで署名します

  • easyrsa sign-req server server

Confirm requested details は yes にします
あとは先程設定した認証局用のパスワードを入力すれば server.crt が作成されます

Please check over the details shown below for accuracy. Note that this request
has not been cryptographically verified. Please be sure it came from a trusted
source or that you have verified the request checksum with the sender.
You are about to sign the following certificate:

  Requested CN:     'vpn.ytest'
  Requested type:   'server'
  Valid for:        '825' days


subject=
    commonName                = vpn.ytest

Type the word 'yes' to continue, or any other input to abort.
  Confirm requested details: yes

Using configuration from /opt/homebrew/etc/easy-rsa/pki/96bd4afc/temp.02
Enter pass phrase for /opt/homebrew/etc/easy-rsa/pki/private/ca.key:
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :ASN.1 12:'vpn.ytest'
Certificate is to be certified until Dec 15 07:19:19 2027 GMT (825 days)

Write out database with 1 new entries
Database updated

WARNING
=======
INCOMPLETE Inline file created:
* /opt/homebrew/etc/easy-rsa/pki/inline/private/server.inline


Notice
------
Certificate created at:
* /opt/homebrew/etc/easy-rsa/pki/issued/server.crt

OpenVPN サーバに設定するのは以下の3つです

  • サーバ証明書 /opt/homebrew/etc/easy-rsa/pki/issued/server.crt
  • サーバ秘密鍵 /opt/homebrew/etc/easy-rsa/pki/private/server.key
  • CA証明書 /opt/homebrew/etc/easy-rsa/pki/ca.crt

クライアント証明書

  • easyrsa gen-req client1 nopass

Common Name を入力します

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [client1]:macos.ytest

Notice
------
Private-Key and Public-Certificate-Request files created.
Your files are:
* req: /opt/homebrew/etc/easy-rsa/pki/reqs/client1.req
* key: /opt/homebrew/etc/easy-rsa/pki/private/client1.key

あとはサーバ証明書同様 CA で署名します
yes と CA のパスワードを入力して完了です

  • easyrsa sign-req client client1
Please check over the details shown below for accuracy. Note that this request
has not been cryptographically verified. Please be sure it came from a trusted
source or that you have verified the request checksum with the sender.
You are about to sign the following certificate:

  Requested CN:     'macos.ytest'
  Requested type:   'client'
  Valid for:        '825' days


subject=
    commonName                = macos.ytest

Type the word 'yes' to continue, or any other input to abort.
  Confirm requested details: yes

Using configuration from /opt/homebrew/etc/easy-rsa/pki/25c39202/temp.02
Enter pass phrase for /opt/homebrew/etc/easy-rsa/pki/private/ca.key:
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :ASN.1 12:'macos.ytest'
Certificate is to be certified until Dec 15 07:25:17 2027 GMT (825 days)

Write out database with 1 new entries
Database updated

WARNING
=======
INCOMPLETE Inline file created:
* /opt/homebrew/etc/easy-rsa/pki/inline/private/client1.inline


Notice
------
Certificate created at:
* /opt/homebrew/etc/easy-rsa/pki/issued/client1.crt

OpenVPN Connect のクライアント側には以下の3つのファイルを使います

  • クライアント証明書 /opt/homebrew/etc/easy-rsa/pki/issued/client1.crt
  • クライアントキー /opt/homebrew/etc/easy-rsa/pki/private/client1.key
  • CA証明書 /opt/homebrew/etc/easy-rsa/pki/ca.crt

最後に

OpenVPN は基本的にはサーバ証明書とクライアント証明書の2つが必要になります
Let’sEncrypt など無料の証明書は基本的に「サーバ証明書」しか発行できないのでクライアント認証などでクライアント証明書が必要になる場合はEasyRSAなどを使ってオレオレ証明書を作成する必要があります

参考サイト

2025年9月14日日曜日

NanoBananaのAPIを使って4コマ漫画を描いてもらった

NanoBananaのAPIを使って4コマ漫画を描いてもらった

概要

前回 NanoBanana に4コマ漫画を描いてもらいました
今回は API を使って描いてもらいました
なお Google AI Studio からは無料で使えますが API は有料なのでご注意ください
料金はだいたい1回で6円くらいです

環境

  • NanoBanana
    • Google AI Studio 2025/09/10 時点
  • Python 3.12.11
    • google-genai 1.36.0

サンプルコード

参照画像を配置するディレクトリなどは適宜変更してください
複数画像のアップロードに対応していますがその分料金もかかります

import argparse
from io import BytesIO

from google.genai.client import Client
from google.genai.types import GenerateContentResponse
from PIL import Image


class NanoBananaClient:
    API_KEY = "xxx"
    MODEL = "gemini-2.5-flash-image-preview"

    response: GenerateContentResponse
    client: Client

    def __init__(self) -> None:
        self.client = Client(api_key=self.API_KEY)

    def run(self, prompt: str):
        # チャットを作成
        chat = self.client.chats.create(model=self.MODEL)
        # プロンプトと複数画像を渡す
        self.response = chat.send_message([prompt] + self.__open_images())
        # 結果を保存する
        self.__save()

    def __open_images(self) -> list:
        # 複数の画像を開く
        image_files = ["4panel.png", "reforg.jpeg"]
        # ref1.pngからref7.pngまでのファイルをimage_filesリストに追加
        for num in range(1, 8):
            image_files.append(f"ref{num}.png")
        images = [Image.open(f"nanobanana/{path}") for path in image_files]
        return images

    def __save(self):
        # 結果の保存
        if self.response.candidates is None:
            raise RuntimeError("No candidates returned")
        if self.response.candidates[0].content is None:
            raise RuntimeError("No content returned")
        parts = self.response.candidates[0].content.parts
        if parts is not None:
            for i, part in enumerate(parts):
                if part.text is not None:
                    # テキスト情報は標準出力
                    print(part.text)
                elif part.inline_data is not None and part.inline_data.data is not None:
                    # イメージはローカルに保存
                    image = Image.open(BytesIO(part.inline_data.data))
                    image.save(f"generated_image_{i}.png")


class NanoBananaPrompt:
    thema: str

    def __init__(self, thema: str = "AIと人間の共存") -> None:
        self.thema = thema

    def create(self) -> str:
        prompt = f"""
        4コマ漫画を描いてください。4コマ漫画のテーマは「{self.thema}」です。
        4コマの配置は4panel.pngを参考にしてください。
        各コマに登場させる人物はref1.pngからref7.pngまでの画像とreforg.jpegを参考にしてください。
        各コマには吹き出しを使って日本語のセリフを入れてください。
        各コマの背景はテーマに沿ったものにしてください。
        """
        return prompt


def main():
    parser = argparse.ArgumentParser(description="4コマ漫画生成スクリプト")
    parser.add_argument("thema", type=str, help="4コマ漫画のテーマ")
    args = parser.parse_args()

    nbp = NanoBananaPrompt(args.thema)
    prompt = nbp.create()
    print(prompt)
    nbc = NanoBananaClient()
    nbc.run(prompt)


if __name__ == "__main__":
    main()

生成された画像は以下です

最後に

NanoBanana の API を使って画像を生成してみました
大量生成したい場合にはやはり API になるかなと思います

再度注意ですが Google AI Studio では無料ですが Gemini API (NanoBanana) は有料です
また NanoBanana は無料枠もないのでいきなり課金となるのでそれも注意してください

参考サイト