OpenAPIドキュメントから生成した静的ファイルをいい感じにホストしてみた

OpenAPIドキュメントから生成した静的ファイルをFirebase Hostingを使っていい感じにホストしてみました。この仕組みにより開発者は簡単に最新の定義にたどり着けるようになります。

概要は以下の通り

  • GitHubにPushしたOpenAPIドキュメントを元にCloudBuildで静的ファイルを生成
  • CloudBuildが生成したドキュメントをFirebase Hostingにデプロイ
  • Cloud Functions for FirebaseでBasic認証

それぞれのサービスをAWSのサービスにマッピングするとFirebase HostingはAmplify Console、Cloud Functions for FirebaseはLambda@Edge、CloudBuildはCodeBuildのようなイメージになります。

それでは行ってみましょう!!

次の環境で検証します。

$ node -v
v10.18.1

今回作成したコードは以下のリポジトリにあげています。

ローカルで静的ファイルを生成

まずはローカルでOpenAPIドキュメントから静的ファイルを生成できる状態にします。任意のディレクトリを作成後、 package.jsonを生成し、redoc-cliをインストールします。

$ yarn init
$ yarn add redoc-cli

次にpackage.json以下のように修正します。

  "scripts": {
    "generate:app": "redoc-cli bundle docs/specs/app.yaml --output app.html"
  },

生成対象となるOpenAPIドキュメントを配置します。Stoplightなどを使うと簡単に作成できます。

openapi: 3.0.0
info:
  title: app
  version: 1.0
paths:
  /:
    get:
      summary: Hoge API
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  payload:
                    type: object
        '400':
          description: Bad Request
      parameters:
        - schema:
            type: string
          in: header
          name: authorization
          required: true

redoc-cliを実行しローカル上で静的ファイルが出力されたことを確認します。

$ yarn run generate:app
$ open app.html

問題なさそうです。

Firebaseプロジェクトの設定

Firebaseプロジェクトの設定を行います。

Firebaseプロジェクト作成

今回はCloudBuildから`FIREBASE_TOKEN`を指定せずにFirebaseにデプロイします。FirebaseのプランはSpark(無料)から Blazeになるので注意してください。

GCPのコンソールからFirebaseHostingを選択し、内容を確認の上プロジェクトを作成します。作成完了後はこのような表示となります。

Firebaseプロジェクト初期処理

ホスティングされたコンテンツにBasic認証をかけるための設定を追加します。

firebaseコマンドをインストールします。firebaseにログインしプロジェクトを確認します。

$ firebase login
$ firebase projects:list

✔ Preparing the list of your Firebase projects
┌──────────────────────────┬───────────────────────────┬──────────────────────┐
│ Project Display Name     │ Project ID                │ Resource Location ID │
├──────────────────────────┼───────────────────────────┼──────────────────────┤
│ XXXXXXXXXXXXXXXXXXXXXXXX │ XXXXXXXXXXXXXXXXXXXXXXXX  │ asia-northeast1      │
└──────────────────────────┴───────────────────────────┴──────────────────────┘

firebase initを実行しfirebaseのコンフィグファイルを作成します。Functions、Hostingの2つを有効にします。

$ firebase init

     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
     ######    ##  ########  ######   ########  #########  ######  ######
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confi
rm your choices. 
 ◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
 ◉ Functions: Configure and deploy Cloud Functions
❯◉ Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules
 ◯ Emulators: Set up local emulators for Firebase features

先ほど作成したプロジェクトを選択します。

=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Please select an option: (Use arrow keys)
❯ Use an existing project 
  Create a new project 
  Add Firebase to an existing Google Cloud Platform project 
  Don't set up a default project 

Cloud Functionsの言語はJavaScriptとします。

=== Functions Setup

A functions directory will be created in your project with a Node.js
package pre-configured. Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? 
❯ JavaScript 
  TypeScript 

その他項目は一旦デフォルトで構いません。処理が完了するとfirebaseのコンフィグファイルが作成されます。

FirebaseHostingサイト追加

Firebase Hostingのサイト追加します。 私の環境では既にサイトが存在したいるため、「別サイトの追加」より追加していきます。

複数サイトを管理する場合は、firebase.jsonにsiteを追加します。

