GitHub Actionsを使ってAPI設計をフロントエンドと共有する話

## はじめに

以下記事を読んで、OpenAPI Specをもっとフロントエンド側で活用できたら良いなと思いました。

https://devblog.thebase.in/entry/2022/03/28/130016

そこで、同じようにGitHub Actionsを使って、GitHub PagesにReDocを配置、Mock Server（Prism）をCloud Runへデプロイしてみることにしました。

なお、Mock Server（Prism）のデプロイ先をCloud Runにしたのは、最小インスタンスを0にする（つまり、アクセスが無い時はインスタンスが落ちている状態にできる）ことができ、課金を抑えられると思ったからです。

## ReDocとは？

ReDocは、OpenAPI Specからドキュメントを生成してくれるツールです。

https://github.com/Redocly/redoc

同じようなツールで、[Swagger UI](https://swagger.io/tools/swagger-ui/)もありますが、Swagger UIはリファレンス要素が強く、ReDocはガイド要素が強い（見易さ重視）印象を受けました。そのため、ドキュメントとして参照するならReDocの方が良いと思いました。

OpenAPI SpecからReDocを静的HTMLとして出力するには、 `redoc-cli` を使用します。

https://github.com/Redocly/redoc/blob/master/cli/README.md

```bash
yarn add -D redoc-cli

// -o オプションを付けて静的HTMLとして出力する
redoc-cli bundle ${OpenAPI Spec Yaml} -o docs/index.html
```

**↓静的HTMLで出力したReDoc**
![静的HTMLで出力したReDoc](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241956/110ef89f-bdbc-cb03-26bd-e87cb0ff7ff6.png)

## Mock Server（Prism）

Prismは、OpenAPI SpecからMock Serverを構築してくれるツールです。

https://github.com/stoplightio/prism

PrismはNode.jsで動くので、ローカル環境で動かすことができます。

```bash
yarn add -D @stoplight/prism-cli

prism mock ${OpenAPI Spec Yaml} -h 0.0.0.0 -p 8080
```

これをCloud Runにアップして動かすには、Dockerコンテナで動かす必要があります。
なので、以下のような `Dockerfile` を用意します。

```docker
FROM node:16

WORKDIR /openapi

COPY ${OpenAPI Spec Yaml} .

# install stoplight/prism
RUN npm install -g @stoplight/prism-cli

CMD ["prism", "mock", ${OpenAPI Spec Yaml}, "-h", "0.0.0.0", "-p", "8080"]

EXPOSE 8080
```

## GitHub Actions

GitHub PagesにReDocを配置、Mock Server（Prism）をCloud RunへデプロイするGitHub Actionsを書きます。

```yaml
name: Build and Push Image

on: [push]

jobs:
  build-and-publish:
    name: Build and Push Docker Image
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          ref: ${{ github.ref }}
      - name: Node setup
        uses: actions/setup-node@v2
        with:
          node-version: 16
          cache: yarn
      - name: Yarn install
        run: |
          yarn install
      - name: Yarn build:doc
        run: |
          yarn build:doc
      - name: Redoc Deploy
        uses: peaceiris/actions-gh-pages@v3
        if: ${{ github.ref == 'refs/heads/main' }}
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./docs
      - name: Setup Google Cloud
        uses: google-github-actions/setup-gcloud@v0
        with:
          service_account_key: ${{ secrets.GCLOUD_AUTH }}
          project_id: ${{ secrets.GCLOUD_PROJECT_ID }}
      - name: Configure docker for artifact registry
        run: |
          gcloud auth configure-docker asia-northeast1-docker.pkg.dev
      - name: Set TAG
        run: |
          echo "TAG=$(npx -c 'echo "$npm_package_version"')" >> $GITHUB_ENV
      - name: Docker Build
        run: |
          docker build -t asia-northeast1-docker.pkg.dev/${{ secrets.GCLOUD_PROJECT_ID }}/prism/prism-image:${{ env.TAG }} ./
      - name: Docker Push
        run: |
          docker push asia-northeast1-docker.pkg.dev/${{ secrets.GCLOUD_PROJECT_ID }}/prism/prism-image:${{ env.TAG }}
      - name: Deploy to Cloud Run
        uses: google-github-actions/deploy-cloudrun@v0
        with:
          service: prism-server
          image: asia-northeast1-docker.pkg.dev/${{ secrets.GCLOUD_PROJECT_ID }}/prism/prism-image:${{ env.TAG }}
          region: asia-northeast1
      - name: Public Access Cloud Run
        run: |
          gcloud run services add-iam-policy-binding prism-server \
            --member="allUsers" \
            --region="asia-northeast1" \
            --role="roles/run.invoker"
```

上から説明していきます。

1. 一旦、 `push` されるとGitHub Actionsが起動する仕組みにしています。
2. `Node setup`にて、 `Node.js`の環境を用意します。
3. `Yarn build:doc`にて、 `redoc-cli bundle ${OpenAPI Spec Yaml} -o docs/index.html` を実行しています。
4. `Redoc Deploy`では、 [`peaceiris/actions-gh-pages`](https://github.com/peaceiris/actions-gh-pages)を使って、静的HTMLのReDocファイルをGitHub Pagesへデプロイしています。デフォルトだと、 `gh-pages` ブランチを作って、そこに `./docs` フォルダの中身をデプロイします。
5. `Setup Google Cloud` から、Google Cloudのセットアップです。(事前にGCPへアクセスできるサービスアカウントの設定やトークンを`GitHub Secrets`へ登録する必要があります）
Cloud Runへデプロイするためには、以下作業をしなくてはなりません。
    1. Dockerイメージを作成
    2. DockerイメージをArtifact Registry（旧：Container Registry）へ格納
    3. Artifact RegistryからCloud Runを起動
    
    このあたりは `Terraform` を使ったほうが便利かと思ったのですが、 `Terraform` を使う場合、Cloud Build等でDockerイメージをビルドする必要があり、だったらGitHub Actions上でビルドした方が楽かなと思ったので、 `Terraform`の採用は止めました。（もし、違っていたら、ご教示頂けると助かります。）
    
6. `Configure docker for artifact registry` にて、Artifact Registryの設定を行います。
7. `Set TAG` で、Dockerイメージのタグを付与します。タグは `package.json` のバージョンにしました。
8. `Docker Build` にて、Dockerイメージを作成。
9. `Docker Push` にて、DockerイメージをArtifact RegistryへPush。
10. `Deploy to Cloud Run` では、[`google-github-actions/deploy-cloudrun`](https://github.com/google-github-actions/deploy-cloudrun)を使って、Dockerイメージを使ってCloud Runにデプロイ。
11. 最後に `Public Access Cloud Run` で、Cloud Runへのアクセス制限をします。今回はアクセス制限せずに公開（`allUsers` に設定）

あとは、GitHubにソースをPushするだけで、ReDocの生成とCloud RunでMock Server（Prism）が動きます。

## Cloud Runのアクセス制御について

今回は `allUsers` を使って、アクセス制限をかけずに公開しました。
しかし、用途によっては、アクセス制限が必要な場合があるので、その手段と個人的に感じたデメリットを以下に纏めます。

### 1) Cloud Load BalansingによるServerless NEGとCloud Armorを使った制限

Cloud Runの前に、Cloud Load BalansingのServerless NEGを置いて、Cloud Armorを使ってアクセス制限する方法。IPアドレスによる制限等が可能です。

- デメリット

Cloud Load Balansingが高価。

### 2) Cloud Endpointによる制限

Cloud Runの前に、Cloud EndpointのESP（Extensible Service Proxy）を置くことで、なにかしらの認証（Google認証、Auth0を使用した認証、API Keyを使った認証など）を行った上でないとアクセスできないようにすることができます。

- デメリット

ESPv2のDockerイメージ格納場所が、Artifact Registryに対応していない。（2022/04時点）
構築が少し大変。

### 3) Cloud RunのIAM（サービスアカウント）による制限

Cloud Runを起動できるIAM（サービスアカウントやGoogle Workspace）を用意してアクセスする。

- デメリット

Authorizationヘッダーにトークンが必要になるので、トークン取得処理が別途必要。

### まとめ
金銭的に余裕があるのであれば、**Cloud Load BalansingによるServerless NEGとCloud Armorを使った制限**をオススメします。
金銭的に余裕がなく、Cloud RunにアップしたMock Server（Prism）を使う人が少数なら、**Cloud RunのIAM（サービスアカウント）による制限**でも良いと思います。
ただし、Mock Server（Prism）を動かしたい人には、サービスアカウントの鍵情報を渡す必要がるので、注意が必要です。（自己責任でお願いしますm(_ _)m）
あと、トークンの取得が必要なので、以下ライブラリも必要になります。

https://github.com/googleapis/google-auth-library-nodejs

APIを呼び出すコードのサンプルも置いておきます。

```tsx
import { JWT } from "google-auth-library"
import service_account from '../${サービスアカウントの鍵}.json'
import axios from 'axios'

;(async () => {
  const client = new JWT({
    forceRefreshOnFailure: true,
    key: service_account.private_key,
    email: service_account.client_email,
  })

  const url = "＄｛Cloud Runのエンドポイント｝"
  const token = await client.fetchIdToken(url)
  const { data } = await axios.get(`${url}`, {
    headers: {
      Authorization: `Bearer ${token}`
    }
  })
  console.log(`data: ${JSON.stringify(data)}`)
})()
```

## さいごに

OpenAPI Specを活用すれば、フロントとバックエンドでAPIの仕様が共有できます。
しかも、事前にこのようなCIを用意しておくことで、共有漏れを防げぐことができ、バックエンド側でAPIを用意できない間でもフロントエンドは手を止めなくて済みます。
とはいえ、実は私自身、これを実践で使ったことがありません😭
でも、こういったことは事前に考えておくことが重要なのだと思いました。🙂