社内ナレッジを 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 -->

### メールアドレス
<!-- 例: [email protected] -->

### 権限レベル
- [ ] 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:意味のある設定になっているか検証をしていないので、誤り等あればご指摘ください

属人化を防ぎ自己管理型になるためにスクラムチームが始めた取り組み

お久しぶりです。スクラムマスターの@sakebookです。
Switch 2が当選してなくて残念ですが、MtGのFFコラボがあるので楽しめてます。

自己管理されたスクラムチームであっても、無意識のうちに得意な人が得意なタスクを手に取り、結果として知識が偏ってしまう…。そんな経験はないでしょうか。それはチームのパフォーマンスを一時的に高めますが、同時に「〇〇さんがボトルネックになる」という将来のリスクも育てています。この『短期的なスピード』と『長期的なチーム力』のジレンマは、チームの成熟度が問われる課題です。

私達のチームでは、属人化を防ぐためにいくつかの取り組みを始めて、うまくいっています。今では大体のことが、チームの誰がやることになっても「わからない」とかはない状態になっています。

前提

私達はPO、SM、エンジニア3名の、合計5人のチームです。なので、エンジニア3人の中で属人化を防げているという話です。
メンバーの減少があって3人ですが、4-5人のときにも属人化を減らせていたと思います。
スプリントは1週間で、チームはフルリモートの環境です。

取り組み

「計画者」と「実装者」をあえて分ける

スプリントプランニングのトピック3「選択した作業をどのように成し遂げるのか?」を、私達はエンジニアで詳細見積もりという枠で行っています。

そこでは、

  • スプリントに積んだプロダクトバックログアイテムに仮に個人をランダムにアサインし、どのように着手して進めるのか各自作業計画を立てる
    • 作業する単位で子課題を作成する
  • その後、各自作業計画を発表して、疑問点や考慮漏れなどを皆で洗い出す
  • そして最後に仮のアサインを外し、スプリント中の実装担当者をアサインする
    • すべてのアサインを決めることはせず、デイリースクラムで調整する

という流れで作業計画を立てています。この作業計画では、コードのこの関数を修正するという話まで行います。

この詳細見積もりを行うことで、

  • 皆が着手する課題に対して同じ理解で着手できる
  • 事前に問題点に気づきやすくなる
    • この作業は他のリリースに影響するので別途ブランチを切ったほうがいい
    • 不確実性が高いのでペアで進めたほうがいい
    • 依存関係があるのでxxまでに終わってないとスプリントゴールに影響しそう
    • などなど

という効果がありました。
私達のチームでは木曜をスプリント始まりにしているのですが、木曜はほぼスクラムイベントで埋まります。この詳細見積もりは2時間枠を取っています。途中休憩を挟みつつ、短く終わるときもあれば長くなるときもあります。

スプリントをまたいだタスクは別メンバーが引き継ぐ

こちらも詳細見積もりでの取り組みです。
前回のスプリントの残りがあったときには、着手していた人に作業計画を共有してもらいます。
アサインを検討するときには、着手していた人以外が行うようにしています。

これは

  • 解決しなかった問題が解決する場合もあるし、複数の視点で見れる
  • 一人が詰まっててスプリントをまたいでしまった可能性もあるので、それを次回にも起こらないように防ぐ

という効果があります。 残りがあったときは、スプリント中におかわりして追加したものであることが多いので、バトンタッチすることで属人化を防ぐ役割もあります。

「自分が作っていない機能」をレビューで発表する

スプリントレビューのための準備の時間をチームで確保するようになりました。スプリントレビュー準備という枠です。
そこでは、スプリントゴールと、スプリントプランニングのときに話したスプリントレビュー予定の内容を照らし合わせてどんなインクリメントを提示できるかを話します。
共有する内容の大筋と発表者を決めるのですが、その内容にあまり関わっていなかった人に発表してもらうようにしています。

