SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

OpenAPIでスキーマ駆動開発をはじめました

こんにちは!スマートキャンプエンジニアの中田です。

普段業務ではBOXIL SaaSの開発に従事しています。

突然ですが皆さんはスキーマ駆動に開発されてますか?

直近、BOXIL SaaSにOpenAPIを導入しスキーマ駆動開発を始めたので、今回はその紹介記事です。 導入の経緯や利用方法、メリット等についてご紹介していくので、導入や改善の参考にしていただけると幸いです!

OpenAPI Specificationとは

OpenAPI Specification(以下OAS)とは、REST APIの仕様を記述するためのフォーマットのことです。

OASはYaml or Json形式で定義でき、この定義が各種プログラミング言語に依存しない形式で標準化されています。 そのためSwaggerツールやその他サードパーティ製の関連ツールと組み合わせ、OASの定義を元に多様な言語でAPI Clientやドキュメントの生成が可能です。

※ SwaggerはOpenAPIの前の名称。

公式にはOpenAPIOpenAPI Specificationが同義であるような記載がありましたが、本記事では説明の便宜上以下の意味で言葉を使い分けます。

  • OpenAPI Specification
    • REST APIの仕様を記述するためのフォーマット(仕様書)
  • OpenAPI
    • OASでAPIの仕様を管理する開発の方式

導入の経緯

はじめにOpenAPIを導入するに至った経緯についてお話します。

外部に公開するAPIを作ることになった

私が直近で開発していたBOXIL SaaSの新機能が、BALES CLOUDという弊社提供の別サービスにBOXIL SaaSで持っているリソースを共有したいという内容のものでした。また、その機能の将来的なビジョンとして、共有を社内サービスだけに閉じず外部のサービスにも拡張できるようにしたいという話もありました。

そこでチームメンバーと相談の末、OpenAPIを導入して開発することを決めました。 その導入の決め手となったOpenAPIへの期待値は以下のようなものでした。

  • 公開APIを作るなら仕様を表したドキュメントが欲しい
    • OpenAPIなら公開用ドキュメントの生成や管理が楽になりそう
  • BALES CLOUD(公開APIを利用する側のサービス)にAPIクライアントの実装が必要
    • OpenAPIならクライアントコードもいい感じに生成できるツールがありそう

とても便利なことに気づいた

実際に外部公開APIを開発するためにOpenAPIを利用してみての体験はかなり良いものでした。

期待値として挙げていた公開用ドキュメントとクライアントコードの自動生成は、周辺ツールと組み合わせることで難なく可能で開発工数も削減でき、OASにAPI仕様の定義を集約できることによる恩恵を多分に感じることができました。

内部利用のAPIの開発にも利用したくなった

外部公開APIの開発がきっかけで「OpenAPIを利用した開発をBOXIL SaaS内部で利用するAPIの開発にも適用したい」と思うようになりました。

内部利用APIへの適用には、前述の外部公開APIの項で挙げたものの他にも以下の期待値があり、内部利用APIの開発にもOpenAPIを適用することに決めました。

  • APIとフロントでのインターフェース定義のズレを改善したい
    • OpenAPIでスキーマ駆動な開発をすることで定義が一元化され改善できそう
  • APIのデバッグコストを下げたい
    • OpenAPIではOASから生成したドキュメントからAPIをコールできるので便利になる

BOXIL SaaSでの利用方法

次に、実際にBOXIL SaaSでOpenAPIをどのように利用しているのかについてご紹介します。

導入ツール

OpenAPIの利用にあたり以下のツールを新たに導入しました。

  • SwaggerUI
    • OASからドキュメントを生成できるツール
  • Committee
    • OASの定義とAPIの仕様に差分がないかRSpecで検証するためのツール
  • OpenAPIGenerator
    • OASからクライアントコードを生成できるツール

これらのツールを利用したBOXIL SaaS OpenAPI開発の全体像は以下の図のようになっています。

全体の構成

OAS

BOXIL SaaSでは上図の通り、外部公開APIについて記述する用と内部利用APIについて記述する用とでOASを2枚定義する運用をしています。

この運用は、外部公開APIと内部利用APIとで欲しいドキュメントやクライアントコードが異なるため、その棲み分けをする意図で行なっています。

SwaggerUI

OASからドキュメントを生成するツールにはSwaggerUIを利用しています。

公式からDockerイメージが公開されているので、それを利用しています。

// ...snip
services:
  swagger-ui:
    image: swaggerapi/swagger-ui
    container_name: swagger_ui
    ports:
      - 9000:8080
    volumes:
      - ./openapi/oas/internal/openapi.yaml:/usr/share/nginx/html/internal.yaml
      - ./openapi/oas/external/openapi.yaml:/usr/share/nginx/html/external.yaml
    environment:
      API_URL: ./internal.yaml
      WITH_CREDENTIALS: 'true'
  // ...snip

