最近スプラトゥーン 3 をやりこんでいて、サーモンランでカンスト(※)を達成しました todacchi です。
ゲーム内の貴重なバッジを獲得し、人生の貴重な時間を失いました。
(※) カンスト: ゲーム内の評価をでんせつ 999 まで上げること。
はじめに
Ruby on Rails を用いた Web アプリケーション開発などでは、YAML ファイルを設定ファイルとして採用し、環境ごとに切り替えるような運用をすることがあります。 しかし、まれに本番環境の部分だけ書き間違えてしまうといったことがあります。 そうすると、あえて本番環境の値を使わせるようなテストを意図的に書かない限りテストが落ちないですし、YAML の文法的には正しい場合もあるのでミスに気付きづらいです。 その結果、テストやステージング環境では正常に動作していても、本番環境にリリースすると障害が発生してしまいます。
例えば以下のような YAML ファイルがあるとします。
default: &default
api:
id: "api_id"
key: "api_key"
development:
<<: *default
test:
<<: *default
integration:
<<: *default
production:
<<: *default
api:
id: "production_api_id"
key: <%= ENV['API_KEY'] %>
この YAML ファイルを修正する時に production 内のインデントを間違えてしまったとしましょう。
default: &default
api:
id: "api_id"
key: "api_key"
development:
<<: *default
test:
<<: *default
integration:
<<: *default
production:
<<: *default
api:
id: "production_api_id"
key: <%= ENV['API_KEY'] %>
このとき、["production"]["api"]["key"]
はnil
になってしまい、本番環境でこの値を使ってるコードがエラーになります。
hash に変換すると以下のような感じで nil になるのは当然ですね。
irb> y = <<~EOT
development: &default
api:
id: "api_id"
key: "api_key"
test:
<<: *default
integration:
<<: *default
production:
<<: *default
api:
id: "production_api_id"
key: "api_key_from_env"
EOT
irb> h = YAML.safe_load(y, aliases: true)
=>
{
"development"=>{"api"=>{"id"=>"api_id", "key"=>"api_key"}},
"test"=>{"api"=>{"id"=>"api_id", "key"=>"api_key"}},
"integration"=>{"api"=>{"id"=>"api_id", "key"=>"api_key"}},
"production"=>{
"api"=>{"id"=>"production_api_id"},
"key"=>"api_key_from_env"
}
}
irb> h['development']['api']['key']
=> "api_key"
irb> h['production']['api']['key']
=> nil
もちろん、コーディング中やレビュー時にミスのないように注意しますが、実務で作る YAML ファイルはたびたび複雑だったり巨大だったりするので先ほどの例のようにわかりやすくはありません。 また、人間がこういったミスがないかを確認する作業はとても生産的とは言えません。 ケアレスミスで起きる障害の芽は減らしていくべきでしょう。
そこで環境ごとに key の構造が同じかどうかを機械的に検証するコードを Gem にしてプロダクトに導入したので、その紹介をします。
作成した Gem
yosipy / yaml_structure_checker
簡単に説明すると key の構造が環境ごとに一致するか検証することができます。 Ruby on Rails を使ったプロジェクトの設定ファイルに対して使用できますが、それ以外のプロジェクトにおいても(root の key が環境名である)同じような YAML ファイルに対して使用することができます。 (もし「こういった形式の YAML ファイルに対しても使用したい」という意見がありましたら Issue を立ててもらえると対応できるかもしれません)
こういった YAML ファイルを検証するツールとして Kwalify のようにスキーマを定義して YAML ファイルをチェックする方法もあり、細かく YAML の構造をチェックできますがスキーマの管理に労力がかかります。 YAML structure checker では「環境ごとの YAML の構造の差異」に焦点を絞ることで、わずかな設定だけでチェックを行えるようにしました。
導入方法
Gemfile に以下を追加して$ bundle install
します。
gem 'yaml_structure_checker'
config/yaml_structure_checker.yml
を作成して検証対象とする path や除外する path などを指定します。
# config/yaml_structure_checker.yml
include_patterns:
- config/**/*.yml
exclude_patterns:
- config/locales/**/*.yml
envs:
- development
- test
- integration
- production
skip_paths:
- config/gcp.yml
最後に$ bundle exec yaml_structure_checker
を実行するだけです。
$ bundle exec yaml_structure_checker
#################################
# YAML Structure Check #
#################################
Exclude paths:
spec/fixtures/checker/target/exclude/example.yml
spec/fixtures/checker/target/failed/alias.yml
spec/fixtures/checker/target/failed/diff.yml
Skip paths:
spec/fixtures/checker/target/skip/example.yml
# spec/fixtures/checker/target/success/alias.yml
Result: OK
# spec/fixtures/checker/target/success/same.yml
Result: OK
#################################
# YAML Structure Check Result #
#################################
NG paths:
nothing
Total count: 6
Exclude count: 3
Skip count: 1
OK count: 2
NG count: 0
より詳しい説明はReadmeをご覧ください。
ソースコードの解説
せっかくなので、コアな処理に絞って解説していきます。
Converter.hash_to_nested_keys
Hash に変換された YAML ファイルを引数に取り、Hash の key の親子関係を配列に変換していく再起メソッドです。 すべての再帰処理が終わったら全体を配列として返します。
引数の Hash の key 一覧を順に読み込んで、その値が Hash クラスであれば再起処理を行い、それ以外の場合は現在の key が一番深い key なので祖先の key 一覧に追加して返しています。
def self.hash_to_nested_keys(
hash,
nested_keys = [],
key_ancestors = []
)
keys = hash.keys
keys.each do |key|
value = hash[key]
child_key_ancestors = nested_key = key_ancestors.clone.push(key)
if value.class == Hash
hash_to_nested_keys(value, nested_keys, child_key_ancestors)
else
nested_keys.push(nested_key)
end
end
nested_keys
end
言葉で説明するとややこしいですが、ソースコードのコメントに書いてあるように以下のような変換を行います。
# args:
# hash = {
# 'development' => { 'x' => 1, 'y' => 2, 'z' => 3 },
# 'test' => {
# 'p' => { 'q' => { 'r' => 4 } },
# 's' => { 't' => { 'u' => 5 } },
# },
# 'production' => 6,
# }
# return:
# [
# ['development', 'x'],
# ['development', 'y'],
# ['development', 'z'],
# ['test', 'p', 'q', 'r'],
# ['test', 's', 't', 'u'],
# ['production']
# ]
Converter.nested_keys_to_nested_keys_each_env
hash_to_nested_keys
で変換された一覧を環境ごとに分類します。
指定した環境以外の最上位 key があれば無視するようにしています。
def self.nested_keys_to_nested_keys_each_env(nested_keys, envs)
nested_keys_each_env = {}
envs.each do |env|
nested_keys_each_env[env] = []
end
nested_keys.each do |nested_key|
nested_key = nested_key.clone
env = nested_key.first
nested_key_without_env = nested_key.drop(1)
if envs.include?(env)
nested_keys_each_env[env].push(nested_key_without_env)
end
end
nested_keys_each_env
end
# args:
# nested_keys = [
# ['development', 'x'],
# ['development', 'y'],
# ['development', 'z'],
# ['test', 'p', 'q', 'r'],
# ['test', 's', 't', 'u'],
# ['production']
# ]
# envs = ['development', 'test', 'production']
# return:
# {
# 'development' => [['x'], ['y'], ['z']],
# 'test' => [['p', 'q', 'r'], ['s', 't', 'u']],
# 'production' => [[]],
# }
Checker#compare
環境ごとに key の構造に差分がないか確認します。
Checker#test_yaml
YAML ファイルごとに検証を行います。
Checker#test_yamls
複数の YAML ファイルの検証を行うメソッドです。 構造チェックに失敗した YAML ファイルが 1 つでもあると例外を発生させます。
CLI コマンドとして実行できるようにする
Gem 開発の際、拡張子のないファイルを exe/におくことで作成できます。
yosipy/yaml_structure_checker/blob/main/exe/yaml_structure_checker
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'yaml_structure_checker'
if !ARGV.empty?
settings_path = ARGV.first
end
YAMLStructureChecker::Runner.invoke(settings_path)
このファイルを読み込む処理は gemspec ファイルに記載されてます。
# frozen_string_literal: true
require_relative "lib/yaml_structure_checker/version"
Gem::Specification.new do |spec|
~ 省略 ~
spec.bindir = "exe"
spec.executables = %w[yaml_structure_checker]
~ 省略 ~
$ bundle exec yaml_structure_checker arg1 arg2
のように実行するとARGV
に['arg1', 'arg2']
のように渡ります。
CI で自動検証する
GitHub Actions を用いて Push されるたびに自動的に検証されるようにします。 コンテナ image はプロジェクトに合ったやつを使ってください。
bundle exec yaml_structure_checker
を実行するだけです。
jobs:
yaml_structure_checker:
runs-on: ubuntu-latest
container:
image: ruby:3.2.2
steps:
- uses: actions/checkout@v3
- name: Install gems
run: bundle install --path=vendor/bundle --jobs 4 --retry 3
- name: Test
run: bundle exec yaml_structure_checker
動いてますね 😉
まとめ
YAML ファイルの key 構造を検証する Gem の紹介を行いました。 小さな改善ですが、開発の安心感は増したと思います。
最近は Gem 開発がちょっとしたマイブームとなっていて、思っていたより簡単に作れるのでぜひ手を出してみてください。
余談ですが、リポジトリの Readme の Top に貼ってる画像をこの記事のサムネにしていて、結構気に入ってたりします。