こうすることで

  • スプリント内で何に取り組んでいたのかの理解を底上げする
  • 作ったけどなんの課題解決につながってるのかわからないというのを防ぐ
  • 皆でやってる意識が持てる
  • ドメイン知識も身についていく

という効果があります。
スプリントレビュー準備では、プロダクトゴールに近づくために必要なことについても考え、スプリントレビューでPOと話す内容として整理します。

AIへの指示(プロンプト)とその結果の要約を残す

JXではいくつものAI Coding AgentやAI Editorを活用していますが、その効果的な使い方は個人の経験に依存しがちです。これも一種の属人化と言えます。そこで、性能や活用具合による差を少なくし、チーム全体の生産性を底上げするためにこの取り組みを始めました。
知見共有も兼ねて、AIに指示させたものはサマリーを作成して特定のディレクトリに保存するというのを試験的に行っています。
AIが、過去の経緯などを調べるためにそのディレクトリを活用するケースもあって、指示の安定化と性能の底上げに貢献しています。

サマリー作成に使うテンプレートの一部

過去のサマリー

取り組みの始まり方

これらの取り組みははじめからあったわけでもなく、同時に始まったものでもありませんでした。
スプリントを繰り返し、スプリントレトロスペクティブで振り返る中で生まれました。
私達のチームでは、スプリントレトロスペクティブの手法を固定せず、複数利用しています。
手法によって話の出やすさや話の切り口は異なります。改善が滞っているチームではいつもと異なる手法を採用してみるというのは効果的かもしれません。
今回紹介した取り組みも、自分たちにも合いそうなものがあったなら改善の一つとしてTRYしてみてください。

Datadogとクラウドの費用を見直し、60%以上費用を削減した話

CTOの小笠原(@yamitzky)です。今日は、JX通信社でも利用しているモニタリングの SaaS 「Datadog」の利用費用を削減した話を書きます。

JX通信社とDatadog

Datadog は「クラウド アプリケーションのための モニタリングとセキュリティ プラットフォーム」です(公式サイトより)。特に主要な使い方としては、サーバーの各種モニタリングや、そのアラート検知などのために利用されることが多いと思います。

JX通信社では、FASTALERTなどのシステム構築のためにGoogle CloudやAmazon Web Servicesなど複数のクラウドサービスを利用していますが、それらを統合的に監視するためにもDatadogを活用しています。細かい利用用途としては以下の通りですが、「AWSとGoogle Cloudを横断でサービス監視する」という用途がメインです。

  • Google Cloud、AWSのマネージドサービス(例:ロードバランサー)の指標の監視
  • 一部、VPS で構築されているシステムの監視
  • Synthetic モニターによる、API の外形監視
  • 朝会で確認するためのダッシュボード

Datadogには他にもたくさんの便利な機能がありますが、弊社の場合は、CloudWatch (Amazon)やCloud Monitoring (Google Cloud)自体での監視も併用しているため、活用の用途は限られていました。

昨今の円安による為替レートの悪化や、Datadogを活用しているシステム数の増加などにより、社内のDatadog利用料金が増大しており、最適化を行う必要がありました。

かかっている費用を“正確に”把握する

Datadog 利用にかかる費用は「Datadogの請求書に記載された費用」だけではありません。 「Datadogを利用するためにGoogle Cloud や AWSからデータを取り込む費用」もかかっており、これらの費用も合わせて最適化を行うことが重要です。

JX通信社の場合、Amazon CloudWatchの指標を取得する費用(CW:GMD-Metrics)や、Google Cloudの指標を取得する費用(Monitoring API Requests)が、Datadogと同じくらいかかっていました。これらも、Datadogが指標を取り込むためにかかっている費用です。

Datadogの管理画面から見れる「Cost Summary」の数字と、AWS、Google Cloudの費用を合わせて分析した結果、次のような比率であることがわかりました。

