よく使われる構成管理ツール 初回のデプロイには便利だけど,その後の継続利用とかにはちょっと,何があるんじゃないかというお気持ち.

よくつかうオプション

特定のホストにだけPlaybookを適応は l オプション

$ ansible-playbook playbook.yml -l TARGET_HOST

ステップ実行は --step (1つのtaskごとにy/nで実行するかどうかを確認出来る)

$ ansible-playbook playbook.yml --step

特定のtask移行を実行は --start-at= オプション

$ ansible-playbook playbook.yml --start-at="Task Name"

実行されるホストを確認する --list-hosts オプション

$ ansible-playbook playbook.yml --list-hosts

実行されるタスクを確認する --list-tasks オプション

$ ansible-playbook playbook.yml --list-tasks

Ansible Custom Moduleの作りかた

Ansibleを書いてて,宣言的にPlaybookとかRoleを作れないと分かったら,諦めて,Custom Moduleを書いた方がてっとり早いと思う. Custom ModuleはPlaybookのymlファイルを配置するディレクトリに library というディレクトリを掘ってその中で配置する. プログラムのファイル名がそのまま,AnsibleのCustom Module名になる.

$ tree .
├── README.md
├── ansible.cfg
├── inventories
├── library
│   └── hogehoge.py
├── requirements.txt
├── roles
└── playbook.yml

libraryの下でAnsibleModuleクラスを用いて,プログラムを作成する.

def main():
    module = AnsibleModule(
        argument_spec=dict(
            dns=dict(required=True),
            zone=dict(required=True),
            hogehoge=dict(required=False, type='bool', default=True),
        ),
        supports_check_mode=True
    )

    dns = module.params['dns']
    zone_name = module.params['zone']
    hogehoge = module.params['hogehoge']

    if module.check_mode:
        module.exit_json(changed=False)

    if hogehoge != True:
        module.fail_json(msg=f"Hogehoge is not true")
    module.exit_json(changed=True)

from ansible.module_utils.basic import AnsibleModule
if __name__ == "__main__":
    main()

これは,とくに何もないCustom Moduleだが,だいたいの使いかたはこれで分かると思う. これを呼び出すば場合は以下のようにPlaybookで書く.

- name: hogehoge
  hogehoge:
    dns: 8.8.8.8
    zone: exmaple.com.
- name: hogehoge?
  hogehoge:
    dns: 8.8.8.8
    zone: exmaple.com.
    hogehoge: False

また,このモジュールではAnsibleのModuleの importmain 関数の直前で行なっている.

from ansible.module_utils.basic import AnsibleModule
if __name__ == "__main__":
    main()

これは,AnsbileのCustoom Moduleはこの import 含めてサーバに持っていく時は,1つのファイルに埋め込まれて実行されるらしく,実際に実行する部分よりも上にあると,エラーなどの行番号がずれて,デバッグなどが行いにくくなるためらしい.

また,Custom Moduleでssh先のファイル操作などを行いたい場合もこの中で,普通にファイルの操作を行えばよい.

import json

def main():
    module = AnsibleModule(
        argument_spec=dict(
            dns=dict(required=True),
            zone=dict(required=True, type='dict'),
            config_path=dict(required=True, type='str')
        ),
        supports_check_mode=True
    )

    dns = module.params['dns']
    zone_config = module.params['zone']
    config_path = module.params['config_path']

    with open(config_path, 'w') as f:
        f.write(json.dumps(zone_config))
    module.exit_json(changed=True)

from ansible.module_utils.basic import AnsibleModule
if __name__ == "__main__":
    main()

Custom Moduleは基本的にターケットとなった環境で実行されるのでライブラリの確認とかは十全にやらないといけない.

AnsibleのOpenStackのインターフェースに併せたOpenStack系のモジュールの作り方

openstack_full_argument_spec を用いるとAnsibleのdefaultのOpenStack関係のモジュールと同様のインターフェースでモジュールを作成する事が出来る。 利用方法としては以下になる。

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.openstack import (
    openstack_full_argument_spec,
    openstack_module_kwargs,
    openstack_cloud_from_module,
)

def main():
  argument_spec = openstack_full_argument_spec(
    port_id=dict(required=True),
  )
  module_kwargs = openstack_module_kwargs()
  module = AnsibleModule(
    argument_spec, supports_check_mode=True, **module_kwargs
  )

  port_id = module.params["port_id"]
  sdk, cloud = openstack_cloud_from_module(module)
  os_network = cloud.get_session_endpoint("network")
  os_token = cloud.auth_token