{
  "hosting": {
    "site": "blog-jogan",
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}

Cloud Functions for Firebaseの作成

ここからCloud Functionsの設定を追加していきます。

まずは、public配下の不要なファイルを削除します。フォルダ自体は必要なため.gitkeepを配置しておきましょう。

$ rm -rf ./public/*
$ touch ./public/.gitkeep

Basic認証を実施するための処理を実装します。functions/index.jsを以下のように修正します。

const functions = require('firebase-functions')
const express = require('express')
const basicAuth = require('basic-auth-connect')

const app = express()

app.all('/*', basicAuth(function(user, password) {
  return user === 'hoge-user' && password === `${functions.config().user.password}`;
}));

app.use(express.static(__dirname + '/static/'))

exports.app = functions.https.onRequest(app)

これで当該ファンクションに到達した全てのリクエストはbasic認証を通るようになり、認証に成功したユーザーのみstaticフォルダにアクセスできるようになります。

パスワードの情報はGitHubにはコミットしたくないので、ファンクションから読み込み可能な環境変数に値をセットします。

$ firebase functions:config:set user.password=<password>

functions/package.jsonenginesのバージョンを10に変更します。

  "engines": {
    "node": "10"
  },

functions配下に移動しCloud Functionsが必要なモジュールをインストールします。

$ cd functions
$ yarn add express
$ yarn add basic-auth-connect

ホストしたサイトへのリクエストは、上記のファンクション経由となるようfirebase.jsonrewritesを属性を追加します。

{
  "hosting": {
    "site": "blog-jogan",
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "function": "app"
      }
    ]
  }
}

CloudBuildの設定

CloudBuildの設定を行います。

まずはCloudBuildから起動するfirebaseコマンドが内包されたコンテナを用意します。任意のディレクトリで以下を実行してください。

$ git clone https://github.com/GoogleCloudPlatform/cloud-builders-community.git
$ cd cloud-builders-community/firebase
$ gcloud builds submit --project <project name> --config cloudbuild.yaml .

これによりfirebaseコマンドが内包されたコンテナがGCRにPushされます。

次にCloudBuildから実行するスクリプトを定義します。package.jsonを以下のように修正します。

  • CloudFunctionsのモジュールをインストールするためのfunctions:installを追加
  • generate:app(redoc-cli bundle)の出力先をfunctions/static/app.htmlに変更
  "scripts": {
    "functions:install": "cd functions && yarn install",
    "generate:app": "redoc-cli bundle docs/specs/app.yaml --output functions/static/app.html"
  },

CloudBuildでstep実行する処理を定義します。この定義がCloudBuildにより実行されると、必要なモジュールのインストール、静的ファイルの生成、firebaseへのデプロイが行われます。

steps:
  - name: node:10.18
    entrypoint: yarn
    args: ['install']
  - name: node:10.18
    entrypoint: yarn
    args: ['functions:install']
  - name: node:10.18
    entrypoint: yarn
    args: ['generate:app']
  - name: 'gcr.io/$PROJECT_ID/firebase'
    args: ['deploy', '--project', $PROJECT_ID]

最後にCloud BuildからFirebase Hosting、Functionsへデプロイするための権限を付与します。サービスアカウント権限から CloudFunction開発者とFirebase管理者を有効にしましょう。

CloudBuildトリガー設定

CloudBuildのトリガーを設定します。

CloudBuildからトリガーを選択し、リポジトリの接続をクリックします。

ソースをGitHubとした後、指定のリポジトリに接続します。

一旦、この状態でトリガーを作成します。

masterへのPushのみ発火するようにトリガーを編集します。

正規表現を^master$に変更し保存します。

自動デプロイ

githubにコードをPushします。リポジトリは適宜読み替えてください。

$ git add . 
$ git commit -m "first commit"
$ git remote add origin https://github.com/jogannaoki/openapi-firebase-hosting.git
$ git push -u origin master

すると、Cloud Buildが自動実行されます。

処理が完了すると、Basic認証のある静的サイトが出来上がります。

うまくホストできました。

さいごに

OpenAPIドキュメントから生成した静的ファイルをFirebase Hostingを使っていい感じにホストしてみました。APIの定義を簡単に開発者に共有できるのはかなり嬉しいのではないでしょうか。どなたかの参考になれば幸いです。 ※Amplify Consoleでも同じようなことはできます。

参考