ブログ

  • EastCloud、国産クラウド総合支援サービスを提供開始外資クラウド依存によるデジタル赤字の解消を目指す

    EastCloud株式会社(本社:東京都、代表取締役:菊地航輔)は、2026年3月1日付で、国産クラウドを活用したシステム設計・構築・運用を一貫して支援する「国産クラウド総合支援サービス」の提供を開始しました。

    ■ サービス提供の背景

    現在、日本のIT基盤は外資クラウドへの依存が進み、データ主権やコスト構造の観点から「デジタル赤字」の拡大が課題となっています。

    特に公共領域や企業システムにおいては、国内で完結するインフラ基盤の整備が求められており、国産クラウドの活用が重要なテーマとなっています。

    ■ サービス概要

    本サービスは、国産クラウドを前提としたシステム基盤の設計から構築、運用までを一貫して支援するサービスです。

    • クラウドアーキテクチャ設計
    • インフラ構築(IaC対応)
    • アプリケーション実行基盤の構築
    • セキュリティ・ネットワーク設計
    • 運用設計・監視体制の構築

    ■ 技術的特徴

    本サービスでは、国産クラウドを中心としたインフラ設計を行い、コンテナ技術を活用した柔軟なアプリケーション実行環境を提供します。

    • さくらのクラウドを活用したインフラ構築
    • AppRunを活用したコンテナ実行基盤
    • セキュアなネットワーク設計
    • マルチクラウドを見据えた設計思想

    ■ 実装実績

    本サービスはすでに教育・医療・行政領域において導入されており、複数組織にまたがるデータ連携基盤の構築に活用されています。

    例えば、教育・保健医療・行政の連携を目指す「xinclusive」プロジェクトにおいては、システム基盤の設計・構築を担当しています。
    https://www.xinclusive.net/

    ■ 今後の展開

    EastCloudは、国産クラウドを軸としたシステム基盤の提供を通じて、国内におけるITインフラの自立性向上を目指します。

    今後は公共領域を中心に導入を拡大するとともに、クラウド統合およびガバナンスを支援する自社プロダクトの開発を進めていきます。

    ■ 会社概要

    会社名:EastCloud株式会社
    代表者:代表取締役 菊地航輔
    所在地:東京都
    事業内容:クラウド基盤設計・構築、システム開発

  • Claude Code × MCP:自社システムにAIを直結させる完全ガイド

    Claude Code × MCP:自社システムにAIを直結させる完全ガイド

    Claude Code × MCP:自社システムに AI を直結させる完全ガイド

    自分たちのビジネスシステムを AI に直結できたらどうなるか。

    自社の営業支援システムでは、Go + Next.js + PostgreSQL のバックエンドに TypeScript 製の MCP(Model Context Protocol)サーバーを接続して、Claude Code から直接データベースクエリを実行したり API を呼び出したりできるようにした。

    この記事では、その実装の全体像と、再現可能なステップバイステップガイドを共有する。

    MCP とは何か — AI ツール連携の新標準

    まず MCP について整理しておく。

    MCP(Model Context Protocol) は、Anthropic が 2024 年 11 月にオープンソースとして公開した、AI モデルと外部ツール・データソースを接続するための標準プロトコルだ。公開直後から業界全体で採用が進み、2025 年には OpenAI、Google、Microsoft もサポートを表明した。現在は Linux Foundation の標準化プロジェクトとして運用されており、月間 9,700 万回以上の SDK ダウンロード8,200 以上の公開 MCP サーバーが存在する巨大なエコシステムに育っている。

    つまり MCP は一企業の独自仕様ではなく、AI ツール連携のデファクトスタンダードだ。今 MCP に対応した仕組みを作っておけば、将来どの AI モデルを使うことになっても活用できる。

    関連記事:生成AI × 業務効率化:ChatGPTを「使える」ツールにする具体的な方法

    Before / After で見る変化

    導入前と導入後の開発フローを比べてみる。

    Before(MCP なし):

    Developer → pgAdmin → スキーマ確認 → コピペ → Claude Code に貼り付け
               → Postman → API テスト → レスポンスコピー → Claude Code に貼り付け
               → 4つのアプリを行き来するコンテキストスイッチ地獄

    After(MCP サーバー導入):

    Claude Code ← MCP Server → API + Database
        ↓
     自然言語でSQLクエリ実行、データ分析、API操作をワンステップ

    たとえば、こんなことがチャットだけで完結する。

    • “顧客テーブルから売上が100万円以上の企業を抽出して” → SQL 自動実行
    • “最新の営業案件を API 経由で取得して、ステータスごとに集計して” → DB + API 連携
    • “このユーザーのプロフィール画像を更新して” → API PATCH 自動実行 + JWT 自動更新

    では、実際の実装に入っていく。

    技術スタック

    使うものを先に整理しておく。

    コンポーネント技術
    MCP SDK@modelcontextprotocol/sdk(TypeScript)
    言語TypeScript 5.x+
    実行環境Node.js + tsx(TypeScript ランナー)
    DB クライアントpg(node-postgres)
    スキーマ検証Zod
    トランスポートStdio(Claude Code との通信)
    認証JWT(自動リフレッシュ機能付き)

    バージョンは npm install 時点の最新を使えば問題ない。MCP SDK は活発に更新されているので、package.json では ^ 指定にしておくといい。

    実装ステップ — ゼロから動くサーバーを作る

    Step 1: プロジェクトセットアップ

    MCP サーバー用の新しい Node.js プロジェクトを作る。

    mkdir my-mcp-server && cd my-mcp-server
    npm init -y
    npm install @modelcontextprotocol/sdk pg zod
    npm install -D typescript @types/node @types/pg tsx
    npx tsc --init

    package.json はこう設定する。

    {
      "name": "my-mcp-server",
      "version": "1.0.0",
      "description": "Custom MCP Server",
      "type": "module",
      "main": "src/index.ts",
      "scripts": {
        "start": "tsx src/index.ts",
        "build": "tsc",
        "typecheck": "tsc --noEmit"
      }
    }

    tsconfig.json の主要な設定:

    {
      "compilerOptions": {
        "target": "ES2022",
        "module": "ES2022",
        "moduleResolution": "bundler",
        "strict": true,
        "esModuleInterop": true,
        "outDir": "dist",
        "rootDir": "src",
        "skipLibCheck": true,
        "resolveJsonModule": true
      },
      "include": ["src/**/*"],
      "exclude": ["node_modules", "dist"]
    }

    ここまでで土台は完成。次にメインのデータベースツールを実装していく。

    Step 2: データベースツールの実装 — SQL を安全に実行する

    PostgreSQL への接続とクエリ実行を担当するツールを作る。MCP サーバーの核心部分だ。

    2.1 基本的な MCP サーバー構造

    src/index.ts のスケルトンから始める。

    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
    import pg from "pg";
    
    // 環境変数から接続情報を読み込み
    const DATABASE_URL = process.env.DATABASE_URL ||
      "postgresql://user:pass@localhost:5432/mydb";
    
    // ロギング(stderr を使用して stdio との干渉を避ける)
    function log(level: string, message: string, data?: unknown): void {
      const entry = { ts: new Date().toISOString(), level, message, ...(data && { data }) };
      process.stderr.write(JSON.stringify(entry) + "\n");
    }
    
    // データベースプール(遅延初期化)
    let pool: pg.Pool | null = null;
    
    function getPool(): pg.Pool {
      if (!pool) {
        pool = new pg.Pool({
          connectionString: DATABASE_URL,
          max: 5,
          idleTimeoutMillis: 30000,
        });
        pool.on("error", (err) => {
          log("error", "Unexpected pool error", { error: err.message });
        });
      }
      return pool;
    }
    
    // MCP サーバーの作成
    const server = new McpServer({
      name: "my-system",
      version: "1.0.0",
    });
    
    // スタートアップ処理
    async function main(): Promise<void> {
      const transport = new StdioServerTransport();
      await server.connect(transport);
      log("info", "MCP Server started");
    }
    
    main().catch((err) => {
      log("error", "Fatal error", { error: err.message });
      process.exit(1);
    });

    ポイント: ログ出力は必ず stderr に書く。MCP は stdin/stdout を通信に使うため、console.log を使うとプロトコルが壊れる。ここはよくあるハマりどころだ。

    2.2 SQL 安全性チェック — 事故を未然に防ぐ

    データベースを直接触れるツールには安全装置がいる。以下のチェック機能を入れる。

    import { z } from "zod";
    
    // 危険なパターンの定義
    const DANGEROUS_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
      { pattern: /\bDROP\b/i, label: "DROP" },
      { pattern: /\bTRUNCATE\b/i, label: "TRUNCATE" },
      { pattern: /\bALTER\b/i, label: "ALTER" },
      { pattern: /\bCREATE\b/i, label: "CREATE" },
      { pattern: /\bDELETE\b(?!.*\bWHERE\b)/is, label: "DELETE without WHERE" },
      { pattern: /\bINSERT\s+INTO\b.*\bSELECT\b/is, label: "INSERT INTO...SELECT" },
    ];
    
    // SQL の危険性をチェック
    function checkSqlSafety(sql: string): string | null {
      for (const { pattern, label } of DANGEROUS_PATTERNS) {
        if (pattern.test(sql)) {
          return label;
        }
      }
      return null;
    }
    
    // 読み取り専用クエリか判定
    function isReadOnly(sql: string): boolean {
      const trimmed = sql.trim().toUpperCase();
      return (
        trimmed.startsWith("SELECT") ||
        trimmed.startsWith("WITH") ||
        trimmed.startsWith("EXPLAIN") ||
        trimmed.startsWith("SHOW")
      );
    }

    この仕組みがあると、本番の全データをうっかり消すような事故を構造的に防げる。特に DELETE without WHERE の検出には実際に助けられた場面が何度かある。

    2.3 query_db ツール — メインのクエリ実行エンジン

    SQL を実行するメインツールだ。

    server.tool(
      "query_db",
      "Execute a SQL query against the database. By default only SELECT queries " +
        "are allowed. Set allow_write=true to permit INSERT/UPDATE/DELETE.",
      {
        sql: z.string().describe("SQL query to execute"),
        params: z
          .array(z.unknown())
          .optional()
          .describe("Parameterized query values ($1, $2, ...)"),
        allow_write: z
          .boolean()
          .optional()
          .default(false)
          .describe("Allow write operations"),
      },
      async ({ sql, params, allow_write }) => {
        log("info", "query_db", { sql, params, allow_write });
    
        // 読み取り専用モードのチェック
        if (!allow_write && !isReadOnly(sql)) {
          return {
            content: [
              {
                type: "text" as const,
                text: "Blocked: Only SELECT/WITH/EXPLAIN queries in read-only mode.",
              },
            ],
            isError: true,
          };
        }
    
        // 危険なパターンのチェック
        const violation = checkSqlSafety(sql);
        if (violation) {
          return {
            content: [
              {
                type: "text" as const,
                text: `Blocked: Dangerous SQL operation (${violation}).`,
              },
            ],
            isError: true,
          };
        }
    
        try {
          const result = await getPool().query(sql, params ?? []);
    
          if (result.rows && result.rows.length > 0) {
            return {
              content: [
                {
                  type: "text" as const,
                  text: `Query returned ${result.rows.length} row(s):\n\n${JSON.stringify(result.rows, null, 2)}`,
                },
              ],
            };
          }
    
          return {
            content: [
              {
                type: "text" as const,
                text: `Query executed. Rows affected: ${result.rowCount ?? 0}`,
              },
            ],
          };
        } catch (err) {
          const message = err instanceof Error ? err.message : String(err);
          log("error", "query_db failed", { error: message });
          return {
            content: [
              { type: "text" as const, text: `SQL Error: ${message}` },
            ],
            isError: true,
          };
        }
      }
    );

    ここで注目してほしいのは params パラメータだ。$1, $2 形式のパラメータ化クエリが使えるので、SQL インジェクションを根本的に防げる。

    関連記事:プロンプトエンジニアリング入門:AI への指示を最適化する技術

    2.4 list_tables ツール — テーブル一覧の取得

    データベースの全テーブルを一覧表示するツール。開発の最初に「何のテーブルがあるっけ?」と確認するときに重宝する。

    server.tool(
      "list_tables",
      "List all tables with row counts and column counts.",
      {},
      async () => {
        log("info", "list_tables");
    
        const sql = `
          SELECT
            t.table_name,
            (SELECT count(*)::int FROM information_schema.columns c
             WHERE c.table_schema = t.table_schema AND c.table_name = t.table_name)
             AS column_count,
            s.n_live_tup::int AS approx_row_count
          FROM information_schema.tables t
          LEFT JOIN pg_stat_user_tables s
            ON s.schemaname = t.table_schema AND s.relname = t.table_name
          WHERE t.table_schema = 'public' AND t.table_type = 'BASE TABLE'
          ORDER BY t.table_name;
        `;
    
        try {
          const result = await getPool().query(sql);
          const lines = result.rows.map(
            (r: any) =>
              `${r.table_name.padEnd(40)} ${String(r.column_count).padStart(4)} cols   ~${String(r.approx_row_count ?? 0).padStart(8)} rows`
          );
    
          return {
            content: [
              {
                type: "text" as const,
                text:
                  `Tables (${result.rows.length}):\n\n` +
                  `${"Table".padEnd(40)} Cols   Approx Rows\n` +
                  `${"─".repeat(65)}\n` +
                  lines.join("\n"),
              },
            ],
          };
        } catch (err) {
          const message = err instanceof Error ? err.message : String(err);
          return {
            content: [
              { type: "text" as const, text: `Error: ${message}` },
            ],
            isError: true,
          };
        }
      }
    );

    2.5 describe_table ツール — カラム・制約・外部キーの詳細表示

    特定テーブルの詳細スキーマを取得する。カラム名、データ型、制約、外部キーまで一度に確認できる。

    server.tool(
      "describe_table",
      "Get detailed schema including columns, types, constraints, and foreign keys.",
      {
        table: z.string().describe("Table name to describe"),
      },
      async ({ table }) => {
        log("info", "describe_table", { table });
    
        // テーブル名検証(SQLインジェクション対策)
        if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) {
          return {
            content: [
              {
                type: "text" as const,
                text: "Invalid table name. Only alphanumeric and underscore allowed.",
              },
            ],
            isError: true,
          };
        }
    
        try {
          // カラム情報取得
          const colSql = `
            SELECT
              c.column_name,
              c.data_type,
              c.character_maximum_length,
              c.is_nullable,
              c.column_default
            FROM information_schema.columns c
            WHERE c.table_schema = 'public' AND c.table_name = $1
            ORDER BY c.ordinal_position;
          `;
          const colResult = await getPool().query(colSql, [table]);
    
          if (colResult.rows.length === 0) {
            return {
              content: [
                {
                  type: "text" as const,
                  text: `Table '${table}' not found.`,
                },
              ],
              isError: true,
            };
          }
    
          // 制約情報取得
          const constraintSql = `
            SELECT
              tc.constraint_name,
              tc.constraint_type,
              kcu.column_name
            FROM information_schema.table_constraints tc
            JOIN information_schema.key_column_usage kcu
              ON tc.constraint_name = kcu.constraint_name
              AND tc.table_schema = kcu.table_schema
            WHERE tc.table_schema = 'public' AND tc.table_name = $1
            ORDER BY tc.constraint_type, tc.constraint_name;
          `;
          const constraintResult = await getPool().query(constraintSql, [table]);
    
          // 外部キー情報取得
          const fkSql = `
            SELECT
              kcu.column_name,
              ccu.table_name AS foreign_table,
              ccu.column_name AS foreign_column
            FROM information_schema.table_constraints tc
            JOIN information_schema.key_column_usage kcu
              ON tc.constraint_name = kcu.constraint_name
              AND tc.table_schema = kcu.table_schema
            JOIN information_schema.constraint_column_usage ccu
              ON ccu.constraint_name = tc.constraint_name
              AND ccu.table_schema = tc.table_schema
            WHERE tc.constraint_type = 'FOREIGN KEY'
              AND tc.table_schema = 'public'
              AND tc.table_name = $1;
          `;
          const fkResult = await getPool().query(fkSql, [table]);
    
          // 出力構築
          const sections: string[] = [];
          sections.push(`Table: ${table}\n`);
    
          // カラムセクション
          sections.push("Columns:");
          for (const col of colResult.rows) {
            let typeStr = col.data_type;
            if (col.character_maximum_length) {
              typeStr += `(${col.character_maximum_length})`;
            }
            const nullable = col.is_nullable === "YES" ? "NULL" : "NOT NULL";
            const def = col.column_default ? ` DEFAULT ${col.column_default}` : "";
            sections.push(
              `  ${col.column_name.padEnd(30)} ${typeStr.padEnd(25)} ${nullable}${def}`
            );
          }
    
          // 制約セクション
          if (constraintResult.rows.length > 0) {
            sections.push("\nConstraints:");
            const grouped = new Map<string, { type: string; columns: string[] }>();
            for (const row of constraintResult.rows) {
              const existing = grouped.get(row.constraint_name);
              if (existing) {
                existing.columns.push(row.column_name);
              } else {
                grouped.set(row.constraint_name, {
                  type: row.constraint_type,
                  columns: [row.column_name],
                });
              }
            }
            for (const [name, info] of grouped) {
              sections.push(
                `  ${info.type.padEnd(15)} ${name} (${info.columns.join(", ")})`
              );
            }
          }
    
          // 外部キーセクション
          if (fkResult.rows.length > 0) {
            sections.push("\nForeign Keys:");
            for (const fk of fkResult.rows) {
              sections.push(
                `  ${fk.column_name} -> ${fk.foreign_table}.${fk.foreign_column}`
              );
            }
          }
    
          return {
            content: [
              { type: "text" as const, text: sections.join("\n") },
            ],
          };
        } catch (err) {
          const message = err instanceof Error ? err.message : String(err);
          return {
            content: [
              { type: "text" as const, text: `Error: ${message}` },
            ],
            isError: true,
          };
        }
      }
    );

    ここまででデータベース操作に必要な 3 つのツールが揃った。次は API 連携機能を追加する。

    Step 3: API ツール実装 — JWT 認証を自動化する

    REST API への認証付きアクセスを提供する。JWT トークンの取得と更新を MCP サーバーが自動で行うので、開発者は認証を意識しなくていい。

    3.1 JWT トークン管理

    let jwtToken: string | null = null;
    let tokenExpiresAt = 0;
    
    const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:8080";
    const API_EMAIL = process.env.API_EMAIL || "[email protected]";
    const API_PASSWORD = process.env.API_PASSWORD || "password";
    
    // API ログイン
    async function apiLogin(): Promise<string> {
      log("info", "API login", { email: API_EMAIL });
    
      const res = await fetch(`${API_BASE_URL}/api/v1/auth/login`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: API_EMAIL, password: API_PASSWORD }),
      });
    
      if (!res.ok) {
        const body = await res.text();
        throw new Error(`Login failed (${res.status}): ${body}`);
      }
    
      const json = (await res.json()) as {
        data?: { token?: { access_token?: string } };
        token?: string;
        access_token?: string;
      };
    
      const token =
        json.data?.token?.access_token ?? json.token ?? json.access_token;
    
      if (!token) {
        throw new Error("API response missing token field");
      }
    
      jwtToken = token;
      // トークン有効期限:1時間。5分前に自動更新
      tokenExpiresAt = Date.now() + 55 * 60 * 1000;
      log("info", "API login successful");
      return token;
    }
    
    // トークン取得(自動リフレッシュ付き)
    async function getToken(): Promise<string> {
      if (!jwtToken || Date.now() >= tokenExpiresAt) {
        return apiLogin();
      }
      return jwtToken;
    }

    JWT(JSON Web Token) は Web API の認証で広く使われるトークン方式だ。サーバーがログイン時にトークンを発行し、クライアントはそれをリクエストに添付してログイン状態を維持する。有効期限があるため、期限切れ前に自動更新する仕組みが欠かせない。

    3.2 api_request ツール — 認証済み API 呼び出し

    server.tool(
      "api_request",
      "Make an authenticated REST API call. Automatically manages JWT authentication.",
      {
        method: z
          .enum(["GET", "POST", "PUT", "PATCH", "DELETE"])
          .describe("HTTP method"),
        path: z
          .string()
          .describe("API path (e.g., /api/v1/users)"),
        body: z
          .record(z.unknown())
          .optional()
          .describe("Request body (JSON)"),
      },
      async ({ method, path, body }) => {
        log("info", "api_request", { method, path });
    
        try {
          const token = await getToken();
          const url = `${API_BASE_URL}${path.startsWith("/") ? path : "/" + path}`;
    
          const headers: Record<string, string> = {
            Authorization: `Bearer ${token}`,
            "Content-Type": "application/json",
          };
    
          const fetchOptions: RequestInit = {
            method,
            headers,
          };
    
          if (body && ["POST", "PUT", "PATCH"].includes(method)) {
            fetchOptions.body = JSON.stringify(body);
          }
    
          const res = await fetch(url, fetchOptions);
          const contentType = res.headers.get("content-type") ?? "";
    
          let responseBody: string;
          if (contentType.includes("application/json")) {
            const json = await res.json();
            responseBody = JSON.stringify(json, null, 2);
          } else {
            responseBody = await res.text();
          }
    
          const statusLine = `${res.status} ${res.statusText}`;
    
          if (!res.ok) {
            return {
              content: [
                {
                  type: "text" as const,
                  text: `API Error (${statusLine}):\n\n${responseBody}`,
                },
              ],
              isError: true,
            };
          }
    
          return {
            content: [
              {
                type: "text" as const,
                text: `${method} ${path} -> ${statusLine}\n\n${responseBody}`,
              },
            ],
          };
        } catch (err) {
          const message = err instanceof Error ? err.message : String(err);
          log("error", "api_request failed", { error: message });
          return {
            content: [
              { type: "text" as const, text: `Request failed: ${message}` },
            ],
            isError: true,
          };
        }
      }
    );

    3.3 api_login ツール — 強制再ログイン

    トークンをリセットして新しいものを取得する。認証エラーが起きたときに使う。

    server.tool(
      "api_login",
      "Force re-login to get a fresh JWT token.",
      {},
      async () => {
        log("info", "api_login (forced)");
    
        try {
          jwtToken = null;
          tokenExpiresAt = 0;
          await apiLogin();
          return {
            content: [
              {
                type: "text" as const,
                text: "Successfully logged in. Token refreshed.",
              },
            ],
          };
        } catch (err) {
          const message = err instanceof Error ? err.message : String(err);
          return {
            content: [
              { type: "text" as const, text: `Login failed: ${message}` },
            ],
            isError: true,
          };
        }
      }
    );

    これで 5 つのツール(query_dblist_tablesdescribe_tableapi_requestapi_login)が全て揃った。次は安全に運用するためのセキュリティ設計だ。

    Step 4: セキュリティ — 本番運用に耐える安全設計

    MCP サーバーはデータベースと API に直接アクセスできる。だからこそ、セキュリティ設計が最も重要なステップになる。ここまでの実装に組み込んだ安全機能を整理する。

    4.1 多層防御の考え方

    このサーバーでは 4 つのレイヤーで安全性を確保している。

    レイヤー機能具体的な実装
    1. 読み取り制限デフォルト読み取り専用allow_write=false で UPDATE/DELETE をブロック
    2. パターン検出危険な SQL 検出DROP、TRUNCATE、DELETE without WHERE を自動ブロック
    3. インジェクション対策パラメータ化クエリ$1, $2 プレースホルダーで SQL インジェクションを防止
    4. 入力検証テーブル名ホワイトリスト英数字とアンダースコアのみ許可

    SQL インジェクションとは、悪意のある SQL 文をアプリケーション経由で実行させる攻撃手法だ。パラメータ化クエリを使えば、ユーザー入力がそのまま SQL として実行されることを防げる。

    4.2 環境変数による認証情報管理

    認証情報をコードにハードコーディングするのは厳禁だ。必ず環境変数で管理する。

    export DATABASE_URL="postgresql://user:[email protected]:5432/mydb"
    export API_BASE_URL="https://api.example.com"
    export API_EMAIL="[email protected]"
    export API_PASSWORD="<secure-password>"
    
    npm run start

    4.3 監査ログ

    すべてのクエリと API 呼び出しは stderr にログ出力される。本番環境ではログ集約サービスに転送すれば、不審なアクセスの検出に使える。

    {"ts":"2026-02-25T10:30:00.000Z","level":"info","message":"query_db","data":{"sql":"SELECT * FROM customers","allow_write":false}}
    {"ts":"2026-02-25T10:30:05.000Z","level":"info","message":"api_request","data":{"method":"PATCH","path":"/api/v1/users/42"}}

    4.4 データベースアカウント権限の最小化

    MCP サーバー用の専用 DB ユーザーを作り、必要最小限の権限だけを付与するのがベストプラクティスだ。

    -- 読み取り専用ユーザー
    CREATE USER mcp_readonly WITH PASSWORD 'secure_password';
    GRANT CONNECT ON DATABASE mydb TO mcp_readonly;
    GRANT USAGE ON SCHEMA public TO mcp_readonly;
    GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;

    Step 5: Claude Code への接続

    最後のステップ。作った MCP サーバーを Claude Code に登録する。

    5.1 Claude Code の MCP 設定

    プロジェクトの .mcp.json(プロジェクト単位)または ~/.claude.json(グローバル)に以下を追加する。

    {
      "mcpServers": {
        "my-system": {
          "command": "tsx",
          "args": [
            "/path/to/my-mcp-server/src/index.ts"
          ],
          "env": {
            "DATABASE_URL": "postgresql://user:pass@localhost:5432/mydb",
            "API_BASE_URL": "http://localhost:8080",
            "API_EMAIL": "[email protected]",
            "API_PASSWORD": "password"
          }
        }
      }
    }

    補足: Claude Code の MCP 設定は claude mcp add コマンドでも追加できる。詳しくは Claude Code 公式ドキュメント を参照。

    5.2 起動確認

    cd my-mcp-server
    npm run start

    stderr に以下のログが出れば接続成功だ。

    {"ts":"2026-02-25T10:30:00.000Z","level":"info","message":"MCP Server started"}

    Claude Code を起動すると、チャット内で query_dblist_tables などのツールが使えるようになる。

    実際の使い方:すぐに試せるユースケース 4 選

    サーバーが動いたら、早速試してみよう。開発で頻繁に使うパターンを 4 つ紹介する。

    ユースケース 1:顧客データの分析

    User: "売上が500万円以上の顧客を全て取得して"
    
    Claude Code:
    > query_db(
        sql: "SELECT * FROM customers WHERE revenue >= 5000000",
        allow_write: false
      )
    
    Result:
    ✓ Query returned 23 row(s):
    [
      { id: 1, name: "ABC Corp", revenue: 6500000, ... },
      { id: 2, name: "XYZ Inc", revenue: 5200000, ... },
      ...
    ]

    pgAdmin で SQL を手書きしていた作業が、自然言語で一発だ。

    ユースケース 2:テーブル構造の確認

    User: "顧客テーブルの構成を教えて"
    
    Claude Code:
    > describe_table(table: "customers")
    
    Result:
    ✓ Table: customers
    
    Columns:
      id                              int8              NOT NULL DEFAULT nextval(...)
      name                            text              NOT NULL
      email                           text              NOT NULL
      revenue                         numeric(12,2)    NULL
      created_at                      timestamp         NOT NULL DEFAULT now()
    
    Constraints:
      PRIMARY KEY pk_customers (id)
      UNIQUE       uq_customers_email (email)
    
    Foreign Keys:
      company_id -> companies.id

    ユースケース 3:API 経由のデータ更新

    User: "user_id=42 のプロフィール画像を更新して"
    
    Claude Code:
    > api_request(
        method: "PATCH",
        path: "/api/v1/users/42",
        body: { profile_image: "https://example.com/avatar.png" }
      )
    
    Result:
    ✓ PATCH /api/v1/users/42 -> 200 OK
    {
      "id": 42,
      "name": "John Doe",
      "profile_image": "https://example.com/avatar.png",
      "updated_at": "2026-02-25T10:35:00Z"
    }

    Postman を起動して URL を入力して認証ヘッダーを設定して……という手順が全部不要になる。

    ユースケース 4:複雑な集計分析

    User: "過去30日間の営業案件をステータスごとに集計して、
    件数と平均金額を出して"
    
    Claude Code:
    > query_db(
        sql: `
          SELECT
            status,
            COUNT(*) as count,
            AVG(amount) as avg_amount
          FROM deals
          WHERE created_at >= NOW() - INTERVAL '30 days'
          GROUP BY status
          ORDER BY count DESC
        `,
        allow_write: false
      )
    
    Result:
    ✓ Query returned 5 row(s):
    [
      { status: "closed_won", count: 47, avg_amount: 1250000 },
      { status: "negotiation", count: 23, avg_amount: 800000 },
      { status: "prospect", count: 15, avg_amount: 500000 },
      ...
    ]

    複雑な SQL もチャットで依頼するだけで実行できる。Claude が SQL を自動生成するので、SQL に詳しくないメンバーでもデータ分析に参加できる。

    関連記事:AI 議事録ツールの選び方:会議の生産性を2倍にする実践ガイド

    既存の MCP サーバーとの違い

    「PostgreSQL 用の MCP サーバーなら既に公開されているのでは?」という疑問はもっともだ。実際、公開レジストリには PostgreSQL 用のサーバーがいくつかある。

    ただ、自社システム専用に作る価値は明確にある。

    観点汎用 MCP サーバー自作 MCP サーバー
    API 連携なし(DB のみ)自社 API に完全対応
    認証汎用的自社の JWT フローに最適化
    セキュリティ汎用ルール自社のポリシーに合わせた制御
    ビジネスロジックなし業務特有のツールを追加可能
    メンテナンス外部依存チーム内で完全制御

    汎用サーバーはデータベースだけ触れればいい場合には十分だが、API 連携、カスタム認証、業務固有のロジックが必要になると自作が圧倒的に有利になる。

    よくある質問(FAQ)

    Q: MCP サーバーは本番環境でも使えますか?

    A: 使えるが、慎重な設計が要る。本番では読み取り専用の DB アカウント、SSL/TLS 通信、アクセスログの監視を必ず設定すること。この記事の「セキュリティ」セクションのベストプラクティスに従えば、安全に運用できる。

    Q: 他の AI ツール(ChatGPT、Gemini など)でも使えますか?

    A: 使える。MCP はオープン標準であり、対応する AI ツールなら何でも接続できる。2025 年以降、OpenAI や Google も MCP サポートを発表しており、一度作った MCP サーバーは複数の AI ツールで再利用可能だ。

    Q: 複雑な JOIN クエリでも大丈夫ですか?

    A: 基本的には問題ない。ただし、何十行にもわたる超複雑なクエリやパフォーマンスチューニングが必要なケースでは、DBeaver のような専用クエリエディタのほうが適している場面もある。MCP は日常の開発作業を効率化するツールであり、すべてを置き換えるものではない。

    Q: セキュリティリスクはありますか?

    A: MCP サーバー自体はローカルで動作し、ネットワークに公開されない。リスクは主に (1) 認証情報の管理ミス、(2) 過剰な DB 権限、(3) 安全チェックの不備 の 3 点。この記事で解説した多層防御(読み取り制限、パターン検出、パラメータ化クエリ、入力検証)をすべて実装すれば、リスクは最小限に抑えられる。

    自社システム × MCP で開発体験を変える

    MCP サーバーを自社システムに統合すると、以下が手に入る。

    • 開発効率の向上 — データベースクエリや API 呼び出しが自然言語で実行できる
    • 堅牢なセキュリティ — 多層防御で、読み取り専用、SQL パターン検出、パラメータ化クエリ、入力検証をカバー
    • 将来性 — MCP はオープン標準なので、一度作ればどの AI ツールでも再利用できる
    • 柔軟な拡張性 — 5 つのツールをベースに、業務に合わせたカスタムツールを足していける

    実装は 600 行程度の TypeScript で完成し、セットアップも半日あれば十分。自社システムを持っているなら、専用の MCP サーバーを作る価値は十分にある。

    この記事のコードをベースに、あなたのシステム専用の MCP サーバーを作ってみてほしい。

  • ブラウザをもっと快適に!作業効率が上がるおすすめ拡張機能まとめ

    ブラウザをもっと快適に!作業効率が上がるおすすめ拡張機能まとめ

    ネット検索、仕事、学習、ショッピング――日々のブラウザ作業をより快適にしてくれるのが「拡張機能」です。
    この記事では、Google Chrome・Microsoft Edge などで使える便利な拡張機能を、目的別にわかりやすくまとめました。

    1. 作業効率アップ系のおすすめ拡張機能

    ● Momentum

    新しいタブを美しい写真と名言、やることリストに変えてくれる拡張機能。 仕事前に気持ちを整えるのに最適です。

    ● OneTab

    開きすぎたタブを一括で整理・保存。 タブをまとめてくれるので、メモリ使用量が大幅に減り、PCの動作も軽くなります。

    ● Tab Manager Plus

    大量のタブをアイコンで一覧表示してくれるタブ管理特化ツール。 タブ探しの時間がゼロになります。

    2. セキュリティ・プライバシー対策に役立つ拡張機能

    ● uBlock Origin

    軽量で強力な広告ブロッカー。 動画広告やポップアップを大幅に減らし、快適にブラウジングできます。

    ● LastPass

    パスワード管理の定番。 複雑なパスワードを自動生成し、ログインもワンクリックで完了。 オンラインアカウントが多い人に必須です。

    ● HTTPS Everywhere

    可能な限りウェブサイトとの通信を暗号化(HTTPS化)してくれる拡張機能。 情報漏えいのリスクを減らせます。

    3. 調べ物・学習に便利な拡張機能

    ● Grammarly

    英語文章を自動でチェックしてくれるツール。 メール、SNS、ブログなど、あらゆる場面で英語の品質が向上します。

    ● Google Dictionary

    わからない単語をダブルクリックするだけで意味を表示。 語学学習にとても便利です。

    ● Evernote Web Clipper

    Webページを画像・テキストとして保存し、メモとして整理できる拡張機能。 調べ物や資料作成に役立ちます。

    4. Webデザイン・クリエイター向けの拡張機能

    ● ColorZilla

    画面上の色をスポイトで取得できるカラーコード確認ツール。 デザイナーに必須です。

    ● WhatFont

    Webページの文字にカーソルを合わせるだけでフォント名・サイズを表示。 サイトのデザイン研究に使えます。

    ● Page Ruler Redux

    要素のサイズ(px)を測れるツール。 Web制作で頻繁に使われる拡張機能です。

    5. 動画・SNSを快適にする拡張機能

    ● Enhancer for YouTube

    広告スキップ・画質固定・ショートカット設定など、YouTubeを快適に使える多機能ツール。

    ● Save to Pocket

    後で読みたい記事を保存しておけるサービスの拡張機能。 動画・記事・SNS投稿を一括管理できます。

    ● Dark Reader

    どんなサイトでも強制的にダークモード表示にしてくれる拡張機能。 夜間の作業や目の疲れ対策に最適です。

    6. インストールの注意点

    • 入れすぎるとブラウザが重くなるため、必要なものに絞る
    • 公式ストア(Chrome Web Store )から入れる
    • 設定画面で不要な権限を確認し、セキュリティを確保

    まとめ

    拡張機能を活用することで、ブラウザは仕事・学習・日常のあらゆる作業を強力にサポートしてくれます。
    ぜひ自分の用途に合った機能を取り入れ、快適なブラウジング環境を構築してみてください。

  • GitHub運用ガイドを社内標準化した話 — 小規模チーム開発の土台を固める

    GitHub運用ガイドを社内標準化した話 — 小規模チーム開発の土台を固める

    GitHub運用がバラバラだった頃

    弊社EastCloudは受託開発と自社プロダクト開発の両軸で事業を進めている。GitHubは早い段階から導入していたが、運用ルールが定まらないまま開発を続けていた。

    プロジェクトが増え、メンバーが増えてくると、ルールの不在が目に見えて開発効率に影響し始めた。そこで社内のGitHub運用を標準化するガイドを作成した。

    ここではガイド作成の背景、内容のポイント、導入後に確認できた効果をまとめる。

    整備前に抱えていた4つの課題

    ガイド作成にあたって社内の開発フローを棚卸しした。見えてきた課題は4つあった。

    • ブランチ運用の属人化 — 命名規則がなく、誰がどのブランチで何を開発しているか把握できていなかった
    • コミット履歴の不透明さ — コミットメッセージが「修正」「対応」ばかりで、変更の意図を後から追えなかった
    • コードレビュー文化の不在 — PRを経由せずmainブランチへ直接pushする運用が常態化していた
    • 非エンジニアメンバーの疎外 — PMやデザイナーがGitHubの画面を見ても開発状況がまったくわからなかった

    いずれも少人数チームで起こりがちな問題だ。暗黙知で回せる段階はすぐに限界を迎える。メンバーが5人を超えたあたりから、明文化されたルールがないと開発効率は目に見えて落ちる。

    ガイドの全体構成

    作成したガイドは10章+付録の構成にした。エンジニアだけでなくPMやデザイナーも読める内容にすることを重視している。

    内容対象者
    1. ブランチ戦略main/develop/feature/hotfixの運用フローエンジニア
    2. コミットメッセージ規約type付きメッセージのルールエンジニア
    3. Issue運用タスク管理の最小単位としてのIssue活用全員
    4. PR運用PRフロー・タイトル規約・マージ方針エンジニア
    5. コードレビュー観点正確性・セキュリティ・可読性・パフォーマンス・テストエンジニア
    6. GitHub Projectsボード運用カンバンのステータス管理全員
    7. CI/CD方針GitHub Actionsによる自動化エンジニア
    8. 日常の開発フロー一連の作業手順まとめエンジニア
    9. よくある質問コンフリクト対応・誤push対応などエンジニア
    10. 非エンジニア向け基礎GitHubの基本概念と3つのアクションPM・デザイナー
    付録Gitコマンド早見表エンジニア

    では各章のポイントを掘り下げていく。

    ブランチ戦略 — 4種類に絞った理由

    小規模チーム向けのブランチ戦略としてはGitHub Flowがよく推奨される。mainブランチとfeatureブランチだけで回すシンプルなモデルだ。ただ、弊社は受託開発も手がけており、リリースタイミングが顧客との調整で決まる案件が多い。

    GitHub Flowはmainにマージしたらすぐデプロイする前提だ。リリースを溜めてまとめて出す受託開発には合わない。そこでGit Flowをベースに、ブランチを4種類に絞った簡略版を採用した。releaseブランチはリリースフローが複雑化するまで導入しない。

    ブランチ用途命名規則
    main本番リリース済みコード
    develop開発統合ブランチ
    feature/*新機能・改善feature/<issue番号>-<説明>
    hotfix/*本番の緊急修正hotfix/<issue番号>-<説明>

    ブランチ名はケバブケースで統一した。feature/6-authentication-apihotfix/23-fix-login-errorといった形だ。Issue番号を先頭に置くことで、どのタスクに対応するブランチかひと目でわかる。

    運用面で大事にしたのは、mainだけでなくdevelopへの直接コミットも禁止した点だ。すべての変更はfeatureブランチからPRを経由してdevelopにマージする。加えてhotfix/*ブランチはmainから作成し、修正後はmaindevelopの両方にマージして修正漏れを防ぐ。

    ここで欠かせないのがGitHubのBranch Protection Rule(ブランチ保護ルール)だ。mainへの直接pushをシステム的に禁止した。ルールは口頭で伝えるだけでは定着しない。ツールで強制するほうが確実だ。

    なおGitHubは2023年7月にRepository Rulesetsという新しい保護機能をGA(一般提供)している。従来のBranch Protection Ruleと併存しており、Organization全体に統一ルールを適用したい場合はRulesetsのほうが柔軟だ。ただし単一リポジトリの保護なら従来のBranch Protection Ruleで足りるため、今回はそちらを採用した。

    コミットメッセージ規約 — 6つのtypeで変更意図を明確にする

    コミットメッセージの統一にはConventional Commitsの考え方をベースにしたシンプルなフォーマットを採用した。

    <type>: <概要>

    scopeは省いた。小規模チームではモジュール境界がまだ流動的で、scopeの粒度をルール化すると逆に混乱する。概要は日本語でも英語でもよい。

    typeは6つに絞った。

    type用途
    feat新機能feat: ログインAPI追加
    fixバグ修正fix: トークン有効期限の修正
    refactorリファクタリングrefactor: バリデーション処理を分離
    docsドキュメントdocs: API仕様書を追加
    testテストtest: 認証コントローラのテスト追加
    choreビルド・設定・その他chore: docker-compose設定を更新

    Issue番号はコミットメッセージには含めず、PRタイトルで紐付ける運用にした。コミットメッセージは変更の意図だけに集中させ、タスク管理の情報はPRに寄せて役割を分けている。

    Issue運用とGitHub Projectsボード

    Issueをタスク管理の最小単位にする

    ガイドではIssueをタスク管理の最小単位と位置づけた。すべての作業はIssueを起点にする。急ぎのhotfixでも事後でIssueを作成するルールだ。

    IssueにはラベルをつけてProjectsボードと連携する。推奨ラベルとしてbackendfrontendinfrastructurebugdocumentationを用意した。プロジェクトに応じて追加・変更できる。

    Projectsボードのステータス管理

    GitHub Projectsボードのステータスは4つに絞った。

    ステータス説明
    Todo未着手
    In Progress作業中
    In ReviewPR作成済み・レビュー待ち
    Done完了

    Issueに着手したら必ずIn Progressに変更する。PRを作成したらIn Reviewに変更する。さらに週次でボードの棚卸しを行い、停滞しているIssueがないか確認する。この棚卸しがないとボードは簡単に形骸化するため、週次ミーティングの固定アジェンダに組み込んだ。

    PR運用 — タイトル規約とマージ方針

    PRタイトルにIssue番号を必須化

    PRタイトルにはIssue番号を必ず含めるルールにした。フォーマットはこうだ。

    feat[#6]: 認証API実装
    fix[#12]: ユーザー一覧の表示不具合を修正

    type[#Issue番号]: 概要の形式で、コミットメッセージ規約のtypeを流用している。PRタイトルを見ただけで、どのタスクの、どんな種類の変更かわかる。

    PRテンプレートの標準化

    PRテンプレートは.github/PULL_REQUEST_TEMPLATE.mdに配置した。セクションは概要・対応Issue・変更内容・確認事項の4つだ。

    特に効いたのが確認事項のチェックリストだ。「ローカルで動作確認済み」「テスト追加・更新済み」「既存機能への影響なし」の3点をセルフチェックさせることで、レビュアーの負担を下げた。

    加えてPR本文にcloses #<Issue番号>を記載すると、マージ時にIssueが自動クローズされる。閉じ忘れを防ぐ仕組みとして活用している。

    マージ方針の使い分け

    マージ方法は2つを使い分ける。

    種別マージ方法理由
    機能ブランチ → developSquash and merge作業途中の細かいコミットを1つにまとめて履歴を整理する
    developmainMerge commitリリース単位のマージ履歴を明示的に残す

    Squash and mergeを使えば、featureブランチ内で何度コミットしてもdevelopの履歴はPR単位で1コミットに整理される。コミットの粒度を気にせず開発できるため、開発者の心理的なハードルが下がる。

    レビュー方針

    レビューは最低1名のApproveを必須にした。ただし軽微な修正(typo、コメント追記等)はセルフマージを許可している。すべてのPRに厳格なレビューを求めると小規模チームでは回らない。重要な変更にレビューリソースを集中させるための割り切りだ。

    コードレビュー観点 — 5つの視点でチェックする

    レビュアーが何を見ればいいか迷わないよう、コードレビューの観点を5つに分けて明文化した。

    正確性 — ビルドが通ること。Issueの要件を満たしていること。境界値やエッジケースが考慮されていること。

    セキュリティ — SQLインジェクション対策でパラメータバインドを使っているか。XSS対策でユーザー入力値をエスケープしているか。機密情報がハードコードされていないか。CSRF対策が必要な箇所で実装されているか。

    可読性・保守性 — 変数名・関数名がその役割を表しているか。1つの関数が1つの責務に集中しているか。デバッグ用コード(console.log等)が残っていないか。マジックナンバーが定数化されているか。

    パフォーマンス — N+1クエリが発生していないか。ループ内で不要なAPIコールがないか。大量データの処理でページネーションが考慮されているか。

    テスト — 変更に対応するテストが追加されているか。正常系・異常系の両方がテストされているか。

    この5観点をガイドに書いたことで、レビューの属人化を防げた。新しくレビュアーになったメンバーでも、何をチェックすべきか迷わない。

    CI/CD方針 — GitHub Actionsによる自動化

    ガイドにはCI/CD方針も盛り込んだ。GitHub Actionsで以下のタイミングに自動化を走らせる。

    トリガー実行内容
    PR作成・更新時Lint + ビルド + テスト
    developへのマージステージング環境へデプロイ
    mainへのマージ本番環境へデプロイ

    CIのチェック項目はコードフォーマット(Lint)、ビルド成功、ユニットテスト通過の3つ。CIが失敗しているPRはマージしない。Branch Protection RuleでCIパスを必須条件に設定すれば、この運用もツールで強制できる。

    非エンジニア向けGitHub基礎 — 社内で最も反応が良かったセクション

    ガイドの中で社内から最も反応が良かったのがこのセクションだ。

    PMやデザイナーがGitHub上で開発状況を把握できるよう、基本概念をスクリーンショット付きで解説した。技術用語は極力使わず、ブランチはWord文書の変更履歴を別ファイルに分けて管理するイメージ、PRはGoogleドキュメントの提案モードに近い仕組み、という比喩で説明している。

    非エンジニアに覚えてもらった3つのアクション

    1. Issueにコメントする
    仕様の質問や確認をGitHub上で行い、情報を一元管理する。Slackやメールに散らばっていた仕様確認がIssueに集約されるだけで、後からの振り返りが格段に楽になる。

    2. PRのFilesタブを確認する
    コードの詳細はわからなくても、何が変わったかを視覚的に把握できる。緑の行が追加、赤の行が削除。この見方だけ覚えてもらった。

    3. PRにApproveする
    PM承認をGitHub上で行い、口頭承認によるコミュニケーションロスをなくす。PR画面の「Review changes」から「Approve」を選ぶだけで完了する。

    この3つを徹底したことで、開発プロセスの透明性が大幅に上がった。PMが開発状況をリアルタイムで把握できるようになり、チーム全体の連携が目に見えてスムーズになった。Projectsボードの見方もあわせて説明し、PMがタスク進捗を一覧で確認できるようにしている。

    導入後の定量効果

    ガイドを展開してから約1ヶ月後に振り返りを行った。確認できた成果は次の通りだ。

    指標導入前導入後
    mainへの直接push週に数回発生ゼロ
    コミットメッセージ規約の遵守率ほぼ0%約80%
    PRの説明記述率約20%約90%
    非エンジニアのGitHub利用ほぼなしPMがIssueコメントを日常的に利用

    遵守率が100%に届いていないのは正直なところだ。typeの使い分け自体はほぼ100%守られている。ただ概要の書き方にはまだばらつきがある。ここは引き続き改善を進めている。

    定性面で大きかったのは、PMがGitHub上でIssueコメントを行うようになった点だ。以前はSlackでの口頭確認が中心で、仕様の決定経緯がよく流れていた。Issueに記録が残るようになったことで情報の散逸を防げるようになった。

    ガイド作成で意識した3つの原則

    今回のガイド作成を通じて、強く意識した原則が3つある。

    ルールを最小限にすること。 覚えきれないルールは形骸化する。コミットメッセージのtypeは6つ、Projectsボードのステータスは4つ、非エンジニアに覚えてもらうアクションは3つ。小規模チームに必要十分な数に絞り、確実な定着を狙った。

    ツールで強制すること。 Branch Protectionによるmainへの直接push禁止、CIパス必須によるマージ制限、PRテンプレートによる説明欄の標準化。人の注意力に頼るルールはいずれ破綻する。守らざるを得ない環境を作ることが運用定着への最短経路だ。

    非エンジニアを巻き込むこと。 開発プロセスの透明性はチーム全体の生産性に直結する。エンジニアだけが使うツールのままではGitHubの価値は半減する。PMやデザイナーも参加できる設計にしたことで、コミュニケーションコストが下がった。

    よくある質問

    Q. GitHub FlowではなくGit Flowベースにした理由は?

    弊社は受託開発も手がけており、リリースタイミングが顧客調整で決まることが多い。GitHub Flowはmainにマージしたらすぐデプロイする前提だ。リリースを溜めてまとめて出す運用にはフィットしない。developブランチを挟む簡略版Git Flowのほうが実情に合った。

    Q. コミットメッセージにscopeを入れなかった理由は?

    小規模チームではモジュール境界がまだ流動的で、scopeの粒度をルール化すると混乱を招く。typeと概要だけのシンプルなフォーマットにして、覚えやすさと定着率を優先した。

    Q. 非エンジニアにGitHubを使ってもらうのは現実的か?

    弊社ではうまくいった。覚えてもらうアクションをIssueコメント、PRのFilesタブ確認、Approveの3つに絞ったことが大きい。いずれもWebブラウザ上で完結するため学習コストは低い。

    Q. Squash and mergeとMerge commitを使い分ける理由は?

    featureブランチでは試行錯誤で細かいコミットが積まれがちだ。Squash and mergeで1コミットにまとめれば、developの履歴はPR単位で整理される。一方、develop→mainはリリース単位の記録として残したいため、Merge commitでマージ履歴を明示する。

    Q. ガイドの分量はどのくらいが適切?

    今回は本文10章+付録の構成だった。各章を独立して読めるようにしたので、エンジニアは1〜9章、非エンジニアは10章とProjectsボードの章だけ読めば運用を始められる。全部読まなくても始められる設計にしておくのが大事だ。

    Q. Branch Protection RuleとRepository Rulesetsはどちらを使うべき?

    単一リポジトリの保護ならBranch Protection Ruleで十分だ。Organization全体に統一ルールを適用したい場合や、複数リポジトリに同じ保護設定を展開したい場合はRepository Rulesetsが向いている。両者は併存できるため、まずBranch Protection Ruleから始めて必要に応じて移行するのがよい。

    関連記事

    ガイドのダウンロード

    今回作成したガイドを、汎用的に使える形に整理したPDF資料として公開している。自社の状況に合わせてカスタマイズして使ってほしい。

    GitHub運用ガイド(PDF)をダウンロードする

    小規模チームこそ早い段階で開発プロセスを整備する意味がある。人数が増えてからでは、ゼロから構築するよりも遥かにコストがかかる。

  • 情報収集を全自動化する方法 ― RSS・LLMキュレーション・チャット配信の実装ガイド

    情報収集を全自動化する方法 ― RSS・LLMキュレーション・チャット配信の実装ガイド

    エンジニアチームが抱える情報収集の4つの壁

    ソフトウェア開発の世界は変化が激しい。毎日のように新しいツールやフレームワークがリリースされる。とりわけ2024年以降はAI関連の進歩が加速しており、1週間キャッチアップを怠ると動向を見逃しかねない。

    弊社のエンジニアチームも、この問題に長く悩んでいた。

    • 個人依存の情報収集 — 各メンバーが独自のやり方で情報を追い、チーム全体への共有が薄い
    • 時間の問題 — 業務時間中にRSSリーダーやHacker Newsを巡回する余裕がない
    • 英語の壁 — 一次情報の大半は英語。全員が均等にアクセスできるわけではない
    • 情報の偏り — 個人の関心領域に引っ張られ、チームとして押さえるべき技術動向にムラが出る

    毎日15分で技術トレンドをチーム全員がキャッチアップできる仕組みが欲しい。その声をきっかけに、情報収集パイプラインの全自動化に取り組んだ。

    では、RSS収集からLLMキュレーション、チャットツールへの配信までを一気通貫で自動化した仕組みと、数ヶ月の運用で得た知見を順に見ていく。


    自動化パイプラインの全体像

    構築したパイプラインは3つのフェーズで動く。

    [収集フェーズ]          [キュレーションフェーズ]        [配信フェーズ]
    
     RSS Feeds ─────┐
                     ├─→ Markdown ─→ LLM(Claude Haiku) ─→ Discord / Slack
     GitHub Trend ──┘    ファイル      10〜15件に厳選         サマリー配信
           │
        DeepL API
       (英→日翻訳)

    まず、それぞれの役割を整理する。

    1. 収集 (rss_collector.py) — 複数のRSSフィードとGitHubトレンドから記事を取得し、英語記事はDeepL APIで翻訳。Markdownファイルとして出力する
    2. キュレーション (news_distributor.py) — LLMが記事を評価・厳選し、チームにとって価値の高い10〜15件を選ぶ
    3. 配信 — DiscordやSlackに通知なしで投稿。サマリーと個別記事の2段構成にする

    すべてPythonで実装し、日次のスケジュール実行で完全に自動化している。


    収集の仕組み ― 複数ソースからの情報集約

    RSSフィードの選定

    収集元のRSSフィードは、チームの技術スタックと関心領域に合わせて選んでいる。

    # フィード設定の例(実際のコードを簡略化したもの)
    feeds = [
        # 主要テックメディア
        "https://hnrss.org/newest?points=100",         # Hacker News (100pt以上)
        "https://www.infoq.com/feed/",                  # InfoQ
        "https://techcrunch.com/feed/",                 # TechCrunch
    
        # AI/LLM特化
        "https://openai.com/news/rss.xml",              # OpenAI News
        "https://rsshub.app/anthropic/news",            # Anthropic (RSSHub経由)
    
        # 開発ツール・クラウド
        "https://github.blog/feed/",                    # GitHub Blog
        "https://aws.amazon.com/blogs/aws/feed/",       # AWS Blog
    
        # 日本語ソース
        "https://zenn.dev/feed",                        # Zenn
        "https://b.hatena.ne.jp/hotentry/it.rss",       # はてなブックマーク IT
    ]

    ここでのポイントは hnrss.org の活用だ。Hacker News本体にはフィルタ付きRSSがない。だが、hnrss.orgを使えば ?points=100 のようにスコアで足切りでき、ノイズを大幅に減らせる。

    加えて、Anthropicのようにネイティブなフィードを提供していないサイトもある。こうしたケースではRSSHubなどのコミュニティツールを使ってフィードを生成する。フィード収集の安定性を考えると、各ソースのRSS提供状況は定期的に確認したほうがいい。

    GitHubトレンドの収集

    RSSだけでは拾いきれない今まさに注目されているリポジトリを把握するために、GitHubのトレンドページからも情報を引いている。新しいOSSツールやライブラリの早期発見に役立ち、チーム内で面白いリポジトリを見つけたという会話のきっかけにもなる。

    DeepL APIによる翻訳処理

    一次情報の多くは英語で出てくる。チーム全員がストレスなく読めるように、英語記事はDeepL APIで日本語に翻訳している。

    # 翻訳処理の概要
    def translate_if_needed(article):
        if detect_language(article.title) == "en":
            article.title_ja = deepl_translate(article.title, target="JA")
            article.summary_ja = deepl_translate(article.summary, target="JA")
        return article

    DeepL APIを選んだのは、技術文書の翻訳精度が高く専門用語の扱いが自然だからだ。2024年7月に発表されたDeepLの次世代言語モデルは、ブラインドテストでChatGPT-4比1.7倍、Google翻訳比1.3倍の品質評価を得ている。とりわけ日英間の翻訳ではニュアンスの再現に強みがある。

    翻訳にLLMを使う選択肢も検討した。しかしコストとレイテンシの両面でDeepLが勝った。DeepL API Freeプランは月50万文字まで無料で使える。Proプランに移行しても月額基本料5.49ドルに加え、100万文字あたり約25ドルで済む。日次パイプラインの翻訳量ならFreeプランの範囲内で収まるケースも多い。

    Markdownへの出力

    収集した記事はMarkdownファイルとして保存する。日付ごとにファイルが生成され、次のキュレーションフェーズへの入力になる。

    ## 2026-02-17 収集記事一覧
    
    ### [1] Anthropic Releases New Model with Enhanced Coding Capabilities
    - URL: https://example.com/article1
    - ソース: Anthropic
    - 翻訳タイトル: Anthropic、コーディング能力を強化した新モデルを発表
    - サマリー: ...

    では、集めた記事をどう絞り込むか。次のキュレーションフェーズが肝になる。


    LLMキュレーションの実装 ― AIが読むべき記事を選ぶ

    キュレーションが必要な理由

    RSSフィードから集まる記事は、1日あたり数十件から百件を超えることもある。これを全部チャットに流すと、情報過多で誰も読まなくなる。

    実際に試した。キュレーションなしでそのまま流したところ、通知が多すぎて見なくなったというフィードバックが1週間で返ってきた。

    だからといって人間が毎日手作業で選定するのは続かない。単純なキーワードフィルタでは文脈を踏まえた取捨ができない。そこでLLMによるキュレーションを導入した。

    選定基準の設計

    LLMに渡すプロンプトでは、選定基準を明確に記述している。

    curation_prompt = """
    以下の記事リストから、ソフトウェア開発チームにとって価値の高い記事を
    10〜15件選定してください。
    
    ## 選定基準(優先度順)
    
    1. AI/LLM動向: 新モデル、新サービス、実用的なユースケース
    2. 開発ツール: 生産性を向上させるツールやサービスのアップデート
    3. SaaS/クラウド: 主要クラウドプロバイダーの新機能、料金変更
    4. GitHubリポジトリ: 実用的で注目度の高いOSSプロジェクト
    
    ## 除外基準
    
    - 初心者向けチュートリアル
    - 基礎的すぎる内容(チームの技術レベルに合わない)
    - 広告色の強いプレスリリース
    - 古い情報の焼き直し
    
    ## 出力形式
    選定した記事ごとに、選定理由を1行で添えてください。
    """

    ここで重視したのは除外基準の明示だ。LLMは指示がなければ記事を落とすことに慎重になりがちで、件数が膨らむ。除外条件を具体的に書くことで、厳選の精度が上がった。

    Claude Haikuを採用した理由

    キュレーションにはClaude 3 Haikuを使っている。

    • コストが低い — 日次で回すため、ランニングコストの低さは外せなかった。Claude 3 Haikuは入力100万トークンあたり0.25ドル。後継のClaude 3.5 Haikuでも1ドルに収まる
    • タスクに対して十分な精度 — 記事の選定と要約にはHaikuクラスで十分な品質が出る
    • 応答が速い — 収集から配信までのパイプライン全体を高速に回せる

    さらに高性能なモデルを使うことも検討した。ただ、キュレーションは判断の正確さより大外しをしない安定感が求められるタスクだ。コスト対効果でHaikuがベストバランスだった。

    運用で得た知見 ― ハルシネーション対策

    LLMキュレーションで最も注意すべき点がハルシネーションだ。

    弊社の運用でも、ソース情報がURLのみの場合にLLMが記事内容を推測し、実際とは異なるサマリーを生成するケースが発生した。たとえばURLのドメインとパス名だけから、おそらくこういう内容だろうと補完してしまう。

    対策として3つの施策を実施した。

    1. メタデータの事前取得

    URLだけでなく、ページタイトルやog:descriptionを事前にフェッチしてLLMに渡す。これだけでハルシネーション率が大幅に下がった。

    def enrich_article_metadata(article):
        """URLからメタデータを取得し、LLMへの入力を充実させる"""
        metadata = fetch_url_metadata(article.url)
        article.title = metadata.get("og:title", article.title)
        article.description = metadata.get("og:description", "")
        return article

    2. アンチハルシネーション指示の追加

    プロンプトに「提供された情報のみに基づいて要約してください。推測や補完は行わないでください」と明記する。LLMは指示があれば分からないと答えられる。指示がなければ補完しようとする。この差は大きい。

    3. 後処理でのバリデーション

    出力結果に対して、元記事のタイトルやキーワードとの整合性をチェックする。完全な自動検証は難しいが、明らかな逸脱は検出できる。

    この3つの対策を組み合わせたことで、キュレーション結果の信頼性が安定した。では、厳選した記事をどうチームに届けるか。


    チャットツールへの配信設計 ― 業務を邪魔しない情報共有

    通知なし配信という設計判断

    配信先としてDiscordとSlackに対応している。ここで最も大事にした設計判断は通知なしで配信することだ。

    技術ニュースは今すぐ読まなければならない情報ではない。手が空いたときに目を通せばいい。それなのに通知が鳴るたびに集中が途切れては本末転倒だ。

    # Discordへの配信(silent=Trueで通知を抑制)
    async def post_to_discord(channel, message):
        await channel.send(
            content=message,
            suppress_embeds=False,
            silent=True  # SUPPRESS_NOTIFICATIONSフラグを設定
        )

    discord.py 2.2以降では silent=True パラメータが使える。内部的には MessageFlags.SUPPRESS_NOTIFICATIONS フラグが設定され、プッシュ通知やデスクトップ通知を抑制する。チャンネル内のメッセージ自体は通常どおり表示される。

    Slackでも同様に、unfurlは有効にしつつメンションや通知トリガーを含めない形で投稿している。

    メッセージの構成

    配信は2段構成にしている。

    1. サマリーメッセージ

    その日の厳選記事の概要を一覧で示す。忙しい日はこれだけ見ればいい。

    本日のテックニュース(2026/02/17)12件
    
    1. Bun 1.3リリース — GC改善でアイドルCPU消費を100分の1に削減
    2. GitHub Copilot Workspace がGA — AIペアプロの新しい形
    3. AWS Step Functions、Express Workflowsの同時実行上限を5倍に拡張
    4. shadcn/ui にRTLサポートが追加
    ...

    2. 個別記事メッセージ

    各記事について、LLMが生成した要約と元記事のURLを投稿する。

    Bun 1.3リリース — GC改善でアイドルCPU消費を100分の1に削減
    
    JavaScriptランタイムBunの1.3がリリースされた。
    GCをイベントループに統合し、アイドル時のCPU消費を
    100分の1に削減。メモリ使用量も40%減少。
    組み込みSQLクライアントやRedisクライアントも追加された。
    
    https://bun.sh/blog/bun-v1.3

    Discord配信時のレート制限対策

    Discordへの配信では、APIのレート制限に気をつける必要がある。短時間に大量のメッセージを送ると制限に引っかかる。メッセージ間に待機時間を入れて回避している。

    # レート制限対策
    for article in selected_articles:
        await post_article_message(channel, article)
        await asyncio.sleep(0.3)  # 300msの間隔を確保

    0.3秒のインターバルは実運用で安定する最小値だった。10〜15件の配信でも合計5秒以内に完了するため、体感上の遅延はない。


    導入効果 ― 数ヶ月の運用を経て

    パイプラインの運用を始めて数ヶ月が経過した。定量と定性の両面から振り返る。

    定量的な効果

    指標導入前導入後
    チーム全体の情報キャッチアップ率まばら毎日15件を全員が閲覧可能
    情報収集にかかる個人の時間30分〜1時間/日ほぼゼロ(閲覧のみ)
    英語記事へのアクセシビリティ個人差あり全員が日本語で閲覧
    運用コスト日次の手動作業完全自動(月1回程度のメンテ)

    とりわけ情報収集にかかる時間の変化が大きかった。1人あたり毎日30分として、5人チームなら月に約50時間の工数削減になる。

    定性的な効果

    雑談の質が変わった。 昨日のニュースで流れてたあのツール、使ってみた? という会話が自然に生まれるようになった。同じ情報を全員が見ているからこそ起きる変化だ。

    技術選定の引き出しが増えた。 新しいOSSやサービスを早い段階で認知できるため、プロジェクトの技術選定で選択肢が広がった。

    英語記事へのハードルが下がった。 翻訳付きで配信されるので、英語が得意でないメンバーも一次情報に触れられるようになった。結果として、チーム内の情報格差が縮まった。

    受動的な学習効果があった。 自分からは検索しなかっただろう分野の情報にも触れる機会が増えた。知らないことを知らない状態が減り、技術的な視野が広がる実感がある。

    予想外だった効果

    導入前はテック系の情報を追うツールという位置づけだった。ところが運用を続けるうちに、チーム内の技術的な共通言語が増えていくという副次的な変化が見えてきた。全員が同じニュースを読んでいるため、議論の前提をそろえやすい。技術的な意思決定のスピードが上がったのは、この共通基盤のおかげだと感じている。


    構築時のつまずきポイントと対処法

    自動化パイプラインの構築は順調ではなかった。実際にぶつかった問題とその対処法を共有する。

    RSSフィードの不安定さ

    RSSフィードは提供元の都合で突然フォーマットが変わったり、配信が止まったりする。テック企業のブログはサイトリニューアルでURLごと変わることがある。

    そのため、フィードごとにエラーハンドリングを入れ、取得失敗時はスキップして他のフィードの処理を続行する設計にした。加えて、フィードの死活監視を週次で走らせ、応答がないフィードを検出できるようにしている。

    LLMの出力形式のブレ

    LLMの出力は毎回微妙に異なる。JSONで返すよう指示しても、Markdownで返ってきたり余計な前置きが付いたりする。

    ここでは、パース処理に柔軟性を持たせることで対処した。正規表現でJSONブロックを抽出する処理に加え、箇条書き形式もフォールバックとして受け付ける多段パーサーを用意している。

    翻訳の精度とコスト管理

    DeepL APIの翻訳は高品質だが、長文記事を丸ごと翻訳するとコストが膨らむ。タイトルとサマリー、最大300文字程度のみを翻訳対象とすることで、品質を保ちつつコストを抑えた。


    予想FAQ

    Q1. パイプライン全体の構築にどのくらい時間がかかりましたか?

    最初のプロトタイプは2日で動いた。ただし、ハルシネーション対策やレート制限への対応、出力パーサーの改善など、安定稼働させるまでの調整に2週間ほどかかっている。

    Q2. LLMのキュレーション精度はどの程度ですか?

    体感では8割以上の満足度だ。なぜこの記事を選んだ? と思うこともたまにあるが、致命的な選定ミスはほぼない。プロンプトの選定基準を具体的に書くほど精度は上がる傾向にある。

    Q3. DeepL API以外の翻訳手段も検討しましたか?

    Google Cloud Translation APIとLLMによる翻訳を比較した。Google翻訳はコストが安いが技術用語の訳が不自然なケースがあった。LLM翻訳は品質が高いがコストとレイテンシに課題があった。総合的にDeepLのバランスが良かった。

    Q4. フィードの追加や削除はどう管理していますか?

    フィード一覧はYAMLファイルで管理している。追加や削除はYAMLを編集してデプロイするだけで反映される。月に1回程度、フィードの見直しを行い、ノイズが多いフィードの閾値調整や新規フィードの追加を実施している。

    Q5. 他のチャットツールにも対応できますか?

    配信部分はチャットツールごとにアダプタを実装する設計にしている。現在はDiscordとSlackに対応しているが、Microsoft TeamsやGoogle Chatへの対応もアダプタの追加だけで可能だ。

    Q6. 類似のツールやサービスとの違いは?

    RSSリーダーにAIフィルタを組み合わせたサービスは増えている。Feedly AIやInoreaderのAIアシスタントなどが代表的だ。ただ、チーム固有の選定基準をプロンプトで細かく制御できる点、配信先やフォーマットを自由にカスタマイズできる点が自前パイプラインの強みになる。既製品で足りるケースももちろんあるので、チームの規模や要件に合わせて選ぶとよい。


    まとめ

    RSS収集、LLMキュレーション、チャット配信。この3つのフェーズを組み合わせて、情報収集パイプラインを全自動化した。

    技術的に特別なことはしていない。RSSパーサー、翻訳API、LLM API、チャットBot。既存の技術を組み合わせただけだ。ただ、これらを1つのパイプラインとしてつなぎ、日次で安定稼働させることで、チーム全体の情報力を底上げできた。

    今後は収集した情報をナレッジベースとして蓄積する仕組みや、社内のQ&Aシステムとの連携にも取り組んでいく。

  • AppRun入門 共用型・専有型の違いとプライベート環境の構築手順

    AppRun入門 共用型・専有型の違いとプライベート環境の構築手順

    AppRunとは何か

    さくらインターネットが2025年12月9日に正式リリースしたAppRunは、コンテナイメージを渡すだけでアプリを公開できるマネージドサービスだ。裏側ではKubernetesとKnativeが動いている。Knativeは元々Google CloudがCloud Runの基盤として開発し、2021年にCloud Native Computing Foundation(CNCF)へ寄贈されたオープンソースだ。

    ただし利用者がKubernetesやKnativeを直接触る場面はない。具体的には、コンテナイメージとポート番号を指定するだけで、ロードバランシング、オートスケール、TLS終端までAppRunが処理してくれる。つまり、インフラ構築や運用にかける時間を削り、アプリ開発に集中できるわけだ。

    この記事では以下の内容を扱う。

    • 共用型と専有型の料金・機能比較
    • 専有型でSEGを使ったプライベート環境の構築手順
    • apprun-cliによるCI/CDパイプラインへの組み込み方法
    • AWS App RunnerやCloud Runとの違い

    それでは、AppRunが用意している2つのプランから見ていこう。

    この記事の前提条件

    手順を進めるにあたり、以下の準備が必要になる。

    • さくらのクラウドのアカウントを持っていること。コントロールパネルの基本操作に不安がある場合は先にそちらを確認してほしい
    • Docker がローカル環境にインストールされていること
    • コンテナの基本的な概念(イメージ、レジストリ、ポートマッピング)を理解していること
    • 専有型を試す場合はサービスプリンシパルの作成権限が必要。IDポリシーの設定方法についてはこちらの記事で詳しく解説している

    共用型と専有型を比較する

    AppRunには共用型専有型がある。それぞれの違いを以下の表にまとめた。

    項目共用型専有型
    実行基盤共有クラスタ上でコンテナが動作専有VM上でコンテナが動作
    ゼロスケール対応。リクエストがなければインスタンス0になる非対応
    プライベートネットワーク接続非対応SEG経由で対応
    料金目安(税込)月額約3,720円〜(0.5vCPU / 1GiBメモリ)月額約11,000円〜(1vCPU / 2GBメモリ)
    ユースケースWebアプリ、API、検証環境業務システム、DB接続が必要な構成

    共用型はコストを抑えたい場面に向く

    共用型の最大の強みはKnativeベースのゼロスケールだ。アクセスがない時間帯にインスタンスが0になり、その間は課金されない。そのため、個人開発やトラフィックの読めない初期サービスに適している。

    料金体系は1時間単位の従量課金で、最小構成なら約5円/時間だ。月額に直すと約3,720円からスタートできるため、検証用途でも手を出しやすい価格帯といえる。

    とはいえ、ゼロスケールにはデメリットもある。インスタンスが0の状態からリクエストを受けると、コンテナの起動が完了するまで数秒から十数秒の待ち時間(コールドスタート)が発生する。レスポンス速度が求められるサービスでは、最小インスタンス数を1以上に設定しておくほうが安全だ。

    もう1つ注意したいのが、共用型ではプライベートネットワーク接続ができない点だ。クラウド上のデータベースやストレージへの通信はすべてインターネット経由になる。DB接続を含む構成では、次に紹介する専有型を選ぶ必要がある。

    専有型はネットワーク分離が必要な環境向け

    一方で、専有型ではユーザー専用のVMが割り当てられる。他のユーザーとリソースを共有しないため、パフォーマンスが安定しやすい点が特徴だ。

    最小構成は1vCPU/2GBメモリで月額約11,000円、最大8vCPU/8GBメモリまでスケールアップできる。しかし専有型を選ぶ最大の理由は、スペックよりもプライベートネットワーク接続だろう。さくらのクラウドのスイッチにサービスエンドポイントゲートウェイ(SEG)を介して接続することで、クラウド上のデータベースやストレージへインターネットを経由せずプライベートIPで直接通信できる。

    したがって、エンタープライズシステムや自治体業務のように外部へトラフィックを流したくないケースでは、この仕組みが不可欠になる。国産クラウドとしてのセキュリティ基盤についてはさくらのクラウドのSOC2 Type1取得に関する記事でも解説しているので、あわせて確認してほしい。

    専有型でプライベート環境を構築する5ステップ

    ここからは専有型AppRunで、プライベートなコンテナ実行環境を組み立てる流れを解説する。共用型にはない設定が複数あるため、順を追って進めよう。全体像としては、ネットワーク準備 → 認証設定 → クラスター起動 → イメージ準備 → 動作確認という流れになる。

    なお、構築中に詰まりやすいポイントについてはAppRun専有型で起動しない場合の対処法にもまとめているので、トラブル時にはそちらも参照してほしい。

    ステップ1 — スイッチを作成しSEGを有効化する

    まず、さくらのクラウドコントロールパネルでスイッチを作成する。次に、そのスイッチの詳細画面にあるサービスエンドポイントゲートウェイタブからSEGを有効化する。

    SEGはプライベートネットワーク内のリソースから、さくらのクラウドのマネージドサービスへ安全に接続するための仕組みだ。ここでIPアドレスとネットマスクを設定し、コンテナレジストリとAppRun専有型コントロールプレーンへの通信を許可しておく必要がある。

    この設定を忘れると、ワーカーノードがコンテナイメージを取得できなくなる。見落としやすいポイントなので、他の設定より先に済ませておきたい。

    ステップ2 — サービスプリンシパルを作成する

    クラスターの起動には適切な権限が必要だ。そこで、さくらのクラウドのIAM機能でサービスプリンシパルを作成し、「さくらのクラウド 作成・削除」の権限を付与する。

    サービスプリンシパルとは、リソースを自動管理するための認証情報のことだ。具体的には、AppRun専有型がワーカーノードの増減やネットワーク設定を行うときにこの認証情報が使われる。なお、IAMの権限管理に不慣れな場合は、先にIDポリシーの設定方法を確認しておくことをすすめる。

    ステップ3 — クラスターとワーカーノードを起動する

    AppRun専有型の管理画面からクラスターを作成する。続いてオートスケーリンググループを追加し、ワーカーノードを起動する。

    このとき重要なのはNIC設定だ。ステップ1で作成したSEG有効化済みスイッチを選び、ネームサーバにはSEGのIPアドレスを指定する。この接続設定により、ワーカーノードがプライベートネットワーク経由でコンテナレジストリや管理サーバーと通信できるようになる。

    CPUとメモリはアプリの負荷に合わせて選べばよい。初回の検証であれば最小構成で十分だ。

    ステップ4 — コンテナイメージを用意してデプロイする

    現時点でAppRunはさくらのクラウドのコンテナレジストリ専用となっている。そのため、Docker Hubなど外部レジストリから直接pullしてデプロイすることはできない。外部レジストリへの対応はロードマップに入っているものの、具体的な時期は未定とされている。

    外部のベースイメージを使いたい場合は、ローカルでpullしてからさくらのコンテナレジストリに再pushする方法を取る。以下にNode.jsアプリの例を示す。

    FROM node:20-slim
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --production
    COPY . .
    EXPOSE 8080
    CMD ["node", "server.js"]

    また、AppRunはx86_64アーキテクチャで動作する点にも注意が必要だ。Apple Silicon搭載のMacでビルドする場合は --platform linux/amd64 を明示的に付けなければならない。

    # ビルドしてさくらのコンテナレジストリにpush
    docker build --platform linux/amd64 -t my-app.sakuracr.jp/my-app:latest .
    docker login my-app.sakuracr.jp
    docker push my-app.sakuracr.jp/my-app:latest

    イメージのpushが完了したら、ロードバランサをステップ1のスイッチに接続し、アプリケーションを追加する。コンテナポート8080を外部ポート80にマッピングし、ホスト名を指定すれば準備完了だ。

    ステップ5 — 接続確認と動作検証

    デプロイ後は、プライベートセグメント経由でDBや他のリソースに到達できるか確認する。接続先のプライベートIPは環境変数で渡すと管理しやすい。

    外部DNSサービスでAレコードを登録すれば、指定したホスト名で外部からアクセスできるようになる。

    確認時に起動しない、通信できないといったトラブルが起きた場合は、まずSEGの設定とNIC設定を見直そう。よくある原因と対処法はこちらの記事にまとめている。

    運用で押さえておきたいポイント

    構築が完了したら、運用フェーズで気をつけるべきことを整理しておこう。

    コンテナイメージの更新フロー

    アプリを更新するたびにコンテナレジストリへの再pushが必要になる。手動運用ではミスが起きやすいため、後述するapprun-cliを使ったCI/CDパイプラインの構築を早い段階で検討したい。

    オートスケールの挙動

    専有型でもCPU使用率をベースにしたオートスケールが利用できる。トラフィック増加時には新しいインスタンスが自動的に立ち上がり、負荷を分散してくれる。ただし共用型と異なり、インスタンスを0にするゼロスケールは専有型では動作しない。常に最低1台のインスタンスが稼働し続ける点を理解しておこう。

    監視とログ

    AppRunのコンソールからコンテナのログを確認できる。本番運用では外部の監視ツールと組み合わせ、レスポンスタイムやエラー率を継続的に観測する体制を整えておくと安心だ。

    apprun-cliでデプロイを自動化する

    コントロールパネルでの手動操作に加え、CLIツールのapprun-cliも利用できる。CI/CDパイプラインに組み込む場合はこちらのほうが扱いやすい。

    apprun-cliはfujiwaraさんが開発した非公式ツールで、Homebrewまたはgo installでインストールできる。

    # Homebrewでインストール
    brew install fujiwara/tap/apprun-cli
    
    # または go install
    go install github.com/fujiwara/apprun-cli/cmd/apprun-cli@latest

    このツールはアプリケーション定義をJSONまたはJsonnet形式のファイルで管理する仕組みだ。既存のアプリケーション設定をエクスポートするには、以下のコマンドを実行する。

    # 既存アプリケーション設定をJsonnetファイルに出力
    apprun-cli init --name my_web_app --jsonnet > myapp.jsonnet

    設定ファイルを編集したら、deployコマンドで反映する。

    # 設定ファイルを指定してデプロイ
    apprun-cli deploy --app myapp.jsonnet

    Jsonnet形式を選ぶメリットは、テンプレート機能を活用できる点だ。例えば、must_env() 関数で環境変数を読み込めるため、シークレット情報をファイルに直接書かずに済む。さらに tfstate() でTerraformの状態を参照したり、secret_value() でSecret Managerにアクセスしたりも可能だ。

    GitHub ActionsやGitLab CIのステップに組み込めば、pushのたびにAppRunへ自動デプロイする仕組みが構築できる。なお、アプリケーションが見つからない場合、deployコマンドは新規作成として処理される。つまり、初回も更新も同じコマンドで済む点が運用を楽にしてくれる。

    GitHub Actionsでの設定例

    参考として、GitHub Actionsでapprun-cliを使う場合の基本的なワークフロー構成を示す。

    name: Deploy to AppRun
    on:
      push:
        branches: [main]
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
    
          - name: Build and push image
            run: |
              docker build --platform linux/amd64 -t my-app.sakuracr.jp/my-app:${{ github.sha }} .
              echo "${{ secrets.SAKURA_CR_PASSWORD }}" | docker login my-app.sakuracr.jp -u ${{ secrets.SAKURA_CR_USER }} --password-stdin
              docker push my-app.sakuracr.jp/my-app:${{ github.sha }}
    
          - name: Install apprun-cli
            run: |
              go install github.com/fujiwara/apprun-cli/cmd/apprun-cli@latest
    
          - name: Deploy
            env:
              SAKURA_APPRUN_TOKEN: ${{ secrets.SAKURA_APPRUN_TOKEN }}
              IMAGE_TAG: ${{ github.sha }}
            run: |
              apprun-cli deploy --app myapp.jsonnet

    この例ではmainブランチへのpushをトリガーに、イメージのビルド・push・デプロイを自動実行している。must_env() を使えば、Jsonnet内で IMAGE_TAG などの環境変数を安全に参照できる。

    AWS App RunnerやCloud Runとの違い

    名前の似たサービスが海外クラウドにもある。主要な違いを表にまとめた。

    サービス提供元ゼロスケールプライベートネットワーク料金の特徴
    AppRunさくらインターネット共用型のみ対応専有型でSEG経由対応円建て。共用型は月額約3,720円〜
    AWS App RunnerAWS非対応(最低1インスタンス常駐)VPCコネクタで対応ドル建て。待機中もvCPU/メモリ課金あり
    Cloud RunGoogle Cloud対応VPCコネクタ / Direct VPCで対応ドル建て。リクエスト単位の従量課金

    ゼロスケール対応ではAppRun共用型とCloud Runが並ぶ。一方で、AWS App Runnerは最低1インスタンスが常駐するため、待機中もコストが発生し続ける。

    AppRunを選ぶ理由は大きく3つある。国内データセンターで動作すること、円建てで請求が届くこと、そしてさくらのクラウドの既存リソースとプライベート接続できることだ。

    データの国内保管が求められる案件や、ガバメントクラウドを見据えた自治体向けシステムでは、この3点が決定打になる。すでにさくらのクラウドを運用している環境なら、追加のネットワーク設計も最小限で済む。

    ただし、グローバル展開やマルチリージョン構成が必要な場合はAWSやGoogle Cloudのほうが選択肢は多い。結局のところ、用途に応じた使い分けが現実的だ。

    よくある質問

    Q1. 共用型と専有型、最初はどちらを選ぶべきか

    迷ったら共用型から始めるのがよい。ゼロスケールにより待機コストがかからないため、検証や小規模サービスに適している。その後、プライベートネットワーク接続やリソースの分離が必要になった時点で、専有型へ切り替えれば問題ない。

    Q2. 手元のDockerイメージをそのまま使えるか

    直接は使えない。現時点ではさくらのコンテナレジストリのみに対応しているため、手元やDocker Hubのイメージは一度さくらのコンテナレジストリにpushし直す必要がある。加えて、x86_64(linux/amd64)でビルドされている必要があるので、Apple Silicon搭載のMacで作ったイメージはそのままでは動かない。ビルド時に --platform linux/amd64 を付けよう。

    Q3. 独自ドメインで公開できるか

    専有型ではホスト名を指定し、外部DNSサービスでAレコードを登録すれば独自ドメインで公開できる。一方で、共用型にはAppRun単体での独自ドメイン機能がない。ただし、さくらのウェブアクセラレータを前段に置くことで対応は可能だ。その際、リクエスト先をHostヘッダで判別するため、ウェブアクセラレータ側のHostヘッダ設定に注意が必要になる。

    Q4. ゼロスケールからの起動にどのくらいかかるか

    コンテナイメージのサイズや起動処理によって変わるが、目安は数秒から十数秒だ。コールドスタートを避けたいなら最小インスタンス数を1以上に設定すればよい。ただし、その分は常時課金されるので、コストとのトレードオフになる。アクセス頻度やレスポンス要件に応じて判断してほしい。

    Q5. 専有型でもオートスケールは効くか

    効く。CPU使用率をベースにコンテナインスタンスを自動で増減する仕組みが備わっている。トラフィック増加時には新しいインスタンスが自動的に立ち上がり、負荷を分散してくれる。ただし、インスタンスを0にするゼロスケールは専有型では使えない点に注意してほしい。

    Q6. SEGの設定を忘れるとどうなるか

    ワーカーノードがAppRun管理サーバーやコンテナレジストリと通信できなくなる。結果として、コンテナイメージの取得もクラスタの正常運用もできない。専有型を使うなら、SEGの有効化はスキップできない手順だ。

    まとめ

    AppRunはコンテナデプロイの手間を削り、インフラ管理から開発者を解放するサービスだ。共用型のゼロスケールは待機コストをゼロに抑え、専有型のプライベートネットワーク接続はセキュリティ要件の厳しい環境に応える。

    専有型の構築ではSEGの設定が鍵になる。この記事で紹介した5ステップを押さえておけば、プライベートなコンテナ実行環境を迷わず立ち上げられるはずだ。加えて、apprun-cliを導入すればCI/CDパイプラインによる自動デプロイも実現できる。

    さくらのクラウドの他の機能と組み合わせることで、よりセキュアで運用しやすいインフラを構築できる。関連記事もあわせて確認してほしい。

    参考リンク

  • Thesys Agent Builderで始めるGenerative UI:AIがUIを動的生成する仕組み

    Thesys Agent Builderで始めるGenerative UI:AIがUIを動的生成する仕組み

    チャットボットの応答がテキストだけでは足りない場面

    AIチャットボットの返答は、ほとんどテキストだ。Markdownで整えた文章、コードブロック、リンク。それで済む場面もある。ただ、ユーザーが欲しいのは長い説明文ではなく、その場で触れるインターフェースだったりする。

    たとえばフライトを探す質問を投げたとしよう。候補を箇条書きで返されても、別の画面に移って比較しなければならない。チャット内にフィルター付きの検索カードやカレンダーがそのまま出てきたら、操作が一画面で完結する。

    この発想を形にした技術がGenerative UIだ。LLMがテキストの代わりにUIコンポーネントを生成し、ユーザーはチャット画面の中でそのまま操作できる。2025年11月にはGoogleもGenerative UIに関する研究を公開した。LLMが生成したUIは標準的なテキスト出力より強く好まれるという評価結果が示されている。

    では、このGenerative UIをAPIとSDKで手軽に実装できるThesysのAgent Builderを掘り下げていく。

    Thesysとはどんな会社か

    Thesysはサンフランシスコ拠点のスタートアップだ。共同創業者はRabi Shanker GuhaとParikshit Deshmukh。2024年11月にTogether Fundがリードし、8vcが参加する形で400万ドルのシード資金を調達した。300を超えるチームがすでにThesysのツールを本番環境で利用している。

    主力製品のAgent Builderは、AIの応答にインタラクティブなReactコンポーネントを埋め込める。テキストを返すだけでなく、ボタン・フォーム・カード・グラフを動的に生成して返せる点で、従来のチャットボットビルダーとは方向性が違う。加えてノーコードのAgent Builder UIも提供しており、コードを書かずにGenerative UIアプリを構築することも可能だ。

    ThesysはさらにCopilotKitが開発するAG-UIプロトコルのエコシステムにも参加している。AG-UIはAIエージェントとフロントエンドのリアルタイム通信を標準化するオープンソースのプロトコルだ。MCPがツール呼び出しを標準化するのに対し、AG-UIはエージェントからUIへのイベント配信を標準化する。GoogleやLangChain、AWS、Microsoftなども採用しており、エージェントUIの共通基盤として急速に広まっている。

    公式サイト: https://www.thesys.dev/

    Agent Builderを構成する2つの軸

    Agent Builderの核はC1 APIGenUI SDKだ。それぞれの役割を見ていく。

    C1 API ― OpenAI互換で移行コストが低い

    C1 APIはOpenAIのChat Completions APIと互換性がある。すでにOpenAI向けのコードがあるなら、base_urlとモデル名を差し替えるだけで動く。

    import openai
    import os
    
    client = openai.OpenAI(
        api_key=os.environ.get("THESYS_API_KEY"),
        base_url="https://api.thesys.dev/v1/embed"
    )
    
    response = client.chat.completions.create(
        model="c1/anthropic/claude-sonnet-4/v-20250930",
        messages=[
            {"role": "system", "content": "フライト検索アシスタント。結果はカード形式のUIで表示する。"},
            {"role": "user", "content": "東京からニューヨークへの来週のフライトを探して"}
        ]
    )

    ベースURLはhttps://api.thesys.dev/v1/embedを指定する。モデル名はc1/{プロバイダー}/{モデル名}/{バージョン}という命名規則だ。レスポンスにはテキストだけでなく、UIコンポーネントの構造化データも含まれる。APIコール数とLLMトークン消費量がそれぞれ課金対象になる。

    GenUI SDK ― 生成されたUIをReactで描画する

    APIが返したUI記述を画面に表示するのがGenUI SDKの役割だ。内部的にはThesysのオープンソースUIフレームワークCrayonをベースにしている。CrayonはGitHubでMITライセンスのもと公開されており、エージェントUIを構築するための拡張可能なReactコンポーネント群を提供する。

    npm install @thesysai/genui-sdk
    import { C1Chat } from "@thesysai/genui-sdk";
    import "@crayonai/react-ui/styles/index.css";
    
    export default function App() {
      return (
        <C1Chat
          apiUrl="/api/chat"
          agentName="フライト検索アシスタント"
          formFactor="full-page"
        />
      );
    }

    C1Chatコンポーネントを配置するだけで、Generative UI対応のチャット画面が完成する。AIが返したボタン、フォーム、カード、テーブル、グラフがチャット内にそのまま描画される。テーマのカスタマイズにはCrayonのプリセットも使える。

    Generative UIが威力を発揮する3つのシーン

    テキスト応答だけでは体験が不十分になるケースを具体的に見てみよう。

    Eコマースの商品提案

    ユーザーが「赤いスニーカーを探している」と入力すると、商品画像つきのカードが並ぶ。サイズ選択やカート追加のボタンもUI内に表示されるため、チャットを離れずに購入まで完了できる。テキストで商品リストを返される場合と比べると、コンバージョンまでのステップが大幅に減る。

    ダッシュボード型のレポート

    「先月の売上を見せて」と聞けば、グラフやテーブルが直接返ってくる。フィルタを操作して期間やカテゴリを切り替えることもできる。数字の羅列を読み解くより、触れるグラフのほうが判断は速い。意思決定のスピードを上げたい経営層やアナリストに特に響くユースケースだ。

    フォーム入力の段階的ガイド

    問い合わせ対応のAIが、ユーザーの回答に応じてフォームを動的に組み立てる。全項目を一度に見せるのではなく、必要な入力だけを順番に提示する。この段階的な構成は離脱率を下げやすい。

    競合ツールとの違いを整理する

    Agent Builderに近い領域のツールを並べてみた。それぞれ得意分野が異なる。

    ツール特徴UI生成価格帯
    Thesys Agent BuilderGenerative UI。OpenAI互換API。GenUI SDKAIがUIコンポーネントを動的生成無料枠あり($10クレジット付)。従量課金
    Voiceflowノーコードのチャットボットビルダー。フローチャート式設計テンプレートUIのみ無料のStarterプランあり。Pro月額$60〜
    Botpressチャットボットプラットフォーム。オープンソース版ありカスタムUIは手動で構築無料枠あり。Plus月額$79〜
    DifyLLMアプリケーション構築プラットフォームテキスト中心。UI生成は非対応セルフホスト無料。クラウド版Professional $59〜

    Agent Builderが際立つのは、AIが応答のたびにUIを動的に生成する点だ。VoiceflowやBotpressはチャットウィジェットやテキスト応答が中心で、UIコンポーネントの自動生成には対応していない。Difyはワークフロー構築に強いが、フロントエンドのUI生成は守備範囲外になる。

    一方で、Agent Builderはまだ新しいプロダクトだ。エコシステムの厚みやコミュニティ規模ではVoiceflowやBotpressに及ばない。ノーコードで素早くチャットボットを立ち上げたい場合や、CMS連携を優先したい場合は、既存のビルダーのほうが向いている。

    料金体系を確認する

    Agent Builderには無料のEssentialsプランがある。$10分のクレジットが付与され、C1 APIとGenUI SDKの主要機能を試せる。クレジットを使い切った後は従量課金に移行する。

    課金はAPIリクエスト数とLLMトークン消費量の二軸で計算される。APIコールはプランに含まれる分があり、LLMトークンはプロバイダーごとの単価で別途発生する。チーム向けのTeamプランやエンタープライズ向けのプランも用意されている。最新の詳細は公式の料金ページで確認してほしい。

    導入までの最短ステップ

    実際に動かすまでの流れを整理しておく。

    1. Thesysコンソールでアカウントを作成し、APIキーを取得する
    2. バックエンド側でOpenAI SDKを使い、base_urlhttps://api.thesys.dev/v1/embedを設定する
    3. フロントエンド側で@thesysai/genui-sdkをインストールし、C1Chatコンポーネントを配置する
    4. システムプロンプトでUIの出力形式を指示し、レスポンスを確認する

    ノーコードで試したい場合はAgent Builder UIから直接エージェントを作成し、データを接続してPublishするだけで公開できる。

    よくある質問

    Q1. Generative UIとは何か

    LLMがテキストではなく、ボタン・フォーム・カード・テーブル・グラフといったUIコンポーネントを動的に生成して返す技術だ。ユーザーはチャット内でそのUIを直接操作できる。Googleも2025年11月にGemini 3へのGenerative UI実装を発表しており、業界全体で注目が高まっている。

    Q2. React以外のフレームワークでも使えるか

    C1 APIはREST APIなので、バックエンドはどの言語からでも呼べる。ただしUIのレンダリングにはGenUI SDKが必要で、現時点ではReactとNext.jsが前提になる。

    Q3. 日本語のプロンプトや応答に対応しているか

    C1 APIのバックエンドLLMは多言語に対応しており、日本語のプロンプトと応答は動作する。ただし公式ドキュメントは英語のみだ。

    Q4. OpenAIのコードからの移行は簡単か

    base_urlとモデル名を変えるだけで基本的な移行はできる。Generative UI固有の機能を活かすなら、システムプロンプトの調整やUI関連のパラメータ設定が追加で必要になる。

    Q5. セルフホストは可能か

    現時点ではクラウドサービスのみの提供で、セルフホストには対応していない。ただしCrayonフレームワーク自体はMITライセンスのオープンソースとしてGitHubで公開されている。

    Q6. AG-UIプロトコルとの関係は

    AG-UIはCopilotKitが開発したオープンソースのプロトコルで、AIエージェントとフロントエンドのリアルタイム通信を標準化する。ThesysはこのAG-UIプロトコルを統合しており、C1 APIと補完的な関係にある。LangGraph、CrewAI、Mastra、PydanticAIなど多くのフレームワークがAG-UIをサポートしている。

    Q7. Generative UIと従来のチャットUIは何が違うのか

    従来のチャットUIは、あらかじめ定義されたテンプレートの範囲で応答を表示する。Generative UIはLLMがコンテキストに応じてUIの構造自体を動的に決定する。ユーザーの質問内容やデータの性質に合わせて、テーブル、グラフ、フォームなど最適な表現を自動で選べる点が根本的に異なる。

    まとめ

    Generative UIは、チャットボットの応答をテキストからインタラクティブなUIへ押し上げる技術だ。ThesysのAgent Builderは、OpenAI互換のC1 APIとReact向けGenUI SDKでこの技術を実用レベルで提供している。

    無料枠があるので、小さなプロトタイプから試せる。自社のチャットボットにUI生成機能を加えたいなら、選択肢に入れておいて損はない。

    参考リンク

  • Happy Coder導入ガイド — スマホからClaude Codeを操作する方法

    Happy Coder導入ガイド — スマホからClaude Codeを操作する方法

    Happy Coder導入ガイド — スマホからClaude Codeを操作する方法

    対象読者

    • Claude Codeを日常的に使っていて、外出先からもセッションを操作したい開発者
    • AIコーディングアシスタントに興味があり、モバイルワークフローを試したいエンジニア
    • リモートワークやフリーランスで、場所を選ばず開発を続けたい人
    • チームリーダーやテックリードで、移動中にAIエージェントの進捗を確認したい人

    前提知識:ターミナル操作の基本、Node.jsの基礎知識、Claude Codeの基本的な使い方


    Happy Coderとは

    Happy Coderは、Claude CodeやCodexをスマホやWebブラウザから操作できる無料・オープンソースのモバイルクライアントです。Bulka, LLCが開発し、2025年5月にリリースされました。

    GitHubスターは約12,100(2025年6月時点)。App Storeの評価は4.9/5(467件のレビュー)で、短期間のうちに支持を集めています。

    ターミナルで動いているClaude Codeのセッションを、そのままスマホに映し出せるのが基本的な仕組みです。メッセージの送信やファイルの確認、権限の承認まで、すべてモバイルからできます。

    PCの前にいなくてもClaude Codeを使いたい場面

    Claude Codeはターミナルベースのツールなので、PCがないと操作できません。ただ、実際の開発現場ではこういう場面がよくあります。

    • 大規模なリファクタリングをClaude Codeに任せたが、完了通知をスマホで受け取りたい
    • 通勤中の電車で、コードレビューの指示を出したい
    • ミーティング中に、バックグラウンドで動いているエージェントの進捗を確認したい
    • カフェでスマホだけで簡単な修正を指示したい

    Happy Coderはこうした場面をエンドツーエンド暗号化つきでカバーします。ユーザーからは「スマホだけで2本の技術記事を20〜30分で執筆できた」「PCワークフローと同等の速度を実現できる」といった声も上がっています。


    Happy Coderの7つの機能

    1. リアルタイム双方向同期

    PCのClaude Codeセッションとモバイルアプリがリアルタイムで同期します。ターミナルの出力はミリ秒単位でスマホに表示され、スマホから送ったメッセージもすぐにClaude Codeに届きます。

    PCとスマホに主従関係はありません。どちらのデバイスからでも会話を開始し、メッセージの送受信ができます。

    2. エンドツーエンド暗号化

    通信はすべてQRコードで共有した秘密鍵で暗号化されます。AES暗号化と公開鍵認証を組み合わせた方式です。

    中継サーバーは暗号化されたデータを転送するだけ。サーバー側でコードの内容を読み取ることはできません。

    3. マルチセッション管理

    複数のClaude Codeセッションを同時に実行し、それぞれ独立して管理できます。たとえばフロントエンドとバックエンドで別々のセッションを立ち上げ、スマホから切り替えながら操作する使い方が可能です。

    各セッションは独立した状態を持ち、プロジェクトコンテキストや会話履歴も個別に保持されます。アプリを再起動してもセッションは維持されます。

    4. プッシュ通知

    タスクの完了、エラーの発生、権限の確認要求など、重要なイベントをプッシュ通知で受け取れます。長時間のタスクを実行中でも、スマホを見るだけで状況がわかります。

    通知にはディープリンクが含まれており、タップすると該当セッションの該当箇所に直接ジャンプできます。

    5. 音声エージェント

    音声でClaude Codeに指示を出せます。単なる音声認識ではなく、ElevenLabsの音声認識・合成技術を統合しており、AIが音声の意図を解釈してコード実行要求に変換します。

    「テストを全部実行して」と話しかけるだけで、適切なコマンドが実行される仕組みです。

    6. カスタムエージェント・スラッシュコマンド対応

    ~/.claude/agents/ に配置したカスタムエージェントや、独自のスラッシュコマンドもモバイルからそのまま使えます。デスクトップで使える機能はすべてモバイルでも利用可能です。

    MCP(Model Context Protocol)ツールの権限プロンプトにも対応しています。JIRA、GitHub等の外部サービスとの連携時に、モバイル上で許可・拒否を選択できます。

    7. 完全無料・オープンソース

    MITライセンスで公開されており、テレメトリーやトラッキングは一切ありません。ソースコードはGitHubで確認できます。TypeScript 97.7%で構成されており、コミュニティによる活発な開発が続いています。


    仕組み(アーキテクチャ)

    Happy Coderは3つのコンポーネントで構成されています。

    ┌──────────────┐     暗号化データ     ┌──────────────┐     暗号化データ     ┌──────────────┐
    │              │ ──────────────────▶ │              │ ──────────────────▶ │              │
    │   CLI        │                     │  Relay       │                     │  Mobile App  │
    │  (PC上で実行) │ ◀────────────────── │  Server      │ ◀────────────────── │  (スマホ)     │
    │              │                     │ (中継のみ)    │                     │              │
    └──────────────┘                     └──────────────┘                     └──────────────┘
           │                                    │                                    │
      Claude Codeを                     暗号化blobを                          復号して
      監視・暗号化                       保存・転送                           表示・操作

    中継サーバーが必要な理由

    スマホとPCは通常、別のネットワーク上にあるため直接通信できません。中継サーバーが両方のデバイスからの接続を受け付ける役割を果たします。ポート開放やIPアドレスの設定は不要です。

    中継サーバーにはオフラインファースト設計が採用されています。スマホの接続が一時的に途切れても、サーバーが暗号化データを保持し、接続が回復すると見逃した更新分が自動的に同期されます。

    セキュリティの流れ

    1. QRコードスキャンで共有秘密鍵を交換(サーバーには渡らない)
    2. CLIがClaude Codeの出力を秘密鍵で暗号化
    3. 暗号化されたデータを中継サーバーに送信
    4. サーバーはデータを保存・転送(中身は読めない)
    5. スマホが受信し、秘密鍵で復号して表示

    サーバーを経由しても通信内容が漏れることはありません。


    Happy Coderの導入 6ステップ

    前提条件

    • Node.js 20.0.0以上がインストールされていること
    • Claude Code がインストール済みで、認証が完了していること
    • スマートフォン(iOS または Android)

    Step 1:Claude Codeのインストール(未導入の場合)

    Claude Codeをまだ入れていなければ、先にセットアップします。

    macOSの場合はHomebrewが手軽です。

    brew install claude-code

    ほかの環境ではClaude Code公式ドキュメントを参照してください。

    インストール後、初回起動で認証を行います。

    claude

    認証が完了したら Ctrl+C でいったん終了します。

    注意: 従来の npm install -g @anthropic-ai/claude-code は廃止予定です。Homebrewまたは公式インストーラーを使ってください。

    Step 2:Happy Coder CLIのインストール

    npmでグローバルインストールします。

    npm install -g happy-coder

    バージョンが表示されればOKです。

    happy --version

    Step 3:モバイルアプリのインストール

    お使いのスマートフォンにHappy Coderアプリを入れます。

    Webブラウザから使いたい場合は app.happy.engineering にアクセスする方法もあります。ただしプッシュ通知を受け取りたい場合はネイティブアプリがおすすめです。

    Step 4:CLIを起動してQRコードを表示

    ターミナルで以下を実行します。

    happy

    初回起動時に以下が自動的に行われます。

    1. Claude Codeの認証チェック
    2. 暗号化キーペアの生成
    3. QRコードの表示(ターミナル上に出ます)

    ここで表示されるQRコードが、デバイス間の暗号化通信の鍵になります。

    Step 5:QRコードをスキャンしてペアリング

    1. スマホでHappy Coderアプリを開く
    2. 「Connect」または「ペアリング」をタップ
    3. ターミナルに表示されたQRコードをスマホカメラでスキャン
    4. 接続が確立されると、アプリにClaude Codeのセッションが表示される

    この操作で暗号化の秘密鍵がスマホに安全に転送されます。QRコードには接続情報と暗号鍵が含まれており、中継サーバーには一切送信されません。

    注意: QRコードのスクリーンショットは他人に共有しないでください。暗号鍵が含まれているため、第三者にセッションへのアクセス権を与えてしまいます。

    Step 6:動作確認

    接続が完了したら、以下を確認します。

    1. PC側: ターミナルでClaude Codeが通常通り動作していること
    2. スマホ側: アプリにClaude Codeの出力がリアルタイムで表示されていること
    3. 双方向テスト: スマホからメッセージを送信し、PCのClaude Codeに届くこと

    ここまでの所要時間は約10分です。


    基本的な使い方

    セッションの開始

    # Claude Codeセッションを開始(デフォルト)
    happy
    
    # モデルを指定して開始
    happy -m opus
    
    # 権限モードを指定して開始
    happy -p plan
    
    # 環境変数を渡して開始
    happy --claude-env SOPS_AGE_KEY_FILE=keys.txt

    Codexモードで使う

    OpenAI Codexも同様にモバイルから操作できます。

    happy codex

    Geminiモードで使う

    Google Geminiにも対応しています。初回のみ認証が必要です。

    # 初回認証
    happy connect gemini
    
    # セッション開始
    happy gemini
    
    # モデルを指定
    happy gemini model set gemini-2.5-pro

    その他のCLIコマンド

    happy auth      # 認証の管理
    happy doctor    # 診断の実行(問題が起きた場合)
    happy daemon    # バックグラウンドサービスの管理
    happy notify    # プッシュ通知のテスト送信

    Happy Coderの実践シーン

    シーン1:長時間タスクのモニタリング

    大規模なリファクタリングやテスト実行をClaude Codeに任せ、プッシュ通知で完了を知る使い方です。

    あなた: 「このプロジェクトの全テストを実行して、失敗したものを修正して」
    → PCを離れてミーティングへ
    → スマホに「タスク完了」の通知が届く
    → スマホで結果を確認、追加の指示を出す

    CIの待ち時間やビルド時間が長いプロジェクトでは特に役立ちます。

    シーン2:通勤中のコードレビュー

    電車の中でプルリクエストのレビューをClaude Codeに依頼できます。

    スマホから: 「PR #42のコードをレビューして、セキュリティの問題があれば指摘して」
    → Claude Codeがレビューを実行
    → 結果をスマホで確認
    → 修正の指示もスマホから

    シーン3:複数プロジェクトの並行管理

    フロントエンドとバックエンドのセッションを同時に立ち上げ、スマホから切り替えながら指示を出せます。

    # ターミナル1
    cd ~/projects/frontend && happy
    
    # ターミナル2
    cd ~/projects/backend && happy

    スマホのアプリ上でセッションを切り替えて、それぞれに指示を出せます。各セッションのコンテキストは完全に独立しています。

    シーン4:権限承認のリモート対応

    Claude Codeがファイルの書き込みや外部APIの呼び出しなど、権限が必要な操作を要求した場合、スマホに承認プロンプトが表示されます。タップ一つで許可・拒否を選べるので、PCから離れていても作業が止まりません。

    シーン5:音声でのハンズフリー操作

    運転中や料理中など手が離せないときでも、音声でClaude Codeに指示できます。「テストを実行して」「最後のコミットを元に戻して」と話しかけるだけです。


    競合ツールとの比較

    モバイルからClaude Codeを操作する方法はほかにもあります。

    項目Happy CoderTailscale + TermiusClaude Remote
    セットアップ難易度簡単(npm install → QRスキャン)中〜高(VPN設定が必要)中程度
    料金完全無料一部有料不明
    E2E暗号化ありVPNによる暗号化不明
    音声操作ありなしなし
    プッシュ通知ありなしなし
    オフライン対応あり(再接続で自動同期)VPN接続が必要不明
    マルチセッションあり手動で管理不明
    オープンソースあり(MIT)Termiusは非OSS不明

    Happy Coderは設定の手軽さとセキュリティを両立している点、そしてClaude Code以外のツールにも対応している点で差がつきます。


    セキュリティに関する注意点

    Happy Coderはゼロトラスト設計ですが、運用面で気をつけるべきことがあります。

    • 信頼できるデバイスのみでQRコードをスキャンする
    • 公共Wi-Fiでは機密性の高いコマンドの実行を避ける
    • CLIとアプリは常に最新バージョンに保つ
    • QRコードのスクリーンショットを他人に共有しない(暗号鍵が含まれるため)
    • 不要になったデバイスのペアリングは解除する

    よくある質問(FAQ)

    Q1. Happy Coderを使うのに追加料金はかかる?

    かかりません。Happy Coderは完全無料です。ただしClaude Code自体の利用にはAnthropicのAPIキーまたはサブスクリプションが必要です。Happy Coderはクライアントアプリなので、追加のサーバー費用等は発生しません。

    Q2. 中継サーバーにコードが漏れることはない?

    ありません。すべてのデータはQRコードで共有した秘密鍵で暗号化されます。中継サーバーが扱うのは暗号化されたblobのみで、平文のデータにアクセスすることは技術的に不可能です。

    Q3. PCがスリープするとセッションは切れる?

    macOSの場合、Happy CLIはデフォルトで caffeinate を使ってスリープを抑制します。ただし環境変数 HAPPY_DISABLE_CAFFEINATE=true が設定されている場合はスリープします。万が一接続が切れても、オフラインファースト設計により再接続時に自動で同期されます。

    Q4. Claude Code以外のAIツールにも対応している?

    OpenAI Codex(happy codex)とGoogle Gemini(happy gemini)に対応しています。Geminiは happy connect gemini で初回認証を行ってください。

    Q5. 複数のPCからスマホに接続できる?

    できます。それぞれのPCで happy を起動し、スマホアプリでセッションを切り替えることで、複数のマシンを1台のスマホから操作できます。


    トラブルシューティング

    接続に問題がある場合は、まず診断コマンドを実行してください。

    happy doctor

    よくある問題と対処法

    問題対処法
    QRコードが表示されないnode --version でNode.js 20以上か確認
    スマホに出力が表示されないアプリを再起動し、再スキャン
    CLIがクラッシュするnpm update -g happy-coder で最新版に更新
    macOSがスリープするHAPPY_DISABLE_CAFFEINATE 環境変数が未設定か確認
    Gemini接続に失敗するhappy connect gemini で再認証

    まとめ

    Happy Coderを入れると、Claude Codeがスマホから使えるようになります。QRコードを1回スキャンするだけでE2E暗号化接続が確立され、通勤中のコードレビューやカフェからのバグ対応、ミーティング中のバックグラウンドタスク管理まで、PCの前にいなくても開発が止まりません。

    CodexやGemini CLIにも対応しているので、複数のAIコーディングツールを使い分けている場合も1つのアプリで管理できます。MITライセンスで完全無料、GitHubスターは1.2万超。CLIは npm install -g happy-coder、アプリはApp StoreかGoogle Playからどうぞ。

    ※本記事は2026年2月時点の情報に基づいています。最新の情報はHappy Coder公式サイトをご確認ください。


    関連リンク

  • EastCloud株式会社|挑戦にイノベーションを起こす ― 会社のビジョンと未来像

    EastCloud株式会社|挑戦にイノベーションを起こす ― 会社のビジョンと未来像

    EastCloud株式会社は、「挑戦にイノベーションを起こす」という理念を掲げ、 ITの力を用いて企業や個人の未来を切り拓くパートナーとして活動しています。 アイデアを素早く形にする開発力と、仕組みから支援するサービス体系を通じて、 誰もが新しい挑戦に踏み出せる社会を目指しています。

    1. ビジョン ― 挑戦を“すぐできる”に変える存在へ

    EastCloudが目指すのは、単なる受託開発会社ではなく、 挑戦したい人・企業がすぐに実行できる環境をつくる「仕組みの提供者」です。 「やってみたい」という想いを、実際のプロダクト・サービスへと加速させることが EastCloudのビジョンの中心にあります。

    2. 事業ラインナップ ― ビジョンを支えるサービス構造

    EastCloudは、次のようなサービスを展開しています。

    • スピード特化型Web開発サービス(Korobql)
    • 補助金・助成金を活用したワンストップSIサービス(GrantOps)
    • 転職支援型プログラミング学習サービス(WorkReady)
    • 現役エンジニアによる人材育成(CoreGrow)
    • アプリ管理クラウドプラットフォーム(Stranah)

    これらは、資金・技術・人材といった企業の課題を同時に解決するために設計されており、 「挑戦のハードルを下げる」ことを使命としたラインナップになっています。

    3. 未来像 ― 技術と仕組みで価値を創出する企業へ

    EastCloudが描く未来像は、次の3つの方向性を中心に展開されます。

    • スピードと品質を両立する技術力の強化
    • 教育×実務で人材不足を解決する仕組みの拡大
    • 補助金活用を軸とした挑戦支援のシステム化

    単に開発を行うのではなく、挑戦の成功率を高める“仕組み”を構築することで、 長期的なパートナーとして価値を提供する未来を目指しています。

    4. セキュリティと信頼性 ― ISMS認証取得

    EastCloudは国際標準規格「ISO/IEC 27001(ISMS)」を取得し、 情報セキュリティの高い基準で運用を行っています。 システム開発やデータ取扱いにおいて、強固な管理体制を確立していることは、 顧客との信頼関係における大きな強みとなっています。

    5. ステークホルダーへのメッセージ

    クライアントへ:
    「まずはやってみたい」をすぐに実現できる環境を提供します。 プロトタイプ開発から大規模構築まで、スピード重視で伴走します。

    パートナー・投資家へ:
    技術力・実行力・仕組み化を強みとし、短期間で価値を生み出す体制があります。 共同で新しい価値を創出できる領域は今後さらに広がっています。

    6. EastCloudが創るこれからの社会

    EastCloudは、ITの仕組みを通じて「挑戦が当たり前になる社会」を作ることを目指しています。 技術が人の可能性を広げ、日本全体の価値向上へ繋がる未来。 それこそがEastCloudが掲げる未来像です。


    ■ EastCloud株式会社
    所在地:
    東京都千代田区神田佐久間町3-29-3 B.S.R 秋葉原 3F
    公式サイト: https://east-cloud.jp/

  • 第2回:さくらのクラウドで構築するモダンアーキテクチャ VPC設計完全解説

    第2回:さくらのクラウドで構築するモダンアーキテクチャ VPC設計完全解説


    📚 さくらのクラウドで作るモダンアーキテクチャ(全5回)

    • 第1回:さくらのクラウドで作るモダンアーキテクチャ入門
    • 第2回:さくらのクラウドVPC設計(イマココ)
    • 第3回:GitHub ActionsとSelf-hosted RunnerでVPC内DBにmigrateを実行する方法
    • 第4回:AppRun専有型と共用型の違いを徹底解説
    • 第5回:Terraformで再現するさくらのモダンアーキテクチャ

    はじめに

    モダンアーキテクチャの成否は、ネットワーク設計で8割決まります。

    特に、さくらのクラウドはAWSとは設計思想が異なります。

    • AWS:L3(VPC)中心の抽象化された設計
    • さくら:L2(スイッチ)を自分で組む前提の設計

    この違いを理解しないままAppRunやDBを配置すると、

    • VPC連携がうまくいかない
    • 専有型が起動しない
    • 稼働コンテナ数が0になる
    • 意図せずDBが公開される

    といった事故が起きます。

    今回は、

    • L2(スイッチ)
    • L3(VPCルータ)
    • SEG(サービスエンドポイント)

    この3つを軸に、正しいVPC設計を整理します。


    1. L2(スイッチ)とは何か?

    L2スイッチは、同一セグメント内の通信を担うレイヤーです。

    特徴:

    • 同一IPレンジ内で直接通信
    • ブロードキャストドメインを形成
    • ルーティングはしない

    さくらのクラウドでは、セグメントを自分で作るのが前提です。

    AWSのように「VPCを作れば終わり」ではありません。
    まずは どの役割をどのセグメントに分けるか を決めることから始まります。


    2. L3(VPCルータ)の役割

    VPCルータは、異なるセグメント間を接続するL3層です。

    主な機能:

    • ルーティング
    • NAT
    • パケットフィルタ
    • VPN接続

    AWSで例えると:

    • NAT Gateway
    • Internet Gateway
    • Route Table

    をまとめた存在です。

    原則はシンプル。

    L2で分ける
    L3で繋ぐ


    3. SEG(サービスエンドポイント)を忘れるな

    ここが最重要ポイントです。

    SEGとは?

    SEG(Service Endpoint Gateway)は、

    VPC内からさくらのマネージドサービスへ
    インターネットを経由せずに接続する仕組み

    です。

    対象例:

    • コンテナレジストリ
    • AppRun Dedicatedコントロールプレーン
    • オブジェクトストレージ

    なぜSEGが重要なのか?

    AppRun専有型では、

    • ワーカーノードがVPC内に存在
    • 外部に直接出られない構成も多い

    そのためSEGを設定しないと、

    • Dockerイメージがpullできない
    • デプロイが失敗する
    • activeNodeCount=0 のまま止まる

    という状態になります。

    「ネットワークは正しいのに動かない」場合、
    まずSEGを疑うべきです。


    4. 推奨セグメント構成

    今回のシリーズで採用する基本構成は以下です。

    Internet
       ↓
    Cloudflare
       ↓
    AppRun(Frontend)
    
    [VPC内]
      ├─ public-app-seg
      ├─ private-db-seg
      ├─ runner-seg
      └─ SEG(サービス接続)
    

    設計原則:

    • DBはprivate-db-segのみ
    • DBにグローバルIPを持たせない
    • RunnerのみDBアクセス許可
    • AppRun専有型はSEG経由でサービス接続

    5. Terraformでのネットワーク表現例

    ここまでの設計は、Terraformでは次のように表現できます。

    L2(セグメント)

    resource "sakuracloud_switch" "public_app" {
      name = "public-app-seg"
    }
    
    resource "sakuracloud_switch" "private_db" {
      name = "private-db-seg"
    }
    
    resource "sakuracloud_switch" "runner" {
      name = "runner-seg"
    }
    

    L3(VPCルータ)

    resource "sakuracloud_vpc_router" "vpc" {
      name = "vpc-router"
      plan = "standard"
    
      interface {
        switch_id = sakuracloud_switch.public_app.id
        ipaddress = ["192.168.10.1"]
        netmask   = 24
      }
    
      interface {
        switch_id = sakuracloud_switch.private_db.id
        ipaddress = ["192.168.20.1"]
        netmask   = 24
      }
    
      interface {
        switch_id = sakuracloud_switch.runner.id
        ipaddress = ["192.168.30.1"]
        netmask   = 24
      }
    }
    

    ここで重要なのは、

    Terraform上でも
    「L2で分け、L3で束ねる」という思想がそのまま表現される

    という点です。


    SEG(概念例)

    resource "sakuracloud_service_endpoint" "seg" {
      name      = "seg-main"
      switch_id = sakuracloud_switch.public_app.id
    
      services = [
        "container-registry",
        "object-storage",
        "apprun-dedicated-control-plane",
      ]
    }
    

    ※実際の属性はproviderバージョンにより変更される可能性があります。


    6. AWSとの違い

    観点AWSさくら
    セグメント自動生成手動L2作成
    サービス接続VPC EndpointSEG
    NAT専用VPCルータ内蔵
    思想抽象化明示的設計

    さくらは「分かりやすい」代わりに、
    自分で理解して組む責任がある

    しかし一度理解すれば、
    構造は極めてシンプルです。


    設計原則まとめ

    • L2で役割ごとに分離する
    • L3で境界を制御する
    • SEGでマネージドサービスへ閉域接続する
    • DBは絶対にグローバル公開しない
    • AppRun専有型はSEG前提で設計する

    よくある質問(FAQ)

    Q. SEGは必須ですか?

    AppRun専有型で閉域構成を組むなら必須です。


    Q. SEGとNATの違いは?

    NATはインターネット経由。
    SEGは閉域接続。


    Q. L2とL3の違いが曖昧です。

    L2は同一セグメント内通信。
    L3は異なるセグメント間のルーティングです。


    まとめ

    さくらのクラウドでモダンアーキテクチャを組むなら、

    L2・L3・SEG

    この3点を理解することが最重要。

    ここを押さえれば、
    AppRun × VPC構成で迷うことはありません。


    次回は、

    GitHub Actions × Self-hosted Runnerで
    VPC内DBにmigrateを実行する方法

    を解説します。

    閉域ネットワークとCI/CDをどう両立させるか。
    ここが実運用の核心です。