変える前に、何が変わるのかを見る
目標
チェックモードとdiffを、「フラグ2つ」ではなく「タスクごとに決める振る舞い」として扱います。偽の失敗を自分で作って直し、承認に添付する計画の出力物と、ドリフトゲートまで作成します。
なぜ重要なのか
本番サーバーにプレイブックを初めてかける日、このプレイブックが20台に何をするのか、誰にもわかりません。--checkは、その場所に置かれた道具ですが、2回裏切ります。チェックモードがクリーンだったのに本当の実行が失敗し、チェックモードが真っ赤に失敗したのに本当の実行は問題ありません。2つの裏切りの原因は同じです。チェックモードは、実際には実行せずに結果を推測するモードであり、推測の品質はモジュールごとに異なります。そのため、チェックモードを信頼できるものにする作業は、フラグを付ける作業ではなく、タスクごとに「あなたはチェックモードでこう振る舞え」を決めてあげる作業です。その作業を終えると、dry runは、デプロイ前に見る図を超えて、承認の根拠であり、ドリフトを捉えるゲートになります。
ステップ
/root/anschk/hosts.iniを作成してください。[web]にweb1(ansible_host=127.0.0.1、ansible_port=2222)、[all:vars]でansible_user=rootです。そのあと、/root/anschk/site.ymlを作成してください。プレイ変数app_env(デフォルトはlab)・app_dir_mode(デフォルトは"0755")・motd_owner(デフォルトはunset)を置き、4つのタスクで、/root/anschk/appディレクトリをapp_dir_modeのパーミッションで作成し、/root/anschk/app/app.confにenv=<app_env>とlisten=8080の2行をパーミッション0644で書き込み、/root/anschk/app/motdにWelcome=labhubとOwner=unsetの2行をパーミッション0644で書き込み、最後に、motdの^Owner=の行をOwner=<motd_owner>に合わせます(そのファイルがなければ作成するように、createをオンにしてください。チェックモードでは、前のタスクがファイルを実際には作成しないからです)。まだ収束させず、--check --diffだけで実行して、出力を/root/anschk/out/check1.txtに保存してください。site.ymlを、--diffとともにデフォルト値で1回実際に実行して、出力を/root/anschk/out/converge.txtに保存してください。そのあと、3つの値をコマンドラインで上書きして、--check --diffでもう一度実行してください。app_env=stage、app_dir_mode=0750、motd_owner=platform-teamです。その出力を/root/anschk/out/diff.txtに保存してください。実際のファイルは、そのままenv=lab・パーミッション0755・Owner=unsetである必要があります。/root/anschk/probe.ymlを作成してください。ansible.builtin.commandでgetent passwd rootを実行して、pwでregisterし(changed_when: false)、その値と、今がチェックモードかどうかを、ansible.builtin.debugで、check_mode=<참거짓> pwline=<읽은 값>の形で1行に出力します(プレースホルダーは真偽値と読み取った値です)。このプレイブックを--checkで実行した出力を、標準エラー出力まで含めて/root/anschk/out/probe-check.txtに保存しますが、チェックモードでも実際の値が出力されている必要があります。スキップされるタスクが1つもあってはいけません。/root/anschk/patch.ymlを作成してください。タスクは2つです。1つは、/root/anschk/app/fresh.confにenv=labとlisten=9090の2行をパーミッション0644で書き込み、もう1つは、そのファイルの^listen=の行をlisten=9443に修正します。まず、この状態のまま--checkで実行して、失敗の出力を、標準エラー出力まで含めて/root/anschk/out/false-failure.txtに保存してください。そのあと、後ろのタスクに、チェックモードではスキップするガードを付けて、もう一度--checkで実行し、その出力を/root/anschk/out/false-fixed.txtに保存してください。2回ともチェックモードなので、/root/anschk/app/fresh.confは、最後まで作成されてはいけません。/root/anschk/dryrun.ymlを作成してください。/root/anschk/app/never.confにfeature=onの1行をパーミッション0644で書き込むタスクが1つですが、--checkなしで普段どおり実行しても、絶対にファイルを作成せず、変わるという報告だけを行う必要があります。同じプレイブックに、タスクをもう1つ置いてください。/root/anschk/app/dryrun-ran.txtにreal-runの1行をパーミッション0644で書き込む、普通のタスクです。このプレイブックを、フラグなしで実行して、出力を/root/anschk/out/dryrun.txtに保存してください。終わったら、/root/anschk/app/dryrun-ran.txtはあり、/root/anschk/app/never.confはない必要があります。/root/anschk/undef.ymlを作成してください。定義されていない変数(missing_var)を内容に使う、copyタスクが1つです。/root/anschk/badmod.ymlも作成してください。モジュール名をansible.builtin.coppyとタイプミスした、タスクが1つです。そのあと、/root/anschk/gates.shを作成して、2つのプレイブックのそれぞれに、--syntax-check・--list-tasks・--checkを順にかけ、終了コードだけを/root/anschk/out/gates.txtに1行ずつ書いてください。<파일이름> syntax-check=<코드> list-tasks=<코드> check=<코드>の形式です(プレースホルダーはファイル名とコードです)。スクリプトを実行して、ファイルを残してください。/root/anschk/plan.shを作成してください。ansible.posix.jsonをstdoutコールバックに指定して、site.ymlを--check --diff -e app_env=stageで実行し、そのJSON全体を/root/anschk/out/plan.raw.jsonに保存したあと、そこから変更されるタスクの名前だけを取り出して、JSON配列として/root/anschk/out/plan.jsonに保存します。スクリプトを実行して、2つのファイルを残してください。配列には、名前がちょうど1つだけ入っている必要があります。/root/anschk/drift-gate.shを作成してください。site.ymlをチェックモードで実行して、サマリーのchangedが0なら、1行目がCLEANで始まるメッセージを出力して0で終了し、1以上なら、1行目がDRIFTで始まるメッセージを出力して1で終了します。チェックモードの実行そのものが失敗した場合は、DRIFT-UNKNOWNで始まるメッセージを出力して、1で終了します。スクリプトに渡した引数は、そのままansible-playbookに渡される必要があります。収束した状態で、引数なしで実行した出力を/root/anschk/out/gate-clean.txtに、-e app_env=stageを指定して実行した出力を/root/anschk/out/gate-dirty.txtに保存してください。
参考
- まず、ステップ1でインベントリとプレイブックを作成してください。このPodのsshdは、127.0.0.1:2222で起動していて、キー認証がすでに設定されています。
- コマンドのヒント:
ansible-playbook -i hosts.ini site.yml --check --diffが基本の道具で、ansible-doc -t callback -lで、使えるコールバックを確認します。出力は、> 파일 2>&1で標準エラー出力まで受け取ります(プレースホルダーはファイル名です)。 - コマンドのヒント: マジック変数
ansible_check_modeは、今がチェックモードかどうかを教えてくれます。タスクに付けるcheck_modeキーは、そのタスクだけをチェックモードの外へ出したり(false)、中に閉じ込めたり(true)します。 - よくある間違い: 警告とエラーが標準エラー出力に出ることを知らず、
> 파일だけをかけて、空のファイルを残してしまうことです(プレースホルダーはファイル名です)。 - よくある間違い: 失敗で終わるコマンドの出力を保存しているうちに、
set -eのせいで、そこでスクリプトが止まってしまうことです。 - よくある間違い:
check_mode: falseを、対象を変更するタスクに付けて、dry run自体を嘘にしてしまうことです。 - このPodには、監査ログや外部の承認システムがないので、承認手順は、計画の出力物をファイルとして残すところまでを扱います。capabilityもないので、サービスの再起動のようなタスクは扱わず、ファイルとディレクトリだけを使います。
- チェックモードによる検証・ansible-playbookのオプション・lineinfileモジュール・copyモジュール・条件文
作成したらすぐに、チェックモードで先に見る
/root/anschk/hosts.iniを作成してください。[web]にweb1(ansible_host=127.0.0.1、ansible_port=2222)、[all:vars]でansible_user=rootです。そのあと、/root/anschk/site.ymlを作成してください。プレイ変数app_env(デフォルトはlab)・app_dir_mode(デフォルトは"0755")・motd_owner(デフォルトはunset)を置き、4つのタスクで、/root/anschk/appディレクトリをapp_dir_modeのパーミッションで作成し、/root/anschk/app/app.confにenv=<app_env>とlisten=8080の2行をパーミッション0644で書き込み、/root/anschk/app/motdにWelcome=labhubとOwner=unsetの2行をパーミッション0644で書き込み、最後に、motdの^Owner=の行をOwner=<motd_owner>に合わせます(そのファイルがなければ作成するように、createをオンにしてください。チェックモードでは、前のタスクがファイルを実際には作成しないからです)。まだ収束させず、--check --diffだけで実行して、出力を/root/anschk/out/check1.txtに保存してください。
チェックモードは、対象に変更を加えず、変更が起きそうなら、そのタスクをchangedとして報告します。そのため、まだ何もない状態で実行すると、作成されるもののすべてがchangedとして出ます。--diffを一緒に付けると、作成されるファイルの内容が+の行で見えますが、存在しなかったファイルが作られるdiffは、前側の範囲が0として出力されます。その表示が、「このときは本当にファイルがなかった」という証拠として残ります。出力は、標準出力と標準エラー出力を一緒に受け取るほうが安全です。
1回収束させて、値を3つ変えて、3種類のdiffを見る
site.ymlを、--diffとともにデフォルト値で1回実際に実行して、出力を/root/anschk/out/converge.txtに保存してください。そのあと、3つの値をコマンドラインで上書きして、--check --diffでもう一度実行してください。app_env=stage、app_dir_mode=0750、motd_owner=platform-teamです。その出力を/root/anschk/out/diff.txtに保存してください。実際のファイルは、そのままenv=lab・パーミッション0755・Owner=unsetである必要があります。
--diffは、チェックモード専用ではありません。本当の実行に付けると、変更しながら何を変更したのかを示してくれます。その2つの使い方を、1つのステップで並べて見ます。3つの値を変えると、diffも3種類出ます。ファイルの内容が変わるdiff、ファイルの内容ではなく属性だけが変わってJSONが比較されるdiff、そして1行だけが変わるdiffです。コマンドラインの変数は、プレイ変数より優先されるので、-e 이름=값を3回指定すればよいです(プレースホルダーは名前と値です)。最後に、実際のファイルを自分の目で確認するのを忘れないでください。
チェックモードでスキップされた取得タスクをよみがえらせる
/root/anschk/probe.ymlを作成してください。ansible.builtin.commandでgetent passwd rootを実行して、pwでregisterし(changed_when: false)、その値と、今がチェックモードかどうかを、ansible.builtin.debugで、check_mode=<참거짓> pwline=<읽은 값>の形で1行に出力します(プレースホルダーは真偽値と読み取った値です)。このプレイブックを--checkで実行した出力を、標準エラー出力まで含めて/root/anschk/out/probe-check.txtに保存しますが、チェックモードでも実際の値が出力されている必要があります。スキップされるタスクが1つもあってはいけません。
チェックモードをサポートしているかどうかは、モジュールが決めます。コマンドモジュールは、そのコマンドが何をするのかを知ることができないので、サポートしておらず、サポートしていないモジュールのタスクは、そのままスキップされます。スキップするとregister変数が空になり、後ろのタスクが崩れます。まず、そのまま1回実行して、pwline=の後ろが空になるのを、目で見てください。よみがえらせるキーは、タスクに付ける1行で、付ける基準は1つです。このタスクが対象を変更しないと確信しているか、です。
チェックモードの偽の失敗を作ってみて、ガードで直す
/root/anschk/patch.ymlを作成してください。タスクは2つです。1つは、/root/anschk/app/fresh.confにenv=labとlisten=9090の2行をパーミッション0644で書き込み、もう1つは、そのファイルの^listen=の行をlisten=9443に修正します。まず、この状態のまま--checkで実行して、失敗の出力を、標準エラー出力まで含めて/root/anschk/out/false-failure.txtに保存してください。そのあと、後ろのタスクに、チェックモードではスキップするガードを付けて、もう一度--checkで実行し、その出力を/root/anschk/out/false-fixed.txtに保存してください。2回ともチェックモードなので、/root/anschk/app/fresh.confは、最後まで作成されてはいけません。
チェックモードでは、前のタスクがファイルを実際には作成しません。そのため、そのファイルがあると前提にした後ろのタスクは、存在しないファイルを直そうとして失敗します。プレイブックが間違っているのではなく、チェックモードの限界です。失敗メッセージに、その事実がそのまま書かれて出てくるので、まず読んでみてください。直すよくある方法は、後ろのタスクに条件を1つ付けることで、今がチェックモードかどうかは、マジック変数が1つ教えてくれます。前のタスクにcheck_mode: falseを付ける方法もありますが、その瞬間にdry runではなくなるので、このステップでは使いません。
実際の実行でも絶対に変更しないタスクを作成する
/root/anschk/dryrun.ymlを作成してください。/root/anschk/app/never.confにfeature=onの1行をパーミッション0644で書き込むタスクが1つですが、--checkなしで普段どおり実行しても、絶対にファイルを作成せず、変わるという報告だけを行う必要があります。同じプレイブックに、タスクをもう1つ置いてください。/root/anschk/app/dryrun-ran.txtにreal-runの1行をパーミッション0644で書き込む、普通のタスクです。このプレイブックを、フラグなしで実行して、出力を/root/anschk/out/dryrun.txtに保存してください。終わったら、/root/anschk/app/dryrun-ran.txtはあり、/root/anschk/app/never.confはない必要があります。
前のステップで使ったキーと同じ名前のキーですが、値が反対です。そのキーをタスクに付けると、そのタスクだけが、常にdry runに固定されます。まだ有効にしてはいけないタスクを、プレイブックにあらかじめ書いておいて、影響だけを見たいとき、または、危険なタスクを人が確認するまで縛っておきたいときに使う場所です。「changedと報告されるのに、ファイルはない」というぎこちない状態が、まさにこのキーの働きです。
3つの道具の終了コードがどこで分かれるかを、表にまとめる
/root/anschk/undef.ymlを作成してください。定義されていない変数(missing_var)を内容に使う、copyタスクが1つです。/root/anschk/badmod.ymlも作成してください。モジュール名をansible.builtin.coppyとタイプミスした、タスクが1つです。そのあと、/root/anschk/gates.shを作成して、2つのプレイブックのそれぞれに、--syntax-check・--list-tasks・--checkを順にかけ、終了コードだけを/root/anschk/out/gates.txtに1行ずつ書いてください。<파일이름> syntax-check=<코드> list-tasks=<코드> check=<코드>の形式です(プレースホルダーはファイル名とコードです)。スクリプトを実行して、ファイルを残してください。
前の2つの道具は、対象に接続せず、テンプレートも展開しません。変数は、タスクが実際にチェックモードで実行されるときに、初めて展開されます。そのため、2つのプレイブックの結果が、互いに異なる形で分かれます。どちらがどこで最初に捕捉されるかが、このステップの答えです。終了コードは、コマンドの直後に$?で受け取ります。出力は捨てて、コードだけを残せばよいです。CIを組むとき、この表が順序を決めてくれます。安価なものからかける理由が、ここにあります。
承認に添付する計画の出力物を作成する
/root/anschk/plan.shを作成してください。ansible.posix.jsonをstdoutコールバックに指定して、site.ymlを--check --diff -e app_env=stageで実行し、そのJSON全体を/root/anschk/out/plan.raw.jsonに保存したあと、そこから変更されるタスクの名前だけを取り出して、JSON配列として/root/anschk/out/plan.jsonに保存します。スクリプトを実行して、2つのファイルを残してください。配列には、名前がちょうど1つだけ入っている必要があります。
画面を流れていく緑・黄色の文字は、承認の根拠として残りません。コールバックを変えると、実行全体が構造化されたJSON1つとして出力されます。環境変数ANSIBLE_STDOUT_CALLBACKで指定します。どんなコールバックがあるかは、ansible-doc -t callback -lで確認します。JSONの中で、タスクは.plays[].tasks[]の下にあり、タスク名は.task.name、ホストごとの結果は.hostsの下にあります。app_envだけを変えたので、変更されるタスクは1つだけです。残りはすでにその状態なので、changedではありません。
チェックモードをゲートに変える
/root/anschk/drift-gate.shを作成してください。site.ymlをチェックモードで実行して、サマリーのchangedが0なら、1行目がCLEANで始まるメッセージを出力して0で終了し、1以上なら、1行目がDRIFTで始まるメッセージを出力して1で終了します。チェックモードの実行そのものが失敗した場合は、DRIFT-UNKNOWNで始まるメッセージを出力して、1で終了します。スクリプトに渡した引数は、そのままansible-playbookに渡される必要があります。収束した状態で、引数なしで実行した出力を/root/anschk/out/gate-clean.txtに、-e app_env=stageを指定して実行した出力を/root/anschk/out/gate-dirty.txtに保存してください。
収束が終わったのに、チェックモードが変更されるものを見つけ出すなら、誰かが手で直したか、プレイブックが自力で収束できないという意味です。その条件で失敗で終わるスクリプトをCIにかければ、ドリフトが次のデプロイの前に明らかになります。サマリーの行から数字を取り出すときは、changed=の後ろの数を見ればよく、実行が失敗して、サマリーがそもそもない場合も、別に扱う必要があります。数字が読めなかったのに、黙って通過させると、ゲートではなく飾りになります。スクリプトの引数をそのまま渡す方法は、"$@"です。ゲートが失敗で終わるので、出力を保存するとき、シェルがそこで止まらないようにしてください。