世界を動かす技術を、日本語で。

地獄のYAMLドキュメント (2023)

概要

  • YAML は人間に優しいデータ形式を目指すが、複雑さが逆効果になる場合が多い
  • バージョンやパーサ依存の挙動、危険な落とし穴が多数存在
  • JSONTOML など、よりシンプルな代替案も複数存在
  • YAMLの微妙な仕様がテンプレート化や構成管理を難しくする
  • 最終的には 安全なサブセット利用JSON生成 が現実的な回避策

YAMLの複雑さと落とし穴

  • YAML は人間に優しいフォーマットを目指すが、仕様が極めて複雑
    • 仕様書は10章以上、4階層の節番号、専用のエラッタページまで存在
    • バージョンごとに挙動が異なり、同じ文書でもパース結果が変化
  • JSON と比較すると、YAMLは仕様も実装も遥かに難解
    • JSONは仕様が6つの図で終わるシンプルさ
    • YAMLは頻繁にバージョンアップされ、1.2.2(2021年10月)でも大きく仕様変更
  • 代表的な落とし穴
    • セクサジェシマル(60進数)表記 :22:22がバージョンによって数値や文字列に
    • タグとエイリアス :!や*記法で予期せぬ解釈やエラー発生
    • ノルウェー問題 :"no"や"on"がバージョンやパーサによってboolや文字列に
    • 非文字列キー :on: などがboolキーとなり、言語によって型が変化
    • 意図しない数値化 :9.6.24のようなバージョンが数値として解釈される危険

YAMLのパーサ依存性と安全性問題

  • パーサごとに バージョン対応状況や解釈が異なる ため、同じYAMLでも動作がバラバラ
    • PythonのPyYAMLは1.1仕様、Goのyamlパッケージは独自実装
  • タグ機能により、 任意コード実行のリスク が存在
    • yaml.safe_loadのような安全な読み込み関数利用が必須
  • シンタックスハイライトに頼っても、各エディタやサービスごとに強調箇所が異なる
    • Vim、GitHub、Codebergなどで一貫性がない

テンプレート化とYAMLの危険性

  • YAMLのテンプレート化は 極めて危険
    • ホワイトスペースや構文の微妙な違いで壊れやすい
    • 断片的なテキストの結合・エスケープが困難
  • KubernetesやGitHub Actionsなどでテンプレート化が多用されるが、 バグや予期せぬ挙動の温床

代替となる構成フォーマット

  • TOML
    • 文字列は常にクォートされ、YAML特有の落とし穴が少ない
    • Python標準ライブラリでサポート、広い言語対応
    • 深いネストにはやや不向き
  • JSON with comments
    • コメント可能な拡張JSON(VS Codeなどで採用)
    • 普及度は限定的だが、シンプルな仕様
  • YAMLの安全なサブセット
    • 全ての文字列をクォート、yes/no等のbool表記を避ける
    • 明示的なルール運用が必要だが、逸脱を自動検出するツールは未発達

JSON生成によるYAML回避

  • 多くのアプリはYAMLしか受け付けないが、 YAMLはJSONのスーパーセット
    • JSONを生成すればYAMLとしても利用可能
  • 構成ファイルが肥大化し、抽象化やパーツ共有が必要になった場合
    • テンプレートよりも NixPython 等のプログラミング言語による生成が安全

まとめ:YAMLの利用と今後

  • YAMLは多機能だが、 仕様の複雑さと落とし穴 が多く、慎重な運用が必須
  • 可能なら TOMLや拡張JSON の利用、YAMLが必須なら 安全なサブセット運用JSON生成 が推奨
  • 長期的には、 より安全な構成管理手法や言語 の選択が望ましい

Hackerたちの意見

ノルウェーの問題、ちょっとイライラするよね。Ansibleのドキュメントでは、yes/noがtrue/falseの代わりに使われてることが多いんだ。公式ドキュメントでこれを見た時、Ansibleの推奨スタイルだと思って使ってたんだけど、最近は警告やリントエラーが出るようになって、見つけたら全部修正してる。でも、Ansibleのドキュメントではまだよく使われてるんだよね。