※ 環境変数のWITH_CREDENTIALSは、BOXIL SaaSでCookie(Session)認証を利用している関係で渡しています。 (ブラウザからAPIのリクエストにCookieの情報を付与できるようにするため)

実際にSwaggerUIで生成しているドキュメントが以下になります。

SwaggerUI (1)

OASの定義に従って、定義されているAPIのパスを一覧化してくれます。

SwaggerUI (2)

また、各パスの中に入ると、対象のAPIのコールができたり、req/resの型情報などの詳細が確認できます。

OpenAPIのドキュメント生成ツールの有名どころはSwaggerUIとReDocの2つです。 BOXIL SaaSでSwaggerUIを選択したポイントは以下でした。

  • ブラウザからAPIをコールできる機能がReDocのオープンソースには無かった(商用のReDocでは提供されているそう)
  • OASを2枚運用している関係でSwaggerUIであれば参照するOASをブラウザから手軽に切り替えることができ都合が良い

個人的にUIはReDocの方が好みだったので迷いました。今回はOpenAPIへの期待値としてAPIデバッグコストの低減化があったため、ブラウザから楽に検証ができるSwaggerUIの方を選択しました。

Committee

今後OpenAPIを利用した運用を進めていく中で、「APIに施した修正をOASに反映し忘れたりするんじゃないか」という懸念がありました。

特にBOXIL SaaSでは開発初期からOpenAPIを導入しているわけではなく、これまでのAPI開発の一連の工程に新たにOpenAPI導入により必要な工程が加わる形となるため、先の懸念が現実化する可能性は大いに考えられました。

この懸念を解消するのに便利なツールに、Request SpecでAPIのreq/resの内容とOAS定義の整合性を検証できるCommitteeCommittee::Railsがあったため導入しました。

Committeeでは、提供されるヘルパーメソッドをRequest Specで利用することで、APIのreq/resの内容とOASの定義にズレがないかを検証できます。

(Committeeの利用方法について詳しく知りたい方は公式の利用方法を参照ください)

CommitteeをBOXIL SaaSでの開発形態に合わせてより快適に利用するため以下のような工夫をしています。

RSpec.configure do |config|
  config.include Committee::Rails::Test::Methods
  config.include CommitteeHelper

  config.add_setting :committee_options
  config.committee_options = {
    schema_path: CommitteeHelper::INTERNAL_PATH,
    old_assert_behavior: false,
    query_hash_key: 'rack.request.query_hash',
    parse_response_by_content_type: false
  }

  # hooks for openapi: :internal
  config.before(:all, openapi: :internal) { overwrite_schema_as_internal }
  config.after(:each, openapi: :internal) { assert_openapi_schema(expected_status) }

  # hooks for openapi: :external
  config.before(:all, openapi: :external) { overwrite_schema_as_external }
  config.after(:each, openapi: :external) { assert_openapi_schema(expected_status) }
end
module CommitteeHelper
  include Committee::Rails::Test::Methods

  EXTERNAL_PATH = Rails.root.join('openapi/oas/external/openapi.yaml').to_s
  INTERNAL_PATH = Rails.root.join('openapi/oas/internal/openapi.yaml').to_s

  # Committee提供のassertメソッドのラッパー
  def assert_openapi_schema(expected_status)
    # 400番を期待する場合はCommittee::InvalidRequestが起こることを検証する
    if expected_status == 400
      expect do
        assert_request_schema_confirm
      end.to raise_error(Committee::InvalidRequest)
      assert_response_schema_confirm(expected_status)
    else
      assert_schema_conform(expected_status)
    end
  end

  # Committeeの参照するOASを外部公開に切り替える
  def overwrite_schema_as_external
    RSpec.configure do |config|
      config.committee_options[:schema_path] = EXTERNAL_PATH
    end
  end

  # Committeeの参照するOASを内部利用に切り替える
  def overwrite_schema_as_internal
    RSpec.configure do |config|
      config.committee_options[:schema_path] = INTERNAL_PATH
    end
  end
end

上記の工夫により、実際にOASとAPIの整合性を検証するRequest Specは以下のコードで書けるようになってます。

# 内部利用OASを参照したいRequest Spec
RSpec.describe 'request spec (internal)', openapi: :internal do
  let(:expected_status) { 200 } # アサーション時にこの変数が参照される

  it 'returns :ok' do
    subject
    expect(response).to have_http_status(expected_status)
  end
end

# 外部利用OASを参照したいRequest Spec
RSpec.describe 'request spec (external)', openapi: :internal do
  let(:expected_status) { 200 } # アサーション時にこの変数が参照される

  it 'returns :ok' do
    subject
    expect(response).to have_http_status(expected_status)
  end
