Swagger、結局どう使うの？ を整理してみた

## 概要

Swaggerとは、REST APIの設計を楽にするよう作られたツールです。

とはいえ、作成したAPIサーバに対し、どのように使用するのか・メリットは何か、がいまいち整理できていなかったので、今更ながら実際に使用してみることで整理をしてみました。

API開発者目線の使用感を記載するため、Swagger部分だけでなく、ソースコード開発からAPI公開までの工程について構築手順を記載していきます。

## 想定読者

* Swaggerの使い方の一例を知りたい方

## 目次
* 全体構成図
* 構築方法
  * APIサーバ
  * Swagger
  * GitHub
  * API Gatewayへインポート
* 所感
* まとめ

## 全体構成図

今回は以下を使用して構成しました。
* APIサーバは、spring bootで開発しビルドしたjarファイルをECS上でコンテナとして構築した。
* API Gatewayを用いてAPIを公開した。
* APIドキュメントの公開はGitHub Pagesを用いた。(GitHubと連携しAPIドキュメントの公開を可能にすることを想定)

![全体構成図.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2420547/fb7dd54e-d8ce-c30e-c6de-9caf567a7c0d.png)

## 構築方法
### APIサーバ
#### Eclipse上で開発

Eclipse上で新規Springスタータープロジェクトを作成します。(今回は`demo`プロジェクトを作成しました。)

`src/main/java`内の`com.example.demo`パッケージに、RestAPIの処理内容を記述するjavaクラス`HelloController.java`を作成します。

```java
package com.example.demo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
	@RequestMapping(value="/")
	private String hello() {
		return "Hello world";
	}
```

※上記のコードを記載すると、import文に対し、赤線が引かれエラーが出ていると思います。必要なパッケージが取り込まれていないので、`pom.xml`ファイルに以下を追加し、Mavenを更新することが必要です。

```maven
# dependenciesタブ内に以下を追加
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
```

Mavenの更新方法：`Package Explorer`内の`demo`プロジェクトを右クリックし、`Maven`->`Update Project`を押下、その後のウィンドウでOKを押下する。

#### ローカルPC上でソースコードのテスト

`Package Explorer`内の`demo`プロジェクトを右クリックし、`Run As` -> `Spring Boot App`を押下します。正常にSpring Bootが起動したら、ブラウザで`http://localhost:8080`にアクセスします。`Hello World`と表示すれば期待通りです。

#### jarファイルの作成

`Package Explorer`内の`demo`プロジェクトを右クリックし、`Run As` -> `Maven Install`を押下すると、`target`フォルダ内にjarファイルが作成されます。

#### コンテナイメージの作成

`demo`プロジェクト直下に`Dockerfile`を作成します。中身は以下を記載します。

```Dockerfile
FROM openjdk:17-alpine
COPY target/demo-0.0.1-SNAPSHOT.jar /demo-0.0.1-SNAPSHOT.jar
ENTRYPOINT ["java","-jar","/demo-0.0.1-SNAPSHOT.jar"]
```

ローカル上でdockerコマンドを用いてコンテナイメージをビルドします。ローカル上でコンテナを構築してみて、期待通りコンテナイメージが作成できているか確認します。

```bash
# カレントディレクトリを`demo`プロジェクトのディレクトリに移動して、コンテナイメージをビルド
docker build -t demo .
# コンテナイメージが作成されていることを確認
docker images
# コンテナを構築する
docker run -d -p 8080:8080 demo
# 以下を実行し、期待通りであれば、`Hello World`が出力される。
curl http://localhost:8080
# コンテナを停止する
docker stop [demoコンテナのコンテナID]
```

#### ECS上にデプロイ

ECS上にコンテナをデプロイするため、以下のフローで構築します。

* ECRにコンテナイメージをpush
* ECS上にコンテナを構築
* コンテナとAPI GatewayをつなぐVPC linkを作成する
* APIを作成

AWS CLIを使って、ECRにレポジトリを作成し、前節で作成したコンテナイメージをpushします。

