はじめに
Rubyで稼動するSinatraを利用して自前のWeb APIを実装したプログラムがあります。小規模なプログラムですが必要な機能はクラス単位で分割していたのでRuby言語内であれば変更は容易になるよう設計されています。ドキュメントを準備しようとしたタイミングで将来的にはCLIや様々な環境で実行できる小規模なクライアントが複数必要になりそうだと判断したので、OpenAPIで書き直すことにしました。
これはその際のメモです。
OpenAPIを使おうとして学んだこと
- OpenAPIではなくSwaggerと未だに呼ばれていて、ドキュメントの表記も安定していない
- APIドキュメントを整備するためだけに利用する人もいる (つまり、スタブコードを生成するつもりのない人も存在する)
- 既に存在するAPIにアクセスするクライアントを開発する目的で利用しようとする人もいる (サーバー側のOpenAPI定義や説明がないものもある)
- OpenAPI 3.0策定の過程でゴタゴタがあってopenapi-generatorが開発された
- 自分の用途ではOpenAPI Specificationはopenapi-generatorを中心に学んだ方が良いと思われる
Swaggerを開発したSmartBear社の価格付けは営利目的のチーム開発に特化しているので、会社などで利用するのであれば素直にSwaggerHub等を利用した方が良いと思います。
定義フェーズから実装、テストまで一貫したワークフローを実現する、個人の生産性を上げるツールとしてopenapi-generatorを利用しています。
利用してから10ヶ月ほど経過しましたが、生産性という意味では期待どおりでした。まだまだ学ぶべき点がありますが、満足しています。
参考文献
- https://openapi-generator.tech/ - OpenAPI Generatorの公式サイト
- https://news.mynavi.jp/itsearch/article/devsoft/3854 - OpenAPI 3.0を説明している記事でSwaggerとOpenAPIの関連を知ることもできる
- https://openapi.tools/ - OpenAPIに対応したツールのリスト 対応している仕様のバージョンやGithubに登録されているかなどが一目瞭然
OpenAPI定義の作成とチェック
編集自体はいくつかのサンプルを確認することで可能ですし、公開されているswaggerツールを使ってもv2で保存した定義をv3にコンバートすることもできます。
いずれにしても編集中は、openapi-spec-validatorを利用して問題がないか確認しています。インストールはいくつかの方法が公式サイトに掲載されているので好みに併せて選択すれば良いと思います。
$ ~/.local/bin/openapi-spec-validator openapi.yaml
実際はPATH環境変数に~/.local/bin/を加えています。
コード生成
npm -g
を利用してopenapi-generator-cliを導入した場合、最初に利用するタイミング、バージョンアップが行なわれたタイミングで/usr/local/lib以下に新しいバージョンのJARファイルを保存しようとします。
当然、root権限が必要になるので、次のようなエラーになった場合は、root権限でコマンドを起動してから利用を開始してください。
$ openapi-generator-cli generate -g ruby-sinatra -o code -i openapi.yaml
Download 5.1.0 ...
events.js:287
throw er; // Unhandled 'error' event
^
Error: EACCES: permission denied, open '/usr/local/lib/node_modules/@openapitools/openapi-generator-cli/versio
ns/5.1.0.jar'
Emitted 'error' event on WriteStream instance at:
at errorOrDestroy (internal/streams/destroy.js:108:12)
at WriteStream.onerror (_stream_readable.js:729:7)
at WriteStream.emit (events.js:310:20)
at /usr/local/lib/node_modules/@openapitools/openapi-generator-cli/node_modules/graceful-fs/graceful-fs.js
:303:14
at /usr/local/lib/node_modules/@openapitools/openapi-generator-cli/node_modules/graceful-fs/graceful-fs.js
:333:16
at FSReqCallback.oncomplete (fs.js:155:23) {
errno: -13,
code: 'EACCES',
syscall: 'open',
path: '/usr/local/lib/node_modules/@openapitools/openapi-generator-cli/versions/5.1.0.jar'
}
次のようにroot権限でコマンドを起動します。通常利用時はsudoを使わずに、一般ユーザーの権限で利用することがお勧めです。
$ sudo openapi-generator-cli version
Download 5.1.0 ...
Downloaded 5.1.0
Did set selected version to 5.1.0
5.1.0
この他に、カレントディレクトリに openapitools.json ファイルがある場合には、中に記述されているバージョンが古いと、その古いバージョンのJARファイルをダウンロードし、/usr/local/lib以下にコピーしようとして権限がないことでエラーになる場合があります。
このエラーを回避するためには、openapitools.jsonファイルを削除するか、中に記述されているバージョン番号を最新にすることで回避できます。
openapi-generator-cliでSinatra用コードを生成した後
$ openapi-generator-cli generate-cli -g ruby-sinatra -o code -i openapi.yaml
- 自前のクラスは*code/lib/*に配置し、code/config.ruの中で、require "myconfig" のような形式で記述しています
- ./libをロードパスに加えるために、~/.bundle/configにBUNDLE_PATHを設定するため
$ bundle config set path lib
を実行しています - 定義したAPI毎に各特異メソッド(MyApp.add_route(...))がcode/api/default_api.rbに準備されるので、必要な処理を追記します
---
BUNDLE_PATH: "lib"
config.ruの冒頭に次のような記述を加えています。
require 'bundler/setup'
require 'sinatra/r18n'
require 'myclass'
...
以前はcode/my_app.rbに処理を記述していましたが、openapi-generator-cliによって生成される度に内容(バージョン番号)が変化するため、このファイルを変更することは止めています。
生成したコードの実行
次のように生成したコードが起動するかチェックすることができる。必要に応じて(Ubuntu 18.04の場合)bundler,ruby-rackパッケージを導入すること。
$ cd code
$ bundle config set path lib
$ bundle install
$ bundle exec rackup --host 127.0.0.1 --port 8080
ドキュメント生成
openapi-generator-cliでWebページに組み込むためのHTMLファイルを生成する
-gオプションのターゲットに指定する対象をDocumentationから選択する。ただ、Hugo等のSite Generatorに合わせるようなHTMLの断片を生成する良い方法はなさそう。
$ openapi-generator-cli generate -g asciidoc -o docs -i openapi.yaml
現状では、CSS, JavaScriptについて良いものは発見できていない。Hugoの公式サイトにもあるhugo-openapispec-shortcodeはJavaScriptに強く依存していて最近はきちんと動いていないように見受けられる。
Dockerへの組み込み
デバッグ用途や初期の勉強用にUbuntuイメージは便利ですが、200MB以上のサイズになるため実用途ではruby:3.0-alpineなどのイメージを元にしています。
Gemfileの記述
Gemfileは必要なファイルを記述しますが、ruby:2.7-alpineからruby:3.0-alpineにコンテナイメージを変更した際には、gem "puma"
を明示的に指定する必要がありました。
Ruby-3.0.0のリリースからwebrickを含むいくつかのパッケージは、標準ライブラリから外されました。
このためSinatraフレームワークをサポートする何かしらのWebサーバーを導入する必要があり、ライブラリが存在しないと次のようなエラーになります。
+ bundle exec rackup --host 0.0.0.0 --port 8080
bundler: failed to load command: rackup (/app/lib/ruby/3.0.0/bin/rackup)
/app/lib/ruby/3.0.0/gems/rack-2.2.3/lib/rack/handler.rb:45:in `pick': Couldn't find handler for: puma, thin, falcon, webrick. (LoadError)
必要なことは、"puma", "thin", "falcon", "webrick" のいずれか一つを明示的にGemfileに指定することです。ここでは、次のようにGemfileにpumaを明示的に指定しています。このGemfileはそのままruby-2.7でも利用可能です。
source 'https://rubygems.org'
gem "puma"
gem "sinatra"
gem "sinatra-cross_origin"
gem "json"
Gemfileに"json"を指定すると、高速化のために共有ライブラリを作成するためコンパイラを作成します。
コンパイラを利用しますが実行時には不要なため、Dockerコンテナをビルド(build)する際に、コンテナサイズを圧縮するためマルチステージビルド(multi-stage build)を利用して、コンパイラを実行環境には含まないようにしています。
Dockerfileの記述方法
Sinatraを実行するため、BundlerとRackを導入する。ここではcodeの中にopenapi-generator-cliで生成したコードが入っています。これをDockerイメージの/jobにコピーし、この中でbundle installコマンドを実行しています。
FROM ubuntu:18.04
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y upgrade
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ruby ruby-sinatra ruby-httpclient ruby-rack ruby-sinatra-contrib ruby-bundler bundler
COPY code /job
WORKDIR /job
RUN bundle config set path lib
RUN bundle install
ENV SINATRA_PORT 80
EXPOSE $SINATRA_PORT
ADD run.sh /run.sh
RUN chmod +x /run.sh
ENTRYPOINT ["/run.sh"]
セキュリティのため実行時ユーザーを変更する際には、USER行の次にRUN bundle config set path lib
を改めて実行し、実行用ユーザーの~/.bundle/configファイルを準備します。
ubuntuベースのシステムは柔軟性に長けていますが、Harborに登録した際に、Clairのスキャンにひっかかるので、現在はalpineをベースにしています。
また利用したいライブラリがコンパイルを必要とする場合が良くあるので、multiple stage buildを利用しています。
## Build stage
FROM ruby:3.0-alpine as rubydev
RUN apk --no-cache add tzdata bash ca-certificates make gcc libc-dev linux-headers build-base patch
COPY . /app
WORKDIR /app
RUN cp /usr/local/include/ruby-2.7.0/ruby/defines.h /usr/local/include/ruby-2.7.0/defines.h
RUN bundle config path lib
RUN bundle install
## Runtime stage
FROM ruby:3.0-alpine
RUN apk --no-cache add tzdata bash ca-certificates
COPY --from=rubydev /app /app
WORKDIR /app
ENV SINATRA_PORT 80
EXPOSE $SINATRA_PORT
ADD run.sh /run.sh
RUN chmod +x /run.sh
RUN addgroup sinatra
RUN adduser -S -G sinatra sinatra
USER sinatra
RUN bundle config set path lib
ENTRYPOINT ["/run.sh"]
run.shはWORKDIRで実行されるためbundle execでSinatraを起動します。最近のパッケージでは**--host**オプションを指定しないと、localhostにbindされて、外部からアクセスできなくなるので必ず外部から接続可能なIPを指定してください。
...
#!/bin/bash +x
bundle exec rackup --host 0.0.0.0 -p "${SINATRA_PORT}"
--hostオプションを指定しなかった場合はポートに接続してもDockerコンテナまでRoutingされないため接続がリセットされて、次のようなメッセージが表示されてしまいます。
$ curl http://localhost/
curl: (56) Recv failure: Connection reset by peer
作業ワークフローの設定
古い手ですがMakefileを準備して、作業を自動化しています。
下記のMakefileはopenapi.yamlファイルと同じレベルに配置しているものです。
.PHONY: gen-docs gen-code validate run install-validator
gen-docs:
openapi-generator-cli generate -g html -o docs -i openapi.yaml
gen-code:
openapi-generator-cli generate -g ruby-sinatra -o code -i openapi.yaml
validate:
/home/yasu/.local/bin/openapi-spec-validator openapi.yaml
run:
(cd code; bundle exec rackup --host 0.0.0.0 -p 8080)
install-validator:
pip3 install openapi-sepc-validator --user
さらに生成されたcode/の直下にMakefileを配置して、次のようなタスクを指定します。
NAME = mywebapi
DOCKER_IMAGE = mywebapi
DOCKER_IMAGE_VERSION = 1.0.0
IMAGE_NAME = $(DOCKER_IMAGE)
REGISTRY_SERVER = harbor.example.com
REGISTRY_LIBRARY = mylib
PORT = 8080
.PHONY: run bundle-install docker-build docker-build-prod docker-tag docker-push docker-build docker-run docker-stop
bundle-install:
rm -rf lib/ruby
bundle config set path lib
bundle install
patch -p0 < tmail.diff
docker-build:
rm -f Gemfile.lock
rm -fr lib/ruby
rm -fr .bundle
sudo docker build . --tag $(IMAGE_NAME)
docker-build-prod:
rm -f Gemfile.lock
rm -fr lib/ruby
rm -fr .bundle
sudo docker build . --tag $(IMAGE_NAME):$(DOCKER_IMAGE_VERSION) --no-cache
docker-tag:
sudo docker tag $(IMAGE_NAME):$(DOCKER_IMAGE_VERSION) $(REGISTRY_SERVER)/$(REGISTRY_LIBRARY)/$(IMAGE_NAME):$(DOCKER_IMAGE_VERSION)
docker-push:
sudo docker push $(REGISTRY_SERVER)/$(REGISTRY_LIBRARY)/$(IMAGE_NAME):$(DOCKER_IMAGE_VERSION)
docker-run:
sudo docker run -it --rm -d \
--env LC_CTYPE=ja_JP.UTF-8 \
-p $(PORT):$(PORT) \
--name $(NAME) \
$(IMAGE_NAME)
docker-stop:
sudo docker stop $(NAME)
Content-Typeの変更方法
Ruby/Sinatraは"application/json"は返すことができますが、設定しない場合のデフォルトのContent-Typeは"text/html"です。また"application/yaml"などの比較的新しいmime-typeには対応していません。
【準備作業】"application/yaml"を返すことができるように設定する
ここではmy_app.rbを次のように変更しています。
require './lib/openapiing'
# only need to extend if you want special configuration!
class MyApp < OpenAPIing
self.configure do |config|
config.api_version = '0.3.1'
end
self.mime_type :yaml, "application/yaml"
end
# include the api files
Dir["./api/*.rb"].each { |file|
require file
}
設定した:yamlシンボルは、api/ディレクトリに配置されているOpenAPIを実装するコードの中で使用します。
MyApp.add_route('GET', '/...', {
}) do
cross_origin
response = {}
...
content_type :yaml
return response.to_yaml
end
JSON形式で返したい場合には my_app.rb への変更は不要で、content_type :json
を指定するだけで、"application/json"がContent-Typeに設定されます。
さいごに
Ruby 3.0.0からwebrickが標準ライブラリから削除されたため、Dockerコンテナの元をruby:3.0-alpineにした際にRackに対応したWeb serverをGemfileに追加する必要がありました。このため標準ライブラリのWeb serverを前提としていたopenapi-generator-cliが生成するスケルトンコードはそのままではビルドできなくなってしまいました。
そのため(コンパイラを必要としないpure rubyな)webrickをGemfileに追加するよう、Github上でPull Requestを出し、現在では修正コードが次期リリース候補にマージされています。#9299
openapi-generatorはSwagger社からみると非公式の活動ですが、誕生の原因は仕様策定を牽引していた企業がツールの開発ポリシーを変更したことに起因します。そのために公式ツールのコントリビューターだったメンバーが現在のプロジェクトを立ち上げ、引き続き精力的に開発を続けています。
eBook "REST API のためのコード生成入門 (OpenAPI Generator)" を購入したり、openapi-generator-cliを実行した時に表示されるOpen Collectiveのページで寄附をするなど、コードの改善に貢献する以外の方法でも支援することができますので、良いツールだと感じたら、何等かの方法で支援することも検討してください。
以上