Ansibleの冪等性は導入前に2回実行して壊れ方を見る

Ansibleの冪等性は導入前に2回実行して壊れ方を見るを描いたアイキャッチ画像

ミニPCやLXCの初期設定を毎回手で当て直していて、再構築のたびに「あの一行を入れ忘れた」が起きる。その手順メモをそろそろAnsibleへ移したい——けれど、playbookが途中で止まったときに何が変わったのか分からなくなるのが怖い。今回はそういう段階の運用者を想定した記録です。

この記事はAnsibleの紹介ではありません。家庭ラボの本番に入れる前に、使い捨てのVM上で小さなplaybookをわざと壊し、次の点を実コマンドと実ログで確認したものです。

  • 正常実行の初回と2回目で、changed/failedの件数がどう変わるか
  • 存在しないパッケージ名を渡したとき、どのタスクで止まるか
  • テンプレートの未定義変数で、どんなエラーが出るか
  • nginx設定を壊したとき、validateの有無でreloadの結果がどう変わるか
  • --checkと通常実行の差分
目次

検証環境

検証は使い捨ての検証用VM(Debian bookworm系)で行いました。最小構成のLXCではパッケージ導入やsystemdサービスの起動確認が安定して取れないため、ansibleとnginxを実際に入れ、systemctlでサービス状態まで見られるVMを正直な検証環境として選んでいます。接続先IPなどは伏せます。

対象ホストはlocalhost(ansible_connection=local)で、playbook側からこのVM自身に設定を当てる構成です。インベントリは1行だけです。

localhost ansible_connection=local

実行コマンドと検証用playbook

正常系のplaybook(site.yml)は、パッケージ導入・テンプレート配置・サービス起動の3タスクだけにしました。

- hosts: localhost
  become: true
  vars:
    pkg_name: nginx
    lab_name: ansible-lab
  tasks:
    - name: install package
      ansible.builtin.apt:
        name: "{{ pkg_name }}"
        state: present
        update_cache: true
    - name: deploy index
      ansible.builtin.template:
        src: templates/index.html.j2
        dest: /var/www/html/index.html
        mode: '0644'
        validate: /usr/bin/test -s %s
    - name: ensure nginx started
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

テンプレートは1行です。

hello {{ lab_name }}

実行は次の順で行いました。

# ansible-playbook -i inventory.ini site.yml          # 初回
# ansible-playbook -i inventory.ini site.yml          # 2回目(冪等性確認)
# ansible-playbook -i inventory.ini site.yml -e pkg_name=nginxx
# ansible-playbook -i inventory.ini undef_var.yml
# ansible-playbook -i inventory.ini bad_conf_novalidate.yml
# ansible-playbook -i inventory.ini bad_conf_validate.yml
# ansible-playbook -i inventory.ini site.yml --check --diff

結果

初回と2回目:changed=1からchanged=0へ

初回実行のPLAY RECAPです。

設定項目 [install package] *********************************************************
ok: [localhost]

設定項目 [deploy index] ************************************************************
changed: [localhost]

設定項目 [ensure nginx started] ****************************************************
ok: [localhost]

設定項目 設定項目 *********************************************************************
localhost                  : ok=4    changed=1    unreachable=0    failed=0

パッケージは事前に入っていたためinstall packageok、実際に変わったのはテンプレートを置いたdeploy indexの1件でchanged=1です。続けて同じplaybookをもう一度流すと、こうなります。

設定項目 設定項目 *********************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0

2回目はchanged=0。これが冪等性が効いている状態です。手順メモのままだと「2回流すと毎回どこかが書き換わる」のか「もう何も起きない」のかが見えませんが、Ansibleなら2回目のRECAPで一目で確認できます。逆に言えば、2回目でもchangedが出続けるなら、それは自動化が完成していないサインとして扱えます。

パッケージ名を間違えると、その場で止まる

pkg_name=nginxxという存在しない名前を渡すと、最初のタスクで止まりました。

設定項目 [install package] *********************************************************
fatal: [localhost]: 設定項目! => {"changed": false, "msg": "No package matching 'nginxx' is available"}