```bash
# aws_account_idを変数化
aws_account_id=[aws_account_idを入力]
# 認証する(aws configureの設定をしていない場合は設定します)
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${aws_account_id}.dkr.ecr.ap-northeast-1.amazonaws.com
# リポジトリを作成する
aws ecr create-repository \
    --repository-name demo \
    --image-scanning-configuration scanOnPush=true \
    --region ap-northeast-1
# コンテナイメージにタグ付けする
docker tag demo:latest ${aws_account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/demo:latest
# ECRにpushする
docker push ${aws_account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/demo:latest
```

次にECS上にコンテナを構築します。ECS周りの構成について、以下の図で整理しました。

①でどのコンテナイメージを使用するか、といったタスク定義を作成し、

②でどのタスク定義を使用するか・どのNLBを使用する、といったサービスを作成し、ECS上にコンテナが構築されるイメージです。

![ECS構成.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2420547/b63043a3-51cd-2817-4a87-a15ac633d33f.png)

以下のフローで構築していきます。
* クラスタの作成
  * [コンソール画面のECSサービス](https://ap-northeast-1.console.aws.amazon.com/ecs) からまずはクラスタを作成していきます。今回は簡単のためFargateで作成します。クラスタ名は任意、VPCを新しく作成しクラスタを作成します。
* NLBの作成
  * [コンソール画面のLBサービス](https://ap-northeast-1.console.aws.amazon.com/ec2) からNLBを作成していきます。作成時、以下を設定します。
    * VPCにはECSクラスタ作成時に作成されたものを設定、リスナーのpodにはTCPの8080を指定します。
    * ターゲットグループには、ポートにはTCPの8080を指定します。IPアドレスは一旦設定しません。コンテナへ転送していくよう設定するため、コンテナ構築後、プライベートIPアドレスを指定します。
* タスク定義の作成
  * コンテナイメージにECRにpushしたものを指定して、タスク定義を作成します。
* サービスの作成
  * 上で作成したNLBとタスク定義を指定して、サービスを作成します。作成後コンテナが構築されます。コンテナのプライベートIPアドレスを控えておいてください。
* ターゲットグループの設定
  * 先ほど作成したターゲットグループについて、ECS上に構築したコンテナのプライベートIPアドレスを追加し、NLBからコンテナへ転送されるよう設定します。

上記の構築で、NLBをフロントとしてコンテナへ通信する環境が構築できました。ただ、NLBはVPCにいるため、外部からは接続できません。

次に、作成したNLBとAPI GatewayをつなぐVPC linkを作成します。 [コンソール画面のAPI Gatewayサービス](https://ap-northeast-1.console.aws.amazon.com/apigateway/) から作成をしていきます。
REST APIを選択し、ターゲットNLBに先ほど作成したNLBを選択し作成します(作成完了に数分かかるようです)。作成されたVPC linkのIDを後で使用するので控えておいてください。

APIを作成します。といっても中身の設定はここではしません。(この後Swaggerを用いてAPI Gatewayにインポートする用のyamlファイルを作成します。)
[コンソール画面のAPI Gatewayサービス](https://ap-northeast-1.console.aws.amazon.com/apigateway/) から作成をしていきます。REST APIを選択し、任意の名前を指定して作成します。作成されたAPIのIDを後で使用するので控えておいてください。

### Swagger

API gatewayにインポートするためのyamlファイルをSwaggerを利用して作成します。

今回はSaaSのSwaggerを使用します。[Swagger Editor](https://editor.swagger.io/#/) にアクセスします。左側がEditor画面で右側がUI画面となります。左側にデフォルトで記載されているyamlファイルを全て消し、以下を記載します。

```yaml
swagger: "2.0"
info:
  version: "1.0.0"
  title: "Hello world"
host: "[APIのID].execute-api.ap-northeast-1.amazonaws.com"
basePath: "/demo"
schemes: ["https"]
paths:
  /:
    get:
      summary: "Hello worldを出力"
      description: "Hello worldを出力する。"
      responses:
        "200":
          description: "Success"
        "405":
          description: "Invalid input"
      x-amazon-apigateway-integration:
        type: "http"
        connectionId: "[VPC linkのID]"
        httpMethod: "GET"
        uri: "http://[NLBのホスト名]:8080"
        responses:
          default:
            statusCode: "200"
        passthroughBehavior: "when_no_match"
        connectionType: "VPC_LINK"
```

上記を記載すると、以下のように右側の画面でUIを確かめつつ、公開するAPIの情報を確認することができます。

![swagger_editor.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2420547/697db225-48aa-54eb-bf64-c5377146c8df.png)

### GitHub

Swaggerで作成したyamlファイルをAPIドキュメントとして公開するために、GitHubと連携し、ドキュメント公開ページを作成します。

yamlファイル格納用のGitHubリポジトリを作成します。(自分は`Swagger`という名前のリポジトリを作成しました。[こちら](https://github.com/IhiraShunya/Swagger) で公開しています。

次に、作成したリポジトリにファイルを追加していきます。
やることはざっくり以下です。
* Swagger Editorで作成したyamlファイルを格納する
* GitHub PagesでSwaggerUIの画面を出力できるよう、`dist`ディレクトリをcpする

```bash
# yamlファイル格納用GitHubリポジトリをcloneする
git clone [リポジトリURL]
# Swagger Editorで作成したyamlファイルをdocs/swagger.yamlに格納する
mkdir Swagger/docs
vi Swagger/docs/swagger.yaml
# Swagger-UIリポジトリをcloneする
git clone https://github.com/swagger-api/swagger-ui.git
# Swagger-UIリポジトリ内のdistディレクトリをcpする
cp -r swagger-ui/dist Swagger/docs/  
# distディレクトリ内のファイルを編集する
vi Swagger/docs/dist/swagger-initializer.js
# 星部分を下記のように変更する 
  window.ui = SwaggerUIBundle({
★    url: "../swagger.yaml",
    dom_id: '#swagger-ui',
# pushする
cd Swagger
git add docs
git commit -m "initial commit"
git push
```

リポジトリの作成は完了したので、GitHub Pagesの設定をします。作成したリポジトリの`Setting`を開き、`GitHub Pages`を押下し、`Source`の欄についてbranchには`main`,folderには`docs`を選択し`Save`を押下します。

正常に設定が完了していれば、以下にアクセスすればSwaggerUIの画面が表示されると思います。

https://[ユーザ名].github.io/Swagger/dist/

上記の設定により、`swagger.yaml`を変更してpushすると、同時にGitHub Pagesの内容も変更されるため、自動的にAPIドキュメントも更新することが可能となりました。

### API Gateway

前節で作成したyamlファイルをAPI Gatewayにインポートすることで、外部からアクセスできるAPIを作成します。

* yamlファイルのインポート
  * さきほど作成されたAPIを選択し、`アクション`->`APIのインポート`を押下します。前節で作成したyamlファイルを記載し`インポート`を押下すると、yamlファイルの内容がAPIに反映されます。
* APIのテスト
  * 作成された`GET`リソースを選択し、`テスト`を押下しテストができます。レスポンスが`200`で`Hello world`が出力されていると期待通りです。
* APIのデプロイ
  * `アクション`->`APIのデプロイ`を押下し、APIのデプロイをします。ステージ名は任意ですが、今回はdemoとしました。デプロイが完了して、以下にアクセスして`Hello world`が出力されることを確認してください。

https://[APIのID].execute-api.ap-northeast-1.amazonaws.com/demo

## 所感

* Swaggerを用いると、作成するAPIの設定ファイルがそのまま公開ドキュメントの情報として使えるので、ドキュメントと実態の乖離をなくせるところにメリットを感じました。
* 今回は手動で操作をしたため、例えば以下の操作について自動化を行い、業務効率化を図ると良いと思いました。
  * コンテナイメージの作成、コンテナのデプロイの自動化。いわゆるCICD。
  * yamlファイルの更新をトリガーに、API Gatewayへのインポート・デプロイを自動で実行する(GitHub Actions,Jenkinsなどを使用する想定)

## まとめ

マイクロサービスがトレンドになってきている中、APIの集中管理が欠かせなくなっているため、APIを効率よく管理する方法が求められていると思います。そのため、APIの開発・バージョン管理・ドキュメント管理の効率化を図る必要があり、Swaggerはその中でもAPIの開発の武器になるものと感じました。バージョン管理・ドキュメント管理の効率化のため、Gitとの連携やパイプラインの作成を実施することで、より良い開発環境が実現できるでは、と思いました。