シェルに出るなら、何を自分で引き受けるのか
目標
同じコマンドを、commandとshellのそれぞれで実行して、何が分かれるかを数字で計測し、シェルを使う必要があるときに、報告の基準と失敗の基準とパイプの終了コードを自分で設定する方法を、手を動かして身に付けます。最後に、ガードなしでシェルに出るタスクを見つけ出す監査ツールを作成します。
なぜ重要なのか
Ansibleを初めて使うと、プレイブックがSSHで実行されるシェルスクリプトになりがちです。動くには動きますが、「今すでにその状態なのか」「今回何が変わったのか」「失敗なのか成功なのか」のどれにも答えられないプレイブックになります。モジュールは、その3つに答えるために作られたもので、そのため、同じ作業を行うモジュールがあれば、そちらが先です。かといって、シェルを永久に使わないわけにはいきません。モジュールがない作業は、常に残ります。重要なのは、シェルに出た瞬間に、Ansibleが代わりに行ってくれていた判断がすべてなくなるという事実を知り、その判断を、手で書き入れ直すことです。このラボは、その3つの判断を、1つずつ立てていきます。
ステップ
/root/ansmod/hosts.iniを作成してください。[web]グループにweb1・web2を入れ、2つともansible_host=127.0.0.1、ansible_port=2222を持たせ、[all:vars]でansible_user=rootを置きます。そのあと、ansible-doc -s ansible.builtin.commandの出力を/root/ansmod/out/doc-command.txtに、ansible-doc -s ansible.builtin.shellの出力を/root/ansmod/out/doc-shell.txtに保存してください。/root/ansmod/files/に、空のファイルa.txt・b.txt・c.txtの3つを作成してください。/root/ansmod/boundary.ymlを作成して、web1で4つのタスクを実行してください。ls /root/ansmod/files/*.txtをcommandで1回(失敗しても先へ進むように)、ls /root/ansmod/files/*.txt | wc -lをshellで1回、echo one two three | wc -wをcommandで1回、同じものをshellで1回です。4つの結果を、/root/ansmod/out/boundary.txtに、ちょうど4行で残してください。glob command rc=<값>、glob shell stdout=<값>、pipe command stdout=<값>、pipe shell stdout=<값>の順です(プレースホルダーは値です)。/root/ansmod/report.ymlを作成してください。web1でansible.builtin.commandを使ってid -unを実行し、whoでregisterして、その戻り値から4つの項目だけを取り出して、/root/ansmod/out/result.jsonにJSONで保存してください。rc・stdout・changedは戻り値そのまま、cmdは戻り値の引数リストを空白でつなげた文字列です。このステップでは、changed_whenを付けません。/root/ansmod/report.ymlに、タスクをあと2つ入れてください。1つは、cat /etc/hostnameをcommandで実行して、hnでregisterし、changed_when: falseを付けます。もう1つは、shellでgrep -c "^nosuchuser:" /etc/passwdを実行して、hitsでregisterし、changed_when: falseと一緒に、終了コードが0か1でないときだけ失敗とみなすfailed_whenを付けます。そして、/root/ansmod/out/result.jsonに3つの項目を追加してください。hostname_changed(hnのchanged)、grep_rc(hitsのrc)、grep_failed(hitsのfailed)です。プレイブックは、最後まで実行される必要があります。/root/ansmod/pipe.ymlを作成してください。同じパイプラインcat /root/ansmod/missing.txt | wc -lを2回実行します。1回は、そのままshellで(bareでregister)、もう1回は、set -o pipefailを先頭に付け、executableを/bin/bashに指定して(guardedでregister)です。どちらも、ignore_errors: trueとchanged_when: falseを付けます。結果を、/root/ansmod/out/pipe.jsonに4つの項目で残してください。bare_rc・bare_failed・guarded_rc・guarded_failedです。missing.txtは作成しないでください。/root/ansmod/modernize.ymlを作成してください。commandもshellも一度も使わずに、次のことを行ってください。/root/ansmod/appディレクトリをパーミッション0750で作成し、/root/ansmod/app/app.confにenv=labの1行をパーミッション0640で書き込み、2つのパスの状態をモジュールで読み取って、/root/ansmod/out/modernize.jsonに5つの項目で残します。dir_mode・dir_isdir・conf_mode・conf_size・conf_checksum_len(チェックサム文字列の長さ)です。/root/ansmod/bootstrap.ymlを作成してください。ansible.builtin.rawでcommand -v python3 || echo NOPYTHONを実行してregisterし、行末の空白とCRを取り除いたパスだけを、/root/ansmod/out/raw.txtに1行で保存してください(changed_when: falseを付けます)。そして、これまでに作成した5つのプレイブック(boundary.yml・report.yml・pipe.yml・modernize.yml・bootstrap.yml)のすべてのタスクが、ansible.builtin.で始まるFQCNを使うように整理してください。- まず、検査対象となる
/root/ansmod/legacy.ymlを作成してください。タスクは4つで、ansible --versionをcommandで実行してchanged_when: falseを付けたもの1つ、mkdir -p /root/ansmod/legacy/logsをshellで実行するもの1つ、echo seeded > /root/ansmod/legacy/logs/stamp.txtをshellで実行するもの1つ、ls /root/ansmod/legacy/logsをcommandで実行するもの1つです(あとの3つには、ガードを付けません)。そのあと、/root/ansmod/shell-audit.sh <플레이북경로>を作成してください(プレースホルダーはプレイブックのパスです)。そのプレイブックの中で、commandまたはshellを使っていて、changed_whenがないタスクの名前だけを、1行ずつ辞書順に出力します(短い名前とFQCNの両方を認識する必要があります)。最後に、./shell-audit.sh /root/ansmod/legacy.ymlの出力を、/root/ansmod/out/audit.txtに保存してください。
参考
- まず、ステップ1でインベントリを作成してください。このPodのsshdは、127.0.0.1:2222で起動していて、キー認証がすでに設定されています。
- コマンドのヒント:
ansible-doc -l ansible.builtin | grep -i <낱말>でモジュールを探し、ansible-doc -s <모듈>でオプションの骨組みを見て、ansible-inventory -i hosts.ini --graphで、インベントリの解釈結果を見ます(プレースホルダーは単語とモジュールです)。 - コマンドのヒント:
yq -r '.[].tasks[] | keys | .[]' <플레이북>は、タスクが使うキーをすべて出力します。jq . <파일>で、作成したJSONが本当にJSONかどうかを確認します(プレースホルダーはプレイブックとファイルです)。 - よくある間違い: 取得だけを行う
commandタスクにchanged_when: falseを付けず、実行のたびにchangedが積み上がることです。 - よくある間違い: パイプを
shellに渡すときにset -o pipefailを抜いて、前のコマンドが失敗しても、タスクが成功として通過することです。 - よくある間違い: パーミッションを
mode: 0640のように引用符なしで書いて、8進数が10進数として読まれることです。 - このPodにはcapabilityがないので、
systemctl・mount・sysctl -wは動作しません。そのため、このラボは、ファイル・ディレクトリ・取得コマンドだけを扱います。原理は、サービス管理でも同じです。 - commandモジュール・shellモジュール・rawモジュール・ansible-doc・エラー処理
対象を書き、モジュールをドキュメントから探す
/root/ansmod/hosts.iniを作成してください。[web]グループにweb1・web2を入れ、2つともansible_host=127.0.0.1、ansible_port=2222を持たせ、[all:vars]でansible_user=rootを置きます。そのあと、ansible-doc -s ansible.builtin.commandの出力を/root/ansmod/out/doc-command.txtに、ansible-doc -s ansible.builtin.shellの出力を/root/ansmod/out/doc-shell.txtに保存してください。
インベントリがなければ、何も始まりません。このPodのsshdは127.0.0.1:2222で起動していて、ホストを2つ書いても、どちらも同じsshdに接続します。ansible-docは、インターネット検索ではなく、今このマシンにインストールされているものの一覧です。-sは、プレイブックに貼り付ける骨組みを出力します。2つのファイルを並べて、片方にしかないオプション名を探してみてください。それが、2つのモジュールの性質の違いをそのまま示しています。
同じコマンドをcommandとshellで実行して、違いを計測する
/root/ansmod/files/に、空のファイルa.txt・b.txt・c.txtの3つを作成してください。/root/ansmod/boundary.ymlを作成して、web1で4つのタスクを実行してください。ls /root/ansmod/files/*.txtをcommandで1回(失敗しても先へ進むように)、ls /root/ansmod/files/*.txt | wc -lをshellで1回、echo one two three | wc -wをcommandで1回、同じものをshellで1回です。4つの結果を、/root/ansmod/out/boundary.txtに、ちょうど4行で残してください。glob command rc=<값>、glob shell stdout=<값>、pipe command stdout=<값>、pipe shell stdout=<값>の順です(プレースホルダーは値です)。
commandは、受け取った文字列を単語に分割して、そのまま実行ファイルに渡します。途中にシェルがないので、グロブもパイプも、シェルの文法として解釈されません。グロブは、コマンドが0以外のコードで失敗して、すぐに目に付きますが、パイプは成功したふりをします。その違いを数字で残すことが、このステップのすべてです。失敗するタスクでプレイブックが止まらないようにするには、ignore_errors: trueを付け、取得だけなので、changed_when: falseも一緒に付けます。
モジュールが返すJSONをregisterで受け取って読む
/root/ansmod/report.ymlを作成してください。web1でansible.builtin.commandを使ってid -unを実行し、whoでregisterして、その戻り値から4つの項目だけを取り出して、/root/ansmod/out/result.jsonにJSONで保存してください。rc・stdout・changedは戻り値そのまま、cmdは戻り値の引数リストを空白でつなげた文字列です。このステップでは、changed_whenを付けません。
モジュールの戻り値は、文字列ではなくキーのあるJSONで、registerは、そのJSONをまるごと変数に格納します。rc・stdout・stdout_lines・stderr・changed・failed・cmdが入っています。cmdは、実際に実行された引数のリストなので、文字列にするには、つなげる必要があります。辞書をそのままJSON文字列にしてくれるフィルターがあります。そして、このタスクが何も変更していないのに、changedが何として出るかを、目で確認しておいてください。次のステップの出発点です。
changed_whenとfailed_whenで、報告の基準を自分で立てる
/root/ansmod/report.ymlに、タスクをあと2つ入れてください。1つは、cat /etc/hostnameをcommandで実行して、hnでregisterし、changed_when: falseを付けます。もう1つは、shellでgrep -c "^nosuchuser:" /etc/passwdを実行して、hitsでregisterし、changed_when: falseと一緒に、終了コードが0か1でないときだけ失敗とみなすfailed_whenを付けます。そして、/root/ansmod/out/result.jsonに3つの項目を追加してください。hostname_changed(hnのchanged)、grep_rc(hitsのrc)、grep_failed(hitsのfailed)です。プレイブックは、最後まで実行される必要があります。
grep -cは、見つからなければ終了コード1を返します。それはエラーではなく「0件」という答えですが、デフォルトの判定は0でなければ失敗なので、プレイブックがそこで止まります。failed_whenは、失敗の定義を人が書き直す場所で、条件式の中では、今registerした変数をそのまま使えます。changed_when: falseを付けたタスクと付けていないタスクのchangedの値が、result.jsonでどのように分かれるかを、比較してみてください。
パイプが失敗を飲み込むことを数字で確認して防ぐ
/root/ansmod/pipe.ymlを作成してください。同じパイプラインcat /root/ansmod/missing.txt | wc -lを2回実行します。1回は、そのままshellで(bareでregister)、もう1回は、set -o pipefailを先頭に付け、executableを/bin/bashに指定して(guardedでregister)です。どちらも、ignore_errors: trueとchanged_when: falseを付けます。結果を、/root/ansmod/out/pipe.jsonに4つの項目で残してください。bare_rc・bare_failed・guarded_rc・guarded_failedです。missing.txtは作成しないでください。
シェルでのパイプラインの終了コードは、最後のコマンドのものです。前でcatが失敗しても、wcが0で終われば全体が0で、そのタスクは緑色で通過します。set -o pipefailは、パイプラインの中のどれか1つでも失敗すれば、全体を失敗にします。ただし、デフォルトのシェル(/bin/sh)がそのオプションを知らないことがあるので、シェルを明示する必要があります。2つのrcの値が異なって出れば成功です。その違いが、ansible-lintのrisky-shell-pipeルールが存在する理由です。
シェルの3行をfile・copy・statモジュールに移す
/root/ansmod/modernize.ymlを作成してください。commandもshellも一度も使わずに、次のことを行ってください。/root/ansmod/appディレクトリをパーミッション0750で作成し、/root/ansmod/app/app.confにenv=labの1行をパーミッション0640で書き込み、2つのパスの状態をモジュールで読み取って、/root/ansmod/out/modernize.jsonに5つの項目で残します。dir_mode・dir_isdir・conf_mode・conf_size・conf_checksum_len(チェックサム文字列の長さ)です。
mkdir -pとchmodを一度に代わりに行うモジュール、echo >の代わりになるモジュール、ls -lやstatコマンドの代わりになるモジュールが、それぞれあります。3つの名前が思い浮かばなければ、ansible-doc -l ansible.builtin | grep -i <낱말>で探してください(プレースホルダーは単語です)。ステップ1でドキュメントを取得しておいた理由が、これです。状態を読み取るモジュールの戻り値は、statというキーの下に、mode・isdir・size・checksumが入っています。パーミッションは文字列なので、引用符を忘れると、8進数が10進数として読まれます。
rawでPythonを探し、プレイブックをFQCNで整理する
/root/ansmod/bootstrap.ymlを作成してください。ansible.builtin.rawでcommand -v python3 || echo NOPYTHONを実行してregisterし、行末の空白とCRを取り除いたパスだけを、/root/ansmod/out/raw.txtに1行で保存してください(changed_when: falseを付けます)。そして、これまでに作成した5つのプレイブック(boundary.yml・report.yml・pipe.yml・modernize.yml・bootstrap.yml)のすべてのタスクが、ansible.builtin.で始まるFQCNを使うように整理してください。
rawは、SSHで文字列をそのまま投げ、出てきたものをそのまま受け取ります。そのため、対象にPythonがなくても動きますが、受け取った文字列には、CRと改行がそのまま付いてきます。両端の空白を取り除くJinjaフィルターが1つあります。ファイルにCRが残っているかどうかは、od -cで確認できます。FQCNの整理は、手ではなく目で行います。yq -r '.[].tasks[] | keys | .[]' <파일>で、タスクが使うキーをすべて取り出してみると、短い名前がひと目でわかります(プレースホルダーはファイルです)。
ガードなしでシェルに出るタスクを見つけ出す監査ツール
まず、検査対象となる/root/ansmod/legacy.ymlを作成してください。タスクは4つで、ansible --versionをcommandで実行してchanged_when: falseを付けたもの1つ、mkdir -p /root/ansmod/legacy/logsをshellで実行するもの1つ、echo seeded > /root/ansmod/legacy/logs/stamp.txtをshellで実行するもの1つ、ls /root/ansmod/legacy/logsをcommandで実行するもの1つです(あとの3つには、ガードを付けません)。そのあと、/root/ansmod/shell-audit.sh <플레이북경로>を作成してください(プレースホルダーはプレイブックのパスです)。そのプレイブックの中で、commandまたはshellを使っていて、changed_whenがないタスクの名前だけを、1行ずつ辞書順に出力します(短い名前とFQCNの両方を認識する必要があります)。最後に、./shell-audit.sh /root/ansmod/legacy.ymlの出力を、/root/ansmod/out/audit.txtに保存してください。
レビューで「このshellは本当に必要か」を人が毎回尋ねる代わりに、ツールに尋ねさせるステップです。ツールが表記方法によって目をつぶるなら、それは監査ではありません。shell:とansible.builtin.shell:を、どちらも同じものとして扱う必要があります。イメージに入っているyqはmikefarah版なので、.[].tasks[]でプレイの一覧をたどって入り、ドットが含まれるキーは、.["ansible.builtin.shell"]のように角括弧で書きます。存在しないキーを尋ねるとnullが出るので、select(... != null)で絞り込めばよいです。