設定項目 設定項目 *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1

failed=1で停止し、後続のテンプレート配置やサービス起動には進みません。どこで何が原因で止まったかがメッセージに出るため、手作業より追いやすい失敗の出方です。

テンプレートの未定義変数も、適用前に止まる

テンプレートに定義していない変数missing_valueを入れて流すと、こう出ます。

設定項目 [undefined template variable] *********************************************
fatal: [localhost]: 設定項目! => {"changed": false, "msg": "AnsibleUndefinedVariable: 'missing_value' is undefined. 'missing_value' is undefined"}

設定項目 設定項目 *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1

ファイルを生成して配置する前に止まるので、中途半端なファイルがサーバに残りません。

nginx設定を壊したとき:validate無しはサービスのreloadで落ちる

ここが今回いちばん見たかった箇所です。まずvalidateを付けずに不正なnginx設定(server { listen 80; invalid_directive on; })を書き、そのままreloadさせました。

設定項目 [write invalid nginx config without validate] *****************************
changed: [localhost]

設定項目 [reload nginx without validate] *******************************************
fatal: [localhost]: 設定項目! => {"changed": false, "msg": "Unable to reload service nginx: Job for nginx.service failed."}

設定項目 設定項目 *********************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=1

設定ファイルの書き込み(copy)はchangedとして通ってしまい、壊れた設定がディスクに残った状態でreloadに進み、そこでJob for nginx.service failedで落ちます。つまりvalidate無しだと「壊れた設定を置く」までは止まらず、サービス操作の段で初めて失敗します。

validate付きの結果と、その制約

validate付き(validate: /usr/sbin/nginx -t -c %s)のplaybookは、同じ不正設定を書く内容にしてあります。結果はこうでした。

設定項目 [write invalid nginx config with validate] ********************************
ok: [localhost]

設定項目 設定項目 *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0

ここは正直に書きます。この実行はvalidate無しの直後に行ったため、配置先には既に同じ不正な内容が残っていました。copyは内容が一致していると変更不要と判断してok(changed=0)を返し、変更が発生しなかったためnginx -tによるvalidateそのものが走っていません。

つまりこのログは「validateが不正設定を弾いた」ことを示してはいません。示しているのは、copyが無変更ならchanged=0で何も起きないという冪等性の挙動の方です。validateが実際にnginx -tで弾く様子を見たい場合は、配置先を一度正常な設定に戻し、copyが変更を試みる状態から流す必要があります。validateは変更が走るときにステージしたファイルに対して実行される仕組みなので、無変更だと発火しないためです。

その上で言えるのは、validate無しの方は確実に「壊れた設定を置き、reloadで落ちる」経路を踏んだということです。設定変更の度にサービスへ適用してから失敗に気づく運用は、家庭ラボでもサービス断につながります。

実サービスの確認:reload失敗後も旧プロセスは生き残った

壊れた設定が残った状態でsystemctlcurlを見ると、こうでした。

active
● nginx.service - A high performance web server and a reverse proxy server
    Process: ExecReload=/usr/sbin/nginx ... -s reload (code=exited, status=1/設定項目)
   Main PID: 2985 (nginx)

hello ansible-lab
http_status=200

is-activeactivecurlhttp_status=200hello ansible-labを返しています。一見すると無事ですが、これは限定的な観測です。ExecReloadstatus=1/設定項目で失敗しており、reloadが効かなかった一方でMain PIDの旧マスタープロセスがそのまま生き残って応答を返しているだけ、という状態です。「200が返るから設定は正しい」とは言えません。再起動を挟めば、壊れた設定で起動できず落ちる可能性があります。RECAPのfailed=1systemctlExecReload失敗を併せて見ることが大事です。

–checkと通常実行

収束後に--check --diffと通常実行を続けて流すと、どちらもchanged=0でした。

# --check --diff
設定項目 設定項目: ok=4    changed=0    failed=0
# 通常実行
設定項目 設定項目: ok=4    changed=0    failed=0

