設定ファイルを読み込むライブラリnobushi-config


npmにパッケージをたくさん公開することを今年の目標にしました。公開するからには使ってもらえるようにしたいので、Qiitaに記事を書くところまでを目標にしたいです。

まずは第1弾として設定ファイルを読み込むためのライブラリを公開したので、どんなライブラリなのかを解説します。

# 目的

Node.jsで作成しているWebアプリケーションで、設定ファイルをJSやJSONのような頭使う形式[^1]ではなく、YAMLで書きたい。ファイル内にはデータベースのパスワードやJWTのsecretなど、他人に見られては困るものもある。暗号化すればいいけど、どの設定値が暗号化されていて、どの設定値はされていないのか、明確なルールを作りたい。暗号化には基本的に公開鍵暗号を使いたいが、環境によっては別の方法を使いたいこともあるので、変更できるようにしたい。あと環境変数も使いたい。

こんな要望を一つのパッケージで実現したかったので、作ってみました。

# URL

npm: https://www.npmjs.com/package/nobushi-config
GitHub: https://github.com/kou64yama/nobushi-config

# 使い方

npmパッケージなので、下記のコマンドでインストールできます。

```bash
npm i -S nobushi-config
```

設定ファイルはYAMLかJSONで書きます。JSONはおまけで対応しているだけで、YAML使った方がいいと思います。

```yml
# config/default.yml

server:
    host: localhost
    port: 3000

datasource:
    url: postgresql://localhost:5432/sample
    username: sample
    password: secret
```

基本的な使い方は、`nobushi-config`をインポートするだけです。

```js
// main.js
import config from 'nobushi-config';

console.log(config.server.host);    // => localhost
```

## 設定ファイルのルール

### 読み込み順序

設定ファイルは環境変数`NODE_CONFIG_DIR`で設定されたディレクトリか、設定されていなければプロセスの相対パスで`./config`にあるものを読み込みます。

`default.yml`、`${NODE_ENV}.yml`（環境変数`NODE_ENV`が設定されていれば）の順でファイルを読み込みます（JSONの場合は拡張子を`.json`に読み替えてください）。同じキーがあれば、後から読み込んだもので上書きします。

### オブジェクトの扱い

YAMLやJSONのオブジェクトは何重にもネストして記述することができますが、nobushi-config内部ではすべてフラットな構造に変換されます。

例えば、

```yml
server:
    host: localhost
    port: 3000
```

という設定は、nobushi-config内部では

```yml
server.host: localhost
server.port: 3000
```

と解釈されます。

インポートしたオブジェクトからアクセスする際には、このフラットな構造を元のネストされたオブジェクトに復元します。

この仕様は

```yml
foo:
    bar:
        baz:
            qux: foobarbazqux
```

みたいな設定を

```yml
foo.bar.baz.qux: foobarbazqux
```

と書けるようにしたかったからです。

### 配列の扱い

上記仕様により、配列の扱いには注意しなければなりません。

例えば

```yml
api:
    - https://api1.example.com
    - https://api2.example.com
    - https://api3.example.com
```

のような設定は

```yml
api.0: https://api1.example.com
api.1: https://api2.example.com
api.2: https://api3.example.com
```

と解釈されます。インポートした際もオブジェクトになっています。

```js
console.log(config.api);
// {
//   '0': 'https://api1.example.com',
//   '1': 'https://api2.example.com',
//   '2': 'https://api3.example.com'
// }
```

`${NODE_ENV}.yml`で上書きする際にも配列全体を上書きするようなことはできないので、

```yml
# config/default.yml
api:
    - https://api1.example.com
    - https://api2.example.com
    - https://api3.example.com
```

```yml
# config/production.yml
api:
    - https://api1.example.com/v2
    - https://api2.example.com/v2
```

というように書いても、3つ目の要素がなくなることはありません。

```yml
api:
    - https://api1.example.com/v2
    - https://api2.example.com/v2
    - https://api3.example.com
```

この配列に関する仕様は望ましいものではありませんので、今後のバージョンアップで変更する予定です。

## 暗号化

設定ファイルを公開鍵暗号方式で保護することができます。設定ファイルには公開鍵で暗号化した暗号文を記述し、実行環境では秘密鍵を用いて復号して読み取ります。公開鍵はその名の通り公開してしまってもいいため、暗号化された設定ファイルとともにGitHubなどに挙げてしまっても安心です。

秘密にしたい設定値を暗号化して設定ファイルに書くことになるわけですが、このとき、設定したいキーに接尾語`.secure`をつけます。例えば`datasource.password`の暗号文は`datasource.password.secure`に設定します。

```yml
datasource:
    url: postgresql://localhost:5432/sample_app
    username: sample_app
    password:
        secure: <Base64エンコードされた暗号文>
```

上述のオブジェクトの仕様により、次のように書くこともできます。

```yml
datasource:
    url: postgresql://localhost:5432/sample_app
    username: sample_app
    password.secure: <Base64エンコードされた暗号文>
```

この仕様はTravis CIの[Encryption keys](https://docs.travis-ci.com/user/encryption-keys/)を参考にしています。

秘密鍵はBase64エンコードして環境変数`NODE_CONFIG_PRIVATE_KEY`に設定します。あとは通常の設定ファイルと同様にnobushi-configをインポートするだけです。

```js
import config from 'nobushi-config';

console.log(config.datasource.password);    // => 復号された設定値
```

`config/development.yml`に暗号化されていない開発用データベースのパスワード、`config/production.yml`に暗号化された本番用のパスワード、というように使えます。

## 環境変数

設定値が文字列で`${}`で囲まれた文字列がある場合、環境変数を展開します。

```yml
server:
    port: ${PORT}
datasource:
    url: ${HEROKU_POSTGRESQL_CRIMSON_URL}
message: Hello, ${USER}
```

## middlewareとfilter

暗号化と環境変数の動作はmiddlewareとfilterという仕組みにより実現しています。筆者の勤務先では、データベースのパスワードやJWTのsecretなどを安全にテスト環境や本番環境に配信する仕組みがあり、公開鍵暗号方式ではなくこちらを使いたいこともあります（安全に配信できる仕組み自体はどのみち公開鍵暗号ですが）。そのような場合には独自にmiddlewareやfilterを定義することもできます。この件は長くなりそうなので、別の機会に書くことにします。

# 今後の展開

まずは配列に関する奇妙な仕様を変更したいです。設定値に配列があった場合、配列はフラットな構造にせず、配列の中にオブジェクトがある場合はさらにそのオブジェクトをフラットな構造にする、というようにすればいい感じになるのではと思っています。

また、コードはTypeScriptで書いているので、型ファイルもnpmにアップロードするようにしたいです。

[^1]: JSONだと「"」で囲まないととか余分な「,」をつけてはいけないとか、JSだと「module.exports」とか「export default」とか、とにかく儀式的なルールが多い。JSで設定を書けるようになっていると設定ファイルにロジックを書く人が出てくるのもよくない。