Cloudflare Workers は AI 搭載の Slack ボットを簡単に作れていいぞ

こんにちは、JX通信社で CTO をしている小笠原(@yamitzky)です。

JX 通信社の Slack では、プレスリリースへの反響や、弊社の情勢調査事業に関する発信や、ニュースアプリの「NewsDigest(ニュースダイジェスト)」についての言及を監視するチャンネルがあります。皆さんの会社でも、広報部門が自社の反響を見ているのではないでしょうか。

NewsDigestの速報がSNS上のユーザーに言及されている様子

当該 Slack チャンネルでは、かなり単純なキーワードマッチによる定期監視をしていたのですが、

  • 「NewsDigest(ニュースダイジェスト)」は一般名詞としても使われるため、似たサービスや無関係のドメインの記事を誤検知する
  • 記事本文は無関係なのにサイドバーに自社記事が表示されているだけで検知される

といった課題がありました。 1週間分のメッセージを分析したところ、61% が誤検知でした。これだと、読むべき記事が埋もれてしまい、チャンネルが形骸化してしまいます。

そこで Cloudflare Workers を使って、LLM でニュース記事の関連性を判定してから投稿するボット を作りました。「Slack 上で動く LLM を活用したボット」という要件に必要な機能が全部 Cloudflare に揃っていて開発体験が良かったので共有します。

「Slack 上で動く LLM を活用したボット」に必要な要件

今回の要件は「定期的に RSS を監視し、必要なものだけに絞り込み、Slack に投稿する」という非常にシンプルなボットです。しかし、ある程度使い勝手の良い AI エージェントを作ろうと思うと、求められる要件は意外と多いです。

  • 定期実行できる基盤
  • 一度通知した内容を記憶しておくこと
  • 投稿内容が自社に関係するか判断する LLM
  • Slack とやりとりするためのHTTPサーバー
  • 監視設定が、会話を通じてできること/設定が永続化されていること
  • これらが安く、メンテナンスフリーで実現できること

つまり、安価で、サーバーレスで、ストレージと、Cronと、HTTP サーバーと、LLM が必要なわけですが... なんと Cloudflare なら全部揃っています!

全体アーキテクチャ

作ったボットの全体像です。

外部サービスへの依存は RSS と Slack だけです。それ以外は Cloudflare で完結しています。

Cloudflare のここが便利

Workers AI で LLM が binding 一つで使える

Cloudflare Workers AI を使えば、wrangler.toml に 2行書くだけで LLM が使えます。

[ai]
binding = "AI"

あとはコードから env.AI.run() を呼ぶだけです。

const result = await env.AI.run("@cf/meta/llama-3.3-70b-instruct-fp8-fast", {
  messages: [
    { role: "system", content: "あなたはメディアクリッピングの関連性判定アシスタントです。..." },
    { role: "user", content: "以下の記事の関連性を判定してください。..." },
  ],
});

API キーの管理も SDK のインストールも不要です。 wrangler.toml に binding を書くだけで、TypeScript のコードからそのまま呼び出せます。

KV での永続化/キャッシュ

Cloudflare KV は Workers から使えるキーバリューストアです。 用途に応じて3種類のデータを管理しています。

[[kv_namespaces]]
binding = "KV"
id = "xxxxx"
キー 用途 TTL
feeds RSS フィード URL の配列 なし(永続)
rules フィルタリングルールの配列 なし(永続)
seen:{entryId} 既読エントリの重複排除 7日

seen:* キーの設計がポイントです。 エントリ1件ごとに1つのキーを作り、7日間の TTL を設定しています。

const SEEN_TTL = 604800; // 7日

export async function markSeen(kv: KVNamespace, entryId: string): Promise<void> {
  await kv.put(`seen:${entryId}`, "1", { expirationTtl: SEEN_TTL });
}

Cloudflare には D1(SQL)や R2(オブジェクトストレージ)など用途の異なるストレージがいくつかありますが、今回は単純なキーバリュー操作であり、結果整合性でも問題なかったため KV を選びました。

Cron Triggers での定期実行

15分ごとの RSS ポーリングは、Cron Triggers で実現しています。

[triggers]
crons = ["*/15 * * * *"]
export default {
  fetch: handler.fetch,
  async scheduled(_c: ScheduledController, env: Env, ctx: ExecutionContext) {
    ctx.waitUntil(
      getAgentByName(env.ClippingAgent, "clipping").then((a) => a.poll())
    );
  },
};

wrangler.toml に cron 式を書いて、scheduled ハンドラを export するだけです。 外部のスケジューラや GitHub Actions を用意する必要はありません。 HTTP ハンドラ(Slack Events API の受信)と定期実行が同じ Worker の中に同居できるのも Cloudflare Workers の良いところです。

HTMLRewriter で外部ライブラリなしに記事本文を取得

今回のボットは、検知した記事が自社に関係するかどうかを LLM で判断します。しかし RSS にはスニペット(本文冒頭)しか含まれず、判定材料としては足りないケースがあります。

実データの分析で、スニペットにキーワードが含まれないのに実際は関連記事だったケースがいくつか見つかりました。

この対策として、タイトルとスニペットに監視対象語が含まれない記事だけ、本文を取得して判定材料に加えています。 ここで使うのが Workers ランタイムに組み込みの HTMLRewriter です。

const cleaned = await new HTMLRewriter()
  .on("script, style, nav, header, footer, aside", {
    element(el) {
      el.remove();
    },
  })
  .transform(new Response(html))
  .text();

jsdomcheerio といった外部ライブラリを入れなくても、Workers ランタイムの標準機能だけで HTML からテキストを抽出できます。 依存が増えないのでバンドルサイズも小さく保てます。

Agents SDK を使った Slack ボット

Cloudflare には Agents SDK という、Durable Objects ベースのステートフルなエージェントを構築するためのフレームワークがあります。 公式に Slack Agent のサンプルが用意されており、Slack の署名検証やイベントルーティングといった定型処理を SlackAgent 基底クラスとして提供してくれます。

今回のボットでは、この SlackAgent を継承して独自のエージェントを実装しています。

