こんにちは、インダストリー事業本部 医療チームの高橋(@yukey1031)です。 去年のクリスマス以来の投稿です。AMIAS(アミアス)をお願いします。
さて、今回は自身の周辺業務で普段行っているOpenAPIによるスキーマファースト開発の実施サンプルと先日Google Cloudより発表された「Cloud Run」について少し触れてみたいと思います。
なお、フロントエンドはVue.js+TypeScript+Axios、バックエンドはJava+SpringBootでの話です。
マイクロサービスな現場でのAPI開発
以前の記事でも触れているようにオプティムが提供するAI・IoTプラットフォーム Cloud IoT OSは、マイクロサービスアーキテクチャで構成されており、周辺エコシステムの開発においてもマイクロサービスが前提となります。
マイクロサービスの連携には一般的にWebAPIが利用され、RESTful API、GraphQL、gRPC等のテクノロジーが用途に応じて採用されます。
最近では通信効率が高く、双方向通信、StreamingをサポートするgRPCの採用が増えつつありますが、ブラウザクライアントによるフロントエンド/バックエンドサーバー間通信では、以下のようなブラウザ側の制限やキャッシュ機構等の理由から、今でもほとんどのWeb APIとしてRESTful APIが採用されています。
スキーマファースト開発
RESTful APIでは、GraphQLやgRPCと違い事前にインターフェース定義不要で開発を進めることが可能です。そのような開発を進めていますと、フロントエンド/バックエンドの結合が困難であったりドキュメントと実装との乖離等、危険で効率が悪い状況に陥ることがあります。
スキーマファースト開発とは、APIのインターフェース定義を事前に取り決め、その定義からコードが生成されることを前提に開発を行うアプローチです。 それによりサーバーサイドの開発を待たずにフロントエンドの開発が進められることやインターフェース定義と実装の乖離を無くすことが可能となり、全体的な開発効率が向上する見込みがあります。
OpenAPI
RESTful APIのインターフェイス定義となるとOpenAPIが台頭しています。 OpenAPIは、RESTful APIのインターフェイス定義の記述フォーマットを仕様化したものです。 当該記述フォーマットはOpenAPI Speficificationとして策定されており、現在のバージョンは 3.0.2です。(2019年4月 現在)
OpenAPI Speficificationでは、次のようなAPIのインターフェース定義をYAMLまたはJSONで記述できます。
- 利用可能なエンドポイント(
/users
)と各エンドポイントでの操作(GET /users
、POST /users
) - 操作パラメーター操作ごとの入出力
- 認証方法
- 連絡先、ライセンス、利用規約、その他の情報
尚、周辺の文脈でSwaggerというワードが確認されますが、OpenAPIはSmatrBear Software社が開発したSwaggerという仕様がベースになっていることが起因しています。
現在のOpenAPIは、RESTful APIの記述標準化を目指す団体であるOpen API Initiativeによって推進されてるオープンな規格ですが、Open API Initiative発足時にSmatrBear Software社からSwagger2.0が寄贈されたことで、名前がSwaggerからOpen APIに変わったという経緯があります。また、メジャーバージョンを明示するためにOpenAPI 3といった記載が行われることがありますが、本記事ではOpenAPIの記載で統一しています。
OpenAPI Generator
OpenAPI Generatorは、OpenAPI Speficificationの記述フォーマットで記載されたインターフェース定義からAPIクライアントライブラリ、サーバースタブ、ドキュメントを自動生成することを可能にします。 現在、以下の言語/フレームワークがサポートされています。
Languages/Frameworks | |
---|---|
API clients | ActionScript, Ada, Apex, Bash, C, C# (.net 2.0, 3.5 or later), C++ (cpprest, Qt5, Tizen), Clojure, Dart (1.x, 2.x), Elixir, Elm, Eiffel, Erlang, Go, Groovy, Haskell (http-client, Servant), Java (Jersey1.x, Jersey2.x, OkHttp, Retrofit1.x, Retrofit2.x, Feign, RestTemplate, RESTEasy, Vertx, Google API Client Library for Java, Rest-assured, Spring 5 Web Client), Kotlin, Lua, Node.js (ES5, ES6, AngularJS with Google Closure Compiler annotations, Flow types) Objective-C, Perl, PHP, PowerShell, Python, R, Ruby, Rust (rust, rust-server), Scala (akka, http4s, scalaz, swagger-async-httpclient), Swift (2.x, 3.x, 4.x), Typescript (AngularJS, Angular (2.x - 7.x), Aurelia, Axios, Fetch, Inversify, jQuery, Node, Rxjs) |
Server stubs | Ada, C# (ASP.NET Core, NancyFx), C++ (Pistache, Restbed), Erlang, Go (net/http, Gin), Haskell (Servant), Java (MSF4J, Spring, Undertow, JAX-RS: CDI, CXF, Inflector, RestEasy, Play Framework, PKMST), Kotlin (Spring Boot, Ktor), PHP (Laravel, Lumen, Slim, Silex, Symfony, Zend Expressive), Python (Flask), NodeJS, Ruby (Sinatra, Rails5), Rust (rust-server), Scala (Finch, Lagom, Play, Scalatra) |
API documentation generators | HTML, Confluence Wiki |
Configuration files | Apache2 |
Others | GraphQL, JMeter, MySQL Schema |
本記事では、Server StubとしてSpring、API ClientとしてTypeScript(Axios)を利用した実施サンプルを体験します。
Cloud Run
今までの文脈から少し外れますが、今回の実施サンプルの中では先日Google Cloudより発表された KnativeベースなDockerコンテナのサーバレス実行環境である「Cloud Run」にサーバーサイドアプリのデプロイを体験します。
実施サンプル
Requirement
ローカル環境に以下がインストールされていることを確認してください。
- Jdk 1.8
- Maven 3.6.0
- Node >= v6.5.0
- yarn >= 1.13.0
- Vue CLI >= v3.5.0
- Google Cloud SDK >= 241.0.0 (+beta components)
- Git, Docker
今回用意したサンプルコードをcloneします。
$ git clone https://github.com/optim-corp/axios-sample.git $ cd axios-sample $ ls -l ├ axios-server/ ├ openapi/ ├ Dockerfile └ Readme.md
インターフェイス定義
以下のOpenAPI Specificationで記載されたインターフェース定義(petstore.yaml)を
/openapi/petstore.yaml
に保存します。今回の文脈ではあまり重要では無いので言及しませんが、インターフェイス定義の編集やドキュメント生成には以下のようなツールを活用します。
- Swagger UI (https://swagger.io/swagger-ui/)
- Swagger Edditor (http://editor.swagger.io/)
サーバーサイド実装
- 先程のインターフェイス定義からサーバーサイドのインターフェースを自動生成します。
$ cd axios-server $ mvn clean package [INFO] Scanning for projects... ・ ・ [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 15.307 s [INFO] Finished at: 2019-04-16T18:49:54+09:00 [INFO] ------------------------------------------------------------------------
- pom.xmlのbuildセクションを確認すると分かると思いますが、openapi-generator-maven-pluginにより、インターフェースを自動生成しています。生成されたコードは
target/generated-sources/openapi/src/main/java/com/example
に出力され、build-helper-maven-pluginにより当該ディレクトリを織り込んでいます。
- 自動生成は、インターフェースに留めているので
src/main/java/com/example/web/api/PetsApiController.java
にPetsApiインターフェースの実装を行います。今回は、適当なMockコードを用意していますので当該コードをそのまま利用してください。 - 続いてローカル環境でサーバーサイドアプリを起動し、APIがレスポンスされるか確認します。
$ mvn spring-boot:run & ・・・ 2019-04-16 19:09:57.106 INFO 10988 --- [ main] com.example.Application : Started Application in 5.127 seconds (JVM running for 17.726) $ curl -H 'Content-Type:application/json' http://localhost:8080/v1/pets [{"id":1,"name":"name1","tag":"1555409617414"},{"id":2,"name":"name2","tag":"1555409617414"},{"id":3,"name":"name3","tag":"1555409617414"}]
- 適当なJsonがレスポンスされればOKです👍
- Dockerfileも用意しているので、同様に確認します。
$ cd ../ $ docker image build -t axios-server . Sending build context to Docker daemon 164.4kB ・・・ Successfully built 44ef9fb421f8 Successfully tagged axios-server:latest $ docker container run -d -p 8080:8080 -e "JAVA_OPS=-Xmx512m" --name axios-server axios-server $ curl -H 'Content-Type:application/json' http://localhost:8080/v1/pets [{"id":1,"name":"name1","tag":"1555409617414"},{"id":2,"name":"name2","tag":"1555409617414"},{"id":3,"name":"name3","tag":"1555409617414"}]
- 適当なJsonがレスポンスされればOKです👍
フロントエンド実装
- 続いてフロントエンドの実装をvue-cliから進めていきます。
$ vue create axios-front ? Please pick a preset: Manually select features ? Check the features needed for your project: Babel, TS, Linter ? Use class-style component syntax? Yes ? Use Babel alongside TypeScript for auto-detected polyfills? Yes ? Pick a linter / formatter config: TSLint ? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save ? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files ? Save this as a preset for future projects? (y/N) y 🎉 Successfully created project axios-front. 👉 Get started with the following commands: $ cd axios-front $ yarn serve $ cd axios-front
- HTTPクライアントとしてAxiosを利用しているのでdependenciesに追加します。
$ yarn add axios yarn add v1.13.0 ・・・ └─ axios@0.18.0 Done in 8.81s.
- 続いて、先程のインターフェイス定義からAPI Clientコードを自動生成します。CLIツールがdockerイメージで用意されているので、それを利用します。
$ cd ../ $ docker run --rm -v ${PWD}/openapi:/local -v ${PWD}/axios-front:/axios-front openapitools/openapi-generator-cli generate -i /local/petstore.yaml -g typescript-axios -o /axios-front/src/axios/petstore $ cd axios-front $ ls src/axios/petstore ├ api.ts ├ configuration.ts ├ custom.d.ts ├ index.ts └ git_push.sh
- 画面から適当なリクエストを行うための実装を行います。尚、通常はAtomic Design ベースのコンポーネント設計で実装が行われていますが、今回はサンプルとなりますので最小限の実装で実現する対応を行っています。
$ vi src/components/PetStore.vue + <template> + <div class="petstore"> + <button v-on:click="showPets()">Show Pets</button> + <p>{{ pets }}</p> + </div> + </template> + + <script lang="ts"> + import { Component, Prop, Vue } from 'vue-property-decorator'; + import { Pet, PetsApi, Configuration } from '@/axios/petstore'; + + @Component + export default class PetStore extends Vue { + private pets: Pet[]; + private petsApi: PetsApi; + + constructor(init?: Partial<Pet>) { + super(); + this.pets = []; + this.petsApi = new PetsApi({ + basePath: process.env.VUE_APP_API_BASE_PATH, + }); + } + + public showPets() { + this.listPets(); + } + + private async listPets() { + const response = await this.petsApi.listPets(); + this.pets = response.data; + } + + } + </script> $ vi src/App.vue - <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/> + <PetStore/> - import HelloWorld from './components/HelloWorld.vue'; + import PetStore from './components/PetStore.vue'; - HelloWorld, + PetStore,
- フロントエンドアプリを起動し、ブラウザの「Show Pets」からレスポンスを確認します。尚、ローカル環境でサーバーサイドアプリが8080ポートで起動していることを前提とし、環境変数
VUE_APP_API_BASE_PATH
にベースURLを事前に設定します。
$ export VUE_APP_API_BASE_PATH=http://localhost:8080/v1 $ yarn run serve --port 8082 yarn run v1.13.0 ・・・ Note that the development build is not optimized. To create a production build, run yarn build.
- 適当なJsonが表示出来ればOKです👍
Cloud Runにデプロイ/接続
- スキーマファーストな開発により、フロントエンド開発チームはフロントエンド開発に集中する事が出来ます。Mockとしてバックエンド側のAPIが必要な場合、上記のようにそれぞれのローカル環境で起動することも可能ですが、それすらも許されない場合はバックエンドのビルドパイプラインの中で社内のDocker環境にアプリをデプロイすることが考えられます。しかしながら社内でDocker環境を運用するのはそれなりに骨が折れるので、サーバーレスに運用するユースケースとしてCloud Runにデプロイします。
- ということで、先程のサーバーサイドアプリをCloud Runにデプロイしてみます。
$ ls Dockerfile Dockerfile $ gcloud components update $ gcloud components install beta $ gcloud config set run/region us-central1 $ PROJECT_ID=[YOURE-PROJECT-ID] $ gcloud builds submit --project ${PROJECT_ID} --tag gcr.io/${PROJECT_ID}/axios-server $ gcloud beta run deploy --project ${PROJECT_ID} --memory 512Mi --image gcr.io/${PROJECT_ID}/axios-server ・・・ Service [axios-server] revision [axios-server-00003] has been deployed and is serving traffic at https://axios-server-xxxxxxxxxx-uc.a.run.app
- 環境変数
VUE_APP_API_BASE_PATH
にCloud Runデプロイ時に取得したURLを設定し、フロントエンドアプリを起動し、ブラウザの「Show Pets」からレスポンスを表示出来ればOKです👍
$ cd axios-front $ export VUE_APP_API_BASE_PATH=https://axios-server-xxxxxxxxxx-uc.a.run.app/v1 $ yarn run serve --port 8082 yarn run v1.13.0 ・・・ Note that the development build is not optimized. To create a production build, run yarn build.
おわりに
今回は、OpenAPI, Vue.js, TypeScript, Axios, SpringBoot, Cloud Runといったスタックについて実施サンプルを交えて説明しました。(Cloud Runは、触ってみたかった要素が強いです。) 本記事のアウトプットをベースにパッケージ構成やブランチ戦略、ビルドパイプライン等を工夫して更なる開発効率化に取り組んでもらえればと考えています。
本テックブログでは、RustやDeep Learningな記事が盛り上がりがちですが、Web系やバックエンド技術についても、しっかりと取り組んでおりオプティムの躍進を技術で下支えしています。 そんな現場に興味のあるフロントエンドエンジニア/バックエンドエンジニアの方々は、是非こちらをご覧ください。