SpringBoot のフレームワークを使って開発をしていると、application.yaml に設定値を抜き出して利用することが多いと思います。
しかし、この設定値を環境ごとに変えたい場合、設定ファイルの運用方法については意見がわかれます。
環境ごとというのは、「ローカル環境」や「開発環境」「本番環境」などのことです。
今回は環境ごとの運用方法については大きく触れませんが、yaml ファイル内のプレースホルダの「外部設定値」と「デフォルト値」を抜き出したいケースに遭遇したのでまとめたいと思います。
yamlファイルの運用方法
私が経験したことのある運用方法は大きく 3 つ。
・1つのyamlファイルで全環境の設定を持つ
・ローカル環境のみ application-local.yaml の別ファイルを用意する
・環境ごとに application-{hoge}.yaml の別ファイルを用意する
Spring では、profile を指定すれば application.yaml の他に、環境ごとの yaml ファイルの設定値を上書きして読み込んでくれます。
どちらにしても、すべての設定値をベタ書きすることは稀で、各環境の env などから設定値を埋め込む場合が多いと思われます。
マイクロサービスも一般的な時代ですし、クラウド上のコンテナサービスを利用する場合は環境変数に頼る機会に必ず遭遇するでしょう。
ちなみに私は、2 番目に書いたローカル環境だけ別のファイルを用意するのが好みですが、中には 1 ファイルで済ませたいという人もいるので、ここは最初に決めが大事ですね。
結局、今回は 1 番目のパターンでやることになったのですが、どのパターンもメリット・デメリットがありますので、ここについては実際に運用してみてやりやすい方法を選択したらいいと思います。
1ファイルで運用する方法と課題
さて、1 ファイルで運用する場合、まず問題となるのはローカル開発で使用する値をどこで管理するかです。
クラウド上まで展開してしまえば、各サーバの環境変数の設定さえ間違えなければ管理はしやすいです。
しかし、ローカルとなると話は別。
マイクロサービスの増加で、扱うプロジェクトが増えると、環境変数の管理だけでごちゃごちゃします。
だから application-local.yaml のようなファイルを用意したかったのですが、文句ばかり言っていても仕方ありません。
そこで登場した案が、プレースホルダの「外部設定値」にデフォルト値を持たせて、ローカルではデフォルト値を使うというものです。
仮に、DB と Redis サーバの設定を書いた yaml だとこんなイメージです。
1 2 3 4 5 | db: user: ${db.user:admin} password: ${db.password:password} redis: host: master: ${redis.host.master:https://example.org/master}, slave: ${redis.host.slave:https://example.org/slave} |
The values in application.properties are filtered through the existing Environment when they are used, so you can refer back to previously defined values (for example, from System properties).
デフォルト値はコロンで区切ると指定でき、コロンで区切った 2 番目以降はすべてデフォルト値として扱われます。
上記の yaml をパースすると以下の通りになります。
外部変数名 | デフォルト値 |
---|---|
db.user | admin |
db.password | password |
redis.host.master | https://example.org/master |
redis.host.slave | https://example.org/slave |
さて、ここで課題が 1 つ見つかります。
ローカルで開発しているだけならいいのですが、開発環境や本番環境ではこれらの外部設定値を環境変数で上書きする必要があります。
別に大した問題ではないのですが、環境変数が抜けているとデフォルト値が使われてしまいますし、正しく値を置き換えられているか確認しないと不安ですよね。
ECSのタスク定義に環境変数を設定する
では、AWS の ECS(Amazon Elastic Container Service)で設定するタスク定義を JSON 形式で準備するという想定でやってみましょう。
定義したい内容はこんなイメージですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | "environment": [ { "name": "DB_USER", "value": "admin" } ,{ "name": "DB_PASSWORD", "value": "password" } ,{ "name": "REDIS_HOST_MASTER", "value": "https://example.org/master" } ,{ "name": "REDIS_HOST_SLAVE", "value": "https://example.org/slave" } ] |
ホスト名やパスワードを間違えたくらいなら、コンテナ起動後にアプリがエラーとなって気付きますが、これが最大コネクション数などパフォーマンスに影響する部分なら、気付くのが遅れて致命的な問題につながります。
また、ログレベルを本番では ERROR にしたかったのに、間違って DEBUG のままになっていると CloudWatch や外部の監視サービスに大量のトラフィックが発生し、AWS の費用も一気に跳ね上がってしまいます。
別の意味で怒られるパターンですね・・・。
よって、地道なチェックが重要となるわけなのですが、yaml の外部設定値とタスク定義を見比べるのはちょっと面倒です。
そこで yaml をパースして、この JSON のベースを作ってから各環境の値を設定するとか、ベースと現在の環境の設定値を比較すれば多少はチェックとして機能しそうです。
要は、以下の 2 点さえ気を付ければ大事にはならないと思われます。
・変数名が間違っていない
・値が間違っていない
yamlファイルをパースしてJSONを作成
yaml ファイルのパースはシェルでもいいのですが、1 行に複数のプレースホルダがあると面倒ですよね。
やろうと思えばできますが、サクっとは無理な感じがします。
Mac だと bash3 なので bash4 の機能は使えず、grep、sed、tr、awk などを駆使してやる感じでしょうか。
まあ、実際に途中までやって疲弊したので投げ出したのですけどね・・・。
そこで、Python や PHP や Perl が思い浮かぶのですが、今回は最近ご無沙汰な PHP にしてみます。
人によっては、Ruby がいいとか、Go がいいとか、バラバラだとは思いますが、私の場合は PHP の雑なスクリプトなら 10 分くらいでいけるハズ。
では、ご無沙汰な PHP の強引なパース処理を紹介します。即席とはいえ本当にヒドいソースだ・・・。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | <?php $yaml = file_get_contents('./application.yaml'); $line = explode(PHP_EOL, $yaml); $values = []; foreach ($line as $l) { $match_num = preg_match_all('/\$\{(.+?)\}/i', $l, $target); if (!empty($match_num)) { foreach ($target[1] as $t) { $values[] = $t; } } } if (empty($values)) { echo "no placeholder value." . PHP_EOL; die; } $value_list = []; foreach ($values as $v) { $params = explode(':', $v); $json_name = str_replace('.', '_', strtoupper($params[0])); if (array_key_exists($json_name, $value_list)) { continue; } array_shift($params); $value_list["$json_name"] = implode(':', $params); } $sep = ''; $json = ''; foreach ($value_list as $key => $val) { $json .= <<< EOL {$sep}{ "name": "{$key}", "value": "{$val}" } EOL; $sep = ','; } echo sprintf('"environment": [%s%s%s]', PHP_EOL, $json, PHP_EOL); |
細かな改善の余地は多々ありますが、今回の目的はチェックが最優先なのでこのくらいで十分でしょう。
時間を掛けてまでこだわる部分でもありませんしね。
実行結果はこんな感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | "environment": [ { "name": "DB_USER", "value": "admin" },{ "name": "DB_PASSWORD", "value": "password" },{ "name": "REDIS_HOST_MASTER", "value": "https://example.org/master" },{ "name": "REDIS_HOST_SLAVE", "value": "https://example.org/slave" } ] |
あとは、環境ごとに value の中身を正しく設定してあげれば OK です。
また、項目に変更が生じた場合は、お互いの設定値を比較するなどチェックを怠らないようにしておきましょう。
まとめ
AWS の ECS や EKS、GCP など、これらのコンテナのマネジメントサービスは便利ですが、さらにこれらの設定を Terraform で管理しようとなると疲弊します。
慣れてくれば Terraform も確かに便利なのですが、どこまで仕組みに任せるか悩ましいところではあります。
それは教育コストもありますが、まずは画面レベルでの設定や、そもそもの ECS の仕組みなどの理解をして欲しいからです。
いきなり仕組みが Terraform で用意されていると、初めて触る人はパニックになっちゃいますよね。
応用すれば、Kubernetes(EKS)の config-map.yaml や deployment.yaml などの動的な変数も定義できそうです。
たまには、こんなちょっとしたチェックツールを作ってみるのもいいですね。