[[durable_objects.bindings]]
name = "ClippingAgent"
class_name = "ClippingAgent"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["ClippingAgent"]
import { SlackAgent } from "./slack-agent"; // 公式サンプルをベースにした基底クラス

export class ClippingAgent extends SlackAgent<Env> {
  // Slack メンションで呼ばれる対話処理
  async onSlackEvent(event) {
    // → LLM の tool calling でフィード管理・ルール管理を実行
  }

  // Cron で呼ばれる定期クリッピング処理
  async poll() {
    // → RSS 取得 → フィルタ → LLM で判定 → Slack 投稿
  }
}

継承元の SlackAgent が署名検証・ACK・イベントルーティングを担い、 ClippingAgent の実装はビジネスロジック(対話管理と定期クリッピング)に集中できます。 Agent は Durable Object として動くため、Worker の fetch / scheduled いずれのハンドラからも getAgentByName() で同一インスタンスにアクセスできます。

Slack メンションによる自然言語オペレーション

今回のボットの利用者は、エンジニア(ボットの開発をする人)だけではありません。むしろ広報部や、マネジメント系の人の方が、よく利用するでしょう。そこで、ボットのチューニングや設定変更はエンジニアだけではなく、他の職種の人でも Slack 上で簡単にできることを目指しました。

@clippingbot example.com は除外して
→ ✅ ドメイン除外ルールを追加しました: example.com

@clippingbot 今のルール教えて
→ 現在のフィルタリングルール:
   1. [exclude_domain] example.com
   2. [exclude_keyword] ほげほげダイジェスト

@clippingbot サイドバーに載ってるだけの記事は無視して
→ ✅ AI判定基準を追加しました: サイドバーに載ってるだけの記事は無視して

裏側では Workers AI の Function calling 機能を使っています。add_feedremove_feedadd_rulelist_rules など、TypeScript で作ったツールの定義を LLM に渡し、ユーザーの自然言語メッセージに対して LLM がどのツールをどんな引数で呼ぶべきかを判断します。

const result = await env.AI.run(MODEL, {
  messages: [
    { role: "system", content: "管理アシスタントとして、適切なツールを呼び出してください。" },
    { role: "user", content: userMessage },
  ],
  tools: [
    { name: "add_feed", description: "RSSフィードを追加する", parameters: { ... } },
    { name: "add_rule", description: "フィルタルールを追加する", parameters: { ... } },
    // ...
  ],
});
// result.tool_calls に LLM が選んだツールと引数が入る

// 選ばれたツールごとに実行される、実際の処理を用意しておく
private async executeTool(name: string, args: Record<string, string>): Promise<string> {
  const kv = this.env.KV;
  switch (name) {
    case "add_feed": {
      const feeds = await getFeeds(kv);
      if (feeds.includes(args.url)) return `${args.url} は既に登録されています。`;
      feeds.push(args.url);
      await setFeeds(kv, feeds);
      return `フィードを追加しました: ${args.url}`;
    }
    case "remove_feed": {
    }
    // ...
  }
}

Workers AI の無料枠

これらの仕組みは Cloudflare の無料プランだけでも運用できます。AI を使っていても、一定ラインまでは無料です。

リソース 無料枠
Workers AI 10,000 Neurons/日
KV 読み取り 100,000回/日
Workers リクエスト 100,000回/日

「Neurons」という見慣れない単位がありますが、 Cloudflare Workers AI のプライシングページに、トークン数と Neurons の関係性が記載されています。ユースケースにもよりますが、1日数百メッセージ程度であれば収まるのではないかと思います。

Cloudflare を使ってみて良かったところ

これらの要素技術自体は、Google Cloud や Amazon Web Services などのパブリッククラウドにも備わっているものが多いです。

Cloudflare の場合、デプロイが圧倒的に速いのが驚きでした。 10 秒くらいで完了します。そのため、作る→試す→作る→試すのトライアンドエラーがとてもやりやすかったです。

そして AI の実行にも無料枠があるので、気軽に AI つき Slack ボットが作れます。

注意点1:LLM の精度と速度

Cloudflare Workers AI にはたくさんのモデルが用意されていますが、精度、速度、コストなどの特性・トレードオフがあります。また「精度」もツール呼び出しの精度(壊れづらさ)と、実施したいタスクの正解率があります。

当初は @cf/meta/llama-3.3-70b-instruct-fp8-fast を使っていましたが、ツール呼び出しの精度が悪く @cf/openai/gpt-oss-120b に移行しました。以下は、ツール呼び出しの精度を簡易的に検証したものです(正確ではない可能性があるので、ご自身での検証をおすすめします!)。

モデル 合格率 中央値レイテンシ 実際の判断
llama-3.3-70b 13% 精度が悪く、本番で使い物にならなかった
gpt-oss-120b 87% 2.2s 採用。致命的失敗モードなし
qwen3-30b 91% 不採用。質問→勝手に変更の致命的な失敗が1回
kimi-k2.6 95% 11.7s 不採用。高価($0.95/$4.00 per 1M)
gemma-4-26b-a4b-it 23/23 (100%) 15.4s(最大125s) 不採用。メンション対話には遅すぎる

注意点2:セキュリティ

このようなボットを作るうえで、一点、注意いただきたいことがあります。それはプロンプトインジェクションの危険性です。RSS など信頼できない情報は、攻撃的なプロンプト指示を混ぜ込まれる危険性があります。

プロンプトインジェクションの可能性と対策

そこで、

  • Slack 上の社員からのメッセージ起点にした自動化 → ある程度信頼できるため、幅広にツールを呼び出せるようにする
  • RSS など信頼できない情報を起点にした自動化 → 信頼できないので、ツールは無効化した状態で LLM を呼び出し、サニタイズする(LLM は true / false を返すだけ)

としています。LLM を使った業務自動化をする際は「信頼できないメッセージ × 権限の強いツール呼び出し」をしないように注意してください。メールやカレンダーを活用した自動化などでも同じ問題があります。

参考記事

Cloudflare の Developer Advocate の yusukebe さんの記事がとても参考になりました。ありがとうございます。

zenn.dev

