サイオステクノロジー(Tech.Lab)のブログ - TECH PLAY

TECH PLAY

サイオステクノロジー(Tech.Lab)

サイオステクノロジー(Tech.Lab) の技術ブログ

全737件

はじめに 前回 は、KubernetesのバックアップツールであるVeleroのインストールと、MinIOを保存先とした初期設定について解説しました 。 前回のハンズオンが終わっていれば、環境構築が完了し、Veleroサーバーがクラスター内で正常に稼働している状態になっているかと思います。 本記事では、PersistentVolume (PV) を伴わないステートレスなアプリケーションを対象に、クラスターリソース(Kubernetesオブジェクトの定義)のバックアップ手順を解説します。 PVを含むステートフルなデータのバックアップについては、次回以降の記事で詳しく取り扱う予定です。 環境構築の前提 本手順は、前回の記事で構築した以下の環境が動作していることを前提とします。 Kubernetesクラスター: Minikube (v1.36.0) が起動中であること  Velero: v1.17.1 がインストール済みで、MinIOへの接続設定が完了していること  CLIツール: kubectl および velero コマンドが利用可能であること MinIO: RELEASE.2025-09-07T16-13-09Zが起動中であること バックアップ対象リソースのデプロイ アプリケーションの作成 まずはバックアップの検証用として、Deployment、Service、ConfigMapを含むシンプルなNginxアプリケーションを作成します。今回は永続ボリューム(PV / PVC)を含まない構成とします。 demo-app.yaml apiVersion: v1 kind: Namespace metadata: name: demo --- apiVersion: v1 kind: ConfigMap metadata: name: nginx-config namespace: demo labels: app: nginx data: index.html: | <html> <head><title>Velero Demo</title></head> <body> <h1>Hello, Velero!</h1> <p>This is a cluster resource backup demo.</p> </body> </html> --- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment namespace: demo labels: app: nginx spec: replicas: 1 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:latest ports: - containerPort: 80 volumeMounts: - name: html-volume mountPath: /usr/share/nginx/html volumes: - name: html-volume configMap: name: nginx-config --- apiVersion: v1 kind: Service metadata: name: nginx-service namespace: demo labels: app: nginx spec: selector: app: nginx ports: - protocol: TCP port: 80 targetPort: 80 type: ClusterIP アプリケーションのデプロイと確認 次は、作成したマニフェストを適用し、リソースが作成されたことを確認します。 $ kubectl apply -f demo-app.yaml $ kubectl get all -n demo # 出力例 NAME READY STATUS RESTARTS AGE pod/nginx-deployment-856dc6c76b-v6crd 1/1 Running 0 8s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/nginx-service ClusterIP 10.43.107.135 <none> 80/TCP 8s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/nginx-deployment 1/1 1 1 8s NAME DESIRED CURRENT READY AGE replicaset.apps/nginx-deployment-856dc6c76b 1 1 1 8s 以上で アプリケーションの準備ができたので、Veleroを使用してバックアップを取得します。 基本的なバックアップコマンドの実行 バックアップの実行 ここではNamespace内のデータをバックアップします。そのため、velero backup createコマンドに–include-namespaces オプションをつけて対象のNamespaceを指定します。 $ velero backup create demo-resource-backup --include-namespaces demo # 出力例 Backup request "demo-resource-backup" submitted successfully. Run `velero backup describe demo-resource-backup` or `velero backup logs demo-resource-backup` for more details. コマンドを実行すると、「Backup request “demo-resource-backup” submitted successfully.」と表示され、バックアップ処理がバックグラウンドで開始されます。 状態の確認 バックアップが正常に完了したか確認するには velero backup describe コマンドを使用します。出力結果の Phase が Completed になっていれば成功です。 $ velero backup describe demo-resource-backup # 出力例 Name: demo-resource-backup Namespace: velero Labels: velero.io/storage-location=default-backup-storage-location Annotations: velero.io/resource-timeout=10m0s velero.io/source-cluster-k8s-gitversion=v1.32.3+rke2r1 velero.io/source-cluster-k8s-major-version=1 velero.io/source-cluster-k8s-minor-version=32 Phase: Completed ... 詳細なログを確認したい場合は、以下のコマンドを実行します。 $ velero backup logs demo-resource-backup バックアップデータのオブジェクトストレージでの確認 Veleroが正しくMinIOへデータを保存できているか確認します。 前回の記事で作成した minio-client Pod  を使用して、バケットの中身を確認します。 $ kubectl exec -it minio-client -- /bin/sh # バケット内のバックアップデータを確認(エイリアス 'myminio' は設定済みとする) $ mc ls -r myminio/minio-bucket/backups/demo-resource-backup 以下のように、JSON形式のメタデータファイルや、リソース定義が含まれた圧縮ファイルが表示されれば、オブジェクトストレージへの保存は成功しています。 [202X-XX-XX XX:XX:XX UTC] 4KB velero-backup.json [202X-XX-XX XX:XX:XX UTC] 2KB demo-resource-backup-logs.gz [202X-XX-XX XX:XX:XX UTC] 15KB demo-resource-backup-resource-list.json.gz … リソースの絞り込みと除外名前空間 実運用では、Namespace丸ごとではなく、特定のリソースのみを対象にしたり、逆に特定のリソースを除外したい場面があります。 ラベルによる絞り込み –selector オプションを使用すると、KubernetesのLabel Selectorの記法で対象を絞り込めます。 # app=nginx ラベルが付与されたリソースのみをバックアップ $ velero backup create nginx-only --selector app=nginx --include-namespaces demo 特定リソースの除外 例えば、「Event情報は不要」といったケースで –exclude-resources を使用します。 $ velero backup create no-events --include-namespaces demo --exclude-resources events システムNamespaceの除外 バックアップを取る際、kube-system や velero 自身を含めると、リストア時に競合等のトラブルになることがあります。これらを除外するには –exclude-namespaces を使用します。 $ velero backup create cluster-resources --exclude-namespaces kube-system,velero Hookの活用 VeleroのHook機能を使うと、バックアップ実行の前後にコンテナ内で任意のコマンドを実行できます。今回はバックアップ実行の記録を残すシンプルな例を紹介します。 Hookは対象のPodにアノテーションを付与することで設定します。 $ kubectl annotate pod -n demo -l app=nginx \ pre.hook.backup.velero.io/command='["/bin/sh", "-c", "echo [$(date)] Backup started >> /tmp/backup.log"]' \ pre.hook.backup.velero.io/container=nginx この設定後にバックアップを取得すると、対象Pod内の /tmp/backup.log に実行日時が記録されます。これはバックアップ処理が正しくPodにアクセスできているかの疎通確認としても利用できます。 スケジュールバックアップの設定 日次や週次など、定期的なバックアップを自動化するには velero schedule create コマンドを使用します。設定方法はLinuxのCron記法と同様ですが、デフォルトのタイムゾーンがUTCになっているため、時刻はUTCで指定する必要がある点には注意してください。 # 毎日午前3時(UTC)に demo ネームスペースをバックアップ $ velero schedule create daily-demo --schedule="0 3 * * *" --include-namespaces demo 設定したスケジュールは以下で確認できます。 $ velero schedule get # 出力例 NAME STATUS CREATED SCHEDULE BACKUP TTL LAST BACKUP SELECTOR PAUSED daily-demo Enabled 2026-08-04 01:43:39 +0000 UTC 0 3 * * * 0s n/a <none> false スケジュールに基づいて実行されたバックアップは、daily-demo-<タイムスタンプ> という命名規則で保存されていきます。 まとめ 本記事では、PersistentVolumeを含まないクラスターリソース(Deployment、Service、ConfigMap)に焦点を当て、Veleroによるバックアップ手法を解説しました。 velero backup create でNamespace単位のバックアップが可能 バックアップデータはオブジェクトストレージ(MinIO)に格納される ラベルセレクタや除外設定により、必要なリソースだけを柔軟に管理できる 次回は、今回取得したバックアップからのリストアについて深掘りしていきます。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Velero実践:クラスターリソースのバックアップ first appeared on SIOS Tech Lab .
はじめに こんにちは!サイオステクノロジーのなーがです。 前回、 Claude Code のサブエージェントが勝手に多段委譲してトークンを溶かす問題を、hooks で機械的に止めた話 を書きました。「プロンプトでのお願いは守られないことがあるので、破られると困るルールは hook に落とす」という教訓ですね。 その味をしめて、同じ個人開発の Python プロジェクトで、今度は テスト に同じ手を使いました。Stop hook で「編集した領域のテストが通るまでセッションを終わらせない」ゲートを作ったんです。 しばらくは満足していたのですが、あるとき「テストゲートが過剰に発生して、トークンと実装時間を無駄にしている」という指摘を受けました。それで腰を据えて調査したところ、 自分が想定していたのとまったく違う形で壊れていた ことが分かりました。 今回は、そのテストゲートを作った動機と仕組みから、調査で出てきた原因、そして「公式ドキュメントを最初に読んでいれば防げた」という一番痛い教訓までを、実測値つきで共有します。 なお、この記事で紹介する hook 一式は 最小構成サンプルとして GitHub で公開 しています。 なぜ Stop hook でテストゲートを作ったのか 「テストは通っています」が検証の代わりにならない AI にコードを書かせていると、次の2つがけっこうな頻度で起きます。 起きること 何が困るか 「テストを書きました」「テストは通っています」と報告するが、実際にはテストを実行していない 報告が検証の代わりにならない。結局こちらが毎回自分で回すことになる ファイルを編集したまま、何も検証せずに応答を終える 壊れたまま次のタスクに進み、後段で原因の切り分けが難しくなる CLAUDE.md に「実装したら必ずテストを実行してください」と書く方法は、もちろん効くときもあります。ただ、守られるときと守られないときがあって、 再現性がありません 。 前回の記事で書いた多段委譲 とまったく同じ構図ですね。 そこで、判断を AI 側に委ねるのをやめました。 hook 側でテストコマンドの終了ステータスだけを見て判定し、未検証の編集が残っている状態では停止させない 。これなら「テストを実行したかどうか」は自己申告ではなく事実になります。 Stop hook とは Claude Code の hooks の中でも、今回の主役は Stop です。 Claude が応答を終えてターンを閉じようとする直前 に発火するイベントで、ここでフックが decision: "block" を返すと、Claude は停止できずに作業を続行します。 reason に書いた文面はそのまま Claude へのフィードバックになります。 出力する JSON はこんな形です。 { "decision": "block", "reason": "テストが失敗しています。以下の失敗を確認し、全テストが成功するまで実装またはテストを修正してください。" } つまり Stop は、「終わってよいかどうかを外から審査する」ための場所です。テストゲートにはうってつけでした。 参考: Claude Code の hooks(公式ドキュメント) / hooks ガイド(公式ドキュメント) テストゲートの仕組み 構成は hook が2つだけです。編集を記録する側と、停止時に検証する側に分かれています。 .claude/ ├── hooks/ │ ├── mark-tests-pending.sh # PostToolUse(Edit|Write): 編集ファイルから対象ラベルを記録 │ ├── stop-test-gate.sh # Stop: 未検証の領域があればテストを実行し、失敗ならブロック │ └── test-gate.conf # 対象パス → ラベル → テストコマンドの対応表 ├── tests/ # hook 自体のテスト └── settings.json # 上記2つの hook の登録 正常系の流れは次のとおりです。 編集されたファイルからラベルを積む PostToolUse (matcher は Edit|Write )で発火する mark-tests-pending.sh が、編集されたファイルのパスを設定ファイルと照合し、対応する ラベル とテストコマンドを状態ファイル pending に追記します。 ここでのポイントは、記録するのがファイル名ではなく ラベル だということです。 src/ 配下を10ファイル編集しても、積まれるのは python というラベル1つ。同じラベルは1回の停止につき1度しか実行されません。 停止時にラベル分のテストを実行する Stop で発火する stop-test-gate.sh が pending を読み、残っているラベルのコマンドを順に実行します。全部成功すれば pending を消して exit 0 、つまり停止を許可します。1つでも失敗すれば decision: "block" を返して作業を続けさせます。 ブロック時の reason には、失敗したラベル名・終了ステータス・出力の末尾30行が入ります。実際の文面はこんな感じです。 テストが失敗しています(テストゲート 1/3回目)。以下の失敗を確認し、全テストが成功するまで実装またはテストを修正してください。各領域のテストコマンドは .claude/hooks/test-gate.conf を参照。 --- python 失敗 (exit 1, 出力末尾30行) --- (テストコマンドの出力) Claude はこれを読んで、そのまま修正作業に入ります。無限ループを避けるため上限は3回で、そこに達したら警告を出して停止を許可します。この3回という上限は、Claude Code 組み込みの停止ブロック上限(連続8回。 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で変更可)より厳しいので、組み込み側の強制解除には到達しません。 設定ファイルで対応づける プロジェクト固有なのは test-gate.conf だけです。 <パスglob>|<ラベル>|<テストコマンド> の3列で、上から順に照合して 最初にマッチした1行だけ を適用します。 # --- 対象外(先に除外する) --- node_modules/*|skip| */dist/*|skip| .venv/*|skip| .claude/*|skip| # --- ドキュメント: リンク切れ検査 --- docs/*|docs-links|python3 -m unittest discover -s tests -t . -k links *.md|docs-links|python3 -m unittest discover -s tests -t . -k links # --- ブラウザテスト (web/* より前に置く) --- web/e2e/*|web-e2e|cd web && npm run test:e2e:only # --- フロントエンド (npm workspace) --- web/dashboard/*|web-app-dashboard|cd web && npm test -w dashboard web/*|web-workspace|cd web && npm test --workspaces --if-present # --- Python: フルスイート (unittest) --- src/*|python|python3 -m unittest discover -s tests -t . tests/*|python|python3 -m unittest discover -s tests -t . hook の登録は .claude/settings.json にこう書きます( timeout の値は後で痛い目を見るので、あえて修正後の値を載せています)。 { "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/mark-tests-pending.sh\"", "timeout": 10 } ] } ], "Stop": [ { "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/stop-test-gate.sh\"", "timeout": 900, "statusMessage": "テストゲート: 変更領域のテストを実行中..." } ] } ] } } これで「編集したのに検証していない領域があるまま応答を終える」ことが構造的にできなくなりました。……できなくなったのは事実なのですが、代償が思ったより大きかったのです。 参考: Claude Code の settings(公式ドキュメント) 「トークンを無駄にしている」と言われて調べた 指摘を受けたとき、私の頭にあった仮説は「ブロックされる回数が多すぎるのだろう」でした。ブロックされるたびに Claude は修正作業を続けるので、そのぶんトークンを食う。分かりやすい話です。 そこで、手元に残っていた トランスクリプト166本 を全部解析してみました。 ブロックの回数は主犯ではなかった 結果はこうでした。 指標 値 編集があったセッション 95 ゲートが1回以上ブロックしたセッション 20 ブロック注入の総数 91(内訳: 1回目 56、2回目 18) 推定ゲート実行回数 244(編集セッションあたり平均 2.6) ブロックしたのは95セッション中20だけ でした。仮説は外れです。体感の悪さは「ブロックが多いこと」ではなく、 1回あたりの実行内容が重く、しかも重複していたこと に由来していました。 自分で作ったものだけに「たぶんこれだろう」で直しにいかなくて本当によかったと思います。実際の実行結果をきちんと数えてみないと、どこに原因があるのかは分からないものですね。 1回あたりの実行内容が重かった 各テストコマンドの実測値がこちらです。 コマンド 実測 Python の全テスト(unittest 1448件) 236秒 ブラウザでのレンダリングテスト(54件) 199秒 全ワークスペースのビルド + 単体テスト 60秒 lint 一式(リポジトリ全体) 32秒 単一アプリの型チェック + 単体テスト 17.6秒 ドキュメント整合性チェック単体 0.5秒 上2つが飛び抜けて重いことが分かります。そして、この重いものが1回の停止で 同時に、しかも重複して 積まれていたのが問題の本体でした。 見つかった原因 調査で出てきた原因を、ひとつずつ紹介します。どれも「動いているように見えるのに壊れている」タイプで、特に1つ目と5つ目は ゲートが黙って効かなくなる という一番まずい壊れ方をしていました。 原因1: 状態ファイルがセッションをまたいで残る まず、いちばん怖かったものからいきます。 pending は 全ラベルが成功したときにしか消えません 。ここまでは設計どおりです。問題は、公式仕様上 Stop フックがユーザーの割り込みでは発火しない ことでした。私はこれを知りませんでした。 つまり、Esc で止めたとき・ /clear したとき・コンテキストが尽きて終わったときは、 Stop が走らないので pending が残ったままセッションが終わります。そして当時の状態ファイルは全セッション共有だったので、この残骸が 次のセッションの最初の停止で実行される わけです。 実際に、 1ファイルも編集していないセッションで約12.3分のテストが走る 状態を観測しました。ユーザーから見ると「何も触っていないのに、応答を終えようとしたら数分固まる」です。これは相当に不気味な挙動でした。 そして pending 以上に危険だったのが、リトライカウンタ retries の残存です。中断で古いカウンタが2まで進んだまま残っていると、次に起きた 本物の失敗が1回ブロックしただけで打ち切り(3回)に達します 。ゲートは何も言わずに停止を許可するので、 効いていないことに誰も気付けません 。 ガードレールが誤作動して開発を止めるのは、少なくともすぐ気付けます。でも「効かなくなる」のは気付けない。ここが決定的に違うところでした。 対策として、状態ファイルを pending-<session_id> / retries-<session_id> のようにセッション単位で分け、使われなくなったものは7日で GC するようにしました。あわせて、新しい検証サイクルの開始時( pending がまだ存在しないとき)に前サイクルのカウンタを捨てます。 # 新しい検証サイクルの開始(pending がまだ無い)なら、前サイクルのリトライ # カウンタを捨てる。中断で retries だけ残ると、次に起きた本物の失敗が1回 # ブロックしただけで打ち切り(MAX_RETRIES)に達し、ゲートが黙って効かなくなる。 [ -f "$PENDING" ] || rm -f "$RETRIES_FILE" なお、 session_id が取得できなかった場合は、全セッション分の pending を取り込んでまとめて検証する安全弁を置いています。「取りこぼして黙って無反応になる」より「余計に走る」側に倒す、という判断です。異常系で迷ったら常にこちらに倒す、というのが今回の調査で身についた原則です。 原因2: ラベルの重複排除が完全一致でしか効かない 次は、体感の重さに直結していた原因です。 同じラベルの二重登録は防いでいたのですが、それは 完全一致 の話でした。実行内容が包含関係にあるラベル、たとえば「フルスイート」と「その部分集合」が同時に積まれても、名前が違うので間引けません。 実際に観測された5ラベルのキューがこちらです(ラベル名は公開リポジトリの記録に合わせた一般名です)。 ラベル 内容 コスト 重複 web-shared 共有UIパッケージ。全ワークスペースのビルド + 単体テスト + レンダリングテスト 259秒 — python Python の全テスト 236秒 — web-e2e レンダリングテスト 約245秒 レンダリングテストを2回目(しかも無駄なリビルド付き) web-app-dashboard 単一アプリの型チェック + 単体テスト 17.6秒 全ワークスペースのテストに完全に内包 docs-links ドキュメント整合性チェック 0.5秒 Python の全テストに完全に内包 合計は約12.3分。そのうち 約4.4分(36%)が純粋な重複 でした。 図にすると分かりやすいのですが、これは「テストが多い」問題ではなく、 同じテストを2回走らせている 問題です。人間なら「いま全部回したから、そっちはいいや」と判断するところを、ラベル名の一致だけで見ていたので判断できませんでした。 対策は、設定ファイルに ラベルの包含関係を宣言する 指令行を足すことでした。 # 形式: @supersedes|<上位ラベル>|<下位ラベル…(スペース区切り)> @supersedes|python|docs-links @supersedes|web-workspace|web-app-dashboard 「上位ラベルが対象に含まれているなら、下位ラベルは実行せずに捨てる」という宣言です。ここで気をつけたのは、 「実行時間が長いほうが上位」ではなく「コマンドの検証内容が下位を完全に含んでいるか」 で判断することでした。たとえばレンダリングテストは実行時間こそ長いですが、単体テストを含まないのでどのラベルの上位にもなりません。 もうひとつ、この仕組みには前提があります。 ラベルとコマンドが1対1でなければならない ことです。同じラベルに違うコマンドがぶら下がっていると、どちらが登録されるかが編集順に依存し、さらに上位ラベルで下位を間引いた結果 実際には検証されない領域が生まれます 。しかもテストは緑のまま通るので気付けません。実運用ではこの不変条件が崩れていたラベルが2つあったので、そこも分離しました。今はテストで機械的に検査しています。 原因3: 失敗すると成功済みのラベルまで再実行する 失敗したときに pending を丸ごと残していたので、1つの領域が落ちている間、 リトライのたびに無関係な領域まで再実行 していました。この構成なら 12.3分 × 最大3回です。 対策は単純で、検証が済んだ(成功した、または内包により省略した)ラベルを pending から行単位で落とすようにしました。落ちた領域を直す過程で他の領域を編集すれば、 mark-tests-pending.sh がそのラベルを積み直すので、カバレッジは落ちません。 原因4: pending の更新に排他制御がない ここからの2つは、 公式ドキュメントと照らし合わせて初めて見つかった ものです。 pending への追記は「 grep で存在確認 → 無ければ追記」という read-modify-write でした。1メッセージで複数の Edit が発行されると PostToolUse も同時に走るので、その隙間に別プロセスが割り込むと 同じラベルが二重登録 されます。結果、1回の停止でフルスイートが2回走ります。 正直「理屈上は起きうるけど、実際にはめったに踏まないのでは」と思ったので、再現実験をしてみました。 flock バリアで16並列を同時解放し、40ラウンド回します。 重複が出たラウンド 修正前 4 / 40 修正後 0 / 40 (最終確認 0 / 30) 普通に起きていました。10回に1回です。対策は flock による排他ですが、ひとつ重要な注意点があります。 テスト実行中にロックを保持しないこと です。テストは数分かかるので、握ったままだとその間の PostToolUse が全部待たされます。実行対象をスナップショットしてからロックを解放し、実行後に取り直して行単位で間引く、という順序にしました。 # フックは並列に実行される。1メッセージで複数の Edit が発行されると # PostToolUse も同時に走るため、下の grep(存在確認)と追記の間に別プロセスが # 割り込むと同じラベルが二重登録され、1回の停止でフルスイートが2回走る。 exec 9>"$LOCK" 2>/dev/null || exit 0 command -v flock >/dev/null 2>&1 && flock -x 9 ガードレールの回帰テストは「ロックを取っているつもりで取れていない」実装を検出したかったので、 外部でロックを保持している間フックが待たされること を直接確認する behavioral テストと、ロック取得記述の静的検査の二本立てにしました。 原因5: Stop の timeout が既定値のままだった 最後がこれです。個人的には、原因1と並ぶ怖さでした。 settings.json の Stop の timeout を、私は明示していたつもりで 既定値と同じ600秒のまま にしていました。「明示したから大丈夫」と思っていたわけです。 ところが公式仕様では、 タイムアウト超過は非ブロッキングエラー扱い になります。つまり、テストが時間内に終わらなかった場合、 失敗を検出しないまま停止が通ります 。 実測の最悪ケース(重いラベルが2つ積まれた場合)は約500秒でした。600秒まで、あと100秒。しかもマシンの負荷次第で簡単に前後する範囲です。ここを超えていたら、ゲートは何のエラーも出さずに「今日はテストしませんでした」と静かに素通りしていたことになります。 原因1のリトライカウンタと、まったく同じ壊れ方です。 ガードレールは、効かなくなったことを自分では教えてくれません。 対策として timeout を900秒に引き上げました。数字そのものより、「自分のテストスイートの実測の最悪ケースを知った上で、余裕を持たせて設定する」という手順のほうが大事だと思っています。 参考: Claude Code の settings(公式ドキュメント) 公式ドキュメントを後から読んだら書いてあった さて、ここが今回いちばん恥ずかしい話です。 原因4と原因5は、いずれも 公式ドキュメントに明記されていました 。 「フックは 並列に実行される 。ファイル競合を避けよ」という注意書きと、その対策としての flock の例示 無限ループ防止に stop_hook_active を使うこと タイムアウト超過が非ブロッキングエラー扱いになること そして白状すると、 初回の実装時、私はこれらのドキュメントを参照していませんでした 。読んだのは全部、インシデントの調査中です。 なぜ読まなかったのかを考えてみると、「何度か hook を作成したことがあったから」でした。前回の記事でも hooks を書いていましたし、これまでの実行結果をClaude Codeに調査させることで Stop の入力と出力の形さえ分かれば動くものは作れます。実際に動きましたし、ちゃんとブロックもしました。 動いてしまったことが、仕様を確認しない理由になっていた わけです。 事後に公式ドキュメントを照合してみると、幸い仕様に沿っていた点もありました。 Stop で decision: "block" + reason を返す形式 常に exit 0 して JSON で制御する方式(公式にも「exit 2 は JSON を無視する。構造化された制御には exit 0 + JSON」とあります) jq が無い環境向けの python3 フォールバック(公式のトラブルシューティングが挙げる jq: command not found 対策) $CLAUDE_PROJECT_DIR の利用 ブロック上限を3回にしていたこと(組み込みの停止ブロック上限である連続8回より厳しいので、強制解除には達しません) ……なのですが、これは部分的な仕様が「たまたま合っていた」だけです。合っていた項目と外していた項目を分けたのは、私の理解の深さではなく運でした。 なお stop_hook_active については、公式サンプルどおり「 true なら常に exit 0 」にすると ブロックが実質1回に制限されてゲートが弱くなる ので、そのままは採用しませんでした。自前の有界な3回制カウンタで同じ目的を満たしているため、 置き換えではなく多重化 しています。 stop_hook_active が true かつ 自前のカウンタが読めない (=進捗を追跡できない)ときに限ってループを打ち切る、という使い方です。 ここで言いたいのは、 自作のガードレールほど、土台の仕様確認を飛ばしがちだ ということです。ガードレールは普段は何も言わずに黙っています。黙っているのが正常なのか、壊れて黙っているのかは、外からは区別がつきません。だからこそ、その土台になっているイベントが「いつ発火して、いつ発火しないのか」「異常時にどちらへ倒れるのか」は、最初に確認しておくべきでした。 参考: Claude Code の hooks(公式ドキュメント) / hooks ガイド(公式ドキュメント) 直した内容と結果 実施した変更をまとめます。 変更 内容 状態のセッション分離 pending-<session_id> / retries-<session_id> 。 session_id が取れない場合は全セッション分をまとめて検証する安全弁つき。7日で GC リトライカウンタのリセット 新しい検証サイクル開始時に前サイクルのカウンタを捨てる 成功ラベルの間引き 失敗時、検証が済んだラベルを pending から行単位で落とす @supersedes 設定ファイルでラベルの包含関係を宣言し、上位が対象なら下位を捨てる ラベルとコマンドの1対1化 同じラベルに2種類のコマンドがぶら下がっていた2ラベルを分離( @supersedes の前提) flock 排他 両フックが状態ディレクトリの .lock を取る。テスト実行中は保持しない stop_hook_active カウンタ追跡不能時の保険として多重化 timeout 600 → 900秒 効果を確かめるため、原因2で紹介した5ラベルのキュー( web-shared / python / web-e2e / web-app-dashboard / docs-links )を、修正後の設定ファイルでそのままリプレイしてみました。 @supersedes の判定で、次の3つは「検証内容が上位ラベルに含まれている」として実行前に捨てられます。 捨てられるラベル 上位ラベル 捨ててよい理由 docs-links python ドキュメント整合性チェックは Python のフルスイートの中で実行される web-app-dashboard web-shared 単一アプリの型チェック + 単体テストは、全ワークスペースのビルド + 単体テストに含まれる web-e2e web-shared レンダリングテストは共有UIパッケージ側でも走る。削減量が一番大きいのはここ 結果、実際に実行されるのは python (Python のフルスイート)と web-shared (共有UIパッケージ: 全ワークスペースのビルド + 単体テスト + レンダリングテスト)の2つだけ になりました。この2つは検証範囲が互いに重ならないので、これ以上は削れません。捨てられた3ラベル分(約4.4分)がそのまま消えた形です。 さらに、2ラベルとも成功すれば pending と retries は削除されるので、テストが終わったあとに状態ディレクトリへ残るのはロックファイル( .lock )だけになります。原因1で書いた「次のセッションへ持ち越される残骸」が発生しない状態です。 修正前 修正後 5ラベル同時の1回 約12.3分 約7.9分 (-36%) 編集ゼロのセッション 約12.3分 0秒 失敗1件のリトライ 全ラベル再実行 × 最大3回 失敗ラベルのみ 重複が消えたぶんがそのまま36%の短縮になり、いちばん不気味だった「編集していないのに走る」は 0秒になりました。 もうひとつ、記事の本筋からは少し外れますが、前回の記事で紹介した実装担当のサブエージェント( implementer )にも手を入れています。エージェント定義が編集内容に関わらず毎回フルスイート(236秒)と lint を実行しており、その後にゲートが同じものを再実行していたためです。TypeScript だけを編集した場合も Python の全テストを回していました。 実測では72回・35セッションで使われていたので、これも地味に効いていたはずです。変更領域に対応する範囲だけを検証するよう縮小し、 implementer 経由の Python 変更でフルスイートが2回走っていたのを1回にしました。 さいごに Claude Code の Stop hook で作ったテストゲートが、過剰実行と「黙って効かなくなる」壊れ方を同時に起こしていた話でした。要点をまとめます。 体感の悪さの原因は思い込みで当てにいかない。 ブロックの回数は主犯ではなかった (95セッション中20だけ)。重かったのは1回あたりの実行内容で、その36%は純粋な重複だった 状態ファイルはセッションをまたぐ 。 Stop hook はユーザーの割り込み(Esc・ /clear )では発火しないので、残骸が次のセッションで実行される。編集ゼロで約12.3分走っていた 重複排除の単位を ラベルの完全一致から包含関係へ 広げる。ただし「実行時間が長いほう」ではなく「検証内容が下位を完全に含むほう」を上位にする フックは並列に実行される ので、状態ファイルの read-modify-write は flock で排他する。ただしテスト実行中はロックを保持しない Stop の timeout 超過は 非ブロッキングエラー扱い 。実測の最悪ケースを知った上で余裕を持たせる 異常系で迷ったら、 無反応になる側ではなく余計に走る側へ倒す そして一番の教訓は、 自作のガードレールほど、土台の仕様確認を飛ばしがちだ ということです。私は「動いたから正しい」と思い込んで公式ドキュメントを読まず、結果として flock もタイムアウトの挙動も、そこに書いてあったものを事後に読むことになりました。 ガードレールは、効かなくなったことを自分では教えてくれません。 誤検知して開発を止めるタイプの故障はすぐ気付けますが、黙って素通りするタイプの故障は、次に本当のバグが漏れるまで誰も気付けません。だからこそ、作った本人が定期的に「これ、まだ効いてる?」と疑いに行く必要があります。 今回の hook 一式は、プロジェクト固有の内容を設定ファイルに閉じ込めた最小構成サンプルとして公開しています。hook 本体・設定・テスト・動作確認用の最小サンプルが入っているので、 .claude/ にコピーして test-gate.conf を自分のパスとコマンドに書き換えれば動きます。 いきなりブロックまで作り込まなくても、まずは PostToolUse で「編集されたファイルをログに残す」だけでも、自分がどれだけ検証せずに進んでいるかが見えて面白いと思います。ぜひ試してみてください!それでは! ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Claude Code のテストゲートが編集ゼロで12分!?Stop hook が「黙って効かなくなる」まで first appeared on SIOS Tech Lab .
はじめに こんにちは!サイオステクノロジーのなーがです。最近は Claude Code の Skill や Subagent を育てるのがすっかり日課になっていて、気づけば .claude/ 配下のファイルがかなりの数に膨れ上がってきました。 ただ、増えてくると困るのが 設定の記述ミス です。SKILL.md のフロントマターのキーを typo した、Skill の説明文からリンクしていたファイルをリネームして参照切れになった、 settings.json の hooks でイベント名を間違えた……。 こうしたミスの厄介なところは、 実行するまで気付けない ことです。しかも Claude Code は壊れた設定をエラーで教えてくれるとは限らず、 該当の Skill や Hook を黙って無視する ことがあります。「あれ、この Skill 最近発動してなくない?」と気付いた頃には、いつのコミットで壊れたのか分からない……なんてことも。 これはもう Linter の出番だなということで、 .claude/ 配下をコミット前に静的検証する agentlint という Linter を自作しました。今回はそのご紹介です。 agentlint とは agentlint は、Claude Code のエージェント設定( .claude/ 配下の skills / commands / agents / hooks 設定)を検証する Python 製の Linter です。フロントマターの記述ミス、壊れたファイル参照、hooks 設定の構造ミスをコミット前に静的検出し、pre-commit や CI に組み込めるようにしています。 検証対象のファイルは以下の通りです。 your-project/ ├── CLAUDE.md # 参照切れ検出 └── .claude/ ├── skills/**/SKILL.md # フロントマター検証 + 参照切れ検出 ├── commands/**/*.md # フロントマター検証 + 参照切れ検出 ├── agents/*.md # フロントマター検証 + 参照切れ検出 + 再委譲禁止記述の検証 ├── settings.json # hooks設定検証 └── settings.local.json # hooks設定検証 4つのチェック チェック内容は大きく4つに分かれています。 チェック 対象 概要 1. フロントマター検証 .claude/skills/**/SKILL.md 、 .claude/commands/**/*.md 、 .claude/agents/*.md YAML フロントマターの構文・必須キー・列挙値・型を検証 2. 参照切れ検出 上記 Markdown 本文 + ルートの CLAUDE.md $CLAUDE_PROJECT_DIR / $CLAUDE_SKILL_DIR / .claude/ 起点の参照、および本文中の相対パス参照の実在確認(すべて warning) 3. hooks 設定検証 .claude/settings.json 、 .claude/settings.local.json トップレベル hooks の構造・イベント名・matcher・handler、および command が参照するスクリプトの実在(AL305)を検証 4. サブエージェント再委譲禁止の検証 .claude/agents/*.md 本文に「他のサブエージェントを呼び出さない」等の再委譲禁止の記述があるかを検証(AL401、回帰防止) 4つ目だけ少し毛色が違いますが、これは私のプロジェクトで「サブエージェントがさらに別のサブエージェントを呼び出す多段リレーを禁止し、その旨を各エージェント定義の本文にも明記する」という運用をしているため、その記述が抜け落ちたときに警告してくれる回帰防止用のチェックです。この多段リレーを hooks で機械的に防ぐ話は 別の記事 にまとめているので、AL401 の背景が気になる方はあわせてどうぞ。なお、検証対象の一つである Agent Skills の仕組みそのものについては、 弊社メンバーのブログ記事 で紹介しているので、あわせて読んでいただけると理解が深まると思います。 なお、フロントマターの有効値リスト(イベント名、 model / effort / permissionMode の列挙値など)は、公式ドキュメントを出典としてデータ専用のモジュール( src/agentlint/spec.py )に切り出してあり、仕様変更時はこのファイルだけを更新すればよい作りにしています。 参照: Claude Code Hooks – 公式ドキュメント finding コード一覧 検出結果(finding)にはコードを振っています。AL1xx がフロントマター、AL2xx が参照・ファイルシステム、AL3xx が settings/hooks、AL4xx が運用ルール系です。 コード 重大度 内容 AL001 warning agentlint 自身の内部エラー(ツールのバグでコミットをブロックしないための最終防波堤) AL101 error フロントマターの YAML がパース不能 AL102 error 必須キー欠落(agent の name / description) AL103 error 値が無効(model / effort / permissionMode 等の列挙違反、または列挙キーの値が文字列でない) AL104 warning 未知のキー(「もしかして」候補付き) AL105 error 型違反(bool / str / list / int 等、フロントマターのキーが文字列でない場合も含む) AL106 warning description 欠落(SKILL.md のみ。commands では任意のため対象外)、または description + when_to_use 合計が1536文字超 AL201 warning アンカー付きパス参照切れ(Markdown 本文中) AL202 warning 相対パス参照が見つからない(Markdown 本文中) AL203 warning hooks が直接実行するスクリプトに実行権限がない AL301 error settings JSON がパース不能 AL302 error 存在しないイベント名(「もしかして」候補付き) AL303 error matcher の正規表現が不正 AL304 error 構造違反(配列でない、type が未知等) AL305 error hooks の command が参照するスクリプトが存在しない(スクリプトパスと確信できるトークンのみ判定対象) AL306 warning matcher 非対応イベントへの matcher 指定、未知キー AL401 warning エージェント定義本文に再委譲禁止(「他のサブエージェントを呼び出さない」等)の記述がない 出力形式 出力は ruff 風の1行1finding形式です。 ファイルパス:行番号: コード [重大度] メッセージ の並びで、最後にサマリー行が付きます。 .claude/agents/foo.md:3: AL103 [error] 'model' の値が無効: 'gpt-4' agentlint: 1 error(s), 0 warning(s) 問題がなければこうなります。 agentlint: ok (12 files checked) 設計思想: 誤検知ゼロを最優先 このツールを作るうえで一番こだわったのが、 誤検知(false positive)を出さない ことです。 pre-commit に組み込む Linter は、誤検知が1件でも起きると「またこれか」とチーム内で無効化・放置されてしまい、それ以降の見逃しの方が遥かに高コストになります。そこで agentlint では 「error にするなら warning 以上に保守的に。迷ったら検出しない」 を設計原則にしました。 error を2種類に限定した理由 コミットをブロックする error は、次の2種類だけに限定しています。 フロントマター / settings の構文・構造エラー (AL101 / AL102 / AL103 / AL105 / AL301 / AL302 / AL303 / AL304): YAML や JSON としてそもそも壊れている、必須キーがない、列挙値が無効、型が違う、など 機械的に白黒つけられるもの hooks の command が参照するスクリプトの実在確認 (AL305): 「スクリプトパスだと確信できるトークン」だけに絞った実在確認 一方で、Markdown 本文中の参照切れ(AL201 / AL202)は 常に warning です。Skill や Agent の説明文には .claude/skills/my-skill/SKILL.md のような 例示パス が頻出し、プレースホルダ判定だけでは実在するパスと原理的に区別できないためです。本文中の参照切れでコミットを直接ブロックすることはしません。 未知のキー(AL104)も同様に warning に留めています。公式ドキュメントの更新で新しいキーが追加されたとき、agentlint 側の追従が遅れると誤検知になってしまうためです。 参照切れ検出そのものも「迷ったら検出しない」方針で、プレースホルダらしき文字列( path/to 、 example 、 your- 、 my- を含む等)や、絶対パス、URL(スキーム付き・裸ドメインの両方)、ワイルドカードを含むトークンは対象外にしています。 AL305 のスクリプトパス判定の工夫 error に昇格させた AL305(hooks のスクリプト実在確認)は、その分だけ判定を慎重にしています。というのも、hooks の command 文字列には「 / を含むけどパスではない」トークンが山ほど出てくるんですよね。例えば…… sed -i 's/foo/bar/g' — sed の置換パターン jq -r ".a/b" — jq のフィルタ rm -rf *.log — glob date +%Y/%m/%d — 日付フォーマット $HOME/... — 未解決のシェル変数 これらを素朴に「パスっぽいから実在確認しよう」とやると誤検知まみれになります。そこで agentlint では、 「 $CLAUDE_PROJECT_DIR 置換後、未解決の変数( $ )や glob( * ? {} )を含まず、 .sh / .py 等の既知の実行系拡張子で終わる」トークンだけ を実在確認の対象にしています(この判定は src/agentlint/pathtokens.py に共通化しています)。 さらに、引数位置(2番目以降)のトークンは、先頭トークンがインタープリタ / ランナー( bash / python / uv / node 等)の場合のみ対象にしています。これは cp src.sh dst.sh の宛先のような「実行対象ではない引数パス」を誤検知しないための対策です。 AL001: 自身のバグでコミットをブロックしない もうひとつの防波堤が AL001 です。agentlint 自身のバグで想定外の例外が起きた場合、そのファイルの検査は諦めて AL001 の warning として報告 し、他のファイルの検査は継続します。 Linter のバグでユーザーのコミットがブロックされるのは、体験として本当に最悪なんですよね。なので「ツールが壊れても error にはしない」を仕組みとして保証しています。チェック処理は1ファイル単位で例外を捕捉するラッパー越しに実行しているので、1ファイルで転んでも残りのファイルの検査結果はちゃんと出ます。 使い方 ここからは実際の使い方です。ローカル実行 → pre-commit → CI の順に組み込んでいきます。 インストールと実行 agentlint は GitHub で公開 しています。PyPI などのパッケージレジストリには出していないので、リポジトリを clone して uv 経由で実行する形になります。 git clone https://github.com/Shotaro-Yoshinaga-sti/agentlint cd agentlint && uv sync セットアップできたら、あとは検証したいプロジェクトを --root で指定して実行するだけです。 uv run agentlint # カレントディレクトリの .claude/ を検証 uv run agentlint --root ../other # 別ディレクトリを指定 uv run agentlint --strict # warningのみでもexit code 1にする uv run agentlint --version 例えば、こんな設定ミスを仕込んだサンプルの .claude/ を用意してみます。 agents/foo.md : 必須キーの name / description が欠落、 model: gpt-4 (無効な値)、再委譲禁止の記述なし skills/deploy/SKILL.md : description を descripton と typo、本文から存在しない ./checklist.md を参照 settings.json : hooks のイベント名を PreToolUses と typo、存在しないスクリプト .claude/hooks/check.sh を command で参照 これに対して実行すると、以下の出力になります(実際の実行結果です)。 uv run agentlint --root ../broken-example .claude/agents/foo.md:1: AL102 [error] 必須キー 'description' が欠落している .claude/agents/foo.md:1: AL102 [error] 必須キー 'name' が欠落している .claude/agents/foo.md:1: AL401 [warning] サブエージェントの再委譲禁止(「他のサブエージェントを呼び出さない」等)の記述が見当たらない .claude/agents/foo.md:2: AL103 [error] 'model' の値が無効: 'gpt-4' .claude/settings.json:3: AL302 [error] 未知のイベント名 'PreToolUses'(もしかして: PreToolUse) .claude/settings.json:3: AL305 [error] hooks が参照するスクリプトが存在しない: .claude/hooks/check.sh .claude/skills/deploy/SKILL.md:1: AL106 [warning] description が設定されていない .claude/skills/deploy/SKILL.md:3: AL104 [warning] 未知のキー 'descripton'(もしかして: description) .claude/skills/deploy/SKILL.md:6: AL202 [warning] 相対パス参照が見つからない: ./checklist.md agentlint: 5 error(s), 4 warning(s) typo には「もしかして」候補が付くので、修正もすぐ終わります。これが地味に嬉しいんですよね。 exit code は error があれば 1、warning のみなら 0 です。CI で warning も落としたい場合は --strict を付けると warning のみでも exit code 1 になります。 また、 .claude/ ディレクトリが存在しない場合は何もせず exit code 0 で終了します。モノレポの一部ディレクトリなど、対象外の場所で実行されても邪魔をしません。 agentlint: .claude ディレクトリが見つかりません(/path/to/other/.claude)。何もしません。 pre-commit への組み込み agentlint は pre-commit hook としての利用を想定していて、リポジトリに .pre-commit-hooks.yaml を同梱しています。 公開しているので、利用側の .pre-commit-config.yaml にリポジトリを直接指定できます。 # .pre-commit-config.yaml - repo: https://github.com/Shotaro-Yoshinaga-sti/agentlint rev: v0.2.0 hooks: - id: agentlint 手元で改造しながら試したいときは、 pre-commit try-repo でローカルのチェックアウトを直接指定するのが手軽です。 pre-commit try-repo ../agentlint agentlint --all-files hook 定義側で files: ^(\.claude/|CLAUDE\.md) を指定してあるので、 .claude/ 配下か CLAUDE.md に変更があったコミットのときだけ動きます。 参照: pre-commit 公式ドキュメント CI での利用 pre-commit をすり抜けたケース( --no-verify でのコミットなど)に備えて、CI でも同じ検証を回しておくと安心です。GitHub Actions なら以下のようなジョブになります。 uvx --from git+... で公開リポジトリから直接取得して実行するので、事前インストールは不要です。 # .github/workflows/agentlint.yml name: agentlint on: [push, pull_request] jobs: agentlint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 - name: Run agentlint run: uvx --from git+https://github.com/Shotaro-Yoshinaga-sti/agentlint agentlint --strict ローカルの pre-commit では error のみブロック、CI では --strict で warning も含めて検知、という使い分けもできます。 既知の制限と使う上での考慮点 万能ではないので、現時点の制限も正直に書いておきます。ただ、どれも「知っていれば運用でカバーできる」類のものなので、制限ごとに「では利用者側はどう考慮すればいいか」までセットで整理します。 有効値リストは手動メンテ src/agentlint/spec.py の有効値リスト(イベント名や model / effort / permissionMode の列挙値など)は、2026-07 時点の公式ドキュメント準拠です。Claude Code 側の仕様変更に自動追従はしないため、新しいイベント名やフロントマターのキーが追加されると、 spec.py を更新するまでは誤検知(や見逃し)が起こり得ます。 使う側の考慮点としては、まず 未知のキー(AL104)が warning 止まりなのは、まさにこの事態のための設計 だと知っておくことです。仕様変更の直後に AL104 が出てもコミットはブロックされません。 公式ドキュメントに載っている正しいキーに対して AL104 が出ているなら、それは agentlint 側の追従漏れなので、その finding は無視して大丈夫です(そして spec.py に1行足せば直ります)。運用としては「公式ドキュメントの更新に気付いたら spec.py をメンテする」を回すイメージですね。 matcher の検証は Python の re による近似 hooks の matcher は Claude Code 内部では JavaScript の正規表現として解釈されますが、agentlint は Python の re モジュールで近似検証しています。両者の構文はおおむね互換とはいえ差異はあるので、JS では有効なのに Python では不正、といったパターンで誤検知 / 見逃しがあり得ます。 なので、 AL303 の error が出たときは「即修正」ではなく、「実際に Claude Code 上でその hook が動くか」を先に確認する のがおすすめです。Claude Code 上で正しく動いているなら構文差異による誤検知の可能性が高いです。そのうえで、matcher をツール名の完全一致や Bash|Edit のような単純な alternation に寄せておくと、そもそもこの構文差異を踏まなくなります。 対象はプロジェクトスコープのみ agentlint が見るのはプロジェクトスコープの .claude/settings.json / settings.local.json だけで、ユーザースコープの ~/.claude/ は対象外です。つまり、個人環境の ~/.claude/ に置いた設定が壊れていても検出されません。 これは「リポジトリにコミットされるものをコミット前に検証する」というツールの性格上の割り切りです。裏を返すと、 チームで共有したい Skill / Agent / hooks はプロジェクトスコープ(リポジトリ内の .claude/ )に寄せる 運用が前提になります。 共有物をリポジトリ側に置いておけばすべて agentlint の検証対象に入りますし、個人設定の壊れは被害が本人で閉じるので、まずは共有物を守る、という優先順位です。 AL305 が見るのは「スクリプトパスと確信できるトークン」だけ 設計思想のところ で書いた通り、AL305 の実在確認は .sh / .bash / .py / .js / .mjs / .ts の既知拡張子で終わるトークンだけが対象です。バイナリや拡張子なしスクリプトを直接実行している場合は、実在しなくても検出されません。また、引数位置のパスは先頭トークンがインタープリタ / ランナーの場合だけ見るので、 find -exec 等の別コマンドに渡したスクリプトパスも見逃します。いずれも誤検知回避を優先した 意図的な制限 です。 裏を返せば、 hooks の command を「インタープリタ + 拡張子付きスクリプトパス」の形(例: bash .claude/hooks/check.sh )に寄せておくと、AL305 の検証の恩恵をフルに受けられる ということです。凝ったワンライナーを command に直書きするより、処理を .sh / .py に切り出してシンプルに呼ぶ——という、hooks の可読性の面でもどのみち好ましい書き方に倒すほど、Linter もよく効くようになります。 このほか細かい点として、 .claude/commands/*.md では $CLAUDE_SKILL_DIR アンカーの参照を検証しません(commands では未定義のため)。 さいごに 今回は、Claude Code の .claude/ 配下を静的検証する自作 Linter「agentlint」を紹介しました。ポイントを整理します。 .claude/ 配下の設定ミスは 実行するまで気付けず 、Claude Code は壊れた設定を黙って無視することがある agentlint は フロントマター検証・参照切れ検出・hooks 設定検証・再委譲禁止記述の検証 の4チェックをコミット前に静的実行する 設計原則は 「error にするなら保守的に。迷ったら検出しない」 。error は構文・構造エラーと AL305 に限定し、誤検知でツールが放置される事態を避ける pre-commit と CI に組み込めば、壊れた設定がリポジトリに入る前に検知できる Skill や Subagent が増えてくると、 .claude/ 配下は立派な「コード」です。コードなら Linter があって然るべき、ということで作ってみましたが、導入してからはフロントマターの typo やリネーム漏れをコミット前に何度も拾ってくれています。 みなさんも .claude/ が育ってきたら、設定の静的検証を仕組み化してみてはいかがでしょうか。この記事がその参考になれば嬉しいです! ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Claude Code の .claude/ 設定を静的検証する Linter「agentlint」を作った話 first appeared on SIOS Tech Lab .
こんにちは。サイオステクノロジーの橋本です。 SW360を構築した際に非常に手間がかかったので(2営業日)、備忘録として手順をまとめ、ブログとして公開します。 注意事項 本記事は必要最低限の項目を設定して立ち上げているだけですので、セキュリティ面などに懸念が残ります。利用の際は自己責任の元、必要に応じて設定やセキュリティを見直してください。 環境 OS : RHEL podman : podman-5.4.0-12.el9_6.x86_64 podman-compose : podman-compose-1.5.0-2.el9.noarch SW360 : v1.0.0-16-g038175e7 参考:基本的な手順(公式) Deploy 2.0 Containers | SW360 公式の手順は「自端末(localhost)にSW360を構築すること」を前提としており、そのままでは外部(自端末以外)からのアクセスがまったくできません。そのため、外部からIPアドレスでアクセスできるように各種設定ファイルを修正していきます。 構築手順 1. ソースのダウンロードとネットワーク作成 まずはGitHubからソースコードを取得し、Podmanネットワークを作成します。 Bash # cd /opt/ # git clone https://github.com/eclipse-sw360/sw360-frontend.git Cloning into 'sw360-frontend' ... remote: Enumerating objects: 32749, done . remote: Counting objects: 100% (2349/2349), done . remote: Compressing objects: 100% (741/741), done . remote: Total 32749 (delta 2042), reused 1613 (delta 1607), pack-reused 30400 (from 4) Receiving objects: 100% (32749/32749), 14.61 MiB | 22.87 MiB/s, done . Resolving deltas: 100% (21359/21359), done . # cd sw360-frontend # podman network create sw360net sw360net # git describe --tags v1.0.0-16-g038175e7   2. 設定ファイルの修正 外部からのアクセスを許可するため、各設定ファイルの localhost をサーバーのIPアドレス(以下 ${IP_address} と表記)に変更し、いくつかの起動オプションを追加します。 ※ 実際の作業時は、 ${IP_address} の部分をご自身のサーバーIPに置き換えて設定してください。 config/nginx/.env.web 9行目 修正前: SERVER_DOMAIN=localhost 修正後: SERVER_DOMAIN=${IP_address} config/front-end/.env.frontend 10行目 修正前: NEXTAUTH_URL=https://localhost 修正後: NEXTAUTH_URL=https://${IP_address} 14行目 修正前: NEXT_PUBLIC_SW360_API_URL=https://localhost 修正後: NEXT_PUBLIC_SW360_API_URL=https://${IP_address} config/keycloak/.env.keycloak 27行目 修正前: KC_HOSTNAME_URL=https://localhost/kc 修正後: KC_HOSTNAME_URL=https://${IP_address}/kc 29行目 修正前:(なし) 修正後: KC_HOSTNAME_STRICT_HTTPS=false (※追記) config/sw360/.env.backend 45行目 修正前: SW360_SECURITY_JWT_ISSUERS_0_ISSUER_URI=http://localhost:8080/authorization 修正後: SW360_SECURITY_JWT_ISSUERS_0_ISSUER_URI=http://${IP_address}:8080/authorization 46行目 修正前: SW360_SECURITY_JWT_ISSUERS_1_ISSUER_URI=https://localhost/kc/realms/sw360 修正後: SW360_SECURITY_JWT_ISSUERS_1_ISSUER_URI=http://${IP_address}:8083/realms/sw360 82行目 修正前: SW360_THRIFT_SERVER_URL=http://localhost:8080 修正後: SW360_THRIFT_SERVER_URL=http://${IP_address}:8080 83行目 修正前: SW360_BASE_URL=http://localhost:8080 修正後: SW360_BASE_URL=http://${IP_address}:8080 84行目 修正前: SW360_FRONTEND_URL=http://localhost:3000 修正後: SW360_FRONTEND_URL=http://${IP_address}:3000 docker-compose.yml 42行目(ヘルスチェックのタイムアウト調整) 修正前: start_period: 60s 修正後: start_period: 600s 88行目 修正前: NEXT_PUBLIC_SW360_API_URL: ${NEXT_PUBLIC_SW360_API_URL:-https://localhost} 修正後: NEXT_PUBLIC_SW360_API_URL: ${NEXT_PUBLIC_SW360_API_URL:-http://${IP_address}} 90行目 修正前: NEXTAUTH_URL: http://localhost 修正後: NEXTAUTH_URL: http://${IP_address} 106行目付近(Keycloakの起動コマンド追加) 修正前: YAML keycloak: image: "ghcr.io/eclipse-sw360/sw360/keycloak:26.6.4" container_name: keycloak restart: unless-stopped env_file: ./config/keycloak/.env.keycloak 修正後: command: start --http-enabled=true を追加 YAML keycloak: image: "ghcr.io/eclipse-sw360/sw360/keycloak:26.6.4" container_name: keycloak command: start --http-enabled=true restart: unless-stopped env_file: ./config/keycloak/.env.keycloak 3. コンテナの起動 上記の修正が完了したら、 podman-compose を使って起動します。 初回の起動には10~15分程度要します。 Bash # podman-compose -f docker-compose.yml up -d   しばらく待った後、 podman ps コマンドで全てのステータスが (healthy) になることを確認しましょう。 Bash [root@ip-172-31-15-70 sw360-frontend] # podman ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c6547edcc86b docker.io/library/couchdb:3.5-nouveau /usr/bin/java -se... 5 minutes ago Up 4 minutes 0.0.0.0:5987-5988->5987-5988/tcp couchdb_nouveau b84fc441690a docker.io/library/postgres:18.4-alpine postgres 5 minutes ago Up 4 minutes (healthy) 0.0.0.0:5432->5432/tcp postgres bbaf3539a33e docker.io/library/couchdb:3.5 /opt/couchdb/bin/... 5 minutes ago Up 4 minutes (healthy) 0.0.0.0:5984->5984/tcp, 4369/tcp, 9100/tcp couchdb d1955264c00d ghcr.io/eclipse-sw360/sw360/keycloak:26.7.0 start --http-enab... 4 minutes ago Up 4 minutes (healthy) 0.0.0.0:8083->8083/tcp, 8080/tcp, 8443/tcp, 9000/tcp keycloak f35d08b298ac ghcr.io/eclipse-sw360/sw360:main 4 minutes ago Up 4 minutes (healthy) 0.0.0.0:8080->8080/tcp sw360 722b25096736 ghcr.io/eclipse-sw360/sw360-frontend:main node server.js 4 minutes ago Up 4 minutes 0.0.0.0:3000->3000/tcp sw360-frontend e4aeff7140e0 docker.io/library/nginx:1.31.3-trixie /bin/sh -cexp... 4 minutes ago Up 4 minutes 0.0.0.0:443->443/tcp, 80/tcp sw360_web   アクセス方法 ブラウザから https://${IP_address} でアクセス可能です。 ログインユーザとパスワードについては、公式リポジトリ内の README_DOCKER.md に記載されているデフォルトの Basic Auth 情報を参照してください。 SW360 Basic / Built-in Basic Auth 認証情報 既知の不具合について(2026年8月現在) 現在、Keycloak でのログイン後にホーム画面で NEXT_HTTP_ERROR_FALLBACK;404 のエラーが表示される既知の不具合が確認されています。 詳細や進捗については、以下の GitHub Issue をご確認ください。 [Bug] Error: NEXT_HTTP_ERROR_FALLBACK;404 displayed on Home page after Keycloak login (latest/20.1.0) #4293 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 【備忘録】RHEL 環境でのSW360コンテナ構築手順 first appeared on SIOS Tech Lab .
はじめに こんにちは!サイオステクノロジーのなーがです。2026年7月上旬、いったん提供停止されていた Claude Fable 5 が再公開されましたね。Fable 5 は並列サブエージェントのディスパッチ・管理が得意とされる一方で高価なモデルなので、メインセッションの Fable にはオーケストレーション(計画・分解・統合)だけをさせて、実作業は安価なモデルのサブエージェントに委譲する運用が定石として広まっています。 私もこの流れに乗って、個人開発の Python プロジェクトで Claude Code のマルチエージェント運用を始めてみました。ところが、いざ動かしてみると「サブエージェントの多段委譲問題」と呼ばれる典型的な落とし穴にきれいにハマり、トークンを溶かすはめになりました。 参考: 「Claude Fable 5」が復活、7月7日まではプランの上限内で試用可能(窓の杜) / サブエージェント活用で Claude Fable 5 をコスパよく運用する(Zenn) 今回は、サブエージェントを設定してからこの問題を踏むまでの流れと、「プロンプトによる指示」と「hooks による機械的な拒否」の多層防御で解決するまでの経緯を、途中でやらかした hook 自身の誤検知も含めて、実際の実装とあわせて紹介します。 サブエージェントを設定する まずは私がやった設定から紹介します。Claude Code では、 .claude/agents/ にカスタムエージェントを定義しておくと、メインセッションが Agent ツールでそれらを呼び出せます。今回は役割を3つに分けました。 investigator (調査、haiku):複数ファイルにまたがる調査・検索。読み取り専用 implementer (実装、sonnet):設計確定後のコード編集・テスト・lint reviewer (レビュー、opus):コミット前の品質・セキュリティレビュー。読み取り専用 参考: Claude Code のサブエージェント(公式ドキュメント) エージェント定義はこんな感じです( investigator.md )。読み取り専用の役割なので、 tools を参照系に絞っています。 --- name: investigator description: 複数ファイルにまたがる調査・コード検索・現状把握を行う読み取り専用エージェント。 model: haiku tools: Read, Glob, Grep, Bash --- あなたは調査専門エージェント。 - 読み取り専用で動く。ファイルの作成・編集・削除はしない。 - 結論を先に、根拠となるファイルパスと行番号(`path:line`)を添えて報告する。 あわせて、メインセッションが迷わないように、CLAUDE.md に「エージェント委譲ルール」を書きました。ポイントは、役割の対応表を 固定パイプラインではない と明言することです。 ## エージェント委譲ルール メインセッションは設計・統括・結果検証に徹し、作業はサブエージェントに委譲する。 下記は役割の対応表であり、全タスクに強制する固定パイプラインではない: - 複数ファイルにまたがる調査・検索 → `investigator` - 設計確定後の実装(コード編集・テスト・lint) → `implementer` - コミット前のコードレビュー → `reviewer` 運用原則: - 同じ作業内容を複数のエージェントに順番にリレーしない。 1つのタスクで各役割のエージェントを使うのは最大1回ずつ。 前段の結果を丸ごと次段の課題として再送するのは禁止。 - 互いに独立したタスクは、1つのメッセージで複数のAgent呼び出しを 同時に発行して並列実行する。 - 不要なフェーズは飛ばす。調査が不要なら `investigator` を起動しない。 - 単一ファイルの軽微な修正はメインセッションが直接行ってよい。 「リレー禁止」「独立タスクは並列実行」「不要フェーズのスキップ」の3点セットまで書いて、これでメインの Fable は統括に専念、実作業は3役に振れる分業体制ができました。……はずでした。 早速つまずいた「サブエージェントの多段委譲問題」 運用を始めてすぐ、ダッシュボードまわりの1タスクで様子がおかしくなりました。呼び出しツリーを見ると、メインセッションから呼ばれた implementer が(本来は禁止のはずの)Agent ツールでさらに implementer を呼び、それがまた次へ……と、気づけば 5段ネスト 。各段が2.6万トークン前後を消費し、最深段は6.5万トークンに達していました。CLAUDE.md にわざわざ「リレー禁止」と書いたのに、です。この呼び出しツリーを見つけたときは、さすがに焦りました。 あとで知ったのですが、これは「 サブエージェントの多段委譲問題 」として知られる典型的な失敗でした。委譲の「深さ」と「回数」を放任すると起きるもので、整理すると原因は「 誰が誰を呼ぶか(深さ) 」と「 何を渡すか(内容) 」という2つの軸に分かれます。 再委譲: サブエージェントがさらにサブエージェントを呼ぶ 1つ目は 構造(深さ)の問題 です。委譲されたサブエージェントが自分でも Agent ツールを使い、さらに別のサブエージェントへ仕事を投げてしまうケースです。 こうなると、実際に手を動かしているのが誰なのかメインセッションから見えなくなります。孫エージェントはユーザーの元の意図を知らないまま作業するのでコンテキストが失われ、結果の品質も制御できません。 トークン消費の面でも、単に段数に比例して増えるだけでは済みません。本来不要な中間層のエージェントが1つずつ起動すること自体がコストで、各中間層はコンテキストの読み込み・状況把握・指示の再構成といった同じような処理を重複して行います。つまり、中抜きすれば丸ごと不要だったはずのコストが、層の数だけ積み上がっていくわけです。 多段リレー: 前段の結果を丸ごと次段に再送する 2つ目は 内容の問題 で、もう少し気づきにくいです。メインセッションが「調査 → 実装 → レビュー」をパイプラインだと思い込み、 前段のエージェントが返した長大な結果をほぼそのまま次段のプロンプトに貼り付けて 順送りしてしまうケースです。 一見それらしく動いているのですが、実態は同じテキストがセッション内を何往復もしているだけです。本来メインセッションがやるべき「結果を咀嚼して、次のフェーズに必要な情報だけを渡す」という仕事が抜け落ちており、次のような問題が起きます。 結果の劣化 : 各エージェントが要点の抽出をサボり、丸投げの連鎖になる トークン消費 : 長文が段数ぶん重複して送られ、コストが跳ね上がる 制御不能 : 不要なフェーズ(調査不要のタスクでの investigator 起動など)まで律儀に実行される 冒頭の5段ネストは、まさにこの2つが同時に噴き出した状態でした。 implementer が implementer を呼んでネストが深くなっている点は再委譲そのもの、渡している内容がほぼ同じ点はリレーそのもの です。「深さ」と「内容」という別々の軸なので、原因が違えば防ぎ方も層で分かれます。これは仕組みで止めるしかありません。次章から、その対策を見ていきます。 解決アプローチ: プロンプトと hooks の多層防御 対策は1つではなく、階層の異なる4つを重ねています。すでに設定時に書いた CLAUDE.md の委譲ルールが1つ目のプロンプト層で、ここにエージェント定義の制約(もう1つのプロンプト層)と、2つの hooks(機械的な強制)を足していきます。先ほどの2軸に対応づけると、 再委譲(構造)は主にエージェント定義のツール制限で根本から止め、多段リレー(内容)は主に hook で止める という役割分担になっています。 関連ファイルの構成は以下のとおりです。 .claude/ ├── agents/ │ ├── investigator.md # 調査担当(読み取り専用) │ ├── implementer.md # 実装担当 │ └── reviewer.md # レビュー担当(読み取り専用) ├── hooks/ │ ├── agent-relay-guard.sh # PreToolUse: リレー検出・拒否 │ └── agent-turn-reset.sh # UserPromptSubmit: 履歴リセット ├── tests/ │ └── test_agent_relay_guard.py # hook の挙動テスト ├── agent-calls/ # Agent呼び出し履歴(セッションごとのJSONL、gitignore対象) └── settings.json # hooks の登録 この一式は、そのまま .claude/ に置いて使える最小構成のサンプルとして GitHub で公開 しています。hook・テスト・エージェント定義がそろっているので、動かしな がら読むとわかりやすいと思います。 エージェント定義に再委譲禁止を明記する 設定時に CLAUDE.md へ書いた委譲ルールだけでは足りませんでした。そこで .claude/agents/ の各エージェント定義にも、再委譲を禁止する制約を追記しました。先ほどの investigator.md に、次の「## 制約」セクションを足した形です。 ## 制約 - 他のサブエージェントを呼び出さない(Agentツール使用禁止)。 タスクが担当範囲を超える場合は、その旨を報告して終了する。 - 報告は結論と根拠のみを簡潔に。調査ログや試行過程を全文貼り付けない。 この「制約」セクションは implementer.md / reviewer.md にも同じ文面で入れています。ポイントは2つです。 Agent ツール使用禁止 を明記し、担当範囲を超えたら「報告して終了」という逃げ道を用意する(禁止だけだと無理に自力で解決しようとするため) 報告を「結論と根拠のみ」に絞る。 前段の報告が短ければ、そもそも丸ごとリレーする材料が生まれにくい なお investigator と reviewer は frontmatter の tools で使えるツール自体を読み取り系に絞っており、そもそも Agent ツールを持たせていません。プロンプトの制約とツール制限の二重がけです。 再委譲(サブエージェントがさらにサブエージェントを呼ぶ構造)を根本から止めているのは、実はこのツール制限です。 Agent ツールを持っていなければ、そもそもネストのしようがありません。この後の hook は、主にもう一方の軸である多段リレー(内容の丸ごと再送)を担当します。 ただし、ここまでは全部「お願い」です。CLAUDE.md もエージェント定義もプロンプトの一部でしかないので、コンテキストが長くなると平気で忘れられるんですよね。実際、明文化した後もリレーは散発しました。そこで hooks の出番です。 agent-relay-guard: PreToolUse hook でリレーを拒否する 本丸が agent-relay-guard.sh です。PreToolUse hook を Agent ツールにマッチさせ、 Agent 呼び出しが実行される前に リレーかどうかを判定して、リレーなら拒否します。 参考: Claude Code の hooks(公式ドキュメント) 実体は bash スクリプトですが、bash 部分は薄いラッパーで、判定ロジック本体はスクリプト内にヒアドキュメントで埋め込んだ Python コード( PYSCRIPT )を python3 -c に渡して実行しています(抽出〜判定〜履歴の記録までを1つの Python プロセスに一本化し、ロジックの二重管理を避けるためです)。以降のコード例は、この Python 部分からの抜粋です。 仕組みはシンプルで、セッションごとの Agent 呼び出し履歴を .claude/agent-calls/<session_id>.jsonl に記録しておき、新しい呼び出しのたびに履歴と突き合わせます。 実は最初に作ったバージョンは、この履歴を 同一セッション内でずっと 持ち続ける実装でした。これがあとで誤検知の原因になるのですが、それは後述するとして、まずは判定ロジックを見ていきます。判定は2つです。 判定1: 同一役割への2回目の呼び出しを拒否する investigator / implementer / reviewer の3役については、履歴に同じ役割の呼び出しが残っていれば一律拒否します。CLAUDE.md の「1つのタスクで各役割は最大1回ずつ」をそのまま機械化したものです(以下、最終版の該当部分の抜粋)。 ROLE_NAMES = {"investigator", "implementer", "reviewer"} if subagent_type in ROLE_NAMES: for entry in history: if entry.get("subagent_type") == subagent_type: # この reason が最終的に deny の JSON として stdout に出力される reason = ( f"同一タスク内で役割 '{subagent_type}' への2回目以降のAgent呼び出しは" f"サブエージェントの多段リレー防止のため拒否します。..." ) break この判定は、冒頭で挙げた 再委譲のバックストップ も兼ねています。 implementer は( investigator / reviewer と違って)ツール制限をかけていないため Agent ツールを持っており、プロンプトの禁止をすり抜けて implementer が implementer を呼ぶ再委譲が起こり得ます。そのネストも「同じ役割の2回目」としてここで引っかかるので、エージェント定義のツール制限を主とし、この hook が二段目の網になります。 判定2: 直前呼び出しとの prompt 類似度で「丸ごと再送」を拒否する 役割が違っても、直前の Agent 呼び出しと prompt がほぼ同じなら、それは前段の結果の丸ごと再送です。prompt を行単位に正規化(前後空白を除去し空行を捨てる)した上で、 行集合の Jaccard 係数 を計算し、0.7 を超えたら拒否します。Jaccard 係数は、2つの集合の共通要素が全体(和集合)に占める割合を表す 0〜1 の類似度指標です(以下、該当部分の抜粋)。 SIMILARITY_THRESHOLD = 0.7 def line_similarity(a_lines: list[str], b_lines: list[str]) -> float: set_a, set_b = set(a_lines), set(b_lines) union = set_a | set_b if not union: return 0.0 return len(set_a & set_b) / len(union) 「前段の結果を丸ごと貼って、末尾に指示を1行足しただけ」のようなプロンプトは、行の大半が共通するので類似度が高く出ます。逆に、フェーズごとに内容を咀嚼して書き直したプロンプトなら共通行はほとんど残らないので通過します。 文字単位ではなく行単位の集合比較にしているのは、この「コピペ再送」の検出に特化するためです。こちらの判定は3役に限らず general-purpose などすべての subagent_type に効きます。 拒否時は、PreToolUse hook の JSON 出力で permissionDecision: "deny" を返します。出力の構造は次のとおりで、理由の全文は後述の「実際の挙動」で紹介します。 { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "(拒否理由のテキスト)" } } 設計上の工夫: ガードは fail-open に倒す このガードで一番気を使ったのが異常系の扱いです。開発を止めないことが最優先なので、 判定に必要な情報が揃わないケースはすべて許可側に倒す(fail-open) 設計にしています。 stdin が空・JSON が不正 → 許可 session_id が取れない(セッション単位で状態を分離できない) → 許可 状態ディレクトリが作れない・履歴ファイルが書けない → 許可 さらに、誤検知したときのために 環境変数によるバイパス を用意しています。 AGENT_RELAY_GUARD_DISABLE=1 これをセットするとチェックも履歴の記録もすべてスキップされます。拒否メッセージ自体にこのバイパス方法を書いてあるのもポイントで、誤検知に遭遇した未来の自分(や Claude)がその場で回避策にたどり着けます。 ここまでが対策の第1弾です。これで一件落着……と思いきや、導入してみると今度は ガード自身が誤検知 を起こしました。 agent-turn-reset: ユーザー発言をタスク境界として履歴をリセットする 誤検知の症状はこうです。初版のガードは履歴をセッション単位で持っていたため、同一セッション内では役割ごとに1回しか Agent を呼べず、 独立した別タスクなのに2回目以降の呼び出しが拒否される 。午前中に implementer を使ったせいで、午後の全く別の修正依頼で implementer が呼べない、という状態です。これは明らかにおかしいですよね。 原因は判定単位のズレでした。CLAUDE.md のリレー禁止ルールは「1つのタスクで各役割は最大1回ずつ」という タスク単位 のルールなのに、初版のガードはこれを セッション単位 で判定していたのです。ルールを機械化するときは、条件だけでなく 適用単位 まで正確に写し取る必要がありました。 そこで第2弾の修正として、判定単位をセッションからタスクへ揃えました。 「ユーザーの新しい発言 = 新しいタスクの開始」 とみなし、UserPromptSubmit hook( agent-turn-reset.sh )でそのセッションの呼び出し履歴を削除します。 # UserPromptSubmit hook: ユーザーの新しい発言 = 新しいタスクの開始とみなし、 # agent-relay-guard.sh が使う「そのタスク内のAgent呼び出し履歴」をリセットする。 # 古い(7日以上前の)セッション状態ファイルを掃除する。 find "$STATE_DIR" -maxdepth 1 -name '*.jsonl' -mtime +7 -delete 2>/dev/null || true safe_session=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9_.-' '_') rm -f "$STATE_DIR/$safe_session.jsonl" 2>/dev/null || true exit 0 あわせて、ガード側の履歴にも TTL(既定30分 = 1800秒、 AGENT_RELAY_GUARD_TTL_SEC で変更可)を導入し、リセットが何らかの理由で動かなかった場合も古い履歴を引きずらないようにしました。拒否メッセージも「同一タスク内」という表現に改め、「独立した別タスクなら、ユーザーの次の指示以降は再び呼び出せます」という再呼び出し可能な条件を明記しています。 これで「同一セッションでも、別タスクなら同じ役割を再び呼び出せる」という自然な挙動になりました。この hook もノンブロッキングで、何が起きても exit 0 します(リセットに失敗しても TTL が最終的に古い履歴を無効化してくれます)。 地味な注意点として、履歴を書く側(agent-relay-guard)と消す側(agent-turn-reset)で状態ディレクトリの既定値を 同じ導出方法で 揃える必要があります。ここがすれ違うと、リセットが効かずに誤検知が復活します。実装では両方とも hook スクリプト自身の位置からリポジトリルートを導出しており、この契約はテストで検証しています。 振り返ると、プロンプトで守らせられないルールを hook で機械化したら、今度は hook 側の「判定単位バグ」と付き合うことになったわけです。機械化は誤検知とセットで考え、fail-open・バイパス・境界でのリセットといった逃げ道を最初から用意しておくことの大切さを痛感しました。 実際の挙動 2つの hook は .claude/settings.json に次のように登録します。 { "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/agent-turn-reset.sh\"", "timeout": 10 } ] } ], "PreToolUse": [ { "matcher": "Agent", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/agent-relay-guard.sh\"", "timeout": 10 } ] } ] } } hook 単体の挙動は、JSON を stdin に流せば手元で確認できます。同じセッション ID で implementer を2回呼んでみます。 echo '{"session_id":"demo","tool_name":"Agent","tool_input":{"subagent_type":"implementer","prompt":"課題Aの実装をお願いします。"}}' \ | .claude/hooks/agent-relay-guard.sh 1回目は何も出力されず終了コード0(許可)です。続けて2回目を実行すると deny の JSON が返ります。 { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "同一タスク内で役割 'implementer' への2回目以降のAgent呼び出しはサブエージェントの多段リレー防止のため拒否します。独立した別タスクなら、ユーザーの次の指示以降は再び呼び出せます。誤検知の場合は環境変数 AGENT_RELAY_GUARD_DISABLE=1 で一時的に無効化できます。" } } 類似度判定に引っかかった場合は、理由に類似度の実測値が入ります。 investigator の報告を丸ごと implementer に再送しようとしたケースでは、こんなメッセージで拒否されました。 直前のAgent呼び出し(役割: investigator)とpromptの類似度が高く(0.83 > 0.7)、 前段の結果の丸ごと再送とみなし拒否します。 誤検知の場合は環境変数 AGENT_RELAY_GUARD_DISABLE=1 で一時的に無効化できます。 実セッションでは、Claude が Agent ツールを呼ぼうとした瞬間にこの deny が割り込み、ツール実行はブロックされます。Claude 側には拒否理由がそのままフィードバックされるので、Claude は「リレーが禁止されている」ことをその場で理解し、前段の結果を自分で咀嚼して直接作業を進めるか、次フェーズ用にプロンプトを書き直す方向に軌道修正します。 つまり、 拒否理由がそのまま Claude への行動指針になる ように文面を書いておくのがコツです。 実際にガードが拒否したときの様子がこちらです。拒否理由が赤字のエラーとして表示され、直後に Claude が「自分は implementer なのだから Agent ツールを呼ぶべきではない、直接実装しよう」と軌道修正しています。 なお、こうした hook はエージェントの動作を止めうるものなので、テストを書いておくと安心です。このリポジトリでは「同一役割の2回目は拒否」「ユーザー発言後は再び許可」「別セッションには影響しない」「TTL切れの履歴は無視」「壊れた JSON は許可(fail-open)」といった分岐を unittest で網羅しています。 追記: リレーガードが並列実行を止めてしまった ここまでで一段落……と思っていたのですが、しばらく運用するうちに、このガード自身にもう1つ穴が見つかりました。今度は誤検知どころか、 推奨していたはずの並列委譲まで巻き添えで止めてしまう という、なかなか根の深い問題でした。この後日談も含めて共有します。 症状はこうです。設定時の CLAUDE.md には「互いに独立したタスクは、1つのメッセージで複数の Agent 呼び出しを同時に発行して並列実行する」と書いていました。ところがいざ独立タスクを並列で投げると、 implementer を3本同時に発行したうちの 2本目以降がガードに拒否される のです。前述の「判定1: 同一役割への2回目の呼び出しを拒否する」が、並列に発射した兄弟呼び出しまで「2回目」と数えてしまっていました。リレーを止めるつもりのガードが、自分で推奨した並列委譲を殺していたわけです。 なぜ並列とリレーを取り違えたのか 原因は、判定が 回数と順序 だけを見ていたことでした。「同一役割の2回目」も「直前の呼び出しとの類似度」も、時間的な前後関係しか見ていません。しかし、そもそも 本物のリレーの定義は「前段が 完了 し、その結果を受け取ってから、その内容を次段へ渡す」こと です。この「完了してから」という条件がすっぽり抜けていました。 1つのメッセージで同時に発射した並列の兄弟呼び出しは、まだ誰も完了していません。それを「同じ役割の2回目」と数えてしまったのが取り違えの正体でした。リレーと並列は、回数で見ると区別がつかないのです。 判定対象を「完了済みの呼び出し」だけに絞る そこで判定の軸を回数から 完了 へ切り替えました。 比較対象を「完了済みの Agent 呼び出し」だけに限定する のがポイントです。 こうすると並列は構造的に必ず許可されます。1メッセージで同時発行した兄弟たちは、お互いまだ完了していないので、判定時点で比較対象がゼロ。比較する相手がいなければ、リレー判定のしようがなく素通りします。役割ごとの回数制限はきれいに撤廃し、「何回呼んだか」ではなく「完了した前段の内容を使い回しているか」という 内容ベース の判定に置き換えました。 「完了」は SubagentStop で捉える(PostToolUse ではない) ここで地味に嵌まったのが、「完了」をどのイベントで捉えるかです。素直に考えると Agent ツールの PostToolUse (実行後)が完了に思えますが、これは 罠 でした。 Claude Code のサブエージェントは既定でバックグラウンド実行されるため、 PostToolUse は 起動が返った瞬間 ( tool_response.status が async_launched )に発火します。つまり実処理の完了ではなく、あくまで「起動できた」の合図です。実測すると、 PostToolUse は起動の約0.4秒後、並列呼び出しどうしの間隔は約0.8秒、実際の完了イベントは約6秒後でした。 PostToolUse を完了とみなすと、並列2本目の PreToolUse より前に「1本目は完了済み」と誤認してしまい、また並列が壊れます。 そこで 「完了 = SubagentStop 」 と定義し直しました。役割を3つの hook に分けます。 PostToolUse (Agent、 agent-call-record.sh )… その呼び出しの prompt と agent_id の紐付けを記録する(起動時点。 完了ではない ) SubagentStop ( agent-call-complete.sh )… サブエージェントの最終報告テキストと 完了 を記録する PreToolUse (Agent、 agent-relay-guard.sh )… 上記2つが書いた記録を読んで判定する 判定側が見るのは、 SubagentStop が書いた「完了済み」の記録だけ。これで初めて、並列の兄弟が互いを完了済みとみなさないことが保証されます。 deny と ask を使い分ける 内容ベースに寄せたことで、判定は2段階になりました。 完了済みエージェントの「出力」を丸ごと貼り付けて再送 している(出力の行が高い割合で prompt に含まれる) → 強い証拠なので deny 完了済みエージェントの「 prompt 」の使い回し(行集合の類似度が高い) → グレーなので ask 以前は類似度が高ければ一律 deny でしたが、ここを ask(確認)に緩めました。似た前置き(リポジトリの説明やテストコマンドなど)を共有する独立タスクを、うっかり殺さないためです。ask なら「これは独立した別作業です」と承認してそのまま続行できます。あわせて、箇条書き記号やコードフェンスのような 短い定型行だけの偶然の一致 で誤検知しないよう、比較する行に最小文字数・最小行数の下限も設けました。 状態設計を作り直し、再委譲の深さは公式の仕組みに任せる 判定単位をタスクに揃えるために前章では agent-turn-reset.sh で履歴を削除していましたが、この作り直しでその hook 自体が不要になりました。状態を 1呼び出し1ファイル ( <session>/<prompt_id>/<id>.{start,call,done}.json )に分解し、 タスク境界を prompt_id で表現 するようにしたためです。ユーザーの新しい発言は新しい prompt_id 、つまり別ディレクトリになるので、履歴は削除しなくても自動的に切り替わります。1ファイル1呼び出しなのでロックなしで並列安全になり、「拒否された呼び出しが次の判定を巻き込む」カスケードも消えました。かつての削除リセット hook は、古いセッションを掃除するだけの agent-calls-gc.sh に縮小しています。 もう1つ、内容ベースに寄せたことで、以前は「判定1」が兼ねていた 再委譲(ネスト)のバックストップ が外れました。これは公式の仕組みに委ねます。サブエージェント内からの呼び出し(入力に agent_id が入る)はガードの判定対象外にし、入れ子の深さ制限は Claude Code 公式の環境変数 CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH に任せることにしました。 .claude/settings.json の env で 1 に設定しています。 { "env": { "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH": "1" } } hook の登録も、記録用の2つ( PostToolUse / SubagentStop )が増え、 UserPromptSubmit は掃除用の agent-calls-gc.sh に差し替わります。 { "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/agent-calls-gc.sh\"", "timeout": 10 } ] } ], "PreToolUse": [ { "matcher": "Agent", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/agent-relay-guard.sh\"", "timeout": 10 } ] } ], "PostToolUse": [ { "matcher": "Agent", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/agent-call-record.sh\"", "timeout": 10 } ] } ], "SubagentStop": [ { "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/agent-call-complete.sh\"", "timeout": 10 } ] } ] } } 結果として、防御は「 ツール制限(エージェント定義)+ 深さ上限(公式の env)+ 内容判定(hook) 」という、役割がきれいに分かれた3枚構成に落ち着きました。回数で殴るのをやめて「完了」と「内容」で見るようにしただけで、並列委譲もリレー防止も両立できたのは、我ながらスッキリした着地でした。 さいごに Claude Code のマルチエージェント運用で起きた「再委譲」と「多段リレー」を、多層の防御で解決するまでの試行錯誤の話でした。要点をまとめます。 問題は2軸あり、 再委譲 (構造・深さ: サブエージェントがサブエージェントを呼ぶ)と、 多段リレー (内容: 前段の結果を咀嚼せず丸ごと再送する)。似て見えるが原因が違う。 防御は役割で分ける。 再委譲はエージェント定義のツール制限+公式の CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH で深さを止め、 多段リレーは PreToolUse hook(agent-relay-guard) が内容を見て止める リレーと並列は回数では区別できない。判定を「回数」から「 完了 」へ切り替え、比較対象を完了済み(SubagentStop)の呼び出しだけに絞ることで、並列の同時発行を構造的に常に許可しつつリレーだけを止める 内容判定は2段階。完了済みの 出力の丸ごと再送は deny 、prompt の使い回しは ask にして、似た前置きを共有する独立タスクを殺さない 一般化すると、 プロンプト指示だけでは守られないルールは、hook で機械的に強制する というのが今回の教訓です。CLAUDE.md に何を書いても、それはあくまで「お願い」であり、コンテキストが長くなれば忘れられます。破られると困るルールほど、hook のような決定的な仕組みに落とすべきで、その際は fail-open とバイパスをセットで用意しておくと運用が破綻しません。 ちなみに、エージェント定義に「再委譲禁止の記述が存在すること」自体は、自作の Linter である agentlint(ルール AL401)で静的に検証するようにしています。agentlint については 別の記事 で詳しく紹介しているので、あわせてどうぞ。 サブエージェントの委譲制御に悩んでいる方は、いきなり拒否まで作り込まなくても、まずは PreToolUse hook で Agent 呼び出しをログに記録するところから試してみてください!それでは! ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Claude Code でサブエージェントが5段ネスト!?トークンを溶かす多段委譲をハーネスで防ぐ! first appeared on SIOS Tech Lab .
PSSL の佐々木です 移動中に Claude Code を回しておきたいのですが、満員電車でノート PC を開くのは無理です。カバンに入れたまま処理だけ走らせておきたいです。 そうすると解決したい問題が 2 つ出てきます。 フタを閉じるとスリープして処理が止まる テザリングだとスマホのバッテリーが減るうえ、PC とスマホが不意に離れると Wi-Fi が切れる この 2 つを潰す設定をまとめます。Windows 側はすべて PowerShell で完結させます。 最近はPCがスリープしないようにちょっとだけPCを開けたまま手に持ち歩く人もいるみたいですが、PCを落としたり普通に邪魔なのでカバンに入れた状態でもClaudeが動くための環境を作りました。 この設定は普通に会社から怒られるかもしれないので自己責任でお願いします。 1. フタを閉じても止めない 管理者権限の PowerShell で実行します。 powershell # フタを閉じたときの動作を「何もしない」に(バッテリー駆動時) powercfg /setdcvalueindex SCHEME_CURRENT SUB_BUTTONS LIDACTION 0 # 電源接続時も同様 powercfg /setacvalueindex SCHEME_CURRENT SUB_BUTTONS LIDACTION 0 # スリープ移行そのものを無効化(0 = 無効) powercfg /change standby-timeout-dc 0 powercfg /change standby-timeout-ac 0 # ディスプレイは切ってよい(1分) powercfg /change monitor-timeout-dc 1 # 反映 powercfg /setactive SCHEME_CURRENT LIDACTION の値です。 0 ... 何もしない 1 ... スリープ 2 ... 休止状態 3 ... シャットダウン 現在値の確認はこちら。 powershell powercfg /query SCHEME_CURRENT SUB_BUTTONS powercfg /a # モダンスタンバイ対応かの確認 WSL 側も落ちないようにしておきます。 %USERPROFILE%\.wslconfig に追記して wsl --shutdown 。 ini [wsl2] vmIdleTimeout = -1 処理自体は tmux の中で回しておくと、復帰後にそのまま続きが見られます。 bash tmux new -s claude claude --dangerously-skip-permissions # Ctrl+b → d でデタッチ   Macの場合はさらに簡単でamphetamineというアプリを入れておくと蓋を閉じてもスリープしなくなります。 https://apps.apple.com/jp/app/amphetamine/id937984704?mt=12 2. テザリングをやめてリチャージWi-Fiにする テザリングをやめる理由は 2 つです。スマホのバッテリーが目に見えて減ること、そして満員電車で体勢が変わって PC とスマホが離れると Wi-Fi が切れることです。これを解決するためにリチャージWi-Fiを買いました。 リチャージWi-FiはTypeCをPCに直刺しするとネットにつながります。(特に設定もなく、さしてしばらくするとネットにつながるようになります。) リチャージWi-Fiは100GB8000円ぐらいから売っていて、100GB使い切ったらポータルサイトでチャージするとまた使えるようになります。 https://www.amazon.co.jp/【リチャージWiFi】バッテリーレス-100GB-リチャージ-一体型【M4-100GB-365日】/dp/B0DXVNKZ8T/ref=asc_df_B0DXVNKZ8T?mcid=c2d7374fee12300a9c74a8ba8ade9134&tag=jpgo-22&linkCode=df0&hvadid=707549940401&hvpos=&hvnetw=g&hvrand=8194794017880763317&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=1009285&hvtargid=pla-2444400325634&psc=1&hvocijid=8194794017880763317-B0DXVNKZ8T-&hvexpln=0 カバンにPCを入れるとこんな感じです。リチャージWi-Fiが映えますね カバンが汚いのは見なかったことにしてください。   3. 注意:フタを開けたら充電が空になっていることがある ここまでやると、フタを閉じてもディスプレイが消えるだけで中身は全力で動き続けます。当然ながら バッテリーの減りはかなり速い です。 私は移動中に走らせたまま忘れて、目的地でフタを開けたら電源が落ちていた経験があります。 客先での商談がある場合には注意が必要が必要です。 そのため、 カバンにはモバイルバッテリー(PD 対応)を一緒に入れておく 長時間走らせるときは充電しながらにする 移動が終わったら設定を戻す 4. まとめ 満員電車で立ったまま何もできない時間が、そのまま実行時間になります。設定は 10 分で終わるので、通勤が長い方は一度入れておくと効きます。 次は軽くて大容量のバッテリーが欲しいです。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 1人がこの投稿は役に立ったと言っています。 The post 満員電車でも Claude Code を動かし続ける技術 first appeared on SIOS Tech Lab .
こんにちは、サイオステクノロジー武井です。 AI コーディングエージェントを使い始めて、ふと不安になったことはないでしょうか。 「このAI、プロジェクトの中を自由に読めるけど…… .env   みたいな機密情報も読めてしまうのでは?」 そうなんです。読めちゃうんです。しかも、ただ読めるだけでは終わりません。読んだ内容がドキュメントに紛れ込んだり、コミットに含まれたりして、 GitHub リポジトリ経由で外部に漏れる ところまで想像すると、なかなか怖い話です((((;゚Д゚))))ガクガクブルブル この記事では、その対策として用意されている Claude Code の権限機能とサンドボックス機能を取り上げます。ただ「こう設定すれば安全」という話で終わらせるのではなく、 なぜその機能が必要なのか、どこまで守れて、どこに穴が残るのか を、OS の仕組みまで降りて整理していきます。途中で Linux カーネルの   fork / exec   という基本機能まで出てきますが、そこが腑に落ちると、サンドボックスが「なぜそういう形をしているのか」まで見通せるようになります。 なお、以降は Claude Code を題材に話を進めますが、 基本的な仕組みは他のコーディングエージェントでも多分同じ   です。設定ファイルの名前や項目名は製品ごとに違いますが、どれも「エージェント本体のプロセスがファイルを読む」「シェルコマンドを子プロセスとして起動する」という同じ形で動いていて、OS が用意している道具立て(macOS の Seatbelt、Linux の namespace など)も共通です。この記事で扱うのは、その   共通部分の構造   です。「アプリが自分で我慢しているのか、OS が強制しているのか」という見分け方を持っておけば、お使いのツールがどちらの層で守ってくれているのかも、同じ物差しで判断できるようになります。 何も気をつけないと、AIは機密情報を読んでしまう まず、問題の全体像を確認します。 多くのプロジェクトでは、API キーやデータベースの接続文字列といった秘密情報を   .env   ファイルに書いて、プロジェクト直下に置いています。ローカルで開発する分には便利な運用です。 ところが、AI コーディングエージェントはプロジェクト内のファイルを読んで文脈を理解します。つまり、 放っておくと   .env   も読める ということです。そして厄介なのは、読んだ後に何が起きるか予測しづらい点です。 「README を書いて」と頼んだら、環境変数の設定例として本物のキーが埋め込まれる コード生成の過程で、秘密情報を含むファイルを参照した内容がコメントに残る それらをコミットして   git push   した瞬間、 公開リポジトリに秘密が載る 「AIが勝手に漏らした」というより、「AIが読めたものが、生成物を経由して、いつの間にか外に出ていく」という流れです。悪意がなくても起きるからこそ、仕組みで防いでおく必要があります。 まず思いつく対策 ―― でも、これには穴がある Claude Code には、こうした事故を防ぐための権限機能があります。 settings.json   の   permissions.deny   に、読ませたくないファイルを書いておく方法です。 { "permissions" : { "deny" : [ "Read(./.env)" , "Read(./.env.*)" ] } } これで   Read   ツールから   .env   へのアクセスがブロックされます。 Glob   や   Edit   など、ファイルに触れる他のツールについても同様に指定できます。 一見これで安心に見えますが、 大きな穴があります 。 permissions.deny   がブロックしているのは、あくまで   Claude Code というプログラム自身が持つ機能(Read ツールなど)   です。言い換えると、これは   アプリケーションが自分で「このファイルは読まない」と自制しているだけ   なのです。 では、その自制を回避する経路があったらどうなるでしょうか。あります。そう、それは、、、、なんと!! Bash ツール です。 cat .env   も   diff .env other   も、Read ツールを使っていません。 別プロセスとして起動されるシェルコマンド です。だから「Read ツールを塞ぐ」というアプリレベルの自制は、ここには効きません。正面玄関に鍵をかけても、裏口が開いている状態です。 この「裏口」を塞ぐために登場するのが、サンドボックスです。 サンドボックスとは何か ―― システムコールそのものを止める アプリの自制ではなく、OSの強制 permissions.deny   が「アプリの自制」だったのに対し、サンドボックスは   OS レベルの強制   です。 Claude Code が Bash コマンドを実行するとき、そのコマンドを   サンドボックスで包んで起動   します。すると、そのコマンドは「特定のファイルにアクセスするシステムコール自体を呼べない」状態になります。 cat   だろうが   diff   だろうが Python スクリプトだろうが、ファイルを読むには必ず OS(カーネル)に   open()   というシステムコールを発行しなければなりません。サンドボックスはこの   カーネルへの入り口   で検問を張るので、どんなプログラムを使っても迂回できません。 どのレイヤーで検問しているのか 「アプリの自制」と「OS の強制」は、ソフトウェアの層で見るとまったく違う場所に立っています。図にすると、こうです。 上から順に見ていきます。 アプリケーション層 :   permissions.deny   はここにいます。この層の特徴は、 判定ロジックも判定対象も同じプロセスの中にある   ことです。Claude Code が「 .env   を読もうとしている自分」を、自分のコードで止めている。つまり自主規制です。自主規制の弱点は明確で、 そのルールを持っていないコードに仕事を渡せば、ルールは適用されない   こと。 cat .env   は Claude Code のコードではないので、 deny   の判定ロジックをそもそも通りません。 システムコールの境界 :   プロセスは自分の中では何でもできますが、 ファイルを読む、ネットワークに出る、といった「外の世界に触る操作」だけは自分では実行できません 。必ず   open()   connect()   といったシステムコールでカーネルに依頼する必要がある。ここがプロセスの自由が終わる線です。 そして重要なのは、この線が   プログラムの種類に依存しない   こと。 cat   でも   diff   でも Python でも、自作のバイナリでも、ファイルを読むなら全員この一本の線を越えます。だから、この線の上に検問を置けば、 「別のプログラムを使う」という迂回が原理的に成立しません 。アプリ層の検問が「使うツールを変える」で抜けられたのと、ちょうど対照的です。 OS カーネル層 :   実際の判定はこの層で行われます。 sandbox-exec   が登録したポリシーは   カーネル側にそのプロセスの属性として記録され 、以降そのプロセスがシステムコールを発行するたびにカーネルが照合します。 ここで立場が逆転しています。アプリ層では「自分が自分を止めていた」のに対し、カーネル層では   止める側が、止められる側から手出しできない場所にいる 。だから「自制」ではなく「強制」と呼べるわけです。実装は OS ごとに違いますが、性質は同じです。 macOS (Seatbelt) : カーネル内のフックでシステムコールごとにポリシーを照合し、許可されないアクセスを拒否する Linux (bubblewrap) : namespace を使って、 そのプロセスから見えるファイルシステムの景色自体を作り替える 。拒否する以前に、 .env   が存在しない世界を見せる 前者は「頼んでも断られる」、後者は「頼む対象がない」という違いですが、どちらも   プロセスの外側で決着している   点が共通です。 ハードウェア層 :   ここが「関与しない」になっているのは、単なる余白ではなく   限界の宣言   です。サンドボックスの強制力は「カーネルが正しく動いていること」に全面的に依存しています。カーネルの脆弱性を突かれて特権を取られた場合、判定者そのものが倒れるので、サンドボックスも一緒に倒れます。 つまりサンドボックスは、 「うっかり   cat .env   される」を確実に防ぐ道具 であって、「カーネルまで攻略してくる攻撃者」を想定した壁ではない、という位置づけです。 この「境界の上か下か」という視点は、後半で出てくる抜け穴を理解するときにも効いてきます。 どのプロセスに効いているのか では、その強制は Claude Code のどのプロセスに効いているのでしょうか。中の構造はこうなっています。 ポイントは、 Bash ツールは「起動時に明示的にサンドボックスでラップする」から拘束される   という点です。裏を返せば、ラップされていないものには効きません。ここが後半の伏線になります。 なぜ Read ツールにはサンドボックスが効かないのか 「システムコールを止めるなら、Read ツールも止められるのでは?」と思うかもしれません。ここが理解の勘所です。 サンドボックスは   プロセスに貼り付ける制約   です。プロセスが生成される瞬間に適用され、そのプロセスと、さらにその子プロセスに効きます。 一方、Read ツールは   Claude Code 本体プロセスに組み込まれた機能   で、 子プロセスを作りません 。本体が自分で   open()   を呼んでファイルを読むだけです。つまり、サンドボックスを「貼り付ける対象となる新しいプロセス」がそもそも生まれない。だから、Read ツールを守りたければ本体ごとサンドボックスに入れるしかなく、それをやると本体の正常動作(設定の読み書きや通信)まで壊れてしまいます。 だから役割分担になっているのです。 Read などの本体機能   →   permissions.deny (アプリの自制)で守る Bash が起動する子プロセス   → サンドボックス(OSの強制)で守る 両方揃えて初めて、正面玄関も裏口も塞がる   わけです。 設定はこう書く ―― サンドボックスを有効にする 仕組みの話が続いたので、ここで一度、実際の設定を見ておきましょう。 まずは   /sandbox   で様子を見る 一番手軽なのは、セッション中にスラッシュコマンドを打つ方法です。 /sandbox サンドボックスの設定パネルが開き、モードの選択(サンドボックス内のコマンドを自動承認するか、通常の権限確認を残すか)や、現在の設定内容を確認できます。ここで選んだ内容は、そのプロジェクトの   .claude/settings.local.json   に保存されます。 Linux で必要なパッケージが足りていない場合は Dependencies タブが出て、何が足りないかを教えてくれます。まずはこれを開いてみるのが早いです。 settings.json に書く すべてのプロジェクトで有効にしたいなら、ユーザー設定   ~/.claude/settings.json   に書きます。 { "sandbox" : { "enabled" : true } } これだけです。以降、Bash ツールが起動するコマンドは、次の節で見る「 fork   して、隙間で設定して、 exec 」という流れでラップされて動きます。 なお、動く環境には条件があります。 macOS : OS 内蔵の Seatbelt を使うので、追加インストールは不要 Linux / WSL2 :   bubblewrap (ファイルシステム隔離)と   socat (ネットワーク中継)が必要 sudo apt-get install bubblewrap socat # Ubuntu / Debian ネイティブの Windows は非対応で、WSL2 の中で動かす必要があります。 重要 ―― 有効にするだけでは   .env   は読めたままです そして、この記事のテーマにとって一番大事な注意点です。 "enabled": true   にしただけでは、 .env   の読み取りは止まりません。 デフォルトのポリシーは、読みと書きで非対称になっています。 書き込み : 作業ディレクトリ(と一時ディレクトリ)だけ許可 読み取り :   マシン全体が許可 (一部の拒否ディレクトリを除く) つまり、デフォルトのサンドボックスが主に想定しているのは「作業ディレクトリの外を勝手に書き換えられること」と「知らないドメインに通信されること」の防止です。 読み取りについてはかなり緩く、 ~/.aws/credentials   や   ~/.ssh/   すら読めます 。 ですから、 .env   を守りたければ、読み取り拒否を明示的に書く必要があります。 { "sandbox" : { "enabled" : true , "filesystem" : { "denyRead" : [ "./.env" ] } , "credentials" : { "files" : [ { "path" : "~/.aws/credentials" , "mode" : "deny" } , { "path" : "~/.ssh" , "mode" : "deny" } ] , "envVars" : [ { "name" : "GITHUB_TOKEN" , "mode" : "deny" } ] } } } filesystem.denyRead : サンドボックス内のプロセスからの読み取りを、OS レベルで拒否する credentials.files : 同じことを「秘密情報」としてまとめて書ける枠( "mode": "deny" ) credentials.envVars :   サンドボックス実行前に、その環境変数を消す 。ファイルを塞いでも、同じ秘密が環境変数に入っていては意味がないので、ここも大事です パスの書き方には癖があります。 sandbox.filesystem.*   は一般的な慣習どおりで、 /tmp/build   が絶対パス、 ~/   がホーム、 ./   がプロジェクトルートです。ただし   ./   がプロジェクトルートを指すのは   プロジェクト設定( .claude/settings.json )に書いた場合だけ   で、ユーザー設定に同じものを書くと   ~/.claude   からの相対になってしまいます。上の例のような   ./.env   は、プロジェクト側に置いてください。 2箇所に書くことになる ここで、2章の   permissions.deny   と並べてみると、役割分担がはっきりします。 守りたい経路 書く場所 効いている層 Read / Edit / Glob(本体の機能) permissions.deny アプリケーション層(自制) Bash が起動する子プロセス sandbox.filesystem.denyRead   /   sandbox.credentials OS カーネル層(強制) 同じ   .env   を守るために、 2箇所に書く ことになります。冗長に見えますが、これは重複ではありません。 効いている層が違うので、片方だけでは片方の経路しか塞げない のです。さきほどの「両方揃えて初めて塞がる」を、設定ファイルの言葉に翻訳するとこうなる、というわけです。 サンドボックスの正体 ――   fork   と   exec ここで、サンドボックスが具体的にどう「プロセスに制約を貼る」のかを見ておきます。実はこれは、特別な魔法ではなく   Linux カーネルの基本的な仕組み   をそのまま使っています。 Unix / Linux では、「プロセスを作る」と「プログラムを実行する」が   別々のシステムコール   に分かれています。 fork() → 今のプロセスを複製する。新しいプロセスができる exec() → 今のプロセスの「中身」を、別のプログラムに丸ごと入れ替える exec()   が独特です。新しいプロセスを作るのではなく、 自分自身の中身を捨てて、別のプログラムに変身する   のです。器(プロセス)はそのまま、中の人だけが入れ替わる。プロセス番号(PID)も変わりません。 一番わかりやすい確認方法があります。ターミナルで試してみてください。 echo "PID: $$ " exec bash # 新しい bash に変身する echo "PID: $$ " # ← PIDが変わっていない! 新しいシェルになったはずなのに PID が同じ。「変身」が起きている証拠です。 この仕組みが、サンドボックスの土台になっています。 sandbox-exec (macOS の場合)のようなラッパーは、次の順番で動きます。 1. fork() で子プロセスを作る(この時点では sandbox-exec のコード) 2. カーネルにサンドボックスのポリシーを登録する 3. exec("bash") で自分自身を bash に変身させる ↑ ここで「sandbox-exec プロセス」は消え、同じ器が bash になる 4. 以降、その bash には制約が貼り付いた状態 つまり、 「サンドボックスというプロセスが監視役として残る」わけではありません 。 fork   で複製し、 exec   で目的のコマンドに変身するときに、 制約という「属性」だけがそのプロセスに刻まれて残る   のです。制約はカーネル側でそのプロセスに紐づいて記録されるので、 exec   で中身が入れ替わっても消えません。むしろ、消えずに引き継がれるからこそ、この仕組みが成立します。 なぜわざわざ「複製してから変身」という回りくどいことをするのか。理由は、 fork   と   exec   の間に「設定をするための隙間」ができる   からです。この隙間で「サンドボックスのポリシーを登録する」「作業ディレクトリを変える」「環境変数を設定する」といった細工を、普通のコードとして自由に挟み込めます。 env (環境変数を設定して実行)や   nice (優先度を変えて実行)といったおなじみのコマンドも、実は全部この「隙間で細工してから   exec   する」という同じパターンで作られています。サンドボックスは、その一族の一員にすぎないのです。 そして、この制約は   子孫プロセスに継承されます 。サンドボックス化された Bash が   cat   を呼べば、 cat   も同じ制約を受け継ぐ。だから「Bash 経由なら何を使っても読めない」が成立するわけです。 疑似コードで見る ―― サンドボックスが適用される瞬間 ここまでの話を、コードの形で一度に見てみましょう。ラッパーがやっていることは、だいたいこういう形です。 if ( fork ( ) == 0 ) { // 子プロセスの側 // ここは「まだ自分のコード」なので、好きなことができる chdir ( "/tmp" ) ; // 作業ディレクトリを変える setuid ( 1000 ) ; // 権限を落とす close ( 0 ) ; open ( "input.txt" , . . . ) ; // 標準入力を差し替える setenv ( "LANG" , "C" , 1 ) ; // 環境変数を設定 sandbox_init ( profile , . . . ) ; // サンドボックスを適用 exec ( "/bin/ls" , . . . ) ; // ここで初めて ls に化ける } fork()   の戻り値が   0   になる側が子プロセスです。そして   この   if   の中、 exec()   までの数行が、さきほど言った「隙間」   です。この時点でプロセスの中身はまだラッパー自身のコードなので、特別な仕掛けは何もいりません。普通の関数呼び出しとして準備ができます。 注目してほしいのは、並んでいる5行が   全部同じ性質の操作   だということです。 chdir   でも   setuid   でも   setenv   でも   sandbox_init   でも、設定している相手は共通で、 「これから   ls   になる、いまの自分」   です。 つまり、 サンドボックスの適用は「特殊な起動方法」ではありません 。 chdir()   や   setenv()   と同じ列に、同じ資格で並んでいる   ただの1行   です。3章の冒頭で「サンドボックスは   env   や   nice   の一族にすぎない」と書いたのは、こういう意味です。 そして最後の   exec("/bin/ls") 。ここでプロセスの中身が   ls   に入れ替わりますが、 直前に設定したものは全部残ります 。 カレントディレクトリは   /tmp UID は 1000 標準入力は   input.txt 環境変数は   LANG=C そして、サンドボックスのポリシーも ls   のコードは、自分がこれらを設定された覚えなどありません。それでも、 生まれた瞬間からその条件下にいる 。ここがサンドボックスの本質です。 ls   はサンドボックスの存在を知らないし、協力することも拒否することもできません。制約は「 ls   が守るべきルール」ではなく、 「 ls   が置かれている環境」   だからです。 だからこそ、 ls   を   cat   に変えても、自作のバイナリに変えても意味がない。ここが、アプリケーション層の自制( permissions.deny )との決定的な違いです。 もうひとつ、 順番が命   だという点も押さえておきたいところです。 sandbox_init()   は必ず   exec()   より前になければいけません。「 exec   した後に設定すればいい」は成立しません。 exec   の後には   もう自分のコードが存在しない   からです(中身は   ls   になっている)。設定するチャンスは   fork   と   exec   の間、この隙間しかないのです。 Claude Code が Bash ツールを起動するときも、やっていることはこれと同じです。逆に言えば ――   この隙間を通らずに起動されたプロセスには、当然、何の設定もされていない 。これが次章の話につながります。 さらに残る抜け穴 「Read は   permissions.deny 、Bash はサンドボックス。これで完璧」と言いたいところですが、 まだ抜け穴があります 。 そのひとつが   MCP サーバー   です。MCP サーバーは、AI に外部ツール(ファイル操作、DB アクセスなど)を持たせるための仕組みで、Claude Code とは別のプロセスとして起動します。 「別プロセスなら、サンドボックスで守られるのでは?」と思うところですが、そうはなりません。もう一度、先ほどの図を見てください。 鍵は、図の一番上に書いてある   「Claude Code 本体はサンドボックス未適用」   という点です。 サンドボックスは「制約を持つプロセスから子へ継承される」仕組みでした。ところが本体自身は制約を持っていません。だから、本体が子プロセスを起動するとき、 その起動処理がわざわざサンドボックスでラップしなければ、子は素のまま生まれます 。 Bash ツールは「ラップする」ようにわざわざ実装されているので拘束されます。しかし MCP サーバーは   別のコードパスで起動される   ため、そのパスがラップしていなければ、制約なしで動いてしまう。「子プロセスだから安全」ではなく、「 ラップされた子プロセスだけが安全 」なのです。継承元が無拘束である以上、包む処理を通らない経路はすべて素通りになります。 さきほどのレイヤー図の言葉で言い直すと、こうなります。システムコールの境界は   すべてのプロセスに共通して存在している   のに、そこで照合されるポリシーは   プロセスごとに違う 。MCP サーバーは、境界を越えていないわけではありません。 境界は越えているが、そのプロセスには照らし合わせるルールが登録されていない   ので、カーネルはそのまま通してしまうのです。 MCP サーバーは、ファイルにも DB にもネットワークにもアクセスできる強力な存在です。それが制約の外で動くというのは、 .env   にとっては立派な裏口になり得ます。 MCP サーバーを包む ――   @anthropic-ai/sandbox-runtime では、この裏口は塞げないのでしょうか。塞げます。 @anthropic-ai/sandbox-runtime   という、Anthropic が公開しているサンドボックスツールを使います。 これは Claude Code の内蔵サンドボックスと同じ OS の仕組み(macOS なら Seatbelt、Linux なら bubblewrap)を、 任意のプロセスに対して外から適用できるようにした単体パッケージ   です。 srt   というコマンドを提供していて、公式のドキュメントでも「ローカル MCP サーバーを包むこと」が主要な想定用途として挙げられています。 npm install -g @anthropic-ai/sandbox-runtime やっていることは、3章で見た   sandbox-exec   とまったく同じです。 fork   して、隙間でポリシーを登録して、 exec   で目的のコマンドに変身する。だから、 包まれたプロセスの子孫にも制約が継承されます 。 .mcp.json   の   command   を差し替える やり方はシンプルで、 .mcp.json   の   command   を   srt   に差し替えて、本来のコマンドを引数に回すだけです。 包む前: { "mcpServers" : { "filesystem" : { "command" : "npx" , "args" : [ "-y" , "@modelcontextprotocol/server-filesystem" ] } } } 包んだ後: { "mcpServers" : { "filesystem" : { "command" : "srt" , "args" : [ "npx" , "-y" , "@modelcontextprotocol/server-filesystem" ] } } } 差分は「 command   を   srt   にして、元の   command   を   args   の先頭に押し込む」だけです。これで、この MCP サーバー(と、それが起動する子プロセス)はサンドボックスの中で動くようになります。 MCP サーバー側に手を入れる必要はありません 。サーバーは自分が包まれていることを知りませんが、それでも制約は効く ―― 3章の   ls   の話とまったく同じ構図です。 制約の内容は   ~/.srt-settings.json   に書きます。 { "filesystem" : { "denyRead" : [ "./.env" , "~/.ssh" , "~/.aws" ] , "allowWrite" : [ "." ] , "denyWrite" : [ "./.env" ] } , "network" : { "allowedDomains" : [ ] , "deniedDomains" : [ ] } } srt --settings /path/to/srt-settings.json ...   のように、サーバーごとに別の設定ファイルを渡すこともできます。 デフォルトの挙動には癖があるので、ここは押さえておいてください。 書き込み : デフォルトで   全部拒否 。 allowWrite   に書いたものだけ許可される ネットワーク : デフォルトで   全部拒否 。 allowedDomains   に書いたものだけ許可される 読み取り : デフォルトで   全部許可 。 denyRead   で塞ぐ 内蔵サンドボックスと同じで、ここでも   読み取りだけは緩い   という構図です。 .env   を守りたければ   denyRead   を明示的に書く必要があります。しかも   srt   では   allowRead   が   denyRead   より優先される   ので、広い   allowRead   を書くと   denyRead   が打ち消されてしまいます(内蔵サンドボックスとは優先順位が逆なので、ここは要注意です)。 逆に、書き込みとネットワークはデフォルトで全部閉まっています。 包んだ MCP サーバーが動かなくなったら、まず   allowWrite   と   allowedDomains   の不足を疑う   のが正解です。 ただし注意点もあります。 @anthropic-ai/sandbox-runtime   は   ベータのリサーチプレビュー   という位置づけで、設定フォーマットは今後変わる可能性があると明記されています。運用ルールに組み込む場合は、そのつもりで扱ってください。 じゃあ、結局どうすればいいのか ここまで、「経路を1つ塞いでも、別の経路が出てくる」というモグラ叩きを見てきました。Read を塞げば Bash、Bash を塞げば MCP……。 塞ぐアプローチには、常に「列挙し忘れた経路」が残る という本質的な弱さがあります。 そこで発想を変えます。 そもそも   .env   に本物の秘密を置かなければ、何経路から読まれても平気   です。守るべきものが最初からそこに無ければ、読まれても被害はありません。 理想的なのは、 短命なアクセストークン   を使う運用です。永続的な秘密鍵をファイルに置くのではなく、必要なときにだけ発行される、寿命の短いトークンで認証する。イメージとしては、こういう流れです。 1. 開発者が最初に CLI でログインする(人間の認証) $ my-tool login 2. その権限をもとに、短命なアクセストークンをリクエストする → 数十分で失効するトークンが発行される 3. アプリはそのトークンを使って動く → ファイルには本物の秘密鍵が存在しない → 万一トークンが漏れても、すぐ失効するので被害が限定的 実際のコマンドで見るとこうなる my-tool login   は架空のコマンドですが、実際のクラウドでは、おなじみのコマンドがそのまま当てはまります。 AWS の場合 # 1. 人間の認証(ブラウザが開いて SSO でログインする) aws sso login --profile dev # 2〜3. あとは CLI / SDK が一時credentialを自動で取得して使う aws s3 ls ~/.aws/credentials   に永続的なアクセスキーを置く代わりに、 aws sso login   で得た権限をもとに、裏側で数時間で失効する一時credentialが発行されます。トークンは   ~/.aws/sso/cache/   にキャッシュされるだけなので、期限が切れればそれ以上使えません。 Azure の場合 # 1. 人間の認証 az login # 2. 必要なリソース向けの短命トークンを取得(有効期限は1時間程度) az account get-access-token --resource https://vault.azure.net アプリ側は   DefaultAzureCredential   を使えば、この CLI のログイン状態を自動で拾ってくれます。 Google Cloud の場合 # 1. 人間の認証 gcloud auth application-default login # 2. 短命なアクセストークン(有効期限は1時間程度) gcloud auth print-access-token こちらも ADC(Application Default Credentials)という仕組みがあり、SDK が自動でトークンを取得してくれます。 どれも形はまったく同じです。 「人間が一度ログインする」→「そこから短命なトークンが発行される」→「アプリはそれを使う」 。そして共通して、 .env   に書くべき永続的な秘密がどこにも登場しません 。 クラウドのマネージド ID(実行環境自身の身元でシークレットストアからトークンを取得する仕組み)や、OIDC ベースの一時credential(GitHub Actions からクラウドに、静的なアクセスキーを Secrets に置かずに認証する構成など)が、この考え方の実装にあたります。 秘密は「ファイルに書いて守る」のではなく、「 そもそも書かず、必要な瞬間に短命なものを取りに行く 」。これが、経路を1つずつ塞ぐ発想から抜け出す、根本的な解決策です。 とはいえ、理想はなかなか大変 ただ、正直に言うと、この理想形はすぐに実現できるものではありません。 トークンを発行する仕組み(認証基盤、シークレットストア)を用意する必要がある 既存のアプリを「ファイルから読む」前提で書いている場合、その書き換えが要る ローカル開発、CI、本番で、それぞれトークンの取得経路を整える必要がある 規模の小さいプロジェクトや、とりあえず今動いているものに、いきなりこれを導入するのは腰が重い、というのが現実です。 なので、当面の現実的な立ち回りは、こうなります。 理想を目指しつつ 、本物の秘密を短命トークンに寄せていく(長期的な方向性) それができるまでは 、経路を1つずつ地道に塞ぐ permissions.deny   で Read / Edit / Glob をパス単位でブロック サンドボックスを有効にし、 denyRead   /   credentials   で読み取りも明示的に塞ぐ(有効化だけでは読み取りは止まらない) MCP サーバーは使うものを吟味し、 srt ( @anthropic-ai/sandbox-runtime )で包む .env   にはできる限りダミー値だけを置き、本物は最小限に 結局のところ、 「守るべきものを、そこに置かない」が最強の対策   です。しかしそれが難しい間は、 「経路をひとつずつ調べて防ぐ」しか、今のところ道はありません 。モグラ叩きに聞こえるかもしれませんが、どの経路がどのレイヤーで守られるのか(アプリの自制なのか、OS の強制なのか)を理解していれば、叩き漏らしはずっと減らせます。 まとめ コーディングエージェントを実用化させるためにはセキュリティは欠かせません。でもだからといって、コーディングエージェントならではの知識が必要なわけではなく、 OS の基本的な仕組みと、プロセスの性質を理解していれば十分   です。今回の話は、Claude Code に限らず、コーディングエージェント全般に当てはまります。なので、やっぱりいくらAIを使って業務をしても、やっぱり基本が大事だなと思います。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post コーディングエージェントに機密情報を読ませない基本的な仕組み first appeared on SIOS Tech Lab .
こんな方へ特におすすめ エヴァンゲリオンが好きな方 ローカルLLM( Ollama )や Raspberry Pi で、何か動くものを作ってみたい方 LangGraph でマルチエージェントを試したい方 概要 こんにちは。サイオステクノロジーのはらちゃんです! 今回はOSCの展示ブース用開発として、『新世紀エヴァンゲリオン』の意思決定コンピュータ MAGI を、Raspberry Pi 3台 + ローカルLLM(Ollama)で再現してみました。 ―― 3体の人格が別々のマシンで議論し、多数決で結論を出す その設計や速度チューニング、実機を立ち上げる過程でハマった落とし穴まで、実務にも通じる知見をまとめていきます。 背景 MAGI は、3つの独立したコンピュータ Melchior・Balthasar・Casper が、それぞれ異なる人格で同じ議題を判断し、多数決で結論を出す合議システムです。 これをただの1プログラムで再現するのは簡単です。 でも、それだと何かが違う。「 1エージェント = 1台の独立コンピュータ 」という原作の設定を、そのまま物理的に再現できないだろうか? 武井さん の協力の元、Raspberry Pi を3台並べることにしました。とはいえ、非力な Raspberry Pi でLLMなんてまともに動くのか、と最初は半信半疑でした。 結論から言うと、役割を割り切って軽量モデルとチューニングを重ねれば、合議AIは十分に動きます。 システム構成|3台の Pi + 母艦 役割分担はシンプルです。 各Pi は、ただの推論バックエンド。Ollama が待ち受けているだけで、人格ごとのプログラムは書きません。 母艦(オーケストレータ) となる1台のPCが、合議の進行・集計・画面表示を担当します。今回は PC上の WSL2 で Flask + LangGraph を動かしました。 ブラウザ / kiosk UI       ▲ ▼   SSEで投票をリアルタイム配信 母艦 — Flask + LangGraph(合議・集計)       ▲ ▼   イーサネット(各Piのollama :11434) +-----------------+-----------------+-----------------+ | Pi1 Melchior | Pi2 Balthasar | Pi3 Casper | | Ollama | Ollama | Ollama | +-----------------+-----------------+-----------------+ どのPiがどの人格かは、母艦の `.env` にある 各Piの固定IPだけ で決まります。人格ごとの特別なコードは無く、role名で接続先を引くだけ。この割り切りのおかげで、Pi側は「モデルを入れて待ち受ける」だけで済みます。 合議フロー|LangGraphで組む2ラウンド投票 合議の本体は LangGraph の StateGraph です。1回目で全会一致なら即確定、意見が割れたら「討論」を挟んで再投票する、という2ラウンド構成にしました。 prepare :開始を通知 3体を並列実行 :Melchior / Balthasar / Casper が同時に投票(fan-out) 1回目集計 :賛成が2票以上なら「可決」 分岐 :全会一致ならそのまま確定。割れたら 討論 → 再投票 → 再集計 finalize :評決を確定して配信 ここでは「アイス食べたい」をテーマにしています。2:1で可決されました。 各人格には異なる「観点」を与えています。 Melchior :論理性・合理性・整合性を最重視 Balthasar :人間要因・感情・受容性を重視 Casper :現実性・リスク・実務性を重視 ここでは「アイス食べたい」をテーマにしています。クリックで理由が表示されます。 実装のキモ|小さいモデルに「JSONだけ」返させる 各人格への問い合わせは1つの関数に集約しています。小さいモデルを安定させるコツは、 出力をJSONに固定する こと。 format="json" と temperature=0 を指定し、 role / reason / vote の3項目だけを返させます。 必ずJSONのみで返してください: { "role": "{role}", "reason": "80字以内で簡潔に(必ず日本語で)", "vote": "approve or reject" } 返ってきた文字列は、 ```json のコードフェンスを剥がし、最初の {` から最後の `} までを抜き出してからパースします。理由が80字を超えたら、途中で切れないよう「最後の句読点」で丸める。小さいモデルは文字数指示を守りきれないので、この後処理は必須でした。 Raspberry Pi で待たせない3つの速度チューニング 非力なPiで素直にLLMを動かすと、1票に何十秒もかかります。体感速度を詰めるために効かせた工夫が主に3つ。 1. コンテキスト長を絞る 対応: num_ctx を既定の 4096 から 1024 へ。 根拠: 投票プロンプトは数百字なので、大きな窓は無駄にメモリと時間を食うだけ。 2. 生成トークン数を制限する 対応: num_predict = 128 。 根拠: 理由は80字上限なのでこれで十分。 出力が短いほど速い。 3. モデルをRAMに常駐させる 対応: keep_alive = -1 + 起動時に一度空打ちして先読み。 根拠: 初回質問のコールドロード待ちを消せる。 OLLAMA_NUM_CTX=1024 # 4096 → 1024 OLLAMA_NUM_PREDICT=128 # 理由は80字で足りる OLLAMA_KEEP_ALIVE=-1 # モデルをRAMに常駐 このほか、3体を 並列実行 (直列の約1/3の時間)し、最終要約は LLMを使わず定型文 で組み立てて呼び出しを1パス削減しています。 モデル選定|0.5Bの罠を、プロンプトで直す 速度優先でまず qwen2.5:0.5b を使ったところ、奇妙な出力に遭遇しました。 理由には「適切だ」と書いてあるのに、投票は reject となっている、つまり reason と vote が食い違うのです。 原因は集計バグではなく、モデルの出力そのものの矛盾でした。しかも当初のプロンプトは vote を reason より先に書かせていたため、モデルは先に投票を決め打ちしてから、後付けで理由を書いていたのです。 そこで、JSONの並びを reason → vote に変更し、「理由を先に書き、それに一致する投票を選べ/矛盾してはならない」と明示。0.5Bのままでも矛盾が大きく減りました。 最終的に qwen2.5:1.5b に上げると、投票と理由の整合性が安定します。おまけに、 num_ctx=1024 のままでも約1300字の長文議題を破綻なく処理できました。 課題|本当に時間を溶かしたのはネットワークとOllama 正直、アーキテクチャよりも実機3台の立ち上げのほうが遥かに大変でした。同じ轍を踏む人のために残しておきます。 Wi-Fiが突然切れる GUIから静的IPを設定したらWi-Fiが切断。プロファイルからパスワード(PSK)が抜け落ちていました。 解決: プロファイルを削除して nmcli device wifi connect で作り直す。静的化も nmcli でパスワードごと一括指定するのが確実です。 (今回の構成としてはインターネット不要なのでここはスキップできちゃいます。) Ollamaのサービスが壊れる unit is masked → ディレクトリ欠落 → ssh: no key found と、1台だけドミノでおかしくなりました。 解決: 中途半端に直すより、 完全に削除してから install.sh で入れ直す のが最短でした。 母艦からPiに届かない Ollamaの既定は 127.0.0.1 待ち受けで、他マシンから見えません。 解決: OLLAMA_HOST=0.0.0.0:11434 を設定して再起動。 ss -tlnp | grep 11434 で待ち受けが 0.0.0.0 になっているか必ず確認しましょう。 検証|「意見が割れる議題」で試す 多数決システムの見どころは、票が割れて「討論→再投票」が発動する瞬間です。だから 論理・感情・現実の3視点が対立する議題 を選びます(例:「AIに人事評価を任せるべきか」「延命治療を中止すべきか」)。 面白かったのは頑健性で、 音声入力の変換ミスで議題が多少崩れていても 、3体とも妥当な結論に収束しました。むしろ本番では、コンテキスト長よりも 音声認識の精度のほうが実害リスクが大きい という気づきが得られました。 母艦を起動して [warmup] done が出て、3体の投票がそろい評決が返ってくれば成功です! ここでは「残業を法律で全面禁止すべき」をテーマにしています。 まとめ 「 Raspberry Pi でLLMなんて動くの?」という半信半疑から始めましたが、役割を割り切って設計し、チューニングを重ねることで、合議AIはしっかり動きました。 MAGI = 3つの人格が多数決で決める合議システム。 「1人格 = 1台の Raspberry Pi 」 を物理的に再現。 構成は 3台のPi(Ollama)+ 母艦(Flask + LangGraph) 。役割は .env のURLだけで振り分けるシンプル設計。 非力なPiでも、 コンテキスト長・生成量・モデル常駐 の3点を絞れば実用的な速度で動く。 小さいモデルの「投票と理由の矛盾」は、 プロンプトの並び替え という小さな工夫で大きく改善できた。 一番の敵はAIではなく ネットワークとサービス管理 。ここを乗り越えれば合議AIは立ち上がる。 今回はイベント会場に向けて軽量・高速に特化させましたが、精度を重視し他のモデルを試していきたいと考えています。 今後も、こうした個人開発を通して得られた知見を、皆さんに共有していきたいと思います! 宣伝 生成AIを活用する開発として、自分専用のRAGを作ることもやりました。興味がある方はぜひのぞいてみてください。 RAGの作り方|LlamaIndexで簡単2ステップ RAGの育て方|LlamaIndexでペルソナ設計 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 32人がこの投稿は役に立ったと言っています。 The post ラズパイ3台でエヴァのMAGIを作る|ローカルLLMで動く合議AIシステム first appeared on SIOS Tech Lab .
PSSLの佐々木です MCP サーバーを書いていると、ある機能を @mcp.tool で実装すべきか @mcp.resource で実装すべきかで迷う場面が出てきます。公式のコース教材でも「ドキュメントを読む」という同じ処理が tool と resource の両方で実装されていて、最初に読んだときは違いがピンと来ませんでした。 仕様を読み直して整理したところ、判断基準は思っていたよりはっきりしていたので、使い分けの考え方としてまとめます。 この記事では、 tool と resource の違いは「機能」ではなく「誰が呼ぶか」であること どちらで実装するかを決める判断フロー ユースケース別の使い分け resource 側にしかない機能(URI テンプレート、変更通知)の活かし方 迷ったときに両方出しておく実装パターン ハマりどころ(Messages API の MCP connector は tool のみ、など) についてまとめました。 1. 何がわからなかったのか Anthropic のコース教材では、インメモリのドキュメント管理サーバーを題材に、こういう tool が定義されます。 @mcp.tool( name="read_doc_contents", description="Read the contents of a document and return it as a string." ) def read_document( doc_id: str = Field(description="Id of the document to read") ): if doc_id not in docs: raise ValueError(f"Doc with id {doc_id} not found") return docs[doc_id] ところが少し後の章で、 まったく同じことをする resource が出てきます。 @mcp.resource("docs://documents", mime_type="application/json") def list_docs() -> list[str]: return list(docs.keys()) @mcp.resource("docs://documents/{doc_id}", mime_type="text/plain") def fetch_doc(doc_id: str) -> str: if doc_id not in docs: raise ValueError(f"Doc with id {doc_id} not found") return docs[doc_id] やっていることは docs[doc_id] を返すだけで、tool 版と 1 行も違いません。なぜ 2 つあるのか。 2. 違いは「誰が呼ぶか」 答えは機能差ではなく 制御主体 です。MCP の仕様では tool は model-controlled、resource は application-controlled と明確に区別されています。 つまり read_doc_contents は「Claude に自分で判断して使ってほしい機能」、 docs://documents/{doc_id} は「 @ メンション UI のためのデータソース」です。同じ処理でも役割がまったく違う、というのが教材の意図でした。 ここを押さえると、資料でよく見る「resource はデータ、tool はアクション」という説明が、もう一段深く理解できます。読み取り専用かどうかが本質なのではなく、 呼ぶ判断をモデルに委ねるのか、アプリが握るのか が本質です。 3. 判断フロー 実装するときは、この順番で考えるとよさそうです。 Q1 は単純です。書き込み・削除・外部への送信は必ず tool にします。 resources/read はクライアントが再読み込みやキャッシュをする前提の操作なので、ここに副作用を置くと何回呼ばれるか分からず事故ります。「GET に副作用を持たせない」と同じ話です。 Q2 と Q3 が実質的な分かれ目です。読み取り専用でも、 「どのデータが必要かをモデルに判断させたい」なら tool です。ここを「読み取りだから resource」と機械的に決めてしまうと、モデルからは存在しないデータになってしまいます。 仕様側にもこの指針が書かれていて、 モデルに対してデータを自動的に公開したい場合は Tools のような model-controlled なプリミティブを使うべき とされています。 4. ユースケース別の使い分け 具体例に落とすとこうなります。 やりたいこと 選択 理由 @ でドキュメントを参照させる resource ユーザーの UI 操作が引き金。モデルの判断は不要 「report.pdf を要約して」に応える tool どのドキュメントが必要かはモデルが判断する ドキュメントを編集する tool 副作用がある 全文検索して該当箇所を返す tool 検索クエリをモデルが組み立てる プロジェクト規約や DB スキーマを常に文脈に入れる resource アプリが定型的に注入すればよい ログの最新状態を追わせる resource + subscribe 変更通知がプロトコル標準にある チケットを作成する tool 副作用がある ユーザーが選んだファイルを添付する resource 選択したのはユーザー 「検索は tool、指定は resource」と覚えると整理しやすいと感じています。何を取るかが決まっていないなら tool、決まっているなら resource です。 5. resource 側の機能を活かす resource を選んだ場合、tool にはない仕組みが使えます。ここを使わないと resource にした旨味が薄くなります。 5.1 URI テンプレートと補完 docs://documents/{doc_id} のようなテンプレートは RFC 6570 の URI Template 構文で、 resources/templates/list で discovery できます。パラメータは MCP の completion API で自動補完に対応させられるので、 @ メンションの候補表示が標準の枠に乗ります。Python SDK はテンプレートのパラメータを自動でパースして関数のキーワード引数に渡してくれるので、実装側は URI のパースを書く必要がありません。 5.2 変更通知(subscribe) サーバーが subscribe capability を宣言すると、クライアントは resources/subscribe で個別の URI を購読でき、内容が変わると notifications/resources/updated が飛びます。クライアントはそれを受けて再読み込みします。ログやメトリクスのような「更新され続けるデータ」を扱うなら、ポーリングを自作せずに済みます。 5.3 mime_type mime_type はクライアントがレンダリングを決めるヒントになります。JSON を返すのに text/plain を書いておくと、クライアントによっては素の文字列として扱われます。SDK が戻り値のシリアライズはやってくれますが、MIME タイプの正しさは面倒を見てくれないので、ここはサボらないほうがいいです。 6. 迷ったら両方出しておく 判断フローで整理しても、実際には「両方あると便利」というケースが出てきます。そのときは 内部実装を 1 本にして、tool と resource の両方から呼ぶ のが素直です。 理由は、resource の扱いがクライアント実装に委ねられているからです。仕様上、ユーザーに明示的に選択させるクライアント、ヒューリスティクスで自動選択するクライアント、モデル自身に選ばせるクライアント、どれもあり得るとされています。実際にも resources/list は実装済みでも read の UX がまちまち、 subscribe は未対応、といった差があります。resource しか用意していないと、未対応のクライアントからは中身が空っぽに見えます。 from mcp.server.fastmcp import FastMCP from pydantic import Field mcp = FastMCP("DocumentMCP", log_level="ERROR") docs = { "deposition.md": "This deposition covers the testimony of Angela Smith, P.E.", "report.pdf": "The report details the state of a 20m condenser tower.", } # --- 実装は 1 箇所だけ --- def _list_doc_ids() -> list[str]: return list(docs.keys()) def _read_doc(doc_id: str) -> str: if doc_id not in docs: raise ValueError(f"Doc with id {doc_id} not found") return docs[doc_id] # --- resource: アプリの @ メンション UI 用 --- @mcp.resource("docs://documents", mime_type="application/json") def list_docs_resource() -> list[str]: return _list_doc_ids() @mcp.resource("docs://documents/{doc_id}", mime_type="text/plain") def fetch_doc_resource(doc_id: str) -> str: return _read_doc(doc_id) # --- tool: Claude が自分で判断して呼ぶ用 --- @mcp.tool( name="list_documents", description="List the ids of all available documents." ) def list_documents() -> list[str]: return _list_doc_ids() @mcp.tool( name="read_doc_contents", description="Read the contents of a document and return it as a string." ) def read_document( doc_id: str = Field(description="Id of the document to read") ): return _read_doc(doc_id) 追加コストはデコレータ数行なので、割に合うと思っています。ただし tool を増やすとその定義はモデルのコンテキストを常に消費するので、「何でも tool にも出しておく」は避けたほうがいいです。文脈注入で完結するデータは resource だけに留めます。 動作確認は Inspector が楽です。 uv run mcp dev mcp_server.py Resources と Resource Templates が別枠で表示されるので、テンプレートのパラメータ解決まで確認できます。 7. 注意点(ハマりどころ) 7.1 Messages API の MCP connector は tool しか使えない これは事前に知らないと詰みます。公式ドキュメントに明記されていて、MCP 仕様の機能セットのうち 現時点では tool call のみサポート です。さらにサーバーは HTTP で公開されている必要があり、 ローカル STDIO サーバーは直接接続できません 。 mcp_servers パラメータ(MCP connector) ├─ tool call ... OK ├─ resource ... NG ├─ prompt ... NG └─ stdio サーバー ... NG 「resource を作ったのに Messages API から読めない」は仕様どおりの挙動です。 7.2 resource を使いたいならクライアント側ヘルパーに寄せる では API 経由で resource を使う手段が無いのかというと、そうではなく 自分で MCP クライアント接続を管理する 側に回ります。Anthropic SDK にはそのための変換ヘルパーが用意されています。 # pip install "anthropic[mcp]" (Python 3.10 以降) from anthropic.lib.tools.mcp import mcp_resource_to_content resource = await mcp_client.read_resource(uri="file:///path/to/doc.txt") response = await client.beta.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{ "role": "user", "content": [ mcp_resource_to_content(resource), {"type": "text", "text": "Summarize this document"}, ], }], ) mcp_resource_to_file を使えばそのまま Files API にアップロードもできます。公式の使い分けも明快で、 URL で到達できるリモートサーバーで tool だけ使いたいなら mcp_servers パラメータ、ローカルサーバーや prompt / resource を使いたいならクライアント側ヘルパー です。 resource は「Claude が勝手に読むもの」ではなく「アプリが読んでプロンプトに詰めるもの」だという 2 章の話が、SDK の API 設計にそのまま現れています。 7.3 resource link はクライアント側で解決してから渡す 変換ヘルパーは、未対応のコンテンツタイプや MIME タイプ、そして resource link を渡すと例外を投げます(Python なら UnsupportedMCPValueError )。resource link は MCP クライアント側で実体に解決してから変換する必要があります。 7.4 大きい resource はコンテキストを食う resource の中身は最終的にプロンプトに入ります。ログファイル全体のような resource をうっかり注入すると一撃でコンテキストが埋まります。大きいものは resource link で参照させるか、tool 側でフィルタ・要約してから返す設計にしたほうが安全です。 8. まとめ tool と resource の違いは機能ではなく 制御主体 。tool は model-controlled、resource は application-controlled 判断は「副作用があるか」→「呼ぶタイミングをアプリが決められるか」→「モデルに存在を知らせる必要があるか」の順で考える 読み取り専用でも、 何を取るかをモデルに判断させたいなら tool 。読み取りだから resource、と機械的に決めるとモデルから見えないデータになる resource を選んだら URI テンプレート・補完・ subscribe ・ mime_type まで使い切ると効果が出る 両方あると便利なケースは、内部関数を共有して二重提供する。ただし tool 定義はコンテキストを消費する点に注意 Messages API の MCP connector は tool のみ。resource を使うならクライアント側ヘルパー( mcp_resource_to_content / mcp_resource_to_file )に寄せる 「モデルに判断させる」のか「アプリが決め打ちする」のかを設計として先に決めておくと、実装もレビューもぶれなくなります。何でも tool にしてモデルに探索させるとトークンも増えて挙動も揺れますし、逆に何でも resource にするとモデルからは存在しないデータになります。AI エージェントと組み合わせる開発では、この「どこまでを機械的に確定させるか」の線引きが毎回論点になるなと感じています。 参考リンク MCP connector – Claude Platform Docs: https://platform.claude.com/docs/en/agents-and-tools/mcp-connector Resources – Model Context Protocol 仕様: https://modelcontextprotocol.io/specification/draft/server/resources Connect to local MCP servers: https://modelcontextprotocol.io/docs/develop/connect-local-servers Connect Claude Code to tools via MCP: https://code.claude.com/docs/en/mcp MCP TypeScript SDK: https://github.com/modelcontextprotocol/typescript-sdk AI エージェント導入のご相談 サイオステクノロジーでは、本記事で扱った MCP サーバーの設計・実装を含む AI エージェントの SI サービス を提供しています。既存システムと AI エージェントの接続、RAG の精度評価と改善など、PoC から本番運用までの実装フェーズをまとめてご支援します。 「自社の業務でどこまで自動化できるのか」「どのデータをエージェントに渡すべきか」といった構想段階のご相談も歓迎です。無料相談も承っておりますので、ご興味のある方は下記サービスサイトからお気軽にお問い合わせください。 サイオス ネクストテックソリューション 生成AI導入支援サービス: https://nextech-solutions.sios.jp/genai/ ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post MCP の tool と resource の使い分け first appeared on SIOS Tech Lab .
「Figmaデザインエージェント(ベータ版)」が登場 5月20日に、Figmaにて「デザインエージェント」機能がベータ版として登場しました。 今回は、この機能を少し利用した内容を、使用感レポートとして、キャプチャー画像中心にお伝えします。 公式の案内はこちらをご覧ください。 公式ブログ: Figmaデザインエージェントが登場 | Figma Blog 利用方法は簡単で、既存のFigmaデザインのナビゲーションに追加された「エージェント」や、各フレームなどに表示されるエージェントアイコン(✦)から自然言語でプロンプトを入力すると、「エージェント」がモックアップ生成や変更をしてくれます。 (現在この機能は有償アカウントにて、AIクレジットを消費せずに、試用できます。正式リリース後はAIクレジットが消費されるようです。) 例1)簡単なプロンプト まず、何も無い画面に、以下の簡単なプロンプトを送信しました。 Jira、Asana、Nulab Backlog、Redmineのような、SaaSのプロフェッショナルなタスク管理ツールの画面モックアップを作成してください。 画面は「タスク一覧ページ」と「各タスクの詳細ページ」の2つの画面。 2分ほどで、UIモックアップが描画され、品質としては、このまま利用しても、ほぼ問題無いものです。 配置されているUI要素、基本レイアウト、スペーシング(余白)、配色、文字サイズ、ダミーコンテンツ内容、どれも基本的に、不自然さはありません。  簡単なプロンプトでもUIモックアップが生成される もちろん、生成されたモックアップはFigmaデザインのオートレイアウトが適用されたレイヤー要素で構成されており、手動で編集可能です。 生成されたモックアップは手動編集も可能 例2)デザインシステムを指定 Figmaデザインエージェントは、既存のライブラリを指定可能です。UIコンポーネントやスタイル定義が格納されたライブラリを参照させることで、細かい指示なしでも意図に沿ったUI構築が期待できます。 具体的には、デザインファイルに対して、まずライブラリを追加します。(公式: Figmaのライブラリに関するガイド ) そのうえで、エージェントのプロンプト欄のオプションにて有効にするライブラリを指定します。(チェックを入れる) 今回は公開されている「 Primer Web 」(Figmaデザインファイル)をライブラリとして指定し、プロンプトでは「例1」のプロンプトに加えて、念のため「デザインシステムは『Primer Web』を使用」と追記して実行したところ、3分半ほどで精度の高い出力が得られました。 プロンプトを送信する前に、ライブラリを指定 ライブラリが適用されたUIモックアップ 出力されたモックアップについて、UIラベルや、コンテンツを日本語化したいため 画面の内容を日本語化して と指示を行い、 さらに、タスク詳細ページの要素に重なりが発生しているので、詳細画面を選択して コメント入力欄と、画面右側のステータスなどの要素が重なっているので、重ならないようにして。 と指示をすると、想定どおりに更新されました。 ライブラリのコンポーネント適用 各UI要素が、ライブラリのUIコンポーネントをインスタンスとして配置されているか、確認したところ、「ボタン」や「パンくずリスト」「ラベル」「アバター」などは、問題ありませんでした。 しかし「セレクト」や「データテーブル」などは、ライブラリが利用されていませんでした。 セレクト要素を選択して、ライブラリのコンポーネントを利用するように指示したところ、コンポーネントが適用されましたが、「優先度: すべて」などのラベル文字が無効化されたため、エージェントによる更新を「元に戻す」を行い、ラベル内容を保持するように再度指示をしたところ、そのとおりとなりました。 表(テーブル)部分は、DataTableコンポーネントの適用を指示しても、なかなか期待通りにはなりません。 テーブルには、ヘッダーや行、列、フッター、さらにセルの要素のバリエーションなど複雑なため、利用するコンポーネントの特性を踏まえて、指示をする必要がありそうです。 エージェントの制御が難しい場合は、早めに手動編集に切り替えるのが良いかもしれません。 なお、Figmaデザインエージェントは、同時に複数のプロンプトを送信しても並行して処理をすることも可能です。 例3)バリエーション生成 「例1」「例2」にて生成された画面は、デスクトップ用のライトモード画面でしたが、ダークモードやモバイル画面を、下記のようなプロンプトで依頼しました。 この画面のダークモード版を別のframeで作成して。 これら2つの画面に対する、モバイル(スマートフォン)用の画面を作成して。 結果としては、ダークモードは期待通りでした。 モバイル画面は、少々ぎこちないですが、たたき台として利用できそうです。 生成されたダークモードのUIモックアップ 生成されたモバイル版のUIモックアップ 他のサービスと比較 以上がFigmaデザインエージェント(ベータ版)の簡単な使用感ですが、比較として、同じプロンプトを次の2つのサービスに送信してみました。 ChatGPT(画像生成) 静的な画像として出力されます。レイアウトや要素の構成は適切ですが、Figmaなどで編集可能なレイヤーとして出力はされないため、プロトタイプへの落とし込みや、詳細を手動編集するには別途作業が必要です。 ChatGPTにより生成されたUIモックアップ Figma Make 「Figma Make」は2025年リリースの機能です。(公式: Figma Makeを探る ) プロンプトからフロントエンドコードの生成に重きを置いており、Figmaエージェントとは出力の指向性が異なります。Figma Makeで生成した内容はFigmaデザインファイルへコピー&ペーストして編集することも可能です。 プロンプトを送信すると、数分でタスク一覧画面、詳細画面のプロトタイプができました。 画面の要素、レイアウトや配色は完成されており、違和感はありません。 「Make」と「デザインエージェント」は同じFigmaのサービスでも、同じプロンプトから生成される内容は異なる印象です。 Figma Makeで生成されたプロトタイプ おわりに 上記の公式案内に …「AI生成か、それとも直接操作か?」といった、偽りの二者択一が浮上しています。しかし、どちらかを選ぶ必要などないはずです。(省略)私たちの目標は、Figmaに精通し、チームの働き方に自然に溶け込むエージェントを作成することでした。… とあるように、手作業とエージェントとをうまく合わせることで、UIモックアップ、プロトタイプ作成、バリエーションづくりの工数削減や、品質の向上が実現できそうです。 Photo by Zac Wolff on Unsplash   ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Figmaデザインエージェントを試す first appeared on SIOS Tech Lab .
はじめに こちらの記事 で実際にKubernetes環境にKubeBlocksを導入し、DBaaSの基盤を構築しました。 前回の記事 ではKubeBlocksを利用してDBaaS基盤上にMySQLを構築しました。 今回は、KubeBlocksを利用してNoSQLのインメモリデータベースであるRedisを構築していきます。 導入環境構成図 以下の図は、DBaaS基盤上にRedisを導入する環境の構成図です。 「KubeBlocksオペレーター」は こちらの記事 で構築しました。 本記事では、赤丸で囲まれた「DB(Redis)」の構築を対象とします。 Redisはすべてのデータをメモリ上で処理する「インメモリデータベース」の一種で、NoSQLに分類されます。 ディスク(SSD/HDD)にアクセスする一般的なデータベースと比較して圧倒的に高速なのが特徴で、主にWebサイトやアプリの高速化(キャッシュ)や、リアルタイム処理に利用されるDBになります。 導入環境構成図 Redisの構築方法 KubeBlocksを使用してRedisを構築する手順をご紹介します。 今回は最もシンプルな、1台のサーバーでRedisを稼働させるスタンドアロン構成で作成していきます。 前提条件 KubeBlocksが構築済みであること KubeBlocksによってデフォルトでインストールされるRedisアドオン(以下コマンド結果のredis 1.0.1)が有効になっていること 以下のkbcliコマンドで有効化されているアドオンを確認することができます。 kbcli addon list # 出力例 NAME VERSION PROVIDER STATUS AUTO-INSTALL qdrant 1.0.1 community Disabled false rabbitmq 1.0.1 community Disabled false apecloud-mysql 1.0.1 community Enabled true etcd 1.0.1 community Enabled true kafka 1.0.1 community Enabled true mongodb 1.0.1 community Enabled true mysql 1.0.1 community Enabled true postgresql 1.0.1 community Enabled true redis 1.0.1 community Enabled true Namespeaceの作成 まずはRedisをデプロイするNamespeaceを作成します。 kubectl create namespace redis # 出力例 namespace/redis created デフォルトユーザー認証用Secretの作成 Redisのデフォルトユーザー用のユーザー名・パスワードを設定したSecretを作成します。 kubectl create secret generic custom-redis-root-secret \ --from-literal=username='default' \ --from-literal=password='<任意の値>' \ -n redis # 出力例 secret/custom-redis-root-secret created ※Redis作成時に本手順で作成したSecretを指定することで、デフォルトユーザーのパスワードを任意の値で設定することができます。 Secretの指定がない場合は、KubeBlocksがデフォルトユーザー用のパスワードを自動発行します。 Redisの作成 MySQLクラスター構築時と同様に、KubeBlocksのカスタムリソースである「Cluster」のマニフェストを適用し、Redisを作成します。 cat <<EOF | kubectl apply -f - apiVersion: apps.kubeblocks.io/v1 kind: Cluster metadata: name: redis-cluster namespace: redis spec: terminationPolicy: Delete clusterDef: redis topology: standalone componentSpecs: - name: redis replicas: 1 systemAccounts: - name: default secretRef: name: custom-redis-root-secret namespace: redis serviceVersion: 8.0.3 disableExporter: false resources: limits: cpu: "0.5" memory: "0.5Gi" requests: cpu: "0.5" memory: "0.5Gi" volumeClaimTemplates: - name: data spec: accessModes: - ReadWriteOnce resources: requests: storage: 20Gi EOF # 出力例 cluster.apps.kubeblocks.io/mycluster created clusterDef: redis Redisアドオンが提供するRedisの構成テンプレートを、作成するDBクラスターのベースとして指定する設定です。 topology: standalone Redisを単一のRedisサーバーインスタンスで構成されるスタンドアロンクラスターとして起動する設定です。 terminationPolicy: Delete クラスターを削除した際、関連するデータも一緒に削除する設定です。 systemAccounts Redisのデフォルトユーザーの認証情報(ユーザー名・パスワード)に、事前に作成したSecretを割り当てる設定です。 volumeClaimTemplates DBのデータを保存するためのPVの設定です。 Redisの作成確認 Redis作成コマンド実行後、以下のコマンドでクラスター・Podのステータスを確認します。ステータスがRunningになっていれば正常に動作しています。 kubectl get cluster redis-cluster -n redis # 出力例 NAME CLUSTER-DEFINITION TERMINATION-POLICY STATUS AGE redis-cluster redis Delete Running 110s kubectl get pods -n redis # 出力例 NAME READY STATUS RESTARTS AGE redis-cluster-redis-0 4/4 Running 0 2m15s 動作確認 Redisへの接続テストを行います。 まず、作成したRedisのエンドポイント(Service名)を確認します。 kubectl get svc -l app.kubernetes.io/instance=redis-cluster -n redis # 出力例 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE redis-cluster-redis-redis ClusterIP 10.43.163.160 <none> 6379/TCP 3m kubectl runコマンドでRedisクライアント用のPodを作成し、コンテナ内でシェルを起動します。 kubectl run redis-client -n redis --rm -i --tty --image=redis:8.0.3 --restart=Never -- bash # 出力例 If you don't see a command prompt, try pressing enter. root@redis-client:/data# redis-clientコマンドを使用し、確認したRedisのエンドポイント、「 デフォルトユーザー認証用Secretの作成 」で作成したユーザ名・パスワードを指定して接続します。 root@redis-client:/data# redis-cli -h redis-cluster-redis-redis.redis.svc.cluster.local -p 6379 --user default --pass xxxxx # 出力例 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. redis-cluster-redis-redis.redis.svc.cluster.local:6379> Redisに正常に接続できているか、以下のPINGコマンドを使用して確認します。 redis-cluster-redis-redis.redis.svc.cluster.local:6379> PING # 出力例 PONG 「PONG」が出力されれば、正常に接続できています。 続いて、データの登録・取得が行えるかを確認します。 キー「test」に値「”Hello World” 」を登録します。 redis-cluster-redis-redis.redis.svc.cluster.local:6379> SET test "Hello World" # 出力例 OK 登録したデータが正しく取得できるかを確認します。 redis-cluster-redis-redis.redis.svc.cluster.local:6379> GET test # 出力例 "Hello World" 登録した「”Hello World” 」という文字列が出力されれば、Redisへのデータ登録と取得は正常に行えています。 これでRedisの構築と動作確認は完了になります。 おわりに 前々回構築したKubeBlocksを使用し、実際にRedisの構築から接続テストを行うまでの流れをご紹介しました。 前回のMySQL構築に引き続き、容易にRedisを構築できることを体感できたのではないでしょうか 。 今回は最もシンプルな「Redisサーバ1台のスタンドアロン構成」として構築しましたが、KubeBlocksなら本番環境向けの冗長構成も、マニフェストの設定を少し変更するだけで簡単に構築することができます。 KubeBlocksは様々なDBをサポートしているため、他のDBも同様の方法で手軽に導入することができます。 本記事が、Kubernetes上でDBを構築する際の選択肢として、KubeBlocksを検討するきっかけになれば幸いです。 参考文献 https://kubeblocks.io/docs/preview/kubeblocks-for-redis/03-topologies/01-standlone https://kubeblocks.io/docs/preview/kubeblocks-for-redis/06-custom-secret/01-custom-secret ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post KubeBlocksでRedisを導入!Kubernetes上での高速キャッシュ/NoSQL構築を体験 first appeared on SIOS Tech Lab .
前回の記事 ではHCPの概要やメリットについて触れましたが、本記事では実際にROSA HCPクラスターを構築する手順を徹底解説します。 各種CLIツールのインストールから、ROSA特有のIAMロールの作成、そして実際のクラスターデプロイと接続確認まで、ハンズオン形式で一通り実践できる内容となっています。 「ROSAを触ってみたいけれど、何から始めればいいか分からない」という方は、ぜひ手元の環境で実際に構築してみてください。 ROSA構築のための事前準備 ROSA(Red Hat OpenShift on AWS)でクラスターを実際に作成していくにあたって、事前に必要となるアカウントや環境は以下の通りです。 AWSアカウント この記事では、AWSアカウントが作成済みであることを前提としています。 補足:ROSAの構築には適切なIAM権限(AdministratorAccessなど)が必要になります。 Red Hatアカウント この記事では、Red Hatアカウントが作成済みであることを前提としています。 まだ作成していない場合は、 Red Hatの公式サイト からアカウントを作成してください。 AWSアカウントとRed Hatアカウントの紐づけ AWSコンソールから、Red Hat OpenShift Service on AWS (ROSA)ページに移動し、「使用を開始」をクリックします。 ROSA HCPを有効化にし、「Red Hatに進む」でRed Hat側でアカウントを紐づけてください。 ターミナル この記事では、Windowsの WSL(Ubuntu) を使用して構築を進めていきます。 macOS: 標準のターミナル(bash/zsh)であれば、基本的にそのまま同様の手順で進行可能です。 その他(GitBash, PowerShellなど): コマンドの挙動が異なる場合があり、動作を保証できませんのでご注意ください。 注意事項 ROSAの構築・利用にあたっては、 AWSのインフラ利用料およびRed Hatのサブスクリプション料金(従量課金など)が発生します 。 予期せぬ課金を防ぐため、検証が終わったら必ずクラスターを削除するなど、料金面についてあらかじめご承知おきください。 環境設定 それでは、実際にROSA構築に入る前に、必要なツール(CLI)をインストールしていきましょう。 ROSAの構築・操作には、基本的に以下の3つのCLIを利用します。 AWS CLI ROSA CLI OpenShift CLI AWS CLIのインストール ROSAはAWSのマネジードサービスであるため、AWSアカウントにログインしている状態で利用できます。まずは、以下のコマンドでAWS CLIをインストールします。 # インストーラーのダウンロードと解凍 $ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" $ sudo apt update && sudo apt install -y unzip $ unzip awscliv2.zip # インストール実行 $ sudo ./aws/install # インストール確認 $ aws --version ROSA CLIのインストール ROSA CLIは、ROSAクラスターの新規作成や、クラスター全体のステータス確認・削除など、クラスター単位の操作で使用するCLIです。 以下のコマンドでROSA CLIをインストールします。 # ダウンロードと解凍 $ curl -L https://github.com/openshift/rosa/releases/download/v1.2.60/rosa_Linux_x86_64.tar.gz -o rosa-linux.tar.gz $ tar xvzf rosa-linux.tar.gz # binaryディレクトリに移動 $ sudo mv ./rosa /usr/local/bin/ # インストール確認 $ rosa version OpenShift CLIのインストール OpenShift CLI(oc)は、作成したROSAクラスターの内部にある各種リソース(NodeやPodなど)を操作するためのCLIです。KubernetesのkubectlのOpenShift拡張版とイメージしていただければ大丈夫です。 以下のコマンドでOpenShift CLIをインストールします。 # ダウンロードと解凍 $ curl -LO https://mirror.openshift.com/pub/openshift-v4/clients/ocp/4.20.12/openshift-client-linux.tar.gz $ tar -xvf openshift-client-linux.tar.gz # binaryディレクトリに移動 $ sudo mv ./oc ./kubectl /usr/local/bin/ # インストール確認 $ oc version client ROSAへのログイン 各種CLIの準備ができたら、実際にアカウントへログインしてみましょう。まずROSAを操作するために、AWSへのログイン(認証)を行います。 $ aws login --remote コマンドを実行すると、https://us-east-1.signin.aws.amazon.com/v1/authorize?response_type=…というURLが表示されるのでブラウザで接続します。 Copy verification codeをクリックしてコピーした認証コードをターミナルのEnter the authorization code displayed in your browser:のところに貼り付けます。 ログイン完了後、以下のコマンドを実行し、認証情報を現在のシェル環境変数にエクスポートしておきます。 $ eval $(aws configure export-credentials --format env) 正しくログインできているか、接続中のAWSアカウント情報を確認してみましょう。   $ aws sts get-caller-identity AWSにログインできたら、次はROSAにログインします。 $ rosa login --use-device-code コマンドを実行すると、以下のようにURLと認証用のコード( XXXX-XXXX 部分)がターミナルに表示されます。 INFO: To login, navigate to https://sso.redhat.com/device on another device and enter code XXXX-XXXX 提示されたURL(https://sso.redhat.com/device)をブラウザで開き、ターミナルに表示されているコードを入力してサインインを承認してください。 ROSAへのログインが完了したら、クラスターを構築するデフォルトのリージョンを指定しておきます(ここではus-east-1を指定しています)。 $ export AWS_REGION=us-east-1 最後に、以下のコマンドで接続中のROSAのアカウント情報を確認し、正しく表示されたら、ROSAのクラスターを作る準備はすべて完了です。 $ rosa whoami # 出力例 W: The current version (1.2.60) is not up to date with latest rosa cli released version (1.2.64). W: It is recommended that you update to the latest version. AWS ARN: arn:aws:iam::123456789:XXXXXX AWS Account ID: 123456789 AWS Default Region: us-east-1 OCM API: https://api.openshift.com OCM Account Email: XXXXXX OCM Account ID: XXXXXXXX OCM Account Name: XXXXX OCM Account Username: XXXXXXXX もしこのようなエラーが出てきたら、AWS CLIのセッション切れが原因です。 E: Failed to create AWS client: operation error STS: GetCallerIdentity, https response error StatusCode: 403, RequestID: 76d46612-1094-49a6-a78c-4635978a2a42, api error ExpiredToken: The security token included in the request is expired 以下のコマンドで認証情報を更新してください。 $ unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_CREDENTIAL_EXPIRATION $ eval $(aws configure export-credentials --format env) その後再度rosaコマンドを試してみてください。 クラスター構築 Account Roleの作成 Account Role(アカウントロール)は、Red Hat側がユーザーのAWSアカウント内でクラスターの構築や運用を行うために、安全に権限を委譲するためのIAMロールです。 AWSアカウント単位の共通権限となるため、ユーザーのAWSアカウントにつき1回だけ生成すれば、以降は使い回すことができます。 以下のコマンドで必要なAccount Roleが一括で自動作成されます。 $ rosa create account-roles --hosted-cp --mode auto –hosted-cpというのは、前回の記事で紹介したHCP(Hosted Control Planes)方式に最適化されたAccount Roleを生成するという指定です。 Classic方式との一番大きな違いは、ControlPlane-Role(コントロールプレーン用のロール)が作成されない点です。HCP方式では、コントロールプレーンはRed Hat側で管理するため、ユーザーのAWSアカウント側にIAMロールを生成する必要がありません。 具体的には以下の3つのロールが生成されます。ManagedOpenShift はデフォルトのプレフィックス名です。プレフィックスを指定したい場合は、–prefix <指定したいプレフィックス名> で設定可能です。 ManagedOpenShift-HCP-ROSA-Installer-Role: クラスターの基盤となるVPCやEC2などを自動で組み立てるための権限。 ManagedOpenShift-HCP-ROSA-Support-Role: Red HatのSREチームが障害発生時などにクラスターの状況を調査・対応するための権限。 ManagedOpenShift-HCP-ROSA-Worker-Role: アプリケーションが起動するワーカーノード(EC2インスタンス)が、AWSの他の機能(ストレージなど)を操作するための権限。 作成されたAccount Roleは以下のコマンドで確認できます。 $ rosa list account-roles # 出力例 ROLE NAME ROLE TYPE ROLE ARN OPENSHIFT VERSION AWS Managed ManagedOpenShift-HCP-ROSA-Worker-Role Worker arn:aws:iam::XXXXXXXXXXXX:role/ManagedOpenShift-HCP-ROSA-Worker-Role 4.21 Yes ManagedOpenShift-HCP-ROSA-Installer-Role Installer arn:aws:iam::XXXXXXXXXXXX:role/ManagedOpenShift-HCP-ROSA-Installer-Role 4.21 Yes ManagedOpenShift-HCP-ROSA-Support-Role Support arn:aws:iam::XXXXXXXXXXXX:role/ManagedOpenShift-HCP-ROSA-Support-Role 4.21 Yes Operator Roleの作成 上で作成したAccount Roleは、Red Hat側がユーザーのAWSアカウントを利用して作業するための権限でしたが、Operator Roleは、OpenShiftの内部からAWSリソースを直接操作するためのIAMロールです。 OpenShiftの内部では、ネットワーク管理やストレージ管理、ログ管理など、それぞれの役割に特化した「Operator(オペレーター)」と呼ばれる複数のプログラムが動いています。それらのプログラムがAWSリソース(ロードバランサーやEBSなど)を自動で作成・管理することになるため、各Operatorに必要な最小限の操作権限だけIAMロールとして切り出して、割り当てる仕組みになっています。 また、OperatorにAWSのアクセスキーなどの永続的な認証情報を持たせるのを防ぐため、「AWS STS(AWS Security Token Service)」という仕組みを利用して、OIDCプロバイダーと連携し安全に一時的な操作権限を受け取れるようにします。 そのため、Operator Roleを作成する前に、連携先となるOIDCプロバイダーを用意する必要があります。 以下のコマンドでOpenID Connect Config(oidc-config)を作成することで、AWS側にもOIDCプロバイダーが自動で作成されます。 $ rosa create oidc-config --mode=auto --managed=true --yes 作成できたら、以下のコマンドで生成されたOIDC Configの情報を確認しましょう。 $ rosa list oidc-config # 出力例 ID MANAGED ISSUER URL SECRET ARN XXXXXXXXX true XXXXXXXXX 出力結果に表示される IDはこの後の手順で利用するため、手元に控えておきます。 準備が整ったら、以下のコマンドでOperator Roleを生成します。 $ rosa create operator-roles --hosted-cp --mode auto \ --prefix=demo \ --oidc-config-id=<上で作成されたOIDC ConfigのID> \ --installer-role-arn=<上で作成されたInstaller-RoleのARN> 以下のコマンドで作成されたOperator Roleを確認できます。 $ rosa list operator-roles # 出力例 ROLE PREFIX AMOUNT IN BUNDLE  demo         8 具体的なAWSのリソースとしては、以下のロールが自動生成されます。 補足:IAMロール名がプレフィックス値を含めて64文字を超える場合は、64文字になるように末尾が切り捨てられます。 <prefix>-openshift-ingress-operator-cloud-credentials :ユーザーがアプリを外部公開する際に、AWS側にロードバランサー(ALBやNLB)を自動作成したり、Route 53のDNS設定を管理したりする権限。 <prefix>-openshift-cloud-network-config-controller-cloud-credential :OpenShift内部のネットワークと、AWSのネットワークを連動させるための権限。 <prefix>-openshift-cluster-csi-drivers-ebs-cloud-credentials :AWSのEBSをコンテナのストレージとして割り当てるための権限。 <prefix>-openshift-image-registry-installer-cloud-credentials :コンテナイメージを保存するためにAWSのS3バケットを生成・操作するための権限。 また、従来のClassic方式と比べると、HCP方式ではコントロールプレーンが分離されているため、kube-system-〜 から始まるHCP専用のOperator Roleも生成されます。 <prefix>-kube-system-capa-controller-manager :ワーカーノード(EC2)の台数を自動で増減させる機能の権限(オートスケーリングなど)。 <prefix>-kube-system-control-plane-operator :Red Hat側のコントロールプレーンからの指示でユーザーのAWS環境のインフラを操作できる権限。 <prefix>-kube-system-kube-controller-manager :Kubernetesの標準的な管理プログラムが、AWSのリソースを監視できるようにする権限。 <prefix>-kube-system-kms-provider :AWSのKMSと連携し、Kubernetesのシークレットを暗号化するための権 VPCとサブネットの作成 ROSAクラスタをインストールするVPCを作成します。今回はAWSコンソールで簡単に構築してみます。 AWSでVPCを開き、「 お使いのVPC」で VPCを作成を選択します。 VPCだけでなくサブネットやNAT Gatewayも一緒に作成するために「VPCなど」を選択します。 選択後の各種設定値は、以下の表を参考にしてください。表に記載がない項目はデフォルト(初期状態)のままで問題ありません。今回はデモのために作るので、なるべく最小限必要な構成にしています。 パラメータ 説明 名前タグの自動生成 任意の名前(例:rosa-demoなど) アベイラビリティゾーン (AZ) の数 1 パブリックサブネットの数 1 プライベートサブネットの数 1 NAT ゲートウェイ Zonal(1 AZ 内に配置) VPC エンドポイント なし すべての設定が終わったら、画面一番下にある 「VPC を作成」 ボタンを押します。 (作成完了には数分程度かかります) また、subnet-から始まるパブリックサブネットのIDとプライベートサブネットのIDはこのあとのクラスター作成で使うため、テキストエディタ等にメモしておいてください。 ROSAクラスター作成 Account RoleとOperator Role、そして事前にAWS側に用意したVPC(サブネット)の準備ができたら、以下のコマンドを実行してクラスターを作成します。 $ rosa create cluster --cluster-name <クラスター名> \ --sts \ --mode auto \ --hosted-cp \ --subnet-ids <パブリックサブネットID>,<プライベートサブネットID> \ --oidc-config-id <上で作成されたOIDC ConfigのID> \ --operator-roles-prefix <Operator Roleを作成するときに指定したprefix> \  --domain-prefix <クラスターのURLのサブドメイン名> \ --yes 各パラメータの説明 パラメータ 説明 –cluster-name 作成するクラスターの一意の名前。任意の英数字で指定。 –sts AWSの安全な一時認証(Security Token Service)を使用するという指定。HCP方式では必須。 –mode auto 途中の質問に対して、すべて自動(デフォルト値)で作成を進めるための指定。 –hosted-cp  HCP方式 でクラスターを作成するためのフラグ。 –subnet-ids クラスターを配置するAWSの既存サブネット(パブリックとプライベート)のIDをカンマ区切りで指定。 –oidc-config-id Operator Role作成時に指定したprefixの値(例: demo)を指定。 –operator-roles-prefix 前の手順で作ったOperator Role群を識別するための接頭辞(例: ManagedOpenShift)を指定。 –domain-prefix 自動生成されるクラスターのURL(管理画面やAPIサーバーのURL)のサブドメイン部分をカスタマイズするための設定(オプション)。 –yes コマンド実行時の最終確認(Are you sure?)をスキップするフラグ。 クラスターの作成が完了するまではおよそ10分〜15分程度かかります。バックグラウンドでどのように構築が進んでいるかリアルタイムで確認したい場合は、以下のコマンドを実行してください。 $ rosa logs install --cluster <クラスター名> --watch # 出力例 bak@1010-00867:~$ rosa logs install -c rosa-demo --watch W: The current version (1.2.60) is not up to date with latest rosa cli released version (1.2.64). W: It is recommended that you update to the latest version. I: Cluster 'rosa-demo' is in validating state waiting for installation to begin. Logs will show up within 5 minutes \ 0001-01-01 00:00:00 +0000 UTC hostedclusters rosa-demo Version 2026-07-13 07:04:09 +0000 UTC hostedclusters rosa-demo Condition not found in the CVO. 2026-07-13 07:04:09 +0000 UTC hostedclusters rosa-demo The hosted control plane is not found 以下のようなログが出たら、ROSAクラスターのデプロイは完了です。 I: Cluster 'rosa-demo' is now ready 接続確認 クラスターの構築が完了したら、実際にログインして接続確認を行いましょう。 接続するためには、まず管理者アカウントである cluster-admin を作成する必要があります。 以下のコマンドでcluster-adminを作成します。コマンドを実行すると、passwordが自動生成され、そのままログインできるコマンド(oc login〜)がターミナルに出力されます。 $ rosa create admin --cluster=<クラスター名> # 出力例 I: Admin account has been added to cluster 'your-cluster-name'. I: Please securely store this generated password. If you lose this password you can delete and recreate the cluster admin user. I: To login, run the following command: oc login https://api.your-cluster-name.xxxx.p1.openshiftapps.com:443 --username cluster-admin --password XXXXX-XXXXX-XXXXX-XXXXX 以下のようにoc loginコマンドを入力してLogin successful. と表示されれば、CLIからの接続確認は完了です。 $ oc login https://api.your-cluster-name.xxxx.p1.openshiftapps.com:443 --username cluster-admin --password XXXXX-XXXXX-XXXXX-XXXXX Login successful. もし、以下のエラーが出たら、数分待ってから再度試してみてください。 Login failed (401 Unauthorized) Verify you have provided the correct credentials. また、CLIからだけでなく、ウェブブラウザを使って管理コンソール(GUI)にログインする方法もあります。 以下のコマンドを実行して、出力結果から「Console URL」の項目を確認します。 $ rosa describe cluster --cluster=<作成したクラスター名> #出力例 W: The current version (1.2.60) is not up to date with latest rosa cli released version (1.2.64). W: It is recommended that you update to the latest version. Name: rosa-demo Domain Prefix: rosa-demo Display Name: rosa-demo ID: XXXXXXXXXX External ID: XXXXXXXXXX Control Plane: ROSA Service Hosted OpenShift Version: 4.20.28 Channel Group: stable DNS: XXXXXXXXXX AWS Account: XXXXXXXXXX AWS Billing Account: XXXXXXXXXX API URL: XXXXXXXXXX Console URL: https://console-openshift-console.apps.rosa.xxxxx.openshiftapps.com Console URLをブラウザに入力してアクセスすると、以下のように管理コンソールに接続できますので、CLIでのログイン(oc login)時に使用した、管理者(cluster-admin)のusernameとpasswordを入力してログインします。 このような画面が出たら管理コンソールにログイン成功です。 (オプション)後片付け ROSAを構築するために作成した各種リソースは費用が発生するので、もし今後使わないのであれば片付けておくことをおすすめします。 また、漏れなく削除するために以下の記載順で削除していくことをおすすめします。 1. クラスターの削除 まず、以下のコマンドでクラスターを削除します。 $ rosa delete cluster --cluster=<クラスター名> --yes 削除中のログの確認は以下のコマンドでできます。 $ rosa logs uninstall --cluster=<クラスター名> --watch 実行例です。 $ rosa logs uninstall --cluster=rosa-demo --watch W: The current version (1.2.60) is not up to date with latest rosa cli released version (1.2.64). W: It is recommended that you update to the latest version. 2026-07-13 07:41:44 +0000 UTC hostedclusters rosa-demo invalid service account signing key: failed to get hostedcluster ServiceAccountSigningKey secret bound-service-account-signing-key: Secret "bound-service-account-signing-key" not found 2026-07-13 07:41:45 +0000 UTC hostedclusters rosa-demo pull secret unavailable: Secret "rosa-demo-pull" not found 2026-07-13 07:41:46 +0000 UTC hostedclusters rosa-demo Reconciliation completed successfully 2026-07-13 07:45:48 +0000 UTC hostedclusters rosa-demo All is well 2026-07-13 07:45:49 +0000 UTC hostedclusters rosa-demo ValidAWSIdentityProvider StatusUnknown 2026-07-13 07:45:49 +0000 UTC hostedclusters rosa-demo All guest resources destroyed I: Cluster 'rosa-demo' completed uninstallation 「Cluster ‘XXXXXX’ completed uninstallation」というログが出たら削除完了です。 クラスターの削除が完了したら、AWSコンソールの「VPC」サービス画面を開き、AWSのリソースを削除していきます。 2. NATゲートウェイの削除 VPCコンソール > 左メニューの「NAT ゲートウェイ」に移動し、作成したNATゲートウェイを削除します。削除済み(Deleted)になるまで数分かかることがあります。 3. VPCの削除 VPCコンソール > 左メニューの「お使いの VPC」に移動し、作成したVPCを選択し削除します。VPCを削除すると、以下の関連リソースもすべて削除されます。 サブネット(パブリック / プライベート) ルートテーブル インターネットゲートウェイ(IGW) セキュリティグループ ※注意:VPCの削除でエラーが出る場合 クラスターの削除完了直後にVPCを消そうとすると、「eni-xxxx (VPC Endpoint Interface) が使用中(in use)のため削除できません」というエラーが出る場合があります。 5分ほど待つか、AWSコンソールの「VPC」>「エンドポイント」から対象のID(vpce-xxxx)を手動で削除してから、再度VPCの削除を行ってください。 4. Elastic IP(EIP)の解放 VPCコンソール > 左メニューの「Elastic IP」に移動し、NATゲートウェイ用に自動取得されていたElastic IPを選択し、画面右上の「アクション」>「Elastic IP アドレスの解放」をクリックします。 これでクラスターと関連リソースの削除は完了です。 5. オペレーターロールの削除 $ rosa delete operator-roles --prefix=<プレフィックス名> --mode=auto --yes 以下は実行例です。 $ rosa delete operator-roles --prefix=demo --mode=auto --yes W: The current version (1.2.60) is not up to date with latest rosa cli released version (1.2.64). W: It is recommended that you update to the latest version. I: Fetching operator roles for the prefix: demo I: Deleting operator role 'demo-kube-system-capa-controller-manager' I: Deleting operator role 'demo-kube-system-control-plane-operator' I: Deleting operator role 'demo-kube-system-kms-provider' I: Deleting operator role 'demo-kube-system-kube-controller-manager' I: Deleting operator role 'demo-openshift-cloud-network-config-controller-cloud-credentials' I: Deleting operator role 'demo-openshift-cluster-csi-drivers-ebs-cloud-credentials' I: Deleting operator role 'demo-openshift-image-registry-installer-cloud-credentials' I: Deleting operator role 'demo-openshift-ingress-operator-cloud-credentials' I: Successfully deleted the operator roles 6. OIDC Configの削除 $ rosa delete oidc-config --oidc-config-id=<OIDCのID> --mode=auto --yes 以下は実行例です。 $ rosa delete oidc-config --oidc-config-id 2reuclobhhsao2hdrjpc43lcoq5151go W: The current version (1.2.60) is not up to date with latest rosa cli released version (1.2.64). W: It is recommended that you update to the latest version. ? OIDC Config deletion mode: auto ? Delete the OIDC provider 'arn:aws:iam::XXXXXXXXXXXX:oidc-provider/oidc.op1.openshiftapps.com/2reuclobhhsao2hdrjpc43lcoq5151go'? Yes I: Successfully deleted the OIDC provider arn:aws:iam::XXXXXXXXXXXX:oidc-provider/oidc.op1.openshiftapps.com/2reuclobhhsao2hdrjpc43lcoq5151go I: Registered OIDC Config ID '2reuclobhhsao2hdrjpc43lcoq5151go' has been removed from OCM and can no longer be used まとめ 以上で、ROSAクラスターの構築に必要な事前準備から、各種IAMロールの作成、実際のデプロイ、そして接続確認までの一連の手順が完了しました。 「ROSAって名前は聞くけど、具体的にどうやって始めれば良いんだろう?」という漠然とした疑問やハードルが、この記事を通して少しでも解消できたら幸いです。 次回からは、ログの保管やメトリクスの転送など、クラスターの内部をさらに充実させていく内容を紹介していきます。ぜひ楽しみにしていてください。 参考資料 Getting started with Red Hat OpenShift Service on AWS ( Red Hat Documentation ) Chapter 2. Creating Red Hat OpenShift Service on AWS clusters using the default options( Red Hat Documentation) Create a ROSA with HCP cluster using the ROSA CLI(Red Hat Documentation) Hosted Control Planes(rosaworkshop.io) ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post ROSA(Red Hat OpenShift Service on AWS)を利用したコンテナプラットフォーム構築 ~クラスタ構築~ first appeared on SIOS Tech Lab .
こんにちは、OSSよろず相談室のSKです。 OSS に関するお問い合わせが日々寄せられる中で、今回は 自社でDNSサーバを運用している環境において、BIND の DNSSEC に関連して寄せられたお問い合わせをご紹介します。 少し前ですが、以下のような問い合わせをいただきました。 KSKロールオーバーについてJPRSから以下が公開されています。 新しいKSK(KSK-2024)がルートサーバーで事前公開 DNSSEC に対応しているDNSサーバの、トラストアンカーの自動更新の設定がautoになっています。 ・named.confの設定:dnssec-validation auto; この場合、KSKロールオーバーに関して追加の作業は特に必要ないでしょうか。 回答は「追加の作業は必要ありません。」 named.conf の dnssec-validation auto; 設定が有効になっている場合、BINDは新しいトラストアンカーを自動的に取得するようになっているからです。 rndc managed-keys status コマンドでトラストアンカーが更新されたかどうかを確認することができます。 今回は、このやり取りに関連して、「DNSSEC」「トラストアンカー」「KSKロールオーバー」とは何か、そして dnssec-validation を auto にしておく影響について解説します。 DNSSECとは? DNSはサーバ名からIPアドレスを問い合わせる仕組みですが、応答を途中で改ざんして別のIPアドレスに誘導する「DNSキャッシュポイズニング」という攻撃手法があります。 このDNSの応答データが改ざんされていないことを、暗号署名を用いて検証するセキュリティ拡張の仕組みが「DNSSEC」です。 トランスアンカーとは? DNSSECの署名検証を行う上で、起点となるルートゾーンのKSK(鍵署名鍵)のことです。 BINDなどのDNSサーバは、このルートKSKのコピーを検証の基点となる「トラストアンカー」として自ら保持します。 KSKロールオーバーとは DNSSECでは、ゾーンデータの署名に用いる「ZSK」と、鍵情報の署名に用いる「KSK」が使用されます。 DNSSECの仕組み(DS方式) KSKロールオーバーとは、セキュリティ維持のためにこのKSKを新しい鍵に置き換える(更新する)プロセスのことです。 ルートゾーンのKSKが更新される場合、世界中のDNSサーバは新しいルートKSKをトラストアンカーとして認識し直す必要があります。JPRSの発表にある通り、現在ルートゾーンでは 新しいKSK(KSK-2024) への移行が進行しており、 2026年10月11日 に新KSKへの本番切り替えが予定されています。 新しいKSK(KSK-2024)がルートサーバーで事前公開 dnssec-validation を auto にする BINDの named.conf にて dnssec-validation auto; を設定すると、DNSSECの署名検証とともに、RFC 5011に基づくトラストアンカーの自動更新機能が有効になります。 ルートKSKのロールオーバーが実施された際、BINDは自動的に新しい鍵を検知します。その後、30日間の保留期間(trust pending)を経てから、新しい鍵を正式なトラストアンカーとして信頼(trusted)します。 この設定を auto にしておくことで、手作業による鍵ファイルの書き換えが不要になり、古い鍵のまま検証が失敗して名前解決ができなくなるトラブルを防ぐことができます。 (dnssec-validation の参考: BIND 9 Administrator Reference Manual – DNSSEC Validation Explained ) rndc managed-keys status コマンド トラストアンカーの自動更新の状態は、以下のコマンドで確認できます。 $ rndc managed-keys status rndcコマンド は、BINDのプロセスnamedを制御するためのコマンドツールです。 実行結果の trust の項目を確認することで、BINDが認識している鍵が現在「保留中(trust pending)」なのか、無事に「信頼されている(trusted)」状態に移行したのかなど、ロールオーバーの進捗状況を把握できます。 $ rndc managed-keys status view: _default class: IN name: . keyid: 20326 trust: trusted 出力結果の trust の項目が trusted となっていれば、その鍵は正常にトラストアンカーとして機能しています。ロールオーバーの過渡期には、新しい鍵のステータスが initializing や pending といった状態を経て、最終的に trusted に移行する様子を確認することができます。 まとめ dnssec-validation auto; が設定されている環境では、トラストアンカーの自動管理機能が働くため、KSKロールオーバーに向けた手動での追加作業は必要ありません。 2026年10月11日に予定されている新KSKへの本番切り替えに向けて、30日間の保留期間を考慮すると、残り約3ヶ月となった現在のタイミングで自社のDNSサーバが auto 設定になっているかを確認しておくことを推奨します。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post OSSサポートの現場から!BIND / ルートKSKロールオーバーへの対応 first appeared on SIOS Tech Lab .
2018年6月の創刊から、毎月一歩ずつ歩みを進めて参りました「SIOS OSSよろずNews」のメルマガも、このたび「100号」という大きな節目を迎えることができました。長きにわたり、私たちの発信する情報に目を通し、温かいフィードバックをお寄せくださった読者の皆様に、心からの感謝を申し上げます。 100号記念企画として、皆さまの「現場で体験したOSSにまつわるエピソード」 を募集します。 これまで本メルマガでは、OSSに関する最新情報や技術動向をお届けしてまいりました。今回は少し趣向を変え、OSSを利用・運用する中で皆さまが経験された出来事や思い出をぜひお聞かせください。 「今では笑い話になった失敗談」「OSSに助けられた経験」「思わぬトラブルとの格闘」「コミュニティとの心温まる交流」など、どんなエピソードでも大歓迎です。印象に残った出来事をぜひお聞かせください。 投稿はこちら 過去のメルマガはこちら The post SIOS OSS よろず Newsメルマガ100号記念! first appeared on SIOS Tech Lab .
今号では、2026年 5月号でご紹介した Linux におけるターミナル操作に関する tips の続きをご紹介します! 2026年 5月号の記事は こちら カーソル移動のショートカットキーあれこれ [Ctrl] + [A] カーソルが行頭へ一気に移動します。 [Ctrl] + [E] カーソルが行末へ一気に移動します。 [Ctrl] + [U] カーソルより前の文字をすべて削除します。 [Ctrl] + [K] カーソルより後ろの文字をすべて削除します。 [Ctrl] + [K] カーソルより後ろの文字をすべて削除します。 [Alt] + [←] カーソルより 1単語分戻ります。 [Alt] + [→] カーソルより 1単語分進みます。 ※一部、過去の記事( 知っておくとちょっと便利!コマンド5選 ~ショートカットキー編~ )でご紹介した内容と重複しています。ご了承ください。 出力内容を自在に制御する小技 grep -v で不要な情報を出力しない grep で -v オプションを指定すると、指定した文字を “含まない” 行のみを出力します。 この機能を利用して、下記のように必要な情報だけを抽出するのに便利です。 設定ファイルのコメントアウト部分 (行頭に # が記載されている個所) を除外して検索 # less httpd.conf | grep -v "^#" アクセスログから、正常なログ (ステータスコードが 200) を除外して検索 # less /var/log/httpd/access_log | grep -v "status:200" less -S で出力の折り返しを防ぐ less で -S オプションを指定すると、画面の右端で出力を折り返さず、矢印キー (← →) でスクロールして読めるようになります。 特に長いログを表示した場合は、形が崩れずに読みやすさが向上します。 192.168.10.45 - - [06/Jul/2026:12:01:45 +0900] "GET /api/v1/products?category=> 10.0.2.15 - - [06/Jul/2026:12:02:10 +0900] "POST /api/v1/cart/checkout HTTP/1.> 192.168.11.102 - - [06/Jul/2026:12:03:02 +0900] "GET /assets/js/main.min.js?v=> 172.16.5.89 - - [06/Jul/2026:12:04:15 +0900] "GET /api/v1/user/profile HTTP/1.> 192.168.10.45 - - [06/Jul/2026:12:05:59 +0900] "GET /images/products/laptop_01> ※矢印キー (← →) で左右にスクロールします。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 1人がこの投稿は役に立ったと言っています。 The post 知っておくとちょっと便利!ターミナル操作の時短テクニック2 first appeared on SIOS Tech Lab .
こんにちは! 今月も「OSSのサポートエンジニアが気になった!OSSの最新ニュース」をお届けします。 2026/7/1、LPI-Japan は Linux サーバ構築の知識を学べる学習用教材「Linuxサーバー構築標準教科書」の Ubuntu版をリリースし、ホームページで公開しました。 LPI-Japan、「Linuxサーバー構築標準教科書」のUbuntu版を公開 https://cloud.watch.impress.co.jp/docs/news/2121848.html 2026/7/13、株式会社ニチレイは不正アクセスによるシステム障害が発生したと発表しました。 ニチレイロジグループ各社の冷蔵倉庫の入出庫業務、およびニチレイフーズの冷凍食品出荷業務に影響が出ているとのことです。 ニチレイ、不正アクセスによるシステム障害が発生。入出庫などの業務に影響 https://internet.watch.impress.co.jp/docs/news/2124795.html 2026/7/23~24、グランフロント大阪にて「事業変革を推進するための最新技術とつながる総合展」EdgeTech+ West 2026 が開催されます。 EdgeTech+ West https://www.jasa.or.jp/etwest/ ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 【2026年7月】OSSサポートエンジニアが気になった!OSS最新ニュース first appeared on SIOS Tech Lab .
こんにちは、サイオステクノロジー武井です。今回はasync/awaitの動きをOSカーネルのレイヤーから追うことで理解を深めようとしてみます。 1. async/awaitはむずい async / await 、なかなかにとっつきにくい概念だと思います。 「非同期処理には   async   を付ける」「 await   しないと結果が取れない」——ルールとしては覚えられます。コードも動きます。でも、こんな疑問にぶつかった経験はないでしょうか。 await   している間、プログラムは「止まって」いるの? 止まっていないの? 「待つ」のに、なんで速くなるの? await   を付け忘れると Promise が返ってくるのはなぜ? そもそも誰が「処理が終わった」ことに気づいて、続きを実行してくれるの? これらイベントループやepollの仕組みを知っていると、スッキリ解決するのではと思い、今回、一筆したためました。 キーになるのは、 イベントループ という仕組みと、さらにその下で動く   OS カーネルの epoll   という機構です。 async / await   はこの2つの上にかぶせられた「一番上の薄い皮」にすぎません。皮だけを眺めて中身を推測しようとするから難しいのであって、下の層から順に積み上げれば、実はかなり素直な仕組みです。 この記事では、その水面下を「イベントループと fd」→「実際のコードの動き」の順で見ていきます。 2. イベントループとは何者か 「待つ」のはもったいない プログラムがデータベースや外部 API にアクセスすると、返事が来るまでの待ち時間が発生します。CPU の感覚では、この待ち時間は途方もなく長い。メモリアクセスを「1秒」とすると、ネットワークの応答待ちは「数週間〜数ヶ月」に相当します。 つまり I/O 待ちの間、CPU は膨大にヒマをしています。「このヒマな時間に別の仕事をさせたい」——これが非同期処理のすべての動機です。 1本のスレッドで、全部の「待ち」を見張る 古典的なやり方は、リクエストごとにスレッドを用意して、それぞれに待たせる方式でした。しかしスレッドは1本あたり数MBのメモリを食うため、同時接続数が多くなると厳しいことになります。 そこで発想を逆転させます。 1本のスレッドが、大量の「待ち」をまとめて見張り、準備ができたものから順に処理する 。レストランでいえば、客1組ごとにウェイターを張り付かせるのではなく、1人のウェイターが全テーブルを担当し、「呼ばれたテーブルにだけ行く」方式です。 この「ぐるぐる回りながら、準備できた仕事を順に捌く」ループこそが イベントループ です。 ここで2つの重要な部品が登場します。 epoll   と   fd   です。 fd (ファイルディスクリプタ)とは? fd(ファイルディスクリプタ)   は、OS が通信の窓口(ソケットなど)1つ1つに割り振る、ただの整数です。DB への接続なら fd9、外部 API への接続なら fd10、というように、「どの待ち先か」を番号で識別できます。 ポイントは、 fd は単なる番号であって、中身のデータでも処理でもない こと。「9番の窓口」「10番の窓口」という札だと思ってください。 epoll とは? epoll   は Linux カーネルの機能で、やることは一言でいうと: 大量の fd を登録しておくと、「データが届いた/送れる状態になった」fd だけをまとめて教えてくれる イベントループの   epoll.wait()   は、カーネルへの「どれか準備できるまで寝てるから、できたら起こして」という依頼です。1万個の fd を登録していても、実際に用があるのが2個なら、2個分の番号だけが返ってきます。全部を1個ずつ見回る必要がない——ここが決定的に効率的です。 fd とコールバックは「別の場所」に保管される もう1つ、仕組みの肝になるのが情報の置き場所です。 カーネル が持つのは「監視すべき fd の一覧」と「準備できた fd の一覧」——つまり 番号だけ 「fd9 の準備ができたら、どの処理(コールバック)を実行するか」という**対応表(登録簿)**は、Node.js でいえば   libuv (ランタイムの C エンジン)が持つ カーネルからの通知は「fd9 が準備完了」という番号だけ。libuv はその番号を鍵にして、自分の登録簿から「fd9 → このコールバック」を引き当て、実行キューに積みます。番号で照合して、初めて「続きの処理」が動き出すわけです。 この分業を頭に入れた上で、実際のコードを見てみましょう。 3. コードで追う:DB アクセスと API アクセスが「すれ違いで」進む 例えば、こんなコードがあるとします ユーザー情報を   DB   から、最新のお知らせを   外部 API   から取ってくる——お互いに関係のない2つの非同期処理です。 // DBからユーザーを取得する処理 async function getUser ( ) { console . log ( "A-1: DBに問い合わせを実行" ) ; const user = await db . findUser ( 9 ) ; // ← DBアクセス(待ち発生) console . log ( "A-2: DB応答が届いた!" ) ; return user ; } // 外部APIからお知らせを取得する処理 async function getNews ( ) { console . log ( "B-1: APIに問い合わせを実行" ) ; const news = await api . fetchNews ( ) ; // ← APIアクセス(待ち発生) console . log ( "B-2: API応答が届いた!" ) ; return news ; } // 2つを同時にスタートさせて、両方の完了を待つ const [ user , news ] = await Promise . all ( [ getUser ( ) , getNews ( ) ] ) ; console . log ( "C: 両方そろった!" ) ; 実行すると、ログはこの順で出ます。 A-1: DBに問い合わせを実行 B-1: APIに問い合わせを実行 ← DBの返事を待たずにBが始まっている! A-2: DB応答が届いた! B-2: API応答が届いた! C: 両方そろった! 注目してほしいのは   A-1 の直後に B-1 が出る ことです。DB の応答(A-2)を待たずに、API への問い合わせ(B-1)が始まっています。 await   で「待っている」はずなのに、なぜ次の処理へ進めるのか——ここで、さきほどのイベントループと fd が登場します。 図と対応させて、裏側を追う await db.findUser(9)   の裏側で起きていることを、4枚の図で「 登録 → すれ違い → 完了 」の順に追っていきます。登場人物は、コードを走らせる アプリケーション 、番号だけを見張る カーネル 、対応表を持つ libuv 、そしてその対応表である libuv の登録簿 の4者です。 ステップ1: getUser   の番—— await   に到達し、fd9 を登録する getUser()   が呼ばれ、A-1 のログを出し、 await db.findUser(9)   に到達します。ここで 登録フェーズ が動きます。   ①   アプリケーションが libuv に「 DB アクセス処理 を頼む」と依頼します。DB への問い合わせも、この依頼にのって送り出されます。 ②   libuv がカーネルに「この接続を見張って」と依頼します。 ③   カーネルの監視対象に、DB 接続の窓口である   fd9   が登録されます。以後カーネルは fd9 を見張り続けます。 ④   libuv が自分の登録簿に「 fd9 → コールバック処理(A-2 以降の続き) 」を控えます。 そして決定的なのが次の一歩です。 getUser   は DB の返事を 待ちません 。①〜④の依頼と登録だけ済ませて、その場で中断し、 制御をイベントループに返します 。ウェイターが注文を厨房に通して、すぐテーブルを離れるのと同じです。 ステップ2:空いた隙間で、 getNews   の番——fd10 も登録する getUser   が席を離れた瞬間、イベントループは次の仕事に取りかかれます。それが   getNews()   です。B-1 のログが出て、 await api.fetchNews()   に到達し—— まったく同じ①〜④が、今度は別の fd(fd10)で 繰り返されます。   ①   アプリケーションが libuv に「 API アクセス処理 」を依頼(ここで API への問い合わせも実行)。 ② ③   libuv がカーネルに依頼し、カーネルの監視対象に   fd10   が加わります。 ④   libuv の登録簿に「 fd10 → コールバック処理(B-2 以降の続き) 」が控えられます。 そして   getNews   も席を離れます。 これが「 A-1 の直後に B-1 が出る 」カラクリです。 DB の待ち時間という「ヒマ」に、API への問い合わせという別の仕事が差し込まれた わけです。ステップ2の状態を見てください。カーネルは fd9(DB)と fd10(API)の2つを並べて見張っており、libuv の登録簿にも2つの続きが控えられています。スレッドはどちらの返事も来ていないので   epoll.wait()   で眠っており、CPU 消費はゼロです。 ステップ3:返事が届いた——fd9 の完了フェーズ やがて DB の応答が届きます。ここからは 完了フェーズ です。   ①   見張っていたカーネルが「fd9 にデータが届いた」ことを検知し、libuv に 完了通知 を送ります。流れるのは「 fd9 が準備完了 」という番号だけです。 ②   libuv がその番号を鍵に登録簿を引き、「fd9 → コールバック処理」を 取得 します。 ③   そのコールバックがアプリケーション上で 実行 され、 getUser   が   await   の次の行(A-2)から再開 します。 ステップ4:少し遅れて、fd10 の完了フェーズ 少し遅れて API の応答も届けば、今度は fd10 について まったく同じ①〜③ が走ります。   ①   カーネルが「fd10 が準備完了」を libuv に通知。 ②   libuv が登録簿から「fd10 → コールバック処理」を取得。 ③   コールバックが実行され、 getNews   が   B-2 から再開 します。 両方そろったところで   Promise.all   が解決し、C のログが出て完了です。 ところで、ここまで   await db.findUser(9)   と一息に書いてきましたが、この一行が裏で返している   Promise   という存在に、まだきちんと触れていませんでした。実はこの Promise こそ、いま追った図の世界と、私たちが書く JavaScript のコードとをつなぐ「窓口」です。同じ図をもう一度、今度は Promise の視点から見直してみましょう。 4. Promise とは何か db.findUser(9)   はその場で「券」を返す await db.findUser(9)   という一行は、実は2つの動作に分かれています。 const promise = db . findUser ( 9 ) ; // ① findUser がすぐ何かを返す const user = await promise ; // ② その「何か」の中身を取り出す db.findUser(9)   を呼ぶと、 その場ですぐ戻り値が返ってきます 。とはいえ、返ってくるのは DB のデータ……ではありません。まだ返事は届いていないのですから当然です。代わりに返るのが   Promise ——「結果は今はまだ無いけれど、あとで必ず渡す(か、失敗を伝える)と約束する券」です。レストランの 番号札 を思い浮かべてください。料理(データ)と引き換えるための、 非同期処理の引換券 です。 これを図のステップ1に重ねると、こうなります。 db.findUser(9)   を呼んだ瞬間——①で DB への問い合わせが実行され、③で fd9 がカーネルの監視対象に登録された、あの瞬間——アプリケーションの手元に残るのは引換券(Promise)だけで、中身はまだ空っぽです。   券には「ステータス」がある——監視対象と連動している この引換券は、ただの紙きれではありません。処理が今どこまで進んだかという ステータス を持っていて、その移り変わりを1枚にすると次の図になります。   図の見方はこうです。 左の   pending (保留中)   が出発点です。 db.findUser(9)   を呼んで券を受け取った直後—— ステップ1 の状態にあたります。まだ結果は無く、券は「保留中」のまま宙に浮いています。 ここから券は 2方向のどちらか一方 にしか進みません。返事が無事に届けば、右上の   fulfilled (成功)   へ。上向きの矢印に添えた   resolve (成功が確定)   がその引き金です。これは ステップ3 で受信コールバックが走り、データが揃った瞬間に相当します。 逆に通信エラーなどで失敗すれば、右下の   rejected (失敗)   へ。下向きの矢印の   reject (失敗が確定)   が引き金です。 そして矢印の向きに注目してください。 pending   から出ていく矢印はあっても、戻ってくる矢印はありません 。一度   fulfilled   か   rejected   に確定した券は、二度と別の状態に変わらない——これが「引換券のステータスは 一方通行 」という意味です。だから同じ Promise を何度   await   しても、確定済みの同じ結果が返ってきます。 さらに肝心なのは、この券のステータスが、 前章の図でカーネルが見張っている監視対象(fd9)の状態と連動している ことです。fd9 にまだ返事が来ていないステップ1〜2の間、券は   pending 。ステップ3でカーネルが「fd9 準備完了」を通知し、受信コールバックがデータを揃えた瞬間に、券は   pending → fulfilled   へ動きます。つまり Promise とは、 イベントループが見張っている対象のステータスを、開発者が JavaScript の世界から覗くための窓口 でもあるわけです。カーネル・libuv という水面下の状態が、この一枚の券に映し出されている、と考えると腑に落ちます。 券から中身を取り出す:本来は   .then() 券を握っているだけでは、データは使えません。「券が   fulfilled   になったら、この処理をして」とあらかじめ登録しておく必要があります。それが   .then()   です。 const promise = db . findUser ( 9 ) ; // 引換券を受け取る(pending) promise . then ( ( user ) => { // 「券が確定したら、これを実行して」と予約 console . log ( user . name ) ; } ) ; これ、見覚えがないでしょうか。前章の完了フェーズ(ステップ3)で「fd9 → 続きの処理」が登録簿に控えられ、券が確定した瞬間に呼び出される——構造はまったく同じです。 .then()   に渡した関数こそ、図の登録簿に入る「続き」の正体のひとつです。 await   は「券が中身に化けるのを待つ」ための、読みやすい書き方 .then()   でもデータは取り出せますが、処理が連なると入れ子が深くなり読みづらくなります(いわゆるコールバック地獄)。そこで   await   の出番です。 // .then スタイル:券を受け取って、続きをコールバックで書く db . findUser ( 9 ) . then ( ( user ) => { console . log ( user . name ) ; } ) ; // await スタイル:券が中身に化けるのを待って、同期的に受け取る const user = await db . findUser ( 9 ) ; // ← 券の中身をそのまま変数へ console . log ( user . name ) ; await   は「右側の Promise(券)が   fulfilled   になるまで席を離れて待ち、確定したら中の値を取り出して返す」係です。 .then()   のコールバックを書く代わりに、 あたかも同期処理のように、上から下へ 書けるようになります。裏で起きていること(登録して席を譲る → 券が確定 → 続きを再開)は前章の図とまったく同じで、 await   はそれを読みやすい見た目に整えているだけ——ここでも「一番上の薄い皮」なのです。 「 await   を付け忘れると Promise が返る」理由 ここまで分かると、冒頭の疑問——「 await   を付け忘れると Promise が返ってくるのはなぜ?」——にも答えられます。 const x = db . findUser ( 9 ) ; // awaitなし console . log ( x ) ; // → Promise { <pending> }(券そのもの) const user = await db . findUser ( 9 ) ; // awaitあり console . log ( user ) ; // → { id: 9, name: "..." }(中身) await   を付けなければ、券(Promise)を中身に引き換える動作が走らないまま次の行へ進みます。だから手元には引換券が残ったまま。「付け忘れると Promise が返る」の正体はこれです。 async   は「この関数は引換券を返す」という宣言 ここで、ずっと脇役だった   async   の正体もはっきりさせておきましょう。 async   は関数に付ける 印 で、意味はとてもシンプルです。 async   を付けた関数の戻り値は、必ず Promise(引換券)に包まれる。 return user   と書いても、呼び出し側が受け取るのは   user   そのものではなく   Promise<user> 。途中で例外を投げれば、その券は   rejected   になります。「この関数はすぐには結果を返さない。まず引換券を渡して、中身はあとで確定させる」——それを宣言するのが   async   です。 async function getUser ( ) { return { id : 9 , name : "Alice" } ; // ← ただのオブジェクトを返しているつもりでも } const r = getUser ( ) ; console . log ( r ) ; // → Promise { <fulfilled> }(券に包まれている) そしてもう一つ大事なルールがあります。 await (席を離れて待つ動作)が使えるのは、 async   を付けた関数の中だけ です。裏返すと、関数の中で一度でも   await   したいなら、その関数は   async   にせざるを得ない。 await   する=「自分もすぐには終われず、結果はあとで返す」ということなので、当然その関数も引換券を返す関数になる、という筋の通った話です。 async   は呼び出し元へ「伝染」していく この2つのルールを組み合わせると、面白い——そして最初は戸惑う——現象が起きます。 async / await   が、呼び出しチェーンを上へ上へと伝播していく のです。 // 末端:DBを待つので await が要る → async が必須 async function getUser ( ) { const user = await db . findUser ( 9 ) ; return user ; // 戻り値は Promise<user> } // 中間:getUser() の中身を使いたい → await する → 自分も async に async function buildProfile ( ) { const user = await getUser ( ) ; // ここで await return ` ${ user . name } さん ` ; // これも Promise<string> になる } // 上位:buildProfile() の中身を使いたい → また await → また async async function handler ( req , res ) { const text = await buildProfile ( ) ; // ここでも await res . send ( text ) ; } 一番下の   getUser   が「DB を待つ」ために   async   になった瞬間、その戻り値は引換券になります。その中身を使いたい   buildProfile   は   await   するしかなく、すると   buildProfile   も引換券を返す関数( async )になる。さらにその上の   handler   も……と、 「あとで返る」という性質が、値を使うすべての呼び出し元へ玉突きで広がっていく わけです。 なぜ途中で断ち切れないのでしょう。「中間の関数が、こっそり結果を待って 同期的な値 にして返す」ことができれば伝播は止まります。しかしそれは、その場に立ち止まってスレッドをブロックすることに他なりません。イベントループの大前提—— 待つ間に他の仕事を捌く ——を壊してしまいます。だから「待ちがある」という事実は隠せず、型(Promise)として全経路に正直に現れる。これは面倒というより、 「この経路には待ちが含まれる」がコード上で追跡できる 、むしろ安全な設計だと考えると腑に落ちます。 では、この伝播はどこで止まるのか。答えは エントリポイント です。Express のリクエストハンドラ、 main() 、イベントリスナ——プログラムの一番外側で誰かが最後に受け止めれば、そこで鎖は終わります。普段のアプリ開発で末端からフレームワークまで   async   を意識せずに書けているのは、 この一番外側をフレームワークが引き受けてくれている からです。私たちは「待ちを使う関数に   async   を付ける」を末端でやるだけで、あとは自然に上流までつながります。 では、その登録簿に控えられる「続き」—— .then()   の中身にあたる 受信コールバック は、具体的に何なのでしょうか。次章では、それを自分の手で書いてみます。 5. 登録簿に入るのは「受信コールバック」 ここまでで「fd の準備ができたら、対応する処理が実行される」と説明してきました。では、その「対応する処理」——図の登録簿に入っているものの正体は、具体的に何でしょうか。 この受信コールバックは、普段は   fetch   や   axios 、DB ドライバといった ライブラリが内部で用意している という点です。「ライブラリが勝手にやってくれる」の正体は、ライブラリの中で誰かがこのコールバックを書いて登録している、というだけ。つまり、 自分でも書けます 。低レベルな姿から順に見ていきましょう。 レベル1:最も生の姿(ソケット + コールバック) タイマーもHTTPライブラリも使わず、「fd を監視して、読めたらコールバック」を手で書くと、こうなります。Node の   net   モジュールが、図の登録簿への登録を担当します。 const net = require ( "net" ) ; // APIサーバーにTCP接続する(ここでfdが1つ割り当てられる) const socket = net . connect ( 80 , "example.com" , ( ) => { // 接続できたらHTTPリクエストを"実行"する(図の①) socket . write ( "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n" ) ; } ) ; // ★これが「登録簿に入るコールバック」そのもの★ // 「このソケット(fd)が読めるようになったら、これを実行して」と登録している socket . on ( "data" , ( chunk ) => { console . log ( "受信コールバックが起動:" , chunk . toString ( ) . slice ( 0 , 50 ) ) ; } ) ; socket . on ( "end" , ( ) => { console . log ( "受信完了" ) ; } ) ; socket.on("data", ...)   に渡している関数—— これが図の「fd9 → 受信コールバック」の中身 です。 .on("data")   を呼んだ瞬間、内部で「このfdが読める状態になったら、この関数を呼べ」と libuv の登録簿とカーネルの epoll に登録されます。ライブラリがやっていた「勝手に入れてくれる」の正体は、この   .on("data", callback)   の一行なのです。 ただしこれは コールバックスタイル なので、 await   はまだ使えません。 await   で受けたいなら、次のレベルで Promise に包みます。 レベル2:自分でPromiseを作って、awaitできるようにする 受信コールバックを   new Promise   の中に入れて、データが全部届いたら   resolve   する——これで   await   可能な関数を 自作 できます。 const net = require ( "net" ) ; // API取得を、await可能な関数として自作する function fetchFromApi ( ) { return new Promise ( ( resolve , reject ) => { const socket = net . connect ( 80 , "example.com" , ( ) => { socket . write ( "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n" ) ; // ① 実行 } ) ; let received = "" ; // ★登録簿に入る受信コールバック(自分で書いている)★ socket . on ( "data" , ( chunk ) => { received += chunk . toString ( ) ; // 届いたぶんを溜める(届くたびに呼ばれる) } ) ; // 全部届いたらPromiseを解決する socket . on ( "end" , ( ) => { resolve ( received ) ; // ← ここで pending → fulfilled(ステップ3③の引き金) } ) ; // 失敗したらreject socket . on ( "error" , ( err ) => { reject ( err ) ; } ) ; } ) ; } // これで自作関数をawaitできる! async function main ( ) { console . log ( "問い合わせ前" ) ; const body = await fetchFromApi ( ) ; // ← 自分で作ったPromiseをawait console . log ( "受信完了:" , body . slice ( 0 , 50 ) ) ; } main ( ) ; このコードで、記事の第3章で見た図の全要素が、自分のコードとして揃います。 図の要素 上のコードの該当箇所 ステップ1① 問い合わせの実行 socket.write(...) 登録簿に入る受信コールバック socket.on("data", ...) ステップ3③の引き金(Promise解決) resolve(received) await   の続き console.log("受信完了", ...) new Promise   の中身が、これまで「ライブラリが書いていた低レベル処理」だったもの です。それを自分の手で書いているのがこのコードで、 axios   や   fetch   の内部も、煎じ詰めれば(エラー処理・タイムアウト・再接続などを足しつつ)これと同じことをやっています。 レベル3:「登録簿に入るコールバック」だけを最小で ライブラリも DB も API も使わず、「fd を監視して、読めたらコールバック」の骨組みだけを抜き出すと、こうなります。標準入力(これも立派な fd=0 です)で見せると一番シンプルです。 // 標準入力(fd=0)も監視対象のひとつ process . stdin . on ( "data" , ( data ) => { // ★これが登録簿に入るコールバック★ // 「fd0が読めるようになったら(=キー入力があったら)これを実行」 console . log ( "入力コールバックが起動:" , data . toString ( ) . trim ( ) ) ; } ) ; console . log ( "入力待ち(でもプログラムは固まっていない)" ) ; これを実行すると、 .on("data", ...)   の関数がカーネルの epoll 経由で「fd0 が読めるようになったら呼ばれる」よう登録され、キーを打つたびに起動します。 待っている間もプログラムは固まらない ——これこそ、イベントループが背後で回っている何よりの証拠です。 まとめ:登録簿の中身は「返事が来たら動く係」 3つのレベルを通して見えてくるのは、登録簿に入るものの正体です。 入るのは「API アクセス処理(これから叩く)」ではなく、 受信コールバック(返事が来たら処理する係) その実体は   .on("data", ...)   に渡す関数で、「fd が読めたら実行して」と libuv/epoll に登録される await   可能にするには、その受信コールバックを   new Promise   で包み、データが揃ったら   resolve   する 普段使う   fetch ・ axios ・DBドライバは、この「受信コールバック +   new Promise 」を、実運用に耐えるよう堅牢に書き上げたもの 普段のアプリ開発で、この低レベルコードを自分で書くことはまずありません。それでも「ライブラリの   await   の裏では   new Promise   とソケットの受信コールバックが動いている」と知っていれば、タイムアウトやコネクション管理でトラブルが起きたとき、 どの層を見ればいいかを切り分けられる ようになります。ライブラリをブラックボックスにしないための、確かな足場になるはずです。 おわりに かなり色々説明をはしょったような気もしますが、おおまかな概念は伝えきれたようにお思います。これでasync/await怖くなくなれば幸いです。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 2人がこの投稿は役に立ったと言っています。 The post 世界一わかりみの深いasync/awaitによる非同期処理 first appeared on SIOS Tech Lab .
  Linux初心者がAzure仮想マシンでHTTPサーバーを構築した話 --> こんにちは!新卒でサイオステクノロジーに入社したごままぐろです。 本記事では、Linux初心者が研修で得た学びを以下の構成でご紹介します。LinuxはITパスポートなどの資格勉強で少し知っている程度だったのですが、この度新卒研修の一環としてLinux仮想環境でのサーバ構築を行いました。 これからLinuxを学ぶ方の参考になれば幸いです。 Linuxについて Linuxとは LinuxはWindowsやMacのようなOS(Operating System)の一種です。OSS(Open Source Software)として公開されているため、世界中の開発者が自由にプログラムの中身を確認したりカスタマイズできます。 厳密にいえば、「Linux」とはOSの核心である「カーネル」のことを指すようです。しかし実際には、カーネルに独自の管理ツールやアプリケーションを組み合わせた「Linuxディストリビューション」のことをLinuxと呼ぶことが多いそうです。 有名なLinuxディストリビューションには以下のものがあります。 RedHat系: RHEL、AlmaLinux など Debian系: Debian、Ubuntu など Linuxの特徴 Linuxには以下のような特徴があります。 基本的に文字ベースで操作する データをディレクトリとファイルの入れ子構造で管理する ユーザごとに権限を設定できる 私の場合、このあたりの特徴を掴むのに時間がかかりました。今も完全に理解できているか分からないのですが、私なりの言葉で簡単に説明したいと思います。 LPI-Japanという団体が初学者向けに 「Linux標準教科書」 という資料を公開しているので、正確な情報を知りたい方はそちらを参照してみてください。 1.基本的に文字ベースで操作する Linuxでは、キーボードで命令(コマンド)を入力するCUI(Character User Interface)での操作が基本です。 Linuxのコマンドは基本的に「コマンド」「オプション」「引数」で構成されます。 同じ操作でも、ディストリビューションによっては別のコマンドやオプションが存在する場合があるため、注意が必要です。 2.データをディレクトリとファイルの入れ子構造で管理する Linuxでは、データはディレクトリとファイルの入れ子構造で管理されます。 ディレクトリはフォルダのようなもので、ファイルを整理するための入れ物です。ディレクトリの中にさらにディレクトリを作ることもでき、これを入れ子構造と呼びます。 ファイルを操作する時には、ファイルの住所(パス)を指定する必要があります。パスには、最上位(/ルートディレクトリ)からの絶対パスと、現在のディレクトリからの相対パスがあります。 絶対パス: /home/username/newdir/file1.txt 相対パス:(現在の位置が/home/usernameの場合) newdir/file1.txt 3.ユーザごとに権限を設定できる Linuxには「ユーザ」という概念があります。ユーザには、人がログインして操作を行うためのものの他に、人がログインできず内部でプログラムを動かすためのものもあります。 Linuxでは、ユーザごとに権限を設定できます。権限には、読み取り(r)、書き込み(w)、実行(x)の3種類があります。 例えば、ファイルに何か書き込みたいと思っても、自身が操作するユーザにその権限がなければ書き込むことはできません。 権限を設定するコマンドもあるため、必要に応じて割り当てたり変更する必要があります。 本章の内容は以上です。 次章では、実際にLinux仮想環境を用意してサーバ構築を行った手順をご紹介します。 Linux仮想環境でのサーバ構築 仮想マシン(AzureVM)の用意 Linuxは、コンピュータ(ハードウェア)の機能を管理しプロセスとの仲立ちを行うカーネルと、カーネルに人間の出した命令(コマンド)を翻訳し伝えるシェルで構成されています。 実際にLinuxを動かして学ぶには、コマンドを出す対象となるコンピュータが必要です。 しかし私が現在使っているコンピュータはWindowsという別のシステムが管理しているため、Linuxのコマンドを書いても通じません。 そのため今回は仮想化されたコンピュータである「Azure VM(Virtual Machines)」というクラウドサービスを利用して仮想環境を構築しました。 仮想マシンOSには、「Red Hat Enterprise Linux (RHEL) 9.4」を選択しました。 Windows環境を維持したままLinuxを実際に操作する方法は他にもいろいろあるので、ご自身のパソコンやその他条件に応じて選択することをおすすめします。 仮想マシンにSSH接続 仮想マシンを作成したら、次はそれに接続する必要があります。 今回はSSHという通信プロトコルを使って接続しました。 SSHは暗号化された通信を行うためのプロトコルで、今回のようにコンピュータが手元にない(リモート)場合でも安全に接続することができます。 SSH接続の手順は以下の通りです。 仮想マシン作成時に生成された秘密鍵を自分のパソコンに保存する ターミナル(コマンドプロンプト)を開き、以下のコマンドを実行して接続する ssh -i 秘密鍵のパス ユーザー名@IPアドレス 初回接続時には、ホストキーの確認が求められるので「yes」と入力 HTTP通信に必要なもののインストールと設定 作成した仮想マシンには最低限の機能しかないため、目的に応じて必要な部品や設定を追加する必要があります。 今回は、Webブラウザにアドレスを入力するとWebページが表示されるようにしたかったので、Apacheというものをインストールしました。 1.Azure Portalで受信ポート規則の変更 認証のため受信ポート22番(SSH)を許可する ソースは安全のため自分のIPアドレスに限定する 2.仮想マシン上でApacheのインストール sudo dnf install httpd 3.Apacheの起動と自動起動設定 ・サービスの起動 sudo systemctl start httpd ・自動起動の有効化 sudo systemctl enable httpd 4.RHELの内部ファイアウォールでHTTPトラフィックの許可 ・HTTPの許可 sudo firewall-cmd --add-service=http --permanent ・設定の反映 sudo firewall-cmd --reload 5.Azure Portalで受信ポート規則の変更 Webページを閲覧するため受信ポート80番(HTTP)を許可する ソースは自分のIPアドレスに限定する 表示するコンテンツの作成 Apacheのデフォルト設定では、「/var/www/html」ディレクトリにHTMLファイルを置くことで、Webページとして表示されるコンテンツを作成できます。 今回は、以下のコマンドで「index.html」を作成しました。 sudo vi /var/www/html/index.html Linuxでファイルを編集するには、viエディタなどのテキストエディタを使用します。 viエディタには実際に中身を編集するインサートモードと、ファイルの保存や終了などの操作を行うコマンドモードがあります。 慣れないうちは操作が難しく、何度かパニックになりました。   Webページの表示確認 Webブラウザに仮想マシンのIPアドレスを入力すると、作成したWebページが表示されます。 Azure Portalの受信ポート規則でソースを自分のIPアドレスに限定しているため、他のIPアドレスからはアクセスできません。 感想とまとめ 以上が今回のサーバ構築で行った内容でした。全くの初心者の私にとっては非常に充実した経験となりました。 実はこの後も様々な学習を続けており、現在は認証認可というもっと高度な設定に挑戦しています。余裕があれば現在取り組んでいる内容もまとめたいと思っているため、また機会があればよろしくお願いします。 長くつたない文章を最後までお読みいただき、ありがとうございました。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 1人がこの投稿は役に立ったと言っています。 The post Linux初心者がAzure仮想マシンでHTTPサーバを構築した話 first appeared on SIOS Tech Lab .
はじめに こちらの記事 で実際にKubernetes環境にKubeBlocksを導入し、DBaaSの基盤を構築しました。 今回は、作成したDBaaS基盤の上に実際にMySQLクラスターを構築していきます。 導入環境構成図 以下の図は、DBaaS基盤上にMySQLを導入する環境の構成図です。 前回の記事 では「KubeBlocksオペレーター」を構築しました。 本記事の対象範囲は、赤丸で囲まれた「DB(MySQL)」です。 今回は例として、プライマリ-レプリカ構成のMySQLをKubeBlocksオペレーターを利用して導入します。 この構成では、データの書き込み・読み込みを行う「プライマリ」と、読み込み専用の「レプリカ」を配置します。 万が一プライマリに障害が発生したとしても、レプリカが自動的にプライマリに昇格して処理を引き継ぐフェイルオーバー機能が働き、高可用性を確保できるのが特徴です。 導入環境構成図 MySQLクラスターの構築方法 KubeBlocksを使用してMySQLクラスターを構築する手順をご紹介します。 前提条件 KubeBlocksが構築済みであること KubeBlocksによってデフォルトでインストールされるMySQLアドオン(以下コマンド結果のmysql 1.0.1)が有効になっていること 以下のkbcliコマンドで有効化されているアドオンを確認することができます。 kbcli addon list # 出力例 NAME VERSION PROVIDER STATUS AUTO-INSTALL qdrant 1.0.1 community Disabled false rabbitmq 1.0.1 community Disabled false apecloud-mysql 1.0.1 community Enabled true etcd 1.0.1 community Enabled true kafka 1.0.1 community Enabled true mongodb 1.0.1 community Enabled true mysql 1.0.1 community Enabled true postgresql 1.0.1 community Enabled true redis 1.0.1 community Enabled true Namespeaceの作成 まずはMySQLクラスターをデプロイするNamespeaceを作成します。 kubectl create namespace demo # 出力例 namespace/demo created rootユーザー認証用Secretの作成 MySQLのrootユーザー用のユーザー名・パスワードを設定したSecretを作成します。 kubectl create secret generic custom-mysql-root-secret \ --from-literal=username='root' \ --from-literal=password='<任意の値>' \ -n demo # 出力例 secret/custom-mysql-root-secret created ※MySQLクラスター作成時に本手順で作成したSecretを指定することで、rootユーザーのパスワードを任意の値で設定することができます。 Secretの指定がない場合は、KubeBlocksがrootユーザー用のパスワードを自動発行します。 MySQLクラスターの作成 KubeBlocksのカスタムリソースである「Cluster」のマニフェストを適用し、レプリカ数2のプライマリ-レプリカ構成のMySQLクラスターを作成します。 cat <<EOF | kubectl apply -f - apiVersion: apps.kubeblocks.io/v1 kind: Cluster metadata: name: mysql-cluster namespace: demo spec: clusterDef: mysql topology: semisync terminationPolicy: Delete componentSpecs: - name: mysql serviceVersion: 8.0.35 disableExporter: false replicas: 2 systemAccounts: - name: root secretRef: name: custom-mysql-root-secret namespace: demo resources: limits: cpu: '0.5' memory: 1Gi requests: cpu: '0.5' memory: 1Gi volumeClaimTemplates: - name: data spec: accessModes: - ReadWriteOnce resources: requests: storage: 20Gi EOF # 出力例 cluster.apps.kubeblocks.io/mycluster created clusterDef: mysql MySQLアドオンが提供するMySQLの構成テンプレートを、作成するDBクラスターのベースとして指定する設定です。 topology: semisync MySQLクラスターをプライマリ-レプリカ構成の準同期レプリケーションモードで起動する設定です。 terminationPolicy: Delete クラスターを削除した際、関連するデータも一緒に削除する設定です。 replicas: 2 MySQLサーバーを2台(プライマリ1台、レプリカ1台)デプロイする設定です。 systemAccounts MySQLのrootユーザーの認証情報(ユーザー名・パスワード)に、事前に作成したSecretを割り当てる設定です。 volumeClaimTemplates DBのデータを保存するためのPVの設定です。 MySQLクラスターの作成確認 MySQLクラスター作成コマンド実行後、以下のコマンドでクラスター・Podのステータスを確認します。ステータスがRunningになっていれば正常に動作しています。 kubectl get cluster mycluster -n demo # 出力例 NAME CLUSTER-DEFINITION TERMINATION-POLICY STATUS AGE mycluster mysql Delete Running 7m30s kubectl get pod -n demo # 出力例 NAME READY STATUS RESTARTS AGE mycluster-mysql-0 4/4 Running 0 8m mycluster-mysql-1 4/4 Running 0 8m 動作確認 接続テスト MySQLクラスターへの接続テストを行います。 まず、作成したMySQLクラスターのエンドポイント(Service名)を確認します。 kubectl get svc -n demo # 出力例 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE mycluster-mysql ClusterIP 10.43.32.224 <none> 3306/TCP 8m kubectl runコマンドでMySQLクライアント用のPodを作成し、コンテナ内でシェルを起動します。 kubectl run mysql-client -n demo --rm -i --tty --image=mysql:8.0 --restart=Never -- sh # 出力例 If you don't see a command prompt, try pressing enter. sh-5.1# mysqlコマンドを使用し、確認したMySQLクラスターのエンドポイントを指定してログインします。パスワードを求められたら設定したパスワードを入力します。 sh-5.1# mysql -h mysql-cluster-mysql.demo.svc.cluster.local -u root -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 246 Server version: 8.0.35 MySQL Community Server - GPL Copyright (c) 2000, 2026, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> mysql>という出力が表示されればMySQLクラスターに接続できています。 フェイルオーバーテスト MySQLクラスターのフェイルオーバー機能を実際にテストしてみます。 まず起動したMySQLのPodのうち、どれが「プライマリ」で、どれが「レプリカ」なのかを確認します。 kbcli cluster describe mycluster -n demo | grep -E "ROLE|primary|secondary" # 出力例 COMPONENT SERVICE-VERSION INSTANCE ROLE STATUS AZ NODE CREATED-TIME mysql 8.0.35 mycluster-mysql-0 primary Running us-east-1a ip-10-1-0-23/10.1.0.23 Jun 25,2026 08:00 UTC+0000 mysql 8.0.35 mycluster-mysql-1 secondary Running us-east-1a ip-10-1-0-24/10.1.0.24 Jun 25,2026 08:01 UTC+0000 各PodのROLEの値を確認すると、mycluster-mysql-0がprimary、mycluster-mysql-1がsecondaryとなっています。 これは、mycluster-mysql-0が「プライマリ」、mycluster-mysql-1が「レプリカ」として構成されていることを表しています。 次にプライマリのPodを削除することで、プライマリに擬似的な障害を発生させます。 kubectl delete pod mycluster-mysql-0 # 出力例 pod "mycluster-mysql-0" deleted プライマリのPodを削除後、レプリカのPodが自動的にプライマリに昇格していることを確認します。 kbcli cluster describe mysql-cluster -n demo | grep -E "ROLE|primary|secondary" # 出力例 COMPONENT SERVICE-VERSION INSTANCE ROLE STATUS AZ NODE CREATED-TIME mysql 8.0.35 mycluster-mysql-0 secondary Running us-east-1a ip-10-1-0-23/10.1.0.23 Jun 25,2026 08:06 UTC+0000 mysql 8.0.35 mycluster-mysql-1 primary Running us-east-1a ip-10-1-0-24/10.1.0.24 Jun 25,2026 08:01 UTC+0000 先ほどレプリカであったmycluster-mysql-1が、プライマリに昇格していることがわかります。 これでMySQLクラスターの構築と動作確認は完了になります。 おわりに 今回は 前回 構築したKubeBlocksを使用し、実際にMySQLクラスターの構築から接続テスト・フェイルオーバのテストを行うまでの流れをご紹介しました。 KubeBlocksを活用すれば、シンプルなマニフェストを1つ適用するだけで、容易に冗長化されたMySQLクラスターを構築できることを体感できたのではないでしょうか。 KubeBlocksの大きな特徴は、様々なデータベースを同じ操作感で統一して管理できる点にあります。 そこで次回は、MySQLとはまた異なる特性を持つNoSQLのインメモリデータベースであるRedisの構築についてご紹介します。 参考文献 https://kubeblocks.io/docs/release-1_0_1/kubeblocks-for-mysql/02-quickstart https://kubeblocks.io/docs/release-1_0_1/kubeblocks-for-mysql/06-custom-secret/01-custom-secret ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post KubeBlocksでMySQLを導入!Kubernetes上でのDB構築を体験 first appeared on SIOS Tech Lab .
プロダクト開発やサービスデザインにおいて、ユーザーの体験を時系列で可視化する「カスタマージャーニーマップ」。ユーザー理解を深め、チームの目線を合わせるための強力なツールですが、作成する中で以下のような壁にぶつかったことはありませんか? ユーザーの行動は並べられたけれど、その裏にある『真の感情』や『潜在的な課題(インサイト)』まで踏み込めていない気がする チーム内だけでブレインストーミングをしても、ありきたりな課題や、自分たちの都合の良い解釈ばかりが出てきてしまう こうした膠着状態を、ChatGPT、Gemini、ClaudeなどのAIチャットが助けてくれるかもしれません。 今回は、チャットボットを「壁打ち相手」として活用し、カスタマージャーニーマップのインサイトを深掘りするための具体的なステップと、プロンプトの例をご紹介します。 AIチャットでの壁打ち:インサイトを深掘りする4つのステップ それでは、実際にAIチャットを使ってジャーニーマップをブラッシュアップしていくプロセスを見ていきましょう。 ステップ1:ペルソナと前提条件の「同期」 AIチャットに壁打ちを依頼する前に、まずはリサーチデータやペルソナの情報をインプットします。ここでの情報の具体性が、その後のアウトプットの質を左右します。 【入力プロンプト例】 「あなたの一流のUXリサーチャーとして、私のカスタマージャーニーマップ作成の壁打ち相手になってください。 まずは前提となるペルソナとプロダクトの情報を共有します。内容を理解したら、質問をせずに「前提を理解しました。いつでもディスカッションを始められます」とだけ返答してください。 ■ プロダクト:20代〜30代向けの、資産運用を自動化するスマホアプリ ■ ペルソナ:都内在住の会社員(28歳)、将来への漠然とした不安はあるが、投資の知識はなく『損をするのが怖い』と感じている。平日は仕事が忙しく、スマホを触るのは通勤中と就寝前のみ。」 ステップ2:行動の理由を分析させる 前提を同期したら、ジャーニーマップの特定のフェーズを切り出してAIチャットにぶつけます。ユーザーが特定の行動をとった時の「言語化されていない動機」を掘り下げます。 【入力プロンプト例】 「ペルソナが『アプリをダウンロードしたものの、初期設定(銀行口座の連携)の画面で離脱した』というフェーズについて考えます。 単に『面倒くさいから』という理由以外で、この時ペルソナの脳内で起きている心理的な葛藤や不安、あるいは『私には関係ないかも』と感じてしまうような感情を、認知心理学やUXの観点から5つ洗い出してください。」 AIチャットは、「セキュリティへの不安」「今すぐやらなければいけない強制感への反発」「投資金額の決定に対するコミットメントへの恐怖」など、行動の裏に潜む心理的背景を論理的に言語化してくれます。 ステップ3:ペルソナになりきってもらい「不満」を吐き出させる 次に、AIチャットにペルソナそのものになってもらい、インタビュー形式で壁打ちを行います。客観的な分析ではなく、「主観的な感情」を引き出すアプローチです。 【入力プロンプト例】 「これからは、先ほど共有したペルソナ(28歳会社員)になりきって回答してください。私はインタビュアーです。 「ねぇ、アプリを開いて最初の画面を見たとき、ぶっちゃけどう思った?何が一番モヤモヤした?』」 AIがペルソナを演じることで、「専門用語が多くて、自分が場違いなところにいる気がした」「いきなりお金の話をされて、心の準備が追いつかなかった」といった、人間のチームだけでは見落としがちな生々しい感情のインサイトが浮き彫りになるかと思います。 ステップ4:「悪いシナリオ」と「理想のシナリオ」のギャップを埋める 最後に、ジャーニーマップにおける「悪い体験」をどうやって「良い体験」に変えられるか、具体的な改善アイデアの種を蒔きます。 【入力プロンプト例】 「ペルソナが最も不安に感じる『口座連携』のステップにおいて、他業界(例えばエンタメ、SNS、ゲームなど)で使われている『ユーザーの心理的ハードルを下げるオンボーディングの手法』を参考に、このアプリに転用できるアイデアを3つ提案してください。」 AIチャットと壁打ちする際の3つの注意点 AIチャットは優秀なパートナーですが、過信は禁物です。 実際のユーザーインタビュー(一次情報)を最優先にする AIチャットが出してくれるインサイトは、あくまで「理論上、そうなる可能性が高い仮説」です。AIとの壁打ちで得られたインサイトは、実際のユーザーテストやインタビューデータと照らし合わせて検証してください。 ハルシネーション(誤情報)を前提に、根拠を問う AIチャットがそれらしい回答をしたときは、「なぜそう思うの?その根拠となるペルソナの行動特性はどこにある?」と追加で質問(深掘り)してください。これにより、AIの適当な思いつきを排除し、筋の通ったインサイトを抽出できます。 機密情報の入力には配慮する AIチャットは入力された情報を学習していくことを前提で利用しましょう。未発表のプロダクト名や、個人情報を入力しないようにしたり、学習させない設定や、Google Workspace上のGeminiを利用するなど、対策をしてください。 まとめ:AIとの協働で可能性を拡げる カスタマージャーニーマップの作成において、AIチャットを壁打ち相手にすると、「チームの視野を広げること」ができるでしょう。 一人で、あるいはチームだけで悩んでジャーニーマップが「綺麗だけど浅いもの」になってしまいそうなときは、AIチャットに「ちょっとこのユーザーの気持ちを想像して」と話しかけてみてください。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 生成AIを「壁打ち相手」にしてカスタマージャーニーマップのインサイトを掘り下げてみる first appeared on SIOS Tech Lab .