大コンテナ時代を生きのこるためのJSON Schema

実行環境をコンテナ化するDockerが普及して久しく、CIやローカルの開発環境などどこかでコンテナ技術に触れているのではないでしょうか。

コンテナはその性質上、設定のプロビジョニングに古典的な設定ファイル (のパス) 受け渡しが難しいです。etcdconsulのようなKVS (= Key-value store) を用いることもあるでしょうが、素朴には環境変数で与えることが多いでしょう。

HerokuはThe Twelve-Factor Appというパターンを提唱し、その中でStore config in the environmentと述べています。

The twelve-factor app stores config in environment variables (often shortened to env vars or env). Env vars are easy to change between deploys without changing any code; unlike config files, there is little chance of them being checked into the code repo accidentally; and unlike custom config files, or other config mechanisms such as Java System Properties, they are a language- and OS-agnostic standard.

The Twelve-Factor App

参考: Building Twelve Factor Apps on Heroku | Heroku

しかし環境変数グローバル変数なので無闇に参照すれば古式ゆかしきスパゲティコードまっしぐらです。また環境変数の値は文字列として扱われるので表現力に乏しく利用者のために値の定義域を示す必要がしばしば生じますが、どのように行うべきでしょうか?

そんな悩みをJSON Schemaで解決しようという試みを紹介します。

環境変数の値の定義

ある環境変数がとりうる値の形式や範囲を考えてみます。

たとえば真偽値 (boolean) が挙げられます。 DEBUG=1デバッグフラグを立てるような変数です。
1か0か、あるいは値はなんでもよくて定義されていれば真とする場合もありますが、併せて真偽値とします。

あるいは数値 (number) も考えられます。 LOGLEVEL=1LOGLEVEL=7 など、ログ出力を数値で指定するケースがあります。

真偽値と数値を一般化すると列挙 (enum, enumeration) になります。たとえば PLACK_ENV=test PLACK_ENV=production など、文字列として扱われるがそのとりうる値があらかじめいくつかに決まっています。

また特殊な例として複数値 (multiple values) についても考える必要があります。
環境変数は辞書構造なので、ある名前に対応する値はただひとつに定められるため、複数の値を渡したい場合は値をデータ区切り文字 (delimiter) で分割して扱います。

たとえば PATH は値をコロンで区切ります *1

PATH はコロンで区切るというコンセンサスが得られているのであまり問題になりませんが、独自の変数を定義するときに区切り文字は何であるかはいよいよもって明示しなければ正しく使うことはできません。

JSON Schemaの利用

そこでJSON Schemaを利用します。

JSON Schema (JSON Schema Core)はその名の通りJSONの形式を表明する形式で、スキーマに基づいて入力の検証を行うためのボキャブラリを定義するJSON Schema Validationなどの仕様とあわせて利用されます。
以下、CoreとValidationをまとめてJSON Schemaと呼びます。

JSONと名がついていますがあくまでシリアライズ形式にJSONを選んでいるというだけで、利用する上でその他の制約はありません。

JSONとしてシリアライズ可能、すなわちJSONが定義する辞書構造 (object) とコレクション (array) のほかいくつかのスカラ値 (string, boolean, null) のいずれかで表現できる入力であればJSON Schemaで表明・検証できます。

先に述べた通り環境変数は名前と値がそれぞれ文字列である辞書構造なのでJSONとしてシリアライズ可能です。

以下はあるスクリプトが期待する環境変数JSON Schemaの例です:

{
  "title": "Schema of some script's environment variables",
  "$schema": "http://json-schema.org/draft-06/schema#",
  "type": "object",
  "properties": {
    "DEBUG": {
      "type": "string",
      "enum": [
        "1",
        "0"
      ]
    },
    "LOGLEVEL": {
      "type": "string",
      "enum": [
        "0", "1", "2", "3", "4", "5", "6", "7"
      ]
    },
    "PLACK_ENV": {
      "type": "string",
      "enum": [
        "development", "test", "production"
      ]
    },
    "PATH": {
      "type": "string",
      "pattern": "[^:]+(:[^:]+)*"
    }
  },
  "required": [
    "PLACK_ENV"
  ]
}

先に述べたnumberやbooleanを期待する例がJSON Schemaで記述されています。

実際にこのスキーマを用いて検証を行ってみます。

Rubyjson-schema.gemを利用します。以下のGistにコード例を示します:

Gemfile · GitHub

実行結果の例です:

✘╹◡╹✘ < ruby validates_env.rb
The property '#/' did not contain a required property of 'PLACK_ENV'
✘>﹏<✘ < PLACK_ENV=a ruby validates_env.rb
The property '#/PLACK_ENV' value "a" did not match one of the following values: development, test, production
✘>﹏<✘ < PLACK_ENV=development ruby validates_env.rb
✘╹◡╹✘ <

PLACK_ENV=a を渡したらきちんと怒られてかわいいですね。

独自実装を含む他の検証方法に比してJSON Schemaを選ぶ利点はいくつかあります。

  • 相互運用性が高い
    • ランタイムによらず利用できるため、複数の言語を採用している場合に障壁となりません
  • 検証のセマンティクスの一貫性が保たれる
  • スキーマの再配布・拡張が容易
    • たとえば組織内で設定に用いる環境変数を共通化したいとき、あるURLでスキーマを配布・参照することで普及させやすくなるかもしれません
  • JSON Schemaのエコシステムの恩恵に与れる
    • たとえばドキュメント生成が簡単になるかもしれません

一方、物足りないと感じる点としては、enumやpatternプロパティを駆使するほかなく、あまり可読性が高くない点でしょうか。

この点については、JSON Schemaの仕様ではformatプロパティに独自のキーワードを追加・利用することを許容しているため、booleanライクな文字列の定義を追加し、それをformatに指定することで冗長な定義を避けられるかもしれません。

Implementations MAY add custom format attributes. Save for agreement between parties, schema authors SHALL NOT expect a peer implementation to support this keyword and/or custom format attributes.

JSON Schema Validation: A Vocabulary for Structural Validation of JSON

むすび

以上、設定のスキーマJSON Schemaで記述・検証することでコンテナ時代に即したtwelve-factor appパターンを促進する試みでした。

私はtwelve-factor appの環境変数を推進する姿勢については環境変数の表現力の乏しさを懸念し懐疑的でしたが、JSON Schemaを利用することを思い付いてからは懸念も解消され強く共感します。

既に述べたように独自formatを定義・利用するバリデータがあるとより便利かもしれないという思いもあるので、引き続き検討・導入していきたいところです。


この記事ははてなエンジニア Advent Calendar 2017の8日目の記事でした。

明日 (12/9) はid:ikesyoです、おたのしみに!

*1:ただしUNIX/Linuxにおける場合、Windowsでは異なる