公式ドキュメントには、上記記事に載ってないようなプラクティスも大量に乗っているのでぜひ参考にしてみてください。

developers.cloudflare.com

まとめ

Cloudflare Workers は、AI ボットに必要な機能が一通り揃ったプラットフォームです。 Workers AI で LLM を呼び、KV でステートを管理し、Cron Triggers で定期実行し、HTMLRewriter で HTML を処理し、Agents SDK で Slack ボットにする。これらが全部簡単に設定できることをご紹介しました。

ちなみに、元々 61% が誤検知だったところ、今回の Slack bot によってほぼ誤検知がなくなりました。もっと早く作っておけばよかったです。

このような社内のちょっとした困りごとを AI で解決したいけれどインフラの構築が面倒、という方はぜひ Cloudflare Workers を試してみてください。

BigQueryの物理バイト課金への切り替えでストレージ料金を削減する

こんにちは、CTOの小笠原(@yamitzky)です。

この記事では、BigQueryのストレージ料金を物理バイトストレージ課金に変更して削減した取り組みについて書きます。

コードの修正もデータの移行も不要で、ALTER SCHEMAを一文流すだけなのですが、BigQueryのストレージ料金の削減率は数十パーセント規模になりました。

BigQueryのストレージ課金とは

JX通信社では社内のデータ基盤・分析を BigQuery をベースに作っています。

社内のサーバー費を分析していたところ、BigQueryのストレージ課金が積み上がっていることに気が付きました。

というのも、JX通信社では、膨大なオープンデータの解析情報や、NewsDigestのユーザー行動ログなどを扱っています。さらに、生データの層(Datalake)、整形して統合した層(DWH)、用途別に集計した層(Datamart)の3層構造になっており、データは冗長に保管されています。

ストレージ課金は、分析等でアクティブに利用されていなくても、ただ大量のデータがBigQuery上に保管されているだけで発生する料金です。あまり使われてないからといって、削除してしまってよいか悩ましいようなデータもあります。

そこで、保管しているデータそのものの課金を減らすために、物理バイトストレージ課金(Physical Bytes Storage Billing、PBSB)への切り替えを順次行いました。BigQueryでなくGoogle Cloud Storageに保管するという手もありますが、それよりもお手軽にできる選択肢です

ストレージ課金の2つのモデル

BigQueryのストレージ課金には、データセット単位で選べる2つのモデルがあります。

モデル 課金対象 デフォルト
論理バイトストレージ課金(Logical Bytes Storage Billing、LBSB) 非圧縮換算のバイト数 デフォルトはこちら
物理バイトストレージ課金(Physical Bytes Storage Billing、PBSB) 圧縮後の実バイト数

物理バイトストレージ課金は2023年7月にGA(一般提供)となった機能で、元々は論理バイトストレージ課金しかありませんでした。

BigQuery のようなカラムナ型(列志向)のデータベースでは、一般的に、列ごとに圧縮された状態で物理保存されています。しかしながら、デフォルトの論理バイトストレージ課金では、圧縮前のサイズに対して課金がされます。コンソールで普段見ている「テーブルサイズ」が圧縮前のサイズです。

それに対して、物理バイトストレージ課金は、圧縮後のサイズに対して課金がなされるような課金体系です。

論理バイトストレージ課金から物理バイトストレージ課金に切り替えても、クエリの性能にもクエリ課金にもデータの中身にも影響しません。保存されているものは何も変わらず、変わるのは請求の計算式だけです。設定変更も ALTER 文だけでできます。

物理バイトストレージ課金は、論理バイトストレージ課金よりも高い

では単純に全部物理バイトストレージ課金にすればお得かというと、そうではありません。物理バイトストレージ課金の単価は、論理バイトストレージ課金よりも高く設定されているからです。

東京リージョン(asia-northeast1)の単価をカタログから引くと、記事執筆時点ではおおよそ次の関係になっています。

  • Active(直近90日以内に変更があったデータ):physical は logical の約2.3倍
  • Long Term(90日間変更がないデータ):physical は logical の約1.6倍

バイトあたりの単価が約1.6倍から2.3倍に上がる代わりに、課金対象のバイト数が圧縮後のサイズになる、というトレードオフです。つまり、圧縮率が、この課金単価の比率を上回れば安くなります。

ログデータ等であれば、基本的には圧縮率が上回ることの方が多いのではと思います。実際、JX通信社でも2〜30倍の圧縮率がありました。

タイムトラベルとフェイルセーフ

圧縮率の他に気をつけるべき点が、BigQuery に備わっているタイムトラベルとフェイルセーフ機能です。過去7日に遡って BigQuery のデータを復元したり、過去のデータに対してクエリをすることができる機能です。

タイムトラベル分のデータ保管料は、物理バイトストレージ課金に対してのみ発生します。追記するだけで書き換えないログ系だと良いのですが、毎日全件を洗い替えするテーブルだと、物理バイトストレージ課金の方が割高になることがあります。

物理バイトストレージ課金で課金額を抑えるなら、タイムトラベルの期間を短くするなどの対策が考えられます。

損益を分ける圧縮率

ストレージ課金設定は、データセット単位で設定できます。

したがって、損益はデータセット単位で、次のように比較することになります。

論理バイトストレージ課金  = (非圧縮バイト数) × logical単価
物理バイトストレージ課金 = (圧縮後バイト数) × physical単価 + タイムトラベル分

全データセットの棚卸し

論理バイトストレージ課金と物理バイトストレージ課金、どちらの設定の方が得なのかを調べるため、Google Cloudから、クエリが提供されています。

TABLE_STORAGE ビュー  |  BigQuery  |  Google Cloud Documentation

とはいえ、当該クエリは全てのプロジェクトでそれぞれ発行しなければなりません。また、月額の削減幅が小さいものはわざわざ変えたくないかと思います。なので、Claude Code などを使って次のように伝えると良いです。((権限の都合でINFORMATION_SCHEMAが見られない場合は、tables.get相当のAPIから物理バイト数や time travel バイト数を集めても同じ集計ができます。))