それは、ファイルをどう解析/デコード/アンマーシャルするかによるね。「一般的な」YAMLパーサーを使うと、noはfalseに変換されるけど、パーサーがデータ構造の型を知ってたり、特定の文字列を置き換えないように指示できたり、フックがあればnoを文字列として扱えるんだ。だから、リンターがパーサーとは違う動作をすることもあるかもね。

Ansibleはドキュメントの金字塔ってわけじゃないよ。ドキュメントは更新されてるし維持されてるけど、基盤となるインターフェースが一貫してないから、それがドキュメントにも影響してる。なぜそうなってるのか不思議だよね、たぶんスタイルガイドなしで異なるアイデアを持った開発者がいるからだろうけど。とはいえ、Ansibleは素晴らしいツールだよ、これらの特異性を許せるならね。

これって本当に過去10年間の問題だったの?確か仕様のバージョン1.2が2009年に修正したはずなんだけど。

これのほとんどは、基本的に文字列を引用符で囲むことで解決できるよ。YAMLには、再帰やアンカー/エイリアス/タグみたいに、JSONではできないことがあるからね。まあ、もしかしたらcue/dhall/hclの方がうまく解決できるかも。Jsonnetもそうだね。どれだけ良いのか、あんまり試したことがないからわからないけど。

ほとんどの問題は、基本的に文字列を引用符で囲むことで解決されるよね。そうそう、私も最初にそう思った。個人的にはYAMLは気にしないけど、文字列を引用する習慣はつけてる。だって、JSONではキーと文字列の両方を引用してるから、YAMLではキー/値ペアごとに約2つのダブルクォートを節約できるってことだし、これが重要な指標ならね。

JSONでは仕様の一部としてやってないけど、ポストプロセッシングとしてやるのを止めるものはないよ。例えば、OpenAPIでは特別な$refキーを使って、ポストプロセッサがそこに参照されている値を入れ替えるんだ。これはjsonnetやcue、hclがやってることと実質的に同じだけど、ポストプロセッサじゃなくてプリプロセッサとしてね。

そうだね、これがデフォルトでyamllintによって強制されてる。 "なんでこんなトリビアルな設定ファイルフォーマットにリンターが必要なんだよ"って叫ぶのはとても公平だし、こういう足元をすくうような問題はYAMLを避ける正当な理由だよ。でも全体的に、YAMLの不安定さは解決しやすい問題だし、YAMLを使う理由があって、リンターを追加するのが可能なコンテキストがあれば、そんなに大したことじゃないと思う。投稿でも示唆されてるけど、実際に確立された普遍的な代替手段はないしね。TOMLは良いデフォルトだけど、かなり単純なものにしか使えない。個人的には「Nixを使えばいい」ってアプローチが好きだけど、どこにでもNixインタープリターを置けるわけじゃないし。Cueはほとんどのユースケースにはオーバースペックだと思う。要するに、結論は「YAMLを使うな」じゃなくて、「YAMLの足元をすくう問題に気をつけて、代替手段を知っておけ」ってことだね。

記事から: > YAMLの問題の多くは、引用されていない文字列のように見えるものが異なる動作をすることから生じている。これは簡単に避けられる: すべての文字列を常に引用すること。

22:22が文字列として引用される必要があるって、俺にはすごく直感に反するな。機能的にはK-Vペアなのに。YAML自体も辞書の構文で:を使ってるし!

この二つの原則、(1) YAMLは引用を必要とすべきで、(2) YAMLの値は再帰/アンカーにあるっていうのが、YAMLの存在理由や人々が使う理由と根本的に逆だと思う。YAMLの特徴は、明示的な開始や - もっと重要なのは - 終了の区切りがない「簡単さ」にあるんだよね。これは、構造のためのホワイトスペースの区切りと、値のためのヒューリスティック解析を組み合わせて実現されてる。後者は根本的に欠陥があるけど、YAMLファンはその欠陥を価値あるトレードオフだと思ってる。もし区切りを必須にするなら、YAMLは存在理由を失うと思う。再帰やアンカーは、ほとんど使われないオプションの追加機能で、一部のパーサーはサポートすらしてない。もしそれがYAMLの主な価値だったら、もっと普及してるはず。ちなみに、俺はYAMLが嫌いで、存在しなければいいのにと思ってるけど、なぜ存在するのかは理解してるし、そのニーズを満たす代替案についてはあまり良い提案がないんだ。TOMLも欠陥があるし。

