DMM.comの、一番深くておもしろいトコロ。

Ansible(jinja2)のdictで使ってはいけない変数名の話

Ansible(jinja2)のdictで使ってはいけない変数名の話

  • このエントリーをはてなブックマークに追加

DMMグループ Advent Calendar 2021 4日目の記事です。

DMMの動画配信におけるインフラを担当している菅野です。

普段はオンプレで物理サーバ買ったりいじったりしてますが、最近ではGCPをいろいろ触ったりもしてます。

今回はオンプレサーバを管理するためにansibleを触っていたとき少しハマってしまった内容を記事にします。

事の起こり

サーバに入れてるミドルウェアのアップデートしたいので、ansibleでミドルウェアインストール用に切り出した role にフラグ判定で latest を入れる処理を入れてみました。

nginx.yml
--
tasks:
  # nginxを入れるtask
  - name: install nginx
    yum:
      name: nginx
      state: present

  # nginx.updateがtrueならlatestを入れることで最新版に上げる処理
  - name: update nginx 
    yum:
      name: nginx
      state: latest
    when: ( nginx.update | default(false) | bool ) 
group_vars/all.yml
--
nginx:
  update: true

これで nginx.updatetrue が入り本来であれば latest の方も実行されて最新版に更新される想定でした。

しかし、実際の実行結果では nginx.update != true と判定され skip になりました。

PLAY [all] ********************************************************************************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************************************************************************************************************************************************************************
ok: [server01]

TASK [install nginx] **********************************************************************************************************************************************************************************************************************************************************************************
changed: [server01]

TASK [update nginx] ***********************************************************************************************************************************************************************************************************************************************************************************
skipping: [server01]

PLAY RECAP ********************************************************************************************************************************************************************************************************************************************************************************************
server01                   : ok=3    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   

原因

ansibleの変数は基本的にjinja2のテンプレートで処理が実施されるのですが、中身的にはpythonのdict型なため、pythonのdict型に組み込まれているメソッドがそのまま組み込まれています。

そして、そのメソッド名と同じ変数名をつけると外から見る分には見れるのですが、実際に参照しようと思うとメソッド呼び出し扱いになり、想定外の挙動になりました。

試しに hoge というdictを用意してその下に update, upgrade, var1 というkeyで値を設定し、 hoge 全体と各key毎の値を出力してみました。

group_vars/all.yml
--
hoge:
  update: "update"
  upgrade: "upgrade"
  var1: "var1"
echo-vars.yml
--
- hosts: all
  tasks:
    - name: debug hoge
      debug:
        var: hoge
    - name: debug hoge.var1
      debug:
        var: hoge.var1
    - name: debug hoge.upgrade
      debug:
        var: hoge.upgrade
    - name: debug hoge.update
      debug:
        var: hoge.update

上記の playbook を実行した結果が下記になります。

PLAY [all] ********************************************************************************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************************************************************************************************************************************************************************
ok: [server01]

TASK [debug hoge] *************************************************************************************************************************************************************************************************************************************************************************************
ok: [server01] => {
    "hoge": {
        "update": "update",
        "upgrade": "upgrade",
        "var1": "var1"
    }
}

TASK [debug hoge.var1] ********************************************************************************************************************************************************************************************************************************************************************************
ok: [server01] => {
    "hoge.var1": "var1"
}

TASK [debug hoge.upgrade] *****************************************************************************************************************************************************************************************************************************************************************************
ok: [server01] => {
    "hoge.upgrade": "upgrade"
}

TASK [debug hoge.update] ******************************************************************************************************************************************************************************************************************************************************************************
ok: [server01] => {
    "hoge.update": "<built-in method update of dict object at 0x7f559978d240>"
}

PLAY RECAP ********************************************************************************************************************************************************************************************************************************************************************************************
server01                   : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

見て分かる通り update だけが built-in method 扱いになっていることが分かります。

しかし、hoge 全体で参照しようと思うと見れてしまうことに加えて when で処理する際もエラー等にはならず処理自体は通ってしまうため、デバッグでも見落としやすいです。

他のbuilt-in method

pythonのドキュメントから見て判断可能だった組み込みメソッドは下記の通りです。

docs.python.org

  • clear
  • copy
  • get
  • items
  • keys
  • pop
  • popitem
  • setdefault
  • update
  • values

試しに上記の一覧全てを同様に変数出力してみました。

group_vars/all.yml
--
hoge:
  update: "update"
  upgrade: "upgrade"
  var1: "var1"
PLAY [all] *************************************************************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************************************
ok: [server01]

TASK [debug hoge] ******************************************************************************************************************************************************
ok: [server01] => {
    "hoge": {
        "update": "update",
        "upgrade": "upgrade",
        "var1": "var1"
    }
}

TASK [clear] ***********************************************************************************************************************************************************
ok: [server01] => {
    "hoge.clear": "<built-in method clear of dict object at 0x7feaab32b240>"
}

TASK [copy] ************************************************************************************************************************************************************
ok: [server01] => {
    "hoge.copy": "<built-in method copy of dict object at 0x7feaab2c2678>"
}

TASK [get] *************************************************************************************************************************************************************
ok: [server01] => {
    "hoge.get": "<built-in method get of dict object at 0x7feaac09d480>"
}

TASK [items] ***********************************************************************************************************************************************************
ok: [server01] => {
    "hoge.items": "<built-in method items of dict object at 0x7feaab827090>"
}

TASK [keys] ************************************************************************************************************************************************************
ok: [server01] => {
    "hoge.keys": "<built-in method keys of dict object at 0x7feaab2c3510>"
}

TASK [pop] *************************************************************************************************************************************************************
ok: [server01] => {
    "hoge.pop": "<built-in method pop of dict object at 0x7feaab2d9a20>"
}

TASK [popitem] *********************************************************************************************************************************************************
ok: [server01] => {
    "hoge.popitem": "<built-in method popitem of dict object at 0x7feaab760e58>"
}

TASK [setdefault] ******************************************************************************************************************************************************
ok: [server01] => {
    "hoge.setdefault": "<built-in method setdefault of dict object at 0x7feaab795a68>"
}

TASK [update] **********************************************************************************************************************************************************
ok: [server01] => {
    "hoge.update": "<built-in method update of dict object at 0x7feaab2dab88>"
}

TASK [values] **********************************************************************************************************************************************************
ok: [server01] => {
    "hoge.values": "<built-in method values of dict object at 0x7feaab273090>"
}

PLAY RECAP *************************************************************************************************************************************************************
server01                   : ok=12   changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

定義していないkeyについてもメソッドとして定義があるため参照することができ、全てが組み込みメソッド扱いとしてメッセージ表示される結果となりました。

まとめ

社内では結構クラウド寄りな記事が多そうだったので、あえてAnsibleを題材にヒトネタ書いてみました。

jinja2に潜む意外な罠でしたが、考えてみると「そうだよなあ」となりそうなものなのに、dict全体であれば変数が見れてしまうためハマってしまうと意外とすぐに気付けず苦労をしました。

また、ansibleだとデバッグ方法もtaskの間にdebugモジュールの処理を混ぜるぐらいしかできず、結構しづらいこともあるかと思います。それもまたハマりどころの一因かもしれないので良い方法があれば採用していきたいですね。

今回の記事の検証は下記のリポジトリにもまとめていますので、気になる方は試してみてください。

github.com

qiita.com