以下は、BigQueryの論理バイトストレージ課金と物理バイトストレージ課金、どちらの方が得かを集計するためのクエリです。
Google Cloud の全プロジェクトに対して発行し、物理バイトストレージ課金に変えるべきデータセットや、タイムトラベルの日数を減らすことで大きくコスト削減できるデータセットを教えてください。
ただし、実際の月額の削減効果の予測額とセットで教えてください。

DECLARE active_logical_gib_price FLOAT64 DEFAULT 0.02;
DECLARE long_term_logical_gib_price FLOAT64 DEFAULT 0.01;
DECLARE active_physical_gib_price FLOAT64 DEFAULT 0.04;
DECLARE long_term_physical_gib_price FLOAT64 DEFAULT 0.02;

WITH
 storage_sizes AS (
   SELECT
     table_schema AS dataset_name,
     -- Logical
     SUM(IF(deleted=false, active_logical_bytes, 0)) / power(1024, 3) AS active_logical_gib,
     SUM(IF(deleted=false, long_term_logical_bytes, 0)) / power(1024, 3) AS long_term_logical_gib,
     -- Physical
     SUM(active_physical_bytes) / power(1024, 3) AS active_physical_gib,
     SUM(active_physical_bytes - time_travel_physical_bytes) / power(1024, 3) AS active_no_tt_physical_gib,
     SUM(long_term_physical_bytes) / power(1024, 3) AS long_term_physical_gib,
     -- Restorable previously deleted physical
     SUM(time_travel_physical_bytes) / power(1024, 3) AS time_travel_physical_gib,
     SUM(fail_safe_physical_bytes) / power(1024, 3) AS fail_safe_physical_gib,
   FROM
     `region-asia-northeast1`.INFORMATION_SCHEMA.TABLE_STORAGE_BY_PROJECT
   WHERE total_physical_bytes + fail_safe_physical_bytes > 0
     -- Base the forecast on base tables only for highest precision results
     AND table_type  = 'BASE TABLE'
     GROUP BY 1
 )
SELECT
  dataset_name,
  -- Logical
  ROUND(active_logical_gib, 2) AS active_logical_gib,
  ROUND(long_term_logical_gib, 2) AS long_term_logical_gib,
  -- Physical
  ROUND(active_physical_gib, 2) AS active_physical_gib,
  ROUND(long_term_physical_gib, 2) AS long_term_physical_gib,
  ROUND(time_travel_physical_gib, 2) AS time_travel_physical_gib,
  ROUND(fail_safe_physical_gib, 2) AS fail_safe_physical_gib,
  -- Compression ratio
  ROUND(SAFE_DIVIDE(active_logical_gib, active_no_tt_physical_gib), 2) AS active_compression_ratio,
  ROUND(SAFE_DIVIDE(long_term_logical_gib, long_term_physical_gib), 2) AS long_term_compression_ratio,
  -- Forecast costs logical
  ROUND(active_logical_gib * active_logical_gib_price, 2) AS forecast_active_logical_cost,
  ROUND(long_term_logical_gib * long_term_logical_gib_price, 2) AS forecast_long_term_logical_cost,
  -- Forecast costs physical
  ROUND((active_no_tt_physical_gib + time_travel_physical_gib + fail_safe_physical_gib) * active_physical_gib_price, 2) AS forecast_active_physical_cost,
  ROUND(long_term_physical_gib * long_term_physical_gib_price, 2) AS forecast_long_term_physical_cost,
  -- Forecast costs total
  ROUND(((active_logical_gib * active_logical_gib_price) + (long_term_logical_gib * long_term_logical_gib_price)) -
     (((active_no_tt_physical_gib + time_travel_physical_gib + fail_safe_physical_gib) * active_physical_gib_price) + (long_term_physical_gib * long_term_physical_gib_price)), 2) AS forecast_total_cost_difference
FROM
  storage_sizes
ORDER BY
  (forecast_active_logical_cost + forecast_active_physical_cost) DESC;

JX通信社の場合は、追記型のログデータなどは圧縮率が10倍を超えていました。

逆に、日次でスナップショットを取り込み、毎回全件を入れ替える洗い替えテーブルが多いデータセットは、圧縮率が高くなかったです。試算の結果、10倍程度に悪化するものもありました。そのため個別に、タイムトラベルの日数を減らすか、論理バイトストレージ課金のままとするかを判断しました。

切り替えと効果測定

切り替えるべきものが特定できたら、ALTER 文を発行して変更します。

ALTER SCHEMA `your_project.your_dataset`
SET OPTIONS (storage_billing_model = 'PHYSICAL');

書き換えが多いデータセットを physical にするなら、time travel の保持期間を短くしておくと、課金対象を減らせます(最短2日)。

ALTER SCHEMA `your_project.your_dataset`
SET OPTIONS (max_time_travel_hours = 48);

切り替え前の注意点

切り替えには、知っておくべき制約があります。

  • 反映までに24時間かかります。
  • 一度変更すると14日間は再変更できません。試しに切り替えて翌日戻す、ができないので、事前の損益試算が前提になります。
  • Long Term のステータスは切り替えでリセットされません。90日タイマーが巻き戻る心配はありません。

billing export での効果測定

切り替えたら、本当に効いているかを請求データのエクスポート(Google Cloud Billing export)で日次で追いました。私は怖かったので、Claude Code の /cron 機能で日次でコストを集計しながら、徐々に設定をしていました。

SELECT
  DATE(usage_start_time, "Asia/Tokyo") AS day,
  ROUND(SUM(IF(sku.description LIKE "%Logical%", cost, 0)), 2) AS logical_cost,
  ROUND(SUM(IF(sku.description LIKE "%Physical%", cost, 0)), 2) AS physical_cost
FROM `your_billing_export_table`
WHERE service.description = "BigQuery"
  AND sku.description LIKE "%Storage%"
GROUP BY day
ORDER BY day;

切り替えの翌々日あたりから、論理バイトストレージ課金が落ちて、代わりに小さな物理バイトストレージ課金の課金が乗ってくる様子が観測できました*1。 その水準は、事前にやった圧縮率の試算とほぼ一致しました。

