Swagger/OpenAPIの定義ファイルからCircleCIでスタブサーバーを自動生成して開発を高速化する

この記事は [Voicy Advent Calendar 2020](https://qiita.com/advent-calendar/2020/voicy) 1日目の記事です。

初めまして、Voicyのサーバーサイドエンジニアをやっているカワムラです。

今回は普段の開発でもよくやっているswagger/OpenAPI(以下swagger)のファイルからスタブサーバーを自動生成して開発を効率化させる方法をご紹介したいと思います。

## 背景
新しくHTTPのAPIサーバーを新規開発する際、Swaggerを使用する場合の流れは以下のようなものが一般的かと思います。

1.バックエンド・フロントで相談しながらSwaggerでAPIの仕様を定義する
　　　　　　　　　　　　　↓
2.バックエンド側がエンドポイントを一つずつ開発
　　　　　　　　　　　　　↓
3.バックエンドの開発が終わったエンドポイントに合わせてフロントが開発

しかし、この方法だと同時にフロントとバックエンド開発を始めることができないし、フロントの開発がバックエンドのエンドポイント開発の順序に左右されてしまうというデメリットがあります。

それによって開発が非効率になってしまったり、バックエンドの開発スピードが全体のボトルネックになってしまうなどの問題が生じてしまう可能性があります。

そこで、今回はswaggerでAPIの定義さえしてしまえば指定したレスポンスを返してくれるスタブサーバーのCircleCIを用いた自動生成方法をご紹介します。

## 導入に使用したライブラリ
今回使用したライブラリは以下になります。

swagger-codegen
https://github.com/swagger-api/swagger-codegen

api-spec-converter
https://github.com/LucyBot-Inc/api-spec-converter

## 導入手順
※アプリケーションはEKSで管理しておりGitOpsで運用している前提で、ビルドしたDockerイメージをECRにプッシュするまでの手順を紹介します。

### swaggerファイルを作成する
まず初めにAPIのSwaggerファイルを作成します。swagger/openapiのバージョンは2でも3でも問題はありません。今回はopenapiの3系を使いました。

```yaml

openapi: "3.0.0"
info:
  version: 1.0.0
  title: Swagger Petstore
  description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification
  termsOfService: http://swagger.io/terms/
  contact:
    name: Swagger API Team
    email: apiteam@swagger.io
    url: http://swagger.io
  license:
    name: Apache 2.0
    url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
  - url: http://petstore.swagger.io/api
paths:
  /pets:
    get:
      description: |
        Returns all pets from the system that the user has access to
        Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.
      operationId: findPets
      parameters:
        - name: tags
          in: query
          description: tags to filter by
          required: false
          style: form
          schema:
            type: array
            items:
              type: string
        - name: limit
          in: query
          description: maximum number of results to return
          required: false
          schema:
            type: integer
            format: int32
      responses:
        '200':
          description: pet response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Pet'
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      description: Creates a new pet in the store. Duplicates are allowed
      operationId: addPet
      requestBody:
        description: Pet to add to the store
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewPet'
      responses:
        '200':
          description: pet response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

......もっと続く
```

swaggerの定義に関して詳しく記載しませんが、スタブサーバーを作成した際に指定したレスポンスを返すようにするには、swaggerファイルに以下のように値を記載する必要があります。

```yaml
      responses:
        200:
          description: success
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Pet"
              example:
                - id: 1
                  name: 花子
                - id: 2
                  name: 太郎
                - id: 3
                  name: ぽち

```
 
ここのexampleに記載することでswaggerファイルからサーバーを生成した際に、レスポンスとして返すことが可能になります。

また、スキーマで定義した部分でもexampleを設定することによってレスポンスとして返すことが可能です。

```yaml

components:
  schemas:
    Pet:
      allOf:
        - $ref: '#/components/schemas/NewPet'
        - type: object
          required:
          - id
          properties:
            id:
              type: integer
              format: int64
              example: 1
            name:
              type: string
              example: シロ
```
### swaggerファイルからサーバーを自動生成する
ここからの作業は全てCircle CIで自動化してしまいます。以下が.circleci/config.yamlになります。
全体としてはこちらになりますが、下で部分ごとに分けて説明していきます。

```yaml
version: 2.1

orbs:
  node: circleci/node@3.0.0
  aws-ecr: circleci/aws-ecr@6.9.0

jobs:
  downgrade-swagger-config:
    executor: node/default
    steps:
      - checkout
      - run: sudo npm install -g api-spec-converter
      - run: api-spec-converter --from=openapi_3 --to=swagger_2 --syntax=yaml swagger/swagger.yaml > swagger2.yaml
      - persist_to_workspace:
          root: .
          paths:
            - .
  create-stub-server:
    build:
    machine: true
    steps:
      - attach_workspace:
          at: .
      - checkout
      - run: docker run --rm -v ${PWD}:/local swaggerapi/swagger-codegen-cli generate -i /local/swagger2.yaml -l nodejs-server -o /local
      - persist_to_workspace:
          root: .
          paths:
            - .
  deploy:
    executor: aws-ecr/default
    steps:
      - attach_workspace:
          at: .
      - aws-ecr/build-and-push-image:
          dockerfile: build/stub.Dockerfile
          repo: stub-sample-api
          tag: "${CIRCLE_SHA1}"

workflows:
  version: 2
  deploy-stub-cluster:
    jobs:
      - downgrade-swagger-config:
          filters:
            branches:
              only: dev
      - create-stub-server:
          requires:
            - downgrade-swagger-config
      - deploy:
          name: Deploy Stub to DEV Image
          context: dev
          requires:
            - create-stub-server

```

```yaml:orbs
orbs:
  node: circleci/node@3.0.0
  aws-ecr: circleci/aws-ecr@6.9.0

```

ここでは、今回使用するOrbを定義しています。
OrbとはCircleCIのcommandsやjobs,executors をパッケージとして使い回すことのできる仕組みです。
詳しくは[こちら]
(https://circleci.com/docs/ja/2.0/orb-intro/)

```yaml:jobs

jobs:
  downgrade-swagger-config:
    executor: node/default
    steps:
      - checkout
      - run: sudo npm install -g api-spec-converter
      - run: api-spec-converter --from=openapi_3 --to=swagger_2 --syntax=yaml swagger/swagger.yaml > swagger2.yaml
      - persist_to_workspace:
          root: .
          paths:
            - .
  create-stub-server:
    build:
    machine: true
    steps:
      - attach_workspace:
          at: .
      - checkout
      - run: docker run --rm -v ${PWD}:/local swaggerapi/swagger-codegen-cli generate -i /local/swagger2.yaml -l nodejs-server -o /local
      - persist_to_workspace:
          root: .
          paths:
            - .
  deploy:
    executor: aws-ecr/default
    steps:
      - attach_workspace:
          at: .
      - aws-ecr/build-and-push-image:
          dockerfile: build/stub.Dockerfile
          repo: stub-sample-api
          tag: "${CIRCLE_SHA1}"

```

ここでは3つのJobを定義しています。

まず1つ目の downgrade-swagger-configは、定義したOpenAPI3.0の定義ファイルをswagger2.0にダウングレードさせています。理由としてはOpenAPI3.0からスタブサーバーを生成した場合に、exampleで指定した値がレスポンスとして返されないという仕様だったため、一旦2.0にダウングレードする手順を挟みました。したがって最初からswagger2.0で定義されている場合は必要ありません。

余談ですが、こちらのツールはswagger2.0→openAPI3.0への変換などもコマンドで簡単に実行できるのでおすすめです。
https://github.com/LucyBot-Inc/api-spec-converter

2つ目のcreate-stub-serverは実際にswagger-codegen-cliを用いてサーバーを自動生成する部分です。
ここではDockerHubに上がっているswagger-codegenのイメージを用いて生成しています。
https://hub.docker.com/r/swaggerapi/swagger-codegen-cli

-lで様々な言語でのサーバーの生成が可能なのですが、今回はnodejsを用いています。（普段はGo言語を用いているがGo言語で自動生成されるサーバーの精度が低かったため）

このツールを使って自動生成できるサーバーの言語は以下です。たくさんありますが、言語ごとに生成されるサーバーの精度が異なるため、注意してご利用ください。

Ada, C# (ASP.NET Core, NancyFx), C++ (Pistache, Restbed), Erlang, Go, Haskell (Servant), Java (MSF4J, Spring, Undertow, JAX-RS: CDI, CXF, Inflector, RestEasy, Play Framework, PKMST), Kotlin, PHP (Lumen, Slim, Silex, Symfony, Zend Expressive), Python (Flask), NodeJS, Ruby (Sinatra, Rails5), Rust (rust-server), Scala (Finch, Lagom, Scalatra)


そして最後がdeployです。先ほど生成したサーバーをDockerで実行して、そのImageをECRにデプロイします。orbsを用いることで非常に簡素にECRデプロイを書くことが可能です。

また、Dockerfileに関しては以下のように簡潔なものになっています。

```dockerfile:stub
FROM node:12

WORKDIR /app
ADD . /app

RUN npm install
RUN npm audit fix

COPY . .

EXPOSE 8080
CMD [ "node", "index.js" ]

```

```yaml

workflows:
  version: 2
  deploy-stub-cluster:
    jobs:
      - downgrade-swagger-config:
          filters:
            branches:
              only: dev
      - create-stub-server:
          requires:
            - downgrade-swagger-config
      - deploy:
          name: Deploy Stub to DEV Image
          context: dev
          requires:
            - create-stub-server

```

workflowに関しては先ほど記載したjobを上から順に行っている形で定義しています。

これによってSwaggerを定義→devブランチにマージをすることでサーバーが自動生成されるため、APIの定義さえしてしまえばバックエンド・フロントで並行開発がよりスムーズなるかと思います！

以上がSwaggerの設定ファイルからスタブサーバーを自動生成する方法になります。
最後までお読みいただきありがとうございました！！！