「API開発 + Swagger UIを利用したAPI検証」な環境をDockerで構築する

バックエンド

Swaggerを利用することでREST APIの仕様をドキュメント化できます。
SwaggerではREST APIの仕様をドキュメントしたファイルをSwagger Specと呼びます。

Swagger UIとはSwagger Specの情報を反映させた静的ページを生成するツールのことを言います。
Swagger UIはSwagger Specの情報を可視化するだけでなく、画面上からREST APIを実行する機能も提供しています。

今回は開発中のAPIをSwagger Specでドキュメント化 → Swagger Specの情報が反映されたSwagger UIの画面からAPIリクエストの検証という一連の作業が行えるDocker環境の構築手順について紹介します。

今回作成するDocker環境について

今回作成するDocker環境の仕様
  • docker-compose upだけで環境が準備できる
  • 「/swagger-ui」でSwagger UIの画面が表示される
  • 「/swagger-ui」以外はAPI用のエンドポイントとする
  • Swagger Spec編集後、リロードでSwagger UIに変更内容が反映される
  • APIはRuby on RailsのAPIモードで作成する
  • DBはMySQLを利用

nginxをリバースプロキシとして利用することでAPIの開発環境とSwagger UIを組み合わせます。
図で表現すると以下のようになります。

今回の利用する各種バージョンは以下の通りです。

バージョン一覧
  • Ruby on Rails: 6.0.3.2
  • Ruby: 2.7.1
  • MySQL: 8.0.21
  • nginx: 1.19.3

API開発環境をDockerに作成する

APIモードで作成する『Rails 6 x MySQL 8』Docker環境構築手順を参考に、RailsのAPIモードを利用してAPI開発環境を作成します。

APIモードで作成する『Rails 6 x MySQL 8』Docker環境構築手順

Dockerfileとdocker-compose.ymlは以下の通りです。

Dockerfile

FROM ruby:2.7.1

# 作業ディレクトリを/rails_api_swaggerに指定
WORKDIR /rails_api_swagger

# ローカルのGemfileをDokcerにコピー
COPY Gemfile* /rails_api_swagger/

# /rails_api_swaggerディレクトリ上でbundle install
RUN bundle install

docker-compose.yml

version: '3'
services:
  api: # Ruby on Railsが起動するコンテナ
    build: .
    ports:
      - '3000:3000' # localhostの3000ポートでアクセスできるようにする
    volumes:
      - .:/rails_api_swagger # アプリケーションファイルの同期
    depends_on:
      - db
    command: ["./wait-for-it.sh", "db:3306", "--", "./start.sh"]
  db: # MySQLが起動するコンテナ
    image: mysql:8.0.21
    volumes:
      - mysql_data:/var/lib/mysql # データの永続化
      - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    command: --default-authentication-plugin=mysql_native_password # 認証方式を8系以前のものにする。
    environment:
      MYSQL_USER: 'webuser'
      MYSQL_PASSWORD: 'webpass'
      MYSQL_ROOT_PASSWORD: 'pass'
      MYSQL_DATABASE: 'rails_api_swagger_development'
volumes:
  mysql_data: # データボリュームの登録
RailsのAPIモードでは静的ページを生成する機能は除外されているため、Swagger UIを直接Railsアプリケーションに組み込むことはできませんが、今回の方法を利用すればAPIモードでもSwagger UIを利用できます。

サンプルとなるAPIを作成します。

# コンテナをバックグランドで起動
$ docker-compose up -d

# Eventを操作する機能(モデル、ビュー、コントローラー)を一括作成
$ docker-compose exec api rails g scaffold event title:string

# eventsテーブルを作成
$ docker-compose exec api rails db:migrate

# rails consoleでeventsのレコードを作成
$ docker-compose exec api rails c
> event = Event.new(title: 'サンプルイベント')
> event.save

localhost:3000/eventsにアクセスして以下のようなレスポンスが返ってくればOKです。

リバースプロキシの設定を行い、nginx経由でAPIにアクセスできるようにする

nginx経由でRailsアプリケーションにアクセスできるようリバースプロキシの設定を行ます。

default.conf

server {
  listen 80;
  server_name  localhost;

  # "/"にアクセスがあったときの処理
  location / {
    proxy_set_header Host localhost; # アクセス元のホストをlocalhostにする
    proxy_pass http://api:3000; # apiコンテナの3000ポートにリクエストを送る
  }
}