Jsonnetは結構いいけど、ライブラリのサポートがちょっとイマイチだね。例えば、yaml用の良いライブラリがあって、ラウンドトリップ処理ができるから、プログラム的にyamlを修正してもコメントを保持できるんだ。yamlには確かに欠点もあるし(正直言ってバカみたいなものもあるけど)、いろんな面でうまくいってるから、評価されるべきだと思う。

ADOのパイプラインを書くためにyamlを使わなきゃいけなかった完全な初心者だけど、これらの奇妙なことにはほとんど遭遇しなかったよ。ほとんどのものは引用符で囲まれてたし。

n, no, offの件は悲しいよね。完全に避けられる問題なのに。仕様に入れた人は、賢すぎて逆にバカになっちゃったんじゃないかな。

これがYAMLの問題の基本だよね。誰かがもっと色々追加したくて、作った曖昧さに気づかなかったか、気にしなかったんだろうね。

基本的にオーバーフィッティングみたいに感じる。特定のユースケースを見て、それを追加したんだろうけど、どう一般化するか考えなかったんだよね。で、今ではその素敵なユースケースが、ファイルに時刻フィールドが必要ない人たちを驚かせる代償で不釣り合いにサポートされてる。

ちょっと考えすぎじゃない?

性六進数をサポートするのが良いアイデアだと思った人は、自分のやったことを反省するために、しばらくコンピュータから離れた方がいいよ。

3年前の議論、これが最初に投稿された時のやつだよね: https://news.ycombinator.com/item?id=34351503 , 566ポイント、358コメント

この話題は毎四半期ごとに投稿されてる気がする。

じゃあ、YAMLの良い代替案は何だろう?しばらくはTOMLがいいと思ってたけど、リストをドキュメントのあちこちに広げるやり方が、ちょっと面倒なこともあるんだよね。Dhallはまさに俺好みの型祭りだけど、型システムが思ったほど強くないから、壁にぶつかることもある。

いい答えがあればいいんだけど。Dhall、Nickle、Cue、他にもいくつかに不満があるんだ。Dhallの型システムは、強すぎる(ルーチンFPイディオムをやりたいなら、型変数を手動で設定しなきゃいけない)し、弱すぎる(レコード型であんまりできることがない - 深くネストされたレコードをスウィズルしたり再配置するのがすごく難しい)。それに、文法も解析が難しいんだ。いくつかの候補解析を並行して動かせるパーサーが必要で(古典的な Parser a = Parser (String -> [(a, String)]) 型みたいな)、厄介な構文を明確にするためにね(ファイルパスやURL、レコードアクセス周りかな?忘れちゃった)。これが問題で、パースエラーが全然わかりにくくなるんだ。実際に意図したパースがパーサーに拒否された時、唯一のエラーが「Unexpected ','」だったりすると、どれが本当に意図したものかわからないし。あ、整数同士を掛け算できないのも面倒で、自然数だけなんだよね。もしかしたら、Nixの純粋評価モードがいいかも、変に聞こえるけど。ツールはJSONを受け取って返すのが一番だと思う(例外があるとしたら、古いUNIXスタイルのstdin/stdoutファイルフォーマットに十分シンプルなツールかな)。いつか誰かがJSONの上に良い関数型抽象を考え出すだろうし、それまではDhall、YAML、他の何かでなんとかやっていけると思うよ。

textprotoはどう?プロトコル定義がスキーマを提供してくれるよ。

まだ誰もHashiCorpのHCLについて言及してないけど、あれがあまり注目されなかったのは本当に残念だよね…

