変数の優先順位とファクト
一言でいうと
Ansibleで「変数が効かない」のは、ほとんどの場合、変数がないからではなく、より強い場所で定義された値が勝っているからです。
なぜ必要なのか
同じアプリケーションを、dev・stage・prodにデプロイするとします。ポートとレプリカ数とログレベルだけが異なります。このとき、プレイブックを3つにコピーすると、6か月後には、3つのファイルが互いに別の生き物になっています。値をコードの外に切り出すのは、好みの問題ではなく、分岐が増えるのを防ぐための構造的な選択です。
問題は、値を入れられる場所が多すぎる点です。プレイのvars、別のvars_files、group_vars/、host_vars/、インベントリ内の変数、コマンドラインの-e、ロールのdefaultsとvars、そして実行中にset_factで作った値まで。この場所ごとに優先順位が異なり、その順序を知らないと、「確かに値を変えたのに反映されない」1日を過ごします。
どう動くのか
詳細な順序は長いですが、実務で覚えておくべき骨格は短いです。
| 強さ | 場所 | 性格 |
|---|---|---|
| 最も弱い | ロールのdefaults/main.yml |
「このロールのデフォルト値」。オーバーライドされるために作られた場所 |
| 弱い | インベントリのグループ変数 | 環境ごとの共通値 |
| 中程度 | インベントリのホスト変数 | そのサーバーだけの例外 |
| 強い | プレイのvars、vars_files |
この実行での値 |
| 非常に強い | set_factで作った値 |
実行中に計算された値 |
| 最も強い | コマンドラインの-e(extra vars) |
何にでも勝つ |
2つの原則を守れば、ほとんどは解決します。1つ目は、オーバーライドされてもよい値はdefaultsに置くことです。2つ目は、-eは一度限りの例外にだけ使うことです。-eが習慣になると、その値がどこにも記録されず、次の人が再現できません。
ファクト(fact)は、性格が異なります。これは人が書く値ではなく、対象のサーバーから収集した事実です。gather_facts: trueなら、プレイの開始時にsetupモジュールが動き、OSの種類、アーキテクチャ、ホスト名、ネットワークインターフェース、メモリなどを集めて、ansible_factsの下に入れます。ファクトを使えば、「Ubuntuならapt、RHELならdnf」のような分岐を、対象ごとに自動的に行わせられます。
収集にはコストがかかります。ホストが数百台あると、ファクトの収集だけで数十秒かかります。ファクトが不要なプレイは、gather_facts: falseでオフにすべきで、繰り返しの実行が多いなら、ファクトのキャッシュを検討します。
registerは、3つ目の種類です。タスクの実行結果の全体(標準出力、終了コード、changedかどうか)を、変数に格納します。ここでよくある間違いは、結果のオブジェクト全体をそのままファイルに書いてしまうことです。必要なのは、たいてい.stdout1つです。
現場での姿
1つ目は、-eで急場をしのいで、コードに反映しないことです。障害対応中に、-e replicas=10を渡してサービスを救ったなら、その値は必ずリポジトリにコミットされなければなりません。そうしないと、次のデプロイが静かにそれを元に戻します。これは、Terraformのドリフトとまったく同じ構造の事故です。
2つ目は、ファクトを信頼しつつ検証することです。ansible_distributionのようなファクトは、おおむね正確ですが、コンテナや特殊なイメージでは、予想と異なる値が出ます。ファクトで分岐するときは、予想外の値に対するデフォルトの分岐を残しておきます。
値がどこから来たかを確認する方法
優先順位を暗記するよりも実用的なのは、今このホストでその値が何なのかを、直接問い合わせることです。推論は間違えますが、実測は間違えません。
最も速い方法は、タスク1つで値を出力してみることです。アドホックでdebugを実行して特定の変数を出力すると、そのホストを基準に、最終的な勝者が何なのかがすぐにわかります。そして、複数のホストで一度に実行してみることが、特に価値があります。1台だけ値が異なれば、そのホストのhost_varsやインベントリの行に例外が書かれているという意味で、それがたいてい問題の原因です。
値ではなく、どこから来たのかが気になるなら、範囲を絞り込みながら確認します。グループ変数を一時的に削除してみて、値が変われば、そこから来たものであり、変わらなければ、より強い場所から来ています。そして、実行時に詳細な出力をオンにすると、どの変数ファイルを読んだのかがログに出るので、読み込まれていないファイルを直していたという、よくある状況をすぐに見つけられます。
名前の付け方も、この問題に大きく作用します。変数名にプレフィックスを付ける習慣にすれば、衝突がほとんどなくなります。portよりmyapp_portのほうがよく、ロールの変数は、ロール名をプレフィックスにします。短い名前は、どのロールでも使えるので、別のロールが同じ名前を使った瞬間、2つのうち1つが、静かに負けます。
最後に、どの場所に何を置くかをチームのルールとして決めておくことが、優先順位を暗記するよりも効果が大きいです。環境ごとに異なる値はグループ変数に、サーバー1台の例外はホスト変数に、ロールが提案するデフォルト値はdefaultsに置きます。この3行が守られれば、残りの場所はほとんど使うことがなく、使うことがなければ、優先順位のために驚くこともありません。
次のラボですること
プレイ変数・変数ファイル・グループ変数・ホスト変数をそれぞれ作成して、どれが勝つのかを自分の目で確認し、コマンドラインの-eがそのすべてに勝つのを見ます。ファクトを収集してJSONで残し、registerとset_factで値を組み合わせて、最終的な要約ファイルを作成します。