Chapter 15 - Configuration Specifics, the second half

SRE Workbookの15章の後半を学習目的で翻訳。

カスタムアプリケーションの統合（社内ソフトウェア）

　インフラストラクチャがカスタムアプリケーション（つまり、市販のソリューションではなく、社内で開発されたソフトウェア）を使用している場合は、それらのアプリケーションを再利用可能な設定言語と共存するように設計できます。
このセクションの提案により、設定ファイルを書いたり生成された設定データと対話したりするとき（例えばデバッグ目的で、あるいは他のツールと統合するとき）の全体的なユーザ設定経験を改善します。
また、アプリケーションの設計を簡素化し、データから設定を分離するでしょう。

カスタムアプリケーションにアプローチするための大まかな戦略は、次のとおりです。
・設定言語に、それが設計されているもの、つまり問題の言語的側面を処理させる。
・アプリケーションに他のすべての機能を処理させます。


次のベストプラクティスにはJsonnetを使用した例が含まれていますが、
他の言語にも同じ推奨事項が適用されます。


・単一の純粋なデータファイルを消費し、設定言語がインポートを使用して設定をファイルに分割するようにします。つまり、設定言語の実装は単一のファイルを発行するだけで済みます（そしてアプリケーションは単一のファイルを消費するだけで済みます）。
また、アプリケーションはファイルをさまざまな方法で組み合わせることができるので、
この方法ではファイルを組み合わせてアプリケーション設定を形成する方法を明確かつ明確に示しています。
・オブジェクトを使用して名前付きエンティティのコレクションを表します。
フィールドにはオブジェクト名が含まれ、値にはエンティティの残りの部分が含まれます。
各要素に名前フィールドがあるオブジェクトの配列を使用しないでください。

Bad JSON:
[
   { "name": "cat", ... },
   { "name": "dog", ... }
]
   
Good JSON:
{
   "cat": { ... },
   "dog": { ... }
}



   この戦略はコレクション（そして個々の動物）の拡張をより簡単にします、そして不安定なインデックス（例えばanimals[0]）を参照する代わりに名前（例えばanimals.cat）でエンティティーを参照することができます。


・トップレベルでエンティティーをタイプ別にグループ化しないでください。論理的に関連する設定が同じサブツリーにグループ化されるようにJSONを設定します。これにより、（設定言語レベルでの）機能的な境界に沿った抽象化が可能になります。
Bad JSON:
{
   "pots": { "pot1": { ... }, "pot2": { ... } },
   "lids": { "lid1": { ... }, "lid2": { ... } }
}
   
Good JSON:
{
   "pot_assembly1": { "pot": { ... }, "lid": { ... } },
   "pot_assembly2": { "pot": { ... }, "lid": { ... } }
}
   
 設定言語レベルでは、この戦略によって次のような抽象化が可能になります：
  
