合わさったインベントリ — 何が勝ち、それをどう確かめるか
一言でいうと
インベントリソースが2つ以上あると、ホストとグループはマージされ、同じ名前の変数はあとから読まれたものが勝ちます。その「あと」を決めるのは、-iを書いた順序、ディレクトリ内のファイル名の順序、そしてグループの深さと辞書順です。
なぜ必要なのか
インベントリがファイル1つのときは、読めばわかります。ところが、ファイル1つで済む組織はまれです。共通のホストはプラットフォームチームが管理し、サービスごとのホストは各チームが管理します。本番とステージングは、ファイルを分けて、誤って混ざらないようにします。クラウドのリソースは、人が書く代わりに、動的インベントリのプラグインが生成します。そうなると、-iは2回以上付くことになり、ある時点から、誰も「今のweb1のapp_portはいくつか」を、ファイルを読むだけでは答えられなくなります。
ここで起きる事故は2種類です。1つ目は、見当違いのサーバーが含まれることです。2つのファイルに同じグループ名があるとホストがマージされるので、ステージングのファイルにしかないと思っていたホストが、本番のデプロイ対象に入ります。2つ目は、見当違いの値が勝つことです。古いファイルに残っていたグループ変数が、新しいファイルの値を上書きしたり、逆に、新しく作成したgroup_vars/ファイルが、インベントリ内の値を静かに押しのけたりします。
どちらの事故も、「ファイルを読んで推論する」という方式では防げません。防ぐ方法は1つです。マージされた結果を、実行前に機械に尋ねることです。
どう動くのか
-iを複数回指定すると
ansible-playbook -i a.ini -i b.ini site.ymlのように、複数回指定できます。ソースは書いた順序で読まれ、結果は1つのインベントリにマージされます。
| 何が | マージされるとき |
|---|---|
| ホスト | 和集合。同じ名前なら1つのホストです |
| グループ | 和集合。同じ名前なら、メンバーがマージされます |
| 同じ名前の変数 | あとから読まれたソースが勝ちます |
同じ名前のホストが2つのファイルにあると、2台になるのではなく、1台にマージされます。ansible_hostが互いに異なって書かれていても同じです。あとのものが勝ち、前の値は何の警告もなく消えます。インベントリを移行している途中で、ホスト名はそのままで、アドレスだけを変えたファイルを新しく追加したときに、この動作を知らないと、何日も迷います。
ディレクトリを指定すると
-i inventory/のようにディレクトリを指定すると、その中のファイルを名前の順に読みます。そのため、慣例として、10-base、20-prod、30-overridesのように、数字のプレフィックスを付けます。名前が順序を決め、順序が勝敗を決めます。
ここに、初心者が必ず一度は引っかかる落とし穴があります。ディレクトリインベントリは、ある拡張子をデフォルトでスキップします。設定項目の名前はinventory_ignore_extensionsで、そのデフォルトの一覧に.iniが入っています。.cfg、.retry、.md、.txt、エディタのバックアップ(.bak、チルダで終わるファイル)も同様です。
そのため、inventory/hosts.iniは、-i inventory/hosts.iniでは読み込まれ、-i inventory/では読み込まれません。何のエラーも出ません。ただ、そのファイルに書かれたホストがないことになるだけです。ディレクトリの中に置くINI形式のインベントリは、拡張子を取るか(10-base)、別の名前を使います。YAMLインベントリの.yml・.yamlは、一覧にないので、そのまま読み込まれます。
ディスク上のgroup_varsとhost_vars
インベントリ変数を書く場所は2つあります。1つは、インベントリファイルの中([web:vars]またはYAMLのvars:)、もう1つは、インベントリの隣のgroup_vars/・host_vars/ディレクトリです。2つが同じ変数を定義したら、どちらが勝つのか。
inventory/
10-base [web:vars] app_port=8080
group_vars/
all.yml app_port: 9000 owner: platform
web.yml app_port: 9090
host_vars/
web1.yml app_port: 9999
結果は、次のとおりです。web1は9999、web2は9090、ほかのグループのホストは9000。インベントリファイルの中に書いた8080は、どこにも現れません。公式ドキュメントの優先順位の一覧で、「inventory file or script group vars」が「inventory group_vars/*」よりも下にあるからです。同じディレクトリにあっても、ファイル内のグループ変数が最も弱いということです。これが、実務で最もよく人を驚かせる場所です。
順序を1行で覚えると、次のとおりです。インベントリファイル内のグループ変数 → group_vars/all → group_vars/グループ → インベントリファイル内のホスト変数 → host_vars/ホスト。後ろへ行くほど狭く、狭いものが勝ちます。
group_vars/ディレクトリは、インベントリの隣とプレイブックの隣の2か所に置けて、同じ名前なら、プレイブックの隣が勝ちます。2か所に分けて置くと、半年後に誰も値を見つけられなくなるので、1か所に決めて、チームのルールとして書いておくほうがよいです。
同じ深さのグループが重なるとき
1つのホストが複数のグループに属し、そのグループたちが同じ変数を定義したら、どうなるのでしょうか。デフォルトのルールは、辞書順にマージして、後ろが勝ちます。alphaとzuluの両方に属するホストは、zuluの値を持ちます。辞書順が意味を持っているはずがないので、このデフォルトに頼る設計は、それ自体が危険信号です。
デフォルトをひっくり返すつまみがansible_group_priorityです。デフォルト値は1で、数字が大きいほど、あとにマージされて勝ちます。
[alpha:vars]
color=from-alpha
ansible_group_priority=10
[zulu:vars]
color=from-zulu
こう置くと、辞書順では後ろにあるzuluを押しのけて、alphaが勝ちます。注意すべき点が1つあります。この変数はインベントリソースの中に書いて初めて効きます。group_vars/alpha.ymlに書いても効きません。それらのファイルを読み込む順序を決めるのに使われる値なので、そのファイルの中に書いたのでは、すでに遅いからです。
そして、この優先順位は、同じ深さのグループの間でのみ意味を持ちます。親と子の間では、深さが勝ちます。
子が親に勝つ
[prod:children]の下にwebとdbを入れると、webはprodの子です。2つのグループが同じ変数を定義すると、子であるwebが勝ちます。辞書順とは無関係です。親の名前がzprodで、辞書順で後ろでも、子が勝ちます。
このルールは自然です。親は広く、子は狭く、常に狭いものが勝ちます。prodに共通のデフォルト値を置き、webで必要なものだけをオーバーライドする設計が、そのために成り立ちます。allはすべてのグループの祖先なので、最も広く、最も弱いです。
マージされた結果を尋ねる方法
インベントリを疑うとき、ファイルをにらみつけるのは時間の無駄です。答えは、3つのコマンドにあります。
| コマンド | 答えてくれるもの |
|---|---|
ansible-inventory -i ... --graph |
グループの構造とメンバー。どのホストがどのグループにあるか |
ansible-inventory -i ... --list |
マージされた全体をJSONで。スクリプトで検査しやすい |
ansible-inventory -i ... --host web1 |
そのホストが最終的に持つ変数のすべて |
--hostは、特に値を数えるときに使います。「web1のapp_portは結局いくつか」に、3秒で答えます。--graph --varsを指定すると、グラフに変数まで一緒に表示されます。CIに、このコマンドの結果を検査するステップを1つ置けば、「見当違いのサーバーが含まれた」という事故を、デプロイ前に捉えられます。
ホストパターン: 対象を絞り込む文法
インベントリが大きくなると、「本番のWebサーバーのうち、ステージングに属さないもの」のような対象が必要になります。パターンの文法が、その役割を果たします。
| 記号 | 意味 | 例 |
|---|---|---|
: |
和集合 | web:db |
:& |
積集合 | web:&prod |
:! |
差集合 | prod:!staging |
* |
グロブ | web*.example.com |
~ |
正規表現(先頭に付ける) | 正規表現で名前を選ぶ |
3つをつなげると、web:&prod:!stagingになります。webであり、prodであり、stagingではないホストです。実行前に、--list-hostsで必ず確認します。特に差集合は、除いた結果が空でもエラーではないので、何の対象もないまま緑で終わるデプロイが、ここから生まれます。--limitも同じ文法を使い、プレイブックのhosts:に書いたパターンを、さらに絞り込みます。
動的インベントリは、この絵のどこにあるか
クラウドでは、ホストの一覧を人が書きません。aws_ec2、gcp_computeのようなインベントリプラグインが、APIに問い合わせて、ホストとグループを生成します。マージされるルールはまったく同じです。動的ソースも、-iに書かれた1つのソースにすぎず、順序に応じて静的ファイルと混ざります。よくある設計が、「ホストの一覧は動的ソースから、チームが決める値は静的なgroup_vars/から」である理由が、ここにあります。動的ソースがグループ名だけを作ってくれれば、その名前に合ったgroup_vars/ファイルが値を付けます。(このラボ環境には、インターネットも認証情報もないので、動的プラグインを実行してみることはできません。ルールが同じだということだけを覚えておけばよいです。)
現場での姿
事例1: ディレクトリがファイルを1つまるごと飲み込んだ日。インベントリをディレクトリに移すときに、既存のhosts.iniをそのままinventory/の中に入れました。ansible-inventory --graphを実行してみると、グループが半分しか出ませんでした。エラーはまったくありませんでした。原因は、.ini拡張子がデフォルトの無視リストにあることで、ファイル名から拡張子を取ると、その場で解決しました。その日以降、インベントリを変更するすべてのPRに、--graphの出力の差分を添付するルールができました。
事例2: group_varsを作成したら、値が変わらなかったこと。逆方向の事故もありました。急いでポートを変えようとして、インベントリファイル内の[web:vars]を直しましたが、値が変わりませんでした。数か月前に誰かがgroup_vars/web.ymlを作っておいて、そちらのほうが強かったのです。ファイル内のグループ変数が最も弱い場所であることを、そのとき学びました。今は、インベントリファイルに変数をまったく書かず、group_vars/の1か所だけを使っています。場所を減らすほうが、優先順位を暗記するよりも優れています。
事例3: 辞書順に頼っていた設定。appとbackendの2つのグループに同じホストがあり、どちらもjava_optsを定義していました。辞書順でbackendが勝っていたのですが、ある日、グループ名をappからzappに変える整理作業がありました。名前を変えただけなのに勝者が入れ替わり、ヒープの設定がまるごと変わりました。このような場所には、ansible_group_priorityで意図を明示するか、そもそも1つのホストが、同じ変数を定義する2つのグループに属さないように、設計を直す必要があります。
次のラボですること
インベントリファイルを2つ作成して、-iを2回指定するところから始めます。同じものをディレクトリに入れて、ファイル名の順序が勝敗を決めることを確認し、.ini拡張子のファイルを1つ一緒に置いて、それが静かにスキップされるという事実を、自分の目で見ます。次に、group_vars/all・group_vars/그룹・host_vars/호스트(グループ名・ホスト名の部分はプレースホルダーです)を作成して、ホストごとに異なる値が勝つことを計測し、インベントリファイル内の変数がその3つより弱いことを確認します。同じ深さの2つのグループで、辞書順をansible_group_priorityでひっくり返してみて、親と子では深さが勝つことを見ます。最後に、パターンで対象を絞り込んで結果をファイルに残し、「この変数は結局どの値が勝つのか」に答えるツールを、自分で作成します。