「Ansibleは冪等なのに、なぜサーバは壊れていくのか」 第3回:なぜドリフトは気づかれないまま進むのか
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
シリーズ全体については、以下のまとめブログで整理しています。
📋 目次
- はじめに
- changedが出ないドリフトとはどういう状態か
- changedが出ないドリフトの具体的なパターン
- 「2回実行してchanged=0」という確認でも検知できない
- Playbookが正常終了していてもドリフトは進む
- まとめ
- 次回予告
- 連載一覧:「Ansibleは冪等なのに、なぜサーバは壊れていくのか」
1. はじめに
第2回 では、--checkモードがドリフト検知ツールとして機能するケースと機能しないケースを整理しました。templateモジュールで管理しているファイルの手動変更は--checkで検知できましたが、shellモジュールが管理している処理、Ansible管理外のファイル・プロセスの変化、パッケージのマイナーアップデートは検知できないことを確認しました。
第3回はその問題をさらに掘り下げます。--checkだけでなく、「Playbookを通常実行してもchanged=0のまま通過するドリフト」の存在を整理します。第2回 が「確認ツールとしての限界」だったのに対して、第3回は「Playbookの実行結果そのものが信頼できない場面」を扱います。
2. changedが出ないドリフトとはどういう状態か
このセクションで扱う「changedが出ない」とは、Playbookの実行結果としてchanged=1が返らず、changed=0のまま通過することを指します。ドリフトが実際に発生していても、実行結果にはそれが現れません。
changed=1が出るドリフトと、changed=0のまま通過するドリフトの違いを整理します。
changed=1が出るドリフトは、Playbookを再実行すれば検知・修正されます。第1回 で確認したtemplateモジュールによる上書きがその例でした。このタイプのドリフトは、Playbookを定期実行していれば対応できます。
問題はそれよりも、changed=0のまま通過するドリフトの存在です。Ansibleはタスクに記述されたdesired stateとリモートノードの現在の状態を比較しますが、タスクに記述されていない要素は比較の対象になりません。この構造上の特性が、「changedが出ないドリフト」を生みます。
2種類のドリフトを表に整理します。
| 種類 | Playbook実行時の動作 | 検知されるか |
|---|---|---|
| changedが出るドリフト | 差分を検出してchanged=1を返す |
される |
| changedが出ないドリフト | changed=0のまま通過する |
されない |
「changedが出ないドリフト」が存在するという認識を持つことが、このセクションの到達点です。次のセクションでは、このドリフトが具体的にどのような場面で発生するかを、いずれもchanged=0のまま通過する例として整理します。
3. changedが出ないドリフトの具体的なパターン
Ansibleが管理していない領域で起きる変化として、以下のパターンを整理します。いずれもPlaybookを実行してもchanged=0のまま通過します。
Ansible管理外のcronジョブが設定ファイルを書き換えている
cronジョブがAnsibleの管理対象外のスクリプトを定期実行し、そのスクリプトが設定ファイルを書き換えている場合です。Playbookにそのファイルを管理するタスクがなければ、changed=0のまま通過します。
アプリケーションが、Ansibleの管理範囲外の設定項目を起動時に書き換える
Ansibleがlineinfileモジュールなどで設定ファイルの一部の項目のみを管理しているケースです。同じファイル内に、アプリケーションが起動時に自動生成・更新する別の項目が含まれている場合、その項目はAnsibleのタスクに記述されていないため、比較の対象になりません。Ansibleが管理している項目に変化がなければ、アプリケーションが書き換えた別の項目がどれだけ変化してもchanged=0のまま通過します。第1回 のlineinfile検証で確認した「管理下の行は復元されるが、管理外の行は残り続ける」構造と同じ形です。
セキュリティツールが、Ansibleの管理範囲外の属性を変更する
Ansibleがfileモジュールなどでパーミッション(mode)を管理しているケースです。SELinuxコンテキストやACLなど、パーミッションとは別の属性をセキュリティツールが独自に変更している場合、その属性はAnsibleのタスクに記述されていないため、比較の対象になりません。管理しているmodeに変化がなければ、管理していない別の属性がセキュリティツールによって変更されてもchanged=0のまま通過します。
パッケージの自動更新でライブラリのバージョンが上がっている
state: presentはパッケージの存在有無のみをdesired stateとして定義しており、バージョンはタスクに記述されていません。そのため、マイナーアップデートでバージョンが上がってもchanged=0のまま通過します。冪等性シリーズ第4回 セクション2「state-presentとstate-latestは何が違うのか」で扱ったstate: presentの動作が、ここでも関係します。
これらに共通しているのは、「Playbookのタスクに記述されていない要素は、Ansibleの確認対象にならない」という構造です。ファイルやリソースの一部を管理している場合でも、管理範囲の外で起きる変化には同じ構造が当てはまります。次のセクションでは、この構造が「2回実行してchanged=0になること」という確認手法にどう影響するかを整理します。
4. 「2回実行してchanged=0」という確認でも検知できない
冪等性シリーズ第9回 セクション2「『2回実行してchanged=0になること』は何を意味するのか」では、「2回実行してchanged=0になること」をPlaybookの冪等性を確認する手法として整理しました。この確認が成立する前提として、「外部状態が安定していること」と「実行経路が1種類に限られること」の2つが示されています。
ドリフトの文脈ではこれに加えて、もう一つの前提が必要です。「Ansible管理外の変化が発生していないこと」です。2回実行してchanged=0になったとしても、以下のことは確認されていません。
- Ansible管理外のcronジョブが設定ファイルを書き換えていないこと
- アプリケーションが、Ansible管理範囲外の項目を書き換えていないこと
- セキュリティツールが、Ansible管理範囲外の属性を変更していないこと
- パッケージのマイナーアップデートが発生していないこと
「2回実行してchanged=0」という確認は、Playbookが管理している範囲に対しては有効ですが、changedが出ないドリフトに対しては機能しません。冪等性の確認とドリフトの不在は、別の話です。
5. Playbookが正常終了していてもドリフトは進む
セクション2〜4の内容を踏まえて、この回の構造的な結論を実機で確認します。
セクション3で整理した4パターンのうち、①(Ansible管理外のcronジョブによる書き換え)を実際に再現します。検証には、第1回・第2回 で使用したplaybooks/configure_nginx_worker_processes.yml(lineinfileモジュール、worker_processesの行のみを管理)をそのまま使用します。このPlaybookは/etc/nginx/mime.typesには一切言及していません。
1.【事前確認(1回目の実行)】
↓
2.【cronジョブによる自動書き換え】
↓
3.【2回目の実行】
の順に検証します。第1回・第2回の検証がPlaybookで管理しているファイル(の一部)を対象にしていたのに対し、今回はどのPlaybookのタスクにも一切記述されていないファイルを対象にする点が、これまでの検証との違いです。
■ 検証内容(Ansible管理外ファイルへのcronジョブによる書き換えが、Playbook実行で検知されるか)
1.【事前確認(1回目の実行)】
【コントローラーノード側】
ファイル名: playbooks/configure_nginx_worker_processes.yml(第1回・第2回 で使用したものと同一)
実行コマンド
ansible-playbook -i inventory.ini playbooks/configure_nginx_worker_processes.yml
第1回・第2回 の検証で全ノードのdesired stateへの復元が確認済みのため、ここではchanged=0になることの再確認のみ行います。
▼ 実行結果
PLAY [Configure nginx worker_processes (lineinfile)] ****************************************************************************************************************************************
TASK [worker_processesの値を管理する] *******************************************************************************************************************************************************
ok: [ubuntu-node1]
ok: [ubuntu-node3]
ok: [ubuntu-node2]
PLAY RECAP **********************************************************************************************************************************************************************************
ubuntu-node1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu-node2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu-node3 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
【リモートノード側】
ubuntu-node1上で、/etc/nginx/mime.typesの現在の内容を確認します。
▼ 実行結果
control@ubuntu-node1:~$ cat /etc/nginx/mime.types
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
image/svg+xml svg svgz;
image/webp webp;
application/font-woff woff;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.wap.wmlc wmlc;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
application/vnd.openxmlformats-officedocument.wordprocessingml.document docx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx;
application/vnd.openxmlformats-officedocument.presentationml.presentation pptx;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}
2.【cronジョブによる自動書き換え】
ubuntu-node1上で、検証用のcronジョブを一時的に登録します。1分ごとに/etc/nginx/mime.typesへ1行追記するジョブです。
実行コマンド
(sudo crontab -l 2>/dev/null; echo '* * * * * echo "# added by cron $(date)" >> /etc/nginx/mime.types') | sudo crontab -
登録内容の確認:
control@ubuntu-node1:~$ sudo crontab -l
* * * * * echo "# added by cron $(date)" >> /etc/nginx/mime.types
1〜2分待機し、mime.typesに変更が加わったことを確認します。
▼ 変更後のファイル内容
control@ubuntu-node1:~$ cat /etc/nginx/mime.types
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
image/svg+xml svg svgz;
image/webp webp;
application/font-woff woff;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.wap.wmlc wmlc;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
application/vnd.openxmlformats-officedocument.wordprocessingml.document docx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx;
application/vnd.openxmlformats-officedocument.presentationml.presentation pptx;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}
# added by cron Fri Jul 17 01:21:01 AM UTC 2026
# added by cron Fri Jul 17 01:22:01 AM UTC 2026
※上記の通りファイルの末尾に2行追加されたことを確認済み
変更が確認できたので、後続の実行に影響しないよう、cronジョブを削除します。
sudo crontab -r
削除の確認:
control@ubuntu-node1:~$ sudo crontab -l
no crontab for root
3.【2回目の実行】
実行コマンド
ansible-playbook -i inventory.ini playbooks/configure_nginx_worker_processes.yml
▼ 実行結果
PLAY [Configure nginx worker_processes (lineinfile)] ****************************************************************************************************************************************
TASK [worker_processesの値を管理する] *******************************************************************************************************************************************************
ok: [ubuntu-node2]
ok: [ubuntu-node1]
ok: [ubuntu-node3]
PLAY RECAP **********************************************************************************************************************************************************************************
ubuntu-node1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu-node2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu-node3 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
【リモートノード側】
- ubuntu-node1(192.168.56.31)
※2回目実行後のファイル内容:
control@ubuntu-node1:~$ cat /etc/nginx/mime.types
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
image/svg+xml svg svgz;
image/webp webp;
application/font-woff woff;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.wap.wmlc wmlc;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
application/vnd.openxmlformats-officedocument.wordprocessingml.document docx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx;
application/vnd.openxmlformats-officedocument.presentationml.presentation pptx;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}
# added by cron Fri Jul 17 01:21:01 AM UTC 2026
# added by cron Fri Jul 17 01:22:01 AM UTC 2026
■ 結果
1回目の実行では、全ノードでchanged=0となり、worker_processesが管理対象であるにもかかわらず、/etc/nginx/mime.typesには一切変更が加えられていないことを確認しました。
cronジョブによる自動書き換えでは、mime.typesの末尾に# added by cronで始まる行が2行追加されました。この時点で、ubuntu-node1のmime.typesはdesired stateと異なる状態になっています。
この状態で2回目のansible-playbookを実行した結果、cronジョブによる変更を加えたubuntu-node1を含め、全ノードがchanged=0のままでした。1回目の実行結果と同じであり、--checkを使わない通常実行であっても、この変更は一切検知されませんでした。
2回目実行後のリモートノード側を確認すると、cronジョブが追記した2行はそのまま残っていました。Playbookが管理しているworker_processesの行には手を加えていないため、そちらも変化していません。
この結果から、configure_nginx_worker_processes.ymlのタスクに一切記述されていないmime.typesというファイルは、通常実行であっても比較対象にすら含まれないことが分かります。第2回 セクション6では--check実行時に同様の結果を確認しましたが、今回の検証によって、--checkに限らず通常実行でも同じ限界が働くことが実機で確認できました。
Playbookがchanged=0で正常終了したことは、「Ansibleが管理している範囲に差分がなかった」ことを意味するのであり、「サーバ全体がdesired stateと一致している」ことを意味しません。今回のケースでは、worker_processesという管理対象は正しい状態を保っていましたが、mime.typesというファイルは、cronジョブによって書き換えられた状態のまま、2回のPlaybook実行を通過しました。「Playbookが正常終了している=サーバが正しい状態にある」という理解が成立しない場面があることが、この検証で確認できました。
6. まとめ
この回で整理した内容を確認します。
- 「changedが出ない」とは、Playbookの実行結果として
changed=1が返らずchanged=0のまま通過することを指す。ドリフトが実際に発生していても、実行結果にはそれが現れない - changedが出ないドリフトは、Playbookのタスクに記述されていない要素に対して発生する。cronジョブなど完全に管理外のファイルの変化だけでなく、ファイルの一部だけを管理している場合の管理外項目(アプリケーションによる自己書き換え、セキュリティツールによる管理外属性の変更)、
state: presentのようにバージョンを比較対象に含まない指定も、同じ構造に当てはまる - 冪等性シリーズ第9回 セクション2が示した「2回実行して
changed=0になること」という確認手法は、外部状態の安定・実行経路の単一性という2つの前提のもとで成立する。ドリフトの文脈ではこれに加えて「Ansible管理外の変化が発生していないこと」という前提が必要であり、changedが出ないドリフトはこの前提を崩す - Ansible管理外のファイル(
/etc/nginx/mime.types)をcronジョブで書き換えた状態を実機で作り、通常実行のPlaybookを実行したところ、全ノードでchanged=0のまま通過し、手動変更は検知されなかった。第2回 セクション6で確認した--check実行時と同じ限界が、通常実行でも同様に働くことを実機で確認した - Playbookが
changed=0で正常終了したことは、「Ansibleが管理している範囲に差分がなかった」ことを意味するのであり、「サーバ全体が正しい状態にある」ことを意味しない
7. 次回予告
次回は、「ドリフトを検知して修正する設計」を扱います。
changedが出ないドリフトを含めて、ドリフトを「防ぐ」「検知する」「修正する」という3つの設計パターンをどう運用に組み込むかを整理します。
📑 連載の移動 前の記事:【構成ドリフト編】 第2回 | 次の記事:【構成ドリフト編】 第4回
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
シリーズ全体については、以下のまとめブログで整理しています。
8. 連載一覧:「Ansibleは冪等なのに、なぜサーバは壊れていくのか」
| 回 | タイトル | 内容(概要) |
|---|---|---|
| 第0回 | なぜAnsibleで管理しているのにサーバはずれていくのか | 構成ドリフトを「Playbookで定義したdesired stateと実際のサーバの状態がずれていく現象」として定義し、冪等性シリーズとの違いを整理する。ドリフトが「Ansibleが実行されていない時間」に起きる問題であることを理解する。 |
| 第1回 | なぜ手動変更はAnsibleの管理を壊すのか | 障害対応や一時的な直接編集がドリフトを生む構造を、templateとlineinfileの挙動の違いを通して理解する。手動変更が「次にAnsibleが実行されるまで気づかれない」空白を作ることを整理する。 |
| 第2回 | なぜdriftは--checkで検知できないのか |
--check/--diffモードをドリフト検知の手段として使う場合の限界を整理する。検知できるケースと検知できないケースを実機で切り分け、「--checkで問題が出ない=ドリフトがない」ではないことを理解する。 |
| 第3回 | なぜドリフトは気づかれないまま進むのか | changedが出ない「静かなドリフト」の構造を扱う。cronジョブやアプリケーションの自己書き換え、パッケージの自動更新など、Ansible管理外で起きる変化がPlaybook実行結果に現れない理由を理解する。 |
| 第4回 | ドリフトを検知して修正する設計 | シリーズの結論として、ドリフトを「防ぐ」「検知する」「修正する」の3つの設計パターンを整理する。定期実行・アラート化・自動修正フローなど、運用に組み込むための具体的な設計を扱う。 |