ブログ一覧に戻る

Contract-Driven Development と Specmatic — なぜ契約駆動か、Pact ではなく Specmatic を選んだ理由、そして試して学んだこと

Knowledge

前回の記事 では、NASEBANAL API Specs を公開した背景と、その先に見据えている「内製・外注・エージェントに共通するガードレールとしての OpenAPI Spec」への期待についてお話ししました。そして、その評価の中心に据えている Contract-Driven Development (CDD)、とりわけ Specmatic を用いた取り組みについては「次回のブログで改めて詳しくご紹介する」とお約束していました。本稿はその続報です。

その前に、なぜ私がここに時間を投じているのか、その動機にあたる、もう一段大きな構想についても触れておきたいと思います。


NASEBANAL Evolution で目指したいこと

これまでのブログでご紹介してきたとおり、直近では NASEBANAL Growth という、個人の成長を支援するアプリ群を作り、リリースしてきました。これは自分自身の過去の経験や、いま時点での欲求に根ざして作ったもので、まだまだ拡充したい機能もあり、そのコンセプトにはそれなりの思い入れがあります。ただ、それとは別に、より大きなビジネスを見据えて挑戦していきたい開発があります。それがNASEBANAL Evolution であり、そのソリューションで計画しているのは、AI時代におけるソフトウェア開発プロセスの改変です。

これまで、とりわけ日本のシステム開発では、「餅は餅屋」——ソフトウェア開発はソフトウェアを専業とする業者に任せ、自社は本業にリソースを集中させる、という発想が主流だったのではないでしょうか。そこに経済合理性があることも事実ですが、その外部依存が行き過ぎると新たな問題が顔を出してきます。いまや多くの産業で、ビジネスモデルや収益化のあり方がソフトウェアによって再定義(software-defined)されつつあります。これまで日本が得意としてきたハードウェアの作り込みで競うものづくりも、サブスクリプションのような月額・年額の従量課金でのサービスビジネスへと軸足を移しつつあります。これによりユーザーは、初期投資を抑えつつ、導入効果を実感できた場合に限って本格的な投資へ踏み込めるようになりました。そしてこのサービス化に付随して、ハードウェアの性能にソフトウェアがさらに付加価値を上乗せしていく、という側面も強まっているように思います。こうして産業としての付加価値の源泉は、ハードウェアからソフトウェアという新しいレイヤーへと吸い寄せられていきます。そうなると、ソフトウェアをまるごと外部に委ねている企業は、知らぬ間に競争力の源泉そのものを手放しかねない——これが、「行き過ぎ」が招く課題認識です。

こうしてソフトウェアへの対応の必要性は多くの企業で認識されつつある一方、その開発・保守を内部リソースで賄う「内製化」には、なかなか踏み切れないケースも多いのではないでしょうか。これは白黒のつく話ではなく、どこまでを内部リソースで担うべきかというバランスの議論であり、結果の見えにくい意思決定だからこそ踏み切れない、というのが実情だと感じています。

私自身、Pivotal(後に VMware が買収)というシステムのモダナイゼーション支援に取り組む会社での勤務経験を通して、このあたりの議論に日米の温度感の差を肌で感じてきました。アメリカでは、事業会社自身がシステム開発のモダナイゼーションを進めた事例が数多く公開されている一方、当時の日本で公開されていた事例は一部の Web サービス事業者や SIer が中心で、事業会社自身が本格的なソフトウェアモダナイゼーションを主導する動きは、まだこれからという印象でした。

つまり私の仮説はこうです。内製化の意義は多くの企業で実感されつつも、どこを落としどころにすれば投資対効果を最適化できるのか、その現実的なロードマップを描けていない——ここが大きな課題なのではないか。その議論は期待される効果実績の定量化が難しいこともあり、戦略の意思決定は容易ではないところもあったかと思いますが、そこに昨今の AI の登場と、後述する Contract-Driven Development の仕組みを組み合わせることで、その実現に一歩近づけるのではないかと考えています。


Contract-Driven Development とは、そして NASEBANAL Evolution との接点

CDD は簡単に言えば、ソフトウェア間の連携機能を Contract(契約)として規定し、呼び出される側(Provider)が、呼び出す側(Consumer)の期待するアウトプットを提供できるかを検証する仕組みです。今日の AI コーディングでも注目される Spec-Driven Development の一領域と見ることもできるでしょう。