どこでかかる費用か 名称 比率 補足
Datadog Serverless Invocations 16% Cloud Runなどのサーバーレスなサービスの実行回数に応じた従量課金
Serverless App Instances 14% Cloud Runなどのサーバーレスなサービスの数に応じた従量課金
Synthetics API Test Runs 11% Syntheticsテストの実行回数の従量課金
(その他) 6%
Google Cloud Monitoring API Requests 24% API呼び出し数の従量課金 ≒ プロジェクトが多いほど費用がかかる
AWS CW:GMD-Metrics 29% API呼び出しによる取得指標数の従量課金 ≒ 指標が多いほど費用がかかる

つまり、主要な費用は、

  • Datadogのサーバーレス系の従量課金 (30%)
  • Amazon CloudWatchからの指標の取り込み (29%)
  • Google Cloud Monitoringからの指標の取り込み (24%)
  • Syntheticsテストの従量課金 (11%)

に占められていたということになります。これはあくまで JX通信社のケースですので、会社によって全く異なると思います。特に、JX通信社はサーバーレスを活用したシステム構築が多いため、Infra Hosts=ホスト自体の監視にほぼ費用がかかっていません。

実施した施策

分析の結果、何に費用がかかっているかが判明したので、地道に削っていきます。

サーバーレス系の従量課金の削減

Datadogはサーバーレスなシステムの監視を行う機能があり、そのシステム数や呼び出し数に応じて費用がかかります。

docs.datadoghq.com

説明には「Datadog内で追跡・監視されている呼び出しとアクティブな Lambda 関数の組み合わせ」とありますが、Cloud Runなども同様に費用がかかります。ただし、Lambdaと同じく「アクティブ」かつ「Datadog内で追跡・監視されている」システムのみが対象となります。そのため、(全てのシステムを取り込まず) 本当に必要なシステムだけをフィルターすれば、費用を抑えることが可能です。

JX通信社の場合は、監視していないCloud Runのデータ取り込みに費用がかかってしまっていたため、 service_name: で必要なサービス名のみを指定することで限定し、費用を抑えました。このあたりのフィルター方法が公式ドキュメントに見つけられなかったので、試行錯誤に時間がかかっていました。

CloudWatchからの指標の取り込みを限定する

Amazon Cloud CloudWatchからDatadogへのデータの取り込みは、「CloudWatchのAPIなどをポーリング式で取得する方法」と「ストリームで取得する方法」があります。JX通信社の場合は「ポーリング式」で取得をしていました。*1

「ポーリング式」の場合、10分おきにバッチ処理が発火し、CloudWatch の GetMetricData API を叩き続けます。GetMetricData の料金 は指標の合計取得数に応じてかかります。例えば10,000程度の指標があった場合、月間で4,320万メトリクス分の取得になり、これはおよそ7万円/月の費用に相当します*2

そこで、「CloudWatch自体にある、指標の種類が多いが活用できていない指標を見直す」「Datadog が取り込む対象のリージョン、サービス、リソースを限定する」という2つの方針で、CloudWatchから取り込む指標の数を減らすことに取り組みました。

特に後者について、

  • 全リージョンを取得 → 日本のリージョンのみを有効化
  • 全サービスを取得 → 指標数が多く活用できてなかった EBS などを無効化し、重要なサービス(Lambda、ECS など)のみを有効化
  • Application Load Balancer の全ターゲットグループの指標を取得 → 「datadog:enabled」というタグがついた、重要サービスのみの指標を取得

といった設定変更を行なっています。ただし「リソース」の限定については Lambda、EC2、ロードバランサー、RDSなどが対象で、ECSのサービス名指定はできないようでした。

Cloud Monitoringの呼び出しを削減する

Cloud MonitoringからDatadogへの取り込みについても、AWSと同様、ポーリング式で行われています(5分ごと)

