Usual Software Engineer

よくあるソフトウェアエンジニアのブログ

Ansibleモジュールのchangedの判断基準

Ansible で playbook を実行する際に、事前に check mode で一度変更点を確認することがよくあると思います。 具体的には check mode の出力結果で、 playbook 内のどの task が changed になり、どの task が changed にならないかを見ることで、今回の実行での変更点が何であるかを把握するといった形になります。 ここで重要になるのが、"その module の changed: True は何を表すのか"という部分です。

例えば、"対象のホスト(AWS の EC2)を、登録されているロードバランサー(ELB)から切り離す"というカスタムモジュール(my_ec2_lb とする)を作成する場合、 changed: True/False は何を表すでしょうか。 カスタムモジュールでなくとも、すでに公式で elb_instance というモジュールがあり、それによると

  • ELB に対して操作を行い、対象の EC2 が ELB から切り離された場合は changed: True
  • ELB に対して操作を行い、対象の EC2 はすでに ELB から切り離されていた場合は changed: False

というような判断を行っています。ざっくり言うと変更があったらそのまま True なので、特におかしな点はないように思います。 ですが、自分たちでカスタムモジュールを作成する場合、どんな時に changed: True にすれば良いのかを深く考えなければいけない場合があります。

話を my_ec2_lb に戻して、そのカスタムモジュールは次のような機能であるとします。

  • 対象のホスト(inventory に定義されている host)がどのロードバランサーに登録されているかを、ホスト上で取得する
    • EC2 に ELB を参照/更新する権限を持つ IAM instance profile を設定してあげて、AWSAPI で取得するなど
  • 取得したロードバランサーに対して、対象のホスト(自分自身)を切り離すよう指示する
    • 同様に AWSAPI で更新するなど

この場合、 my_ec2_lb も公式の elb_instance と同じように、単純に変更があったら changed: True になるように実装するでしょうか?

実は"変更があったら"に加えてもう一つの視点がありまして、それは"対象は何なのか"という点です。 my_ec2_lb という架空のカスタムモジュールにおける対象ホストは inventory に定義されている host であり、パラメータで instance_id を指定するような形式ではなく、 playbook の実行対象そのものになります。 ざっくり言うと elb_instance は対象が ELB 、 my_ec2_lb は対象が EC2 ということになります。

あらためて、 my_ec2_lb によってそのホストをロードバランサーから切り離した時、それは対象であるホストを変更したことになる(= changed: True )のでしょうか。なかなか判断が難しいところです。

ここまでがとっても長い前置きの課題の共有ですw

そんなわけで、カスタムモジュールを作成するなら公式のモジュールの実装を参考にすれば良いので、公式のモジュールの check mode の出力結果を見ることで changed の判断基準が少しでも明確になればなと思い試してみました。 使った playbook などは GitHub にあげております。

github.com

前置きが長すぎたのでさくっといきますか。 Ansible のバージョンは 2.4.1.0 を使用しました。

commands 系モジュール

- name: 1.1.1 shell module
  shell: date
  check_mode: no
  register: shell_result

# changed => True

changed の判断基準に関しておそらく shell モジュールや command モジュールはよく取り上げられていると思います。 基本的には check mode では実行が skip されるので changed: True/False どうこうの話ではありません。余談ですが check mode でもコマンドを実行したい場合は check_mode: no として task を定義してあげる必要があります。

どちらのモジュールでも、変更を行うようなコマンド実行であろうが無かろうが、全て結果は changed: True になります。 自分で changed: True/False を操作したい場合は、 chenged_when: を task に定義してあげる必要があります。

この動きからわかることは、"変更される可能性がゼロではない場合、念のため changed: True にすべきである"というような判断がなされていると推測できます。

files 系モジュール

- name: 2.2.1 copy module - same content
  copy:
    content: |
      127.0.0.1       localhost
      ::1     localhost
    dest: /etc/hosts
  register: copy_same_result