考え方の源流をたどると、2006 年に Ian Robinson 氏が Martin Fowler のサイトで提唱した Consumer-Driven Contracts というパターンに行き着きます。サービス同士が連携する際、提供側が一方的に仕様を決めるのではなく、利用側の期待を「契約」として明示し、それを満たすことで安全にサービスを進化させていく——という発想です。その後、マイクロサービスが広く普及した 2010 年代に、この契約をテストとして自動検証する手法(コントラクトテスト)が注目を集め、その代表的な OSS として Pact が登場しました(2013 年ごろ)。さらに近年は、OpenAPI のような既存の API 仕様そのものを契約として扱う spec-first のアプローチも現れており、本稿で取り上げる Specmatic はその系譜に位置づけられます。

CDD を実現するソリューションを提供する主要プレイヤーとしては、SmartBearPactSpecmatic が挙げられます。両者の比較は次のセクションで触れますが、これらのツールが提供する主要な機能は、おおむね以下のとおりです。

  • Contract ファイルに基づき、呼び出し側(Consumer)には API のモックを提供し、Consumer 側のパイプラインで API 連携の模擬テスト(単体テスト)を行えるようにする
  • Contract ファイルに基づき、API 提供側(Provider)には、その契約に沿った呼び出しを用いた模擬テスト(単体テスト)を行えるようにする

システム開発では、単体テスト・結合テスト・システムテスト・受け入れテストといった流れで品質を保証していくのが一般的です。しかし今日の Web サービス開発では、フロントエンドとバックエンドを分離して開発するのが通常で、その場合、結合テストを CI パイプラインに乗せるのはなかなか難しいのが実情です。CI パイプラインをパスした場合にのみ Pull Request を main ブランチへマージする運用が一般的ですが、結合テストは Consumer / Provider の両方が正しい状態で揃ってはじめて成功するため、それぞれを単体で開発しているフェーズでは、そもそもパスさせようがないのです。

そこで CDD のツールが生きてきます。CDD を用いると、モックや模擬ドライバが提供されるため、本来は結合テストで確かめる内容を模擬的な単体テストへと分解(ブレイクダウン)し、CI パイプラインに組み込めるようになります。すなわち、より早いフィードバックループを回せるようになる、というわけです。

以上が CDD の基本コンセプトです。では、この CDD が、先に述べた NASEBANAL Evolution とどう関係してくるのか。

内製化を考える上で重要なのは、ソフトウェアのうちどの部分を自分たちでカバーし、どこを外部リソースに任せるか、その落としどころを見極めることだと考えています。そこで私が提案したいのは、ユーザーが直接触れるフロントエンドは内製化し、そこから呼び出されるバックエンド(API)は必要に応じて外注する、という分担です。

  • フロントエンドはユーザーが直接操作する場所であり、要望への対応にあたってフィードバックループを早く回す効果が発揮されやすい領域である
  • フロントエンドはデータの永続化(データベース)を直接持たないぶん、実装アーキテクチャが比較的シンプルになりやすく、とっつきやすい(AI コーディングでも対応しやすい)
  • フロントエンドは開発前に要件を固めきることが現実的に難しく、往々にして初版を作った後、ユーザーからのフィードバックに応じて継続的な改善を行うことが期待される一方、そこから呼び出されるバックエンドのほうが Spec-Driven Development を採用しやすい

ユーザーに近いところを重視し、自分たちの手で対応する——この視点は、ものづくり一般のアプローチとも通じるところがあるのではないでしょうか。そして、外注するバックエンドへの仕様伝達にこそ、CDD を活かすのがよいのではないかと考えています。

また、内製化の文脈にとどまらず、マルチベンダーでの開発におけるベンダー間の仕様調整や品質保証にも活かせると考えています。

この分担の意義は、これまでのアーキテクチャの典型と対比すると見えやすくなります。

レガシースタイルのエンタープライズアーキテクチャ:システムごとに Frontend・Backend・DB が縦に閉じ、システム間はもっぱら DB を介した夜間バッチで連携する

上の図は、よくあるレガシースタイルのエンタープライズシステムの構成です。システムごとに Frontend・Backend・DB が縦に閉じて作り込まれ、それぞれが独立した「サイロ」として並び立っています。この形では、機能がシステム単位で分断されて全体として有効活用されにくく、システム間の連携も——図の DB 同士をつなぐ夜間バッチのように——データベースを直接受け渡すような硬直的なインターフェースに頼りがちです。

さらに、こうしたシステムは往々にして特定ベンダーに一括で作り込まれ、内部の仕様が発注側から見えない「ブラックボックス」になりがちです。そうなると、外部システムとの連携を一つ直したいだけでも、そのつどベンダーへ個別契約で改修を依頼しなければならず、対応にリードタイムとコストがかかります。この「都度個別契約」の積み重ねが、変化に対する俊敏さ——フレキシビリティ(アジリティ)——を大きく損なう要因になります。