Cloud Monitoring のAPIは、AWSとは異なり、API の呼び出し数に応じて費用がかかります。Datadogからプロジェクトごとに呼び出しが行われるますが、デフォルトではおおよそ 1万円/月 × プロジェクト数の費用がかかります*3つまり実質的には、Google Cloudのプロジェクト数に応じた費用です。

こちらも、有効化するサービスの数を限定することで、Cloud Monitoringの費用を抑えることができます。2023年の公式ブログにはない機能だったので、この1-2年で設定できるようになったようです。

実際活用できていないプロジェクトをDatadogの監視対象から外すなどの地道な変更も行いました。

効果の確認

まず、Datadogの費用について、今回削減に取り組んだ指標を抜粋して記載しました。特にサーバーレス関連の費用を大きく下げることができました。11月の途中に設定変更をしているため、12月にかけて削減されるような形になっています。

次に、AWSについて、Cost ExplorerのGMD-Metricsの使用量(Metrics数)を日次で集計しました。こちらも大きく削減できています。

最後に、Google Cloud Monitoringについても、Cloud Billingの画面から確認し、Cloud Monitoring API Requests が下がっていることを確認しました。

終わりに

今回は、JX通信社で取り組んだ、Datadog関連の費用最適化について共有しました。

Datadogは機能が多いため、会社によってコストのかかり方は全く異なると思いますが、指標取得関連の費用が大きくかかっている会社は多いのではないでしょうか。「Datadogだけでなく、関連費用も見直す」「細かく有効化を切り替える」など、今回の取り組みを参考にしていただけたら幸いです。

*1:ストリーム方式で取得する方法に移行することでコストが下げられる可能性もありますが、JX通信社の場合は逆に大幅に費用が上がってしまったため、断念しています。EC2の情報を意図せず取得してしまい、EC2ホスト台数分の追加費用が大きくかかってしまいました。フィルターすることでEC2の費用を抑えられる可能性もありますが、検証していません

*2:単純計算のため、実際には増減すると思います

*3:newmoさんのブログ でも同様の費用だったので、プロジェクトの規模は大きく影響しないと思われます

browser-use を使って情シス業務を自動化するための実践的テクニック

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

最近、LLMを使ってブラウザ操作を自動化する、browser-use が流行っていますね! 今回は、毎月実施している情シスタスクの一つをテーマに、browser-use で業務自動化できるかを検証してみました。

browser-use とは

browser-use は、AIエージェントを使ってブラウザ操作を自動化するツールです。ブラウザ操作には内部的には playwright を使い、AI 部分は langchain を使っています。また、タスクを遂行するためのエージェントとしての仕組み(タスクを分解して、ネクストアクションを決め、ゴールが達成できているかを評価する)も備わっています。

そのため、ざっくりと「◯◯◯のサイトを開いて、◯◯◯の予約をして」というと、ブラウザを動かしてゴールを達成してくれます。

browser-use.com

ただし、「業務を自動化する」となるといくつかハイコンテキストな部分があり、雑に指示するだけでは難しいな、という印象です。

自動化したい情シスタスクの概要

情シス部門では毎月、「アカウントの棚卸し」という業務を行っています。これは、各種SaaSサービスにログインし、アカウント一覧をCSVなどでエクスポートし、退職済みの方がいないか、使われていないライセンスがないか、などを確認する業務です。セキュリティインシデントの防止や、コスト削減のために、単純ではあるものの重要な業務です。これを解決する SaaS などもありますが、それほど時間がかかっていないというのもあり、現在は手動で行っています。

今回は、Zoomを対象に「アカウント一覧をCSVなどでエクスポート」までを自動化してみます。

Step 1:公式ドキュメントに従って browser-use を使ってみる

import asyncio

from browser_use import Agent, Controller
from browser_use.browser.browser import Browser, BrowserConfig
from langchain_openai import ChatOpenAI