nginxの設定ファイルは/etc/nginx/nginx.confです。
設定ファイルにinclude /etc/nginx/conf.d/*.conf;という記述があることからも分かるように、設定ファイルでは/etc/nginx/conf.d配下の.confという拡張子の設定も読み込んでいます。

つまり、/etc/nginx/conf.d配下に今回作成した設定ファイルを配置することで、nginxコンテナをリバースプロキシとして利用できます。

docker-compose.ymlにnginxコンテナを追加します。

docker-compose.yml

services:
  api:
    (略)
  db:
    (略)
  nginx:
    image: nginx:1.19.3
    ports:
      - '80:80'
    command: [nginx-debug, '-g', 'daemon off;']
    volumes:
      - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - api
volumes:
  mysql_data:

[nginx-debug, '-g', 'daemon off;']はデバッグモードによる起動方法です。1

なお、swagger-uiのDockerイメージもnginxを利用していますが、nginx.confinclude /etc/nginx/conf.d/*.conf;の記述がありません。
ですので、swagger-uiのDockerイメージを利用する場合だと今回のアプローチはうまくいかないので注意してください。

コンテナ起動後、localhost:80/eventsにアクセスして以下のようなレスポンスが返ってくればOKです。(localhost:3000/eventsの結果と同じです。)

Swagger UIをnginxに組み込む

/swagger-uiにアクセスをしたらSwagger UIの画面が表示されるようにnginxにSwagger UIを組み込んでいきます。

Swagger UIの画面はswagger-ui/distによって構成されています。

dist配下のファイルをローカルにコピーし、nginxコンテナにバインドマウントすることで、Swagger UIをnginxに組み込みます。

dist配下のファイルをすべてコピーしてきてもよいのですが、unpkgを利用することでindex.htmlのみをコピーするだけでSwagger UIの画面が作成できます。 2

index.html

<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Swagger UI</title>
-   <link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
+   <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3/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>
  </head>

  <body>
    <div id="swagger-ui"></div>

-   <script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
+   <script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js" charset="UTF-8"> </script>
-   <script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
+   <script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-standalone-preset.js" charset="UTF-8"> </script>

    <script>
    window.onload = function() {
      // Begin Swagger UI call region
      const ui = SwaggerUIBundle({
        url: "https://petstore.swagger.io/v2/swagger.json",
        dom_id: '#swagger-ui',
        deepLinking: true,
        presets: [
          SwaggerUIBundle.presets.apis,
          SwaggerUIStandalonePreset
        ],
        plugins: [
          SwaggerUIBundle.plugins.DownloadUrl
        ],
        layout: "StandaloneLayout"
      })
      // End Swagger UI call region

      window.ui = ui
    }
  </script>
  </body>
</html>

docker-compose.ymlを修正し、作成したindex.htmlをnginxのデフォルトの公開ディレクトリである/usr/share/nginx/html配下に配置します。

docker-compose.yml

services:
  api:
    (略)
  db:
    (略)
  nginx:
    image: nginx:1.19.3
    ports:
      - '80:80'
    command: [nginx-debug, '-g', 'daemon off;']
    volumes:
      - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf
+     - ./nginx/html/swagger-ui:/usr/share/nginx/html/swagger-ui
    depends_on:
      - api
volumes:
  mysql_data:

/swagger-uiにアクセスしたらindex.htmlが表示されるようnginxの設定を追記します。

server {
  listen 80;
  server_name  localhost;

  location / {
    proxy_set_header Host localhost;
    proxy_pass http://api:3000;
  }

  # "swagger-ui"にアクセスがあったときの処理
  location /swagger-ui {
    alias /usr/share/nginx/html/swagger-ui;
  }
}

コンテナ起動後、localhost:80/swagger-uiにアクセスして以下のような画面が表示されればOKです。

ローカルのSwagger SpecがSwagger UI上に反映されるようにする

Swagger UIのurlを変更することで参照するSwagger Specを変更できます。

ローカルのSwagger Specを参照するように変更します。

index.html

- url: "https://petstore.swagger.io/v2/swagger.json",
+ url: "./api.yml",

上記の変更でローカル環境に配置された./nginx/html/swagger-ui/api.ymlの内容がSwagger UIへ反映されます。

なお、./nginx/html/swagger-ui/ディレクトリはバインドマウントされているので、ローカルでSwagger Spec編集後、リロードすればコンテナのSwagger UIに変更内容が反映されます。

サンプルとして作成したGET /eventsを実行するSwagger Specは以下の通りです。

api.yml

openapi: 3.0.2
info:
  title: サンプルAPI
  version: 1.0.0
servers:
  - url: http://localhost:3000
tags:
  - name: イベント
paths:
  /events:
    get:
      tags:
        - イベント
      description: イベント一覧取得
      responses:
        200:
          description: 成功
          content:
            application/json:
              schema:
                type: array
                description: イベントの配列
                items:
                  $ref: "#/components/schemas/Event"
components:
  schemas:
    Event:
      type: object
      properties:
        id:
          description: ID
          type: integer
          format: int64
          example: 1
        title:
          description: タイトル
          type: string
          example: サンプルイベント
        created_at:
          description: 作成日
          type: string
          format: date-time
          example: 2020-04-01 10:00
        updated_at:
          description: 更新日
          type: string
          format: date-time
          example: 2020-04-01 10:00

コンテナ起動後、以下のような画面が表示されればOKです。

CORSの設定をする

Swagger UIはlocalhost:80、APIはlocalhost:3000で起動しています。
この状態でSwagger UIからAPIにリクエストを送るとオリジンをまたがっているためAccess to fetch at 'http://localhost:3000/events' from origin 'http://localhost' has been blocked by CORS policyというエラーが発生します。

クロスオリジン(オリジンをまたぐこと)であることはAPIのエンドポイント(controller)のrequest.original_urlrequest.headers[:origin]でオリジンが異なっていることからもわかります。

app/events_controller.rb

     5: def index
 =>  6:   binding.pry
     7:   @events = Event.all
     8:
     9:   render json: @events
    10: end

> request.original_url
=> "http://localhost:3001/events"

> request.headers[:origin]
=> "http://localhost"

CORSの設定を行い、Swagger UIからAPIリクエストが送れるようにします。
今回はrack-corsを利用してCORSの設定を行ます。

Gemfile

gem 'rack-cors'

config/initializers/cors.rb

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  unless Rails.env.production?
    allow do
      origins(['localhost', /localhost:\d+\Z/])

      resource '*',
        headers: :any,
        methods: [:get, :post, :put, :patch, :delete, :options, :head]
    end
  end
end

コンテナ起動後、リクエストが正常に返ってくればOKです。

参考: CRUD操作を行うSwagger Spec

サンプルAPI一覧
  • GET /events
  • POST /events
  • GET /events/{id}
  • PATCH /events/{id}
  • DELETE /events/{id}

上記のエンドポイントに関するSwagger Specは以下の通りです。

openapi: 3.0.2
info:
  title: サンプルAPI
  version: 1.0.0
servers:
  - url: http://localhost:3000
tags:
  - name: イベント
paths:
  /events:
    get:
      tags:
        - イベント
      description: イベント一覧取得
      responses:
        200:
          description: 成功
          content:
            application/json:
              schema:
                type: array
                description: イベントの配列
                items:
                  $ref: "#/components/schemas/Event"
    post:
      tags:
        - イベント
      description: イベント登録
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  example: サンプルイベント
      responses:
        201:
          description: 作成
  /events/{event_id}:
    get:
      tags:
        - イベント
      description: イベント詳細
      parameters:
        - name: event_id
          in: path
          description: イベントID
          required: true
          schema:
            type: integer
            format: int64
            example: 1
      responses:
        200:
          description: 成功
          content:
            application/json:
              schema:
                type: object
                $ref: "#/components/schemas/Event"
        404:
          description: event not found
    patch:
      tags:
        - イベント
      description: イベント更新
      parameters:
        - name: event_id
          in: path
          description: id
          required: true
          schema:
            type: integer
            format: int64
          example: 1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  example: サンプルイベント
      responses:
        200:
          description: 成功
          content:
            application/json:
              schema:
                type: object
                $ref: "#/components/schemas/Event"
    delete:
      tags:
        - イベント
      description: イベント削除
      parameters:
        - name: event_id
          in: path
          description: id
          required: true
          schema:
            type: integer
            format: int64
            example: 1
      responses:
        204:
          description: No Content
components:
  schemas:
    Event:
      type: object
      properties:
        id:
          description: ID
          type: integer
          format: int64
          example: 1
        title:
          description: タイトル
          type: string
          example: サンプルイベント
        created_at:
          description: 作成日
          type: string
          format: date-time
          example: 2020-04-01 10:00
        updated_at:
          description: 更新日
          type: string
          format: date-time
          example: 2020-04-01 10:00

画面は以下のようになります。

まとめ

以上でAPIとSwagger UIを統合した開発環境の構築手順の紹介を終わります。

今回のまとめ
  • nginxを利用することでSwagger UIとAPIを組み合わせる
  • nginxのリバースプロキシ設定は『/etc/nginx/conf.d』配下に作成
  • オリジンをまたがるリクエストをする際はCORSの設定が必要になる
  • 自作のSwagger SpecをSwagger UIに反映させるにはindex.htmlのurlを変更する

この記事がいいなと思いましたらTwitter(@nishina555)のフォローもよろしくお願いします!