これに対して私が目指したいのが、次の図の To-Be です。

To-Be アーキテクチャ:赤枠で囲った内製範囲(Frontend 群+共通の OpenAPI Spec)を発注側がグリップし、その Spec を契約として品質保証(Quality Assurance)を効かせる。バックエンド/DB は Spec の下に連なる

ここでは、フロントエンドとバックエンドをシステムとして分離し、システムごとに縦に閉じていたサイロを、フロントエンド群とバックエンド/DB 群に切り分けたうえで、その境界に共通の OpenAPI Spec を一本通します。この Spec が発注側・受注側の双方が参照する契約となり、発注側は Spec を通じて全体アーキテクチャの仕様をグリップできます。連携も DB の直接受け渡しではなく API を介すことで機能をより柔軟に組み合わせられ、さらにこの Spec を軸に品質保証(Quality Assurance)を効かせられる——これが、CDD が支える連携の骨格です。

図の右側に描いた品質保証(Quality Assurance)は、単一のツールに閉じたものではありません。NASEBANAL Evolution では、本稿で取り上げる Specmatic によるコントラクトテストにとどまらず、複数のオープンソースツールを組み合わせた API 品質保証機能の提供を構想しています。具体的には、Specmatic による契約(コントラクト)テストに加え、Schemathesis を用いた OpenAPI Spec 由来のファズテスト、Playwright による End-to-End テスト、Locust による負荷テストといった各レイヤーの検証を束ねます。いずれも広く使われているオープンソースツールであり、これらを OpenAPI Spec を起点に組み合わせることで、多面的な品質保証を一貫して回せる仕組みを目指しています。


なぜ Specmatic か — Pact との比較

CDD ソリューションとして真っ先に挙がるのは、おそらく Pact でしょう。私自身も実際に試してみました。

Pact の仕組みを簡単に整理すると、API 提供者を Provider、消費者を Consumer としたとき、Consumer の実装から契約情報(Pact ファイル)を生成してリポジトリに格納し、Provider 側がその契約に対するテストを実行して、Consumer の期待を満たせているかを判定する、というものです。

Pact を使った開発フロー:Consumer のテストから契約(pact ファイル)を生成して Pact Repository に publish し、Provider 側で実物のバックエンドに対して検証する。加えて API Spec を別途維持する必要がある

実際に動かしてみると、Consumer(フロントエンド)のテスト実行で生成した pact ファイルを Pact Repository(Broker) に publish し、Provider(バックエンド)側はそれを取得して、記録済みのリクエストを実物のバックエンドにリプレイして検証する、という流れになります。ここで気になったのが、契約(pact)とは別に、ドキュメントや型生成のために OpenAPI Spec も持っておきたくなるという点です。図の④のように両者は自動では連動しないため、手作業で同期し続ける——いわば「契約を二重に管理する」状態になりがちでした。

これを上述の自分の開発の分業アプローチに当てはめようとしたとき、ポイントになるのは、Pact が Consumer-Driven(消費者起点)の仕組みである、という点でした。Pact では契約(Pact ファイル)が Consumer のテスト実行から生成され、Consumer と Provider の ペア単位で独立 して結ばれます。裏を返せば、契約はあくまで「その Consumer が実際に呼び出した範囲」を写したものであり、Provider を作るより前に、独立した仕様として外注先へ手渡せるような性質のものではありません。

つまり Pact が本領を発揮するのは、API を公開する Provider 側が、多数の Consumer の要望をきちんと満たせていることの裏取りである一方、ここでの議論対象のアプローチでは、Consumer 側が、呼び出す複数の Provider の動作を一通りクリアにする用途にあり、そこにソリューションごとの向き不向きが出ると感じました。

Consumer-Driven:1 つの Provider がすべての Consumer の要望を満たす構図(one Provider satisfies all its Consumers)

上の図のように、多数の Consumer が 1 つの Provider に依存する構図——「その Provider が、すべての Consumer の要望を満たせているか」を裏取りしたい場面では、Pact が力を発揮します。

一方で、私が今回 NASEBANAL で取りたいのは、その逆向きでした。Consumer は発注側(自分たち)で少数に限定して内製し、そこから呼び出される複数の Provider を外注する。そして「ある Consumer から見て、各 Provider が仕様どおりに動くか」を一通り保証したい——以下の図の構図です。

Spec-First:1 つの Consumer がすべての Provider を検証する構図(one Consumer verifies all its Providers)

