設定ファイル統一戦争勃発(もしくはHOCONのススメ

こんにちは、Wano株式会社でVeleTという動画広告配信サービスのエンジニアをやっております。stk0724です。

この記事は[Wanoグループ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/wano-group)の24日目となります。

仰々しいタイトルですが、なんのことはありません。設定ファイル管理をどのようにいい感じにしたかについてお話します。

## 顛末

結論から先に言うと、統一してないです。

大本となるjsonファイルをパラメータファイルとして、用途別のテンプレートファイルから各種設定ファイルをレンダリングする方式をとりました。


## 前提: 設定ファイル、いっぱいありますよね？

webアプリケーションフレームワークやら、各種ミドルウェアやらで最近のサービス開発では複数種類の設定ファイルを扱うことになると思います。

VeleTの場合ですと、

- 管理画面の設定ファイル
- nginxの設定ファイル
- fluentdの設定ファイル

などがあります。

### 設定ファイルがバラけると、どうなる？

設定ファイル毎に全く異なる値を管理しているのであれば問題ないと思うのですが、同じ意味合いの項目が複数の設定ファイルに存在していると、変更が入った場合の反映漏れが怖いです。

これに本番用、開発環境用、ローカル用など、環境別に必要なことを考えると、

設定ファイル種別 * 環境数の数の設定ファイル管理が必要となります。

これを手動で管理するのはちょっと厳しいです。

## ソリューション: 単一パラメータファイルから生成したらよくね？

単一の設定ファイル読み込ませるようにしたら、反映漏れとか発生しないですよね。

じゃあ設定ファイル統一しよう！！

ってのはムリですよね。設定ファイルのフォーマットそれぞれ異なりますし。

なので、環境別に単一パラメータファイルを用意して、なんらかのテンプレートライブラリで設定ファイルテンプレートを書き、パラメータファイルを読み込ませてからレンダリングして作ればいいのではと考えました。

以下のような流れになります。

1. HOCONによるパラメータファイルをjsonに変換する
2. pongo2テンプレートファイルのレンダリングコマンドにパラメータファイルとパラメータファイルを入力して設定ファイル生成

### 単一パラメータファイルにHOCONフォーマットを利用する

現在VeleTでは管理画面をScala + Playframeworkでリニューアル中でして、新管理画面では[HOCON](https://github.com/lightbend/config/blob/master/HOCON.md)フォーマットで設定ファイルを書いています。

HOCONフォーマットはlightbend社が開発したフォーマットで、Playframweorkの他、lightbend社が開発したOSSなどで利用されています。

#### HOCONの利点

Playframeworkを使っている関係から半分成り行きでHOCONを利用することにしたのですが、HOCONにはjson, yamlなどと比較して大きな利点があります。

1. 他のHOCONファイルをincludeできる
2. 既存設定値を上書きできる

1はそのままの意味です。2の利点がとても大きく、ローカル環境用の設定ファイルをベースとして、環境別に上書きが必要な項目のみincludeするといったような書き方ができます。

例えば以下のような感じです。

``` base.conf
domain = "localhost"
...その他いろいろ...
```

``` dev_domain.conf
domain = "dev.velet.jp"
```

``` prod_domain.conf
domain = "velet.jp"
```

``` local.conf
include "base.conf"
```

``` dev.conf
include "base.conf"
include "dev_domain.conf"
```

``` prod.conf
include "base.conf"
include "prod_domain.conf"
```

上記の例ですと、local.confはbase.confそのまま、dev.confはdomainがdev_domain.confの値に上書き、prod.confはprod_domain.confの値に上書きされます。

jsonやyamlの場合ですと、環境別に同じ項目のファイルを用意する必要があるので、差分が発生する項目だけ分けて、includeして上書きできるのは大きな利点と考えています。


### HOCONをjsonに変換する

後述しますが、レンダリング処理でpongo2テンプレートを利用する場合、jsonにしたほうが取り回しが楽なので、HOCONのパラメータファイルをjsonに変換します。

lightbend社純正のHOCONパーサを利用するのが確実なのですが、jvm起動が重たくて嫌なので、[pyhocon](https://github.com/chimpler/pyhocon)というPython用HOCONパーサを利用します。

pyhoconをpipでインストールすると、CLIも入るので、それを利用して以下のように実行します。

``` bash
pyhocon < /path/to/parameterfile > parameter.json
```

(本当はpyhoconが入ったdockerイメージを作って、docker run時に標準入力からパラメータファイルを食わせる方式にしたかったのですが、その場合だと別ファイルのincludeができないので、妥協しています。)


### テンプレートファイルから設定ファイルをレンダリングする

次にjsonをパラメータファイルとして、テンプレートファイルから設定ファイルをレンダリングします。

レンダリングはGoによるCLIコマンドでやりたかったので、テンプレートライブラリには[pongo2](https://github.com/flosch/pongo2)を選択しました。
(他のGo用のテンプレートライブラリも検討しましたが、自分が慣れている and 別の箇所で利用している and どっかでansible使うはずなので、jinja2ライクな記法に慣れておいたほうがよい といった理由でpongo2にしました)

レンダリング用のCLIコマンドですが、以下で公開しています。

[konfu](https://github.com/wano/konfu)

やってることはとても単純で、jsonをmap[string]interface{}としてUnmarshalし、それをpongo2のContextとしてテンプレートファイルのレンダリング処理を実行しているだけです。
(pongo2.Contextはmap[string]interface{}のエイリアス)

``` main.go
...省略...
var parser ParameterParser
if *mode == "yaml" {
  parser = NewYamlParser()
} else {
  parser = NewJsonParser()
}

f, err := os.Open(*config)
if err != nil {
  fmt.Fprintln(os.Stderr, err)
  os.Exit(1)
}
defer f.Close()

var context pongo2.Context
context, err = parser.Parse(f)
if err != nil {
  fmt.Fprint(os.Stderr, err)
  os.Exit(1)
}

pt, err := pongo2.FromFile(*templatePath)
if err != nil {
  fmt.Fprintln(os.Stderr, err)
  os.Exit(1)
}

output, err := pt.ExecuteBytes(context)
if err != nil {
  fmt.Fprintln(os.Stderr, err)
  os.Exit(1)
}
...省略...
```

これであとは目的別のテンプレートを用意すれば、パラメータファイルと一緒にkonfuに入力して設定ファイルをレンダリングすることができます。

## 最後に

どうでしょうか？いい感じではないでしょうか？

**まあ、単一パラメータファイル方式にしても、パラメータに変更が入った後にレンダリングするの忘れたら意味ないんですけどね**

(HOCONファイルにwatchかけて勝手にjson変換、設定ファイル再生成するようにしようか検討中です)