end

コードを説明していきます。

BOXIL SaaSはOASを2枚体制で運用をしているため、外部公開APIのSpecか内部利用APIのSpecかによってCommitteeの参照OASを切り替える必要がありました。 その切り替えに必要な振る舞いを以下の記述により簡易化しています。

(RSpecのメタデータを使ってopenapi: :internalスコープであれば内部利用のOAS、openapi: :externalスコープであれば外部利用のOASに参照をスイッチしている)

config.before(:all, openapi: :internal) { overwrite_schema_as_internal }
config.before(:all, openapi: :external) { overwrite_schema_as_external }

また、「スキーマの整合性検証忘れ」を防止するために、openapi: :internalopenapi: :externalスコープ内ではafter(:each)でフックしてテストケースごとに自動で検証が走るようにしています。

config.after(:each, openapi: :internal) { assert_openapi_schema(expected_status) }
config.after(:each, openapi: :internal) { assert_openapi_schema(expected_status) }

BOXIL SaaSの開発形態にあわせてCommitteeをよしなにラップすることで、かなり便利に使えています。

OpenAPIGenerator

クライアントコードの生成ツールには、OpenAPI Generatorを利用しています。

OpenAPIGeneratorも、SwaggerUI同様にDockerイメージが公開されているためそれを利用しています。

運用している2枚のOASで、それぞれ以下のように生成を仕分けて利用しています。

  • 内部利用APIのクライアントコード
    • TypeScriptで生成
    • BOXIL SaaSのフロントエンドで利用
  • 外部公開APIのクライアントコード
    • Rubyで生成
    • Gem化してBALES CLOUDで利用

外部公開APIのクライアントGemは現時点ではGitHub Packagesを利用して社内でプライベートに管理しています。 APIを完全に外部公開するタイミングで、将来的にはこのGemもパブリックに変更する予定です。

OpenAPIGeneratorで生成したコードを利用したプライベートGem

導入時に意識したこと

導入にあたってはとにかく「恩恵を受けやすい環境を整えること」を意識しました。

現在BOXIL SaaSの開発に携わっているメンバーは10名程とそこそこ多く、このように新規で導入したツールについて30分時間を取り全員に共有するとしても、単純計算で300分と中々のリソースが割かれます。 また既存のシステムに新たに乗せる形で導入しているため、中には「既存の開発方法の方が遥かに良かったじゃないか!」と導入に納得感を得られないメンバーも出てくるかもしれません。

OpenAPIはものとしてかなり便利ですがエコシステムも広大で情報量が多いため、導入時点でなるべく各ツールの利用方法を簡易化する工夫をし利用の流れを型化してドキュメントに起こしました。

そうすることで、利用方法を共有するコストを下げたり、チーム内で使い方にバラつきが出にくくなったり、導入初期時点ではOpenAPIに精通していないメンバーにも「なんか簡単で超便利」とOpenAPIによる恩恵を感じてもらい易くなったりと、OpenAPIを利用した開発が自然とチームに定着していくような環境を目指しました。

導入で感じたメリット

最後にOpenAPIの導入で感じたメリットについて紹介します。

基本的に導入の経緯の章に記載している内容と同じになりますが、以下のようなメリットを感じています。

  • APIの仕様が明確になる
    • OASに他のAPIの仕様もまとまっているため、APIのつくりを型化/統一化できる
    • SwaggerUIで仕様を一見して理解しやすい。デバッグも楽に
  • スキーマ駆動に開発できる
    • OASからドキュメント、クライアントコードが自動生成でき効率的
    • OASを修正すれば上記の生成物もまとめて修正できるため管理コストが低い

導入が直近で本格的な運用はこれからになるため、現時点では明らかに導入によるメリットの方を大きく感じており、デメリットはあまり感じていません。

今後運用が本格化するに連れ、新たに感じるメリットやもしくは運用が大変などのデメリットも出てくるかもしれません。

まとめ

いかがでしたでしょうか?

今回はBOXIL SaaSに導入したOpenAPIについて書かせていただきました。

導入してからまだ日が浅く運用が本格化するのは今後になるため、実際運用してみての記事についてもまたいつか書ければ良いなと思ってます。

今のところ新規開発のAPIのみOpenAPIで開発している状態ですが、既存のAPIも含めマルっとOpenAPI化できるとより恩恵を受けやすくなると思うので、機会をみて徐々に定義を移していきたいなと思ってます。(長い戦いになりそうですが)

最後まで読んでいただきありがとうございました!

あとがき

今回OpenAPI導入のアイデアを提案してくれたのは一緒に新機能の開発をしていた先輩エンジニアでした。 ビジネス要件を上手く噛み砕いて開発的な改善にも繋がった本提案は、一開発者としてとても勉強になりました。 スペシャルサンクスを贈ります!