まとめ

BigQueryのストレージ課金には論理バイトストレージ課金と物理バイトストレージ課金の2モデルがあり、デフォルトでは論理バイトストレージ課金で課金されています。

圧縮率が単価の倍率(Long Term で約1.6倍、Active で約2.3倍)を超えれば物理バイトストレージ課金の方が得になりますが、タイムトラベルとフェイルセーフ機能も課金対象になってしまうのがトレードオフです。

全データセットを実測してコストが削減できるものだけ ALTER SCHEMA で切り替えれば、コード変更なし、低リスクでストレージ課金を大きく圧縮できます。また、タイムトラベルの日数を減らすことでもコスト削減ができます。

BigQuery のストレージ料金に困っている方は、以上を踏まえて、ぜひコスト削減に取り組んでみてください。

*1:billing export は1〜2日遅れて反映されるので、最新日が安く見えるのは取り込み途中であることが多いです。完全に埋まった日で判断します

カスタム Gem × Google Drive で実現!セキュリティチェックシート回答の半自動化

こんにちは、CTOの小笠原(@yamitzky)です。

今回は、Gemini Workspace の「カスタム Gem」を使って、セキュリティチェックシートへの回答を”半”自動化した取り組みについて紹介します。社内ドキュメントを RAG 的に活用するちょっとしたテクニックも含まれているので、同じような業務課題を抱えている方の参考になれば幸いです。

背景:セキュリティチェックシートの悩み

JX通信社では「FASTALERT」というBtoB SaaSを提供しています。企業様に導入いただくにあたって、セキュリティチェックシートへの回答を求められることがあります。セキュリティチェックシートに回答する際の課題としては、以下のようなものがありました。

  • 質問項目が多く、時間がかかる(数十〜百問以上のことも)
  • 企業ごとにフォーマットが異なる
  • 一方で、どの企業でも回答する内容は似ている
  • 過去に何度も同じような回答をしている

セキュリティチェックシートへの回答を簡略化するためのSaaSなども存在しますが、まずは無料でできる範囲を検討することになりました。

解決策:Gemini Workspace のカスタム Gem

この問題を解決するために、Gemini Workspace の「カスタム Gem」を活用することにしました。

カスタム Gem とは

Gemini Workspace には、自分専用の AI アシスタント「Gem」を作成できる機能があります。ChatGPTでいうGPTsです。Gem には事前のプロンプトや解答ルールなどを設定することができますが、それだけでなく、Google Drive 上のファイルを参照する こともできます。この作成したカスタム Gem は、社内で任意のグループに共有することもできます。

つまり、社内ドキュメントを「知識」として持った、社内専用アシスタントを作れるわけです。これを使えば、過去の回答実績や社内規約を踏まえた回答を生成してもらえます。

今回作成したカスタム Gem では、以下のように設定してみました。

参照させるファイル

今回のカスタム Gem には、以下のドキュメントを参照させました。

  1. 社内のセキュリティ規約ドキュメント - 情報セキュリティポリシーなど
  2. 模範解答 doc - 過去のチェックシート回答をまとめたもの

特に重要なのが「模範解答 doc」で、今回の取り組みのうち大部分が、このドキュメントを作る作業でした。とはいえ、この作業も Gemini を使ってかなり楽をしています。

参照ドキュメントの準備

模範解答 doc の作り方

FASTALERT」は多くの企業に導入いただいており、その過程でたくさんのセキュリティチェックシートに回答してきています。しかしながら、過去の回答実績は Excel や PDF など、さまざまな形式で保存されていました。中には、Excel マクロを活用したもの、セレクトボックスがあるものなどもあります。これをそのまま Gem に読ませてもうまく参照できない(誤った回答をすることがある)ので、 Q&A 形式のプレーンテキストに変換することにしました。

変換には、以下のようなプロンプトで Gemini に指示しました。

このドキュメントを、1問1答形式で、内容を欠けることなく、Google Docs 化してください。
書き方は必ず「質問 → 答え」となるようにしてください。

例えば、元の Excel データがこのような感じだったとします。

質問項目 回答 備考
データの暗号化は実施していますか はい。保存時はAES-256、通信時はTLS1.3を使用

これが、以下のような形式に変換されます。

Q: データの暗号化は実施していますか
A: はい。保存時はAES-256、通信時はTLS1.3を使用しています。

このように1問1答形式にすることで、Gem が質問と回答の対応関係を正しく理解できるようになります。

なぜプレーンテキストに変換するのか

Excel や PDF のままだと、表の構造やレイアウト情報がノイズになってしまいます。シンプルな Q&A 形式にすることで、Gem が「この質問にはこの回答」というマッピングをしやすくなります。RAG(Retrieval-Augmented Generation)でいうところの、検索しやすいチャンク分割をしているイメージです。

以前は元の回答ファイル(ExcelやPDF自体)を NotebookLM に参照させていましたが、誤った参照をしたり、すでに内容が変わっている古い回答を参照してしまうことがありました。回答案マスタを作ることで、精度を改善することができました。

実際の使い方と効果

Gem への質問例

カスタム Gem を作成したら、あとはチェックシートの質問を投げるだけです。

以下のセキュリティチェックシートの質問に回答してください。
過去の回答実績と社内規約に基づいて、適切な回答を1行で作成してください。

Q1. サーバーはどこの国に構築されていますか?

出力イメージ

Gem は参照ドキュメントを踏まえて、以下のような回答を生成してくれます。

A1. サーバーは日本国内(Amazon Web Services および Google Cloud の東京リージョン)に構築されています 。

過去の回答と整合性のとれた内容が出てくるので、あとは必要に応じて微調整するだけです。

実際の動作確認

以下は、架空のセキュリティチェックシートのExcel ファイルを渡した例です。

Excel ファイル自体の読み書きは残念ながらできませんが、Google スプレッドシートへのエクスポートが可能なので、短い作業時間で回答できます。

やらなかったこと

Gem の社内配布

カスタム Gem 自体は特定のユーザー、グループに共有することができますが、今回は行っていません。あくまで専門家(情シス部門)が回答の正しさを保証する必要があるためです。

