API開発をするサーバサイドとAPIを利用するクライアントサイドというチーム構成でプロダクト開発をしていくにあたり、クライアントサイドがAPIの開発完了を待つことなくつなぎこみの準備をすすめておくためには、クライアントサイドとサーバサイドで事前にAPIのインタフェースを見やすい形式で共有しておく事が必要です。
また、現在開発しているAPIは外部に公開する性質のものではなく、クライアントアプリのみとの通信を想定した内部APIであるため、社内のGSuiteアカウントを持っている人のみが閲覧可能としたい物でした。
さらに、プロダクトがまだリリース前の模索の時期であるため、開発コストやサービスの利用コストをなるべく少なくしたいため、現在利用しているAWS上で簡単に設定できる物がのぞましい状況でした。
そこで、Rails上にCDN経由でSwaggerUIを表示し、GoogleのOAuth2認証をAWSのALBレイヤーでかける事という方法をとりました。
やった事
OpenAPIのドキュメントをRailsプロジェクト内のdocs/openapi.yaml
に記述
OpenAPIのドキュメントをdocs/openapi.yaml
に配置しました、OpenAPI自体はJSONでもYAMLでも書けますがエディターでの編集性を考えるとYAMLの方が書きやすかったため、YAML形式にしています。
また、プロジェクト内に配置する事でcommitteeなどのgemを利用して、 ドキュメントと実際の挙動が一致しているかのテストをCIで実施する事も可能になります。
json-schema-ref-parserでYAMLからJSONに
YAMLで編集する中で、一つのドキュメントに全ての情報をまとめていると、 次第にドキュメントが長くなっていき編集コストや管理コストが増大してしまいます。
そこで、OpenAPIの$ref参照を用いて他のファイルの中身を参照できる機能を利用し、 API毎などの単位で適宜複数のドキュメントに分割した方がドキュメントの見通しが良くなります。
現在利用しているテストライブラリでは、このドキュメントの参照の解決がうまく動作しなかったため、 事前に参照を解決し一つのJSONファイルに変換するためにjson-schema-ref-parserというnodeのライブラリを利用しました。
そして、以下のようなスクリプトを docs/generate_openapi_json.js
として保存しておきます。
const $RefParser = require("@apidevtools/json-schema-ref-parser"); const fs = require('fs'); const path = require('path'); const yaml_file_path = path.join(__dirname, 'openapi.yaml'); const json_file_path = path.join(__dirname, '../app/views/swagger/uis/openapi.json'); $RefParser.bundle(yaml_file_path, (err, schema) => { if (err) throw err; fs.writeFile(json_file_path, JSON.stringify(schema), (err) => { if (err) throw err; }); })
このスクリプトを適宜、package.jsonなどの"script"から呼びだせるようにしておきます。
swagger-uiをRailsから配信する
ドキュメントを閲覧しやすく表示するために、SwaggerUIを利用しています。 いくつかのGemでは、SwaggerUIをRailsに埋めこむ物を提供していましたが、 あまりメンテナンスされていなかったり、対応しているSwaggerUIのバージョンが、 プロジェクトで利用しているOAS3に対応していなかったりしたため、今回は自前でSwaggerUIを表示する事に決めました。
SwaggerUIを配信するためのコントローラーをRailsに配置
# config/routes.rb namespace :swagger do get '/ui', to: 'uis#index' get '/openapi', to: 'uis#openapi' end
# app/controllers/swagger/uis_controller.rb # frozen_string_literal: true module Swagger class UisController < ::ApplicationController def index render layout: false end def openapi render :openapi end end end
このコントローラでは、/swagger/ui
というpathでSwaggerUIを、 swagger/openapi.json
というパスでopenapi.jsonを配信する想定となっています。
また、openapi.jsonファイルの生成時に app/views/swagger/uis/openapi.json
に配置したファイルをopenapiアクションでは表示するようにしています。
ViewファイルでSwaggerUIをCDN経由で表示
SwaggerUIはunpkg越しに配信されています。
swagger-ui/installation.md at master · swagger-api/swagger-ui · GitHub
今回はそちらを利用しています:
<!-- app/views/swagger/uis/index.html.erb --> <div id="swagger-ui"></div> <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3.25.0/swagger-ui.css"> <style> html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; } *, *:before, *:after { box-sizing: inherit; } body { margin:0; background: #fafafa; } </style> <script src="https://unpkg.com/swagger-ui-dist@3.25.0/swagger-ui-standalone-preset.js"></script> <script src="https://unpkg.com/swagger-ui-dist@3.25.0/swagger-ui-bundle.js"></script> <script> window.onload = function() { // Build a system const ui = SwaggerUIBundle({ url: "<%= Rails.application.credentials.dig(:swagger, :document_url) %>", dom_id: '#swagger-ui', deepLinking: true, presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ], plugins: [ SwaggerUIBundle.plugins.DownloadUrl ], layout: "StandaloneLayout", }) window.ui = ui } </script>
このViewではunpkg越しにswagger-uiを埋めこんでいるだけでなく、
OpenAPIのドキュメントがどこから手にはいるのかを、Railsのcredentialsから取得しています。
たとえば、development環境ではhttp://localhost:3000/swagger/openapi.json
から手にはいりますが、
ステージングサーバーではホスト名がことなったりするため、このようにしています。
Google OAuth2認証を作成する
こちらは、Google Developer Consoleの「APIとサービス」から「OAuth 同意画面」と「認証情報」のOAuthクライアントを作成する事で可能です。 この時に、OAuth 同意画面を「内部」に設定しておく事で、組織内部の人間のみが認可されるようになります。
ALBレイヤーで/swagger/*
にGoogle OAuth2認証をかける
弊社のプロジェクトでは、terraformを利用してAWSのインフラを量していたため、
OAuthクライアントのキーとシークレットをAWS KMSで暗号化し、以下のようにaws_lb_listener_rule
として追加しました。
resource "aws_lb_listener_rule" "swagger" { listener_arn = data.aws_lb_listener.main.arn priority = 1 action { order = 1 type = "authenticate-oidc" authenticate_oidc { authentication_request_extra_params = {} authorization_endpoint = "https://accounts.google.com/o/oauth2/v2/auth" client_id = var.client_id client_secret = "${data.aws_kms_secrets.main.plaintext["client_secret"]}" issuer = "https://accounts.google.com" on_unauthenticated_request = "authenticate" scope = "openid" session_cookie_name = "AWSELBAuthSessionCookie" session_timeout = 604800 token_endpoint = "https://oauth2.googleapis.com/token" user_info_endpoint = "https://openidconnect.googleapis.com/v1/userinfo" } } action { order = 2 target_group_arn = data.aws_lb_target_group.main.arn type = "forward" } condition { field = "path-pattern" values = [ "/swagger*", ] } }
このようにする事で、/swagger*
に一致するパスにアクセスされようとした場合に、自動的にGoogle OAuth2認証がかかるようになります。
結果
無事、SwaggerUIが表示されるようになり、アクセスしようとすると認証が求められるようになりました!
追記
SwaggerUIを配信する部分をRailsのMountableEngineのgemとして切りだしました