今回の範囲では--checkは「これ以上変わる予定はない」ことの確認に使い、予告どおりに通常実行もchanged=0になりました。注意したいのは、--checkは変更を予告するだけで実際には適用しない点です。今回見たreload失敗のように、設定をサービスへ適用して初めて表面化する失敗は、ドライランでは踏みません。--checkは「何が変わりそうか」を見る道具で、「適用したら本当に動くか」までは保証しないと考えておくと安全です。

比較:家庭ラボで設定を当てる手段

手段 失敗の検出 変更範囲の把握 2回目の収束 サービスを壊す前に止まるか
手順メモで手動 自分の目視のみ 記憶頼み 毎回手作業 自分でnginx -tを打つ前提
通常実行(validate無) 適用後にfailedで判明 RECAPのchangedで追える changed=0で確認できる 止まらない(reloadで初めて失敗)
--check --diff 予告のみ、適用しない diffで事前に見える 予告できる 適用しないので壊さないが直しもしない
validate付き 変更時にnginx -t等で事前に弾ける(※無変更時は発火せず) RECAPのchangedで追える changed=0で確認できる 変更が走れば適用前に止まる

読者の疑問への回答

–checkで見える失敗と、実行して初めて分かる失敗は何が違うのか。 --checkは「どのタスクが変更を出しそうか」を予告します。一方で、壊れた設定をサービスにreloadして落ちる、といった失敗は実際に適用して初めて起きます。今回のreload失敗はその典型で、ドライランの対象外です。

2回目実行でchanged=0に近づける意味は何か。 「もう何も書き換わらない」状態に到達したことの確認です。2回目でもchangedが出続けるなら、毎回設定を上書きしているだけで再現可能になっていない、というサインです。

templateやservice reloadの前にvalidateを入れる価値はあるか。 validate無しでは、壊れた設定がディスクに残った上でreloadに進んで失敗しました。validateは変更が走るときにnginx -t等で事前に弾ける仕組みなので、設定変更を伴うタスクには入れる価値があります。ただし今回のログのように、配置内容が既に同じだとvalidateは発火しません。validateは「変更時の保険」であって、無変更時の検査ではない点は押さえておいてください。

手順メモからAnsibleへ移す最初の単位はどこがよいか。 今回のsite.ymlのように、パッケージ導入・設定ファイル配置・サービス起動の3点セット、しかも2回流してchanged=0になることを確認できる小さな単位から始めるのが無難です。最初から全部を移そうとせず、壊れ方を見られる最小の塊で慣らすのが安全です。

Ansibleを家庭ラボに入れてよい条件・まだ早い条件

入れてよいと判断できるのは、(1)使い捨て環境で2回実行してchanged=0に収束することを自分の目で確認した、(2)設定変更を伴うタスクにvalidateを入れ、無変更時には発火しないことも理解した、(3)failed時にどのタスクで止まるかをログで追える、という状態に達したときです。

逆に、--checkで出る予告とサービスの実挙動を同じものだと思っている段階や、2回目実行の収束をまだ見ていない段階なら、本番に入れる前にこの記事のような小さな破壊実験を一度通すことをおすすめします。playbookは書けたことより、失敗時にどこで止まり、2回目に余計な変更を出さないかを先に見ておく方が、家庭ラボでは確実に役立ちます。

まとめ

  • 初回changed=1→2回目changed=0で冪等性を確認できた。
  • パッケージ名ミスと未定義変数は、適用前にそのタスクで止まった。
  • validate無しの不正nginx設定は、設定を置いた上でreloadに進んで失敗した。validate付きは今回は無変更で発火せず、検証としては片側のみ確認できた。
  • reload失敗後も旧プロセスが残り200を返したが、これは「正しい」状態ではない限定的観測。
  • --checkは変更予告までで、適用して初めて分かる失敗は別にある。

この検証を回している環境

この検証は、自宅の常設ラボ(使い捨てVM/LXCを回す母艦+GPU+VLAN分離ネットワーク)で動かしています。使っている機材と選定理由、全体構成は1本にまとめています。

ラボ構成のまとめを見る →
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

目次