完全自動化

今回、元の Excel ファイルへの書き込みは目指していません。これは、

  • Excel に直接書き込みができる AI サービスは限られているが、それよりも RAG の精度を重視していた
  • 企業から受領する Excel ファイルの中には複雑な構造のもの、マクロを使ったものなどがあり、難易度が高いと想定した

ことなどが理由です。

今後の展望

  • 新しいチェックシートに回答するたびに模範解答 doc を更新し、知識を蓄積していく
  • 他の定型業務(FAQ対応など)にも同じアプローチを適用したり、社内での活用を増やしていく
  • ちょうど2026年1月12日に発表された Claude Cowork なども気になっており、こういったツールとの組み合わせも試してみたい

まとめ

Gemini Workspace のカスタム Gem を使って、セキュリティチェックシートへの回答を”半”自動化しました。ポイントは、過去の回答実績を Q&A 形式のプレーンテキストに変換して参照させること。これにより、社内ドキュメントを RAG 的に活用した専用アシスタントを簡単に作ることができます。

似たような定型業務に悩んでいる方は、ぜひ試してみてください。

社内ナレッジを Claude Code の Marketplace として限定配布する

こんにちは。JX通信社でCTOをしている小笠原(@yamitzky)です。

今回は、Claude Code の Marketplace や Agent Skill の社内活用の取り組みを紹介します。

課題

実は JX通信社では元々、ソースコード管理には GitLab を利用しており、GitLab CI で各種 CI/CD (lint, test, deploy など)を組んでいました。直近では、GitHub と GitHub Actions へ移行するという取り組みをしています。

ソースコードの移行だけであれば簡単な単純作業だけで終わりますが、GitLab CI から GitHub Actions への移行には時間がかかります。ただ YAML の書き方の変更するだけではなく次のような点を考慮しながら移行する必要がありました。

  • 環境ごとのデプロイフローの整備
  • プライベートリポジトリの依存関係の扱い
  • Google Cloud へデプロイする際は、OIDC 関連の設定の変更
  • AWS へデプロイする際は、OIDC 関連の設定の変更
  • その他、GitLab CI にはあるが、GitHub Actions にはない設定の移行や廃止
  • 実際に lint, test, deploy が動くかの確認

これを、各リポジトリごとに設定しなければなりません。LLM によってだいぶ楽はできますが、これを毎回プロンプトを考えるのでは大変です。

そこで今回、今後のことも考え、このような 社内のナレッジを Claude Code のプライベート Marketplace として整備 することにしました。

Claude Code の Plugin と Marketplace とは

Claude Code には、機能を拡張するための Plugin という仕組みがあります。Plugin は以下のようなコンポーネントを含むことができます:

コンポーネント 説明
Agent Skills Claude の能力を拡張するナレッジモジュール。タスクに応じて自動適用される
Slach Commands /command で呼び出せるカスタムコマンド
Sub Agent 特定のタスクを処理する専用エージェント
Hooks イベントに応じて実行されるスクリプト
MCP .mcp.json による MCP の設定

そしてこれらの Plugin を配布・管理するための仕組みとして、Plugin Marketplace というものがあります。Marketplace といっても、アプリのStore のように中央集権的・事前審査制のものではなく、分散型の Marketplace なので、誰でも作ることができます。AnthropicもGitHub上に Marketplace を公開しています。

Anthropic の例のように GitHub の公開リポジトリを Marketplace にするだけでなく、プライベートリポジトリでも Marketplace を作成できる ので、社内限定配布も簡単です。

/plugin marketplace add your-org/claude-plugins

このコマンドで、GitHub のプライベートリポジトリを Marketplace として追加できます。

参考:

今回作った構成

社内の Marketplace は、以下のような構成にしました:

jxpress-claude-code-marketplace/
├── .claude-plugin/
│   └── marketplace.json        # マーケットプレイス定義
└── plugins/
    ├── common/                 # 全チーム共通プラグイン
    │   ├── .claude-plugin/
    │   │   └── plugin.json
    │   └── skills/
    │       └── gitlab-to-github-actions/
    │           ├── SKILL.md
    │           └── references/
    ├── fastalert/              # FASTALERT チーム専用
    │   └── skills/
    ├── team-a/              # チームA専用
    │   └── skills/
    └── team-b/               # チームB専用
        └── skills/

今後も同様の構成とするかはわかりませんが、現在は全社共通プラグイン+チームごとプラグインという形でリポジトリ(Marketplace)を構成しています。

マーケットプレイスの定義はシンプルな JSON ファイルです:

{
  "name": "jxpress-claude-code-marketplace",
  "owner": {
    "name": "JX Press Corporation",
    "email": "(略)"
  },
  "plugins": [
    {
      "name": "jxpress-common",
      "source": "./plugins/common",
      "description": "JXPress 全チーム共通のスキル・ツール集"
    },
    {
      "name": "jxpress-fastalert",
      "source": "./plugins/fastalert",
      "description": "FASTALERT チーム専用のスキル・ツール集"
    },
    {
      "name": "jxpress-team-a",
      "source": "./plugins/team-a",
      "description": "ほかチーム専用のスキル・ツール集"
    },
    ...
  ]
}

Agent Skill の整備

Plugin は Commands や Agents など複数のコンポーネントに対応していますが、今回は Agent Skill で整備しました。今回の用途(GitLab CI を GitHub Actions に移行したい)であれば、コマンドでもよかったと思います。

今回の Skill を用意したことで、例えば「GitLab CI を GitHub Actions に移行して」と依頼すると、Claude が自動的に gitlab-to-github-actions スキルを参照し、社内で決めたベストプラクティスに沿った形でワークフローを生成してくれます。OIDC の設定、プライベートリポジトリの依存関係、ブランチングルールなど、毎回説明していた内容が、社内ルールやリポジトリの設定に則って自動的に反映されるわけです。

実際に作ったスキルの一部を紹介します:

---
name: gitlab-to-github-actions
description: GitLab CI を GitHub Actions に移行する。.gitlab-ci.yml を読み取り、GitHub Actions のワークフローファイルを生成する。CI/CD移行、GitHub Actions設定、ワークフロー作成時に使用。
---