両者を分けている本質は、 契約の出どころにあります。Pact の契約が Consumer の実装から生成される(consumer-derived)のに対し、私が欲しいのは、実装に先立って著した独立の仕様を外注先へ手渡せる spec-first な契約でした。

この観点から関心を持ったのが Specmatic でした。Specmatic は OpenAPI Spec をそのまま契約とするコントラクト定義の仕組みであり、Provider が提供する API 単位で合意する仕様です。実装に先立って契約を固定できる(spec-first)ため、Provider が存在しない段階でも外注先へ仕様を手渡せます。外注する際には、OpenAPI Spec を請負側に渡し、それを契約とすればよい。フロントエンドは Specmatic が生成するモック(スタブ)で開発を進められ、請負側は Specmatic が提供するテスト呼び出し(Example 情報)で品質保証を行えます。さらに、AI コーディングが進んだ今なら、OpenAPI Spec を入力とした Spec-Driven Development でバックエンドの一部を自動生成することも期待でき、オフショアリソースを使った開発と、AI を活用した内製化の間でのベストミックスを考えることも視野に入ってくるかと考えています。

Pact と比べたときの Specmatic の利点は、まずこの構成のシンプルさにあります。契約を別途格納・運用する Pact Repository(Broker)のセットアップが不要で、すでに持っている OpenAPI Spec をそのまま契約として使える点です。先ほどの Pact の図で見たような「API Spec と契約を並行して維持する」冗長な情報管理が要らず、単一の OpenAPI Spec を真実の源(single source of truth)として、Consumer と Provider の双方がそれを参照・検証する——この一本化が、運用上は地味ながら効いてくる差だと感じています。

この「スタブの作りやすさ」は、Consumer の CI/CD を組むうえでも効いてきます。Pact のモックは、自分が手書きしたインタラクションの範囲しか応答しません。そのため、ひとつの Consumer の CI で複数の Provider を相手にアプリを動かそうとすると、契約として記述した呼び出し以外の API は、NockWireMock などで別途スタブを用意する必要が出てきます(Pact ファイルをスタブとして再生する `pact-stub-server` もありますが、それでも「記録済みのインタラクション」の範囲を出ません)。一方 Specmatic は、各 Provider の OpenAPI Spec からスタブを丸ごと自動生成できるため、呼び出すすべての Provider について仕様準拠のフェイクを少ない手間で立ち上げられます。Consumer 側のパイプラインを組みやすいという点も、今回 Specmatic に惹かれた理由のひとつでした。

以下が、実際に NASEBANAL Recorder(フロントエンド)の単体テストシナリオの実行前に呼び出している Specmatic の Mock 起動箇所の実装です。見てのとおり、OpenAPI Spec ファイルを渡すだけで、指定したホスト名(ここでは localhost)とポート番号に Docker コンテナのプロセスがマッピングされていることをご確認いただけます。

export async function setup(): Promise<void> {
  const spec = specPath();
  const specDir = resolve(spec, '..');
  const specFile = spec.slice(specDir.length + 1);

  removeContainer(); // drop a leftover from an interrupted run

  execFileSync(
    'docker',
    [
      'run', '--rm', '-d',
      '--name', CONTAINER,
      '-p', `${HOST_PORT}:9000`,
      '-v', `${specDir}:/spec:ro`,
      SPECMATIC_IMAGE,
      'stub', `/spec/${specFile}`, '--port', '9000',
    ],
    { stdio: 'inherit' },
  );

  // The stub serves bare path templates (no /api/v1 base path), and /health is
  // public (security: [] in the spec), so it answers 200 without a token.
  await waitForStub(`http://localhost:${HOST_PORT}/health`);
}

先に述べたとおり、フロントエンドとバックエンドを分離して開発すると、End-to-End の結合テストを CI/CD パイプラインに乗せるのは難しく、双方の実装が揃ってから結合テストを実施して仕様の認識ずれが発覚し、改修にかかる——という形でリードタイムが伸びがちです。Specmatic による CDD では、OpenAPI Spec を契約として単体レベルで仕様準拠を確かめられるため、認識ずれの多くを結合テストより手前で潰せます。最終的に End-to-End の結合テストは依然として必要ですが、そこへ到達する時点での残存バグが減り、検知の精度も上がることが期待できます。


試したことと学び

こうした背景から、CDD、とりわけ Specmatic を実際に動かしてみました。NASEBANAL の OpenAPI Spec・バックエンド・フロントエンドを一巡させて、契約の更新が全レイヤーに伝播していく流れを組み上げたのが、次のパイプラインです。

