TECH PLAY

株式会社豆蔵

株式会社豆蔵 の技術ブログ

100

はじめに # 本ページは「AIエージェントとシステムをつなぐMCP入門」の続編です。 今回はStreamableHTTPで通信するMCPサーバー(ステートレス)の実装について説明します。 前回のstdio実装編はMCPクライアントがサブプロセスとして実行しローカルで完結する構成でした。StreamableHTTPはHTTP経由でMCPサーバーを公開し、複数のMCPクライアントから利用可能な構成です。 MCPサーバーがWebAPIを呼び出してMCPクライアントに最新データを参照させる用途に向いています。 また、ステートレスは、リクエストごとに処理が完結するため、ライフサイクルの管理が単純で扱いやすいのが特徴です。 文量が多くなったので、ステートレスとステートフルは分けて説明します。 本ページで掲載しているコードは こちら で公開しています。 --> シリーズ目次 連載:AIエージェントとシステムをつなぐMCP入門 イントロダクション stdio実装編 StreamableHTTPステートレス実装編(本ページ) 今回使用するライブラリなど # npm@11.11.1 node@22.22.0 typescript@6.0.3 @modelcontextprotocol/sdk@1.29.0 zod@4.3.6 簡単なサーバーを実装 # 簡単にMCPサーバーを実装して動作確認します。 サーバーの実装 # stdioでも触れましたが、基本的な要素(「サーバーインスタンスの生成」「ツールの登録」「起動処理」)は同じです。 stdioに比べて特色のある「起動処理」について説明します。 起動処理 受け付けるエンドポイントを定義 トランスポート設定、ツール登録およびレスポンス後処理などを定義 StreamableHTTPなので、 StreamableHTTPServerTransport のインスタンスをconnectの引数に設定します。 ステートレスとしているので、セッションIDの振り出しもありません。 app.post("/mcp", async (req, res) => { const server = createServer(); const transport = new StreamableHTTPServerTransport(); try { // 1: 下記コラム参照 await server.connect(refineTransport(server, transport)); await transport.handleRequest(req, res, req.body); } catch (error) { console.error("Error handling MCP request:", error); if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null }); } } finally { await transport.close(); await server.close(); } }); --> 1. 引数型に合わせるため、型アサーションで型を調整しています StreamableHTTPServerTransport は Transport を実装しています。 ただし、今回使用したバージョンでは、 Transport が定義している振る舞いと StreamableHTTPServerTransport の定義に差異があります。(oncloseはその一例) 該当箇所を部分的に掲載しています // transport.d.ts export interface Transport { onclose?: () => void; } // streamableHttp.d.ts export declare class StreamableHTTPServerTransport implements Transport { set onclose(handler: (() => void) | undefined); get onclose(): (() => void) | undefined; } server.connect は Transport を引数型としていますが構造が合わないため、型アサーションで調整しています。 型調整に使用しているコード export const refineTransport = (server: McpServer, transport: StreamableHTTPServerTransport) => { return transport as Parameters<typeof server.connect>[0]; }; 拒否するエンドポイントを定義 拒否したいエンドポイントは405などのレスポンスを定義します。 app.get("/mcp", (_req, res) => { res.writeHead(405).end(JSON.stringify({jsonrpc: "2.0", error: {code: -32000, message: "Method not allowed."}, id: null})); }); ポートにバインディング 指定したポートにバインディングします。 app.listen(MCP_PORT, (error?: Error) => { //omit }); 全コード index.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { z } from "zod"; import { refineTransport } from "./transport.util.js"; import { ApplicationError } from "./application.error.js"; const PORT = Number(process.env.PORT ?? "3000"); const WEB_API_BASE_URL = process.env.WEB_API_BASE_URL ?? "http://localhost:3001"; const WEB_API_CALL_FAILED_MESSAGE = "WebAPI call failed"; function createServer() { // サーバーインスタンスの生成 const server = new McpServer({ name: "todo-mcp-stateless", version: "1.0.0", }); // ツールの登録 server.registerTool( "get_todo", { title: "get_todo", description: "Todoを1件取得する", inputSchema: { id: z.number().describe("TodoのID"), }, }, async ({ id }) => { const endpoint = `${WEB_API_BASE_URL}/todos/${id}`; try { const response = await fetch(endpoint, { method: "GET", headers: { "Content-Type": "application/json" }, signal: AbortSignal.timeout(10_000), }); // 登録を省略しているため、レスポンスの検証は省略しています。 const body = await response.json(); return { content: [{ type: "text", text: JSON.stringify(body) }] }; } catch (error) { const message = error instanceof ApplicationError ? error.message : WEB_API_CALL_FAILED_MESSAGE; throw new ApplicationError(message); } }, ); return server; } const app = createMcpExpressApp(); // 起動処理 async function boot() { // POSTリクエストを受け付けるエンドポイント app.post("/mcp", async (req, res) => { const server = createServer(); const transport = new StreamableHTTPServerTransport(); try { await server.connect(refineTransport(server, transport)); await transport.handleRequest(req, res, req.body); } catch (error) { console.error("Error handling MCP request:", error); if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null }); } } finally { await transport.close(); await server.close(); } }); // GETリクエストを抑止 app.get("/mcp", (_req, res) => { res.writeHead(405).end(JSON.stringify({jsonrpc: "2.0", error: {code: -32000, message: "Method not allowed."}, id: null})); }); // DELETEリクエストを抑止 app.delete("/mcp", (_req, res) => { res.writeHead(405).end(JSON.stringify({jsonrpc: "2.0", error: {code: -32000, message: "Method not allowed."}, id: null})); }); // ポートにバインド app.listen(PORT, (error?: Error) => { if (error) { console.error("Failed to start stateless server:", error); process.exit(1); } console.error(`Stateless MCP endpoint: http://localhost:${PORT}/mcp`); }); } await boot(); サーバーの動作確認 # MCP Inspectorを使って、同じ動作を確認します。 --> 例外がスローされて終了した場合の表示 接続先のAPIサーバーを止めてツールを実行して、表示を確認 まとめ # StreamableHTTPのステートレス実装では、リクエストごとにMcpServerとTransportを生成し、処理後にクローズすることで、シンプルなライフサイクルを保てます。 MCP SDKはマイナーバージョン更新でも型定義や挙動差分の影響を受けることがあるため、依存バージョンは固定し、更新時は検証手順を用意して段階的に確認するのが安全です。 本編では最小構成でステートレスの流れを確認しました。APIのCRUDを網羅したサンプル実装も こちら に公開しているので、必要に応じてご参照ください。 次編ではステートフル構成との違い(セッション管理とサーバーライフサイクル)を扱います。
はじめに # 本ページは「AIエージェントとシステムをつなぐMCP入門」の続編です。 今回はstdioで通信するMCPサーバーの実装について説明します。標準入出力(stdin/stdout)を利用したMCPサーバーの構築手順と、stdio特有の注意点について見ていきます。 本ページで掲載しているコードは こちら で公開しています。 --> シリーズ目次 連載:AIエージェントとシステムをつなぐMCP入門 イントロダクション stdio実装編(本ページ) StreamableHTTPステートレス実装編 今回使用するライブラリなど # npm@11.11.1 node@22.22.0 typescript@6.0.3 @modelcontextprotocol/sdk@1.29.0 zod@4.3.6 簡単なサーバーを実装 # 簡単にMCPサーバーを実装して動作確認します。 サーバーの実装 # サーバーインスタンスの生成 サーバー名やバージョンを設定し、MCPサーバーのインスタンスを生成します。 ツールの登録 MCPサーバーにツール(公開する振る舞い)を登録します。 ここで登録したツールをMCPクライアントから呼び出せます。 ツールには、下記のような内容を実装します。 ツール名 入力スキーマ: ツールが受け取るデータの構造 出力スキーマ: ツールが返却するデータの構造。構造化したデータを返却したい場合のみ定義する。 リクエストハンドラー: ツールの内部処理。contentの返却は必須。 起動処理 MCPサーバーの起動処理。 今回はstdioなので、 StdioServerTransport のインスタンスをconnectの引数に設定します。 index.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; // サーバーのインスタンス化 const server = new McpServer({ name: "hello-world-server", version: "1.0.0", }); // ツールの登録 server.registerTool( "hello", { title: "hello, world!", inputSchema: { name: z.string().describe("メッセージに追加する名前") }, // 入力スキーマの定義 outputSchema: { message: z.string().describe("メッセージ") }, // 出力スキーマの定義 }, async ({ name }) => { return { content: [{ type: "text", text: `Hello, ${name}!` }], // デフォルトのレスポンス structuredContent: { message: `Hello, ${name}!` }, // 出力スキーマに基づくレスポンス }; }, ); // 起動処理 async function boot() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP Hello World Server (Modern) running on stdio"); // 標準出力にログを出力するとエラーになるため、`console.error`を使用しています } try { await boot(); } catch (error) { console.error("Fatal error:", error); process.exit(1); } サーバーの動作確認 # コンソールで確認 コンソール上でサーバーを起動して、動作を確認します。 起動 サーバーのビルドと起動 npx tsc node dist/index.js ツール一覧取得 tools/list を実行して一覧を取得。 ツール一覧取得と結果 // 標準入出力に下記を入力 {"jsonrpc":"2.0", "id":"1", "method":"tools/list", "params":{}} // ツール一覧が表示されました ※レスポンスは一部成形しています {"result":{"tools":[ { "name":"hello", "title":"hello, world!", "inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string","description":"メッセージに追加する名前"}},"required":["name"]}, "execution":{"taskSupport":"forbidden"}, "outputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"message":{"type":"string","description":"メッセージ"}},"required":["message"],"additionalProperties":false} } ]},"jsonrpc":"2.0","id":"1"} ツール呼び出し tools/call で hello を実行。 ツールの呼び出しと結果 // 標準入出力に下記を入力 {"jsonrpc":"2.0", "id":"1", "method": "tools/call", "params": { "name": "hello", "arguments": { "name": "MCP" }}} // ツールの実行結果が表示されました ※レスポンスは一部成形しています {"result":{ "content":[{"type":"text","text":"Hello, MCP!"}], "structuredContent":{"message":"Hello, MCP!"}}, "jsonrpc":"2.0","id":"1"} MCP Inspectorで確認 MCP Inspectorを使って、同じ動作を確認します。 デバッグログを出力したらどうなるのか検証 # 通信に使われる標準出力へJSON形式ではない文字列を出力した場合の挙動を検証してみます。 通常、標準エラー出力はプロトコル本体とは分離されるため、併せて挙動を確認します。 ツールを追加実装 # 標準出力(stdout)、標準エラー(stderr)にログを出力するツールを追加実装します。 index.ts server.registerTool( `output_log`, { title: 'output_log' }, async () => { console.log('debug log'); // to stdout console.info('info log'); // to stdout console.warn('warn log'); // to stderr console.error('error log'); // to stderr return { content: [{ type: "text", text: 'output log tool' }] }; }, ); 追加したツールの動作確認 # 追加したツールを実行してみます。 基本的にstdioは標準出力をJSON-RPCメッセージ専用、ログは標準エラー出力へ分離して動作します。 標準出力にJSON形式ではない文字列が混入したタイミングで、パースエラーになりました。 標準エラー出力にも同様の出力していますが、分離されているためエラーは発生しません。(標準出力を汚さないことが前提) MCP Inspectorを起動しているコンソール Received POST message for sessionId xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx1xx Error from MCP server: SyntaxError: Unexpected token 'd', "debug log" is not valid JSON at JSON.parse (<anonymous>) at deserializeMessage (file:///C:/Users/~/shared/stdio.js:26:44) at ReadBuffer.readMessage (file:///C:/Users/~/shared/stdio.js:19:16) at StdioClientTransport.processReadBuffer (file:///C:/Users/~/client/stdio.js:126:50) at Socket.<anonymous> (file:///C:/Users/~/client/stdio.js:92:22) at Socket.emit (node:events:519:28) at addChunk (node:internal/streams/readable:561:12) at readableAddChunkPushByteMode (node:internal/streams/readable:512:3) at Readable.push (node:internal/streams/readable:392:5) at Pipe.onStreamRead (node:internal/stream_base_commons:189:23) Error from MCP server: SyntaxError: Unexpected token 'i', "info log" is not valid JSON at JSON.parse (<anonymous>) at deserializeMessage (file:///C:/Users/~/shared/stdio.js:26:44) at ReadBuffer.readMessage (file:///C:/Users/~/shared/stdio.js:19:16) at StdioClientTransport.processReadBuffer (file:///C:/Users/~/client/stdio.js:126:50) at Socket.<anonymous> (file:///C:/Users/~/client/stdio.js:92:22) at Socket.emit (node:events:519:28) at addChunk (node:internal/streams/readable:561:12) at readableAddChunkPushByteMode (node:internal/streams/readable:512:3) at Readable.push (node:internal/streams/readable:392:5) at Pipe.onStreamRead (node:internal/stream_base_commons:189:23) --> MCP Inspectorの表示 上記の通り、コンソール上はパースエラーになりました。 今回使用したInspectorは後続の正しいJSONメッセージを拾ってくれたため、UI上は成功に見えます。 まとめ # stdioでは標準出力をJSON-RPCメッセージ専用にし、ログは標準エラー出力へ分離する。 標準出力に非JSON文字列が混入すると、クライアント側でパースエラーが発生しやすい。
はじめに # アジャイルグループの石田です。 先日、3月24日に開催された 豆寄席「スクラムマスターのAI活用を考える 〜透明性・検査・適応 三本柱を強化する実践アプローチ〜」 に登壇しました。第50回という節目のイベントに想定以上の多くの方にご参加いただき、誠にありがとうございました。改めて、スクラムマスターとAIという組み合わせの注目度の高さを感じました。 発表では、スクラムマスターがAIをどう活用できるかをメインテーマにお話ししましたが、その中でAIを使ったプレゼンテーション作成についても触れました。今回はその内容をより詳しくご紹介します。 プレゼン作成に使えるテンプレートリポジトリを GitHubで公開 しました。細かい使い方はリポジトリのREADMEをご参照ください。この記事では「AIでプレゼンを作ることのメリット」と「テンプレートの特徴」を中心にお伝えします。 AIでプレゼンを作るとはどういうことか # 従来のプレゼン作成の課題 # プレゼン資料の作成は、意外と時間がかかります。大まかな流れを整理すると、次のようなステップを踏むことが多いのではないでしょうか。 伝えたいことを整理し、構成を考える 各スライドの内容を起こす PowerPoint や Keynote でレイアウト・デザインを整える 見直し・修正を繰り返す このうち、特に 「1. 構成を考える」 と 「3. デザインを整える」 に時間を取られがちです。構成はゼロから考えると手が止まりやすく、デザインは凝り始めるとキリがありません。 AIが変えること # 生成AIを活用すると、上記の課題を次のように解消できます。 構成案の生成 :「○○について発表したい」という要件を渡すだけで、章立てやアウトラインを提案してもらえます スライドテキストのドラフト生成 :構成が決まれば、各スライドの本文もAIに任せることができます 仕様書・要件からの変換 :仕様書や議事録など既存のドキュメントをインプットとして与え、プレゼン向けに要約・再構成させることも可能です さらに重要なポイントとして、文字生成を得意とするAIはMarkdownと相性がよく、スライドをMarkdownで書くことでコンテンツ生成・修正を自然な会話の延長で依頼できます。PowerPoint のようにバイナリファイルを直接操作する必要がなく、差分管理もしやすくなります。 テンプレートリポジトリの紹介 # mamezou-presentation は、 Marp + GitHub Actions + GitHub Pages を組み合わせた豆蔵向けのプレゼンテンプレートです。MIT Licenseで公開しているため、社外の方も自由に利用・改変いただけます。 リポジトリの主な構成 # mamezou-presentation/ ├── presentation/ │ ├── 01_intro.md # スライド(数字順に結合される) │ ├── 02_main.md │ └── ... ├── images/ │ └── title.svg # タイトルスライド用画像 ├── mamezou-theme.css # 豆蔵ブランド テーマ(紫系) └── .github/workflows/ # 自動ビルド・デプロイ設定 スライドは presentation/NN_name.md の形式で分割して管理し、数字順に結合されます。1ファイルに全スライドを詰め込む必要がないため、セクションごとにファイルを分けて管理しやすくなっています。 テンプレートの特徴 # Push するだけで公開される # このテンプレート最大の特長は、 mainブランチにプッシュするだけでHTML/PDFが自動生成され、GitHub Pagesに公開される ことです。 「ビルドコマンドどうやるんだっけ」「PDFに書き出すにはどうすればいい」といった手順を覚える必要がありません。Markdownを書いてpushするだけで、発表用の成果物が自動的に用意されます。 git add presentation/01_intro.md git commit -m "スライド追加" git push origin main # → GitHub Actions が起動し、HTML/PDF を自動生成・公開 VS Code + Marp 拡張でリアルタイムプレビュー # ローカル開発では、VS Code の Marp for VS Code 拡張を使うことで、Markdownを編集しながらスライドをリアルタイムにプレビューできます。「書く → 確認 → 修正」のサイクルが素早く回せます。 AIワークフローとの統合(GitHub Spec Kit) # このテンプレートは、 GitHub Spec Kit を介したAIワークフローに対応しています。Gemini CLI や Claude Code と連携し、次のコマンドでスライド作成をAI主導で進められます。 コマンド 内容 /speckit.specify 発表内容の仕様(何を伝えるか)をAIと一緒に整理する /speckit.plan 仕様をもとにスライド構成案を生成する /speckit.tasks 各スライドの作成タスクに分解する 「何を話すか」を仕様として書き出せば、AIがスライドの骨格を提案してくれます。人間はレビューと肉付けに集中できるため、質の高いプレゼンをより短時間で仕上げられます。 また、Spec Kit の Constitution (規約・制限) を活用することで、AIが生成するテキストのトーンやスタイルを統一できます。Constitutionとしてルールを定義しておくことで、複数のスライドにわたって一貫した文体が保たれます。たとえばこのテンプレートでは次のようなルールを定義しています。 スライド本文の口調は「だ・である」調(常体)で統一する(導入部は「です・ます」調も可) 強調には鉤括弧(「」)を避け、HTMLの <strong> タグを使用する 図表の自動変換(Mermaid → PNG) # Mermaid形式( .mmd )のファイルをリポジトリに置くと、GitHub Actions が自動的にPNGへ変換し、スライドに埋め込めるようになります。 graph LR A[仕様を書く] --> B[AIが構成提案] B --> C[Markdownで執筆] C --> D[Push] D --> E[自動ビルド・公開] テキストベースで図を管理できるため、バージョン管理との相性も抜群です。 プレゼンターモード・タイマー機能 # 発表本番を支援する機能も備えています。 プレゼンターモード : ブラウザで p キーを押すと、スピーカーノート付きのプレゼンターモードに切り替わります カウントダウンタイマー : 発表時間を管理するタイマーアプリをAIを活用して作成しており、アジェンダとともにカスタマイズ可能です JavaScriptによる拡張 : プレゼン資料はHTMLベースのため、JavaScriptを組み込むことで各章へのショートカットを作成するなど、自由度の高いカスタマイズが可能です AIでプレゼンを作るメリットまとめ # ここまでの内容を整理すると、AIを活用したプレゼン作成には次のメリットがあります。 メリット 内容 スピード 構成案やドラフトをAIが生成するため、立ち上がりが速い 品質 抜け漏れの少ない構成をAIが提案してくれる 一貫性 テンプレートにより毎回同じクオリティのデザインが保たれる 再現性 Markdownなのでバージョン管理・差分確認が容易 集中力 デザインではなく「何を伝えるか」に集中できる 特に「集中できる」という点は体感として大きく、AIとテンプレートによってデザインの迷いがなくなると、コンテンツの質に意識を向けやすくなります。 おわりに # 今回は豆寄席での発表をきっかけに、AIを使ったプレゼンテーション作成の取り組みをご紹介しました。 テンプレートリポジトリはMIT Licenseで公開していますので、豆蔵社外の方も自由にご利用・ご改変いただけます。 リポジトリ: mamezou-ishida/mamezou-presentation 「試してみたい」「こんな機能があると嬉しい」といったフィードバックや質問は、GitHubのIssueにてお気軽にどうぞ。AIとMarkdownでのプレゼン作成、ぜひ一度お試しください。
はじめに # 本シリーズでは、MCP(Model Context Protocol)の基本から実装まで段階を分けて解説します。 「AIエージェントに社内システムや外部APIの知識を与えたい」という方へ向けた内容になります。 今回はMCPそのものについて説明します。 今後、トランスポート(stdio, Streamable HTTP)ごとの実装、MCPの自動生成などへの展開を予定しています。 --> シリーズ目次 連載:AIエージェントとシステムをつなぐMCP入門 イントロダクション(本ページ) stdio実装編 StreamableHTTPステートレス実装編 なぜMCPが必要なのか # AIエージェント(Claude、GPT-4、Geminiなど)は、膨大な知識を持っていますが「今この瞬間のデータ」や「社内システムの情報」には直接アクセスできません。 たとえば「最新の受注状況」や「在庫数」をAIに聞いても、学習データに含まれていなければ答えられません。 こうした「知識の壁」を越えるための仕組みがMCPです。 MCPを使うことで、AIエージェントが社内外のシステムやデータベースと安全に連携できます。 併せて、認証や認可(アクセス制御)が自身の方式に統合できるように検討するのが重要な課題です。 MCPとは # AIエージェントが外部サービスと通信するための仕様で、2024/11にAnthropic社によって初版がリリースされました。( 公式サイト ) MCPを使用することで、AIエージェントは外部サービスの機能を効果的に利用できます。 MCPサーバーは、AIエージェントが外部ツールやデータソースと通信するため、MCPプロトコルを実装したサーバーを指します。 MCPクライアントは、AIエージェントなどのMCPサーバー利用者を指します。 graph LR Agent[AIエージェント] MCP[MCPサーバー] API1[受注/社内サービス] API2[天気API/社外] DB[社内DB] Agent -- "MCP/HTTP(JSON-RPC)" --> MCP MCP -- "REST/HTTP" --> API1 MCP -- "外部API" --> API2 MCP -- "SQL" --> DB ※MCPは「AIエージェントと社内外サービスをつなぐハブ」のような役割を果たします。 項目 MCP プロトコル JSON-RPC 2.0 over stdio または HTTP/SSE(Streamable HTTP) データ形式 JSON (JSON-RPC 2.0) エンドポイント 統一エンドポイント( /mcp のみ) 操作 JSON-RPCメソッド( tools/list , tools/call ) エラー JSON-RPC Error(code, message) --> SSE(Server-Sent Events)とは HTTP接続を維持したまま、サーバーからイベントを逐次送信する仕組み。( text/event-stream ) MCPでは、ツールの呼び出し結果やストリームレスポンスを段階的に返す用途で利用されます。 MCPサーバーの主な役割 # MCPサーバーが担う主な役割は下記の通りです。 BFFのMCPクライアント版のような役割と言えます。 プロトコル変換(MCP⇔REST) 認証・認可 流量制限 監査ロギング ルーティング エラーハンドリング レスポンスの変換、合成 MCPサーバーによる効果 # AIエージェントは、学習データに含まれていない最新データや外部データは活用できません。 MCPサーバーで外部ツールやデータソースにアクセスできるツールを公開することで、リアルタイムデータにもとづいて返答できるようになります。 sequenceDiagram actor User actor Agent as AIエージェント participant MCP as MCPサーバー participant API as 受注サービス note over User, Agent: MCPなし User->>+Agent: 受注番号O001の状況を教えて Agent-->>-User: 受注番号「O001」の状況を確認する手段がありません。担当の受注管理部門にお問い合わせください。 note over User, Agent: MCPあり User->>+Agent: 受注番号O001の状況を教えて。調べる際はget_orderを使って。 Agent->>+MCP: 受注取得ツール呼び出し<br>get_order(order_id="O001") MCP->>+API: 受注取得APIリクエスト<br>GET /orders/O001 API-->>-MCP: 受注を返却<br>{ "order_id": "O001", "status": "shipped", ... } MCP-->>-Agent: 受注を返却<br>{ "jsonrpc": "2.0", "result": "{ "order_id": "O001", "status": "shipped", ... }", "id": 1 } Agent-->>-User: 受注O001の状況は発送済みです MCPの活用例 AIチャットボットが「受注番号O001の出荷状況を教えて」と聞かれたとき、MCP経由で社内の受注管理システムに問い合わせて最新情報を返す 社内FAQボットが、MCPを通じて人事システムや勤怠データベースから必要な情報を取得し、社員の質問に答える 外部API(天気、為替レートなど)と連携し、AIがリアルタイムなデータをもとにアドバイスを行う トランスポート(通信方式)の種類 # MCPのトランスポートには stdio と Streamable HTTP があります。 stdio(標準入出力) # MCPクライアント(AIエージェントなど)がMCPサーバーをサブプロセスとして起動し、ローカル環境で通信する方式です。 通信 データ形式: 改行区切りのJSON-RPCメッセージ。改行を許容するため、メッセージに改行を含めることはできません。 データ送受信: 標準入出力(stdin/stdout)を利用します。 セッションの終了: クライアントが入力ストリームを閉じるか、プロセスを終了する。 用途 個人のPC上で動くAIエージェントにローカルリソース(ファイルやツール)を操作させる。 Streamable HTTP # MCPサーバーは独立したプロセスとして稼働し、単一のHTTPエンドポイントで複数のMCPクライアントからの接続を受け付ける通信方式です。 通信 POSTとSSEの組み合わせ: クライアントからのリクエストはPOSTで送信し、サーバーからのレスポンスや通知はSSEを利用してストリーミング配信します。 非同期・準双方向通信: GETを使って独立した受信ストリームを開いておくことで、サーバーから任意のタイミングで自発的に通知を送ることが可能です。 回復と状態管理: ネットワークが切断されても Last-Event-ID を用いて途中から再接続できる機能や、 MCP-Session-Id ヘッダーを使ってセッション管理する機能が備わっています。 用途 リモートサーバーでの利用: ネットワーク越しに配置されたサーバーの機能(クラウド上のデータベースや外部APIなど)を利用する場合に適しています。 複数クライアントからの利用: 1つのMCPサーバーを複数のMCPクライアントから利用したい場合、Webサービスのような構成で活躍します。 --> `MCP-Session-Id` クライアントとサーバー間のやり取り(セッション)を管理するIDのこと。 Streamable HTTPにおいて、サーバーは初期化時にセッションID(UUIDやJWTなど)を発行します。 クライアントは、これ以降のすべてのHTTPリクエスト(POSTやGET)に MCP-Session-Id ヘッダーとしてこの値を含める必要があります。 --> Last-Event-ID セッション内で開かれたSSEストリーム通信の連続性を保証するIDのこと。 サーバーからクライアントへメッセージを送るSSEストリーム上の各イベントにはIDが付与されます。 ストリームが切断された際、クライアントは再接続時に Last-Event-ID ヘッダーを用いて「最後に受け取ったイベントID」をサーバーに伝えることで中断したところからストリームを再開できます。 今後の展開 # 本シリーズでは、MCPの基本から実践的な実装方法を解説します。 トランスポートごとの違い、自動生成ツールの活用など、段階的に解説していく予定です。 「AIエージェントとシステムの連携をもっと身近に」するための内容を目指します。 次回もぜひご期待ください。
はじめに # 前回の記事 では、Google Cloud認定の鬼門であった「Professional Security Operations Engineer(PSOE)」を突破し、念願の「Google Cloud認定全冠」を達成したことをご報告しました。 そして今回、AWS側で唯一未取得となっていた最新認定「 AWS Certified Generative AI Developer - Professional (AIP-C01) 」が2026年4月14日に正式リリースされたため、さっそく受験してきました。結果は無事 合格 ! これにより、目標として掲げていた 「AWS&Google CloudのW全冠達成」 をついに果たすことができました。 本記事では、過去のベータ試験での不合格体験を振り返りつつ、リベンジに向けてどのような対策を行ったのか、そして正式リリース版試験の所感についてまとめます。 --> Information 秘密保持契約(NDA)があるため、詳細な試験内容については触れることができませんので、ご了承ください。 また記載の情報は2026年4月時点のものです。 試験の概要:AWS Certified Generative AI Developer - Professional # AWS Certified Generative AI Developer - Professional (AIP-C01) は、GenAI デベロッパーの役割を担う方を対象としたプロフェッショナルレベルの認定です。基盤モデル(FM)をアプリケーションとビジネスワークフローに効果的に統合し、AWSテクノロジーを使用してGenAIソリューションを本番環境に実装する実践的な知識が検証されます。 主な出題分野と重みは以下の通りです。 コンテンツ分野 1: 基盤モデルの統合、データ管理、コンプライアンス (31%) コンテンツ分野 2: 実装と統合 (26%) コンテンツ分野 3: AI の安全性、セキュリティ、ガバナンス (20%) コンテンツ分野 4: GenAIアプリケーションの運用効率と最適化 (12%) コンテンツ分野 5: テスト、検証、トラブルシューティング (11%) 試験は75問(うち10問は採点対象外)で構成されており、100~1,000の換算スコアで750点以上が合格ラインとなります。 ベータ試験での敗因と課題 # 実は2025年12月に、この認定のベータ試験を受験していましたが、残念ながら 不合格 という悔しい思いをしていました。 当時の振り返りでも触れましたが、主な敗因は以下の点にありました。 長文問題による時間不足 : プロフェッショナルレベル特有の非常に長い問題文と選択肢に圧倒されました。複数のサービスを組み合わせた複雑なシナリオから、要件とアーキテクチャ図を素早く頭の中で組み立てるのに時間がかかり、ギリギリまで時間を使ってしまいました。 本番環境運用を考慮した実践的知識の不足 : Amazon Bedrockを中心とした構成において、スケーラビリティ、セキュリティ、コスト最適化などのベストプラクティスに基づいた判断が求められましたが、複数の正解に見える選択肢から最適なものを絞り切れませんでした。 リベンジに向けた対策 # 前回の反省を踏まえ、正式版でのリベンジに向けて以下の対策を重点的に行いました。 試験ガイドの再確認と分野の深掘り : 試験ガイドに記載されている、ベクトルストアやRAGの設計、プロンプトエンジニアリングの適用、エージェンティックAIソリューションの実装など、出題の核となる技術要素を再確認しました。 関連サービスのベストプラクティスの学習 : Amazon Bedrockを使った実際のアプリケーション構築をイメージし、連携するAWS Lambda、Amazon API Gateway、Amazon CloudFrontといったサービス群の詳細な理解を深めました。また、Amazon BedrockガードレールによるAIの安全性とガバナンスや、Amazon Comprehend等を用いたデータ保護についても重点的に学習しました。 長文シナリオへの対応 : 長い文章から即座に「要件は何か」「制約は何か(コスト優先か、レイテンシー優先かなど)」を整理し、構成図をイメージするトレーニングを意識的に行いました。 実際の試験で問われた技術テーマの傾向 # 今回の試験を通して、単なるGenerative AIの機能知識だけでなく、エンタープライズの「本番運用」に直結する実践的な知見が深く問われていると感じました。NDAの範囲内で、特に重要だと感じたテーマをいくつか共有します。 厳格なコンプライアンス要件を伴うユースケース 金融、医療、研究機関といった機密性の高いデータを扱うシナリオが頻繁に登場しました。単にAIを使うだけでなく、「データレジデンシー(特定のリージョン・域内からデータを持ち出さない原則)をどう担保するか」といった法令準拠の観点が求められます。 データ保護と安全性の確保 Amazon Comprehendなどを活用したPII(個人情報)の秘匿化や、Amazon Bedrockのガードレール機能による「暴力・ヘイト等の不適切な表現のフィルタリング」など、AIを安全に提供するためのガードメカニズムの実装手法は必須知識です。 効率的な運用とガバナンス 複数の部署でプラットフォームを共有する際の「部署ごとの権限・コスト分割(IAMやタグ付けの戦略)」をどうアーキテクチャに組み込むかが問われました。また、状況に応じて「アプリケーション側のコードを修正することなく、バックエンドの基盤モデル(FM)を柔軟に切り替える構成」なども重要なテーマでした。 プロンプト管理と稼働中の自動評価・アラート Amazon Bedrock プロンプト管理等を用いた「プロンプトのバージョン管理と本番適用の承認フロー」に加え、稼働中のAIの応答結果をビジネス指標に基づいて継続的かつ自動的に評価し、「品質の低下を検知してアラートを発報する(CloudWatchを通じた異常検知)」といった、リリース後の品質監視運用に対する知識も深く問われました。 マルチアカウント環境での権限管理と閉域網接続 企業での利用を想定し、「アプリケーションアカウント」と「データレイクアカウント」が分かれているような複数アカウント構成において、どのようにセキュアなクロスアカウント権限設定を行うか。また、情報をインターネットに出さずにVPCエンドポイント(AWS PrivateLink)等を経由して閉域網内でAI系APIを呼び出すネットワークアーキテクチャも頻繁に問われました。 生成AI特有のMLOps(LLMOps) 基盤モデルの微調整(ファインチューニング)やRAGの運用において、「データの追加 → モデルの再学習・評価 → テスト → デプロイ」という一連のパイプラインを自動化するMLOps的な知識も一部で求められました。 生成AIのUX向上とフロントエンド連携 アプリケーションのユーザー体験(UX)を向上させるための実践的なアーキテクチャとして、Amplify AI Kit を用いた連携や、Amazon API Gateway 経由でAIの生成結果をストリーミング処理してフロントエンドに返すユースケースなども問われました。 Human-in-the-loop(HITL)ワークフローの設計 AIによる完全自動化だけでなく、AWS Step Functions などを用いて「基盤モデル(FM)の応答内容を最終的に人間がチェック・承認する(Human-in-the-loop)」といった、実運用における安全性を担保するワークフローの設計手法も出題されました。 目的に応じたベクトルデータベースの最適な選定 RAG(検索拡張生成)の構築において、Amazon OpenSearch Serverless を一律に選ぶのではなく、既存データの特性やユースケースに合わせて Amazon Aurora PostgreSQL(pgvector)や Amazon DocumentDB などを最適なベクトルデータベースとして見極める設計力が問われました。 処理時間に合わせたアーキテクチャ選定 推論やデータ処理にかかる「予想処理時間」を事前におおまかに見極め、短時間で終わるなら AWS Lambda、長時間の非同期フローになるなら AWS Step Functions や Amazon EKS、自律的な構成を重視するなら AIエージェント(Agents for Amazon Bedrock等)を活用するといったように、要件に合わせたリソースの正確な使い分けも重要なテーマでした。 コスト・レイテンシ最適化のためのキャッシュ戦略 応答速度の向上やAPIコスト削減の要件において、「推論結果を Amazon ElastiCache や Amazon DynamoDB 等に自前で保存して再利用する」といった従来のアーキテクチャよりも、基盤モデルレベルの組み込み機能である「プロンプトキャッシュ(Prompt Caching)」を素直に活用する構成が、よりスマートな最適解として問われる傾向が見受けられました。 目的に応じた監視手法(監査とメトリクス)の使い分け 監視の要件において、「誰がいつAPIを利用したかといった操作履歴を追う(AWS CloudTrailによる監査機能)」のか、「LLMの応答遅延増やトークン消費量などのパフォーマンス推移を追う(Amazon CloudWatchによるメトリクス監視やアラート)」のかといったように、目的に応じて適切なAWSサービスとアーキテクチャを正確に使い分ける運用知識も重要でした。 制限(クォータ)超過へのレジリエンスとモニタリング AIサービスの時間当たりの利用上限(スロットリング)に達してしまった場合の対策として、「エクスポネンシャルバックオフ(指数関数的バックオフ)」を用いた再試行の実装や、制限到達をAmazon CloudWatch等で適切にモニタリングしアラートを通知する仕組みの構築といった、システムの可用性を高める構成も問われました。 クロスリージョン推論による負荷分散 前述の制限超過対策にも関連して、特定リージョンのトラフィック急増に伴う一時的なスループット低下を回避するため、Amazon Bedrockの「クロスリージョン推論(Cross-region inference)」プロファイルを利用して複数のリージョンへ自動的に負荷分散(ルーティング)を行うアプローチも、非常に実践的な最適化手法として出題されました。 プロフェッショナルレベルの試験では、1つの長文シナリオの中にこれらの観点が3〜4つ同時に絡み合って出題されます。そのため、紛らわしい選択肢の中から最適な設計を選ぶためには、「その課題において達成すべき最優先の要件は何か」を素早く洗い出し、各選択肢がそれらの要件を満たしているかを短時間で正確に判断する総合力が必要不可欠となります。 受験の所感(ついにW全冠達成!) # リベンジとなる今回の受験では、長文問題に対するタイムマネジメントも改善され、ベータ試験の時よりも落ち着いてシナリオを読み解くことができました。学習を重ねたことで、コストとパフォーマンスのトレードオフや、責任あるAIの実装、運用効率の最適化といった観点から自信を持って選択肢を絞れるようになっていたのが大きかったです。 そして結果は合格。無事にAWS側の未取得認定を埋めることができました! 直前の4月11日にGoogle Cloud認定の全冠を達成したばかりだったので、立て続けの朗報に感無量です。 【余談】AIP合格に伴う下位認定の自動更新について # AWS認定には、 公式の再認定プログラム にも記載されている通り、「上位資格に合格すると、関連する下位資格も自動的に更新される」という嬉しい仕様があります。 今回、AIPに合格した場合に具体的にどのアソシエイト・基礎レベル認定が更新対象になるのか、これまで公式ページに明記されておらず不明だったのですが(※本記事公開時点)、今回実際に合格してようやくその詳細が判明しました! 結果として、AIPに合格すると DEA(Data Engineer)、MLA(Machine Learning Engineer)、AIF(AI Practitioner)、CLF(Cloud Practitioner) の4つの認定が一気に自動更新されました。 それぞれの階層(Professional、Associate、Foundational)における、更新の相関関係を整理して図解すると以下のようになります。矢印の元となる認定に合格すると、矢印の先の認定が更新されます。 flowchart TD classDef pro fill:#fce4ec,stroke:#c2185b,stroke-width:2px; classDef assoc fill:#e3f2fd,stroke:#1565c0,stroke-width:2px; classDef found fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px; %% Nodes SAP["SAP<br>(Solutions Architect Pro)"]:::pro DOP["DOP<br>(DevOps Pro)"]:::pro AIP["AIP<br>(GenAI Pro)"]:::pro SAA["SAA<br>(Solutions Architect Assoc)"]:::assoc DVA["DVA<br>(Developer)"]:::assoc SOA["SOA<br>(SysOps)"]:::assoc DEA["DEA<br>(Data Engineer)"]:::assoc MLA["MLA<br>(Machine Learning)"]:::assoc CLF["CLF<br>(Cloud Practitioner)"]:::found AIF["AIF<br>(AI Practitioner)"]:::found %% Subgraphs subgraph Professional SAP DOP AIP end subgraph Associate SAA DVA SOA DEA MLA end subgraph Foundational CLF AIF end %% 更新関係 (AIP) AIP --> DEA AIP --> MLA %% 更新関係 (SAP) SAP --> SAA %% 更新関係 (DOP) DOP --> DVA DOP --> SOA %% 更新関係 (Associate) DEA --> CLF MLA --> CLF MLA --> AIF SAA --> CLF SOA --> CLF DVA --> CLF AIPは生成AIとデータ処理の両領域に深く関わるため、DEAやMLAといった複数の直近に追加されたアソシエイト資格の更新を一度にカバーしてくれる構成になっています。特にDEA、MLA、AIFの3つは2024年に新設されて順次取得した認定であり、来年がちょうど更新時期だったため、今回の合格でまとめて更新されたのは地味に助かりました。複数の資格を維持する立場としては、この更新仕様は非常にありがたい設計ですね。 おわりに # AWS認定全13冠、そしてGoogle Cloud認定全14冠 という、長きにわたる私のクラウド認定挑戦記は、これにて一つの大きな到達点を迎えました。 今回、AWSとGoogle Cloudという主要なクラウドプラットフォームを両方とも体系的に学べたことは、今後マルチクラウド環境での設計や提案の実務に挑むにあたって、間違いなく強力な土台になってくれると信じています。まずは今回の経験や知識の整理も兼ねて、 「AWSとGoogle Cloudのアーキテクチャや思想の違いを対比する記事」 なども積極的に執筆し、皆さんに知見を共有していく予定です。 また、単なる認定保持にとどまらず、こうしたデベロッパーサイトへの継続的な記事の寄稿や情報発信を通して、今後は 「Top Engineer(トップエンジニア)」 としての選出も視野に入れながら、クラウド技術の普及とコミュニティの発展に一層貢献していきたいと考えています。 (短期間にこれだけの認定を一気に取得してしまったため、「数年後に一斉に大量の更新ラッシュが押し寄せてくる恐怖」からは目を逸らしつつ……) これからクラウド認定に挑戦される方の参考になれば幸いです!
はじめに # これまで豆蔵デベロッパーサイトでAWS認定に関する記事を執筆し、直近の2026年3月26日には「 Google Cloud認定全制覇!……まであと一歩で跳ね返されたリアルな軌跡 」という記事を公開しました。 前回の記事では、約2か月の怒涛の受験ラッシュで全冠を目指したものの、最後の1つ「Professional Security Operations Engineer(PSOE)」で不合格となり、無敗記録がストップしてしまった経緯をお伝えしました。 しかしこの度、2026年4月11日にPSOEのリベンジ受験に挑み、無事に合格することができました!これにより、目標であった 「Google Cloud認定全冠」をついに達成 しました。 本記事では、鬼門だったPSOEの突破劇や、前回触れられなかった「再受験ポリシー(Retake Policy)」、PearsonVUE移行に伴う変化、そして今後の目標についてまとめます。 --> Information 秘密保持契約(NDA)があるため、詳細な試験内容については触れることができませんので、ご了承ください。 また記載の情報は2026年4月時点のものです。 待望の「PSOE日本語版」リリースと勉強法の見直し # 前回1月31日に受験した際、PSOEは英語のみの配信でした。セキュリティ特有の特殊なツールが扱われる上に、トラブルシューティングという複雑なシチュエーションを英語で読み解こうとしたことが大きな敗因でした。 そのため、公式の学習プラットフォームである「Google Cloud Skills Boost」を活用し、ハンズオンを通じて「まずは日本語で概念と対処法を理解する」という方針に切り替えて対策を進めていました。 そんな中、朗報が飛び込んできました。なんと 2026年3月31日にPSOEの日本語版がリリース されたのです。PearsonVUEへの移行のタイミングで日本語化される気配はないと前回記事でこぼしていましたが、まさに絶好のタイミングでの対応でした。Skills Boostでの実践的なハンズオン学習に加えて、日本語で問題文の細かなニュアンスを読み取れるようになったことが、今回の合格を大きく後押ししてくれました。 余談ですが、私が2月に英語で苦戦しながら合格した「Professional Cloud Database Engineer(PCDBE)」についても、直近の 4月7日に日本語版がリリース されました。これにより、これからGoogle Cloud認定を目指す方にとっては、 現時点で有効なすべての認定が日本語で受験できる 素晴らしい環境が整ったと言えます! Google Cloud認定の「再受験ポリシー」について # 前回の記事では触れませんでしたが、Google Cloud認定には厳格な 再受験ポリシー(Retake Policy) が設定されています。短期間で合格を目指す上で、この待機期間は大きなネックになり得るため注意が必要です。 1回目の不合格:次回受験までに 14日間 待つ必要がある 2回目の不合格:次回受験までに 60日間 待つ必要がある 3回目の不合格:次回受験までに 365日間 待つ必要がある 私の場合、1月31日に不合格となったため、最短であれば2月14日以降に再受験が可能でした。しかし、演習に使っていたUdemy教材の一部削除による学習方針の転換や、PearsonVUEへの移行タイミングでの日本語版リリースへの淡い期待なども相まって、結果的にしっかりと準備期間を設けて4月11日に受験する形を取りました。特に2回連続で落ちると「60日間(約2ヶ月)」も足止めを食らってしまうため、確実な準備をしてから臨むことが重要です。 見据える先は「Google Cloud Partner All Certification Holders」 # 今回、私がGoogle Cloud認定の全冠にこだわった理由の一つに、 「 Google Cloud Partner All Certification Holders 」 という表彰プログラムの存在があります。これは、Google Cloudの認定資格をすべて保持している方を発表するもので、Google Cloud Japanよりパートナー企業に対して公式に感謝の意が伝えられるプログラムです。直近では2025年11月にも多数の受賞者が発表されています。 将来的に自社がGoogle Cloudのパートナーとして登録されるような機会があれば、その暁にはこの「All Certification Holders」として正式に名を連ねることを密かな目標として見据えています。 最終的な受験履歴(全冠達成!) # これまでの受験履歴の最終版は以下の通りです。 No 受験日 認定名称 略称 受験言語 結果 1 2025-09-15 Associate Cloud Engineer ACE 日本語 合格 2 2025-12-27 Professional Cloud Architect PCA 日本語 合格 3 2026-01-12 Professional Cloud Developer PCD 日本語 合格 4 2026-01-17 Professional Cloud DevOps Engineer PCDOE 日本語 合格 5 2026-01-25 Professional Cloud Network Engineer PCNE 日本語 合格 6 2026-01-29 Cloud Digital Leader CDL 日本語 合格 7 2026-01-31 Professional Cloud Security Engineer PCSE 日本語 合格 8 2026-01-31 Professional Security Operations Engineer PSOE 英語 不合格 9 2026-02-05 Associate Google Workspace Administrator AGWA 日本語 合格 10 2026-02-08 Associate Data Practitioner ADP 日本語 合格 11 2026-02-08 Generative AI Leader GAIL 日本語 合格 12 2026-02-11 Professional Data Engineer PDE 日本語 合格 13 2026-02-13 Professional Cloud Database Engineer PCDBE 英語 合格 14 2026-02-21 Professional Machine Learning Engineer PMLE 日本語 合格 15 2026-04-11 Professional Security Operations Engineer PSOE 日本語 合格 これで、現在提供されているGoogle Cloud認定をすべて網羅することができました。同じテストセンターに何度も通い詰めたため、スタッフさんにも無事に全冠達成の報告ができて感無量です。 PearsonVUE移行に伴う変化と注意点 # 今回の受験から、試験配信プロバイダがKryterionからPearsonVUEへと移行されましたが、実際に受験してみていくつか大きく変わった点や気づきがありましたので、併せて共有しておきます。 1. テストセンターの選択肢増加と、日時変更の柔軟性アップ 私が受験した地域では、Kryterion時代は特定のテストセンター一択でしたが、PearsonVUEに変わったことで複数のテストセンターが選択できるようになりました。また、Kryterionでは試験日時の変更が72時間前でロックされていましたが(手数料を払えば変更可能)、PearsonVUEでは24時間前まで変更可能になったのは受験者にとってかなりの朗報です。 2. 受験システムが使いやすくなった(取り消し線機能) PearsonVUEの試験システムでは、選択肢のテキスト上で「右クリック」をすると、その選択肢に 取り消し線(ストライクスルー)を引くことができます 。明らかに間違っている選択肢を視覚的に除外しながら進められるため、消去法での解答が非常にやりやすくなりました。地味ですがかなり強力で便利な機能です。 3. 合格後の各種通知スピードが格段に早くなった 個人的にとても嬉しかったのが、合格後の正式な通知が送られてくるスピードです。これまでは、Credlyのバッジ発行通知が「翌日6時頃」、Google Cloudからの合格確定通知が「翌々日5時頃」といったスケジュール感でしたが、これが大幅に早まっていました。 Credlyバッジ発行通知:翌日の6時頃 ➔ 受験の約1時間後 Google Cloud合格確定通知:翌々日の5時頃 ➔ 翌日の4時頃 すぐに正式な合格を実感できるのは、受験直後の不安な時間を減らしてくれるため非常にありがたい改善です。 4. 【要注意】全冠達成後はPearsonVUEへの入り口が消滅する これは全冠達成(すべての試験に合格)という特殊な状況ゆえの落とし穴なのですが、未取得の受験可能な試験がなくなってしまうと、Google Cloudの認定ポータルからPearsonVUEのダッシュボードへ遷移するためのアクセス口がなくなってしまいます。 これの何が問題かというと、PearsonVUEマイページにログインできなくなるため、 「領収書の再発行(メールの再送)」の手続きができなくなってしまう 点です。特に「試験を申し込んだ後に受験日を変更した」場合、最初に受領した申し込み時点の領収書と実際の受験日が異なってしまいます。後から正しい受験日が記載された領収書を取り直そうと思ってもポータルに入れないため、会社の経費精算などで困る事態になりかねません。 なお、日本のインボイス制度に対応した「適格請求書発行事業者登録番号」入りの領収書については、PearsonVUE側から別途発行が可能な仕組みにはなっています。 ちなみに、同じくPearsonVUE経由で配信されているAWS認定のポータルでは、既に取得済みの試験からでもPearsonVUEのダッシュボードへ遷移できるためこの問題は起きません。単純に私がGoogle Cloudのポータルで別のアクセス手順を見落としているだけかもしれませんが、もし現在の挙動が仕様であるなら、ぜひGoogle Cloud側にもAWSと同様の改善を期待したいポイントです。 5. 【小ネタ】認定証や有効期限の日付が一時的に米国時間(前日)になる PearsonVUEへの移行による影響か、試験直後に発行される認定証の取得日や有効期限が、システム上で一時的に米国時間ベース(前日付)で登録されることがあるようです。今回、私は日本時間の「4月11日」に受験して合格したのですが、試験当日に発行された認定証は「4月10日」と1日ズレて記載されていました。ただ、翌日になって改めてシステムを確認したところ、正しい日本時間の日付(4月11日)に自動で修正されていましたので、もし皆さんの中で受験直後に日付がズレてしまっている方がいても、慌てずに翌日まで待ってみてください。 おわりに # AWS認定に続き、Google Cloud認定の全冠も達成することができました。 実は、一つだけ未取得となっているAWSの最新認定「Generative AI Developer - Professional (AIP-C01)」が4月14日に正式リリースされるため、4月16日にそちらの受験も予定しています。この認定は以前ベータ版で受験して悔しくも不合格となっているため、私にとってはAWS側での 「リベンジマッチ」 となります。無事に合格できれば晴れて「AWS&Google CloudのW全冠達成」となりますので、その際はまた別の記事でご報告できればと思います。 今回、両方の主要クラウドを深く体系的に学んだことで、マルチクラウド環境での設計・提案力が一段と引き上がったと感じています。今後は、この経験を活かして 「AWSとGoogle Cloudのアーキテクチャや思想の違いを対比する記事」 なども積極的に執筆していく予定です。 一方で、短期間にこれだけの認定を一気に取得してしまったため、 「2年後に一斉に大量の更新ラッシュが押し寄せてくる恐怖」 に早くも震えていたりもします。実は昨年、AWS認定でも同様の更新ラッシュを経験したのですが、AWSには「上位資格を更新すれば関連する下位資格も自動更新される」という素晴らしい制度がありました。Google Cloudには残念ながらその制度がないため、 下位資格も含めすべての認定を一つずつ再受験して更新 しなければなりません。一応は「再認定用の簡易試験(Recertification)」が用意されるという救済措置はあるものの、全冠を維持する道のりを想像するだけで震えが止まりません(笑)。 そして、AWS、Google Cloudと来て、ふと頭をよぎるのは…… 「さて、Azureはどうしよう?」 という問題です。実はAzureに関しても入門資格である「AZ-900」だけは4年前のAWS認定挑戦中に既に取得済みであるため、3大クラウド制覇の道がうっすらと見えてしまっている気もしますが、先述の恐怖の更新ラッシュを考えると少し足踏みしてしまうところです。 ひとまずは、私のGoogle Cloud認定全冠挑戦記はこれにて完結です。これから認定に挑戦される方の参考になれば幸いです!
はじめに # こんにちは。私の参加しているプロジェクトでは、毎朝の朝会で各自のコンディション(ニコニコ、ちょいニコ、普通、ちょいしんど、しんどめ、地獄など)を共有する「ニコニコカレンダー」を活用しています。 先日、私がその日の気分を「ちょいしんど」、理由に「寝不足」と入力していたことから、メンバーに「副鼻腔炎が悪化しましたか?」と心配されました。そこで「最近は副鼻腔炎は落ち着いているんですが、実は子供のネット制限のために徹夜で家庭内ネットワークをいじってまして……」と雑談をしたところ、「興味深いからぜひ記事にしてほしい!」と思わぬリクエストをいただきました。 これまでクラウド資格関連(全冠記など)の記事ばかり書いていたので、「たまには資格以外の息抜き記事も書いてみようかな」と思い立ち、今回筆をとりました。 当サイトの他の記事と違い、少し古い技術の組み合わせにはなりますが、ぜひ息抜きがてら読んでみていただけると嬉しいです。 とはいえ、今回扱っているような泥臭いネットワークの基礎知識(サブネット、DHCP、DNSの仕組みなど)を実際に手を動かして理解しておくことは、クラウド環境を設計・構築する際や、クラウド系の資格を受験する際にも非常に有用な土台となることを、 クラウド認定”ほぼ”全冠の私が保証します! ITエンジニアの皆様におかれましては、お子さんのインターネットやゲームとの付き合い方に悩まれている方も多いのではないでしょうか。我が家で発生した問題と、エンジニアの親として実施した「本気のネットワーク対策」についてご紹介します。 (※なお、本記事に記載しているIPアドレスやサブネットなどのネットワーク情報は、セキュリティの観点から実際の数値から変更してぼかして記載しています) 我が家のネットワーク事情と事の発端 # まずは前提となる我が家の状況(住環境や家族構成、働き方)は以下の通りです。 家族構成、働き方 子供 : 中学生と小学生の2人 妻 : たまに自宅からテレワークをする日がある 私(筆者) : 常に自宅の仕事部屋からフルテレワーク 住環境 : 4DK 周辺住宅からの電波干渉が激しく、2.4GHz帯(11g等)は使い物にならない状態 そのため、障害物に弱いが高速な5GHz帯(11a/ac/ax等)をメインにする必要がある また、夫婦のテレワーク(Web会議等)のため、家中のどこでも「安定したWi-Fi環境」が必須要件 結果として、それぞれの部屋までの物理的な距離や壁を網羅するため、Wi-Fiルータが合計3台必要 子供たちにはそれぞれ学校からタブレットが支給されており、小学生は週に一回、宿題用で持ち帰るのですが、中学生のタブレットは常に自宅に置いている状態です。 ちなみに、子供用のスマホはAndroidを持たせており、そちらはGoogleの「ファミリーリンク」機能を使って利用可能な時間をしっかりと制限しています。 しかしある日、子供が盲点となっていた「学校タブレット」をベッドに持ち込み、夜中にゲーム等をしていることが発覚しました。寝不足で朝起きられなくなり、部活も休みがちになるという悪循環に陥ってしまいました。 (なお、学校タブレットはGoogle Workspaceで管理されているのですが、「Google Workspace側でインストールするアプリや閲覧可能なサイトを制限してほしいものだが。。。」というのが親としての本音です)。 これはいけないと思い、スマホ以外の端末に対しても、最初は以下のような対策を実施しました。 Wifiルータのキッズコントロール機能 を活用し、利用可能時間を制限 プロバイダ提供ルータと家庭用のBuffalo製ルータ(2台)を用いた計3台運用 によるアクセスポイントの分離 中央ルータ(プロバイダ提供ルータ)と私の仕事部屋のルータは同じメインSSIDに設定 子供部屋のルータは別の「子供用SSID」に設定 ゲーム機、学校のタブレット、チャレンジタッチ(進研ゼミ)などの子供用端末は、すべて「子供用SSID」に接続させる なお、この時点ではネットワークは分割しておらず、すべて同じメインのネットワーク(例: 192.168.10.0/24 )内で運用していました。 イタチごっこの始まり # 初期対策で安心したのも束の間、平日の帰宅後にキッズコントロールで許可されている時間内は、勉強をせずにYouTubeを見たりゲームをしたりして過ごすようになってしまいました。 さらに悪いことに、いつの間にか子供が「メインのSSID」に接続しており、再びベッドへ端末を持ち込んでいるのを発見してしまったのです。 スマホはファミリーリンクで制限できていても、他の端末が抜け穴になっては意味がありません。寝不足で部活に行けない、勉強もできていない……。このまま子供の自制心に任せたままにするわけにはいかないと判断し、根本的なネットワーク改修を実施することにしました。 「DHCPの設定で、子供の端末にだけPi-holeのDNSを割り当てればいいのでは?」と思われるかもしれません。しかし、多くの家庭用ルータにはそこまで高度なDHCP機能はなく、また同じサブネット内にいると端末側でDNSを手動設定されるだけで簡単に突破されてしまいます。ネットワーク(サブネット)を物理的・論理的に完全に分けることで、逃げ場をなくす「境界での制御」を重視しました。 今回行った根本的な対策(ガチ構成) # ITエンジニアの親として、物理的・論理的に抜け穴を塞ぐためのネットワーク構成へと変更しました。 実は当初、賃貸物件に無償で付帯しているケーブルテレビ回線のルータがあり、子供用端末はそちらの別回線へ完全に追い出してしまう(物理的な回線分離)という強硬手段も考えました。しかし、無料回線ゆえに下り速度があまりにも遅く、本来の目的である学校の遠隔授業や調べ物に支障が出てしまっては本末転倒です。そのため、あくまでメインの高速回線を生かしつつ、論理的に安全なネットワークを構築する方向で設計しました。 なお、我が家の環境は プロバイダの提供ルータと、一般的な家庭用のBuffalo製ルータの組み合わせ です。設計を考えながら、何度「ああ、VLANさえ使えればどんなに楽だったか……!」と天を仰いだことかわかりません。もし企業向けの多機能ルータなどを使っていれば、VLANを活用してスマートに論理分割できたのですが、今回はあくまで「できるだけ自宅にあるものを組み合わせて一晩で完成させる」ことを裏テーマとし、手持ちの民生機だけで そのまま徹夜で泥臭く一気に組み上げました 。 --> Information 💡 補足: VLAN(仮想LAN)とは 物理的なLANケーブルの配線やルータの接続構成を変えずに、ネットワーク機器(スイッチやルータ)内部の設定だけで論理的にネットワークを安全に分割できる仕組みです。これがあれば、物理的なルータを複数台またがって繋ぎ、手作業で泥臭くサブネットを分離するような苦労は不要になります。 作業中、ふと「豆蔵入社以前に在籍していた会社での社内インフラ構築を思い出すな……」と深夜に我に返りつつも、実施した対策は以下の通りです。 MACアドレス制限の導入 中央のルータにMACアドレスのブラックリストを設定し、子供の端末がメインSSIDに直接繋がらないようにシャットアウトしました。 サブネットの隔離 子供用SSIDのルータを、メインサブネット配下に別のサブネット(例: 192.168.20.0/24 )として構築し、論理的にネットワークを隔離しました。なお、子供用サブネットをメインサブネットの下にぶら下げる構成(NAT)にしているため、メインサブネット側に置かれている家庭用プリンタなどへの通信は引き続き利用可能です。 Raspberry Piによる独自DNS(Pi-hole)の構築 特定のドメインをブラックリストで弾くこと自体は市販のルータの機能でも可能ですが、ルータの標準機能だけでは「有志がインターネット上で公開している膨大なブラックリストをそのまま継続的にインポートして使う」といった柔軟な運用が困難です。そこで、子供用のサブネット内にRaspberry Piを設置し、強力なドメインフィルタリング機能を持つ「Pi-hole」を利用して専用のDNSサーバーを構築しました。 (なぜ自宅にラズパイが転がっているのかは聞かないでください。徹夜で仕上げるために自宅の余り物を深夜にかき集めた結果、今回使用したのは少し古いRaspberry Pi 2Bです)。 DHCPによる独自DNSの強制 子供用ルータのDHCP設定を変更し、ラズパイのIPアドレスをDNSサーバーとして配布するようにしました。 特定ドメインの問い合わせ拒否と正常通信の転送(Upstream DNS) Pi-holeのフィルタリング設定で、ゲームや動画サイトに関するDNS問い合わせをすべて拒否(ブラックホール化)しました。一方で、ブロック対象外の正常な問い合わせ(学習用サイトなど)については、Pi-holeから上位のパブリックDNS(Google DNSの 8.8.8.8 や Cloudflareの 1.1.1.1 など)へ転送させることで、安全な通信のみを許可する構成にしています。 暗号化DNS(DoH / DoT)への対策 実は学校のタブレットがDNS over HTTPS(DoH)などの暗号化されたDNS通信を使用していたため、通常のポート53のフィルタリングだけでは抜け道になってしまいます。そこで、Cloudflareなどの主要なパブリックDNSサービスに対する通信自体も拒否する設定を追加しました。 --> Information 💡 補足: 親(管理者)泣かせの DoH (DNS over HTTPS) 従来のDNS通信(ポート53)は暗号化されていないため、経路上に置いたPi-hole等で「どこにアクセスしようとしているか」を簡単に監視・遮断できました。しかし近年はプライバシー保護の観点から、OSやブラウザがDNS通信をHTTPS(ポート443)の暗号化通信に丸めてしまう機能(DoH)をデフォルトで利用するケースが増えています。これを使われると通信の中身がただのHTTPS通信に見えるため、ローカルのDNSサーバーが意図せずスルー・回避されてしまいます。セキュリティ的には素晴らしい技術ですが、家庭内ネットワークの管理者としては非常に悩ましい壁となります。 以上を踏まえた、最終的な我が家のネットワーク構成図は以下のようになります。 flowchart TD WAN((Internet)) --- CenterRouter[プロバイダ提供ルータ<br>兼 中央ルータ] subgraph MainNetwork[メインネットワーク: 192.168.10.0/24] CenterRouter --- WorkRouter[仕事部屋ルータ] CenterRouter -.- MainSSID((メインSSID)) WorkRouter -.- MainSSID MainSSID --- ParentDevices[親・仕事用端末] MainSSID --- Printer[家庭用プリンタ] CenterRouter -.-x |MACアドレス制限で遮断| BlockedKids[子供用端末] end subgraph KidsNetwork[子供用ネットワーク: 192.168.20.0/24] CenterRouter --- KidsRouter[子供部屋ルータ] KidsRouter -.- KidsSSID((子供用SSID)) KidsSSID --- KidsDevices[子供用端末<br>タブレット・ゲーム機等] KidsRouter --- PiHole[Raspberry Pi 2B<br>Pi-hole] KidsRouter -.-> |DHCPで独自DNSを強制| PiHole PiHole -.-x |特定ドメインの名前解決拒否<br>DoHの問い合わせもブロック| KidsDevices end KidsDevices -.-> |NAT経由で印刷可能| Printer 今後の展望と親の葛藤 # これで現在のところ、子供の夜更かしネットサーフィンは完全に防ぐことができています。 親としては、本来であればこんなシステムでガチガチに管理するのではなく、できれば子供自身の自制に任せて成長を見守りたいというのが本音です。しかし現状では完全に自制に任せきりにすることができず、今回は苦渋の決断としてシステム的な制限を強めることになりました。 ただ、今回の構成も完璧ではありません。子供用サブネットから外部への標準的なDNS通信(UDP 53ポート)は通過させているため、もし今後、子供が知識を付けて、端末のネットワーク設定から自分でGoogle DNS( 8.8.8.8 や 8.8.4.4 )などを設定できるようになれば、この制限は突破されてしまいます。その場合は追加の検討が必要です。 ……とはいえ、もしそこまでのネットワークの仕組みを自力で理解し、制限を突破できるほどの知識を身につけたのであれば、ITエンジニアの親としては「それはそれで喜ばしいことかもしれない?」と、少し複雑な感情も抱いています。いつかはこうした制限をすべて外し、自分自身の力でコントロールできるようになってくれることを願うばかりです。 おわりに # 家庭内ネットワークも、要件(家族の働き方や子供の利用状況、リテラシー、手持ちの機材)に合わせてアーキテクチャを見直していく必要があると痛感しました。同じようなお悩みを持つ方の参考になれば幸いです。
新年度が始まりました。2026年1-3月のサマリーです。 記事数・執筆者数 # この3ヶ月で34本の記事が投稿され、記事数は876になりました。新たに2名が執筆デビューし、累計76名になりました。 連載 # SysML モデリング連載 # 複雑なシステムをモデリングするための新しい言語である SysML v2。この SysML v2モデルを作成・編集するためのグラフィカル・モデリングツール、SysON を紹介するシリーズ。 /blogs/2026/01/08/sysmlv2-tool-syson-intro/ 全6回の連載記事となっています。記事のインデックスは以下のリンクから参照してください。 無料のOSSツールSysONで始めるSysMLv2モデリング スクラムマスターのAI活用 # スクラムマスターの AI 活用に関する連載記事です。 /blogs/2025/12/04/scrum-ai-1/ 全3回です。記事のインデックスは以下のリンクから参照してください。 スクラムマスターのAI活用を考える 記事の筆者石田がこのテーマで「豆寄席」に登壇しました。 https://mamezou.connpass.com/event/386346/ テーマ別の記事 # フロントエンド # /blogs/2026/01/09/nuxt_supabase_auth/ /blogs/2026/02/18/next_storybook_1/ /blogs/2026/02/18/next_storybook_2/ /blogs/2026/03/25/dev-lib-efficiently-using-yalc/ ロボット # /robotics/yaskawa/yaskawa-hses-agent-skills/ /robotics/solar-panel-clean-robot/dji-drone-psdk-introduction/ /robotics/solar-panel-clean-robot/dji-drone-psdk-custom-widget/ /robotics/3dgs/3dgs-beginners-guide/ /robotics/industrial-network/cs-ads-communication/ /robotics/bizen/bizen_sw_architecture_with_grpc/ /robotics/industrial-network/pc_sdk-avoiding-pitfall/ /robotics/lerobot/lerobot_introduction/ AI エージェント # /blogs/2026/01/30/copilot-agent-setting/ /blogs/2026/01/30/kiro_cli_ralph/ /blogs/2026/03/17/use-mcp-on-vscode/ 認定資格 # /blogs/2026/01/12/golden-kubestronaut/ /blogs/2026/03/26/google_cloud_all_certified/ AWS # /blogs/2026/03/06/fms-security-policy-article/ /blogs/2026/03/24/aws-ssm-gitbash-encoding/ /blogs/2026/03/30/quicksight_cicd_part1/ 個人開発 # /blogs/2026/03/26/build-your-own-text-editor/ さいごに # 以上、2025年度第4四半期のサマリーでした。ロボットの記事が非常に多かった印象です。 よかったら フィード の購読、 X や Bluesky でのフォローもお願いします。 Facebook でも本サイトの注目記事をはじめ豆蔵に関するイベントを紹介しています。 note にも時々本サイト関連の記事が掲載されています。
要件定義入門①:要件定義とは何か ~現場での役割と全体像~ # 1. はじめに # 要件定義という言葉はよく耳にするものの、 「実際に何をしているのか分からない」と感じる方が多いのではないでしょうか。 特に現場に入りたての頃は、実装やテストといった開発工程に関わることが多く、 要件定義については「最初にやる工程らしい」という程度の理解に留まりがちです。 そのため、 「要件定義って何をしているのだろう」 と疑問に思う場面も少なくありません。 本記事では、要件定義の役割について、 具体例を交えながら分かりやすく整理していきます。 2. 要件定義とは何か # 要件定義とは、「システムが満たすべき条件(要件)を明確にし、関係者間で合意する工程」です。 ここでいう「要件」とは、 「システムが何を実現すべきか」という条件を指します。 なぜ要件定義が必要なのか # システム開発は、顧客の「やりたいこと(要望)」から始まります。 しかし、この要望を具体化せずに開発側へそのまま委ねてしまうと、 完成したものが「思っていたものと違う」という事態が発生しやすくなります。 さらに、「予算」「人員」「技術的な制約」といった要因により、そもそも実現が難しいケースもあります。 要件定義でやっていること # そのため、要件定義では以下のようなやり取りを繰り返します。 顧客の要望を整理する 実現可能性を開発側で検討する 実現方法や制約を踏まえて提案する 顧客が提案内容を確認・調整する このプロセスを繰り返し、 最終的に「これを作る」という合意内容を定義します。 要件定義は、 「要望」と「現実(制約)」のすり合わせを行い、 実現可能な形に落とし込む工程とも言えます。 また、顧客がすべてを明確にできるとは限らないため、 開発側が質問や提案を通じて要件を具体化していくことが重要です。 3. 設計とはどう違うのか # 要件定義とよく混同されるのが「設計」です。 両者の違いは、以下のように整理できます。 要件定義:何を作るか(What) 設計:どのように作るか(How) 具体例 # 例:ユーザーがログインできる機能。 要件定義 ユーザーがIDとパスワードでログインできる 設計 認証方式はJWTを使用する パスワードはハッシュ化して保存する 要件定義の段階で設計の話をしてしまうと、 後から要件変更があった際に柔軟に対応できなくなります。 そのため、「What」と「How」を意識的に分けることが重要です。 4. 基本的な流れ # 要件定義は、一般的に以下のような流れで進みます。 「要望」と「要求」は似ていますが、本記事では以下のように定義します。 要望:顧客の主観的なやりたいこと 要求:具体化されたニーズ 5. 具体例 # 例:勤怠管理システム。 本節では、要件定義の流れを具体的なケースに当てはめて整理します。 ■ 要望(やりたいこと) # 顧客の初期的な要望は、抽象的な状態で提示されることが一般的です。 (例) 「社員の勤怠を管理したい」 ※この段階では、業務フローや必要な機能は明確になっていません。 ■ 要求(具体化されたニーズ) # 開発側がヒアリングを通じて、要望を具体的な要求へと整理します。 (例) 「出勤・退勤時刻を記録したい」 「スマートフォンから打刻できるようにしたい」 「月ごとの勤務時間および残業時間を集計したい」 ■ 整理・検討(要求の整理・実現可能性の確認) # 要求を整理し、開発側が以下のような観点で検討します。 業務要件として妥当か(業務フローとの整合性) 技術的に実現可能か コスト・スケジュールに収まるか 必要に応じて、要求の取捨選択や優先度付けも行います。 ■ 提案(実現案の提示) # 検討結果を踏まえ、開発側から具体的な実現案を提示します。 (例) 「Webアプリケーションとして提供する」 → PCおよびスマートフォンのブラウザから利用可能とする 「打刻方法はブラウザベースとする」 → 専用アプリ開発は行わない(開発コスト・期間を考慮) 「既存の人事システムとの連携は行わない」 → 初期リリースでは単体運用とし、将来的な拡張を検討する また、必要に応じて以下のような観点も提示します。 機能の優先度(必須 / 任意) スコープ(今回対応範囲) トレードオフ(コスト・品質・スピード) ■ 検討(顧客側確認) # 提案内容について、顧客側で妥当性を確認します。 業務上問題なく利用できるか 必要な機能が満たされているか 不足・過剰な要件がないか 必要に応じて、再度要望の修正・追加をします。 ■ 合意(要件確定) # 顧客と開発側で内容に合意し、 「何をどこまで実現するか」を確定します。 この合意内容が、以降の設計・実装の基準となる「要件」となります。 6. まとめ # 要件定義は「何を作るか(What)」を決める工程 要望と制約をすり合わせ、実現可能な形にする 開発側が主体的に要件を具体化していくことが重要 要件定義は単なる前工程ではなく、 プロジェクト全体の品質を左右する重要な工程です。 次回は、実際に要件をどのように洗い出し、整理していくのかを解説します。
こんなひとにおすすめ # マルチモーダルAI、フィジカルAI、模倣学習、強化学習などロボティクス分野のAIに興味はあるが、どこから手をつけたらいいのか分からない方 実機のロボットの価格が高くて試せないと感じている方 手を動かして学びたいが、低コストで始めたい方 はじめに # 本記事では、オープンソースプロジェクトである LeRobot とオープンソースのアームロボットである SO-101 を題材に、マルチモーダル AI に関する技術紹介と環境構築の手順を解説します。 最終的なゴールは、片方のアームを動かすともう片方のアームが同じ動作を追従する動作を再現することです(下記GIF参照)。 用語の説明 # マルチモーダルAI とは # 従来のロボティクス開発では、ロボット本体、カメラ、通信規格など複数の技術(モダリティ)が個別に動作し、出力されるデータ形式も異なるため、それらを統合して1つのシステムとして運用するのは容易ではありませんでした。 近年、Transformer 系モデルなどの進展により、画像・音声・テキスト・センサー値など複数モダリティを統合して扱う「マルチモーダルAI」の研究・実装が進んでおり、異なるデータ形式の扱いや統合が以前より容易になっています。 これに伴い、ロボティクス領域では前述の問題点の解消が期待され、マルチモーダルAIを活用した開発が増えつつあります。 --> Information 似たような用語として「フィジカルAI」が存在します。こちらはロボットや物理世界での学習・意思決定に焦点を当てる用語で、マルチモーダルAIと重なる部分が多いですが、本記事では区別せず「マルチモーダルAI」として扱います。 LeRobot とは # 公式リポジトリ: LeRobot LeRobot は Hugging Face が公開しているオープンソースのロボティクスライブラリで、実ロボット向けのモデル、データセット、ツールを提供しています。LeRobot を使うことで多様なロボットの制御やデータ収集、学習ワークフローの構築が容易になります。 本記事ではLeRobotのインストールと設定に焦点を当て、最終的にSO-101を動作させる手順を説明します。 [1] SO-101 とは # 公式リポジトリ: SO-101 SO101 Follower の画像。 実物のロボットに近しい構成となっている。 SO101 Leader の画像。 人間が操作しやすいようにグリップ部分がある。 SO-101 は RobotStudio と Hugging Face が共同で開発した、低コストのオープンソース・ロボットアームで、ロボティクス分野への参入ハードルを下げることを目的としています。 SO-101 は「Leader(先導者)」と「Follower(従者)」の2台のロボットアームで構成されます。一般的な利用では Leader をユーザーが手動で操作してデータ収集をし、Follower はその記録や学習済みモデルに基づいて同じ動作を再現することを想定しています。 本記事では SO-101 の入手方法 と、LeRobot で動かす際に必要な手順を解説します。 動作環境 # 本記事では環境構築の手順に重点を置くため環境要件は簡略化しますが、LeRobot の多くのチュートリアルが CLI 操作を前提としていることから OS は Linux または macOSを 推奨します。 本記事では整合性を図るために、Linux を前提としています。 --> GPU に関して 将来的に LeRobot と SO-101 で学習するのであれば、VRAM容量が 8GB 以上の GPU も必要となります。 --> あると便利なもの 本記事の手順を遂行するのに必ずしも必要ありませんが、以下の物があると便利です。 2口以上の電源タップ:2台のロボットアームへの電源供給に必要なため。 2口以上で電源供給可能な USB3.0 ハブ:2台のロボットアームをPCに接続するため。 タックシール:複数のモーターや部品を管理しやすくするため。 タックシールに関しては、ダイソーの タックシール キレイにはがせるタイプ 525枚 をおすすめします。こちらは SO-101 のほぼ全てのパーツの大きさに対応しており、下記画像のようにラベリングが可能になります。 タックシールを用いて、モーターをラベリングしている時の画像。 タックシールを用いて、モーターバスをラベリングしている時の画像。 環境構築方法 # こちらでは LeRobot と SO-101 を用いた環境構築の手順を解説します。 手順としては次のように進めますが… LeRobot の環境構築方法 SO-101 の環境構築 事前に SO-101 の印刷(もしくは購入) を確認いただき、SO-101 のパーツを揃えることをおすすめします。 LeRobot の環境構築方法 # LeRobot の環境構築方法は Hugging Face の Installation ページを参照しているため、重要な手順に絞って解説します。 0. 【任意】conda 系のパッケージがインストールされていない場合 # LeRobot は複数の Python パッケージを扱うため、Conda を用いて仮想環境を構築することを勧めている。そのため、Conda がインストールされていない場合は下記コマンドでインストールします。 wget &quot;https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh&quot; bash Miniforge3-$(uname)-$(uname -m).sh 1. 仮想環境構築 # Conda で仮想環境を構築する # 下記の lerobot は構築する環境の名前なので、お好きなものに変えても構いません # その場合、他の手順も同様に変更を加えてください conda create -y -n lerobot python=3.12 Conda の仮想環境をアクティブする conda activate lerobot ffmpeg を仮想環境にインストールする conda install ffmpeg -c conda-forge LeRobot の現在のバージョンでは、 ffmpeg のバージョン 8.x に対応していないので、前のコマンドでインストールされた ffmpeg のバージョンを確認します: ffmpeg -version このとき、 ffmpeg のバージョン 8.x になっていれば、下記コマンドで ffmpeg のバージョンをダウングレードします: conda install ffmpeg=7.1.1 -c conda-forge 2. LeRobot をインストールする # LeRobot はリポジトリのソース、もしくは PyPl からインストール出来ます。将来的に個人開発したい場合は、コードの編集が可能なソースからのインストールを推奨します。 リポジトリのソースからインストールする場合は下記コマンドを実行します。 git clone https://github.com/huggingface/lerobot.git cd lerobot # 編集可能モードで Conda 環境にインストールする pip install -e . PyPl からインストールする場合は下記コマンドを実行します。 pip install lerobot SO-101 の環境構築 # 1. SO-101 の印刷(もしくは購入) # SO-101 のハードウェア構成は 公式リポジトリ で公開されています。 リポジトリの3Dデータを使ってパーツを3Dプリントするか、リポジトリで案内されている 公認販売サイト からパーツキットを購入して、Follower と Leader の両アームを組み立てられます。 注意点として、公式リポジトリで入手できるのは主に外装・機構パーツで、サーボモーターなどの駆動部品は別途用意する必要があります。モーター購入時は公式リポジトリ内の Parts For Two Arms (Follower and Leader Setup): の記載を参照するか、公認販売元のキットを購入してください。 参考として、筆者の場合は公式リポジトリで紹介されていた秋月電子通商のサイトから、以下の2つのキットを購入しました。また、説明の整合性を図るために、これらのパーツを基準として手順を解説します。 [131169]SO-101 オープンソースロボットアームキット Pro版 [131222]SO-101 オープンソースロボットアームキット 3Dプリントパーツ 2. SO-101 のセットアップ # SO-101 のはセットアップ Hugging Face の SO-101 ページを参照しているため、重要な手順に絞って解説します。また、前述でも触れましたが、SO-101 は Leader と Follower の2台のロボットアームで構成されるため、一部の手順が異なる点には留意してください。 初めに下記のコマンドで SO-101 を動かすために必要な SDK をインストールします。 pip install -e &quot;.[feetech]&quot; 2.1. モーターの仕分け 参考リンク: Configure the motors 初めに 前のセクション で用意したモーターを Leader と Follower 用に分けます。Leader 側は複数種類のギア比を持つモーターで構成されるのに対し、Follower 側は同一仕様(1 / 345)のモーターで構成されるためです。 Leader の各関節に割り当てるモーターID ( 後述を参照 )とギア比は次の通りです。 Leader-Arm Axis Motor Gear Ratio Base / Shoulder Pan 1 1 / 191 Shoulder Lift 2 1 / 345 Elbow Flex 3 1 / 191 Wrist Flex 4 1 / 147 Wrist Roll 5 1 / 147 Gripper 6 1 / 147 モーターの種類は本体のラベル(シール)で判別できます。Leader 用モーターにはギア比が明記されていることが多く、Follower 用は同一仕様のためギア比の表記がないことがあります。 2.2. MotorBus(モーターバス)のセットアップ モーターの仕分けが終わりましたら、モーターバス(下記画像参照)の設定します。 <img width=400" src="https://akizukidenshi.com/img/goods/L/131540.jpg"/> モーターバスは複数のモーターをまとめて管理・通信するための機器です。 前セクション で分けた Leader 用の全モーターに対して1台、Follower 用の全モーターに対して1台のモーターバスを用意します。このとき、Leader と Follower のモーターを誤って混同すると後で修正が大変なので、タックシールなどで全モーターにラベルを貼り、どのモーターバスに対応させるかを明確にするといいです。 以降の手順では便宜上、次の略称を使います。 Leader用モーターバス:Leader_MB Follower用モーターバス:Follower_MB Leader_MB と Follower_MB の電源を入れ、PC に接続する。 各モーターバスの USB ポートを確認する。 両方のモーターバスを接続した状態で下記を実行する。 スクリプト実行中に指示が出たら、Leader_MB の USB ケーブルを抜いて Enter キーを押す。 lerobot-find-port 例)スクリプトの出力例: Finding all available ports for the MotorBus. ['/dev/ttyACM0', '/dev/ttyACM1'] Remove the usb cable from your MotorsBus and press Enter when done. #(対応する Leader_MB のケーブルを抜いて Enter を押す) The port of this MotorsBus is /dev/ttyACM0 Reconnect the USB cable. #(対応する Leader_MB のケーブルを差し直す) Reconnect the USB cable. が表示された時点で Leader_MB のケーブルを差し直してください。上記例では /dev/ttyACM0 が Leader_MB のポートとなるので、こちらを記録してください。 同様に Follower_MB の USB ケーブルを抜いて、Follower_MB のポートを確認・記録してください。上記例では /dev/ttyACM1 が Follower_MB のポート番号になりますが、環境によっては異なることには注意してください。 --> Linux のデバイス権限に関して Linuxではデバイスファイルのアクセス権が原因で認識できないことがあるため、必要に応じて以下を実行して権限を付与してください。下記例では ttyACM0 デバイスの権限を変更しておりますが、デバイス名は環境により変わることには注意してください。 sudo chmod 666 /dev/ttyACM0 2.3. モーターとモーターバスを連動させる 出荷時のモーター ID はすべて 1 に設定されているため、モーターバスと正しく通信・連動させるには各モーターに一意の ID を割り当てる必要があります。 まずは Leader_MB を設定するために、下記コマンドを実行してください。 lerobot-setup-motors \ --robot.type=so101_follower \ --robot.port=/dev/ttyACM0 # Leader_MB のポート番号 --robot.port= には MotorBus(モーターバス)のセットアップ で確認した Leader_MB のポートを入力します。確認しポートが /dev/ttyACM0 以外であれば、適切に変更してから実行してください。 PC と Leader_MB の通信が確率すると、下記メッセージが表示されます。 Connect the controller board to the '<関節名>' motor only and press enter. ここで表示される &lt;関節名&gt; は、これから ID を割り当てるモーターが担当する関節名(例: gripper )を示します。手順は次の通りです。 モーターの仕分け で用意したモーターとその章のテーブルと照合して、該当する種類のモーターを準備する(例:gripper → ギア比 1 / 147 のモーター) 該当モーターをモーターバスに接続する(モーター付属ケーブルを使う) ターミナルで Enter キーを押す ID 割り当てが正常に行われると、次のように表示されます。 '<関節名>' motor id set to <ID> 続けて他の Leader 用モーターにも同様に ID を設定してください。これら手順の一連の流れは下記の公式動画から確認できます。 Leader_MB の設定が終わったら、同じように Follower_MB の設定します。前述の通り、Follower 側のモーターは同一のギア比で構成されていることが多いので、モーターが担当する関節名を照合する必要はありません。ただ、設定した ID を混同しないよう、留意してください。 --> アドバイス 公式リポジトリの動画ではモーターを既にアームに取り付けた状態で ID を設定していますが、筆者は取り付け前に ID を設定することをおすすめします。Leader 側は複数のギア比の異なるモーターで構成され、取り付け済みだと種類判別が難しく、誤った位置に取り付けると分解・ID 再設定が必要になるためです。 事前にIDを振り、ラベル(タックシール等)で識別してからアームに組み付けるとトラブルを大幅に減らせます。 2.4. ロボットアームの組み立て SO-101 の組み立て手順は多岐にわたるため、まずは公式チュートリアル動画や販売元の組み立て動画を参照してください。 LeRobot 公式サイトのチュートリアル WoWRobo が公開している組み立て手順の動画 --> Warning 組み立て後は、各公式ページやチュートリアルで案内されているキャリブレーション手順に従ってモーターの位置合わせを必ず行ってください。キャリブレーションが不十分だと関節角がずれて想定外の姿勢で動作し、周囲の物や人にぶつかるなど危険が生じます。 --> アドバイス 組み立てに関しては、上記のいずれかの動画を見ていけばいいのですが、サーボホーン(下記画像参照)でいくつかアドバイスをしたいと思います。 サーボホーンにロボットアームの外装・機構パーツを固定する際は、大きなネジを使います。モーター本体をロボットアームに固定するためには、小さなネジを使います。 サーボホーンは「凸部(突起)」が外側(表)に見える向きではなく、突起が見えないように裏返して取り付けてください(平らな面を表にする)。裏表を誤るとネジが最後まで締まらず、アームの可動域やスムーズさに影響することがあります。 [2] 3. 動作確認 # 参考リンク: Imitation Learning on Real-World Robots 最後に、LeRobot が提供する模倣学習のサンプルコードを使って、Leader から Follower へのテレオペレーションで動作確認を行います。モーターバスがPCに接続されていることを確認してから、下記コマンドを実行します。 lerobot-teleoperate \ --robot.type=so101_follower \ --robot.port=/dev/ttyACM0 \ # MotorBus(モーターバス)のセットアップで確認した Follower_MB のポートを入力してください --robot.id=my_awesome_follower_arm \ # 好きな変数名を入力してください --teleop.type=so101_leader \ --teleop.port=/dev/ttyttyACM0 \ # MotorBus(モーターバス)のセットアップで確認した Leader_MB のポートを入力してください --teleop.id=my_awesome_leader_arm # 好きな変数名を入力してください 今までの手順が正しく行われていれば、 はじめに の章の GIF のように、Leader を動かすとFollower が追従して動作します。 最後に # 本記事は LeRobot と SO-101 を使ったフィジカルAI入門として、環境構築と基本的な動作確認手順を紹介しました。今後も機会があれば、LeRobotを使った学習(模倣学習・強化学習)や、ロボティクス領域のAI手法の詳細について掘り下げていく予定です。 機会があれば、学習方法に関して記事を書くかもしれません。 ↩︎ 著者はこれのせいで、組み立て済みのロボットアームを分解して、再度組み直すことになりました。 ↩︎
はじめに # こんにちはDX戦の檜尾です。 初めての投稿になりますドキドキ。 日々の業務において、AWSQuickSightのダッシュボード定義をコードとして管理する「BI as Code」の重要性が高まっていると感じます。 従来のGUI上での直接編集はアジリティが高い反面、変更履歴の追跡や誤操作によるロールバックが困難になるという運用上の課題を抱えています。 AWSではこれらの問題に対してビジネスインテリジェンス運用 (BIOps)という考えを適用しようとしています。 DevOpsで行っていたことをBIでも適用できるのではないかということですね。 本記事では、QuickSightの定義ファイルをGitでバージョン管理し、安全に運用するためのCI/CDパイプライン構築手順を全3章に分けて解説します。 第1章:自動アップロード(本記事) QuickSight上でのダッシュボード公開をトリガーとし、定義ファイル(JSON)を自動的にGitHubへバックアップする仕組みを構築。 第2章:自動テスト(次回予定) エクスポートされた定義ファイルに対する静的解析や、依存するデータセットの整合性チェックを自動化する仕組みを解説予定。 第3章:自動デプロイ(次々回予定) GitHub上でレビュー・マージされた定義ファイルを、別環境(本番環境等)のQuickSightへ自動でデプロイするパイプラインを解説予定。 今回はベースとなる 第1章:自動アップロード環境の構築 について、具体的なアーキテクチャと実装手順、および構築時に陥りやすい技術的な仕様(Tips)を解説します。 第1章:自動アップロード環境の構築 # 本章では、QuickSightの「Asset Bundle API」を活用し、イベント駆動型でダッシュボード定義を抽出・保存します。 1. 全体の流れ # AWS CloudTrail / Amazon EventBridge : QuickSightにおけるダッシュボードの公開( UpdateDashboard / CreateDashboard )を検知。 AWS CodeBuild : EventBridgeをトリガーとして起動し、Pythonスクリプトを実行。 AWS QuickSight (Asset Bundle API) : CodeBuildからのリクエストに応じ、ダッシュボードの定義をJSON形式でエクスポート。 GitHub : 抽出されたファイルをCodeBuildが対象リポジトリへCommitおよびPush。 2. 実装手順 # Step 1: GitHub認証情報のAWS Secrets Managerへの登録 CodeBuildがGitHubへアクセスするためのPersonal Access Token (PAT) を発行し、AWS Secrets Managerに保存します。 シークレットのタイプ : その他シークレットのタイプ キー/値のペア : UIのキー・値入力ではなく、「プレーンテキスト」タブから以下のJSON形式で保存する必要がある。理由はCodeBuildのソースフェーズで ServerType is required エラーが発生する為。 { &quot;ServerType&quot;: &quot;GITHUB&quot;, &quot;AuthType&quot;: &quot;PERSONAL_ACCESS_TOKEN&quot;, &quot;Token&quot;: &quot;ghp_から始まるPAT&quot; } シークレット名 : QuickSightGitHubToken Step 2: 実行スクリプトの配置 バックアップ先となるGitHubリポジトリの直下に、エクスポート処理を担うPythonスクリプトとCodeBuildのビルド仕様ファイルを配置します。 1. export.py Boto3を使用してAsset Bundle Export APIを呼び出します。今回はダッシュボード定義のみを対象とするため、 IncludeAllDependencies=False を指定しています。 import boto3 import time import requests import zipfile import io import os import uuid account_id = os.environ['AWS_ACCOUNT_ID'] raw_dashboard_id = os.environ['DASHBOARD_ID'] # EventBridgeからARNが丸ごと渡ってくる region = os.environ['AWS_REGION'] dashboard_id = raw_dashboard_id.split('/')[-1] client = boto3.client('quicksight', region_name=region) job_id = str(uuid.uuid4()) arn = f&quot;arn:aws:quicksight:{region}:{account_id}:dashboard/{dashboard_id}&quot; print(f&quot;Exporting dashboard: {dashboard_id}&quot;) # エクスポートジョブの開始 client.start_asset_bundle_export_job( AwsAccountId=account_id, AssetBundleExportJobId=job_id, ResourceArns=[arn], IncludeAllDependencies=False, ExportFormat='QUICKSIGHT_JSON' ) # 非同期処理の完了待機(ポーリング) while True: response = client.describe_asset_bundle_export_job( AwsAccountId=account_id, AssetBundleExportJobId=job_id ) status = response['JobStatus'] if status == 'SUCCESSFUL': url = response['DownloadUrl'] break elif status in ['FAILED', 'FAILED_PARTIAL']: raise Exception(&quot;Export failed!&quot;) time.sleep(5) # アーカイブのダウンロードと展開 res = requests.get(url) with zipfile.ZipFile(io.BytesIO(res.content)) as z: z.extractall(&quot;quicksight_backup&quot;) print(&quot;Download and extraction complete.&quot;) 2. buildspec.yml CodeBuildの動作を定義します。 version: 0.2 env: secrets-manager: GITHUB_TOKEN: &quot;QuickSightGitHubToken:Token&quot; phases: install: runtime-versions: python: 3.11 commands: - pip install boto3 requests build: commands: - python export.py post_build: commands: - git config --global user.name &quot;QuickSight Auto Backup&quot; - git config --global user.email &quot;bot@example.com&quot; - git remote set-url origin https://${GITHUB_TOKEN}@github.com/YourOrg/YourRepo.git - git add quicksight_backup/ - git commit -m &quot;Auto backup dashboard ID - ${DASHBOARD_ID}&quot; - git push origin main Step 3: IAMロールとCodeBuildプロジェクトの作成 CodeBuildに付与するIAMロールには、最小権限の原則に従い以下のポリシーをアタッチします。 quicksight:StartAssetBundleExportJob quicksight:DescribeAssetBundleExportJob quicksight:DescribeDashboard secretsmanager:GetSecretValue CodeBuildプロジェクトを作成し、ソースプロバイダとして対象のGitHubリポジトリを指定します。アカウント認証情報から接続をしておきます。また、環境変数に AWS_ACCOUNT_ID (12桁の数字)を設定します。 Step 4: EventBridgeルールの設定 CloudTrailが有効化されている前提で、EventBridgeルールを作成します。 イベントパターン : { &quot;source&quot;: [&quot;aws.quicksight&quot;], &quot;detail-type&quot;: [ &quot;AWS API Call via CloudTrail&quot;, &quot;AWS Service Event via CloudTrail&quot; ], &quot;detail&quot;: { &quot;eventSource&quot;: [&quot;quicksight.amazonaws.com&quot;], &quot;eventName&quot;: [&quot;CreateDashboard&quot;, &quot;UpdateDashboard&quot;] } } ターゲット設定 : 作成したCodeBuildプロジェクトを指定し、「入力トランスフォーマー」機能を用いてダッシュボードIDを環境変数として渡します。 入力パス : {&quot;dashboard_id&quot;: &quot;$.detail.serviceEventDetails.eventRequestDetails.dashboardId&quot;} 入力テンプレート : { &quot;environmentVariablesOverride&quot;: [ { &quot;name&quot;: &quot;DASHBOARD_ID&quot;, &quot;type&quot;: &quot;PLAINTEXT&quot;, &quot;value&quot;: &quot;&lt;dashboard_id&gt;&quot; } ] } ここまでの実装でダッシュボードを公開すると、自動的に裏側のGithubにダッシュボードのバックアップができるようになります。 3. Tips: 構築時におけるQuickSight APIの技術的制約と解決策 # 自動化パイプライン構築において、QuickSight特有の仕様によりエクスポートが FAILED となるケースがあります。以下に代表的な事象とその解決策を提示します。 事象: APIにおけるローカルファイルデータセットの非互換性 IncludeAllDependencies=True を指定時、 File source type is not supported in Public API というエラーが発生する。 原因 : Asset Bundle APIは、ユーザーが手動でアップロードしたローカルファイル(CSV/Excel等)の抽出に非対応。 対策 : スクリプト側で IncludeAllDependencies=False を指定しダッシュボードのみを抽出する。もしくはデータソースをS3やAmazon Athena経由の参照モデルに改修する必要がある。 第2章・第3章に向けて # 本章により、ダッシュボードの変更が自動的にGitリポジトリへコミットされる環境が整いました。これにより、変更履歴の可視化とバックアップの自動化が達成されます。 次回の 第2章:自動テスト では、取得したJSON定義に対するスキーマ検証や、不要な変更が含まれていないかを自動検知する仕組みについて解説しようとおもいます。 参考資料 # 本環境を構築するにあたり、以下のAWS公式ドキュメントおよび公式ブログを参考にしています。さらに詳細な仕様やAPIのオプションについて知りたい方は、併せてご参照ください。 Amazon QuickSight BIOps – パート3 : API を使用したアセットのデプロイ (AWS公式ブログ) Boto3 Documentation: QuickSight - start_asset_bundle_export_job Python (Boto3) からエクスポートジョブを実行する際の、詳細なパラメータ( IncludeAllDependencies 等)。 AWS CloudTrail を使用した Amazon QuickSight API コールのログ記録 (AWS公式ドキュメント) QuickSightでの操作がどのようにCloudTrailに記録されるか(API Call と Service Event の違いなど)の仕様が記載。 AWS CodeBuild の buildspec リファレンス (AWS公式ドキュメント) buildspec.yml 内で AWS Secrets Manager から安全に認証情報(GitHub PAT)を取得するための構文規則について解説。
2025年10月8日、ロボット産業を揺るがす大きなニュースが飛び込んできました。 ソフトバンクグループがスイスの重電大手 ABB [1] から、ロボティクス部門を買収する記事でした。 ちょうどそのころ私はABBのロボットコントローラと連携するプログラムの開発で日夜格闘していました。 ロボット制御APIである PC-SDK [2] を使った連携を試みましたが、何度も落とし穴に落ちました。 まさに「死にゲー」をプレイしている感じです。何度も失敗を繰り返しながら、APIの動作を確認し、使い方を覚え、最適な手順を考えて1つ1つ問題やタスクを解決していきました。 この体験は今となってはPC-SDK攻略のための私のノウハウになっています。そこで印象に残った落とし穴を10個ピックアップしました。どのような落とし穴があるのか、どのようにして回避したのかを備忘録も兼ねて公開したいと思います。 --> ロボット開発の前提知識 オフラインティーチングやロボット制御APIとは何かを知りたい場合は「 産業用ロボットの教示方法とその応用 」をご覧ください。 PC-SDKとは # ここで紹介するPC-SDKとは、PCからABBのロボットコントローラ/ロボットを制御・監視するための開発キット(ライブラリ)を指します。 .NET Frameworkを使用して、Windows PC上で動作するカスタムアプリケーションを作成できます。.NET Framework依存かつ後述する通信ドライバがWindows専用のためLinuxには対応していないようです。 主な機能 コントローラ状態アクセス: ロボットコントローラの実行状態、ロボットの姿勢取得、I/O信号の読み書き プログラム操作: プログラムのロード、開始、停止 データアクセス: ロボットプログラムの変数の読み書き ファイル転送: PCとロボットコントローラ間でのファイル送受信処理 --> シミュレータ環境での利用 開発時に使用するツールとしては PC-SDK の他に RobotStudio [3] があります。RobotStudioは仮想ロボットコントローラを内包し、GUIアプリケーションでオフラインティーチングが行えるアプリケーションです。PC-SDKは、この仮想ロボットコントローラに対しても接続できるためRobotStudioがあれば実機が無くてもPC-SDKによる開発ができます。 落とし穴 ティア表 # PC-SDK利用時に遭遇する落とし穴をダメージレベルごとにランク付けしました。これをベースに落とし穴を評価します。 ランク ダメージレベル S あり得ないだろ!?どうやって回避するの?精神的ダメージを受けるレベル A え、何で?びっくりしたー。た、たぶん・・・なんとかなるよねレベル B なるほど、まあよくあるよね。やられたなぁレベル C 事前に回避可能 または 落ちても痛くないレベル あくまでも個人の感想です。 🕳️1. Web上の情報が少ない B # 落とし穴 オープンソースのライブラリを使っているとき、解らないことがあればネットで検索しますよね。同じ要領でPC-SDKに関する情報やAPIを検索すると、ほとんど情報がなくABBのサイトかStack Overflowのようなプログラミングに関する題材を扱う英語のQAサイトが表示されます。 日本語で書かれた個人サイトやABB以外のテック企業による説明などはほとんどありません。ABBのサイトもサンプルコードは非常に少ないです。そのため、英語のQAサイトを丹念に調べ、翻訳 [4] しながら内容を確認します。 ただし、あまり有用な情報が得られない場合や5年~10年前の古い情報だったりすることもあります。 対策 オフィシャルサイト(またはネット検索)からAPIリファレンスや取説などがPDFファイルでダウンロードできます。手元に置いておき1次資料としてザックリと内容を把握しておき、解らないことがあればそこから調べます。 最近はGoogleのNotebookLMを使ったりしています。APIリファレンスや取説のPDF、情報として有益なサイトをNotebookLMに登録しておけば、プロンプトで質問ができます。また要約してくれてエビデンスも表示されるので自分で検索するよりも簡単に情報にアクセスできます。 🕳️2. AIが頻繁にハルシネーションを起こす B # 落とし穴 最近は何か解らないことがあれば検索ではなくAIに問い合わせることが多いのですが、 「PC-SDKのAPI xxxx について使い方を教えて」 「xxxxを使ってxxxxxの処理のサンプルを提示して」 などプロンプト入力すると先ほどの「🕳️1. Web上の情報が少ない」の影響か、存在しないAPIや引数が間違えているコードサンプルを出力します。 さらっと自然に嘘をつきます。誤りを指摘しつつ再度プロンプト入力すると、今度は別の引数が間違っていたり、古いコードで動作しないものが出力されたりしてほとんど役に立たないことがあります。 対策 Visual Studioなどでプロジェクトを作成し、PC-SDKのライブラリを参照させ、オブジェクトブラウザでAPIを確認する PC-SDKに付属する abb.robotics.controllers.pc.xml をエディタで開きAPIに関する説明情報を参考とする 🕳️3. ロボットコントローラが見つからないことがある A # 落とし穴 APIではローカルネットワーク上で動作しているロボットコントローラを検索(UDPブロードキャスト)し、見つかったロボットコントローラに対してログインしてロボットコントローラに接続します。 RobotStudioはローカルPC上で動作しているため一瞬で接続できますが、運用環境ではロボットコントローラの検索でタイムアウトになったり、2回目の検索で検知するといった謎の現象が発生しました。 対策 ネットワークインタフェース(NIC)が複数ある環境(4とか8とか)ではどのネットワークを探せばよいのかわからないため検索時にタイムアウトとなり見つからない現象が発生していました。これを解決するには検索する前にロボットコントローラのIPアドレスを指定してあげます。 実機ロボットコントローラへの接続例 var scanner = new NetworkScanner(); // ロボットコントローラのIPアドレスを直接指定して、スキャナの探索リストに登録する // これにより、どのNICを通すべきかPC-SDKが判断する NetworkScanner.AddRemoteController(&quot;xxx.xxx.xxx.xxx&quot;); // 事前に取得しておいたロボットコントローラのUUIDを指定して検索する var systemId = Guid.Parse(&quot;xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&quot;); // systemId, 待ち時間[msec], リトライ数 var controllerInfo = scanner.Find(systemId, 1000,3); if (controllerInfo == null) { throw new Exception(&quot;コントローラが見つかりませんでした。&quot;); } // 実機に接続 _controller = Controller.Connect(controllerInfo, ConnectionType.Standalone); IPアドレスを指定するのならFind()を使う意義は薄いと思いますが、これで解決します。 🕳️4. 制御権の獲得し忘れ C # 落とし穴 ロボットコントローラに接続し、下記のような状態変更を促す処理を行うとエラーになります。 RAPID [5] 変数の値を更新する サーボモーターをOnにする RAPIDプログラムをロード(タスク割り当て)する RAPIDプログラムを開始する 対策 コントローラにMastershipのリクエスト(書き込み権限のリクエスト)して取得してから更新します。サンプルコードや検索すれば例はいくらでも出てきますので慌てることはありません。 Mastershipのリクエスト using (Mastership.Request(_controller)) { // ここで更新処理を記述 } ただし、Mastershipのリクエストに失敗することがあります。リトライ処理を入れたり、例外処理でエラー処理を記述するなどの仕組みが必要です。 なお、コントローラの状態の取得やRAPID変数の値取得などReadOnlyなものはMastershipのリクエストは必要ありません。 🕳️5. 運用環境でロボットコントローラと接続できない S # 落とし穴 開発環境でRobotStudio上の仮想ロボットコントローラには接続できています。しかし、運用環境でロボットコントローラに接続するとIPアドレス等が正しく ping も通るのにロボットコントローラの状態が取得できない。 開発環境の構成 運用環境の構成 対策 RobotStudioをインストールすると、(裏で)ロボットコントローラと通信するためのドライバもインストールされます。これがないと通信できません。 運用環境ではRobotStudioは不要なのでインストールせず、PC-SDK(ライブラリ)だけを利用するとドライバがインストールされていないのでエラーになります。 ABBのサイトから RobotWare_Tools_and_Utilities_x.x.x.zip (x.x.xはバージョン)をダウンロードし、展開して RobotCommunicationRuntime/ABB Industrial Robot Communication Runtime.msi を実行するとドライバがインストールされPC-SDKで接続できるようになります。こんなん解らんて。 🕳️6. リモートPCから RobotStudio に接続できない C # 落とし穴 先ほどの「🕳️5. 運用環境でロボットコントローラと接続できない」を開発環境で検証しました。 PCを2台用意する 一方(Aとする)をアプリケーション動作環境とする もう一方(Bとする)をロボットコントローラとみなしてRobotStudio(仮想ロボットコントローラ)をインストールする AとBに RobotWare_Tools_and_Utilities_x.x.x.zip のドライバをインストールする AからPC-SDKでBの仮想コントローラに接続する 接続テスト環境の構成 上記を試みましたが、あえなく撃沈。接続できせんでした。 対策 RobotStudioはローカルPC上からのアクセスしか受け付けないためリモートPCからの接続はできない仕様のようです。 これはライセンスが絡んでいる(1 RobotStudio 1ライセンス)からではないでしょうか。仕方がないですね。 🕳️7. 運用環境でRAPID を実行できない A # 落とし穴 RobotStudio上ではログインして問題なくRAPIDのロードや実行ができます。しかし、運用環境ではコントローラに接続できましたが、RAPIDのロードや実行を指示すると例外が発生します。何で? 対策 ロボットコントローラに接続する際のデフォルトユーザーは Default User ですが RobotStudioと運用環境とで権限が異なっています。 様々な権限がありますが実行権限とプログラムのロード権限に違いがありました。 権限 RobotStudio 実機 実行権限 あり なし ロード権限 あり なし Default User でもRobotStudioではさまざまな権限が最初から付与されているようですが、実機では権限が付与されていないものがありました。 そのため、実機上に新しくユーザーを作成し、RobotStudio上と同じことができるように権限を付与し、そのユーザーでログインしたところ実行できました。 なお、RobotStudio上での仮想コントローラではユーザーの作成や権限を付与する機能はなく、実機コントローラでのみ可能となっているのもハマった理由として挙げられます。 🕳️8. デジタル出力ができない B # 落とし穴 ロボットコントローラには各種デバイスとデジタル信号(0 or 1)で連携するための物理インタフェース(I/Oポート)があります。この出力ポートに0または1を書き出すと例外が発生し、出力できませんでした。 対策 ABB ロボットコントローラではI/O設定で物理インタフェースのどこにデジタル出力を割り当てるかを指定します。このとき Access Level を指定します。デフォルト値では Default となっています。 Access Level はレベル毎に制御する側のコンテキストでRead/Writeが有効かどうかが異なっています。 Access Level Rapid Local Client in Auto Mode Remote Client in Auto Mode All Write Enabled Write Enabled Write Enabled AWACCESS Write Enabled Write Enabled Read Only Default Write Enabled Read Only Read Only Internal Read Only Read Only Read Only ReadOnly Read Only Read Only Read Only Auto Mode とは人が操作するのではなくプログラムでロボットを動かすモードを指します。 ロボットコントローラが Auto Mode のとき、ロボットコントローラの外部からアクセスしてI/Oを操作するときは一番右の Remote Client in Auto Mode 列となります。 ロボットプログラムの実行は Rapid 列に相当します。 今回はPC-SDKを利用して外部からロボットコントローラをプログラムで制御しているので Remote Client in Auto Mode となっています。 Access Level は Default 行となり、動作モードは Remote Client in Auto Mode 列となります。その重なり部分は Read Only となっていることがわかります。 つまり Access Level が Default だったので書き込みができない状況でした。 Access Level が All でないと書き込みできません。厳しいですね。 というわけで、デジタル出力を割り当てるときの Access Level を All にすることで無事書き込めるようになりました。 --> Information Access Level は新しく追加もできるようです。 🕳️9. 配列のデータ転送が遅い A # 落とし穴 RAPID側での配列の定義 MODULE MainModule PERS num dataArray{100}; ENDMODULE 配列に値を書き込む一般的な記述は下記となります。 // 最初に1回だけ取得しておく RapidData rd = _controller.Rapid.GetRapidData( &quot;T_ROB1&quot;, &quot;MainModule&quot;, &quot;dataArray&quot;); : using (Mastership.Request(_controller)) { for (int i = 0; i &lt; 100; i++) { rd.WriteItem(new Num(i), i); } } このとき、rd.WriteItem()をコールするたびにネットワークアクセスします。そのためトータルで数百[msec]~数[sec]掛かります。 対策 なるべくrd.WriteItem()のコール回数を少なくし、一括でデータを設定するようにします。 RAPID側で RECORD型 で構造体を定義します。 MODULE MainModule RECORD StructData num value1; num value2; num value3; num value4; num value5; ENDRECORD : ENDMODULE C#側はその構造体をUserDefined型として参照できます。 UserDefined型に値を設定するときは以下のようにします。 // 最初に1回だけ取得しておく(RAPID側のStructDataのコピーを作成) UserDefined ud = new UserDefined(_controller.Rapid.GetRapidDataType( &quot;T_ROB1&quot;,&quot;MainModule&quot;,&quot;StructData&quot;)); // 最初に1回だけ取得しておく(RAPID側のStructDataの参照を作成) RapidData rd = _controller.Rapid.GetRapidData( &quot;T_ROB1&quot;,&quot;MainModule&quot;,&quot;StructData&quot;); : using (Mastership.Request(_controller)) { int value1 = 1; int value2 = 2; int value3 = 3; int value4 = 4; int value5 = 5; // UserDefinedに設定するデータを作成 structData = $&quot;[{value1},{value2},{value3},{value4},{value5}]&quot;; // UserDefinedにデータを設定 ud.FillFromString2(structData); // ロボットコントローラにデータ転送 rd.Value = ud; } また、RAPIDのデータ型であるrobtargetやjointtargetは非常にデータサイズが大きいです。一部のデータのみ更新するのであればその値のみ転送し、RAPID側でデータを更新して利用することも有効です。 --> Caution ud.FillFromString2(&quot;[0,1,2,3,4,5,.....]&quot;) のように文字列リテラルで全要素を直接設定できます。しかし、巨大な構造体や配列の場合、途中までしか値が設定されていないことがありましたので注意が必要です。また、パース処理に時間が掛かりますがネットワークアクセスに比べると無視できるレベルです。 --> Caution AIでサンプルを提示してもらうと、おそらく古いAPIかと思われますが、存在しないAPIが提示されコンパイルエラーとなりました。 🕳️10. 実機でRAPIDを実行すると実行時エラーになる S # 落とし穴 RobotStudio上のシミュレータでRAPIDを実行しても問題なく動作し、プログラムの構文チェックも問題なくパスするのに、同じものを実機で動作させると実行時にエラーとなってしまう。 下記の例外が出力されたら要注意!! Operation is illegal in current execution state 実行時エラーなので状態に起因することは分かっていますが、何の状態なのかがさっぱりわかりません。コントローラのログを見ても直接的な原因が記述されていません。 「制御あるある」ですが原因不明の実行時エラーが一番辛いです。 対策 シミュレータ上では動くことから、運用環境との環境設定の違いに原因がありそうだと直感的にわかります。プログラムの開始からどこまで進むとエラーになるかをRAPIDプログラムのソースコードを全コメントアウトし、バイナリサーチ的にコメントアウトを解除して再実行する手順で探しました。(もしかしたらステップ実行で行けたかもしれません) 結果、2つ問題がありました。 I/Oの定義が実機ではされていなかった シミュレータ上で定義してあったI/Oの名前(参照するときは文字列で指定)が見つけられずに実行時エラーとなっていた 割り込みタイマーのトリガー時間が実機では早すぎた 10[msec]としていてシミュレータ上では動作していたが、実機では実行時エラーとなっていた 上記の問題の修正自体は簡単でしたが、見つけるのに手間が掛かりました。環境の違いに起因する実行時エラーにはご注意を。 リスクを軽減するための開発スケジュール # いかがだったでしょうか?大半は開発環境(RobotStudioのシミュレータ)で問題の無かったものが、運用環境(実機コントローラ)で問題となって現れたものとなっています。しかも、穴から這い上がったと思ったらまたすぐに落とされる状況がありました。挫けそうになりますよね。 開発環境では仮想コントローラに接続するユーザーに対して、セキュリティは緩く、大きな権限を持たせています。一方、運用環境ではセキュリティは厳しく、権限も最小限にしているため不具合発生するパターンがよくありました。 運用環境が遠隔地にある場合、現地での対応には人員、時間、移動距離、金銭の面で多大なコストを要する課題があります。そのため、開発・動作確認を開発環境で行い、システムテストのみを運用環境で一括実施する計画を立てた場合、不測の事態によって進捗に遅延が生じる懸念があります。 リスクを回避するため、スケジュール内に複数のマイルストーンを設け、現地での動作確認を段階的に実施することを強く推奨します。また、現地のエンジニアに検証を委託すること(なかなか難しいですが)も、費用対効果の観点から非常に有効な手段であると考えられます。 まとめ # 日本ではFANUCや安川電機(YASKAWA)といった世界トップクラスのロボティクスメーカーのマーケットシェアが高いため、欧州の雄ABBのシェアは数%程度だそうです。 ABBのロボット開発拠点はスイスにあるため、高度な技術課題については日本国内のサポートを経由し、本国の技術者へエスカレーションする場合があります。その際、時差や拠点間の連携プロセスにより、回答までに時間を要した経緯がありました。 PC-SDKについて辛口の内容ではありましたが、ロボットやロボットコントローラの機能や性能は素晴らしく、RobotStudioでのオフラインティーチング環境もトップレベルで使いやすいです。ABBのロボット部門がソフトバンクグループとなったことで日本でのシェア拡大を狙っていてもおかしくはありません。そうなると営業やサポート部門の規模や質もより重厚になると思われます。今後のABBロボット事業の展開に期待します。 今回はPC-SDKの落とし穴と題して幾つか挙げましたがRAPIDにも落とし穴が潜んでいます。機会があればそちらも記事にできればと思います。 エー・ビー・ビーと呼びます。Asea社とBrown Boveri社の合弁で設立(アセア・ブラウン・ボベリ) ↩︎ PCからABB ロボットコントローラに接続するためのSDK(ライブラリ) ↩︎ ABBが提供しているオフラインティーチング(シミュレーション)ソフトウェア ↩︎ DeepL, Google翻訳, ブラウザで右クリックして「日本語に翻訳」など ↩︎ ABBの産業用ロボットを制御するために開発された専用のプログラミング言語 ↩︎
はじめに # これまで豆蔵デベロッパーサイトで、AWS認定に関する記事( 2022年の12冠達成 やその後の新認定取得など)をいくつか執筆してきました。現在、AWS認定は最新の「Generative AI Developer - Professional (AIP-C01)」以外はすべて取得しています。 そんなAWS偏重な私が、今回は「Google Cloud認定の全冠」に挑戦しました。結論から言うと、約2か月で一気に制覇しようと挑んだものの、あと1歩のところで失敗してしまいました。本記事では、Google Cloud認定を目指した経緯や、短期集中で受験した所感、おすすめの受験順番、そしてなぜ失敗したのかについてまとめます。 --> Information 秘密保持契約(NDA)があるため、詳細な試験内容については触れることができませんので、ご了承ください。 また記載の情報は2026年3月時点のものです。 Google Cloud認定を目指したきっかけ # 事の発端は、2025年夏に携わったプロジェクトです。このプロジェクトではGoogle Cloud上で開発が行われていましたが、私の担当はAWS側でのAPI開発だったため、実際にはGoogle Cloud環境を触っていませんでした。しかし、「今後のためにも知っておいた方がいいだろう」と思い立ち、勉強がてらGoogle Cloudの「Associate Cloud Engineer (ACE)」を受験し合格しました。 ちょうどその頃はAWS認定の更新時期と重なっていたため、まずはそちらを優先しました。そして2025年12月に「Japan AWS All Certifications Engineers」の条件を満たしたのを区切りとして、本格的にGoogle Cloud認定の全冠を目指し始めました。 約2か月間の怒涛の受験ラッシュとテストセンター裏話 # 2025年12月末のProfessional Cloud Architectを皮切りに、全冠を目指して2026年1月〜2月にかけて約2か月間で一気に受験を進めました。 以下がその受験履歴です。既に取得済みだったAssociate Cloud Engineerも一覧に含めています。 No 受験日 認定名称 略称 受験言語 結果 1 2025-09-15 Associate Cloud Engineer ACE 日本語 合格 2 2025-12-27 Professional Cloud Architect PCA 日本語 合格 3 2026-01-12 Professional Cloud Developer PCD 日本語 合格 4 2026-01-17 Professional Cloud DevOps Engineer PCDOE 日本語 合格 5 2026-01-25 Professional Cloud Network Engineer PCNE 日本語 合格 6 2026-01-29 Cloud Digital Leader CDL 日本語 合格 7 2026-01-31 Professional Cloud Security Engineer PCSE 日本語 合格 8 2026-01-31 Professional Security Operations Engineer PSOE 英語 不合格 9 2026-02-05 Associate Google Workspace Administrator AGWA 日本語 合格 10 2026-02-08 Associate Data Practitioner ADP 日本語 合格 11 2026-02-08 Generative AI Leader GAIL 日本語 合格 12 2026-02-11 Professional Data Engineer PDE 日本語 合格 13 2026-02-13 Professional Cloud Database Engineer PCDBE 英語 合格 14 2026-02-21 Professional Machine Learning Engineer PMLE 日本語 合格 なお、Google Cloud認定の試験プロバイダは2026年2月23日までKryterionによる配信でしたが、3月からはPearsonVUEに変更になりました。 私が受験した地域では、Kryterion時代は特定のテストセンター一択でしたが、PearsonVUEに変わったことで複数のテストセンターが選択できるようになりました。また、Kryterionでは試験日時の変更が72時間前でロックされていましたが(手数料を払えば変更可能)、PearsonVUEでは24時間前まで変更可能になったのは受験者にとってかなりの朗報です。 ちなみに、私はこの2か月間で同じテストセンターに11回も通った(2回は同日受験)ので、すっかりスタッフの方に顔を覚えられてしまいました。スタッフの方と少し雑談した際に「PearsonVUEに対応する準備をしている」と聞いていたのですが、3月中旬からPearsonVUEにも対応したとのことで、今後も通い慣れたテストセンターを引き続き利用できそうです。 受験料は「米ドル決済」である点に注意 # テストセンターについての余談に関連してもう一つ、実際に短期間で大量受験して痛感したのが 受験料の支払い通貨 の違いです。 AWS認定の受験料は日本円(JPY)で確定決済されますが、Google Cloud認定の受験料はプロバイダ画面上で 米ドル(USD)決済 となります。 Professionalレベルの試験は1回200ドルかかるため、これだけの数を短期間で一気に受験すると、クレジットカードの請求額が為替レートの影響をダイレクトに受けます。会社の資格取得支援制度などを利用して経費精算をする場合は、予算申請の際に為替変動分のゆとりを持たせておくことをお勧めします。 全体的な難易度:AWS認定との比較 # これだけ短期間で多くの試験に合格できたのは、 「合格した認定のほとんどがAWSの知識の焼き直しでクリアできる内容だったから」 です。加えて、Googleが提唱するSRE(Site Reliability Engineering)をしっかりと理解しておくことが大事だと感じました。 また、文章量という点でも難易度に違いがありました。AWS認定のProfessionalやSpecialityは問題文も選択肢も文章が長く、内容を把握するのに苦労しましたが、Google Cloud認定のProfessionalはほとんどがAWS認定のAssociateレベルの長さであり、読み取るのにあまり苦労しませんでした。 Google Cloud試験の鍵を握る「SREの基本概念」 # 複数の試験(特にCloud ArchitectやDevOps Engineerなど)を通して、Googleが提唱する SRE(Site Reliability Engineering) のコア概念は共通言語として頻出します。ここをしっかり押さえておくと、シナリオ問題においてGoogle Cloudが推奨する「正解の行動」がすぐに選べるようになります。 SLI / SLO / SLA の違い : サービスの信頼性を測る指標(SLI)、開発チームと運用チームの共通の目標値(SLO)、顧客とのビジネス上の契約(SLA)の役割の違い。 エラーバジェット(Error Budget) : 100%の可用性を目指すのではなく「許容できる障害の予算」を定め、予算内であれば新機能のリリースを優先し、予算が尽きたら信頼性向上(バク修正など)を優先するという運用ルール。 トイル(Toil)の削減 : 手作業で反復的かつ自己修復されない運用作業(トイル)を、システム化や自動化によって徹底的に減らすこと。 非難なきポストモーテム(Blameless Postmortem) : 障害発生時に特定の個人を責めるのではなく、システムやプロセスの欠陥を分析し、自社システムにおける再発防止の仕組みづくりにフォーカスする文化。 AWSとGoogle Cloudの概念の違い(試験で注意すべきポイント) # AWSの知識があれば解ける問題が多いとはいえ、アーキテクチャの基本概念においていくつか決定的な違いがあり、試験でもここが問われます。代表的なものをいくつか挙げます。 VPCのスコープ  : AWSのVPCは特定の「リージョン」内に作成されますが、Google CloudのVPCは「グローバル」リソースです。VPCの配下に作成するサブネットが各リージョンに紐づくため、複数リージョンにまたがるネットワーク構築の考え方が大きく異なります。 リソースの管理単位(アカウントとプロジェクト)  : AWSでは環境や権限の分離に「AWSアカウント」を境界として使用しますが、Google Cloudでは「プロジェクト」という単位が基本となり、これらを「フォルダ」や「組織」で階層化して管理します。 ロードバランサの配置  : AWSの主要なロードバランサ(ALBなど)はリージョンリソースですが、Google Cloudのグローバルロードバランサ(Cloud Load Balancing)は、単一のAnycast IPアドレスを使用して世界中のユーザーからのトラフィックを最も近いリージョンに振り分けることができます。 各レベル・特徴的な試験の所感 # Google Cloud認定には、AWSのような「上位資格取得による下位資格の自動更新」の仕組みがありません。そのため、有効期限(2年または3年)が切れる前に各資格を個別に更新する必要があります。 ただし、有効期限が近づいた資格保持者向けには通常の新規受験とは異なる 「更新用の試験(Recertification Exam)」 が提供されているため、更新時期には公式の案内に従ってそちらを受験する形になりそうです。 Foundationレベル # 50~60問。90分。$99(税別)。3年間有効。 Cloud Digital Leader (CDL) と Generative AI Leader (GAIL) は、Google Cloudのどのサービスが使えるかという基本的な内容や、AIの一般論などがメインでした。試験時間は90分ですが、45分くらいで解き終わるボリューム感です。 Associateレベル # 50~60問。120分。$125(税別)。3年間有効。 Associate Cloud Engineer (ACE) と Associate Data Practitioner (ADP) は、AWSの知識を焼き直せばすぐに解ける内容でした。こちらは120分ですが、1時間もかからずに完了しました。 少し毛色が違うのが、Associate Google Workspace Administrator (AGWA) です。その名の通りGoogle Cloudに関する内容はなく、Google Workspaceの管理(監査対応、退職者対応、入職者対応、企業合併などで必要な作業等)がメインです。私は独自ドメインのメールアドレスを管理しており、Geminiなどを使うためにGoogle Workspaceに移行していたため、だいたいの概念は理解できておりスムーズに対応できました。 Professionalレベル # 50~60問。120分。$200(税別)。2年間有効。 先に書きましたが、問題文はAWS認定のAssociateレベルの長さであり、読み取るのにあまり苦労しませんでした。ただし、たまに長文が出てくることもありますので油断は禁物です。 特徴的な問題 # Professional Cloud Architect (PCA) では、画面の半分くらい(調整可能)に事例会社の説明が表示され、その内容を基に回答を選択するケーススタディ問題がありました。ただし、こちらは2026年3月30日に試験内容が更新される予定のため、今後形式が変更になる可能性があります。 また、Professional Cloud Network Engineer (PCNE) に関しては、公式が発表している問題数の上限(50〜60問)である「60問」がみっちり出題されました。他のProfessionalレベルの試験より問題数が多くなるケースがあるため、集中力を維持する体力的なハードルも少し高めでした。私は39問目の回答中に問題数に気づいたため心理的ダメージが大きかったです。最初に問題数を確認することをお勧めします。 唯一の壁、PSOEの難しさと反省点 # 順調に進んでいた全冠への道ですが、ちょうど折り返し地点であるProfessional Security Operations Engineer (PSOE) で不合格となり、ここで無敗記録がストップしてしまいました。 他の試験(AWS認定やPSOE以外のGoogle Cloud認定)は「どのように設計するか」が主に問われますが、PSOEは「問題が起きた時にどのように対処するか」に重きを置いています。扱われるセキュリティツールも特殊であり、そもそも英語が苦手なのに「英語の問題を解いて理解しようとした」のが間違いでした。 PSOEは日本語未対応です。PearsonVUEに移行するタイミングでついでに日本語化されることを願っていましたが、今のところその気配はありません。英語のみのProfessional Cloud Database Engineer (PCDBE) は内容が単調だったため何とか読み取れましたが、PSOEの複雑なシチュエーションは苦戦しました。 勉強方法の転換と「Google Cloud Skills Boost」の活用 # これまで私は、サードパーティの教材(Udemyの演習問題など)を活用して対策を進めていました。しかし、演習に使用していたUdemyの教材の一部が削除されてしまったこともあり、今後は方針を転換します。 これからは、公式の学習プラットフォームである 「Google Cloud Skills Boost」 をしっかりと使用していく予定です。 Google Cloud Skills Boostとは? # Google Cloudが公式に提供しているオンデマンドの学習プラットフォームです。主に以下のような特徴があります。 実践的なハンズオンラボ: 用意された一時的なGoogle Cloud環境を使って、実際のコンソール画面やCLIから手を動かしながら学ぶことができます。 認定試験向けの学習パス: 各資格試験の出題範囲に合わせたコースやクエストが体系的にまとめられています。 日本語での概念理解: 各種サービスの概念やベストプラクティスを解説する動画やドキュメントが充実しています。 PSOEのような実践的なトラブルシューティングが問われる試験では、単なる暗記ではなく実際の挙動を知っておく必要があります。そのため、Skills Boostのハンズオン等を活用して 「まずは日本語で概念と対処法を理解する → その後英語で読めるようにする」 という順番で対策を進めるべきだと痛感しました。 おすすめの受験順番と難易度(完全主観) # これからGoogle Cloud認定を目指す方に向けて、私がおすすめする受験順序と主観的な難易度(★5段階)を表にまとめました。 基本的な戦略として、 「インフラ系 → データ系 → 機械学習系 → 管理系」 の順に進めるのがベストです。最初のインフラ系でネットワーク境界やセキュリティ周りをしっかり理解できているため、データ系以降の学習でその知識をそのまま活かすことができます。 振り返ってみると(まだ全冠は終わっていませんが)、完全ではありませんが我ながら非常に理にかなった順番で受験していたなと感じています。 受験順 分野 認定名称 難易度 備考・おすすめの理由 1 インフラ系 Cloud Digital Leader ★☆☆☆☆ どこでも可。 ACEの前なら軽く知識が入る。 上位の資格取得後ならほぼ勉強なしで合格できる。 2 インフラ系 Associate Cloud Engineer ★★☆☆☆ 3 インフラ系 Professional Cloud Architect ★★★☆☆ 4 インフラ系 Professional Cloud Developer ★★★☆☆ 5 インフラ系 Professional Cloud DevOps Engineer ★★★☆☆ 6 インフラ系 Professional Cloud Network Engineer ★★★★☆ 7 インフラ系 Professional Cloud Security Engineer ★★★☆☆ 8 インフラ系 Professional Security Operations Engineer ★★★★★ 英語のみ。 9 データ系 Associate Data Practitioner ★★☆☆☆ 10 データ系 Professional Data Engineer ★★★★☆ 11 データ系 Professional Cloud Database Engineer ★★★☆☆ 英語のみ。 どこでも可。 データを扱うという点でPDEの付近がおすすめ。 12 機械学習系 Generative AI Leader ★☆☆☆☆ どこでも可。 PMLEの前なら軽く知識が入る。 PMLEの後ならほぼ勉強なしで合格できる。 13 機械学習系 Professional Machine Learning Engineer ★★★★★ 14 管理系 Associate Google Workspace Administrator ★★☆☆☆ どこでも可。 おわりに # 約2か月での「無敗での全冠制覇」という目標は、唯一不合格となったPSOEによって阻まれてしまいましたが、AWSの知識ベースがあればGoogle Cloudのキャッチアップも非常にスムーズに行えることが実証できました。 当初は3月中のリベンジを考えていましたが、諸事情と教材の見直しにより4月に先延ばしにしました。4月にはSkills Boostを活用した新たな勉強方法でPSOEにリベンジし、次こそは「Google Cloud全冠達成」の記事を書けるように頑張ります!
テキストエディタ難民 # 皆さん、テキストエディタは何を使っているでしょうか。 最近だと、VS Code ですかね。猫も杓子も といった感じですし。 でもわたし、VS Code は好きになれないんですよね。ゴチャゴチャしていて。 なので Sublime text をメインに使っていましたが、日本語の扱いが微妙な所があったり、巨大なファイルを開くのが遅かったりと不満もあり、状況に応じて色々なテキストエディタを切り替えて使う難民生活を送っていました。 コーディングには IDE を使えばいいので、日常のテキスト編集をストレスなく行える、ただそれだけのシンプルなエディタがほしかったのです。 具体的には以下のような感じです。 Markdown でメモ取り コードフェンスでシンタックスハイライトはほしい 表を Markdown テーブルとしてペーストしたい(特にペーストできないことで有名なパワポの表も貼り付けたい) 巨大なログ・ファイル確認 素早く開いてエラー箇所をフィルタリングしたい Grep して調査したい SQL や JSON の加工 マルチカーソルで編集したい インデント整形したい 巨大 CSV ファイル編集 カラム毎に揃ったインデントで操作したい マルチプラットフォーム 異なる環境でもキーバインドは同じにしたい 設定やプラグインをこねくりまわすことなくアウトオブボックスで使いたい 全角スペースやタブ表示 キャレットのある行だけ表示したい セッションの保存 未保存で閉じても前回内容を復元したい 難民からの脱却 # 難民生活にも疲れてきた折、自分でサクッと作ったほうが早いのではないか? そう思い立ち、テキストエディタを自作して、現在は難民生活を抜け出すことができました。 しかし、全然「サクッと」とはいかず、思った以上に大変でした。 見返してみると、初回コミットは Sep 12, 2022 となっています。 単純なテキスト編集だけであれば割とすぐに動くのですが、普段使いできるレベルになるまでには1年以上かかった気がします。その上、未だに変更加えてます。 どうしても、夜に数十分だけ実装するという細切れの進め方になってしまうので、翌日には何をどこまで進めたかも忘れているという感じで、モチベーション維持を含め、ある程度まとまった時間で集中して作業しないと進まないなぁ というのが印象です。 学生のように時間のある時期ならまだしも、この年になってやるものではないなと思いますが、ほしい機能があればすぐに追加できるので、思い通りにカスタマイズ可能なエディタが手に入ったと思うことにしています。 ということで本稿では、テキストエディタの自作をはじめるにあたって、知っておきたかった事柄について記しておこうと思います。 テキストバッファ # テキストエディタを自作するにあたり、最初に考えなければならないのが、文字シーケンスをどのようなデータ構造で扱うか になります。 世の中のテキストエディタは大抵、Gap Buffer、Linked List、Rope、Piece Table のいずれかのデータ構造を元にして、独自の工夫を施しているので、これらを見ていきましょう。 Array # 最初は、最も単純な例から始めます。文字シーケンスを配列として扱う方法です。 以下のように文字シーケンスを単にバイト配列として扱うことを考えます。 byte[] bytes = Files.readAllBytes(path); この時、 This is apple. という文字シーケンスは以下のような連続したメモリレイアウトとして確保されます。 an という文字列を挿入するには、元のサイズより 3 大きい配列を再確保し、文字を新しい配列にコピーして設定する必要があります。 当たり前の話ですが、編集の度に新しいメモリ領域を確保しなおす必要があるため、非常に効率が悪いです。 そこで、メモリ領域を余分に確保しておき、バッファとして利用することを考えます。 Buffer # テキスト編集の度にメモリ領域を再確保するのは明らかに非効率なため、メモリ領域中に未使用のバッファ領域を用意します。 概念的には、以下のような Buffer を考えることになります。 class Buffer { byte[] bytes; int length; } 5要素分の未使用領域(網掛け部分)をバッファとして確保した場合は以下のようになります。 文字列の挿入時には、新しい配列を作成することなく、値の設定と移動で編集操作が完結します(バッファ領域が不足した場合には、新たなメモリ領域を確保してコピーする必要があります)。 文字列の編集をバッファ領域で吸収することで、余計なメモリの確保が削減できました。 しかし、編集位置から末尾まで値を移動させる必要があり、もう少し工夫の余地がありそうです。 Gap Buffer # 多くのテキスト編集操作は、カーソル位置に対して行われる という事実を利用したものが Gap Buffer です。 先程の例では、メモリの末尾にバッファを用意しましたが、 Gap Buffer では、カーソル位置(キャレット位置)にバッファを設けます。 概念的には、以下のような GapBuffer として考えます。 class GapBuffer { byte[] bytes; int gapIndex; int gapLength; } 現在のカーソル位置がオレンジの矢印にあり、Gap として5要素分の未使用領域を割り当てた場合は以下のようになります。 カーソルを移動する際、その移動に合わせてバッファ位置を移動させていきます。 これにより、テキストの編集操作を常にバッファ位置で行うことができます。先の例で見た、編集点以降の値のシフトが不要になりました。 バッファの開始位置と長さがわかっているため、インデックスを指定したランダムアクセスも簡単に実現できます。 Gap Buffer は、Emacs でも使われている効率の良いテキストバッファの実現手法になります。 さて、ここまでは文字シーケンスを1つの塊として扱う例を見てきましたが、これらを複数の小さな塊として分割統治することも考えられそうです。 Linked List # 文字シーケンスを1つの連続した領域として管理するのではなく、小さな塊として分割統治できれば、編集操作を局在化して扱うことができます。 そこで真っ先に思いつくのは、文字シーケンスを特定サイズのチャンクに分割したノードとして扱い、連結リストとして管理する方法です。 概念的には、以下のようなクラスを考えることになるでしょう。 class LindedList { Node head; Node tail; } class Node { byte[] chunk; Node next; Node prev; } 例えば、先の例を、単語単位のチャンクとして扱った場合は以下のような構造になります。 テキストの編集処理は、新しいノードを作成してリンクの張り替えを行うだけで済むため効率も良さそうです。 ここでは説明のため単語単位のチャンクとして扱いましたが、細かくし過ぎるとリンクポインタのメモリ使用量が多くなるため、通常は1行をチャンクとして扱う実装が現実的です(改行の挿入でノードの分割、行の削除でノードの結合)。 文字シーケンスを連結リストとして扱うのは、実装が簡単ですが、要素へのランダムアクセスが遅いという欠点があります。 インデックスアクセスを行う場合は、先頭からリストを辿る必要があり、大きなテキストファイルでは特に非効率です。通常は、キャレット位置をカーソルとして、カーソルベースで要素にアクセスするなどの工夫が必要です。 では、文字シーケンスをチャンクに分割した上で、インデックスによるランダムアクセスを効率的に行うことはできないでしょうか。 Rope # 文字シーケンスをチャンクに分割し、インデックスアクセスを二分木で追尾できるようにしたものが Rope です。紐(String)を強化した縄(Rope)というわけです。 概念的には、以下のようなクラスを考えます。 class Rope { Node root; } interface Node { int weight(); int totalLength(); } record Branch(Node left, Node right, int weight) implements Node { Branch(Node left, Node right) { this(left, right, left.totalLength()); } public int totalLength() { return left.totalLength() + right.totalLength(); } } record Leaf(String text) implements Node { public int weight() { return text.length(); } public int totalLength() { return weight(); } } ここで、 weight は、左側ノードの文字列長の合計を表します。 Leaf ノードには文字シーケンスのチャンクを保持し、 Branch ノードで weight を管理します。 単語単位のチャンクとして扱った場合は以下のような二分木として管理します。 weight は、そのノードが分割する文字シーケンスの先頭からのインデックス位置となるため、例えばインデックス 10 の位置は、以下のように木を辿ることで到達できます。 ルートノードは、左側の this is と 右側の apple. に分割した中央部分のインデックスを表すため、 10 のインデックス位置は右側木の 2 の位置に存在し、 2 の位置は 5 より小さいため、その左側のノードに存在する といった具合です。 連結リストで問題だった、ランダムアクセスの効率が改善されていることがわかると思います。 前述までと同様に an を挿入する場合は以下のように木を更新することになります。 この更新による weight の更新範囲は、対象ノードを上に辿ったノードに対してのみに限定できる という点に注目してください。 apple と . を含む右側のノードにはなんの影響も及ぼしません。 元の木構造はそのままに、テキストの更新後の木構造を更新箇所を上に辿ったノードに限定してリンクし直すことで、変更後の文字シーケンスを表現できるのです。つまり、以下のように、文字シーケンスの変更をイミュータブルに扱うことができる ということです。 テキストエディタでのUndo操作は、1つ前のルートノードに戻すだけで実現できる点にも注目してください。 このように Rope は文字シーケンスを表現する強力なデータ構造ですが、木構造を維持するためのオーバーヘッドが必要であり、木のバランシング操作も必要です。 加えて、テキストファイルを開いた際に木を構築する必要があるため、巨大なファイルを開くのには時間を要します。 巨大なファイルを効率よく編集できるデータ構造は考えられないでしょうか。それが次に紹介する Piece Table です。 Piece Table # Piece Table は、テキストの変更操作を追記管理するデータ構造です。 Piece Table では、元の文字シーケンスを read only なバッファで管理し、そこに加えられた変更を append only なバッファに配備し、それらのバッファのインデックス位置をテーブルで管理します。 This is apple. というファイルを開いた直後は以下のようになります。 インデックス8の位置に an を挿入する場合は、以下のようにテーブルのインデックスを更新することで変更を扱います。 概念的には、以下のようなクラスを考えることになるでしょう。 record Piece(Buffer target, int index, int length) { int end() { return index + length; } } class PieceTable { List&lt;Piece&gt; pieces; Buffer appendBuffer; PieceTable(Buffer readBuffer) { this.pieces = new ArrayList&lt;&gt;(); this.pieces.add(new Piece(readBuffer, 0, readBuffer.length())); this.appendBuffer = AppendBuffer.of(); } } 元のファイルが巨大でも、 read only なバッファは直接ファイルやメモリマップドファイル として扱うことで、メモリ確保がほぼ不要になり、編集内容は変更分の Piece の管理だけで済みます。 ただし、変更を繰り返すと Piece の数が増加したり、append only なバッファが断片化するデメリットがあります。 テキストバッファのデータ構造まとめ # これまで見てきたデータ構造をまとめると以下の様になります。 データ構造 特徴 Gap Buffer 実装が非常にシンプル カーソル位置での入力が極めて高速( O(1) ) カーソル位置の大きな移動・マルチカーソル操作で遅延が生じる可能性がある バッファ領域の枯渇でメモリの再割り当てが必要 Emacs で利用されている 連結リスト 位置特定後の挿入や削除が一定時間で可能( O(1) ) ランダムアクセスが O(N) となるため、カーソルを利用するなどの工夫が必要 実装が簡単なため、初期のエディタ実装で採用されることが多い Rope 挿入やランダムアクセスが O(log N) 二分木の構築やバランシング処理によるオーバヘッドが発生 編集操作をイミュータブルにすることができる Zed Editor では Rope を基にしたデータ構造が利用されている Piece Table メモリ効率が良い 挿入や削除は追記型となるため高速( O(1) ) 編集によりPieceの数が増加するとパフォーマンスが劣化する VS Code では Piece Table を基にしたデータ構造が利用されている(インデックスアクセスにはRopeのように二分木を使用) どれも一長一短があり、どんな状況においてもベスト という解はありません。 私の場合は、巨大ファイルを扱いたいため、Piece Table を採用することにしました。 それぞれの利用用途に応じてデータ構造を選んでください。 データ構造だけでは語れない # データ構造が決まってテキストエディタを作り始めても、途中で迷うポイントが色々と発生します。 いくつか代表的なものに絞って以下に見てみましょう。 スクロール位置の特定 # テキストエディタには、通常、縦スクロールバーと横スクロールバーが表示されます。 縦スクロールバーを表示するには、ファイル全体の行数と、現在画面に表示されている行数を特定する必要があります。 横スクロールバーを表示するには、ファイル中の最長の行と現在の画面幅を特定する必要があります。 厄介なのは横スクロールバーの制御で、最長の行は、編集操作によって最長の行ではなくなる可能性があるので、編集の都度最長の行を探し出す必要があります。 私の場合は、横スクロールバーを完全にトラックすることは諦め、現在画面に表示している行に限定して最長行を見つける実装としました。 折り返し表示 # テキストエディタでは、1行をウインドウ幅で論理的に折り返して表示できるものがほとんどです。 しかし、この折り返し判定は厄介で、画面表示上のグリフに応じた文字の幅を計算する必要があります。 通常、文字の幅は文字毎に異なり、さらに面倒なのが、結合文字や合字(Ligature)という概念です。 これらは、複数の文字を繋げて一文字として表示するため、文字幅の計算が都度必要です。 厳密に対応しようとすると、全ての行に対して画面表示上の幅を計算し、適切な折り返し位置を見つける必要があり、非常に高価な処理になります。 折り返し計算は画面に表示されている部分だけ行う、という割り切りもできますが、そうすると先に述べたスクロールバーの長さと辻褄が合わなくなってしまいます。 ですので、折り返し表示は一定サイズ以下のファイルに制限したり、文字幅の計算はある程度割り切った近似値として扱うなどして、パフォーマンスとの折衷案を設けるのが現実的です。 文字シーケンスへのランダムアクセス # データ構造のまとめでは、ランダムアクセスが O(1) で効率が良い などと書きましたが、現実的には難しい面があります。 全てASCII 文字で、1文字は1バイトという理想的な世界であれば話は簡単ですが、現実はそう上手くは出来ていません。 例えば UTF-8 は、1文字(コードポイント)で 1~4byte の可変エンコーディングとなるため、N文字目にアクセスしようとしてもインデックスアクセスはできず、1つずつバイト長を見ていく必要があります。 テキストファイルを全てメモリに読み込み、メモリ上でアクセスしたとしても、多くの言語ランタイムは、文字列を内部的に UTF-16 でエンコードされたバイト列として扱うため、結局可変エンコードを加味する必要があります。 固定長の UTF-32 としてテキストを保持すれば、ランダムアクセスは容易になりますが、メモリ使用量が増大します。 例えば、Piece Table で、UTF-8 のファイルを直接 read-only buffer として利用する場合を考えてみましょう。 画面上をクリックし、その位置が画面表示上のN文字目(Unicodeコードポイント)だった場合、言語ランタイム上の UTF-16(1〜2バイト) におけるインデックス位置に変換し、ファイル上のUTF-8(1〜4バイト)におけるインデックス位置にさらに変換し、そしてようやくファイルの当該位置にアクセスできる といった具合で、いかにも非効率になります。 先に述べたスクロールバーや折り返し表示のことも考え、行数や文字数といったメタ情報とテキストバッファとのマッピング、グリフ幅などの表示上の情報も合わせて文字シーケンスを扱わなければならず、これらをパフォーマンスとメモリ効率のバランスを取りながら実装を工夫する必要があります。 このあたりがテキストエディタ実装の奥深さになります。
はじめに # 共通機能やAPIスキーマなどをライブラリ化して利用する場合、モジュール化したものを公開して各アプリケーションに組み込むと思います。 テストコードで動作確認すべきですが、実際に組み込むと軽微な修正が発生してしまうことがあります。 ファイルを相対参照させるとdist配下の構造が変わってエントリーポイントになるファイルの位置が変わってしまうなどの問題にも困っていました。 そんな悩みを解決してくれた yalc の活用方法を説明します。 yalcとは # yalcは、ローカルで開発中のnpmパッケージをローカルに公開し、GitHub Packagesなどに公開されているパッケージと同じようにアプリケーションに組み込んで開発できるようにするツールです。 同じような役割を持つ仕組みを持つ npm link / npm pack との違い # 観点 yalc npm link npm pack 依存参照の実態 .yalc と node_modules に展開(通常の利用形態に近い) シンボリックリンク tarballを手動で作成/配置 変更反映のしやすさ yalc push で利用先に伝搬 リンク先依存で環境差が出やすい 毎回 pack/install が必要 運用向き 複数アプリで同時検証しやすい 小規模・一時検証向き 配布物の確認向き 事故防止 yalc check で混入検知可能 標準で混入検知なし 手順の属人化に注意 利用手順 # yalcを活用した開発の流れを説明します。 下記の構成で説明します。 ライブラリ ディレクトリ: packages/math-utils パッケージ名: @sample-yalc/math-utils バージョン: 1.0.0 ライブラリを利用するプロジェクト ディレクトリ: demo-app よく使うコマンド一覧 コマンド 説明 yalc publish パッケージをyalcストアに公開 yalc push パッケージを再公開し、利用先に変更を伝搬 yalc add &lt;package&gt; パッケージを追加 yalc update 追加済みパッケージを更新 yalc remove &lt;package&gt; パッケージを削除 yalc remove --all すべてのyalcパッケージを削除 yalc installations show &lt;package&gt; パッケージの使用箇所を表示 yalc installations clean &lt;package&gt; パッケージの使用箇所をクリーン 最短で試す(3分) # ライブラリ側で公開 cd packages/math-utils yalc publish 利用側で追加 cd demo-app yalc add @sample-yalc/math-utils ライブラリ変更後に反映 cd packages/math-utils yalc push 基本はこれだけです。 以降に図解しながら詳細な流れを説明しているので、併せてご確認ください。 事前作業 # まずはyalcをインストールします。 インストール npm install -g yalc ライブラリのローカルへの公開から利用するまでの流れ # ローカルで開発中のライブラリをローカルに公開し、それを利用するまでの流れは以下の通りです。 パッケージをローカルに公開する(ライブラリ側) publishすると、パッケージがローカルのyalcストアに保存されます。 yalcストアにパッケージをコピー yalc.sig: パッケージの内容から算出した識別情報。ライブラリの変更有無を判定する際に使用します。 yalcストア上のpackage.json: yalcSigの加筆 $ cd packages/math-utils $ yalc publish @sample-yalc/math-utils@1.0.0 published in store. --> Information yalcストア yalcを使ってpublishしたパッケージが公開される場所のこと。 Windows: %LOCALAPPDATA%\Yalc (e.g. C:\Users\sample-user\AppData\Local\Yalc ) mac/Linux: ~/.yalc dirで実際のディレクトリが確認できます。 $ yalc dir C:\Users\sample-user\AppData\Local\Yalc プロジェクトにパッケージを追加する(利用側) addすると、パッケージを取り込んで、依存関係が更新されます。 installations.json: インストール先として加筆 package.json: 依存関係の追加/変更 &quot;dependencies&quot;: { // パッケージの参照先が.yalc配下に変更されます &quot;@sample-yalc/math-utils&quot;: &quot;file:.yalc/@sample-yalc/math-utils&quot; }, .yalc : yalcストアからパッケージがコピーされます node_modules/{パッケージスコープ/パッケージ名}: .yalcからパッケージがコピーされます yalc.lock: 新規作成されます $ cd demo-app $ yalc add @sample-yalc/math-utils Package @sample-yalc/math-utils@1.0.0 added ==&gt; C:\Users\sample-user\demo-app\node_modules\@sample-yalc\math-utils ここまでで、リモートに公開されているパッケージと同じように利用できます。 ライブラリの変更を伝搬する # ライブラリの変更を、利用先に伝搬する手順は以下の通りです。 ライブラリのコードを変更する(ライブラリ側) 変更を伝搬する(ライブラリ側) pushすると、利用先に変更内容を伝搬します。 yalcストアにパッケージを再公開 更新されたパッケージの利用先に変更を反映(設定不備で失敗することがあります) $ cd packages/math-utils $ yalc push ライブラリの利用を終了する # yalcパッケージとの依存を除去する手順は以下の通りです。 yalcパッケージを削除する(利用側) removeすると、利用側にコピーされたライブラリが削除されます。 package.json : 依存関係を削除 .yalc : ディレクトリを削除 node_modules/{パッケージスコープ/パッケージ名} : ディレクトリを削除 yalc.lock : 依存関係を削除 ロック対象のyalcパッケージがすべてなくなったらファイルごと削除します。 yalcストアのパッケージ: 削除されません $ cd demo-app $ yalc remove @sample-yalc/math-utils # 特定のパッケージを指定してyalcパッケージを削除する場合 $ yalc remove --all # すべてのyalcパッケージを削除する場合 Appendix. 利用上の注意点など # yalc関連のファイルはgit管理から除外 # あくまでも開発時に利用するツールなので、yalcを使う時は .gitignore に該当ファイルを登録しておきます。 # yalc .yalc/ yalc.lock コミット前に yalc check で混入防止 # .yalc 参照( file:.yalc/... や link:.yalc/... )が package.json に残ったままコミットすると、CIや他環境で問題になりやすいです。 # package.jsonにyalc依存が残っていないかチェック yalc check pre-commitで実行するようにしておくと、誤コミットを防ぎやすくなります。 パッケージが更新されない # yalcパッケージをいったん削除して、再登録してください。 # キャッシュをクリアして再追加 yalc remove @sample-yalc/math-utils yalc add @sample-yalc/math-utils (yalcパッケージを削除してもうまくいかない場合)node_modulesを削除して、再登録してください。 # node_modulesを削除して再インストール rm -rf node_modules # Remove-Item -Recurse -Force node_modules # PowerShellの場合 npm install yalc add @sample-yalc/math-utils (それでもうまくいかない場合)インストール先のパスが誤っている可能性があります。確認して誤っていた場合はパスを修正してください。 # 特定のパッケージの情報 yalc installations show @sample-yalc/math-utils まとめ # ライブラリを開発しながら動作検証できるのは非常に助かります。 同じような苦労をされている方がいらっしゃれば、開発に組み込んでみてはいかがでしょうか。
はじめに # 普段、業務ではどのような OSを使っているでしょうか。 筆者個人では Macを使っていますが、業務では Windowsを利用しています。 Windows環境では、軽量で扱いやすく、POSIXライクな操作ができる Git Bashを利用しています。 Windows Terminalからも使えるため、普段使い慣れたコマンドをそのまま利用でき、 AWS CLIとの相性が良い点も便利です。 https://developer.mamezou-tech.com/blogs/2023/09/08/windows-terminal-with-git-bash/ このような理由から、AWS Systems Manager Session Manager(以下、SSM)経由で EC2に接続する際にも Git Bash を利用していました。 しかし、Windows 環境の Git Bash では、 lsblk や systemctl status などに含まれる一部の罫線・記号が文字化けすることがありました。 本記事では、この事象の再現内容、原因、対処方法を紹介します。 結論としては、 session-manager-plugin を最新版へ更新するのが第一選択 です。 更新が難しい場合は、 chcp.com 65001 でコードページを UTF-8 に変更する方法 が有効でした。 なお、 Macから接続した場合は同様の問題は発生しませんでした。 前提 # 以下の環境で検証しています。 OS: Windows: 11(Pro) Terminal: Git Bash AWS CLI: aws-cli/2.32.16 Python/3.13.11 Windows/11 exe/AMD64 session-manager-plugin: 1.2.707.0(検証開始時)→1.2.792.0(解決) 次のような方を対象としています。 Windows + Git Bash で AWS CLI / SSM を利用してEC2への接続している方。 lsblk や systemctl status の罫線・記号などの文字化けする方。 先に結論 # 対処方法は次の通りです。 優先度 対処方法 補足 1 session-manager-plugin を最新版( 1.2.792.0 以降)へ更新する 根本対応。まずはこちらを推奨 2 chcp.com 65001 を実行する プラグインを更新できない場合の暫定対処 3 .bashrc に設定する 暫定対処を継続利用する場合の恒久化 推奨対処: session-manager-plugin を最新版へ更新する # 検証を進める中で、 最新版の session-manager-plugin に更新すると事象が解消する ことを確認できました。 ※このバージョンは、記事の執筆・検証時点の3日前に最新版がリリースされていました。 執筆時点では、最新版の 1.2.792.0 にプラグインを更新することで文字化けが再現しなくなりました。 インストール方法は、以下の AWS 公式ドキュメントを参照してください。 WindowsでのSession Managerプラグインのインストール なぜ 1.2.792.0 で改善したのか # 執筆時点での最新版である 1.2.792.0 のリリースノートには、 Windows環境でのキーボード入力や文字処理に関する修正が含まれていました。 session-manager-plugin 1.2.792.0 リリースページ 1.2.792.0 に関連する以下のPRを見ると、本記事の事象と近い問題が修正対象になっていることが分かります。 PR: Windowsプラットフォームにおける中国語および日本語の文字表示の不具合の修正 PRでは、Windows版の session-manager-plugin が出力時に windows.WriteFile APIを使用しており、 UTF-8 の一部の言語の文字が正しく処理できていなかったことが説明されています。 その結果、中国語や日本語の入力・出力の一部で文字化けが発生していたと考えられます。 本記事で確認した文字化けも、この修正を含む 1.2.792.0 で改善した可能性が高いと考えられます。 本記事の後半は、 古いバージョン ( 1.2.707.0 ) を利用していたときの再現内容と暫定対処 をまとめたものです。 まずはプラグインを最新版へ更新し、改善するかを確認してください。 文字化けの事象(古いプラグインの場合) # 古いバージョンのプラグイン(例: 1.2.707.0 )を使用している場合、以下のようなコマンドを実行した際に文字化けが発生しました。 今回は Amazon Linux 2023 と Ubuntu 24.04 の公式AMIで検証しましたが、AMIに依存せず、同様の箇所で文字化けが発生しました。 lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS loop0 7:0 0 27.8M 1 loop /snap/amazon-ssm-agent/12322 loop1 7:1 0 74M 1 loop /snap/core22/2339 loop2 7:2 0 48.1M 1 loop /snap/snapd/25935 nvme0n1 259:0 0 8G 0 disk 笏懌楳nvme0n1p1 259:1 0 7G 0 part / 笏懌楳nvme0n1p14 259:2 0 4M 0 part 笏懌楳nvme0n1p15 259:3 0 106M 0 part /boot/efi 笏披楳nvme0n1p16 259:4 0 913M 0 part /boot systemctl status sshd 笳・sshd.service - OpenSSH server daemon Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; preset: enabled) Active: active (running) since Sun 2026-03-20 14:18:54 UTC; 1min 12s ago Docs: man:sshd(8) man:sshd_config(5) Main PID: 1553 (sshd) Tasks: 1 (limit: 1067) Memory: 2.3M CPU: 15ms CGroup: /system.slice/sshd.service 笏披楳1553 &quot;sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups&quot; ご覧の通り、罫線やステータスアイコンが文字化けしています。 原因 # この文字を調べてみたところ、以下の通りでした。 本来 Unicode 文字化け ├─ U+251C , U+2500 笏懌楳 └─ U+2514 , U+2500 笏披楳 ● U+25CF 笳 (末尾不正) 以上から、今回の文字化けは UTF-8の文字列が CP932(Shift_JIS 系)として誤って解釈されたことで発生していると考えられます。 つまり、 SSM経由で受け取った文字列と Git Bash側のコンソールのページ文字コード設定が一致しておらず、その結果として罫線や記号が正しく表示されなかった、という状況です。 Git Bash側の設定確認と暫定対処 # Git Bashの現在の設定を確認する # 今回の環境では、 chcp.com の結果は CP932でした。 自身の Git Bashの現在の設定を確認したい場合は以下のコマンドで確認できます。 chcp.com 現在のコード ページ: 932 暫定対処: コンソールの文字コードをUTF-8に変更する # --> Information まずは session-manager-plugin を最新版へ更新し、改善することを確認してください。 ここで紹介する方法は、プラグインを更新できない場合の暫定対処です。 最新版のダウンロードができない・インストールに制約がある環境の方は以下の通り、現在のページのコードをUTF-8へ変更するコマンドを試してみてください。 chcp.com 65001 Active code page: 65001 Git Bash上では単なる chcp を認識せず、期待通り動作しません。 これは Windows標準の C:\Windows\System32\chcp.com を明示的に呼び出す必要があるためです。 参考情報 Git Bash の日本語の文字化けを解消する方法 Qiita: Git Bash で 文字コードを変換する方法(Windows) Microsoft chcp Microsoft Code Page Identifiers 結果 # コンソールの文字コードを UTF-8へ変更後に再度確認したところ、文字化けが発生していた箇所が正常に出力されることを確認できました。 最新版の session-manager-plugin に更新・利用した場合は、 chcp.com 65001 を実行しなくても、文字化けせずに出力されました。 lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS loop0 7:0 0 27.8M 1 loop /snap/amazon-ssm-agent/12322 loop1 7:1 0 74M 1 loop /snap/core22/2339 loop2 7:2 0 48.1M 1 loop /snap/snapd/25935 nvme0n1 259:0 0 8G 0 disk ├─nvme0n1p1 259:1 0 7G 0 part / ├─nvme0n1p14 259:2 0 4M 0 part ├─nvme0n1p15 259:3 0 106M 0 part /boot/efi └─nvme0n1p16 259:4 0 913M 0 part /boot systemctl status sshd ● sshd.service - OpenSSH server daemon Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; preset: enabled) Active: active (running) since Sun 2026-03-20 15:22:04 UTC; 33s ago Docs: man:sshd(8) man:sshd_config(5) Main PID: 1549 (sshd) Tasks: 1 (limit: 1067) Memory: 2.3M CPU: 15ms CGroup: /system.slice/sshd.service └─1549 &quot;sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups&quot; 設定の恒久化 # 毎回コードページを変更するのが面倒な場合は、 .bashrc に設定しておくことで Git Bash 起動時に自動で UTF-8 に切り替えられます。 echo 'chcp.com 65001 &gt; /dev/null' &gt;&gt; ~/.bashrc まとめ # 今回は、Git Bash で SSM を利用した際に発生する文字化けの原因と対処方法を紹介しました。 ポイントをまとめると、次の通りです。 まずは session-manager-plugin を最新版へ更新する 古いプラグインを使っている場合、Git Bash側のページ文字コードが原因で文字化けすることがある 更新できない環境では、 chcp.com 65001 によるUTF-8化が有効 継続利用する場合は .bashrc への設定で恒久化できる これまでは文字化けが起きても、表示が崩れた部分を手で読み替えながら対応していました。 最近は EC2 周りの作業が増え、文字化けに遭遇する頻度も高くなったため、本記事で対処方法を調べました。 解決方法にたどり着くまで時間がかかったので、本記事の内容がお役に立てば幸いです。 最後までご覧いただきありがとうございました。
はじめに # ロボットや製造装置のソフトウェアでは、ユーザーインターフェースと装置制御ロジックの設計が重要になります。特に装置の操作パネルは、装置の状態を分かりやすく表示するとともに、安全に操作を行えるインターフェースである必要があります。 食品盛り付けロボット「美膳®」は、製造現場でのエンドユーザー利用を想定して設計されたロボットシステムです。美膳®の本体には、システムを操作するための専用の操作パネルが用意されています。 本記事では、美膳®のUI設計を例として、次の内容を紹介します。 装置UIのソフトウェアアーキテクチャ Flutterを利用したGUI実装 gRPCによるコンポーネント間通信 双方向ストリーミングを用いたリアルタイム通信 美膳®のソフトウェアアーキテクチャ # 美膳®のソフトウェアは、役割ごとに分離された複数のコンポーネントによって構成されています。それぞれのコンポーネントが明確な責務を持つことで、システム全体の保守性と拡張性を高めています。 主なコンポーネントは次の3つです。 GUIアプリケーション ユーザー操作の入口となるアプリケーションです。装置の状態表示や操作入力を担当し、直接コアロジックにはアクセスせず、必ずAPIを経由して操作を行います。 コントローラAPI コントローラアプリケーションの機能や状態を外部に公開するインターフェース層です。GUIなどの外部アプリケーションは、このAPIを通して装置の機能にアクセスします。 コントローラアプリケーション 美膳®の中核となるコンポーネントです。装置の状態制御、登録データ管理、画像処理などを担当します。 システム構成は次の図のようになります。 このように、GUIアプリケーションは直接コントローラアプリケーションにアクセスするのではなく、APIを介して通信する構造になっています。これにより、UIとコアロジックを独立して開発・保守することが可能になります。 コンポーネント間通信 # 美膳®では、GUIアプリケーションとコントローラアプリケーションの通信に gRPC を採用しています。コントローラAPIはgRPCで実装されており、コントローラアプリケーションはgRPCサーバとして動作します。GUIアプリケーションはgRPCクライアントとして接続し、各種サービスを利用します。 この構成により、UIと制御ロジックを言語や実装に依存せず接続することが可能になります。 --> Information コントローラAPIはインターフェイス定義言語 ( Protocol Buffers )により公開インタフェースを定義し、サーバ側 / クライアント側それぞれのプログラミング言語用に変換して利用する。 通信の用途は主に次の3つです。 システム状態通知 # GUIアプリケーションはサーバに接続している間、システムの状態更新を継続的に受信します。これは サーバストリーミングRPC によって実装されています。コントローラの状態変化に応じてGUIの表示が更新され、装置の状態とUIが常に同期されます。 サービス要求 # ユーザー操作によって発生する単発の処理要求は Unary RPC によって実装されています。GUIアプリケーションから要求が送信され、サーバから処理結果が応答されます。 同期セッション # リアルタイム性が必要な操作では 双方向ストリーミングRPC を使用します。クライアントとサーバが同時にメッセージを送信できるため、リアルタイムな通信セッションを実現できます。 例えば次のような用途で利用されています。 カメラ映像を確認しながらのパラメータ調整 運転開始前の確認操作 ロボットの状態監視 通信の流れは次の図のようになります。 技術概要 # Flutterについて # 美膳®のGUIアプリケーションは、Googleが提供するUIフレームワーク Flutter を利用して実装されています。FlutterではDart言語を使用してアプリケーションを開発します。 Flutterはクロスプラットフォームのフレームワークであり、単一のコードベースから複数のOS向けのアプリケーションを生成できます。また、豊富なUIコンポーネントが提供されているため、操作パネルのようなGUIの開発を効率的に行うことができます。 さらに、Googleが提供する他の技術との親和性が高いことも特徴の一つです。 gRPCについて # gRPCはGoogleが開発したオープンソースのRPC(Remote Procedure Call)フレームワークです。gRPCでは Protocol Buffers(Protobuf) を利用してAPIを定義し、データのシリアライズを高速に行うことができます。 ProtobufでAPIを定義することで、複数のプログラミング言語から同一のインターフェースを利用することが可能になります。そのため、Flutter(Dart)で実装されたGUIと、C++で実装されたコントローラアプリケーションのような異なる言語のシステムを容易に接続できます。 豆蔵では過去のロボット開発プロジェクトでもgRPCを利用した実績があります。 サンプルアプリケーション # 双方向ストリーミングによるリアルタイム通信 # gRPCの双方向ストリーミングは、クライアントとサーバが同時にメッセージを送信できる通信方式です。この仕組みを利用することで、リアルタイム性の高いUI操作を実装できます。 ここでは、FlutterとgRPCを用いた簡単なサンプルアプリケーションを通して、双方向ストリーミングによるリアルタイム通信の仕組みを紹介します。 実際の美膳®では、Flutterで実装されたUIとC++で実装されたコアアプリケーションが通信していますが、ここでは理解を容易にするため、Dartでサーバとクライアントを実装したサンプルを作成します。 このサンプルでは、サーバが共有カウンターを管理し、複数のクライアントがカウンターの更新操作を送信できるアプリケーションを作成します。クライアントから送信された操作はサーバで処理され、その結果がすべての接続クライアントへリアルタイムに配信されます。 以降では、このサンプルアプリケーションの実装手順を紹介します。 ディレクトリ構成 # 今回のサンプルでは複数のプロジェクトを作業ディレクトリ Examples にまとめます。( Examples ディレクトリは任意の場所に作成してください) これから作成するディレクトリの構成は下図のようになります。 Examples/ | +-- counter_server/ | サーバプログラムのプロジェクトディレクトリ | +-- counter_client/ | クライアントプログラムのプロジェクトディレクトリ | +-- counter_api/ gRPCで使用するAPIの定義を格納する --> Information 本サンプルは、下記の環境で作成・動作の確認を行った。 OS: Ubuntu 22.04 LTS Flutter: 3.24.2 / Dart: 3.5.2 gRPCを用いたAPIの定義 # 最初にProtocol Bufferで、クライアント - サーバ間で使用するAPIを定義します。 ターミナルで Examples ディレクトリに入り、下記を実行してください。 mkdir counter_api cd counter_api touch counter_api.proto counter.proto をエディタで開き、gRPCのメッセージとサービスを定義して保存してください。 syntax = &quot;proto3&quot;; package counter_api; /// カウンターサービス定義 service CounterService { /// 双方向ストリーミング /// クライアントは操作を送信 /// サーバーは最新カウント値をストリームで返す rpc SyncCounter(stream CounterRequest) returns (stream CounterResponse); } /// クライアントからの操作リクエスト message CounterRequest { string client_id = 1; // クライアント識別子 oneof action { Increment increment = 2; Decrement decrement = 3; Reset reset = 4; } } /// +1 操作 message Increment { int32 amount = 1; // 通常は1 } /// -1 操作 message Decrement { int32 amount = 1; // 通常は1 } /// リセット操作 message Reset {} /// サーバーから配信される現在状態 message CounterResponse { int32 current_value = 1; // 現在のカウント値 string updated_by = 2; // 更新したクライアントID int64 timestamp = 3; // 更新時刻(Unix ms) } サーバ側のDartプロジェクトの作成 # Examples ディレクトリで下記をターミナルから実行する dart create counter_server cd counter_server 続けて、先に定義した counter_api.proto をコンパイルして自動生成のコードをcounter_serverのソースに加えます。 dart pub global activate protoc_plugin 21.1.2 # &lt;-- protoc_plugin をインストール export PATH=&quot;$PATH&quot;:&quot;$HOME/.pub-cache/bin&quot; # &lt;-- Protocol Buffers コンパイラ (protoc) のPATHを一時的に通す mkdir -p lib/src/generated protoc --dart_out=grpc:lib/src/generated -I../counter_api counter_api.proto # &lt;-- counter_api.proto をコンパイルする counter.proto のコンパイルに成功すると、下記のファイルが Examples/counter_server/lib/src/ に生成されます。 Examples/counter_server/lib/src/generated | +-- counter_api.pb.dart | メッセージ型の本体定義 | +-- counter_api.pbenum.dart | enum定義 (今回は使用しない) | +-- counter_api.pbgrpc.dart | gRPCサービス用のコード | +-- counter_api.pbjson.dart JSON用メタデータ (今回は使用しない) counter_server/pubspec.yaml を編集してプロジェクトを設定します。 name: counter_server description: &quot;gRPC bidirectional streaming counter server&quot; version: 1.0.0 environment: sdk: ^3.5.2 dependencies: grpc: ^3.2.4 protobuf: ^3.1.0 protoc_plugin: ^21.1.2 fixnum: ^1.1.1 dev_dependencies: lints: ^4.0.0 test: ^1.24.0 counter_server/lib/counter_server.dart を編集し、サーバを実装します。 import 'dart:async'; import 'package:grpc/grpc.dart'; import 'package:fixnum/fixnum.dart'; import 'src/generated/counter_api.pb.dart'; import 'src/generated/counter_api.pbgrpc.dart'; class CounterServiceImpl extends CounterServiceBase { int _currentValue = 0; // 接続中クライアントへ配信するためのコントローラ一覧 final List&lt;StreamController&lt;CounterResponse&gt;&gt; _clients = []; @override Stream&lt;CounterResponse&gt; syncCounter( ServiceCall call, Stream&lt;CounterRequest&gt; requestStream) { final controller = StreamController&lt;CounterResponse&gt;(); _clients.add(controller); print(&quot;Client connected&quot;); // 接続直後に現在値を送信 controller.add(_createResponse(&quot;server&quot;)); requestStream.listen( (request) { _handleRequest(request); }, onDone: () { print(&quot;Client disconnected&quot;); _clients.remove(controller); controller.close(); }, onError: (e) { print(&quot;Stream error: $e&quot;); _clients.remove(controller); controller.close(); }, ); return controller.stream; } void _handleRequest(CounterRequest request) { if (request.hasIncrement()) { _currentValue += request.increment.amount; _broadcast(request.clientId); } else if (request.hasDecrement()) { _currentValue -= request.decrement.amount; _broadcast(request.clientId); } else if (request.hasReset()) { _currentValue = 0; _broadcast(request.clientId); } } void _broadcast(String updatedBy) { final response = _createResponse(updatedBy); print(&quot;Broadcast: $_currentValue (by $updatedBy)&quot;); for (final client in _clients) { client.add(response); } } CounterResponse _createResponse(String updatedBy) { return CounterResponse() ..currentValue = _currentValue ..updatedBy = updatedBy ..timestamp = Int64(DateTime.now().millisecondsSinceEpoch); } } Future&lt;void&gt; main() async { final server = Server.create( services: [CounterServiceImpl()], interceptors: const &lt;Interceptor&gt;[], ); await server.serve(port: 50051); print('Counter Server listening on port ${server.port}'); } クライアント側のFlutterプロジェクトの作成 # Examples ディレクトリで下記をターミナルから実行します。 flutter create counter_client cd counter_client 続いてサーバのときと同じように、 counter_api.proto をコンパイルして自動生成のコードをcounter_serverのソースに加えます。 dart pub global activate protoc_plugin 21.1.2 # &lt;-- protoc_plugin をインストール export PATH=&quot;$PATH&quot;:&quot;$HOME/.pub-cache/bin&quot; # &lt;-- Protocol Buffers コンパイラ (protoc) のPATHを一時的に通す mkdir -p lib/src/generated protoc --dart_out=grpc:lib/src/generated -I../counter_api counter_api.proto # &lt;-- counter_api.proto をコンパイルする counter_client/pubspec.yaml を編集してプロジェクトを設定します。 name: counter_client description: &quot;gRPC bidirectional streaming counter client&quot; publish_to: 'none' version: 1.0.0+1 environment: sdk: ^3.5.2 dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.8 grpc: ^3.2.4 protobuf: ^3.1.0 uuid: ^4.4.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^4.0.0 flutter: uses-material-design: true counter_client/lib/main.dart を編集し、クライアントを実装します。 import 'dart:async'; import 'package:flutter/material.dart'; import 'package:grpc/grpc.dart'; import 'package:uuid/uuid.dart'; import 'src/generated/counter_api.pb.dart'; import 'src/generated/counter_api.pbgrpc.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: CounterPage(), ); } } class CounterPage extends StatefulWidget { const CounterPage({super.key}); @override State&lt;CounterPage&gt; createState() =&gt; _CounterPageState(); } class _CounterPageState extends State&lt;CounterPage&gt; { late ClientChannel _channel; late CounterServiceClient _stub; late StreamController&lt;CounterRequest&gt; _requestController; Stream&lt;CounterResponse&gt;? _responseStream; final String _clientId = const Uuid().v4(); int _currentValue = 0; String _lastUpdatedBy = &quot;-&quot;; @override void initState() { super.initState(); _initGrpc(); } void _initGrpc() { _channel = ClientChannel( 'localhost', // サーバーアドレス port: 50051, options: const ChannelOptions( credentials: ChannelCredentials.insecure(), ), ); _stub = CounterServiceClient(_channel); _requestController = StreamController&lt;CounterRequest&gt;(); _responseStream = _stub.syncCounter(_requestController.stream); _responseStream!.listen((response) { setState(() { _currentValue = response.currentValue; _lastUpdatedBy = response.updatedBy; }); }); } void _sendIncrement() { final request = CounterRequest( clientId: _clientId, increment: Increment()..amount = 1, ); _requestController.add(request); } void _sendDecrement() { final request = CounterRequest( clientId: _clientId, decrement: Decrement()..amount = 1, ); _requestController.add(request); } void _sendReset() { final request = CounterRequest( clientId: _clientId, reset: Reset(), ); _requestController.add(request); } @override void dispose() { _requestController.close(); _channel.shutdown(); super.dispose(); } @override Widget build(BuildContext context) { final isMe = _lastUpdatedBy == _clientId; return Scaffold( appBar: AppBar( title: const Text(&quot;gRPC 双方向ストリーミング カウンター&quot;), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '現在値', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 16), Text( '$_currentValue', style: const TextStyle( fontSize: 60, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 20), Text( '最終更新: ${isMe ? &quot;自分&quot; : _lastUpdatedBy}', ), const SizedBox(height: 40), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: _sendIncrement, child: const Text(&quot;+1&quot;), ), const SizedBox(width: 20), ElevatedButton( onPressed: _sendDecrement, child: const Text(&quot;-1&quot;), ), const SizedBox(width: 20), ElevatedButton( onPressed: _sendReset, child: const Text(&quot;Reset&quot;), ), ], ), ], ), ), ); } } サンプルプログラムの実行 # ここでは作成したサンプルプログラムを実行し、gRPCの双方向ストリーミングによるクライアント間の状態同期を確認します。 このサンプルでは、1つのサーバに対して複数のクライアントが接続し、カウンタの状態をリアルタイムに共有します。 サーバは localhost:50051 で待ち受けるように実装されています。 そのため、サーバとクライアントは同じマシン上で実行することを前提としています。 下記の順にサーバとクライアントを起動します。 1. サーバ側 # ターミナルで counter_server のプロジェクトディレクトリに移動し、Dartプログラムを実行します。 cd counter_server dart run サーバが起動すると、次のようなログが表示されます。 Counter Server listening on port 50051 この状態で、クライアントからの接続を受け付けます。 2. クライアント側 # 別のターミナルを開き、 counter_client のプロジェクトディレクトリでFlutterアプリケーションを起動します。 cd counter_client flutter run クライアントアプリケーションが起動すると、gRPCを通して自動的にサーバへ接続されます。 実行結果 # クライアントの [+1] / [-1] ボタンをタップすると、操作内容が双方向ストリーミングRPCを通してサーバへ送信されます。 サーバはカウンタの状態を更新し、その結果を接続中のすべてのクライアントへストリームで配信します。 クライアントは受信した状態をもとに画面の表示を更新します。 上図はカウンタを「2」までカウントアップした状態のクライアントです(以下では「 クライアントA 」と呼びます)。 画面下部の [最終更新] には、最後にカウンタを更新したクライアントが表示されます。 この場合はクライアントA自身が更新しているため、「自分」と表示されています。 次に、別のターミナルを開いてもう1つクライアントを起動します。 これを「 クライアントB 」とします。 クライアントBもサーバから状態ストリームを購読しているため、現在のカウンタ値「2」が表示されます。 しかし [最終更新] には、更新を行ったクライアントAの識別子が表示されます。 この状態でクライアントBからカウンタを更新すると、次のような挙動になります。 クライアントBが更新操作を送信 サーバがカウンタ値を更新 サーバが全クライアントへ状態更新を配信 クライアントAとクライアントBの画面が同時に更新 この結果、 クライアントBでは [最終更新] = 自分 クライアントAでは [最終更新] = クライアントBのID と表示が更新されます。 このように、複数のクライアントが同じ状態をリアルタイムに共有していることを確認できます。 考察 # 今回のサンプルでは、双方向ストリーミングRPCを利用してクライアントとサーバ間の通信を実装しました。 ただし、このカウンタの例だけを見ると、必ずしも双方向ストリーミングを使用する必要はありません。例えば次のような構成でも同様の機能を実現できます。 カウンタ更新 → Unary RPC 状態更新通知 → Server Streaming RPC この方法でもクライアント間の状態同期は可能です。 しかし、実際の装置ソフトウェアでは次のような要件が発生することが多くあります。 操作入力をリアルタイムに送信する カメラ映像などのデータを連続的に受信する 操作と状態更新を同一セッションで同期する このようなケースでは、クライアントとサーバが同時にデータを送受信できる 双方向ストリーミングRPC が有効です。 美膳のシステムでは、この仕組みを利用して次のような操作を実装しています。 カメラ映像を確認しながらのパラメータ調整 ロボット操作の確認セッション UIと装置状態のリアルタイム同期 双方向ストリーミングを利用することで、リアルタイム性を保ちながら複雑な操作セッションを実装することができます。 まとめ # 本記事では、食品盛り付けロボット「美膳®」におけるUIアーキテクチャと、その実装技術の概要を紹介しました。 美膳®では、ユーザーインターフェースとロボット制御ロジックを明確に分離した構成を採用しています。GUIアプリケーションはFlutterによって実装され、コントローラアプリケーションとはgRPCを用いて通信します。 このような構成にすることで、UIと制御ロジックを独立して開発・保守することが可能になります。その結果、装置ソフトウェアの整備性や拡張性を高めることができます。 また、gRPCのストリーミング機能を活用することで、装置の状態通知やリアルタイムな操作セッションなど、用途に応じた通信モデルを柔軟に実装できます。 美膳®のソフトウェア設計のポイントは次の通りです。 UIと制御ロジックの分離 GUIアプリケーションとコントローラアプリケーションを独立したコンポーネントとして構成することで、責務を明確にし、開発と保守を容易にしています。 APIによるコンポーネント接続 コントローラアプリケーションの機能をAPIとして公開することで、UIからのアクセスを安全に制御し、システムの境界を明確にしています。 多言語環境を前提とした通信基盤 Flutter(Dart)とC++という異なる言語で実装されたアプリケーションを接続するために、gRPCとProtocol Buffersを用いた通信基盤を採用しています。 ストリーミング通信によるリアルタイム同期 システム状態の配信や操作セッションなど、装置のUIに必要なリアルタイム通信を効率的に実装しています。 ロボットや装置のソフトウェアでは、UI、通信、制御ロジックなど複数の要素が密接に関係します。本記事で紹介した構成はその一例ですが、装置ソフトウェアの設計を検討する際の参考になれば幸いです。
はじめに # MCPサーバーは、エージェントやツールが呼び出せる実行可能な「サービス」を定義する仕組みです。 このページでは、VS CodeのMCP拡張からMCPサーバー(今回はMarkitdownを使用)を起動して、AIエージェント/MCPクライアントで呼び出す手順を紹介します。 用語補足(この記事での使い方) MCP(Model Context Protocol)サーバー エージェントに実行可能なツールを提供する仕組み。 エージェントはMCPサーバーの「ツール」を呼び出すことで外部処理を実行できる。 Markitdown ( microsoft/markitdown ) PDFやpptxをMarkdownに変換するMCPサーバー。 uvx Markitdownのランチャー。 今回検証に使うMCPサーバーが command として期待するツール。 環境 # Windows 11 VS Code Markitdown@1.8.1: 動作確認に使用したMCPサーバー uvx@0.10.9 Copilot: MCPサーバーの呼び出しに使用したAIエージェント MCP Inspector@0.21.0: MCPサーバーの呼び出しに使用したMCPクライアント 手順 # 今回はMarkitdownを使用して、動作を確認します。 MCPサーバーをVS Codeにインストール GitHub MCP Registry からインストールする場合 GitHub MCP Registryをブラウザで開いて、インストールしたいMCPのinstallボタンを押下。 VS CodeのExtensionsからインストールする場合 VS Code上でMCPサーバーを検索できるように設定を有効化 --> Information ■設定を無効化したい場合 設定を開いてチェックを外す。 フィルタリング( @mcp )された状態でMCPが表示されるので、インストールしたいMCPのinstallボタンを押下。 MCPサーバーのインストール後、 .vscode/mcp.json にMCPサーバーの設定が追加される。 .vscode/mcp.json { &quot;servers&quot;: { &quot;microsoft/markitdown&quot;: { //MCPサーバー名 &quot;type&quot;: &quot;stdio&quot;, //MCPサーバーの種類 &quot;command&quot;: &quot;uvx&quot;, //MCPサーバーが期待するコマンド &quot;args&quot;: [ //コマンドに渡す引数 &quot;markitdown-mcp@0.0.1a4&quot; ], &quot;gallery&quot;: &quot;https://api.mcp.github.com&quot;, &quot;version&quot;: &quot;1.0.0&quot; } }, &quot;inputs&quot;: [] } MCPサーバーを起動 インストール後に作成された .vscode/mcp.json の設定を読み込んで、ローカルプロセスを起動します。 VS CodeのExtensionsから起動 .vscode/mcp.json から起動 起動例 2026-03-10 12:35:30.848 [info] Starting server microsoft/markitdown 2026-03-10 12:35:30.848 [info] Connection state: Starting 2026-03-10 12:35:30.849 [info] Starting server from LocalProcess extension host 2026-03-10 12:35:30.920 [info] Connection state: Starting 2026-03-10 12:35:30.920 [info] Connection state: Running 2026-03-10 12:35:35.925 [info] Waiting for server to respond to `initialize` request... 2026-03-10 12:35:40.922 [info] Waiting for server to respond to `initialize` request... 2026-03-10 12:35:45.927 [info] Waiting for server to respond to `initialize` request... 2026-03-10 12:35:50.921 [info] Waiting for server to respond to `initialize` request... 2026-03-10 12:35:55.923 [info] Waiting for server to respond to `initialize` request... 2026-03-10 12:36:00.924 [info] Waiting for server to respond to `initialize` request... # 以下、略 --> Information ■uvxのインストールを促すコネクションエラー 今回、検証用に使ったMarkitdownはPythonベースでコマンドにuvxを使用します。 そのため、uvxにパスが通ってない状態だと下記のコネクションエラーが発生します。 2026-03-10 12:35:30.848 [info] Starting server microsoft/markitdown 2026-03-10 12:35:30.848 [info] Connection state: Starting 2026-03-10 12:35:30.849 [info] Starting server from LocalProcess extension host 2026-03-10 12:35:30.920 [info] Connection state: Starting 2026-03-10 12:35:30.920 [info] Connection state: Error spawn uvx ENOENT ■uvxをインストールしてパスを通せばエラーは解消します uvx をインストール PS &gt; powershell -ExecutionPolicy ByPass -c &quot;irm https://astral.sh/uv/install.ps1 | iex&quot; downloading uv 0.10.9 (x86_64-pc-windows-msvc) failed to download from https://releases.astral.sh/github/uv/releases/download/0.10.9 trying alternative download URL installing to C:\Users\xxx\.local\bin uv.exe uvx.exe uvw.exe everything's installed! To add C:\Users\xxx\.local\bin to your PATH, either restart your shell or run: set Path=C:\Users\xxx\.local\bin;%Path% (cmd) $env:Path = &quot;C:\Users\xxx\.local\bin;$env:Path&quot; (powershell) ※ spawn uvx ENOENT は「uvx コマンド自体が見つからない(PATHにない)」という意味です。インストール済みでも、VS Codeを再起動してPATHが反映されているか確認してください。 uvxへパスが通ってることを確認 uvx --version でバージョンが表示されることを確認します。 --> Information Waiting for server to respond to initialize request... MCPサーバーの起動に時間がかかっているだけなので、辛抱強く待ちましょう。 MCPサーバーを呼び出し MCPサーバーをインストールすると .vscode/mcp.json に設定されるので、これを使用して呼び出します。 { &quot;servers&quot;: { &quot;microsoft/markitdown&quot;: { //MCPサーバー名 &quot;type&quot;: &quot;stdio&quot;, //MCPサーバーの種類 &quot;command&quot;: &quot;uvx&quot;, //MCPサーバーが期待するコマンド &quot;args&quot;: [ //コマンドに渡す引数 &quot;markitdown-mcp@0.0.1a4&quot; ], &quot;gallery&quot;: &quot;https://api.mcp.github.com&quot;, &quot;version&quot;: &quot;1.0.0&quot; } }, &quot;inputs&quot;: [] } AIエージェントからの呼び出し例 MCPサーバー名と変換対象のファイルを渡して自然言語で実行。 --> Information 実行例では変換だけを指示していますが、AIエージェントから呼び出す場合、結果をファイルとして保存させるなど、プロンプト次第でさまざまな処理が可能です。 MCPのレスポンスを加工するなどの後続処理したい場合はAIエージェントから必要なプロンプトを定義して実行します。 --> Information ■期待した結果が返ってこない 今回使用しているMarkitdownはツールが convert_to_markdown しかないですが、複数のツールを持つMCPサーバーを使っている場合、期待したツールを呼び出してないと想定した結果がかえされません。 実行しているツールが想定と異なる場合は明示的に指定します。 MCPクライアントからの呼び出し例 MCP Inspectorをローカルで起動して convert_to_markdown ツールを呼び出し。 MCPサーバーを停止 確認が終わったら、以下のいずれかの方法で停止。 TerminalでCtrl+C VS CodeのExtensionsから停止 .vscode/mcp.json から停止 まとめ # VS CodeのMCP拡張で microsoft/markitdown を起動し、PDFをMarkdownに変換するまでを実際に確認しました。 例としてMarkitdownを使用しましたが、他のMCPサーバーでも mcp.json に記述されている内容に沿って準備すれば応用できます。 使用したいMCPサーバーがあれば導入して、AIエージェントに力を与えましょう。
本記事ではC#によるADS通信を使ってTwinCAT上にあるPLCデータと連携する方法についてご紹介します。 ロボット制御ではC#が人気? # システム開発では様々なプログラミング言語が利用されています。 Python, JavaScript(Node.js, Deno), C#, Java, C++, C 等がメジャーですね。最近だとRustやGoなども人気があるようです。 ロボット制御や工場の自動化においても同様に多くの言語が利用されています。 サービスや機器を提供するベンダーは自社製品をシステムに提供する際、APIやライブラリも同時に提供します。 そのため、利用者が多いプログラミング言語であったりオープンな規格で提供することが望まれます。 AI関連やオープンソースのものはPythonモジュールでの提供が非常に多いですがライセンスビジネスではC#でのライブラリ提供が多いと感じています。 理由は下記が挙げられるかと思います。 利用者が多い 使い方が簡単(プログラミング言語の敷居が低い) 便利なライブラリが豊富(組み合わせることで開発コスト削減) ベンダーが提供したWindows上のGUIアプリやシミュレータ(C#で開発)との連携で相性が良い ベンダー自体もライブラリの開発がし易い クローズドソース(ライセンスビジネスなどを想定) Linux(.NET Core)でも動作する Pythonモジュールでの提供もありますが、この場合はコアとなるライブラリは(クローズドソースや高速化のため)C++などのライブラリとし、Pythonはそのライブラリを利用するためのWrapperとして提供されます。 ADS通信とは # ADS(Automation Device Specification)はBeckhoff Automation社が開発した独自の通信プロトコルです。 TCP/IPやUDP/IPの上で動作しTwinCATシステム内外のソフトウェアモジュール間でのデータ交換で利用されます。 C#(.NET)のライブラリが提供されており、TwinCATの環境が整っていればすぐに利用することができます。 TwinCATがハブとなりADSプロトコルでTwinCAT PLC変数の監視・操作などができるようになります。 --> TwinCATおよびADS通信に関して 連載記事「TwinCATで始めるソフトウェアPLC開発」「 第1回:環境構築編 」をご覧ください。 TwinCAT ADSを利用したシステム構成例 # TwinCAT PLCを中心にシステムが構成されます。そのためXAR(実行環境)が必要になります。またアプリケーション1-3をADS通信で TwinCATと連携させるためXAE(開発環境)が必要となります。 連携方式 TwinCATがハブとなりデバイスやアプリケーションが産業用ネットワークやADSで接続され、TwinCATを介してデータ連携することができます。 TwinCAT PLC と アプリケーション1-3 は ADS で接続 TwinCAT PLC と Device1 は EtherCAT で接続 TwinCAT PLC と Device2 は EtherNet/IP で接続 --> その他のデータ連携方式 TwinCAT PLCは専用のハードウェアモジュールを追加することで温度計などで計測した温度をアナログ値として電圧値で受信することも可能です。またネットワーク通信用のソフトウェアライセンスを購入することでソケット通信でのデータ受信なども可能です。 用途 TwinCAT PLC上にグローバル変数を定義しておくと以下のような用途で利用することができます。 センサー制御 センサーデータの受信 TwinCAT PLC変数の監視・操作 デバイスやロボット等との連携 TwinCAT PLC上のプログラム(Function Block)へのRPC TwinCAT を介したプロセス間通信 TwinCAT PLC上のグローバル変数への値の設定は TwinCAT 上のプログラムで設定します。 --> グローバル変数の定義・値の設定に関して 連載記事「TwinCATで始めるソフトウェアPLC開発」「 第2回:ST言語でのプログラミング(1/2) 」をご覧ください。 ライブラリのインストール # NuGetパッケージマネージャーで Beckhoff.TwinCAT.Ads を インストールしプロジェクトの参照に設定してください。アップデートサイクル(主にバグフィクス)が比較的早く数カ月毎にマイナーバージョンがアップしています。何度かアップデートしましたが後方互換性があるので既存のコードは問題なく動作しています。 TwinCATのデータ型とC#のデータ型の対応 # TwinCATのデータ型とC#のデータ型の対応表は以下。 INTがshortに対応, REALがfloatに対応 など幾つか注意が必要です。 TwinCATデータ型 ビット幅 C#データ型 説明 BOOL 8 bit byte 真偽値/bool 1bitとの記載もあるが内部的には1byteで扱われる ( 要注意 ) BYTE 8 bit byte 符号なし 8bit 整数 SINT 8 bit sbyte 符号あり 8bit 整数 USINT 8 bit byte 符号なし 8bit 整数 INT 16 bit short 符号あり 16bit 整数 ( 要注意 ) UINT 16 bit ushort 符号なし 16bit 整数 DINT 32 bit int 符号あり 32bit 整数 UDINT 32 bit uint 符号なし 32bit 整数 LINT 64 bit long 符号あり 64bit 整数 ULINT 64 bit ulong 符号なし 64bit 整数 REAL 32 bit float 単精度浮動小数点数 ( 要注意 ) LREAL 64 bit double 倍精度浮動小数点数 ENUM 16 bit short 符号あり 16bit 整数 ( 要注意 ) STRING 1 byte/char string 1文字1バイトのバイト列 + 終端の NULL(0)文字 ( 要注意 ) TIME 32 bit TimeSpan ミリ秒単位の符号なし整数 TwinCAT側の設定 # 以下の条件で変数を登録します PlcProject プロジェクト - GVLs に グローバル変数リスト名 GVL_Test を定義(名前は任意) 変数名 TestData データ型 DINT とする TwinCAT側の設定 {attribute 'qualified_only'} VAR_GLOBAL TestData : DINT; END_VAR それではC#からTestData変数にアクセスしてみましょう。 AdsClientによる変数アクセス # AdsClientはTwinCATとアクセスする際の窓口になります。 数値データはすべて同じ方法でRead/Writeできます。 AdsClient生成の例 using System; using TwinCAT.Ads; namespace AdsComponent { class Program { static void Main(string[] args) { // AdsClientのインスタンス作成 AdsClient client = new AdsClient(); // TwinCATへの接続 // 第1引数: AmsNetId文字列 // 第2引数: ポート番号 (TwinCAT3 PLCの場合は851) client.Connect(&quot;192.168.1.101.1.1&quot;, 851); // TwinCATのグローバル変数を参照するためのハンドルを作成 // TwinCAT側で定義した &quot;グローバル変数リスト名.変数名&quot;で指定する uint handle = client.CreateVariableHandle(&quot;GVL_Test.TestData&quot;); // 書き込み操作 int writeValue = 123; client.WriteAny(handle, writeValue); // 読み取り操作 int readValue = (int)client.ReadAny(handle, typeof(int)); Console.WriteLine($&quot;read:{readValue}&quot;); // ハンドルの解放 client.DeleteVariableHandle(handle); // 接続を閉じて、リソース解放 client.Close(); client.Dispose(); } } } --> 読みやすさ優先のため例外処理や定数の定義等を省いています --> AmsNetIdの指定 連載記事「TwinCATで始めるソフトウェアPLC開発」 「 第1回:環境構築編 」の「ADS通信ルート設定」で表示されているAmsNetIdを指定してください --> コネクションは繋ぎっぱなしでもOKですが使い終わったら必ずリソースを解放してください --> AdsClientはSystem.IDisposableインタフェースを実装しているためusingステートメントを使うことができます データ変更コールバック通知 # TwinCAT側の GVL_Test.TestData 変数が変化したかどうかを監視するにはReadAny()による定期的なポーリングは効率が悪く、スレッドも独自で管理する必要があります。これを解決するための手段として値が変化した際に、自動でクライアント側へコールバック通知を飛ばす仕組みがあります。 データ変更コールバック通知の例 private AdsClient _adsClient; // インスタンス生成、コネクション接続済みとする private uint _handleNotification = 0; // データ変更通知開始 public void StartValueChangeNotification() { // イベントハンドラの登録 _adsClient.AdsNotificationEx += OnAdsNotified; // データ変更通知ハンドルの登録(通知開始) _handleNotification = _adsClient.AddDeviceNotificationEx( &quot;GVL_Test.TestData&quot;, // 50[msec]毎に変更があったときに通知する // 最大遅延時間を0[msec]とする new NotificationSettings(AdsTransMode.OnChange, 50, 0), null, typeof(int)); } // イベント受信 private void OnAdsNotified(object sender, AdsNotificationExEventArgs evn) { if (evn.Handle != _handleNotification) { return; } var data = (int)evn.Value; Console.WriteLine($&quot;notified:{data}&quot;); } // データ変更通知停止 public void StopValueChangeNotification() { // データ変更通知ハンドルの削除(通知停止) _adsClient.DeleteDeviceNotification(_handleNotification); _handleNotification = 0; // イベントハンドラの登録解除 _adsClient.AdsNotificationEx -= OnAdsNotified; } StartValueChangeNotification() を実施した後、TwinCAT側で GVL_Test.TestData の値が更新されるとC#側で OnAdsNotified() がコールバックされます。 なお、コールバック通知処理を終える場合は必ず StopValueChangeNotification() を実施しハンドルを解放してください。 --> `GVL_Test.TestData` の値を更新するには グローバル変数の定義・値を手動で更新するには連載記事「TwinCATで始めるソフトウェアPLC開発」 「 第2回:ST言語でのプログラミング(1/2) 」の「3.3 ログインによる動作確認」よりPLCにログインして該当変数の値を直接書き換えてください コールバック周期通知 # データ変更コールバック通知のパラメータを変えることで周期的な通知も可能です。 コールバック周期通知の例 // 定期的な通知開始 public void StartCyclicNotification() { // イベントハンドラの登録 _adsClient.AdsNotificationEx += OnAdsNotified; // 周期通知ハンドルの登録(通知開始) _handleNotification = _adsClient.AddDeviceNotificationEx( &quot;GVL_Test.TestData&quot;, // 10[msec]毎に通知する // 最大遅延時間を1[msec]とする new NotificationSettings(AdsTransMode.Cyclic, 10, 1), null, typeof(int)); } AdsTransMode.Cyclic を指定することで周期的な通知となります。通知タイミングを10[msec]、最大遅延時間を1[msec]とした場合、1[msec]の遅延が発生する場合が稀にありますが、ほぼ正確に10[msec]毎に通知されました。 TwinCAT側はカーネルモードで動作しているため正確な周期で値の通知が可能かと思われますが、C#側は普通のWindowsアプリでWindows OSのスケジューリングの精度やネットワークドライバの受信処理などによる遅延が発生するかと思いますが不思議な現象です。いつか調査してみたいと思います。 構造体の定義 # Read/Writeするデータやコールバック通知のデータ型にはプリミティブ型だけでなく構造体も使用可能です。また構造体はネストも可能です。 まずは例としてTwinCAT側で構造体 DUT_Sample を定義します。 TwinCAT側の構造体設定 // DUT_Sample構造体定義 TYPE DUT_Sample : STRUCT IsValid : BOOL; // BOOL型 Height : DINT; // DINT型 CurrentMode : EMode; // ENUM型 Status : DUT_Status; // 構造体 END_STRUCT END_TYPE // EMode ENUM型定義 {attribute 'strict'} {attribute 'to_string'} TYPE EMode : ( Vertical := 0, Horizontal := 1 ); END_TYPE // DUT_Status構造体定義 TYPE DUT_Status : STRUCT Status1 : DINT; Status2 : DINT; END_STRUCT END_TYPE グローバル変数リスト GVL_Test に Sample を追加します。 TwinCAT側のグローバル変数設定 {attribute 'qualified_only'} VAR_GLOBAL TestData : DINT; Sample : DUT_Sample; END_VAR グローバル変数 GVL_Test.Sample をC#側でも扱えるようにC#側でも同じ構造体を定義します。 ただし、アライメントの問題(メモリ上でのデータを配置する際の整列ルール)がありますので注意が必要です。 TwinCAT 3では、デフォルトで8byteのアライメントが採用されているため、これにあわせてC#側の構造体を定義する必要があります。 構造体には 属性 [StructLayout(LayoutKind.Sequential, Pack = 8)] を付与する 構造体以外のデータ型は 「 TwinCATのデータ型とC#のデータ型の対応 」 に合わせる 変数の定義順序はTwinCAT側と合わせる なお、変数や構造体の名前はTwinCAT側と合わせる必要はありませんが合わせておくと対応関係が明らかですのでお勧めします。 C#側の構造体の定義の例 // Sample構造体 [StructLayout(LayoutKind.Sequential, Pack = 8)] public struct Sample { public byte IsValid; // BOOL型 =&gt; byte public int Height; // DINT型 =&gt; int public EMode CurrentMode; // ENUM型 =&gt; EMode public Status Status; // 構造体 =&gt; Status } // Emode定義 public enum EMode : short // ENUM型 =&gt; short { Vertical, Horizontal, } // Status構造体 [StructLayout(LayoutKind.Sequential, Pack = 8)] public struct Sample { public int Status1; // DINT =&gt; int public int Status2; // DINT =&gt; int } int readValue = (Sample)client.ReadAny(handle, typeof(Sample)); のように キャスト や typeof() を使うことでプリミティブ型と同じAPIが使えます。 文字列(string)のRead/Write # TwinCAT 側ではSTRING型で文字列を扱うことができます。しかし、1文字1バイトのASCII文字コード(Latin-1)として扱われるためそのままでは日本語を書き込むと文字化けします。また、バイト数を指定して定義する必要があります。そのためUTF-8でエンコードするように指定して定義します。 TwinCAT側の設定 {attribute 'qualified_only'} VAR_GLOBAL TestData : DINT; Sample : DUT_Sample; // UTF-8での文字列定義 {attribute 'TcEncoding':='UTF-8'} Message : STRING(1024); END_VAR 変数定義に {attribute 'TcEncoding':='UTF-8'} を付与することで文字コードをUTF-8と解釈させる 文字サイズはバイト数で指定する 文字サイズは終端のNULL(0)文字まで含めたサイズ UTF-8の場合、1文字のバイト数は可変長(1~4バイト)となります。一般的な日本語は3バイトとなりますので余裕のあるバイトサイズを指定してください。 文字列(string)Read/Writeの例 using System.Text; private const int STRING_SIZE = 1024; private AdsClient _adsClient; // インスタンス生成、コネクション接続済みとする private uint _handle; // 'GVL_Test.Message' を指しているものとする // 文字列の書き込み public void WriteMessage(string message) { // UTF-8の文字列をバイト配列に変換 byte[] utf8Bytes = Encoding.UTF8.GetBytes(message); // バイトサイズチェック if (utf8Bytes.Length &gt; STRING_SIZE) { throw new Exception($&quot;バイト数オーバー&quot;); } // バッファの初期状態はNULL(0)文字で埋められている byte[] targetBuffer = new byte[STRING_SIZE]; // 変換したバイト配列を、固定長配列の先頭からコピーする Buffer.BlockCopy(utf8Bytes, 0, targetBuffer, 0, utf8Bytes.Length); _adsClient.WriteAny(_handle, targetBuffer); } // 文字列の読み取り public string ReadMessage() { // 固定長のバイト配列を取得する(終端のNULL文字も含む) var byteArray = (byte[])_adsClient.ReadAny( _handle, typeof(byte[]), new int[] {STRING_SIZE}); // バイト配列の中から最初のNULL文字(0)を探す int nullCharIndex = Array.IndexOf(byteArray, (byte)0); // NULL文字が見つかった場合は、そこまでを文字列とする if (nullCharIndex &gt;= 0) { // GetString(バイト配列, 開始インデックス, 長さ) return Encoding.UTF8.GetString(byteArray, 0, nullCharIndex); } // NULL文字が見つからない場合 (バッファが文字列で満たされている) は、 // 配列全体を変換する return Encoding.UTF8.GetString(byteArray); } UTF-8文字列の扱いは他のデータ型に比べて少し冗長なコードになります。 文字列は定義時のバイトサイズの配列で送受信されるため、1文字だけ送りたい場合でも残りはNULL文字で埋めてバッファサイズ分データ転送されることになります。 まとめ # ADS通信で様々なデータ型の扱い方とコールバック通知の方法を理解できたかと思います。これを応用することでRPCやプロセス間通信ができます。 PC1上のプロセスA と PC2上のプロセスB とで連携するとき、データ型をJSON文字列(STRING(1024)など)で定義すれば汎用的なデータで分散処理システムを構築できます。またコールバック通知の仕組みを上手く使えばTwinCAT上の変数をTopicとみなすPublish/Subscribe型のシステムも実現できるのではないでしょうか? BeckhoffはADSの通信プロトコル仕様をWebサイト上で公開しており、オープンソースのADS通信ライブラリもあるそうです。そのためPython, Node.js, GoなどからもADS通信ができるようです。ただし、当然これらオープンソースのライブラリはBeckhoffからのサポートが受けられない点に注意が必要です。
0. はじめに # こんにちは。豆蔵R&amp;Dグループの丹羽です。 今回はAWSセキュリティサービスの1つである「AWS Firewall Manager」(以下、FMS) [1] のポリシー設定について紹介したいと思います。 最近、私が関わるプロジェクトで「AWS Shield Advanced」を導入する必要があり、Organizationsでのアカウント管理を行っている場合、任意のOUや任意のアカウントをShield Advancedの保護対象に一括で設定するためには「AWS Firewall Manager」のポリシー設定を利用する手法が推奨されています。 このポリシー設定における挙動がややわかりにくかったため、今回実際に検証してみた結果を共有したいと思います。 現在Organizationsを利用していて、「一元的なDDoS対策やWeb ACL適用をしたい」「AWS Shield Advancedの導入を考えている」「FMSポリシーについて事前に挙動を知っておきたい」など、導入検討や挙動理解をしたい方、AWSセキュリティ運用を本格的に開始しようとしているプロジェクトにとって参考になればと思います。 1. この記事で分かること # この記事では、FMSが提供する 2つの主要なセキュリティポリシー について、実際の検証結果をもとにその挙動を解説します。 FMS Shield Advancedポリシー : 組織全体にDDoS保護を一元適用するポリシー FMS WAFポリシー : 組織全体にWeb ACL [2] ルール [3] を一元配布・適用するポリシー どちらのポリシーについても「目的」「仕組み」「検証して分かったこと」の3つの観点で整理していきます。特に、 自動修復機能(Auto Remediation) [4] の挙動や、既存のWeb ACL(独自に定義したWeb ACLという意味)との共存パターンなど、公式ドキュメントだけでは掴みにくいリアルな動きを中心にお届けします。 2. 前提知識と対象読者 # 前提知識 # 本記事は「FMSポリシー設定における挙動の検証」がメインの内容になるため、FMSや検証時に登場するWAF、Shield、それらに関連するサービスについて簡単に整理しておきましょう。 その他に登場するサービスについては記事の各所で補足していきたいと思います。 --> AWS Firewall Manager とは AWS Firewall Manager(FMS)は、AWS Organizations を利用して、複数の AWS アカウントにまたがる各種セキュリティルールを一元的に管理・適用するためのサービスです。FMS を使うことで、セキュリティポリシーの適用漏れを防ぎ、組織全体のセキュリティレベルを均一に保つことができます。 本記事で紹介するFMSのポリシーは先にも述べた以下の2つです。 FMS Shield Advancedポリシー : 組織全体にDDoS保護を一元適用するポリシー FMS WAFポリシー : 組織全体にWeb ACLルールを一元配布・適用するポリシー 上記2つ以外にもポリシーは存在していますが、今回は触れないため説明は省略します。 補足: AWS Organizationsとは 複数のAWSアカウントを組織として一元管理するサービスです。 OU(Organizational Unit:組織単位) という階層構造でアカウントをグループ化し、ポリシーを一括適用できます。FMSポリシーはこのOU・アカウント単位でスコープを指定して配布します。 --> AWS WAF とは AWS WAF は、Webアプリケーションへの不正なリクエストをフィルタリングするためのサービスです。Web ACL(アクセスコントロールリスト)の中にルールを定義し、CloudFrontやALBなどのリソースに関連付けて使います。 たとえば「SQLインジェクション攻撃をブロックする」「特定の国からのアクセスを制限する」といった防御ルールを、リソース単位で細かく設定できるのが特徴です。 補足: 関連するAWSリソース ALB(Application Load Balancer) : HTTPSレベルでトラフィックを振り分けるロードバランサー。WAF・Shield Advancedの主要な保護対象リソースの1つ。 CloudFront : AWSのCDN(コンテンツ配信ネットワーク)サービス。グローバルエッジでWAFルールを適用できる。 --> AWS Shield とは AWS Shieldは、DDoS攻撃 [5] からAWSリソースを保護するサービスです。2つのプランがあります。 プラン 概要 Shield Standard すべてのAWSユーザーに無料提供。L3/L4層(ネットワーク層・トランスポート層)の一般的なDDoS攻撃を自動防御 Shield Advanced 有料。より高度なDDoS保護、WAF利用料の無料化、DRT(DDoS Response Team) [6] への相談権利、コスト保護などを提供 Shield Advancedを利用する場合、対象リソース(ALB、CloudFront、EIPなど)に「保護(Protection)」を作成して有効化する必要があります。 FMSを利用することで、新規リソース作成時に自動的にShield Advancedで保護し、設定漏れを防ぐことができ、Organizations内の全アカウントのShield Advanced保護を、FMSから集中管理することができます。 この記事の対象読者 # Organizationsを利用した一元的なアカウント管理をしている方 Shield Advancedの導入を検討している方 AWSのセキュリティサービスに興味があり、組織全体での統制を考えている方 FMSを使って「全アカウントに共通のセキュリティルールを適用したい」と考えている方 3. FMS Shield Advancedポリシー # 3-1. 何をするためのものか(目的) # 先ほども軽く触れましたが、Shield Advancedを使うには保護したいリソース1つ1つに対して「保護」を作成する必要があります。しかし、AWS Organizationsで管理している複数のアカウントにまたがるリソースすべてに対して手作業で保護を設定するのは正直なところ現実的ではありません。 FMS Shield Advancedポリシー はこの課題を解決するためのものです。管理者アカウント(または委任管理者アカウント) [7] から「この範囲のアカウント内に存在するALBやEIPには、全部Shield Advancedの保護をかけてね」とポリシーを定義するだけで、FMSが対象リソースを検出し、保護を一元的に適用してくれます。 3-2. 大まかな仕組み # FMS Shield Advancedポリシーの基本的な動作フローを整理すると、以下のようになります。 ここで記載されている 管理者アカウント というのは、Shield Advancedをサブスクライズしたアカウントのことを指します。(基本的にはOrganizationsの管理アカウントと同一のことが多い) flowchart TD A[&quot;管理者アカウント&lt;br&gt;or FMS委任アカウント&quot;] B[&quot;AWS Firewall Manager&quot;] C[&quot;メンバーアカウント&quot;] D_OFF[&quot;検出・報告のみ&lt;br&gt;Non-Compliant として記録&quot;] D_ON[&quot;Shield保護を自動作成&quot;] E{&quot;L7自動緩和&lt;br&gt;有効?&quot;} F[&quot;ALBにWeb ACLを&lt;br&gt;自動作成・関連付け&quot;] G[&quot;準拠(Compliant)状態&quot;] A --&gt;|&quot;① ポリシー作成&lt;br&gt;対象リソース・適用スコープ・自動修復を定義&quot;| B B --&gt;|&quot;② Config Ruleを配布&lt;br&gt;Shield保護の未設定リソースを監視&quot;| C C --&gt;|&quot;③-a 自動修復 OFF&quot;| D_OFF C --&gt;|&quot;③-b 自動修復 ON&quot;| D_ON D_ON --&gt; E E --&gt;|&quot;有効(ALBなど)&quot;| F --&gt; G E --&gt;|&quot;無効&quot;| G 準拠(Compliant)というのは、FMSが対象リソースを検出し、保護を一元的に適用している状態のことを指します。自動修復をOFFにしている場合、FMSは検出のみ行い保護対象には追加しないため非準拠となります。 ここで、AWS Configサービスが関連してくるので簡単に捕捉しておきます。 --> AWS Configとは(FMSとの関係) AWSリソースの設定状態を継続的に記録・評価するサービスです。 AWS Config Rule (設定ルール)を定義することで、「リソースがこの基準を満たしているか?」を自動評価できます。 FMSはこの仕組みを活用し、ポリシー配布時にメンバーアカウントへConfig Ruleを自動定義。「Shield保護が未設定のリソースはないか」「FMS管理のWeb ACLが適用されているか」をConfigが継続監視し、準拠(Compliant)/非準拠(Non-Compliant)として報告します。 特に重要なのは、 L7自動緩和(Automatic application layer DDoS mitigation) [8] の設定です。この機能を有効にすると、ALBのような対象リソースに対して紐づけられているWeb ACL内に、自動緩和用のWeb ACLルール( ShieldMitigationRuleGroup... という名前のルール)がFMSによって自動で作成されます。 --> Information このWeb ACLルールの自動作成は、あくまで「Shield AdvancedのL7自動緩和機能」に必要なものです。WAFポリシーで作成されるWeb ACLとは別物であり、既存のWeb ACLに影響を与えることはありません。 実際、既存のWeb ACLルール内の最後にFMSが作ったルール( ShieldMitigationRuleGroup... という名前のルール)が追加されます。最後に追加されるので、ルール評価順序としても最後になり、既存ルールでチェックしたい内容を妨げることはないです。 3-3. 実際の検証結果 # 検証は、メンバーアカウント上にALBとEIP(EC2にアタッチ)を用意し、管理者アカウントからFMS Shield Advancedポリシーを適用する形で行いました。 --> Caution 念のため断っておきますが、以下の検証はあくまで「検証」という位置づけなので、いきなり本番運用しているアカウントやOUに対して実施することはおすすめしません。もしやらざるを得ない場合でもWebACLのバックアップ(JSONによる設定定義ファイル)をダウンロードしておくなどロールバックできる準備はしておくことをお勧めします。 【検証パターンS-1: 自動修復OFF】 項目 内容 FMS設定 自動修復: 無効(Disabled) 事前状態 ALB・EIPにShield保護なし 期待値 Non-Compliant(非準拠)として検出されるが、保護は作成されない 結果 : 期待通りの挙動 FMSがメンバーアカウントにAWS Configルールを自動配布し、そのルールによってALB・EIPのShield保護未設定が検出されました。FMSコンソール上では、アカウント自体のステータスが「Non-Compliant(非準拠)」、個別リソースのステータスも「Non-Compliant(非準拠)」となります。 管理アカウントのFMSコンソール メンバーアカウントに対するステータスが非準拠 メンバーアカウント内の対象リソース(ALB, EIP)に対するステータスが非準拠の理由 メンバーアカウントのAWS Config画面 メンバーアカウントに作られるConfigルール Configルールによる非準拠リソースの検出 例えば、対象リソースALBのConfig詳細の「リソースタイムライン」を確認すると、FMSによってConfigルールが適用されたことが確認できる --> 補足 Configルールによってリソースの設定情報が「どうだったか」「どうかわったのか」を確認するには、画像にある「ルールのコンプライアンス」や「設定変更」を開くことで、 どのように設定が変更されて保護対象 になったのか(あるいは保護対象外になったのか)、 FMSルールが非準拠・準拠になったかどうか が確認できる。 (リソースの固有情報が割とバッと表示されるのでここでは開いた場合の画像は割愛します(マスクするのがめ…orz)。ご了承くださいmm) 【検証パターンS-2: 自動修復ON】 項目 内容 FMS設定 自動修復: 有効(Enabled) 事前状態 ALB・EIPにShield保護なし 期待値 Shield保護が自動作成され、Compliant(準拠)に変わる 結果 : 期待通りの挙動。( 反映にはタイムラグ があり) 自動修復を有効にしてから数分後、EIPのステータスが先にCompliantになりました。一方、ALBのステータスがCompliantになるまでにはもう少し時間がかかり、追加で数分待つ必要がありました。 ALBについては、Shield保護の作成に加えて、 L7自動緩和用のWeb ACLルール( ShieldMitigationRuleGroup... )が自動作成 されていることも確認できました。これはShield Advancedの機能によるものであり、想定どおりの動作です。 管理アカウントのFMSコンソール 自動修復の有効設定 --> 補足 画像内にある「Replace~」ですが、このチェックをONにすると、もし対象リソース(ALB、EIPなど)にWAF Classicで作られた古いWeb ACLが関連付けられていた場合に、FMSがそれをWAFv2のWeb ACLに自動的に置き換えます。 メンバーアカウントのコンソール画面 WAF&amp;Shieldコンソール FMSによってWeb ACLが作成されます。 FMS作成のWeb ACLはALBなどのリソースへの紐づけはされず、 代わりに既存Web ACLにルールが追加されます。 「ShieldMitigationRuleGroup_...」という名前のルールが既存Web ACLのルールの最後に追加されていることが確認できます。 これがDDoS保護のためのルールのようです。 WebACLの評価は上から順に適用されるので、最後に追加されていることで既存のWeb ACLルール評価順序に影響が出ないような仕組みになっているということですね。 Configコンソール 自動修復の有効にしたことによってリソースが保護対象となりステータスが準拠になりました。 同様にリソースタイムラインから対象リソースがShield Advancedポリシーの保護対象になったことなどが見れます。(画像は割愛します。) ポリシー削除時の挙動 検証完了後にポリシーを削除したところ、メンバーアカウント上にFMSが作成したConfigルールやWeb ACLは きちんと削除 されました。ポリシー削除直後は残っていたリソースも、しばらく待つと自動的にクリーンアップされることを確認しています。 管理アカウントのFMSコンソール 削除 メンバーアカウントのコンソール画面 WAF&amp;Shieldコンソール Configコンソール ルールが削除されるので「利用不可」になる。 4. FMS WAFポリシー # 4-1. 何をするためのものか(目的) # AWS WAFは非常に強力なサービスですが、組織内の全アカウントで「最低限これだけは守ってほしい」という共通ルールを徹底するのは意外と大変です。各アカウントの管理者に「この設定を入れてください」とお願いして回るのは、管理する側も管理される側も負荷が高いですよね。 FMS WAFポリシー は、管理者がWeb ACLの設定内容(適用するルールグループ、デフォルトアクションなど)をポリシーとして定義し、組織全体に一括で配布・適用するためのものです。 4-2. 大まかな仕組み # FMS WAFポリシーには、Shield Advancedポリシーよりも設定項目が多く、特に「既存のWeb ACLがある場合にどう扱うか」が重要な設計ポイントになります。 置換するor統合する 結論から言うと、FMS WAFポリシーを作成しWeb ACLを適用する際には、 管理者が定義したルールをFMSが管理するWeb ACLに 置換 する メンバーアカウントの独自ルールを 挟み込む ような形で統合する の2通りの設定パターンがあります。 挟みこむというのは、 flowchart TD subgraph ACL[&quot;FMS管理 Web ACL&quot;] direction TB FRG[&quot;🔒 First Rule Groups(最初に評価)&lt;br&gt;管理者が設定 · メンバーは変更不可&quot;] CUS[&quot;✏️ メンバーアカウントの独自ルール&lt;br&gt;各アカウントが自由に追加・編集可能&quot;] LRG[&quot;🔒 Last Rule Groups(最後に評価)&lt;br&gt;管理者が設定 · メンバーは変更不可&quot;] DEF[&quot;Default Action&lt;br&gt;Allow または Block&quot;] FRG --&gt; CUS --&gt; LRG --&gt; DEF end --> 補足 First / Last Rule Groups とは FMS WAFポリシーで設定できるルールグループの配置枠です。 First Rule Groups : メンバーアカウントの独自ルールより 先 に評価される。「絶対にブロックしたい通信」を管理者が強制したい場合に使う。 Last Rule Groups : 独自ルールより 後 に評価される。「最後の砦」として組織共通の後処理ルールを設定する。 Default Action : どのルールにもマッチしなかったリクエストへの最終アクション(Allow または Block)。FMSポリシーで設定する。 この構造により、管理者が強制するベースラインを保ちながら、各アカウントが独自ルールを自由に追加できます。 これにより、組織全体の セキュリティベースライン を管理者が強制しつつ、各アカウントのアプリケーション固有のルールも柔軟に追加できる仕組みになっています。 Web ACL管理の4パターンを検証してみる FMS WAFポリシーでは、リソースに対してどのようにWeb ACLを適用するか、いくつかパターンがあると思いますが、今回は以下の4パターンを試してみようと思います。 パターン 自動修復 置換オプション Retrofit [9] 挙動 W-1: 検出のみ OFF - - ポリシー違反を検知・報告するだけ。リソースには何も変更しない W-2: 自動修復(置換OFF) ON OFF OFF Web ACL未設定のリソースにはFMS作成のWeb ACLを適用。 既に独自のWeb ACLが設定されているリソースは変更しない W-3: 自動修復(置換ON) ON ON OFF すべての対象リソースに対して、 強制的にFMS作成のWeb ACLに置き換える W-4: 自動修復(置換OFF) ON OFF ON すべての対象リソースに対して、 FMSのルールを注入する 4-3. 実際の検証結果 # 検証は、メンバーアカウント上にALBと独自のWeb ACL( test-fms-waf-log )を用意し、4つのパターンに沿って設定をおこない挙動を確認しました。 【検証パターンW-1: 自動修復OFF】 項目 内容 FMS設定 自動修復: 無効 事前状態 ALBにWeb ACL関連付けなし 期待値 Non-Compliant。Web ACLは関連付けられない 結果 : 想定通りの結果。FMSコンソール上でNon-Compliantとして検出され、ALBには何も変更が加えられない。 管理アカウントのコンソール画面 該当設定箇所 メンバーアカウントのコンソール画面 ポリシー設定後のWAF/Config画面 リソースに関連付けされていない独自のWeb ACLがあり(事前に作成しているもの)、 ConfigコンソールのFMSが作成したルールでのステータスは、自動修復がOFFであり、ALBにはどのACLも関連付けされていないため、FMS WAFポリシーによる評価が非準拠になる。 管理アカウントのコンソール画面 ポリシー適用後のFMSコンソール画面 【検証パターンW-2: 自動修復ON・置換OFF】 項目 内容 FMS設定 自動修復: 有効 、既存Web ACLの置換: OFF 事前状態 ALBにWeb ACL関連付けなし 期待値 FMS作成のWeb ACLが自動的にALBに関連付けられ、Compliantになる 結果 : 想定通りFMSが自動で作成したWeb ACLがALBに関連付けられた。ステータスもCompliant。 管理アカウントのコンソール画面 該当設定の更新 メンバーアカウントのコンソール画面 fmsが作成したWeb ACLがALBに関連付けられている Configルールも準拠になった 管理アカウントのコンソール画面 しばらくまってから管理コンソールを確認すると 【検証パターンW-3: 置換ONの状態で独自Web ACLを割り当ててみる】 ここからが面白いところです。W-2でFMS管理のWeb ACLが適用された状態から、 手動でメンバーアカウント上で独自のWeb ACL( test-fms-waf-log )に差し替え てみました。 項目 内容 FMS設定 自動修復: 有効 、既存Web ACLの置換: ON 事前状態 ALBにFMS作成のWeb ACLが関連付け済み → 手動で独自Web ACLに変更 期待値 FMS管理のWeb ACLが強制的にALBに関連付けられ、Compliantになる 結果 : 想定通りの結果 メンバーアカウントのコンソール画面 メンバーアカウントで独自Web ACLを割り当てた直後の画面 ↓ ↓ このようにfms管理のWeb ACLが外れ、独自Web ACLが設定されている状態となる。 管理アカウントのコンソール画面 次に、管理アカウントから置換ONに変更してみる。 メンバーアカウントのコンソール画面 メンバーアカウント上でのACLの状態において、fms管理のWeb ACLは以下のようになります。 そして、独自に作ったWeb ACLは以下のようになります。 このように置換設定ONで自動修復を有効にすると、手動で独自Web ACLをALBに関連付けた場合に、一度紐づけていたFMS管理のWeb ACLは自動的に外れますが、 その後FMS管理のWeb ACLが強制的に再度ALBに関連付け られ、Compliantになります。 もし、運用メンバーが意図せずにFMS管理のWeb ACLをALBから外してしまった場合などに、FMS管理のWeb ACLの関連付けに強制的に戻すことができるということですね。 しかし、意図していた場合に強制的に戻されるのは困るわけです。 【検証パターンW-4: Retrofit(既存Web ACLの改修)モード】 そこで存在しているのが 「Retrofit existing web ACLs」 [9:1] パターンなのかなと考えています。 少しややこしくなってきたと思うので、検証内容をざっと説明しておくと、 W-3の状態(FMS管理のWeb ACLがALBに関連付けられている状態)から、一度FMS WAFポリシーの「Replace existing associated web ACLs」オプションをOFFに変更し、改めて独自のWeb ACLをALBに関連付けた状態で、自動修復有効・Retrofitモードも有効にした状態にWAFポリシーを更新します。 flowchart TD S[&quot;W-3の状態&lt;br&gt;FMS管理Web ACL → ALBに関連付け済み&quot;] A[&quot;① 管理アカウントでWAFポリシーを更新&lt;br&gt;置換オプション(Replace)をOFF に変更&quot;] B[&quot;② メンバーアカウントで手動操作&lt;br&gt;独自Web ACLをALBに関連付け&lt;br&gt;(FMS管理Web ACLは自動的に外れる)&quot;] C[&quot;③ 管理アカウントでWAFポリシーを更新&lt;br&gt;自動修復: ON&lt;br&gt;Retrofit: ON&quot;] D[&quot;④ FMSが独自Web ACLに対して&lt;br&gt;First / Last Rule Groupsを注入&quot;] E[&quot;準拠(Compliant)状態&lt;br&gt;独自ルール + FMSルールが共存&quot;] S --&gt; A --&gt; B --&gt; C --&gt; D --&gt; E 項目 内容 FMS設定 自動修復: 有効 、既存Web ACLの置換: OFF 、Retrofit: ON 事前状態 ALBにFMS作成のWeb ACLが関連付け済み → 手動で独自Web ACLに変更 期待値 FMS管理のWeb ACLルールが、独自Web ACLの内容を保ちながらマージされ、Compliantになる 結果 : 想定通りの結果 Retrofitモードでは、FMSは新たなWeb ACLを作成するのではなく、 既存のカスタムWeb ACLの中に、FMS WAFポリシー定義時に設定したルールグループが注入 されました。 具体的には、独自のWeb ACLに定義されていたルールはそのまま維持され、FMS WAFポリシーで設定したFirst Rule Groups(優先ルールグループ)とLast Rule Groups(最終ルールグループ)(今回はFirst Rule Goupsのみ)を サンドイッチする形で追加 されます。 AWS Configのステータスも準拠(Compliant)となり、独自のWeb ACLの独自ルールとFMSのルールが共存している状態が確認できました。 以下検証時のコンソールの様子です。(※ 事前状態 から始めています。) メンバーアカウント上のコンソール メンバーアカウント上でのWeb ACLの様子 管理アカウント上のコンソール 管理アカウントでのWAFポリシー設定を「置換OFFの状態で、RetrofitをONにする」 RetrofitがONだと、自動で置換モードが選択できなくなる(以下の補足参照) メンバーアカウント上のコンソール FMS管理のWeb ACLはリソースへの紐づけがありませんが、 独自に作成したWeb ACLにはFMSのルールが注入されています。 --> 補足 置換ON設定とRetrofitモードは 排他的な設定 です。「置換ON設定で運用中にRetrofitに切り替える」場合は、一度置換OFFにしてからRetrofitを有効化する必要があります。 5. まとめ # 今回の検証を通じて、FMSの2つのセキュリティポリシーの挙動を実際に確かめることができました。検証結果をまとめると、以下のようになります。 Shield Advancedポリシーのポイント # 確認事項 結果 自動修復OFFでの検出 ✅ Non-Compliantとして正しく検出される 自動修復ONでの保護作成 ✅ Shield保護が自動作成される L7自動緩和によるWeb ACL ✅ ALBにはDDoS緩和用のWeb ACLが自動作成される ポリシー削除時のクリーンアップ ✅ FMS作成リソース(Config Rule、Web ACL)は自動削除される WAFポリシーのポイント # 確認事項 結果 自動修復OFFでの検出 ✅ Non-Compliantとして正しく検出される 自動修復ON・置換OFFでのWeb ACL適用 ✅ 未設定リソースにのみFMS Web ACLが適用される 置換OFF時の既存Web ACL保護 ✅ 既存Web ACLは書き戻されない(安全に運用可能) 置換ON時の強制置換 ✅ 既存Web ACLからFMS Web ACLに紐づけが切り替わる(カスタムWeb ACL自体は削除されない) Retrofitモード ✅ 既存Web ACLにFMSルールを注入。独自ルールは維持される 運用する際のポイント # 検証結果を踏まえ、FMSセキュリティポリシーを運用する際に意識しておくべきポイントを挙げます。 まずは自動修復OFFで始める : いきなり自動修復を有効にするのではなく、まずは「検出のみ」モードで対象を把握してから有効化するのが安全。 置換オプションは慎重に : 既にWAFを運用中のリソースがある場合、置換ONにすると既存の防御設定が外れるリスクがある。Retrofitモードの活用も検討。 反映までのタイムラグを考慮する : 地味に大事なのが、fmsポリシーの設定反映には数分〜十数分のラグがあるため、「設定したのに変わらない!」と慌てずに、しばらく待ってから確認したうえで次の設定をしたりするのがいいです。 ポリシー削除時のクリーンアップを信頼する : FMS作成のリソースはポリシー削除時にきちんと削除されます。ただし、こちらも完全削除までに若干の時間がかかります。 FMSは「設定して放っておけば組織全体を守ってくれる」便利なサービスですが、その裏側では多くのリソースが自動的に作成・管理されています。今回はFMSの2種類のポリシーに絞って紹介をしましたが、その挙動や背後で自動的に行われる設定などをざっくりとでも把握しておくことで、確証を持ったセキュリティ対策を実施できると思います。 --> ポリシー適用後のステップ 今回はボリューム感が大きくなってきたため扱えませんでしたが、FMSポリシーを設定したあとにさらに整備しておくと良い内容として、以下が挙げられます。 攻撃通知の整備 : Shield Advancedポリシーで検知した攻撃を管理者へ通知(SNSやCloudWatchアラームの活用) リクエストログの収集・分析 : 保護対象リソースへのリクエストをS3バケットに蓄積し、Athenaなどで分析できる体制を整える 補足: 関連サービスの概要 SNS(Simple Notification Service) : AWSのメッセージ通知サービス。Shield Advancedの攻撃検知イベントと組み合わせてアラート通知に利用する。 CloudWatch : AWSのモニタリングサービス。メトリクスやアラームを設定し、DDoS攻撃検知時に自動通知・自動対応のトリガーとして使える。 S3(Simple Storage Service) : WAFログの保管先。CloudFrontやALBのアクセスログ・WAFログを集約する。 Athena : S3上のデータをSQLでクエリできる分析サービス。S3に収集したWAFログをそのままSQL分析できるため、攻撃パターンの分析に有効。 私自身も今後のプロジェクトの中でこれらは実施していく予定ですので、また皆さんに共有すべき事項が出てきた場合にはまとめてみようと思います。 紹介した検証内容がSheld AdvancedとFMSの理解、そしてセキュリティ対策の一助となれば幸いです。 注釈 # AWS Firewall Manager(FMS) : AWS Organizationsと連携し、組織全体のWAF・Shield・セキュリティグループなどのセキュリティポリシーを一元管理するサービス。参考: AWS Firewall Manager Developer Guide ↩︎ Web ACL(Web Access Control List) : AWS WAFの中核リソース。リクエストの許可・拒否を判定するためのルールをまとめた「箱」。ALBやCloudFrontなどのリソースに関連付けて使用する。参考: AWS WAF Web ACLs ↩︎ ルール(Rule) : Web ACL内に定義する個々の検査条件。「IPアドレスの一致」「リクエストボディの検査」など、トラフィックを評価するための条件と、マッチした場合のアクション(Allow/Block/Count)をセットにしたもの。 ↩︎ 自動修復(Auto Remediation) : FMSポリシーの機能の1つ。ポリシーに違反しているリソースを検出した際に、自動的にリソースの設定(主にWeb ACLのルールに関するもの)を修正すること。また、対象リソース(ELB、EIPなど)を自動的に保護対象に加えること。参考: FMS Policy actions ↩︎ DDoS攻撃(Distributed Denial of Service attack) : 大量のコンピューターから一斉にリクエストを送り、サービスを利用不能にする攻撃手法。 ↩︎ DRT(DDoS Response Team) : AWSのDDoS対策専門チーム。Shield Advanced契約者が利用可能で、大規模攻撃時の対応支援を受けられる。参考: DRT support ↩︎ 委任管理者アカウント : AWS Organizationsの管理アカウントから、特定のサービス(FMSなど)の管理権限を委任されたメンバーアカウント。管理アカウントの権限をむやみに使わず、専用の管理アカウントに役割を分離するのがベストプラクティス。 FMSの場合、 Shield Advancedをサブスクライズしたアカウントを管理アカウントとみなし 、この管理アカウントから、FMSを管理するアカウント(メンバーアカウント)を権限を絞って複数追加することができる。 ↩︎ L7自動緩和(Automatic application layer DDoS mitigation) : Shield Advancedの機能の1つ。アプリケーション層(レイヤー7)のDDoS攻撃パターンを自動検知し、WAF Web ACLにルールを自動追加して緩和する。参考: Shield Advanced application layer DDoS mitigation ↩︎ Retrofit existing web ACLs : FMS WAFポリシーのWeb ACL管理モードの1つ。新しいWeb ACLを作成する代わりに、リソースに既に関連付けられている既存のWeb ACLをFMSの管理下に置き、FMSポリシーのルール(First/Last Rule Groups)を注入するアプローチ。参考: FMS WAF policy ↩︎ ↩︎