TECH PLAY

Google Workspace

イベント

マガジン

該当するコンテンツが見つかりませんでした

技術ブログ

By Satoshi Imai ; Finatext Ltd. What I set out to support, how I built it on AWS, and the architectural philosophy that emerged along the way. Contracts are living things It’s tempting to think of a contract as something you sign once and file away. In practice, a contract is alive. It gets amended. Deadlines shift. New people join the project and need to get up to speed. Renewal windows open and close. Across the whole lifecycle, the people who need to act on a contract — in sales, in finance, in the legal team that supports them — all need to be looking at the same, current  picture. And that’s genuinely hard work. Not because anyone is doing anything wrong, but because it’s an inherent coordination challenge of the domain. The more an organization grows, the more contracts and stakeholders it has, and the more effort it takes to keep everyone aligned on the latest state of each one. This is true for any company and its partners alike — it’s just the nature of contract lifecycle management. So here’s a nice thought to build toward: what if the current state of every contract — its terms, its upcoming milestones, the status of what’s in flight — were something everyone involved could see, the same way, whenever they needed to? That kind of shared, always-fresh picture is exactly what lets both sides of a deal fulfill their contracts smoothly and with confidence. Less time spent chasing status, more time spent on the relationship. That’s what I built ContractOps to support. Not with a single clever prompt, but with a deliberate, down-to-earth architecture. This article is a tour of how I built it — on AWS, fully serverless — and the design philosophy I kept returning to at every turn. What ContractOps actually is Since this article is meant to be the canonical description of the system, let me draw the boundaries clearly before the tour begins. ContractOps is a multi-agent system that supports Legal Operations  — the contract and billing administration that a company’s sales and finance teams carry out, with the backing of the legal department. It is deliberately not a system that renders legal judgments or performs legal work on its own. The work the AI does is bounded and concrete: it structures, indexes, searches, and keeps watch over contract-related information, it assists with drafting (producing first drafts and edits for people to review), and it prepares the groundwork so a human can reach a decision faster. The final judgment always belongs to a human  — and, for anything carrying legal weight, to the legal team or a qualified professional. Human-in-the-loop isn’t a feature I bolted on at the end; as you’ll see, it’s wired into the data model itself. The system is a small pantheon of single-purpose agents, named after Greek deities — less for flair than because a good name makes a responsibility easy to remember. What matters here is that they fall into three layers: The people-facing layer. Themis is the Slack-native assistant people actually talk to — search, analysis, Q&A, and draft assistance — turning information that’s normally scattered across places into something you can simply ask about . Iris is the gateway in front of it, validating incoming Slack events and routing them by domain. The monitoring layer. A small fleet —  Argus , Chronos , and Cassandra  — that keeps the shared picture fresh, each from its own angle. (They’re the stars of Decision 7, so I’ll save the detail.) The knowledge layer. Behind both runs a data pipeline of specialist agents I call the Scribes  — the ones who turn raw documents into trustworthy, structured knowledge. You’ll meet them by name on the tour, exactly where their work comes up. One thing holds for every agent in the system: it reconciles facts, structures information, and notifies. None of them makes a legal call. Chronos can say “this milestone is coming up”; it never decides what should be done about it. That decision is a person’s. The substrate is fully serverless on AWS : AWS Lambda for compute, Amazon DynamoDB for state, Amazon Bedrock (Claude Sonnet 4.5) for inference, Amazon EventBridge for scheduling, Amazon S3 for intermediate artifacts — with Slack and Google Workspace integration at the edges. There is no external message queue. That omission turns out to be a feature, not a gap, and I’ll get to why. ContractOps system architecture (AWS + GCP) ContractOps at a glance: a fleet of single-purpose Lambda agents over a DynamoDB single source of truth, with model inference kept inside the AWS boundary. Now, the tour. Each stop is a design decision, framed the way I actually faced it: a problem anyone building serious LLM agents will hit, the call I made, and what it cost. A principle that kept coming back Before the specifics, one idea connects all of them, so let me name it. An LLM is a probabilistic reasoning engine . The physical data layer — file hierarchies, spreadsheet coordinates like A1:Z45, raw database schemas — is deterministic structure . When you let the two touch directly, the model ends up spending its reasoning on the wrong job: guessing where data lives and how to address it, instead of thinking about your actual question. The fix isn’t novel. It’s a return to principles software engineering settled decades ago — encapsulation, separation of concerns, the Information Expert principle (give a responsibility to the component that actually has the knowledge to fulfill it). Business logic was never asked to understand the physical layout of a database; the industry put a data-access layer in between. An LLM deserves the same courtesy. The further I went, the more this hardened into a full architectural paradigm, which I’ve since written up as a preprint —  [ The Survey-Sonar-Pickup (SSP) Paradigm: Redefining Responsibility Boundaries Between LLMs and Physical Data Layer ] . You don’t need the theory to follow this article; you need the decisions it produced. Here they are. Decision 1 — Don’t send the model exploring. Hand it a menu, not a map. The universal problem. The seductive shortcut in agent design is to grant the model broad access — “here’s read access to the database, go find what you need” — and trust its reasoning for the rest. Modern models are good enough to attempt this, which is exactly the trap. With no map, the agent resorts to trial and error: guessing table names, guessing column meanings, generating speculative queries. Tokens evaporate, the context window chokes on its own speculative garbage, and your system collapses into a spectacular, expensive hallucination loop. The approach. Revoke the model’s right to free exploration. Instead of accepting open-ended search against the physical layer, the tool presents a closed menu of logical options  — semantic IDs plus just enough metadata to choose between them. The model’s job collapses from open-ended generation (“write a query to find X”) into bounded selection (“pick from these”). Hallucination has nowhere to go, because every option on the menu provably exists. The trade-off. The moment you take away free exploration, you owe the model completeness . The menu must exhaustively cover what’s actually there — a candidate missing from the list is one the model can never know to ask for. Drilling down a hierarchy is fine (folders, then files within a chosen folder). Forcing the model to page sideways through “next page, next page” because one response was scoped too narrowly is not. Decision 2 — Pay the comprehension tax up front, not at runtime. The universal problem. Chatbot-era design is poisoned by the Zero-Latency Dogma — the absurd belief that a tool must answer in a second or two or it has failed. But genuinely understanding enterprise documents is expensive . A scanned contract needs OCR and layout reasoning. A richly formatted spreadsheet can encode meaning in cell colors and borders. Done properly, that takes real time — so, under pressure to respond instantly, tools skip the hard part and return whatever flat text they can grab in the moment. That’s how meaning gets dropped before the model ever sees it, with nothing to signal the loss. The approach. Move the heavy structural work off the runtime path entirely and do it asynchronously, ahead of time. In ContractOps, an upfront ETL pipeline absorbs the physical noise before the model is ever involved. Whether the source is a clean digital PDF or a skewed paper scan, the Scribes homogenize it into one rigorous semantic tree — articles, sections, items — expressed as structured Markdown. The model never spends a token interpolating a smudged character or guessing at a layout. The LLM works here too, but as the eyes of a deterministic ETL process , not as a live explorer. The payoff: by query time, the data is already known territory . The latency-versus-completeness dilemma that feels impossible at runtime simply dissolves, because the cost was paid in advance. The trade-off. It looks like you’re trading away instant freshness on brand-new data — but that’s an illusion. The “instant” alternative only ever handed the model impoverished flat text, so there was never any real freshness to lose. And data that genuinely is brand-new — dropped in mid-conversation — gets its own runtime path (Decision 5). The one honest cost is the engineering effort up front: the pipeline has to be designed and maintained. In return, the model can’t be caught off guard by structure that nobody mapped. On AWS. This is a textbook serverless ETL. EventBridge fires on a schedule → Clio (a lightweight Lambda) routes the work → Metis dispatches each document to Theia (vision OCR) or Eunomia (text structuring), both calling Bedrock → intermediate Markdown lands in S3 → structured results land in DynamoDB. Heavy, binary-dependent steps run in container-based Lambdas; everything else ships as a slim ZIP. Idle cost is essentially nothing, because nothing runs when nothing needs to. Clio data pipeline: AllocationMap → contract analysis The pre-runtime comprehension pipeline. Physical noise is absorbed asynchronously and turned into structured, queryable knowledge before any agent asks a question. Decision 3 — Split “finding” from “fetching” at the storage layer. The universal problem. Suppose your data is already beautifully structured. You drop it into one searchable store and let the agent query it. The first hit comes back — and drags a 40-page contract along with it, because the store doesn’t distinguish “the thing you searched for” from “the entire payload of that thing.” A couple of hits like that and the context window is gone. One careless search, total collapse. The approach. Separate the act of finding from the act of fetching , physically, into two stores with two jobs: A search index (the ContractOps table) holds only ultralight metadata — parties, execution dates, status, contract type. The agent scans this to grasp the landscape, with near-zero pressure on the context window. An extraction store (the ContractCodex table) holds the actual payloads — the structured clauses themselves. The flow becomes deterministic: the agent reviews the lightweight menu, picks a document ID and a clause key, and only then fetches exactly those clauses by ID. What used to be an uncertain similarity search across a giant text blob becomes a precise, two-step API call. Searching and reading are no longer the same operation, so reading can never accidentally flood the model. On AWS. Both stores live in DynamoDB under a single-table design, but their responsibilities are cleaved apart by purpose-built tables and secondary indexes — operational metadata indexed for the “find” path, contract knowledge partitioned for the “fetch” path. Same database technology; deliberately different access surfaces. Decision 4 — Don’t hide what you can’t fully trust. Make trust a column. The universal problem. AI-extracted data leaves you with two bad options. Trust it uncritically, and an OCR misread can propagate downstream unnoticed. Or gate every extracted clause behind human review before anyone can use it — honest, but it doesn’t scale. The approach. A third path: don’t block the data, grade it. A dedicated auditor agent, Dike , runs as the last step of the ETL pipeline — long before any model does semantic reasoning. It deterministically cross-checks the structured output against the raw text (using techniques like SHA-256 hashing), marks the indisputable matches as trusted, and marks anything with detected drift as requires verification . Crucially, it does not remove the uncertain data. Everything stays searchable; only its trustworthiness becomes explicit metadata  — a “trust boundary” baked right into the schema. This is what it means to treat the AI’s own output with zero trust . Neither the model nor a person ever has to take a clause on faith. They can see, per item, exactly where automation can be relied upon and exactly where a human should look. And this is where Human-in-the-loop actually lives. It isn’t an abstract principle — it’s a flag in a table that says, for each piece of data, “from here, a person decides.” For a Legal Operations tool, that line — drawn by the system, in data, between what’s safely automated and what a person (and where appropriate, the legal team) should review — is the whole point. The trade-off. The cost is a deterministic audit pass over every clause, paid up front — and it’s a bargain. In return you get two things at once. You structurally prevent the kind of undetected error that no after-the-fact warning can ever catch; and every output now carries an explicit, machine-readable trust boundary. A risk that used to be unknowable — contamination that raises no error of its own — becomes a known, bounded property of the data that humans and agents alike can act on. Decision 5 — Match the pattern to the nature of the data. Reject the universal solution. This is the heart of the tour. Not all data is the same shape, and the single biggest architectural mistake is pretending it is — trying to serve everything through one generic file-reading tool. ContractOps recognizes three fundamentally different kinds of data and applies a different decomposition to each. A. Settled data — already in the system. Past, executed contracts that have been through the ETL pipeline. The comprehension was done up front, so the runtime tooling is lean: scan the lightweight index, then fetch by ID. This is the world of Decisions 2–4. B. Just-arrived data — handed over in the moment. A draft a user just dropped into the chat; a working spreadsheet that didn’t exist five minutes ago. There’s no pre-processing something you’ve never seen, so here the parsing does happen at runtime — but carefully. A heavyweight, isolated tool reconstructs the document into structured Markdown via vision reasoning, then returns just the list of section headers  — a table of contents that doubles as a schema. The moment those headers exist, the model is no longer wandering an unknown document; it’s choosing from a map. It then issues one of two deterministic requests: fetch a whole section, or pull only the blocks matching a condition. The tool runs the repetitive search internally and answers in one shot —  declarative extraction , instead of the model groping item by item. C. Opaque external data — out in the wild (think an untamed Google Drive: folders of mixed PDFs, scans, and docs nobody curated). Files scattered across storage you don’t control and can’t pre-index, where the search API hands you discovery and filtering as a single inseparable operation. Here I keep “finding” fused but force “fetching” to stay separate and lazy  — never opening a file until the model has committed to it. The point isn’t the taxonomy for its own sake. It’s the discipline: a tool is not a one-size-fits-all pipe. Choosing the decomposition that fits the data’s actual structure — its maturity, its time constraints, its opacity — is the design work that keeps the model out of trouble. Themis & supervisor agents flow (interactive + monitoring) The interactive side: Themis reasons over pre-structured knowledge, while the supervisor agents watch the same source of truth from different angles. Decision 6 — Choose an honest limit over false confidence. The universal problem. When the agent searches opaque external storage and hits a file it can’t parse — a legacy Office document, an unreadable image — the tempting design is to filter it out of the results. And even when no one decides this on purpose, simply wiring up generic tools tends to produce exactly that exclusion by default. Cleaner, right? Except now the agent sees nothing where something exists, concludes “no relevant information found,” and reports that with total confidence. A confident false negative  — “that document doesn’t exist” when it very much does — is not just a bug; it is an architectural betrayal. The approach. Don’t hide the unprocessable. The search results include the file even if it can’t be opened. The block happens later, at fetch time, where the tool refuses the unsupported format and says so : “a relevant file exists here, but the system can’t process this format — please check the source directly.” The model stays fully aware the file exists, and instead of inventing an absence, it takes the honest path: it escalates to a person with a precise pointer. This is the point that’s easy to get backwards. When the system honestly reports what it cannot read, it makes the human’s proper role possible: you go to the source and, where it matters, cleanse or contextualize that file by hand before handing it back. On honest information, that isn’t a workaround — it’s the legitimate division of labor between a person and a tool. Real collaboration is built on understood constraints, not on the pretense of unlimited capability. Decision 7 — Govern by choreography, not by a conductor. This is the technical centerpiece, and it’s where the serverless substrate really earns its keep. It’s also where ContractOps does its most valuable supportive work: keeping the shared picture fresh. The universal problem. The default way to coordinate many agents is to appoint a central orchestrator and run everything through an external message queue. It works, but it buys you a single coordination point to keep alive, state duplicated between the queue and your database, and a whole category of distributed-systems bugs — most painfully, notifications that drop or double-fire with nothing to flag either. For supportive monitoring, where the entire value is keeping everyone in sync , a missed or duplicated notification is exactly what you can’t have. The approach, in three parts. A. Choreography over orchestration. There is no conductor. The monitoring agents —  Argus (contract changes), Chronos (deadlines and milestones), Cassandra (early warnings when activity looks likely to run against the terms) — are independent, each woken by its own EventBridge schedule or event. They never call a central brain. Instead, each watches the same single source of truth  — the DynamoDB state — from its own angle. Good coordination emerges from several independent observers cross-checking one shared reality, the way a well-run team stays aligned precisely because more than one person is paying attention. This is also how the supportive jobs map cleanly onto the lifecycle: Argus helps the latest contract state reach the right people; Chronos surfaces milestones early enough to plan around; Cassandra raises an early warning when planned human activity looks likely to run against the schedule or the agreed terms. B. Pure-state idempotency, with no external queue. This is the part I’m proudest of. No SQS, no broker — just Lambda and DynamoDB. Coordination is achieved through DynamoDB conditional writes (optimistic concurrency) plus disciplined state management. Each agent, before it acts, checks the current state and skips if it isn’t what it expected. The result is a fully idempotent “singleton” flow whose design mantra is: a notification is never missed and never duplicated. Those are the two failure modes that plague every naive notifier, and they’re closed off purely through state, not queue plumbing. C. Self-healing on the assumption of undetected failure. Distributed systems fail in ways that raise no error, so the design assumes it. Every Scribe, whenever it runs, also sweeps for stalled jobs  — anything stuck past a short threshold — and resets them to the start of the pipeline. I call it the Ride-Along Sweep : an agent’s ordinary execution doubles as a recovery pass for the stuck work of the colleagues it collaborates with , so a group of collaborators heals itself. Clio runs a full-fleet sweep on a daily schedule as a backstop. The pipeline doesn’t promise it never stumbles; it guarantees it never stays down . A boundary worth naming. Note precisely what these agents do: they reconcile facts and raise notifications. They do not make legal calls. The monitoring fleet hardens the factual groundwork that a person then judges — which is exactly the line a Legal Operations tool must hold. The trade-off. You accept eventual consistency and a little sweep latency. In return you get operational simplicity, low cost, and a kind of fault tolerance that a queue-centric design would charge you dearly for. Decision 8 — Keep secrets inside the boundary. Hold no keys. Contract information is confidential, which makes security its own design pillar — and another place the AWS foundation does heavy lifting. The universal problem. Two fears dominate: that confidential data slips out to a public service or an external AI through some unexpected egress, and that a static credential leaks and hands an attacker the keys. The approach. A private, enclosed boundary. Model inference (Bedrock) and the core retrieval stay inside AWS . Only the strictly necessary paths out — to Google Drive, to Slack — are opened, each behind a tightly scoped API. Unexpected egress is designed out, not merely monitored. Data is encrypted at rest with AWS KMS , and the handful of secrets that do exist live in AWS Secrets Manager  — never in code or environment dumps. Keyless security via Workload Identity Federation. There are no static cloud credentials to leak, because there are none. Starting from an AWS IAM Role , the system federates into Google Cloud service accounts by exchanging short-lived tokens on demand — a multi-cloud privilege chain with nothing long-lived to steal. Dual service-account partitioning. The service account that parses documents and the one that searches are strictly separated, so least privilege is enforced at the infrastructure layer, not by convention. Layered Slack access control. Three gates in series: AWS WAF at the perimeter, Slack request-signature verification for authenticity, and a channel-ID allowlist for authorization — logically sealing the public endpoint down to specific channels only. The throughline is the same one running under every decision in this article: explicit limitation. A boundary you can name and enforce beats a capability you merely hope behaves. The philosophy underneath: autonomy through constraint Step back, and every decision rhymes. Each one takes something away from the model  — the right to roam the physical layer, the burden of guessing at structure it was never told about, the temptation to answer when it shouldn’t — and hands that responsibility to a part of the system actually equipped to bear it. What’s left for the LLM is the thing it’s genuinely good at: reasoning over clean, trustworthy, bounded options. That turned out to be a paradigm worth formalizing. The grand version of the idea — why these boundaries are not just convenient but necessary , and how to reason about them systematically — is laid out in a preprint I’ve written: Survey–Sonar–Pickup (SSP) . ContractOps is its living proof, but the principles are meant to outlive any one system. And the principle, in the end, is a humble one. Real autonomy — and real collaboration between people and AI — doesn’t come from dogmatically worshiping an LLM’s unconstrained capabilities. It comes from ruthlessly enforcing honest, structured boundaries, so the system knows its own limits and a person always holds the decisions that matter. For a tool that supports Legal Operations — work that belongs to people, backed by their legal team — that isn’t a compromise. It’s the entire point. It’s also what lets everyone involved, on both sides of a contract, get on with fulfilling it in confidence. The architectural paradigm behind these decisions — Survey–Sonar–Pickup (SSP) — is described in a preprint: [ The Survey-Sonar-Pickup (SSP) Paradigm: Redefining Responsibility Boundaries Between LLMs and Physical Data Layer ] . ContractOps runs fully serverless on AWS (Lambda, DynamoDB, Bedrock, EventBridge, S3). If you’re building agents that work with messy, high-stakes information, I hope a few of these boundaries are worth borrowing. Special Thanks: By the way, the awesome agent icons used throughout ContractOps (and in the architecture diagram here) were generated by Gemini (nano-banana). In the spirit of full transparency — and recognizing the irony of writing an entire article about strictly limiting AI, only to fully outsource the graphic design to it — huge thanks to the model for bringing these agents to life! ContractOps: A Serverless Multi-Agent System for Contract Operations was originally published in The Finatext Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.
G-gen の西田です。ノーコードで Google Workspace のタスクを自動化できる Google Workspace Studio に導入された、リストデータを繰り返し処理できる ループ処理機能 について解説します。 はじめに Google Workspace Studio とは ループ処理機能の概要 検証シナリオ 設定手順 動作確認 はじめに Google Workspace Studio とは Google Workspace Studio とは、プログラミングを行うことなく、Google Workspace の各アプリケーションや Gemini を組み合わせた自動化フローを作成できる AI エージェントツール です。 ユーザーは「ステップ」と呼ばれる個々のタスクを順に配置することで、複雑な業務プロセスを自動化できます。例えば、「メールを受信したら Gemini で内容を要約し、Google Chat に投稿する」といったフローを直感的に構築可能です。 Google Workspace Studio の詳細やコンセプトについては、以下の記事を参照してください。 blog.g-gen.co.jp ループ処理機能の概要 Google Workspace Studio のフローにおいて、2026年6月に リストをループ処理する機能 が導入されました。これにより、リスト形式のデータに対して同じ処理を繰り返し実行できるようになりました。これまでは1つのデータに対して1つの処理を行うフローが中心でしたが、今回のアップデートにより、リストデータを一括で処理する高度な自動化が容易になります。導入された機能は主に以下の2つです。 機能 概要 Ask Gemini のリスト出力 「Ask Gemini」ステップの「Response format」設定に リスト形式 が追加されました。Gemini に相談した結果を構造化されたリストとして後続のステップに渡すことができます。 Repeat for each Gemini に相談した結果のリストの各項目や Google スプレッドシートの各行に対して、指定した一連の処理(サブステップ)を 繰り返し実行 するステップです。 参考 : Introducing the ability to loop over a list of items in Workspace Studio - Google Workspace Updates 検証シナリオ ループ機能の具体的な活用例として、社内パソコンの管理台帳(Google スプレッドシート)を参照し、OS のサポートが切れている端末の利用者へ案内メールの下書きを作成するフローを構築します。 検証用の管理台帳には、端末管理ソフト等で取得できるデータの一部である、「端末 ID」「マシンベンダー」「OS」「最終起動日」「ログイン ID」「メールアドレス」の項目を持つリストを使用します。 フローでは以下の処理を自動化します。 Gemini で案内メールのテンプレートを作成 管理台帳からデータを取得 各行の OS 情報をチェックし、サポート切れの場合はメールの下書きを作成 管理台帳の「ステータス」列を Done に更新 設定手順 Google Workspace Studio で以下のステップを持つフローを作成します。 ステップ1: トリガー設定 フローが実行されるトリガーを設定します。今回は特定の日時に開始するように設定します。 ステップ2: メール文面作成 サポート切れのパソコンの利用者に向けた案内メールの文面を作成します。検証では「Gemini に相談」を利用して文章を作成します。 なお本来、ここで指定するメールの文面は定型文で問題ありませんが、当記事では Gemini による生成機能の説明を目的として「Gemini に相談」を使用しました。 ステップ3: リストデータ取得 「シートのコンテンツを取得」アクションを選択し、対象のスプレッドシートのシートからリストデータを取得します。すべての行を取得するため「値を取得する行を検索」では、「すべて」を指定します。 ステップ4: ループ処理開始 ループ処理を設定します。日本語の画面では、「それぞれについて繰り返す」というツールを選択します。 「リストを選択します」の変数ボタンから、ステップ3で取得したシートのコンテンツを選択し、「詳細リスト - 一致する値」を指定します。 ステップ5: 処理対象か確認 ループ内のサブステップとして、「OS がサポート切れであるか」を判定するロジックを記述するため、AI アクションの「決定」を追加します。 処理行の「OS」の値を取得するため、プロンプト入力欄の変数ボタンから、ステップ4のループ処理を選択し、その中から「一致する値 "OS" の各アイテム」を指定します。 ここでは、「処理行の OS がサポート切れである」という条件から真偽値を判定させます。 ステップ6: 条件分岐 条件分岐のステップでは、ステップ5の結果が "真" である場合、後続のステップに進ませるようにします。 ステップ7: 下書き作成 条件に一致した行の "メールアドレス" 宛のメールの下書きを作成します。宛先のメールアドレスは、ステップ4のループ処理の中から「一致する値 "メールアドレス" の各アイテム」を指定します。メッセージ(本文)は、ステップ2で作成した文面を指定します。 ステップ8: 台帳更新 下書きを作成した行については、作成済みの記録を入力する処理を設定するため「行を更新」というステップを選択します。更新対象のスプレッドシート、シート、対象の行および入力する値をそれぞれ指定します。 動作確認 フローをテスト実行すると、管理台帳の各行が順番に処理されます。 管理台帳を確認すると、Windows 11 の行以外については、サポート切れ OS と判定され、G 列に下書きが作成された記録が入力されています。 また、作成された下書きメールは以下の通りです。今回の管理台帳では対象となる行が45行ありましたので、同様の下書きメールも45件作成されました。 なお、ループ処理できる行の数は画面の注意書きに記載の通り、100件までです。 101件目以降のデータを処理する場合は、スプレッドシートのシートからのリストデータ取得時に条件を加えるなどして調整します。上記の手順では、ステップ3の「シートのコンテンツを取得」のステップに、「下書き作成」が空白であるといった抽出条件を加えます。 西田 匡志 (記事一覧) クラウドソリューション部 美容商社→物流→情シスを経て、2025年6月G-genにジョイン。Google Cloud を通じて多くの人に貢献できるよう日々精進!
目次 はじめに 背景と課題 方式の検討 全体アーキテクチャ 認証・認可フロー クライアントが認可サーバーを発見するまで トークンの取得 ゲートウェイでの JWT 検証 ゲートウェイの実装ポイント 設定ファイルによるバックエンド管理 検証済みユーザー情報の伝搬となりすまし防止 起動時の OIDC Discovery による fail-fast モック認可サーバーによるローカル開発 インフラ構成 社内での活用事例 今後の課題 おわりに はじめに こんにちは。 開発本部開発3部トモニテ開発部所属の庄司( @ktanonymous )です。 エブリーの開発組織では、日常業務から離れて新しい技術やアイデアに挑戦する「挑戦week」という取り組みを定期的に開催しています。 先日行われた挑戦weekの中で、私たちのチームは全社共通で利用できるリモート MCP ( Model Context Protocol ) サーバー向けの認証・認可ゲートウェイを設計・実装しました。 本記事では、その全体アーキテクチャや認証・認可フロー、実装のポイントなどを紹介したいと思います。 ※ 挑戦weekの詳細については過去の記事で紹介していますので、興味のある方は以下をご覧ください。 tech.every.tv 背景と課題 弊社では非エンジニア職にも Claude が配布され、職種を問わず AI 活用が盛んになってきています。 業務の中で Claude を利用するにあたり、社内情報の取得のために社内 API へ接続したいという需要が高まってきていると感じました。 今後、その手段として各チームがリモート MCP サーバーを立てるケースが増えていくと考えられます。 リモート MCP サーバーはインターネット経由でアクセスされるため、社外の人間が利用できないように認証・認可を考慮する必要がありました。 一方で、MCP サーバーを実装するたびに各チームが認証・認可を設計・実装するのはコスト面でも効率面でも避けたいものです。 そこで、共通で利用できる認証・認可基盤を作り、各チームの MCP サーバーはそれぞれのツールの実装に専念できるようにすることを目指しました。 今回検討した要件は以下の通りです。 Claude (Web / Desktop / Code) などの各種 MCP クライアントから、社内のリモート MCP サーバーのツールを利用できる 弊社の Google Workspace アカウントでログイン済みのユーザーのみがツールを利用できる 各チームが新しく MCP サーバーを追加するとき、認証・認可の実装を不要にする 方式の検討 認証・認可の共通化にあたり、大きく分けて以下 3 つのアプローチを検討しました。 ライブラリ方式: 認証・認可処理を共通ライブラリとして実装し、各 MCP サーバーに組み込む サーバー方式: 認可サーバーを立て、各 MCP サーバーがトークンを問い合わせることで検証する ゲートウェイ方式: ゲートウェイがリクエストを一括で受けて認証・認可を行い、検証済みのリクエストだけを後段の MCP サーバーへ転送する 3 つの内、ライブラリ方式は MCP サーバーの実装言語ごとにライブラリを用意する必要があり、 サーバー方式は、責務こそ分離できるものの、各 MCP サーバー側に認可サーバーへトークンを問い合わせる実装が必要になります。 一方、ゲートウェイ方式であれば、認証・認可をゲートウェイに一元化でき、後段の MCP サーバーは実装言語を問わず認証・認可も意識せずに済みます。 近年の MCP ゲートウェイ製品の設計とも方向性が近いことから、今回はゲートウェイ方式を採用しました。 なお、 mcp-context-forge や agentgateway といった既存の OSS MCP ゲートウェイも調査しましたが、 今回の要件に対して機能・ボリュームが大きすぎたため採用を見送り、必要最小限のゲートウェイを実装することにしました。 全体アーキテクチャ 全体のアーキテクチャは以下の通りです。 アーキテクチャ概要 今回実装したゲートウェイは大きく 4 つの要素から構成されています。 MCP クライアント: Claude Code / Claude Desktop など。OAuth 2.0 のパブリッククライアントの立ち位置 ゲートウェイ: ECS Fargate 上で稼働する Go (Echo) 製のリバースプロキシ。JWT の検証と後段 MCP サーバーへのルーティングを担う。クライアントから直接見える唯一の MCP サーバー。 Amazon Cognito: 認可サーバー兼 IdP。Google Workspace アカウントへのフェデレーションを行い、アクセストークン (JWT) を発行する MCP サーバー群: 各チームが実装するリモート MCP サーバー。private subnet に配置し、ゲートウェイ経由でのみアクセス可能であり、クライアントから直接的には見えていない 意識した点として、JWT を検証するのはゲートウェイだけという点があります。 後段の MCP サーバーは JWT 検証を実装せず、ゲートウェイが注入する検証済みのユーザー情報 (後述の X-Auth-* ヘッダー) を信頼します。 その前提を成立させるため、MCP サーバーはインターネットに直接公開せず、ゲートウェイからのみ到達できるネットワーク構成としました。 認証・認可フロー MCP の認可は MCP Authorization 仕様 で定義されており、 OAuth 2.1 をベースに、PRM (RFC 9728) などの仕様を組み合わせて構成されています。 今回のゲートウェイもこの仕様に沿って実装しています。 具体的には以下のようなフローとなっています。 認証・認可フロー クライアントが認可サーバーを発見するまで MCP クライアントには MCP サーバーの接続先を登録するため、認可サーバーがどこにあるかを予め知ることはできません。 クライアントがトークンなしでアクセスすると、ゲートウェイは 401 Unauthorized を返し、 WWW-Authenticate ヘッダーの resource_metadata パラメータで Protected Resource Metadata (PRM) の URL を通知します。 HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer resource_metadata="https://mcp-gateway.example.com/.well-known/oauth-protected-resource" PRM は、保護されたリソース (今回はゲートウェイ) が「どの認可サーバーに保護されているか」「どのスコープをサポートするか」といった自身のメタデータを公開するための仕様です。 クライアントはこのメタデータを参照することで、トークンの取得先を機械的に発見できます。 クライアントがこの URL にアクセスすると、PRM の仕様で定義された以下のような JSON が返ります。 { " resource ": " https://mcp-gateway.example.com/ ", " authorization_servers ": [ " https://cognito-idp.ap-northeast-1.amazonaws.com/<user-pool-id> " ] , " scopes_supported ": [ " openid ", " email ", " profile " ] , " bearer_methods_supported ": [ " header " ] } クライアントは authorization_servers から認可サーバー (Cognito) を発見し、 さらに Cognito の Authorization Server Metadata ( /.well-known/openid-configuration ) を取得して、 認可エンドポイントやトークンエンドポイントを把握します。 Authorization Server Metadata は、認可サーバーが「どこで認可リクエストやトークン発行を受け付けるか」「どの機能をサポートするか」といった自身の設定情報を公開するための仕様です。 Cognito の場合、以下のような JSON が返ります (主要なフィールドのみ抜粋)。 { " issuer ": " https://cognito-idp.ap-northeast-1.amazonaws.com/<user-pool-id> ", " authorization_endpoint ": " https://<domain>.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize ", " token_endpoint ": " https://<domain>.auth.ap-northeast-1.amazoncognito.com/oauth2/token ", " jwks_uri ": " https://cognito-idp.ap-northeast-1.amazonaws.com/<user-pool-id>/.well-known/jwks.json ", " scopes_supported ": [ " openid ", " email ", " phone ", " profile " ] } この 2 段階のメタデータ取得により、クライアント側に認可サーバーの情報を事前設定することなく、OAuth フローを開始することができます。 トークンの取得 認可サーバーの発見後は、通常の OAuth 2.0 Authorization Code フロー (PKCE 付き) です。 クライアントがブラウザを開いて Cognito の Hosted UI に遷移し、Cognito は Google へフェデレーションします。 ユーザーが自社の Google Workspace アカウントでログインすると、クライアントはアクセストークン (JWT) を取得します。 Cognito 側の設定のポイントは以下の通りです。 パブリッククライアント + PKCE 必須: Claude などの MCP クライアントは利用者の手元で動くため、クライアントシークレットを保持できません。そこでパブリッククライアントとして登録し、PKCE を利用するようにします。 アプリクライアントの事前登録: Cognito では接続元のアプリケーションを「アプリクライアント」として登録します。MCP クライアントごとにユーザープールを登録し、対応するコールバック URL (認可コードの返却先) を設定することで事前に利用するクライアントを登録します。 なお、MCP Authorization 仕様では、クライアントの登録方法として事前登録のほかに、 Client ID Metadata Documents (URL を client_id として扱い、認可サーバーがその URL からクライアント情報を取得する方式) や Dynamic Client Registration (RFC 7591) による動的登録も定義されています。 今回は、Cognito がこれらに対応していないことと、社内利用ではクライアントの種類が限られる (基本的に Claude 系のみ) ことから、仕様でも正規の選択肢とされている事前登録制を採用しました。 また、Cognito のアクセストークンには標準では email クレームが含まれないため、 Pre Token Generation Lambda トリガー を利用して、トークン生成時に email クレームを注入しています。 これにより、後段の MCP サーバーが「誰からのリクエストか」をメールアドレスで判定できるようになります。 ゲートウェイでの JWT 検証 ゲートウェイは、リクエストごとに JWT を検証します。 署名検証には github.com/coreos/go-oidc (v3.18.0) を利用し、 Cognito の JWKS (JSON Web Key Set) は初回取得後にキャッシュされます。 検証項目は以下の通りで、署名・有効期限といった基本的な検証に加えて、クレームベースのチェックを重ねています。 検証項目 内容 失敗時 署名 / iss / exp JWKS による署名検証、発行者・有効期限の確認 401 token_use "access" であること (ID トークンの誤用防止) 401 client_id 事前登録したアプリクライアントの許可リストに含まれること 403 email ドメイン エブリードメインであること 403 scope 必須スコープを満たすこと (設定時のみ) 403 token_use の検証は Cognito 固有のポイントです。 Cognito は ID トークン用とアクセストークン用にそれぞれ別の署名鍵を持ちますが 1 、両方の公開鍵が同一の JWKS で公開されます。 そのため、JWKS 内のいずれかの鍵で署名が検証できることだけを条件にすると、ID トークンも検証を通過してしまいます。 クライアントが誤って ID トークンを Authorization ヘッダーに載せてきた場合に備えて、 token_use クレームが "access" であることを確認しています。 なお、検証項目に aud (audience) クレームは含まれていません。 MCP Authorization 仕様ではリソースサーバーによるトークンの audience 検証が求められていますが、 Cognito のユーザープールが発行するアクセストークンには aud クレームが含まれず、代わりに client_id クレームが含まれます。 そのため、今回の実装では client_id の許可リスト検証によって、トークンが事前登録済みのクライアントに発行されたものであることを確認する形をとっています。 ゲートウェイの実装ポイント ゲートウェイ本体は Go (1.26.3) + Echo (v4.15.2) で実装しました。 ここでは設計上のポイントについて触れます。 設定ファイルによるバックエンド管理 ゲートウェイがバイパスする MCP サーバー (バックエンド) は YAML で宣言的に管理しています。 backends : - name : server-a url : http://server-a.internal:8081 - name : server-b url : http://server-b.internal:8082 ゲートウェイは起動時にこのファイルを読み込み、 /<name>/mcp というパスを各バックエンドの /mcp にマッピングします。 たとえば POST /server-a/mcp へのリクエストは http://server-a.internal:8081/mcp に転送されます。 プロキシ部分は Go 標準ライブラリのリバースプロキシをベースに、以下のように実装しています。 標準実装は転送先のホストを書き換えるだけでパスはそのまま転送するため、転送直前に呼ばれるリクエストの書き換え処理を拡張して、パスプレフィックスの除去を加えています。 func newBackendProxy(targetURL, stripPrefix string ) (*httputil.ReverseProxy, error ) { u, err := url.Parse(targetURL) if err != nil { return nil , fmt.Errorf( "parse %q: %w" , targetURL, err) } rp := httputil.NewSingleHostReverseProxy(u) originalDirector := rp.Director rp.Director = func (req *http.Request) { // 標準の書き換え処理 (転送先を u に向ける) を実行した上で、 // ゲートウェイ側のパスプレフィックスを除去 (/server-a/mcp → /mcp) originalDirector(req) req.Host = u.Host if stripPrefix != "" { req.URL.Path = strings.TrimPrefix(req.URL.Path, stripPrefix) if req.URL.Path == "" { req.URL.Path = "/" } } } // バックエンドに到達できない場合は 502 を返す rp.ErrorHandler = func (w http.ResponseWriter, r *http.Request, err error ) { log.Printf( "[proxy] upstream error %s %s: %v" , r.Method, r.URL.Path, err) http.Error(w, "bad gateway" , http.StatusBadGateway) } return rp, nil } 新しい MCP サーバーを追加したいチームは、サーバーをデプロイしてこの YAML に 1 エントリ追記するだけで、 認証・認可付きのリモート MCP サーバーを公開できます。 検証済みユーザー情報の伝搬となりすまし防止 ゲートウェイは JWT の検証後、 Authorization ヘッダーを除去し、検証済みのクレームを X-Auth-* ヘッダーとしてバックエンドへのリクエストに注入します。 X-Auth-Sub : ユーザー識別子 (sub クレーム) X-Auth-Email : メールアドレス (Pre Token Generation Lambda で注入した email クレーム) X-Auth-Client-Id : OAuth クライアント ID X-Auth-Scope : 許可されたスコープ一覧 このとき重要なのが、クライアントから送られてきた X-Auth-* ヘッダーを必ず削除してから再注入することです。 // 受信した Authorization / X-Auth-* を全て削除(なりすまし防止) stripIncomingAuthHeaders(req.Header) // 検証済みクレームから X-Auth-* を再注入 req.Header.Set( "X-Auth-Sub" , claims.Sub) req.Header.Set( "X-Auth-Email" , claims.Email) req.Header.Set( "X-Auth-Client-Id" , claims.ClientID) req.Header.Set( "X-Auth-Scope" , strings.Join(claims.Scopes, " " )) これにより、クライアントが偽の X-Auth-Sub を付けてリクエストすることで他人になりすますのを防ぎます。 ゲートウェイがクレームヘッダーの削除と再注入を強制するため、バックエンド側は常にゲートウェイで検証済みの X-Auth-* ヘッダーの利用を保証できます。 加えて、バックエンドにはゲートウェイ経由でしか到達できないようにネットワークを構成しています (後述)。 X-Auth-* を信頼できるのは「ゲートウェイを必ず通る」ことが前提なので、アプリケーション実装とネットワーク構成をセットで設計する必要があります。 起動時の OIDC Discovery による fail-fast ゲートウェイは起動時に Cognito へ OIDC Discovery ( /.well-known/openid-configuration の取得) を行います。 issuer の設定ミスなどがあればこの時点で起動エラーになるため、リクエストを受けてから認証エラーが多発する、という事態を防げます。 モック認可サーバーによるローカル開発 ローカル開発用に、OIDC Discovery・JWKS・トークン発行だけを備えた最小限のモック認可サーバーを用意しました。 ゲートウェイから見ると issuer の URL が違うだけなので、実際の Cognito と同じコードパスで JWT 検証まで通しでテストできます。 docker compose でゲートウェイ・サンプルバックエンド・モック認可サーバーを一括起動できるようにしており、AWS 環境なしで認証フロー全体を確認できます。 インフラ構成 インフラは Terraform で管理しています。要点は以下の通りです。 ECS Fargate: ゲートウェイは private subnet に配置し、外部への通信は NAT Gateway 経由 ALB: TLS を終端し、ゲートウェイへ転送。セキュリティグループでゲートウェイへの入力は ALB からのみに制限 Cognito User Pool: Google フェデレーション、アプリクライアント、Resource Server、Pre Token Generation Lambda を Terraform で定義 デプロイ: GitHub Actions から OIDC でロールを引き受けて ECR push と ECS デプロイを実行 MCP クライアント (Claude など) は固定 IP を持たないため、ALB は HTTPS (443) を全公開とし、アクセス制御は JWT 検証と WAF に任せます。 バックエンドの MCP サーバーは private subnet 内でゲートウェイからのみ到達できるようにし、ゲートウェイを経由しない場合はネットワーク的に到達不可能にしています。 社内での活用事例 実際に社内で Redash を操作するリモート MCP サーバーをゲートウェイを利用して社内向けにリリースされました。 Claude とのチャットだけで、クエリの実行やダッシュボードの操作といった Redash 上のほとんどの操作ができます。 ローカル版の Redash MCP サーバーについて、以前の挑戦weekの記事で紹介しています。 tech.every.tv Redash MCP サーバーをリモート化し、認証・認可をゲートウェイに集約することで、個人ごとに API Key などの配布や登録が不要になり、 ビジネスサイドのメンバーであっても、Redash アカウントを持っていれば Google アカウントにログインするだけで API 経由で Redash を操作できるようになりました。 Redash MCP を社内に公開しました 今後の課題 短期間での構築だったため、以下のような課題が残っています。 きめ細かなポリシー管理: 現状は「自社ドメインの社員であること」の確認までで、厳密に権限管理をする場合には、所属チームなどの属性に応じて利用できる MCP サーバーやリソースを制限する仕組みが必要になります。 VPC 間の接続: 各チームの MCP サーバーは別の VPC や AWS アカウントで稼働するケースもあります。そのため、今後さらに MCP サーバーの利用を展開していくためには、VPC Peering などによる VPC 間接続を検討する必要があります。 おわりに 本記事では、全社共通のリモート MCP サーバー向け認証・認可ゲートウェイの設計と実装を紹介しました。 ゲートウェイ方式を採用したことで、認証・認可の実装をゲートウェイに一元化でき、 各チームは MCP サーバーのツール実装に専念して、設定ファイルへの追記だけで認証付きのリモート MCP サーバーを公開できるようになりました。 MCP の認可まわりは仕様の整備が活発に進んでいる領域なので、今後も動向を追いながら基盤を育てていきたいと思います。 この記事が、社内での AI 活用の中で同じような課題感を持っている方の参考になれば幸いです。 最後まで読んでいただき、ありがとうございました。 Understanding user pool JSON web tokens (JWTs) - Amazon Cognito (2026年6月11日閲覧) 。「Amazon Cognito generates two pairs of RSA cryptographic keys for each user pool. One private key signs access tokens, and the other signs ID tokens.」と記載されています。 ↩

動画

該当するコンテンツが見つかりませんでした

書籍