Buri Memo:

アイデアや気づきとかが雑に書き殴られる

PythonのJinjaを使って環境毎の設定をシンプルに管理したい

ある OSS を使いたい。そして、テスト環境と本番環境ではもちろん異なる config ファイルを使う必要がある。

そういった状況での一番単純な方法として、test-config.yaml, prod-config.yaml のように環境毎の設定ファイルを作ることがまず思い浮かぶが、以下のようなデメリットがあると思う。

  • (環境間で共通の)設定を変えたい時に、両方のファイルを変更する必要があって面倒くさい
  • 手動で変更すると打ち間違えや変更漏れによって異なる設定になってしまう恐れがある
  • 意図しない差があるとテスト環境はうまく動くが本番環境で壊れるようなことが起きる

そこで、環境が複数あっても基本的な記述はひとつにまとめ、差がある部分だけを変数として分けることで環境間の差を最小限に保つことはできないかと考えていた。そうすることが出来ればリスクが最小限になる。1

結論としてはタイトルの通り python のテンプレートエンジンである jinja を使うことで簡単に達成できた。例では環境ごとの変数を YAML で書いているが、辞書型であれば何でも渡せるので少し修正すれば JSON や INI でも記述できる。

jinja は単純に変数をレンダリングするだけでなく、条件分岐やマクロ等の複雑な機能を使うこともできて便利。

jinja.palletsprojects.com

ちなみに、undefined=StrictUndefined を設定すれば、必要な変数が足りてない場合にエラーになるため設定ミスに早期で気づくことができる(自動ビルドに組み込んだり、CI 化も簡単)。

environment: broken

admin:
    p0rt: 9090  # port --> p0rt とタイポした
$ python render.py 
Traceback (most recent call last):
  ... 中略 ..
jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'port'

サンプルコード

スクリプト

import yaml
import os
from jinja2 import Environment, FileSystemLoader, StrictUndefined

VARS_DIR = "./vars/"
OUTPUT_DIR  = "./output/"

# undefined=StrictUndefined に設定するとテンプレートが求める変数が存在しない場合に失敗させられる
env = Environment(loader=FileSystemLoader("./"), undefined=StrictUndefined)
template = env.get_template("./config.yaml.jinja2")

class RenderVars:
    def __init__(self, var_file, output):
        self.var_file = var_file
        self.output = output

render_vars = [
    RenderVars(var_file="test.yaml", output="test-config.yaml"),
    RenderVars(var_file="prod.yaml", output="prod-config.yaml"),
]

os.makedirs(OUTPUT_DIR, exist_ok=True)
for render_var in render_vars:
    var_path = VARS_DIR + render_var.var_file
    output_path = OUTPUT_DIR + render_var.output

    # 変数をファイルから読み出しレンダリングして、設定ファイルとして書き出す
    with open(var_path) as f:
        vars_dict = yaml.safe_load(f.read())
        template.stream(vars_dict).dump(output_path)

テンプレート

# environment: {{ environment }}

admin:
  address:
    socket_address:
      address: 127.0.0.1
      port_value: {{ admin.port }}

環境ごとの変数ファイル

environment: test

admin:
    port: 8080
environment: prod

admin:
    port: 80

実行結果

# environment: test

admin:
  address:
    socket_address:
      address: 127.0.0.1
      port_value: 8080
# environment: prod

admin:
  address:
    socket_address:
      address: 127.0.0.1
      port_value: 80

  1. もちろん本番環境の変数にミスがあれば壊れるため慎重に設定する必要はある。しかし、それ以外の部分についてはテスト環境でテストできていれば心配せずに済む。