JSON Schemaを用いたAPI Gatewayの設定ファイル管理

こんにちは、ECプラットフォーム部の鶴見、竹中です。普段はZOZOTOWNのリプレイスに関わるID基盤とAPI Gatewayの開発を行っています。

本記事では、API Gatewayの開発で取り入れているJSON Schemaを使ったドキュメントの自動生成および、スキーマの自動検証を紹介します。

API Gateway設定ファイルの運用改善

弊社で開発しているAPI Gatewayは、APIへのリクエストのルーティングやリトライなど様々な機能の制御を設定ファイルで行っています。

バックエンドチームはアプリケーションおよび、設定ファイルの仕様書を作成し、SREチームはインフラの構成やパフォーマンスを考慮しながら仕様書に記載されたフォーマットで設定ファイルを記述しています。

設定ファイルの仕様書はConfluenceで、アプリケーションはGitHubで管理しています。設定ファイルの仕様変更が発生した場合は、アプリケーションのデプロイ後に仕様書を手動で更新するという運用をしていました。しかし、手動での更新は更新漏れやタイプミスによる設定可能値の誤りなどの問題を発生させていました。

この問題を解決するための方法として、以下の3つが考えられます。

  • 仕様書のレビュー体制の強化
  • 設定値に対する検証処理コードからの仕様書の自動生成
  • 仕様書からの設定値に対する検証処理コードの自動生成

今回は、実現の容易さから最後の「仕様書からの設定値に対する検証処理コードの自動生成」を選びました。この場合、自然言語からの自動生成は困難ですが、何らかのスキーマ言語を用いることで実現が比較的容易になります。

このスキーマ言語に対して求められることは、仕様を記述するのに十分な表現力を有すること、および検証処理の自動生成が容易であることの2つです。

また同時に、スキーマ言語を用いることで可読性が落ちるため、HTMLドキュメントなどへの変換が容易に行える必要もあります。

これらの条件を満たすスキーマ言語としてJSON Schemaを選定しました。JSON Schemaは、JSONドキュメントを検証可能にするための標準規格です。

改善方法

今回の運用改善ですることをまとめると、以下の通りです。

  1. JSON Schemaで設定ファイルの仕様を定義し、アプリケーションと同じGitHubリポジトリで管理
  2. JSON SchemaからHTMLドキュメントを自動生成
  3. アプリケーション起動時、個別に行っていた設定値の検証処理をJSON Schemaに基づいた検証処理へ変更

上記の改善で行ったJSON Schemaを使ったドキュメントの自動生成および、スキーマの自動検証の2つについて具体的な実装方法を紹介します。

JSON Schemaを使ったドキュメントの自動生成

最初にJSON Schemaからドキュメントを自動生成する方法を紹介します。ドキュメント生成には、Python製のJSON Schema for Humansを利用しました。

JSON Schema for Humans

JSON Schema for Humansは、JSON Schemaからドキュメント化した静的なHTMLページを生成します。実際はAPI Gatewayの仕様を定義したJSON Schemaを使用しますが、今回は説明のため公式サンプルのJSON Schemaを元にドキュメント化してみます。

{
  "$id": "https://example.com/arrays.schema.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "description": "A representation of a person, company, organization, or place",
  "type": "object",
  "properties": {
    "fruits": {
      "type": "array",
      "items": {
        "type": "string",
        "examples": ["apple"]
      }
    },
    "vegetables": {
      "type": "array",
      "items": { "$ref": "#/definitions/veggie" }
    }
  },
  "definitions": {
    "veggie": {
      "type": "object",
      "required": [ "veggieName", "veggieLike" ],
      "properties": {
        "veggieName": {
          "type": "string",
          "description": "The name of the vegetable.",
          "examples": ["potato"]
        },
        "veggieLike": {
          "type": "boolean",
          "description": "Do I like this vegetable?",
          "examples": ["true"]
        }
      }
    }
  }
}

generate-schema-docコマンドを使い、JSON Schema(schema.json)を元にドキュメント(schema_doc.html)を生成します。

generate-schema-doc schema.json schema_doc.html

コマンド実行により、以下3ファイルが自動生成されました。

  • schema_doc.html
  • schema_doc.min.js
  • schema_doc.css

生成されたHTML(schema_doc.html)を確認します。

JSON Schemaがドキュメント化されました。

ドキュメント生成の自動化

JSON Schemaを修正するたびに、generate-schema-docコマンドでドキュメント生成できます。しかし、手間がかかるため自動化します。自動化にはGitHub Actionsを利用しました。

GitHub Actionsは、ワークフローを自動化するサービスです。JSON SchemaはGitHubリポジトリで管理しているため、プッシュやプルリクエストがマージされたタイミングでドキュメント生成処理をすることで自動化できると考えました。

以下のステップでドキュメントを自動生成しています。

  1. JSON Schemaを修正し、GitHub masterブランチにマージ
  2. GitHub Actions上でPythonおよび、JSON Schema for Humansをセットアップ
  3. generate-schema-docコマンドにより、JSON Schemaからドキュメント(HTML/CSS/JavaScript)を生成
  4. ドキュメントファイルをS3にアップロード

自動化によりS3へアクセスすれば、常に最新のドキュメントを確認できます。S3は、社員のみ参照できるようアクセス制限をしました。

GitHub Actions

GitHub Actionsで行っている処理内容を紹介します。JSON Schema for Humansをセットアップ、JSON Schemaからドキュメントを生成し、S3にアップロードする方法です。

name: Update Docs