このサンプルは入力からopenstacksdkのcloudを取得して、NeutronのEndpontをOpenStackのアクセストークンを取得している。 OpenStackのKeystoneに取得されているEndpointから取得出来るので便利。

先に実行されたタスクの実行結果がchangedの時にのみ実行する

Ansibleを書いてて, shell とかを使う時に実行するスクリプトファイルとかを template で生成した場合に,変更があった時だけ実行したいといった事が考えられる. そのような場合には, registerwhen を使って実行結果を確認する.

- name: Set hoge commands
  template:
    src: hoge.sh.j2
    dest: /tmp/hoge.sh
    mode: 0755
  register: hoge_scrpit

- name: Run hoge command
  shell: /tmp/hoge.sh
  when: hoge_script.changed

日本語で調べても地味にこれだけっていう情報が見付からなかった. registerに入ってる値は以下のページで確認出来る.

Custom Factsについて

Ansibleはホスト毎にユーザが保存した情報などを Custom Facts として,サーバマシンなどに保存し,次回の,Playbookの実行時にその値を読みだす機能がある. /etc/ansible/facts.djson 形式か ini 形式かを選択出来る. 構造化して使用したい場合には json しか選択肢がない. templatejson 生成するの辛い. 以下のようにPlaybookでディレクトリの作成と配置をする.

- name: Create custom fact directory
  file:
    path: /etc/ansible/facts.d
    state: directory

- name: Set hoge fact
  template:
    src: hoge_data.fact.j2
    dest: "/etc/ansible/facts.d/hoge_data.fact"

多分しないとは思うけど(自分はしたんだけど)このfactファイルのパーミッションを下手にいじるとフォーマットエラーと言ってgathering facts中に落ちるので厳しい. もっと,適当に答えて欲しい.

次回の実行からCustom Factsの中身は, ansible_local という変数から呼び出す事が出来るようになる. 今回の場合は ansible_local.hoge_data 以下に jsonKey-Value で入ってるのでよしなに使えばよい.

when と with_dict を同時に使う

条件分岐をしつつ,ループを実行出来る. 具体的には, with_dict が先に実行されて, with_dict のループの中で when が動くような感じ.

- debug:
    msg: ""
  with_dict:
  when: item.key == "hoge"

特権モードでの実行について

Ansibleでは特権モードでの実行には become を使用する. Understanding privilege escalation: become — Ansible Documentation

使用する方法としては,Playbookに直接書くか,task毎に書いていくかのどちらかになる.

Playbookでは以下のように書く

- hosts: test
  become: yes
  roles:
    - nsd

Taskでは以下のように書く

- name: Restart nsd service
  service:
    name: nsd
    state: restarted
  become: yes

基本的にはtask単位で書いた方が良いらしい. 特に,roleの再利用性を上げるには良い.

Roleの中で設定しておけば,Playbook側の書き方に依存せずに実行出来る可能性が上がる.

また, become と同時に become_user を設定する事で,実行するユーザも指定する事が出来る.

- name: Restart nsd service
  command: nsd-control restart
  become: yes
  become_user: nsd

こちらは基本的なパーミッション設定さえそれぞれのtaskでしていればあまり使用する機会はないが,上記のように特定のユーザでのみ実行出来るようなコマンドがある場合に使用する.

名前解決した結果のIPアドレスを挿入

Ansibleで名前解決した結果を元にtemplateなどを使用したい場合がある. 例えば,PromethuesのExpoterを配置したい時に,そのホストがPublic IPとPrivate IPアドレスを持っていて,Private IPアドレスだけでホストしたい場合などに使用する.

{{ lookup('dig', ansible_host) }}

この ansible_host の部分は文字列でかまわない. 自分の場合はアクセスする時のホスト名に対応したIPアドレスを入れたかったから,こんな感じに書いた.

逆にPublic IPアドレスを書きたい場合などに,Publicなドメイン名から設定を入れてもよい. ただ,この機能を使用するには dnspython をインストールする必要がある.

ハンドラーをPlaybookの途中でエラーが発生しても実行する.

インストールスクリプトなどだと初回の実行順序的に必ずエラーが発生してしまい,notifyした,handlerが実行されない事があります. その時に,Playbookの途中でエラーが発生した場合にも,notify先のhandlerを強制的に実行するオプションがあります.

Playbookのhostの中に force_handlers: True と記述すれば何がなんでもhandlerを動かしてくれる.

- hosts: test
  force_handlers: True
  become: yes
  roles:
    - nsd

PlaybookからPlaybookを呼び出す

