🚢

ansible-lint のカスタムルールを利用して Ansible 内での変数命名規則を縛ってみた話

2023/12/02に公開

まえがき

みなさんこんにちは! KMCID: pollenjp です.
本記事は KMC Advent Calendar 2023 の 3 日目の記事と Ansible Advent Calendar 2023 です.

昨日の記事は taisei さんによる Jsoo でスイカゲームもどき | A watermelon game on browser implemented in ocaml でした.
元のスイカゲームは動画などでしか見たことはなかったのですが, スイカゲームもどきを触ってみて絶妙なゲームバランスに納得し楽しめました.

はじめに

さて, 本記事では ansible-lint のカスタムルールを利用して Ansible 内での変数命名規則を縛ってみた話をします.

自分用に作った命名規則を ansible-lint-custom-strict-naming · PyPI パッケージに落とし込んで利用しているので, もし同じ Ansible の悩みを抱えている方がいたら参考にしてみてください.

Ansible の変数スコープや優先順位が分からなくなる

Ansible では様々な場面で変数の定義または代入を行うことができます [1]. 例えば,

  • inventory ファイルの vars セクションで定義して事前に渡す変数
  • ansible.builtin.set_fact で定義した変数または書き換えた変数
  • task の register で定義された変数

等です. しかしながら, これらによって定義される変数は Ansible の独特な優先度 [1:1] に則っています. グローバルのような変数を定義してるものもあれば, スコープ内だけに閉じているものもあり, また書き換えているように見えて実は優先度の高いほかの値によって上書きされるもの等です. 次にこれらよる問題点をいくつか挙げてみます.

※ 追記: 例えば task の vars 等で指定した変数については task 内に閉じているため, 誤解を避けるため記事のセクション名を修正しました.

問題点 1: チーム内で変数の定義・利用場所の把握が難しい

現状, Ansible の Language Server では変数の定義ジャンプ等はまだサポートされていないため, 変数の定義場所がわかりやすいとは言いにくいです.

また, 一人だけで管理するプロジェクトであればまだしも, チームで管理するプロジェクトにおいて, ルール無く扱われる変数を把握することは困難でありバグの温床になります.

問題点 2: 変数を予期せず上書きしてしまうケース

実際にこのパターンは頻繁に踏むわけではありませんが, 例えば次のようなケースが考えられます.

以下のような inventory, role, playbook file 構成で ansible-playbook を実行した場合, 変数 var__overwrite がどのように変化するのかを追ってみます.

ansible-playbook -i inventory/debug.yml playbooks/debug.yml
zsh ❯ tree --charset=ascii
.
|-- ansible.cfg
|-- inventory
|   `-- debug.yml               ... (1) var__overwrite の初期値を定義
`-- playbooks
    |-- debug.yml               ... (2) var__overwrite の値を表示
    `-- roles
        `-- sample
            `-- tasks
                `-- sample.yml  ... (3) var__overwrite を上書き

inventory/debug.yml

---
all:
  hosts:
    localhost:
      ansible_connection: local
      var__overwrite: "original" # (1) var__overwrite の初期値を定義

playbooks/roles/sample/tasks/sample.yml

---
- name: Overwrite parent playbook vars
  ansible.builtin.set_fact:
    # (3) var__overwrite を上書き
    var__overwrite: overwritten (by <some_role>/tasks/sample.yml)

playbooks/debug.yml

---
- name: Overwrite Vars Play
  hosts:
    - localhost
  tasks:
    - name: Playbook 外で定義した変数の値を表示
      ansible.builtin.debug:
        msg: |
          var__overwrite: {{ var__overwrite }}
        ###############################
        # output                      #
        # (2) var__overwrite の値を表示 #
        ###############################
        # var__overwrite: original
    - name: Include role (中で var__overwrite を変数として再代入)
      ansible.builtin.include_role:
        name: sample
        tasks_from: sample.yml
    - name: 変数の値を表示
      ansible.builtin.debug:
        msg: |
          var__overwrite: {{ var__overwrite }}
        ###############################
        # output                      #
        # (2) var__overwrite の値を表示 #
        ###############################
        # var__overwrite: overwritten (by <some_role>/tasks/sample.yml)

上記の playbooks/debug.yml のコメントに記載していますが, var__overwrite の値は ansible.builtin.include_role で呼び出した role 内で上書きされています.
これが意図した動作であれば問題ありませんが, もしかすると sample role は role 内だけで利用する変数を定義したかっただけかもしれません. そのような場合, 値が予期せず変更され, そのまま次の task が実行されてしまいます.

命名規則で改善

上述した問題を完全に解決することは現状難しいですが, 変数側の命名規則を設けることで低コストで改善することは可能です. 例えば,

  • 定数なのか変数なのか
  • どこで定義・利用されているか
  • 期待される変数スコープは何か

などの情報を変数名に含め lint で検知することで最悪のケースを回避する一助となります.

命名規則

ここでは ansible-lint-custom-strict-naming · PyPI で定義している命名規則を紹介します.

role 名, tasks 名を prefix に含める

まず, 「問題点 2: 変数を予期せず上書きしてしまうケース」 については, 変数名の prefix に変数の role や tasks 名を含めることで改善します.

そしてこの考えは ansible-lint の var-naming[no-role-prefix] ルールに一部含まれています.

var-naming[no-role-prefix]: Variables names from within roles should use role_name_ as a prefix. Underlines are accepted before the prefix.

例えば ansible.builtin.include_role 等で sample という role を呼び出し, vars で変数を与える際に sample_ という prefix を使いなさいということです. sample role が受け取る変数sample_ という prefix を付けることを示しています.