local kitchen = import 'kitchen.libsonnet';
{
   pot_assembly1: kitchen.CrockPot,
   pot_assembly2: kitchen.SaucePan { pot+: { color: 'red' } },
}
通常、データ表現の設計は単純にしてください。
-データ表現に言語機能を埋め込むことは避けてください（[「落とし穴1：プログラミング言語の問題として設定を認識できない](https://landing.google.com/sre/workbook/chapters/configuration-specifics/#pitfall-1-failing-to-recognize-configuration-as-a-programming-language-problem)
で説明したように）。
-これらのタイプの抽象化はユーザーに抽象化の機能をデータ表現または設定言語に使用するかの決定を強いる為、威力を失い、混乱を招くだけです。
-過度に冗長なデータ表現について心配しないでください。
冗長性を減らすための解決策は複雑さをもたらし、問題は設定言語で管理することができます。
-アプリケーションで、条件付き文字列やプレースホルダ参照などのカスタム文字列補間構文を解釈しないでください。
純粋なデータバージョンの設定が生成された後に実行されるアクション（アラート、ハンドラーなど）を記述する必要がある場合など、解釈が避けられないことがあります。
それ以外の場合は、設定言語ができるだけ多くの言語レベルの作業を行えるようにします。

前述のように、設定を完全に削除できる場合は、それを行うことが常に最善の選択肢です。設定言語はデフォルト値を持つテンプレートを使用することによって基礎となるモデルの複雑さを隠すことができますが、
生成された設定データは(ツールによる処理、人間による検査、設定データベースへのロードなどは)完全に隠されるわけではありません。
同じ理由で、基になるモデルの矛盾する命名、複数形、または間違いを修正するために設定言語に頼らないでください - それらはモデル自体の中で修正してください。
モデル内の矛盾を修正できない場合は、それ以上の矛盾を回避するために言語レベルでそれらと同居されることをお勧めします。
私たちの経験では、システムの中で、設定の変更が時間の経過とともに停止の根本原因を支配する傾向があります（[Appendix C ポストモーテム分析の結果](https://landing.google.com/sre/workbook/chapters/postmortem-analysis/)
にて、停止の主な原因の一覧を参照ください）。
設定の変更を検証することは、信頼性を維持するための重要なステップです。
設定の実行直後に生成された設定データを検証することをお勧めします。構文検証のみ（つまりJSONが解析可能かどうかのチェック）では多くのバグは見つかりません。
一般的なスキーマ検証の後、アプリケーションのドメインに固有のプロパティ、たとえば、必須フィールドが存在するか、参照されているファイル名が存在するか、および指定された値が許容範囲内であるかなどを確認します。

JsonnetのJSONを[JSONschema](https://json-schema.org/)で検証する事が出来ます。
[プロトコルバッファ](https://developers.google.com/protocol-buffers/docs/proto3)を使用するアプリケーションの場合、Jsonnetからこれらのバッファの正規のJSON形式を簡単に生成できます。
プロトコルバッファの実装はデシリアライゼーション中に検証されます。

どのように検証することにしたとしても、認識されないフィールド名を無視しないでください。これらは設定言語レベルでの誤字を示す可能性があります。
Jsonnetは::構文を使って出力すべきでないフィールドをマスクすることができます。
プリコミットフックで同じ検証を実行することもお勧めです。

設定システムを効果的に運用する

どのような言語でも「設定をコードとして」実装する場合は、ソフトウェア工学全般を支援する分野とプロセスに従うことをお勧めします。

バージョニング

設定言語は通常、エンジニアにテンプレートとユーティリティ関数のライブラリーを書くように促します。
多くの場合、1つのチームがこれらのライブラリを保守していますが、
他の多くのチームがそれらのライブラリを使用する可能性があります。
ライブラリに重大な変更を加える必要があるときは、2つの選択肢があります。
・すべてのクライアントコードのグローバルアップデートをコミットし、
それでも機能するようにコードをリファクタリングします（これは組織的に不可能な場合があります）。
・異なる消費者が異なるバージョンを使用し、独立して移行できるようにライブラリをバージョン管理します。
廃止予定のバージョンを使用することを選択した消費者は、
新しいバージョンの恩恵を受けることはできず、技術的な債務を負うことになります - いつか、彼らは新しいライブラリを使用するためにコードをリファクタリングしなければなりません。

Jsonnetを含むほとんどの言語は、バージョン管理に対して特別なサポートを提供していません。代わりに、簡単にディレクトリを使用できます。
Jsonnetでの実用的な例については、バージョンがインポートされたパスの最初のコンポーネントである[ksonnet-libリポジトリ](https://github.com/ksonnet/ksonnet-lib)をご覧下さい。

local k = import 'ksonnet.beta.2/k.libsonnet';

ソース管理

[14章のConfiguration Design and Best Practices](https://landing.google.com/sre/workbook/chapters/configuration-design/)では、設定変更の履歴記録（変更者を含む）を保持し、
ロールバックが簡単で信頼性の高い事を担保する様に推奨しています。
設定をソース管理にチェックインすると、これらすべての機能に加えて、設定の変更をコードレビューする機能がもたらされます。

ツーリング

どのようにスタイルを強制して設定を変更するかを検討し、これらのツールをワークフローに統合するエディタプラグインがあるかどうかを調べます。
ここでの目標は、すべての作者に渡って一貫したスタイルを維持し、
読みやすさを向上させ、エラーを検出することです。
一部のエディタはー、フォーマッタやその他の外部ツールを実行できるポストライトフックをサポートしています。
チェックインした設定が高品質であることを保証するためにprecommitフックを使って同じツールを実行することもできます。

テスト

上流のテンプレートライブラリに対してユニットテストを実装することをお勧めします。
さまざまな方法でインスタンス化したときに、ライブラリーが期待される明確な設定を生成するようにしてください。
同様に、関数のライブラリには単体テストを含める必要があります。
そうすれば、それらを確実に維持することができます。
Jsonnetでは、Jsonnetファイルとしてテストを書くことができます。

1 .テストするライブラリーをインポートする。
2. ライブラリーを実行する。
3. 出力を検証 するには、assertステートメントまたは標準ライブラリassertEqual関数を使用する。
後者は、エラーメッセージに不一致の値がある場合に表示します。
次の例では、joinName関数とテストしますMyTemplate：


// utils_test.jsonnet
local utils = import 'utils.libsonnet';
std.assertEqual(utils.joinName(['foo', 'bar']), 'foo-bar') &&
std.assertEqual(utils.MyTemplate { tier: 'frontend' }, { ... })

大規模なテスト一式の場合は、[Jsonnetコミュニティメンバーによって開発されたより包括的なユニットテストフレームワーク](https://github.com/yugui/jsonnetunit)を利用できます。
このフレームワークを使用すると、一連のテストを体系的に定義して実行できます。
たとえば、最初の失敗したアサーションで実行を中止する代わりに、
失敗したすべてのテストのセットをレポートできます。

設定を評価する場合

私たちの重要な特性は気密性を含みます。
つまり、設定言語は、実行場所やタイミングに関係なく、同じ設定データを生成する必要があります。
[14章のConfiguration Design and Best Practices](https://landing.google.com/sre/workbook/chapters/configuration-design/)で説明されているように、
システムが完全な環境の外で変更される可能性のあるリソースに依存している場合、
システムをロールバックするのは困難または不可能です。
一般に、気密性とは、Jsonnetコードがそれが表す拡張されたJSONと常に交換可能であることを意味します。したがって、Jsonnetが更新されてからJSONが必要になるまでの間（JSONが必要になるたびに）、いつでもJsonnetからJSONを生成できます。

設定をバージョン管理して保存することをお勧めします。
その後、設定を検証するための最も早い機会はチェックイン前です。
一方で、アプリケーションはJSONデータが必要なときに設定を評価できます。
中道の選択肢として、ビルド時に評価できます。
これらの各オプションにはさまざまなトレードオフがあります。
ユースケースの詳細に従って最適化する必要があります。

ごく初期の評価：JSONをチェックインする

両方をバージョン管理にチェックインする前に、
JsonnetコードからJSONを生成できます。一般的なワークフローは次のとおりです。

1.Jsonnetファイルを変更する。
2.JSONファイルを再生成する為に、Jsonnetコマンドラインツール（おそらくスクリプトにラップ）を実行する。
3. JsonnetコードとJSON出力が常に一貫していることを確認為に、プリコミットフックを使用する。
4. コードレビュー用にすべてをpull requestにまとめる。

長所
・レビュー担当者は具体的な変更を健全性チェックすることができます。
たとえば、リファクタリングは生成されたJSONにまったく影響を与えないはずです。
・生成されたレベルと抽象化されたレベルの両方で、異なるバージョンにわたる複数の作者による行注釈を調べることができます。これは変更を監査するのに役立ちます。
・実行時にJsonnetを実行する必要はありません。
これは、複雑さ、バイナリサイズ、リスクの危険性を制限するのに役立ちます。

短所
・生成されたJSONは必ずしも読み取り可能とは限りません。
たとえば、長い文字列が埋め込まれている場合などです。
・JSONは、他の理由でバージョン管理にチェックインするのには適していない可能性があります。
たとえば、大きすぎる場合や機密事項が含まれている場合などです。
・別々のJsonnetファイルに対する多数の同時編集が単一のJSONファイルに収束すると、マージの競合が発生する可能性があります。

中期：構築時に評価する

ビルド時にJsonnetコマンドラインユーティリティを実行し、生成されたJSONをリリースアーティファクトに（たとえば、tarballとして）埋め込むことで、JSONをソース管理にチェックインすることを回避できます。
アプリケーションコードは、初期化時に単にディスクからJSONファイルを読み取ります。Bazelを使用している場合は、[Jsonnet Bazelルール](https://github.com/bazelbuild/rules_jsonnet)を使用してこれを簡単に達成できます。Googleでは、以下に一覧化した利点があるので、一般的にこのアプローチを支持しています。

長所
・各プルリクエストでJSONファイルを再構築することなく、ランタイムの複雑さ、バイナリサイズ、およびリスクの危険度を制御できます。
・元のJsonnetコードと結果のJSONの間に非同期化のリスクはありません。

短所
・ビルドはもっと複雑です。
・コードレビュー中に具体的な変更を評価することは困難です。

後期：実行時に評価する
Jsonnetライブラリをリンクすると、アプリケーション自体がいつでも設定を解釈し、生成されたJSON設定のメモリ内表現を生成することができます。

長所
・事前評価が不要なため、より簡単です。
・実行中にユーザーから提供されたJsonnetコードを評価することができます。

短所
・リンクされたライブラリは、フットプリントとリスクの危険性を高めます。
・実行時に設定のバグが発見されるかもしれませんが、それは遅すぎます。
・Jsonnetコードが信頼できない場合は、特別な注意を払う必要があります。([次のGuarding Against Abusive Configuration](https://landing.google.com/sre/workbook/chapters/configuration-specifics/#guarding-against-abusive-configuration)で議論します。）
実行中の例に従うと、Kubernetesオブジェクトを生成している場合はいつJsonnetを実行する必要があるでしょうか？

答えは実装次第です。
ksonnet（ローカルファイルシステムからJsonnetコードを実行するクライアントサイドのコマンドラインツール）のようなものを構築している場合、
最も簡単な解決策はJsonnetライブラリをツールにリンクして進行中のJsonnetを評価することです。
コードを作成者自身のマシンで実行するので、そうすることは安全です。

[Box.com](https://www.box.com/)のインフラストラクチャはGitフックを使用して設定の変更を本番環境にプッシュします。
サーバー上でJsonnetが実行されるのを避けるために、Gitフックはリポジトリに保存されている生成されたJSONに作用します。
HelmやSpinnakerのようなdeployment管理デーモンの場合、
唯一の選択は実行時にサーバー上のJsonnetを評価することです（次のセクションで説明されている警告を使用して）。

不正な設定に対する保護

長期実行サービスとは異なり、設定の実行は結果の設定ですぐに終了します。
残念ながら、バグや意図的な攻撃のため、設定には任意の量のCPU時間またはメモリが必要となる場合があります。
その理由を説明するために、次の非終了Jsonnetプログラムを検討してください。

local f(x) = f(x + 1); f(0)

無制限メモリを使用するプログラムは似ています。

local f(x) = f(x + [1]); f([])

関数の代わりにオブジェクトを使用して、または他の設定言語で同等の例を書くことができます。
[チューリングが完全](https://en.wikipedia.org/wiki/Turing_completeness) ではなくなるように言語を制限することで、リソースを過剰に消費しないようにすることができます。
ただし、すべての設定を強制終了しても、リソースの過剰消費を防ぐことはできません。
十分な時間またはメモリを消費して、実際には終了しないプログラムを作成するのは簡単です。
例えば：
local f(x) = if x == 0 then [] else [f(x - 1), f(x - 1)]; f(100)
実際、[そのようなプログラム](https://en.wikipedia.org/wiki/Billion_laughs_attack) はXMLやYAMLのような単純な設定フォーマットでも存在します。

実際には、これらのシナリオのリスクは状況によって異なります。
問題の少ない面では、コマンドラインツールがJsonnetを使用してKubernetesオブジェクトを構築し、それらのオブジェクトを展開するとします。この場合、Jsonnetコードは信頼できます。
非終端記号を生成する事故はまれであり、それらを軽減するためにCtrl-Cを使用することができます。
偶発的なメモリ枯渇は非常に発生しにくいです。
一方、HelmやSpinnakerのように、エンドユーザーから任意の設定コードを受け取り、
それをリクエストハンドラで評価するサービスでは、
リクエストハンドラやメモリを浪費する可能性のあるDOS攻撃を避けるように十分注意する必要があります。
リクエストハンドラで信頼できないJsonnetコードを評価する場合は、
Jsonnetの実行をサンドボックス化することでこのような攻撃を回避できます。
簡単な方法の1つは、別のプロセスを使用することulimit（およびそれと同等のUNIX以外の方法）です。
通常、Jsonnetライブラリをリンクするのではなく、コマンドライン実行可能ファイルにフォークする必要があります。
その結果、特定のリソース内で完了しないプログラムは安全に失敗し、
エンドユーザーに通知します。
C ++のメモリエクスプロイトに対する追加の防御策として、JsonnetのネイティブGo実装を使用できます。

結論

Jsonnetを使用する場合、別の設定言語を採用する場合、または独自に開発する場合にかかわらず、これらのベストプラクティスを適用して、
運用システムを設定するために必要な複雑さと運用負荷を安心して管理できます。

設定言語の最低限重要な特性は、優れたツーリング、気密な設定、および設定とデータの分離です。

ご利用のシステムは設定言語を必要とするほど複雑ではないかもしれません。
Jsonnetのようなドメイン固有の言語への移行は、複雑さが増したときに考慮する戦略です。
そうすることで、一貫性のあるよく構造化されたインターフェースを提供できるようになり、SREチームが他の重要なプロジェクトに取り組む時間を空けることができます。

1古い設定言語を新しい言語に変換するソフトウェアを書くことができるかもしれないことに注意してください。
ただし、元のソース言語が標準的ではない、または壊れている場合、これは現実的な選択肢ではありません。
2計算生物学からビデオゲームまでの分野でJsonnetが使用されていますが、最も熱心な採用者はKubernetesコミュニティの出身です。
Box.comはJsonnetを使用して、Kubernetesベースの内部インフラストラクチャプラットフォームで実行されるワークロードを説明しています。
DatabricksとBitnamiもこの言語を広く使用しています。
3 YAMLストリームは、で区切られた多数のYAMLドキュメントを含むファイルです"---"。
4 YAML仕様§6.9.2。
5 YAML仕様では、オブジェクトはドキュメントとして知られています。