NASEBANAL の CDD リリース/バンプ・パイプライン(API Spec を起点に Backend・Frontend へ伝播)

具体的には、次のステップで一巡します。

  1. OpenAPI Spec を別プロジェクトに切り出し、NPM パッケージとして管理。 あわせて、その契約としての OpenAPI Spec を元にしたリファレンスページを公開する。Spec を変更して Merge すると、新バージョンが publish される。
  2. 新しい Spec が Merge されると、Mend RenovateProvider(バックエンド)側の Bump PR を自動作成する。
  3. バックエンドで、新しい仕様をパスする実装を追加し、Merge する。
  4. その Merge をトリガーに、repository_dispatchSpec リポジトリへ更新シグナルを送信する。
  5. Spec リポジトリ側で、新バージョンの 公開ステータス(API Doc)を更新する。
  6. 続いて Renovate が Frontend(コンシューマ)側の Bump PR を自動作成する。
  7. フロントエンドで追従の実装を行い、Merge する。これで Spec・バックエンド・フロントエンドのすべてが、めでたく新しいバージョンに揃う

この一巡を通して、いくつかの学びがありました。

  • Spec ファイルは独立リポジトリで管理し、Renovate のようなボットでの伝播を基本にする。 契約を独立させることが、CDD のすべての出発点でした。
  • Spec のバージョン管理を明確にすることが前提。 とくに Breaking Change の場合は /v2 のようにバージョンを上げ、別実装として並走させる運用方針を厳守すること。互換性を壊す変更を「こっそり」入れない、という規律がパイプラインの信頼性を支えます。
  • AI が作った Spec にもドリフトは起こる。 これまで AI でフロントエンドとバックエンドを同時に自動開発し、Spec も AI に書かせてきました。それでも Spec のレスポンス定義に誤り(具体的には、ある項目が null になり得るのに、それが Spec に反映されていない、というドリフト)があり、Specmatic のコントラクトテストでそれを検知できました。人が細部まで追い切れなくても、契約がガードレールとして機能することを実感した瞬間でした。
  • Consumer は frontend だけではない。 今回さらに NASEBANAL CLINASEBANAL SDK もリリースしました。これらは Google の gcloud(CLI)や各言語向けの Google Cloud クライアント SDK に相当する、NASEBANAL API を操作するための公式クライアントです。これらも同じく Consumer として位置づけることで、CDD を通じて 各クライアントが API(OpenAPI Spec)との互換性(コンパティビリティ)を保てているかを検証できる環境を作れました。新しい SDK や CLI を出しても、契約に照らして準拠性をきれいに確認できます。

今後チャレンジしたいこと

Consumer という観点で見直すと、上で触れたとおり、フロントエンドのほかにも SDK や CLI が挙がります。次のステップでは、モバイルアプリの開発にも着手していきたいと考えています。

1 つの API Spec(契約)を、Web App・Mobile App・SDK・CLI など複数の Consumer が共有する構図

ここで効いてくるのが、契約を中心に据える CDD の構図です。上の図のように、ひとつの API(Provider)に対して、Web アプリ・モバイルアプリ・NASEBANAL SDK・NASEBANAL CLI といった複数の Consumer が対応しえます。それぞれが個別に API の挙動を確かめるのではなく、全員が同じ OpenAPI Spec を契約として共有する——だからこそ、Consumer をいくら増やしても、準拠性を担保するガードレールは一本に保てます。新しいクライアントを追加するたびに仕様の認識合わせをやり直す必要がなく、契約に従うだけでよい。これは、アプリを多角化していくうえで大きな効きどころになると考えています。

今後は、この OpenAPI Spec を中核に据えてアプリを多角化していき、そこにオフショアだけでなく AI エージェントを交えたベストミックスを組んでいく——人間と AI の最適な分担と、そのコスト最適化が、これからの議論になってくるはずです。

今回のブログのメッセージとは少しずれますが、NASEBANAL Growth ではトークナイゼーション(識別情報の扱い)について議論しました。モバイルアプリや CLI を用い、かつオープンソースで公開し、ID と名前をローカルファイルで管理する形にすれば、サーバー側はどのような情報が手元にあるのかを把握しづらくなります。これは、クライアント側で情報を管理するという設計の可能性にもつながるものと考えています。

CDD を基礎として、Consumer をさらに磨き込んでいきながら、その先では NASEBANAL Evolution として、ここまで述べてきた仕様の伝達と品質保証を仕組みとして束ねる、仕様・品質管理のプラットフォームづくりを計画しています。続報は、また改めてご報告できればと思います。