KDL(https://kdl.dev/)やPkl(https://pkl-lang.org/)はどう?

記事には「yamlの簡単なサブセット」と書いてあって、すでに存在するStrictYAMLのことを指してるね。文字列、リスト、辞書だけで、数字もブール値もない。_countries_もないし、アンカーもなし。JSON互換のブロックもない。だから、基本的には私たちが考える「ちゃんとしたYAML」で、余計なものはないってこと。必要なところで自分のスキーマと型を持ち込めばいいだけだよ。 https://hitchdev.com/strictyaml/

YAMLが設定ファイルの定番になったのは驚きだよね。解析の罠や、一貫性のない挙動があって、経験豊富な開発者でも引っかかることがあるのに。

他の設定フォーマットはあまり表現力がないからだよね。

それに、インデントでネストされたものを表現するビジュアルフォーマットがみんなに好まれてるのがすごいと思う。これがPythonが人気になった主な理由でもあるはず。

たいていの場合、これが一番マシな選択肢だよ。JSONはコンピュータ用だし、手で書いたり編集したりするのはあんまり良くない。エスケープするのも面倒だし、シンプルなマルチラインの文字列とかになるとすごく扱いづらくなる。XMLは逆に行き過ぎてて、手で書くのがめっちゃ面倒くさい。エスケープもイライラすることがあるし、さまざまな言語で簡単に表現できないデータ構造を表現できることも多い。INIは仕様がないからダメだし、ネストしたデータにも向いてない。TOMLは本質的により良いINIファイルを指定することでこれを解決してる。INIファイルと同じように、実際のネストレベルになると崩れちゃうけどね。EverythingElseはあまりサポートされてない。基本的な設定とか人間が扱う必要があるものに関しては、だいたいK=V形式から始めることが多い。どんな言語でも「パーサー」を書くのは大体1分くらいで、依存関係もないから簡単にできる。使い方がそれを超えると(引用、明示的な型、複数行、エスケープとか)YAMLに移行するだけ。ベストではないけど、手に入りやすくて、俺の視点から見ると一番マシだよ。

個人的には、JSON、YAML、TOMLはすべてのキーを文字列として解釈すべきだと思うし、文法的に必要なときだけ引用を強制すればいいと思う。だから、key1は文字列で引用は不要。12345もキーとしては文字列として解釈されるから、引用はいらない。"key 1"はスペースがあるから、引用が必要だね。

仕様を変えて、コアライブラリも全部変更しないといけないよ。大変な作業だね。もっと引用を使って、yamllintを使おう。bashみたいに、もっと引用を使ってshellcheckも。

IMOの設定ってどんな感じ?

誰かがもっとシンプルなYAMLのためにコードを改良したいなら、昔書いたRethinkDBのテスト用のバージョンがあるよ。https://github.com/rethinkdb/rethinkdb/blob/main/test/common... 俺が解決しようとしてたのは、テストが辞書みたいなもの(実際にそうだったんだけど)をたくさん含んでたから、YAMLっぽいパーサーがテストコードにぶつかると解析を止めちゃうことだった。これでエスケープがかなり減って、テストをREPLにコピー&ペーストするのが楽になったんだ(その逆もね)。見た目はYAMLだけど、ほとんどの機能がなくて、足元をすくわれることもない。

JSONのキーは常に文字列だけど、YAMLではブーリアンを含むどんな値でも使えるんだ。今日はYAMLとJSONが同じデータモデルを持ってないことを学んだし、JSONでは表現できないYAMLドキュメントもあるんだね…

JSONのさまざまな拡張があって、複雑さをあまり増やさずに使える設定フォーマットにしているものがあるよ。コメント付きのJSONが一番広まってると思うけど、Visual Studio Codeの設定フォーマットとして使われているからね。これらの主な欠点は、まだ本格的に普及していないから、JSONやYAMLほど広くサポートされてないことだ。驚いたのは、JSONの文法全体がYAMLの文法のサブセットとして含まれていることを知ったこと。だから、すべての有効なJSONドキュメントは自動的に有効なYAMLドキュメントになるんだ。でも、そこで止まる必要はないよ。JSONの文法要素を追加の「ちゃんとしたYAML」のものと組み合わせることもできるし、コメントも含めてね。つまり、YAML設定を受け入れるソフトウェアは、代わりにJSONやコメント付きJSONとしても設定を受け入れられるってこと。エコシステムのブートストラップも必要ないし!(*ほとんどのソフトウェアが、非文字列キーの辞書を使わない限り大丈夫だよ)