- name: Run sample
  ansible.builtin.include_role:
    name: sample
  vars:
    sample_var1: "var1"
    sample_var2: "var2"

しかし, このルールはかなり弱めに設定されています. role が 受け取る変数 (playbook 側から渡す変数) にしか制約が働かないからです.
「問題点 2」で発生した問題は role 内で ansible.builtin.set_fact を使って変数を上書きしてしまうことでしたが, このルールでは明示的に変数を渡しているわけではないため検知でません.

また, ansible.builtin.include_role だけでなく ansible.builtin.include_tasks も外部の tasks を取り込んでいるため同様のルールを適応したいところです.

そこで, ansible-lint-custom-strict-naming · PyPI では次のようなルールを設けています.

  • role の中で定義・代入する変数にはすべて <role_name>_role__ prefix を付与する
  • tasks の中で定義・代入する変数にはすべて <tasks_name>_tasks__ prefix を付与する

このルールは var-naming[no-role-prefix] ルールを完全に内包しています.
加えて, ansible.builtin.include_tasks や role 内での ansible.builtin.set_fact 等にも適応されるため, 予期せず変数を上書きしてしまうケースを検知することができます.

lint ルールの実装コード

roles/sample/tasks/main.yml の例

- name: Set fact
  ansible.builtin.set_fact:
    # role の中で定義・代入する変数にはすべて <role_name>_role__ prefix を付与する
    sample_role__var1: "var1"
    sample_role__var2: "var2"

Playbook 内で定義され, 動的に値が変わりうるものに var__ prefix を付与する

次に, 「問題点 1: チーム内で変数の定義・利用場所の把握が難しい」にも通ずるのですが,
Ansible の変数を扱う際にそれが 動的に値が変わる変数として扱われているのか, 定数として扱われているのか
が明示されていないと, 作業効率が落ちます. 変数を利用する際に値が期待したものであり, 途中で書き換わっていないかを判定する必要があるからです.

ここで inventory ファイルなどで事前に渡しておく変数は後々変更を加えるべきではないためこの変数のことを 定数 (const)と, Playbook 内などで ansible.builtin.set_fact により定義・代入される変数のことを 変数 (var)と記述することにします.

これらの 定数 (const) と 変数 (var) は分けようと意識していても名前衝突の可能性はまだ残っています. そのため, これらの変数にはそれぞれ prefix として const__, var__ を付与することにしましょう.

現在の ansible-lint-custom-strict-naming · PyPI では定数 (const) として検知しようとするとルールが複雑化するため const__ prefix は検知していません.
そのため, 定数 (const) として変数を新たに作る場合には var__ prefix をつけないように人間が気をつける必要があります.

※ 定数 (const) として扱いたいものに var や double underscore を繋げたような prefix をつける人はいないでしょうという気持ちから変数 (var) 側の prefix を var__ にしています.

ルールの結合

上記に上げた 2 つのルールは組み合わせて利用することができます. (ただし, 変数名が長くなるデメリットもある.)

定数 (const) ※1 変数 (var)
playbook 内 const__xxx var__xxx
sample role 内 sample_role__const__xxx sample_role__var__xxx
sample tasks 内 sample_tasks__const__xxx sample_role__var__xxx

※1 ansible-lint-custom-strict-naming · PyPI では未検知

ansible-lint のカスタムルールを作る

ansible-lint は Custom linting rules - Ansible Lint Documentation にある通り, 独自の lint ルールを作ることができます.

プロジェクト特有のルールをローカルに作成する場合は設定ファイルで rulesdir を指定することでカスタムルールを利用することができます (Configuration - Ansible Lint Documentation).

今回紹介している ansible-lint-custom-strict-naming · PyPI は package 化しているため pip install でのみで利用することができます.

例として既出の playbooks/roles/sample/tasks/sample.ymlに対して ansible-lint を実行してみます.

---
- name: Overwrite parent playbook vars
  ansible.builtin.set_fact:
    var__overwrite: overwritten (by <some_role>/tasks/sample.yml)

するとと以下のように検知されます.

ansible-lint-custom-strict-naming<var_name_prefix>: Variables in 'set_fact' should have a 'sample_role__var__' prefix.
playbooks/roles/sample/tasks/sample.yml:2 Task/Handler: Overwrite parent playbook vars

Ansible - Visual Studio Marketplace を利用して VSCode で表示すると以下のようになります.

Image from Gyazo

これで自分もチームも迷うこと無く変数を扱うことができますよね (圧).

まとめ

  • Ansible は便利で用途も広い
  • しかし, 変数のスコープが無いため, 予期せず変数を上書きしてしまう等の問題点もある
  • 一部の問題点は命名規則で改善することはできる
  • ansible-lint のカスタムルールを作ることができれば変数の命名規則を自分やチームに強制することができる

最後に

  • KMC Advent Calendar 4 日目の記事は trdr さんの記事です. お楽しみに!!
  • 本記事のサンプル Ansible プロジェクトは以下にあります
  • ansible-lint-custom-strict-naming · PyPI について
    • 本記事で紹介したルールを実装したパッケージです
    • パッケージの開発は雰囲気でやっており, 試行錯誤段階にあるため, そのまま利用される場合は package バージョンを固定することをお勧めします.
  • なお, Ansible は様々な使われ方ができるため, お使いのプロジェクトに合わせて適切なルールを利用することをお勧めします.
脚注
  1. https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html#defining-variables-at-runtime ↩︎ ↩︎

GitHubで編集を提案

Discussion