ある OSS を使いたい。そして、テスト環境と本番環境ではもちろん異なる config ファイルを使う必要がある。
そういった状況での一番単純な方法として、test-config.yaml, prod-config.yaml のように環境毎の設定ファイルを作ることがまず思い浮かぶが、以下のようなデメリットがあると思う。
- (環境間で共通の)設定を変えたい時に、両方のファイルを変更する必要があって面倒くさい
- 手動で変更すると打ち間違えや変更漏れによって異なる設定になってしまう恐れがある
- 意図しない差があるとテスト環境はうまく動くが本番環境で壊れるようなことが起きる
そこで、環境が複数あっても基本的な記述はひとつにまとめ、差がある部分だけを変数として分けることで環境間の差を最小限に保つことはできないかと考えていた。そうすることが出来ればリスクが最小限になる。1
結論としてはタイトルの通り python のテンプレートエンジンである jinja を使うことで簡単に達成できた。例では環境ごとの変数を YAML で書いているが、辞書型であれば何でも渡せるので少し修正すれば JSON や INI でも記述できる。
jinja は単純に変数をレンダリングするだけでなく、条件分岐やマクロ等の複雑な機能を使うこともできて便利。
ちなみに、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
- もちろん本番環境の変数にミスがあれば壊れるため慎重に設定する必要はある。しかし、それ以外の部分についてはテスト環境でテストできていれば心配せずに済む。↩