# GitLab CI → GitHub Actions 移行

## 移行手順

1. `.gitlab-ci.yml` を読み取る
2. リポジトリ構造を確認(Dockerfile、docker-compose、pyproject.toml など)
3. ワークフローファイルを生成(test.yml と deploy.yml を分離)
4. 必要に応じて environment を作成
5. GitLab 依存を GitHub に置換
6. `.gitlab-ci.yml``.gitlab/ci` を削除する
7. PR作成と動作検証(オプション、ユーザーに確認してから実施)

(略)

## 詳細設定

- **OIDC 設定(AWS/GCP)**: [references/oidc.md](references/oidc.md)
- **プライベートリポジトリ依存**: [references/private-repos.md](references/private-repos.md)

スキル自体は、Anthropicの「skill-creator」のスキル を使って簡易的にスキルを作成しました。より良いスキルを作りたい場合は、Anthropic謹製のスキル作成のベストプラクティス もあるので、こちらを参考にして改善すると良いと思います。

上記の SKILL.md には、2つテクニックがあるので紹介します。

すべてを SKILL.md に書かない

oidc の設定やプライベートリポジトリへの依存などは、すべてのリポジトリの移行に必要なわけではありません。そのような「特定の条件のときだけ必要なもの」は、SKILL.md に参照だけ書いておき、ドキュメントを分割しています。すると、必要なときだけ Claude Code が読み取るので、トークン数の節約につながります。

Claude Code 自身に検証させる

CI の設定が実際に正しい(動くものになっている)のか、人間が確認→修正していては時間の無駄です。「実際に Pull Request を作って CI を発火させる」「実際に開発環境にデプロイする」といった手順を Claude Code に実行させることで、問題があれば Claude Code が GitHub Actions の YAML などを自動的に直してくれます。

このように、成果物の正しさを Claude Code 自身に検証させると、品質の高いアウトプットをしてくれます。

セットアップ

利用者側のセットアップも簡単です:

# マーケットプレイスを追加
/plugin marketplace add org-name/marketplace-repository-name

# プラグインをインストール
/plugin install plugin-name@marketplace-repository-name

さらに、各プロジェクトの .claude/settings.json に設定を追加しておけば、チームメンバーがリポジトリを開いた際に自動的にプラグインを有効化することもできます。

{
  "extraKnownMarketplaces": {
    "jxpress-claude-code-marketplace": {
      "source": {
        "source": "github",
        "repo": "jxpress/claude-code-marketplace"
      }
    }
  },
  "enabledPlugins": {
    "jxpress-common@jxpress-claude-code-marketplace": true
  }
}

他コーディングエージェントへの展開

JX通信社では、Claude Code だけでなく、Cursor、GitHub Copilot など、各メンバーが好きなコーディングエージェントを使っています。

今回紹介したようなスキル(Agent Skill) はopen standardとして標準化されており、一部のコーディングエージェントの対応が進んでいます。12月20日現在、VSCodeや、CodexCursor(Nightly)などでも利用できます。ちなみにGeminiは非対応ですが、GitHub Issueでは「検討する」 という趣旨のコメントが書かれていました。

agentskills.io

Agent Skill が標準化された一方で、今回の Plugin Marketplace の仕組みは Claude Code のための仕組みです。Cursor でも Claude Plugin への対応を目指しているような表示がUIにはありましたが、私の環境では、今回社内配布した Plugin のスキルを取り込むことはできませんでした。

手間になってしまいますが、12月20時点では(シンボリックリンクではなく)複製する必要がありました。

cp -r ~/.claude/plugins/marketplaces/{marketplace}/path/to/skill/dir ~/.codex/skills/path/to/skill/dir

このあたりのポータビリティは引き続き課題です。SkillPortのように、サードパーティなOSSで解決しようという動きもあるみたいです。

まとめ

社内のベストプラクティスやナレッジを Claude Code の Marketplace として整備することで、以下のようなメリットがありました。

  • 複雑な作業に対して、毎回のプロンプト入力が不要になった
  • ナレッジを一元管理する方法を社内標準化できた

今回の例は「GitLab CI → GitHub Actions への移行」というちょっとニッチな例ですが、「GitHub Actions の整備」というスキルや「社内共通のブランチルール」のようなスキルでも、十分有用なのではないかと思います。また、本稿では触れませんでしたが「社内エンジニアブログのレビュー」のようなスキルも早速追加し、この記事をセルフレビューしています。

プライベートリポジトリで簡単に Marketplace を作れるので、ナレッジの社内配布、ぜひ試してみてください。

Claude Code GitHub ActionsとTerraformの組み合わせはいいぞ

こんにちは、JX通信社のCTOの小笠原(@yamitzky)です。

この記事では、TerraformとClaude Code GitHub Actionsを活用した、権限管理の効率化の取り組みについてご紹介します。

権限管理の課題と Terraform による IaC 化

JX通信社では、Google CloudやAWS、GitHub、Cloudflareといった複数のクラウドサービスを利用しています。もともとはすべて手作業で権限管理を行っていましたが、以下のような課題がありました。

  • 管理の属人化: 特定の管理者にしか設定が分からず、作業が集中してしまう。
  • 変更履歴の不透明性: 「いつ」「誰が」「なぜ」権限を変更したのか追跡するのが難しい。
  • 複雑な設定の手間: Workload Identity Federationの設定など、複雑な設定が必要な場合がある。

これらの課題を解決するため、権限設定を Terraform を用いてコード化(IaC: Infrastructure as Code)し、GitHub で一元管理できるよう、徐々に移行を進めています。

例えば、以下のようにTerraformのコードを書くだけで、GitHubにメンバーを招待できます。

resource "github_membership" "yamitzky" {
  username = "yamitzky"
  role     = "member"
}

※実際にはmodule化をすることで、より簡単・汎用的にしています。

現在は、下記の設定などをTerraformで管理し、Pull Requestベースで運用しています。

  • GitHubのアカウントやリポジトリ
  • Google Cloudのプロジェクトやロール
  • Cloudflareのアカウントやロール
  • Workload Identity設定(GitHub Actionsから各種クラウドに対しての認証)