複数のホストなどがある場合に,後から特定のホストに対してタスクを実行させたい場合などを考えると便利. 全部のPlaybookを実行する site.yml などの中で import_playbook を使用して他のPlaybookを読みだし,,それぞれのPlaybookでホストやグループ,まとまり事にタスクを作っていく. 後から,特定のグループを追加する時や,使用しなくなった,playbookを削除していくのにも,分かりやすくて便利.

- import_playbook: nsd.yml
- import_playbook: unbound.yml
- import_playbook: node_exporter.yml

Playbookを1台が成功したら残りに対して実行するように設定する

同時に実行するマシンの指定にはPlaybookに serial という値を追加して指定する. この serial は値か配列を受けとることが出来る. 値を受けとった場合にはその値の台数ずつ実行する. 配列を受け取った場合には,配列の順の台数ずつ実行し,それで終了しない場合には,配列の一番最後の要素だけ実行していく. また,途中でtaskが失敗した場合には,以降の実行は中断される. よって,これを用いることで,最初の1台が成功した場合に残りも実行という処理を行う事が出来る.

以下のように設定すると,1台を実行し,それが成功した場合に残りのホストに対して,並列に実行するようになる.

- hosts: hoge
  become: yes
  serial:
    - 1
    - "100%"
  roles:
    - hogehoge

commandモジュールで実際に実行されるコマンドをcheck modeで確認したい時

Ansibleをデバッグしていてcheckモードで動作を確認した事がよくある. ただ,普通にcheckオプションを付けて実行するだけだと,templateなどの変更の確認などは出来るが,commandモジュールはただただ,skipと表示されるだけで実際に実行されるコマンドなどを確認する事が出来ない. commandもパラメータなどをテンプレートで出力していると,どのようなコマンドが実際に実行されるのか気になる. その場合は, -vvv オプションを併用すれば良い. -vvv オプションを使用していれば,出力内容の, _raw_params の部分にコマンドの実行結果が付加された出力される.

-vvv を付加していると command モジュールの場合 skip の場合でも以下のように出力される.

skipping: [hogehoge] => (item={'key': 'example.com'})  => {
    "ansible_loop_var": "item",
    "changed": false,
    "invocation": {
        "module_args": {
            "_raw_params": "pdnsutil set-meta example.com ALSO-NOTIFY 10.1.1.1 10.1.1.2 10.1.1.3",
            "_uses_shell": false,
            "argv": null,
            "chdir": null,
            "creates": null,
            "executable": null,
            "removes": null,
            "stdin": null,
            "stdin_add_newline": true,
            "strip_empty_ends": true,
            "warn": true
        }
    },
    "item": {
        "key": "example.com"
    }
}

一部のタスクだけローカルホストで実行したい時

Ansibleでホストを対象にroleなどを実行している場合に一部のタスクをローカルで実行したい時がある. 例えば,OpenStack関係のmoduleを実行したい場合などは,リモートホストにopenstacksdkなどのインストールを極力行いたくない. そのような場合には,タスクに delegate_to: localhost を追加する事で,一部のタスクをローカルホストで実行する事が出来る. また,同じ実行roleの中に居るので,その中で実行されたregisterなどはリモートホストで実行中もそのまま実行する事が出来る.

- name: get server info
  os_server_info:
    cloud: mycloud
    server: ""
  register: result
  delegate_to: localhost
- debug:
    msg: ""

特定のGroupからホストをランダムに選択して実行

Playbookのhostsの指定でもjinja2テンプレートを利用出来る。 なので、この部分でシャッフルしてn個取得したら、ランダムにn個のホストに対して実行出来る。 例えば、 some_group_name からシャッフルして4つのホストで実行する場合は以下のようになる。

- hosts: "{{(groups['some_group_name'] | shuffle)[0:4]}}"
  become: yes
  roles:
    - role: randm_exec_role
      tags: randm_exec_role

Ansibleが実行されるホストの一覧を表示

Playbookを実行する前に対象のホストを確認して、安全かどうかを確認したいと思う事が多々ある。 Ansibleではこの確認は --list-hosts オプションで行う事が出来る。

$ ansible-playbook -i inventories/hogehoge hoge_playbook.yml --list-hosts

Ansibleで実行されるタスクの確認

Playbookでtagの指定だとか、リミットなどをした時にどのようなタスクが実行されるのか確認したい事がある。 Ansibleではこの確認は --list-tasks オプションで行う事が出来る。

$ ansible-playbook -i inventories/hogehoge hoge_playbook.yml --list-tasks

巨大なファイルやテンプレートを展開した場合にdiffが表示されない問題