on:
  pull_request:
    branches:
      - master
    types:
      - closed
    paths:
      - schema.json

jobs:
  updateDoc:
    name: Update Docs
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.8.5'

      - name: Generate Docs
        run: |
          pip install json-schema-for-humans
          generate-schema-doc schema.json ./docs/schema_doc.html

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

      - name: Upload
        working-directory: docs
        run: aws s3 sync . s3://docs --delete

プルリクエストがマージされたタイミングで動作するように設定しています。また、GitHubリポジトリにはJSON Schema以外のファイルも含まれているため、pathsで対象JSON Schemaファイル(schema.json)を指定しました。これによりJSON Schemaに変更があった時のみGitHub Actionsが動作します。

JSON Schemaを使ったスキーマの自動検証

設定ファイル(スキーマ)の誤りをアプリケーション起動時に気付けるよう、アプリケーション内で設定ファイルに対しJSON Schemaを利用して検証する仕組みを作りました。

API GatewayではGoを用いて開発しているため、実装にあたりgojsonschemaというパッケージを利用しました。また、設定ファイルはYAMLで管理していたため、JSONにはせず、YAMLのままとしました。検証時はYAMLをJSONに変換しています。

JSON Schemaは、先程のJSON Schema for Humansのサンプルを利用します。

次に、検証するための設定ファイルを用意します。

fruits:
  - apple
  - orange
  - pear
vegetables:
  - veggieLike: true
    veggieName: potato
  - veggieLike: false
    veggieName: broccoli

JSON Schemaを利用して設定ファイルを検証する実装が以下のソースコードです。YAMLからJSONへの変換には、yamlというパッケージを利用します。

package main

import (
    "fmt"
    "io/ioutil"

    "github.com/ghodss/yaml"
    "github.com/xeipuuv/gojsonschema"
)

func main() {
    fruitsData, e := ioutil.ReadFile("fruits.yaml")
    if e != nil {
        panic(e)
    }
    converted, e := yaml.YAMLToJSON(fruitsData)
    if e != nil {
        panic(e)
    }

    schemaLoader := gojsonschema.NewReferenceLoader("file:///home/me/fruits-schema.json")
    documentLoader := gojsonschema.NewStringLoader(fmt.Sprintf("%s", converted))
    result, e := gojsonschema.Validate(schemaLoader, documentLoader)
    if e != nil {
        panic(e)
    }

    if result.Valid() {
        fmt.Printf("The document is valid\n")
    } else {
        fmt.Printf("The document is not valid. see errors :\n")
        for _, desc := range result.Errors() {
            fmt.Printf("- %s\n", desc)
        }
    }
}

実行結果は以下のようになり、設定ファイルがJSON Schemaの定義通りであることが確認できます。

>go run main.go
The document is valid

JSON Schemaとは異なる定義にした設定ファイルを用意します。

fruits:
  - apple
  - orange
  - pear
vegetables:
  - veggieLike: true
    veggieName: potato
    # 必須プロパティ不足
  - veggieName: broccoli

実行結果はエラーになり、設定ファイルのミスに気づけます。

>go run main.go
The document is not valid. see errors :
- vegetables.1: veggieLike is required

細かい制御の追加

ここからは、より実用的な制御例を紹介します。JSON Schemaの例は全体の一部を抜粋して記述しています。他の詳細なオプションは公式ドキュメントをご参照ください。

不要なプロパティを許可しない

"additionalProperties": false の追加により、記述されたプロパティ以外をエラーにします。typoを防ぐのにも役立ちます。

{
  "type": "object",
  "properties": {
    "fruits": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "vegetables": {
      "type": "array",
      "items": { "$ref": "#/definitions/veggie" }
    }
  },
  "additionalProperties": false
}

typoしたYAML設定ファイルを用意します。

# fruitsのtypo
fruit:
  - apple
  - orange
  - pear
vegetables:
  - veggieLike: true
    veggieName: potato
  - veggieLike: false
    veggieName: broccoli

プロパティのエラーとして正しく検出されます。

>go run main.go
The document is not valid. see errors :
- (root): Additional property fruit is not allowed

特定の値のみ許可する

"enum": ["potato", "broccoli", "pumpkin"] の追加により、記述された値以外をエラーにします。

    "definitions": {
      "veggie": {
        "type": "object",
        "required": [ "veggieName", "veggieLike" ],
        "properties": {
          "veggieName": {
            "type": "string",
            "enum": ["potato", "broccoli", "pumpkin"],
            "description": "The name of the vegetable."
          },
          "veggieLike": {
            "type": "boolean",
            "description": "Do I like this vegetable?"
          }
        }
      }
    }

許可されていない値を指定した設定ファイルを用意します。

fruits:
  - apple
  - orange
  - pear
vegetables:
  - veggieLike: true
    veggieName: potato
  - veggieLike: false
    # enumに定義されていない名前
    veggieName: carrot

値のエラーとして正しく検出されます。

>go run main.go
The document is not valid. see errors :
- vegetables.1.veggieName: vegetables.1.veggieName must be one of the following: "potato", "broccoli", "pumpkin"

まとめ

JSON Schemaを使ったドキュメントの自動生成およびスキーマの自動検証を紹介しました。JSON Schemaを用いたことで仕様が管理しやすくなり、仕様書と実際の検証処理に乖離が発生することを防ぐことができました。YAML、JSONを使った設定ファイルの運用方法に迷っている方は参考にしてみてください。

最後に、ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください!

tech.zozo.com

カテゴリー