Terraform による権限管理の問題

Terraformによる権限管理はよく行われていますが、一つ、大きな問題があります。

ずばり、Terraformの構成を書くのが難しくて面倒くさい! ということです。言い換えると「学習コストが高い」ということです。

JX通信社には普段Terraformを書いていないメンバーもいるため、「権限を追加してほしい場合は、Terraformのコードを書いてください」というルールにするのはややハードルが高いです。管理者が代理でTerraformを書く形だと、「管理者への作業集中」という課題は解決できません。

Claude Codeによる自動化で、誰でも権限申請

この「Terraformを書くのが大変」という問題を解決するために、Claude Code GitHub Actionsを導入し、AIによるTerraformコードの自動生成・提案の仕組みを構築しました。

Claude Code GitHub Actions は、Claude CodeによるAIコーディングの仕組みをGitHub Actions上で実行できるものです。

公式ドキュメント紹介記事がたくさんあるので説明を省略します。

申請から適用までの流れ

この仕組みの具体的な流れは以下の通りです。

  1. Issue の作成: 権限を申請したい人は、用途別に用意された Issue テンプレートを使って Issue を作成します。
  2. AIによるコード生成: Issue が作成されると、GitHub Actionsが実行され、AI(Claude Code)がIssueの内容を解釈し、権限設定用のTerraformコードを自動で生成・修正します。
  3. Pull Request の作成: Claude Codeが生成したコードを元に、Pull Requestを作成します。
  4. terraform planの実行: GitHub Action上で、terraform fmt, validate, planを実行し、結果をPull Requestにコメントします。
  5. レビューとマージ: 申請内容と生成されたコード、terraform planの内容をレビューし、問題がなければ管理者がマージします。マージされると、本番環境に権限設定が適用されます。

この仕組みにより、Terraform の知識がないメンバーでも、Issueを作成するだけで、セルフサービスで権限申請を行えるようになりました。

具体例:GitHubメンバーの招待

例えば、新しいメンバーをGitHubに招待したい場合、申請者は以下のようなIssueを作成します。

---
name: GitHubメンバー招待・管理
about: GitHub組織へのメンバー招待や権限変更の依頼
title: '[GitHub Member] '
labels: ['github', 'member']
assignees: []

---

## 依頼内容
- [ ] 新しいメンバーの招待
- [ ] 既存メンバーの権限変更
- [ ] メンバーの削除

## メンバー情報

### GitHubユーザー名
<!-- 例: example-user -->

### メールアドレス
<!-- 例: example@jxpress.net -->

### 権限レベル
- [ ] admin (管理者)
- [x] member (一般メンバー)

### 所属チーム(分かる場合)


---

@claude 上記の内容でGitHubメンバーをお願いします。

ポイントは、最後に @claude とメンションしている点です。claude へのメンションをあらかじめテンプレートに入れておくことで、申請者はユーザー名などを書いて投稿するだけで、権限申請ができるようになります。また、事前設定しておきたい原則ルール(例:adminではなくmember権限を原則とするなど)については予めテンプレートに記入しています。

GitHub Actions の設定

基本的には公式サンプルを踏襲していますが、2点、工夫している箇所があります。

JX通信社では、主にGoogle Cloudを利用しているため、Claude Code GitHub Actions の設定はGoogle CloudのWorkload Identity Federationを利用し、Vertex AI経由で実行しています。Claude CodeはAWSやGoogle Cloudでも使えるため、(SaaSの)Claude自体の利用が始まっていないような会社でも、支払いや許可などの点で利用しやすいのではないかと思います。

また、Claude Codeの実行は数分かかり、GitHub Actionsの費用を浪費してしまいます。そこで、余っているPCで組んだKubernetesクラスター上にActions Runner Controllerを導入し、GitHub Actionsを実行することで、費用を抑えています。

name: Claude Code Action

permissions:
  contents: write
  pull-requests: write
  issues: write
  id-token: write  

on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
  issues:
    types: [opened, assigned]
  pull_request_review:
    types: [submitted]

jobs:
  claude-code-action:
    if: |
      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
      (github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
    runs-on: kubernetes
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - name: Generate GitHub App token
        id: app-token
        uses: actions/create-github-app-token@v2
        with:
          app-id: ${{ secrets.CLAUDE_CODE_APP_ID }}
          private-key: ${{ secrets.CLAUDE_CODE_APP_PRIVATE_KEY }}

      - name: Authenticate to Google Cloud
        id: auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: '(略)'
          service_account: '(略)'

      - name: Run Claude PR Action
        uses: anthropics/claude-code-action@beta
        with:
          github_token: ${{ steps.app-token.outputs.token }}
          use_vertex: "true"
          model: 'claude-sonnet-4@20250514'
        env:
          ANTHROPIC_VERTEX_PROJECT_ID: ${{ steps.auth.outputs.project_id }}
          CLOUD_ML_REGION: us-east5

今後は、CI失敗時のレビューや、terraform fmtなど自動でのツール実行なども自動化させていきたいです。

CLAUDE.mdの設定

CLAUDE.mdには以下の内容だけを設定しています。*1

このリポジトリは Terraform で作られています。Terraform を使って、Google Cloud や AWS などのリソースを管理することを目的としています。
モジュール定義の書き方は @README.md を参照してください。
OIDC 設定を依頼された際は、サンプルとなる GitHub Actions の設定も教えて下さい。
日本語で書いてください。

その代わりに、READMEには各種権限管理モジュールの説明やサンプルなどを充実させ、人間にもAIにも優しいドキュメントを充実させています。

まとめ

本記事では、TerraformとClaude Code GitHub Actionsを組み合わせ、権限管理を効率化・自動化した取り組みについてご紹介しました。

Terraformでの権限管理自体はやっている会社も多いと思いますが、AIコーディングと組み合わせることで、学習コストの壁を取り払うことができました。ぜひ参考にしてください!

*1:意味のある設定になっているか検証をしていないので、誤り等あればご指摘ください