browser = Browser(
    config=BrowserConfig(
        headless=False,
        disable_security=False,
    )
)
controller = Controller()
model = ChatOpenAI(model="gpt-4o")


async def main():
    task = "Zoom のユーザー一覧ページを開き、エクスポート → テーブル内のすべてのユーザー からダウンロードをしてください"
    agent = Agent(
        task=task, llm=model, use_vision=True, controller=controller, browser=browser
    )
    await agent.run()
    await browser.close()


if __name__ == "__main__":
    asyncio.run(main())

公式ドキュメントに従えば、上記のようなコードで動きそう... ですが、実際にはうまくいきません。業務知識として「ZoomのID/PASSWORD」が必要なので、エージェントにはログインできないのです。放置していると、下記スクリーンショットのように、存在しないアカウントでログインを試みます。

Zoomのログインに失敗する様子(3倍速)

Step 2:ログインを実装する

さすがに LLM の API に ID/PASSWORD を送りたくないので、人間がログインをするようにしましょう。また、具体的にどの URL を開くのか指定した方が、結果が安定します。

# ...略...

browser = Browser(
    config=BrowserConfig(
        chrome_instance_path="/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
    )
)

# ...略...

@controller.action("ユーザーにログインを依頼する")
def ask_login():
    # Macユーザー限定
    subprocess.run(["say", "-v", "Kyoko", "ログインしてください"])
    input("ログインしてください。ログインが完了したら、Enterキーを押してください")
    return ActionResult(
        extracted_content="ログインが完了しました", include_in_memory=True
    )


async def main():
    async with await browser.new_context() as context:
        task = "https://zoom.us/profile を開き、ログイン済みかを確かめてください。ログインが完了していない場合は、ユーザーにログインを依頼(ask_login)してください。"
        agent = Agent(
            task=task,
            llm=model,
            use_vision=True,
            controller=controller,
            browser_context=context,
        )
        await agent.run()

        task = "https://zoom.us/account/user#/ を開き、エクスポート → テーブル内のすべてのユーザー からダウンロードをしてください"
        agent = Agent(
            task=task,
            llm=model,
            use_vision=True,
            controller=controller,
            browser_context=context,
        )
        await agent.run()

    await browser.close()

一部を省略しましたが、 @controller.action("ユーザーにログインを依頼する") で tool を登録してあげると、人間に操作を依頼できるようになります。tool 名はプロンプト内で具体的に指定した方が、期待通りに動作をしてくれます。

加えて、次の3つのテクニックを追加しました。こちらは必須というよりは好みの問題です。

  • 複数の Agent に分割している
    • AI になるべく単純なタスクを与えるため。実際には、単一のAgent、単一のプロンプトでもうまくいくことが多いです。
  • Playwright の Headless Chrome ではなく、実際のブラウザを使う(上記例ではChrome Canaryを指定)
    • ログイン情報(セッション)が保存されたり、Chrome 拡張をインストールしておくことがやりやすいです。
  • say コマンドで音声を読み上げる(Macユーザー限定)
    • バックグラウンドで AI に操作させておいて、人間の対応が必要なときに気づけるようになります。

上記の対応で、ログインまではうまくいくようになります...が、実際にはエクスポートしたファイルのダウンロードができていません。

Step3:ダウンロードに対応

Step2では一見するとダウンロードが成功したように見えるのですが、Playwright の仕様でダウンロード済みのファイルにアクセスすることができません。関連した Issue も立っており、回避策が提案されています。

https://github.com/browser-use/browser-use/issues/91

事前に download イベントに対応する処理を記載する必要があります。

# ...略...

def get_or_create_download_dir() -> Path:
    exe_dir = Path(os.path.dirname(os.path.abspath(__file__)))
    download_dir = exe_dir / "downloads"
    download_dir.mkdir(exist_ok=True, parents=True)
    return download_dir