# changed => False

- name: 2.2.2 copy module - different content
  copy:
    content: |
      127.0.0.1 localhost
    dest: /etc/hosts
  register: copy_different_result

# changed => True

続いて一番定番であると思われる files 系モジュールについてですが、こちらは予想通り単純に変更があるか無いかという点がそのまま changed の値となります。 ファイルの名前、中身、 owner/group 、 mode など、一つでも変更があればちゃんと check mode 時も changed: True としてくれます。

この動きからわかることは、"対象のホストのファイルが変更される場合、 changed: True にすべきである"という判断がなされていると推測できます。

packaging 系モジュール

- name: 3.1.1 yum module - already installed
  yum:
    name: python
    state: present
  register: yum_already_result

# changed => False

- name: 3.1.2 yum module - update
  yum:
    name: util-linux
    state: latest
  register: yum_update_result

# changed => True

続いて packaging 系モジュール。面倒なので yumpip しか試しませんでしたが、こちらも単純な結果になります。 すでに求めるパッケージがインストールされている場合は False 、 新規インストールやアップデートが必要になる場合は True という出力になりました。

この動きからわかることは、"対象のホストのパッケージが変更される場合、 changed: True にすべきである"という判断がなされていると推測できます。

他のモジュール

- name: 99.2.1 uri module
  uri:
    url: "http://localhost:8080/"
    method: POST
    return_content: yes
  check_mode: no
  # NOTE: uri module does not support check mode. This task sends a actual request even if check mode
  register: uri_result

# changed => False

最後に他に気になったモジュールの結果についてです。 一応 set_fact モジュールと git モジュールも試しましたが前述と矛盾しない結果になり、 set_fact はただ fact をセットするだけなので changed: False に、 git は clone してホスト上のファイルを変更するので changed: True になりました。 get_url モジュールでファイルのダウンロードを行う場合も同じで changed: True になります。

一番注目すべきは uri モジュールです。 HTTP のリクエストを送信するモジュールですが、 method: POST であっても changed: False になります。 余談ですが uri モジュールも check mode をサポートしてないので、今回の例では check_mode: no として skip されないようにしました。普段は真似しないでください。

この動きからわかることは、"あくまでも対象のホストのファイルなどが変更される場合、 changed: True にすべき"であり、"HTTP の POST のような更新系の操作であっても対象が別であれば changed: True にする必要はない"というような判断がなされていると推測できます。

なぜなら、もし"変更されるか否かがわからないので changed: False としている"というのであれば、前述の commands 系モジュールの判断と矛盾してしまうからです。

check mode 時の changed の判断基準

結論として、カスタムモジュールを作成する場合の changed の判断基準は以下のようになりそうです。

  • 対象のホストの何らかが変更される可能性がゼロではない場合 changed: True にしておく
  • 対象のホストのファイル/パッケージなどが変更される場合、 changed: True にする
  • 何らかの変更があるかもしれないが、それは対象のホストに関するものでない場合、 changed: False にしてよい

これらを元にして、この記事の最初で問いかけた

例えば、"対象のホスト(AWS の EC2)を、登録されているロードバランサー(ELB)から切り離す"というカスタムモジュール(my_ec2_lb とする)を作成する場合、 changed: True/False は何を表すでしょうか

に関しての答えはどうなるかというと、ロードバランサーの切り離しが成功した場合でも、元々ロードバランサーから切り離されていた場合でも、常に changed: False として changed に意味を持たせない、というのが正しいような気がします。

ただ実際には、"ロードバランサーで In Service になっている"ことを"対象のホストの状態"と見なすという視点もあるので、その場合はロードバランサーを切り離したら changed: True になるとも言えますし、こればっかりは好みや開発チームの中でのルール決めに依存しそうです。 バシッと結論が出ない感じで少しモヤモヤしますが、カスタムモジュールを作成することがあれば参考にしていただけたら幸いです。