Ansibleを用いてjinja2のtemplateを展開するといった処理は多くのユーザが行っている。 また、多くのユーザがAnsible Playbookの実行前に --check --diff modeでPlaybookの動作確認を行う。 しかし、この時に、テンプレートの展開時のファイルサイズが大きすぎた場合に、Ansibleはdiffの表示を行ってくれずに以下のようなエラーを発生させる。

diff skipped: destination file size is greater than 104448
diff skipped: source file size is greater than 104448

これは、Ansibleの max_diff_size という変数のdefault値に起因する。 ファイルが max_diff_size byte 大きい場合に、AnsibleはDiffの表示を行わない。 この設定は環境変数や ansible.cfg で変更が出来る。 自分は多くの場合は以下のように、 ansbile.cfg で設定を行っている。

[defaults]
max_diff_size = 1044480

max_diff_size の値は自分の環境により変更しよう。 また、超巨大なfileのdiffはdiffが小さい場合は問題ないが、初回の展開時などは膨大になるので、注意した方が良い。

同じgroup_varsとかを使ったまま実行対象を変更させたい時

開発環境構築用のAnsible-Playbookなどで、設定とかはそのままで良いんだけど、対象となるデプロイ先したいという事がある。 例えば、NginxのNode数を減らして、別のWorker Nodeを増やしたい場合だとか、全ての開発環境をデプロイするのは時間がかかるので必要な開発環境のみをデプロイしたい場合などがある。 この時に、inventoryのフォルダを増やしていくと、例えば変数の修正や変更などがある場合に、変更対象が開発環境という1つの事に対してどんどん増えて行く。 このような場合には、1つのinventoryのフォルダの中に複数のhostsファイルを用意するのが良い。

例えば、以下のようにhosts fileが aaa.yamlbbb.yaml の2つ用意する。

$ tree .
.
├── inventories
│   └── hoge
│       ├── aaa.yaml
│       ├── bbb.yaml
│       └── group_vars
│           └── all
│               └── vars.yaml
└── test.yaml

4 directories, 4 files

この2つのhostsファイルからはそれぞれgroup_varsの内容などは利用する事が出来る。 実行については、 -i オプションでホストファイルまでのpathを指定すれば動作してくれる。

$ ansible-playbook -i inventories/hoge/aaa.yaml test.yaml

サービスのデプロイなどに使うには少し、良くない設定ではあるが。 開発環境のためのinventoryであれば、このような使い方をしても問題無いと思う。

Ansibleの ansible_default_ipv4 が何の値を返すのかという事について。

ansible_default_ipv4 は複数のNICや複数のIPアドレスを持つ環境だと、期待した値ではないIPアドレスが設定される事がある。 では、実際にには何の値が設定されるのかというと、多くの場合にLinuxでdefault経路時にsrc ip addressとして設定された値が利用される。 より具体的にはIPv4では 8.8.8.8 (Google Public DNS)への通信に利用されるIPアドレスが ansible_default_ipv4 として利用される。 ansible_default_ipv4default は default routeの事だと思ってAnsibleを書くとミスが少なくなるかもしれない。

参考ソースコード

Ansibleでやめた方が良いこと

変数から yamljson に変換して設定ファイルを生成する

Ansibleは以下のようにする事で、ansibleの変数からyamlなどのフォーマットを生成する事が出来る。

- name: Copy using inline content
  copy:
    content: "{{ hogehoge | to_yaml }}"
    dest: /etc/hoge/conf.yaml

hogehoge はAnsibleの変数として構造化されたデータで、Ansibleの変数として設定した値をそのまま設定ファイルに起こすことが出来る。 ただ、この方法は個人的にはあまり推奨されない。 以下の理由によりAnsible Playbookの管理を難しくなってしまいます。

  • hash_behaviour の設定により、期待した値が入っているか分からない
  • 構造化された変数の利用範囲を正しく知る事が出来ない

設定ファイルにするという事はある程度の構造化されたデータがありますが、例えば共通の設定としてgroup_varsに定義した物と上書きした物が最終的にどんなデータになるのか想像が難しくなります。

また、例えば構造化されたデータの中で使われている変数の値を変えたいと思った時にこの変数が何処で利用されているのかを分かりにくくします。 to_yaml で利用される場合には構造内のデータが使われている事は検索などでは、発見する事が出来ません。

以上の理由から、Ansibleでは to_yamlto_json を利用する事を自分は推奨しません。 設定ファイルの生成などは、Jinja2テンプレートを正しく使って行くことをおすすめします。