async def handle_download(download: Download):
    original_path = await download.path()
    # ファイル名からダウンロード元サイトがわかるよう、example_com 形式でドメインを追加
    domain_prefix = download.url.split("/")[2].replace(".", "_")
    new_filename = f"{domain_prefix}_{download.suggested_filename}"
    new_path = get_or_create_download_dir() / new_filename
    os.rename(original_path, new_path)

async def handle_new_page(page: PlaywrightPage):
    page.on("download", handle_download)

async def setup_download(playwright_browser: PlaywrightBrowser):
    while len(playwright_browser.contexts) < 1:
        await asyncio.sleep(1)
        print("waiting for contexts to be created")
    for context in playwright_browser.contexts:
        context.on("page", handle_new_page)
        for page in context.pages:
            page.on("download", handle_download)

async def main():
    async with await browser.new_context() as context:
        pw_browser = await context.browser.get_playwright_browser()
        asyncio.create_task(setup_download(pw_browser))

        # ...略...

これで、基本的には問題なく動くと思います!

browser-useでエクスポートが成功している例(3倍速)

Step4:AI に処理の完了をちゃんと確認させる(おまけ)

今回の Zoom のエクスポートはかなり単純な例でした。他の SaaS では、エクスポートが非同期で行われることも多いです。複雑な処理の場合、Agent は誤って「ダウンロードが完了した」と誤解してしまうことがあります。

そこで、Agent にダウンロード済みファイルの確認をさせます。

# ...略...

@controller.action("ダウンロード済みファイル一覧を確認する")
def get_downloaded_files():
    download_dir = get_or_create_download_dir()
    files = list(download_dir.glob("*"))
    if not files:
        return ActionResult(
            extracted_content="ダウンロードフォルダに現在ファイルはありません。",
            include_in_memory=True,
        )

    file_list = "\n".join([f"- {file.name}" for file in files])
    return ActionResult(
        extracted_content=f"ダウンロードフォルダの内容:\n{file_list}",
        include_in_memory=True,
    )


async def main():
    async with await browser.new_context() as context:
        pw_browser = await context.browser.get_playwright_browser()
        asyncio.create_task(setup_download(pw_browser))

        task = "https://zoom.us/profile を開き、ログイン済みかを確かめてください。ログインが完了していない場合は、ユーザーにログインを依頼(ask_login)してください。"
        agent = Agent(
            task=task,
            llm=model,
            use_vision=True,
            controller=controller,
            browser_context=context,
        )
        await agent.run()

        task = "https://zoom.us/account/user#/ を開き、エクスポート → テーブル内のすべてのユーザー からダウンロードをしてください。作業を完了する前に、ダウンロードフォルダのファイル一覧(get_downloaded_files)を確認し、ダウンロードができているかを確認してください。ファイル名にはダウンロード元サイトのドメインが含まれています。"
        agent = Agent(
            task=task,
            llm=model,
            use_vision=True,
            controller=controller,
            browser_context=context,
        )
        await agent.run()

# ...略...

「ダウンロード済みファイル一覧を確認する」という tool を作成し、それによって作業の完了を確認させるようにしました。

実際、意地悪をして get_downloaded_files 関数が常に「ダウンロードフォルダに現在ファイルはありません。」を返すようにすると、延々とダウンロード処理を繰り返します。

振り返り

今回は「Zoom のユーザー一覧をエクスポートする」という作業をテーマに、browser-use での自動化をしてみました。

実際には他にも情シス業務の自動化を検証してみていますが、ハードルはやや高いかなという印象です。例えば、Google Sheets (スプレッドシート) のような複雑な UI の操作はあまりうまくいかない印象で、tool を用意してあげたり、タスクを分解してあげる、人間のレビューを挟むなど、「AI に期待しすぎない」というのがコツかなと思いました。

一方で、「AIでもわかるようにドキュメントや手順を簡略化する」という検討も進むので、良い機会にはなりました!

ぜひ皆さんも業務自動化をチャレンジしてみてください!