📚 連載:Ansibleが理解できない理由はLinuxにあった【Shell編】第4回:なぜ正規表現で壊れるのか(Quote / Evaluation)
※Shellの仕組みからAnsibleを理解するシリーズです。
① なぜShellを理解しないとAnsibleは使えないのか
② なぜAnsibleで出力が取得できないのか
③ なぜ結果が消えるのか
④ なぜgrepで見つからないのか
⑤ なぜ正規表現で壊れるのか
⑥ なぜ環境が違うのか
⑦ なぜ条件分岐が失敗するのか
⑧ なぜループがうまく動かないのか
⑨ なぜ途中で処理が止まるのか
⑩ なぜ変数が意図通りに展開されないのか
⑪ なぜAnsibleの挙動が読めるようになるのか
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
📑 【Shell編】全体のまとめはこちら
→ Ansibleが理解できない理由はLinuxにあった【Shell編】まとめ
📑 連載の移動
前の記事:【Shell編】第3回 | 次の記事:【Shell編】第5回
📋 目次
- 前回の振り返り(第3回)
- 逆引き辞典との連動
- Shellの解釈プロセス:Ansibleがコマンドを投げた直後の挙動
- なぜ壊れるのか:Ansible変数とShell評価の「衝突」
- 観測方法:OSが受け取った「最終形態」の特定
- 【実機検証】文字列の評価と変質:Ansible変数がOSに届くまでの変換
- まとめ:解釈の境界線によるトラブルシュート
- 次回予告
- 連載一覧:Ansibleが理解できない理由はLinuxにあった【Shell編】
※本記事の位置とシリーズ全体の関係を先に確認します。
シリーズ全体構造(学習 × 問題解決)
本シリーズは
「理解(各記事)」と「問題解決(逆引き辞典)」を組み合わせて
スキルを身につける構成になっています。
この図は、どこから学び、どこに進めばよいかを示した“ロードマップ”です。
📍 現在の位置
現在はこの図の「Shell編の第4回」になります
📍 はじめに:この記事のスタンス
これまでの3回で、Ansibleから送出されたデータが通過する物理的な経路(出口・転送・入口)を整理しました。これにより、データが到達しないといった物理的な疎通に関するトラブルの切り分けが可能になりました。
しかし、実務においては物理的な疎通とは別に、以下のような事象が依然として発生します。
-
Ansible変数
{{ pattern }}の展開後、予期しないファイル名へと置換され正規表現が動作しない -
手動実行では成功する正規表現が、Ansibleの
shellモジュール経由ではマッチしない -
スペースを含む値を渡すと、コマンドの引数が意図しない箇所で分断されエラーになる
これらの要因は、正規表現自体の記述ミスではなく、Ansibleから渡された文字列をOS側が受け取った際に行われる「Shellによる評価(Evaluation)」の仕様にあります。
⚠️ 注意事項
本記事の目的は、正規表現の構文解説ではありません。
「Ansibleで定義した文字列を、変質させずにOSの実行プロセスまで正確に届けること」に主眼を置きます。
「Ansible側で正しく記述しても、受信側のOSによって実行直前に書き換えられてしまう」という、自動化における論理的な課題を解決するために、Linuxの評価ロジックを体系的に理解します。
1. 前回の振り返り(第3回)
前回は、Ansibleタスクの実行時に、OSが「引数(ファイル指定)」と「標準入力(パイプ等)」のどちらを優先してデータを読み込むか、その判定プロセスを確認しました。
今回は、この読み込み処理が行われる一歩手前の段階に焦点を当てます。Ansibleから渡された文字列がOSに届いた直後、コマンドとして実行される前にShellがどのように内容を書き換えるのか、その解釈ロジックを整理します。
2. 逆引き辞典との連動
具体的なエラー名から原因を特定したい場合は、以下の逆引き辞典を活用してください。
【保存版】Ansibleよくあるエラー一覧と原因まとめ(Shell編)
- まず「逆引き辞典」で該当ステップを特定する
- 次に「本編(各連載記事)」で仕組みを理解する
このサイクルが、OS編から続くトラブル解決の最短ルートです。
3. Shellの解釈プロセス:Ansibleがコマンドを投げた直後の挙動
Ansibleの shell モジュールでコマンドを実行した際、ターゲットノード(OS側)では即座にバイナリが実行されるわけではありません。
実行の直前、Shellは受け取った文字列をスキャンし、特定の記号を「命令」として変換します。これを「評価(Evaluation)」と呼びます。
| 評価の種類 | Ansibleでの実例 | OS側での書き換え内容 |
|---|---|---|
| 変数展開 | $HOSTNAME |
Shellが持つ環境変数の内容に置き換える |
| パス名展開 | *.log |
カレントディレクトリにあるファイル名の一覧に置き換える |
| クォート除去 | 'text' |
評価の対象外として指定した「囲み記号」を削除し、引数として確定させる |
Ansibleのタスクが意図通りに動作しない場合、指定した正規表現や変数がこの評価プロセスによって実行前に変質している可能性があります。
4. なぜ壊れるのか:Ansible変数とShell評価の「衝突」
Ansibleの変数(Jinja2)が展開された後、その値にShellにとっての「特別な記号」が含まれていると、Shellがそれを命令として二重に評価してしまいます。
これを防ぐには、OS側のクォート(引用符)を適切に使い分け、評価の範囲を制御する必要があります。
💡 技術的裏付け:AnsibleとOSによる「二段階解釈モデル」
Ansibleの shell モジュール実行時、文字列は以下の二段階を経て処理されます。
【テキスト図:評価のバトンタッチ】
[ 1. Ansible側 (Control Node) ]
定義: shell: <command> {{ my_var }}
変数: my_var = "<特殊記号を含む値>"
|
v (Jinja2展開)
送信: <command> <特殊記号を含む値>
|
============|==[ ネットワーク (SSH経由) ]=======================
|
[ 2. OS/Shell側 (Target Node) ]
受信: <command> <特殊記号を含む値>
|
v (Shellの再評価)
・「*」 や 「.*」 → パス名展開(ファイル名一覧に置換)
・$VAR → 変数展開(環境変数の値に置換)
|
v (コマンド実行)
実行: Ansible側の意図とは異なる文字列で処理される
Ansibleで正規表現が壊れる原因は、この「Step 2」におけるShellの自動展開機能にあります。
Step 1:Ansible側(Jinja2展開)
コントロールノード側で、{{ var }} が実際の値に書き換えられます。
この時、Ansibleは 「文字列」 としてリモートへ送る準備をします。
Step 2:OS側(POSIX Shell評価)
リモートサーバーに届いた直後、Shell(/bin/sh)が実行直前に文字列をスキャンします。
- Variable Expansion:
$VARをOSの環境変数で展開 - Filename Expansion:
*などのワイルドカードをファイル名一覧に展開(グロブ) - Quote Removal:最後にクォート記号を外して、コマンド本体へ渡す
Ansibleで正規表現が壊れる正体は、この Step 2におけるShellの仕様(利便性のための自動展開機能) です。
クォートで保護されていない正規表現(例:.*)は、
Ansibleが正しく値を渡したとしても、実行の直前にOSによって
「ファイル名の一覧」に書き換えられてしまいます。
シングルクォート(' ')を重ねることで、このOS側のStep 2(再評価)を強制的に封じ込めることができます。
Ansibleタスクにおけるクォートルール
| 記述方法 | Shellへの影響 | 実務での使い分け |
|---|---|---|
| クォートなし | 全評価(危険) |
* や $ がすべて展開される。誤動作の原因となるため原則避ける。 |
| ダブルクォート | 弱評価 | Ansible変数を展開しつつ、スペースを含むパス等を守る際に使用する。 |
| シングルクォート | 評価の抑止 | Shellによる再評価をほぼ完全に防ぐ。正規表現をそのまま届けたい場合の最適解。 |
5. 観測方法:OSが受け取った「最終形態」の特定
「手動実行では動くがAnsible経由だと失敗する」場合、OSが展開を終えた直後の、最終的な実行コマンドを特定する必要があります。
-
Ansibleのデバッグ出力 (
-vvv)
・リモートサーバーへ実際に送出された生のコマンドラインを確認できます。 -
set -xの埋め込み
・shell: set -x; grep ...のように記述することで、OS側で評価・変換された後の最終形を標準エラー出力(stderr)から観測できます。
状態から原因を逆引きする
| 現象 | 判定される状態 | 解決策 |
|---|---|---|
| 意図しないファイルにヒット | パス名展開の発生 | シングルクォートで囲み、OS側での * の評価を無効化する。 |
| 変数が空、または崩れる | 変数展開の干渉 | シングルクォートで保護し、OS側の $ 評価を抑止する。 |
| Ansible変数がそのまま出る | 評価の未到達 | Ansible側の {{ }} 記述ミスや、YAMLとしての引用符の重複を確認する。 |
6. 【実機検証】文字列の評価と変質:Ansible変数がOSに届くまでの変換
セクション4で解説した「二段階解釈モデル」において、OS側のShellが実行直前にどのように文字列を書き換える(評価する)のか。その物理的な挙動を set -x(実行トレース)を用いて観測します。
6-1. 【検証1】パス名展開(Glob)による正規表現の破壊
Ansible変数として渡した正規表現の .*(任意の文字列の繰り返し)が、OS側の評価(Step 2)によって「ファイル名一覧」へ書き換えられる現象を確認します。
① 検証手順(リモートノード側)
カレントディレクトリに複数のファイルが存在する環境を準備します。これにより、ワイルドカードが展開される条件を整えます。
# 検証用ファイルの作成
[ansibleuser@localhost ~]$ touch file1.log file2.log test.txt
[ansibleuser@localhost ~]$ ls -l
合計 0
-rw-r--r--. 1 ansibleuser ansibleuser 0 5月 7 15:58 file1.log
-rw-r--r--. 1 ansibleuser ansibleuser 0 5月 7 15:58 file2.log
-rw-r--r--. 1 ansibleuser ansibleuser 0 5月 7 15:58 test.txt
② Ansibleによる実行テスト
実行するPlaybook (case1_glob_expansion.yml)
クォートで囲わない場合(OSが再評価できる状態)の挙動を確認します。
---
- name: 検証1 パス名展開による正規表現の破壊
hosts: test_servers
gather_facts: false
vars:
my_regex: ".*"
tasks:
- name: クォートなしで正規表現をecho
# set -x を付与し、OSが展開した後の最終的な実行コマンドをstderrに出力させる
ansible.builtin.shell: "set -x; echo {{ my_regex }}"
register: res_glob
- name: 実行結果とOS側の評価内容を確認
ansible.builtin.debug:
msg:
- "標準出力(stdout): {{ res_glob.stdout }}"
- "OSが実際に実行したコマンド(stderr): {{ res_glob.stderr }}"
実行コマンド
ansible-playbook -i inventory.ini case1_glob_expansion.yml
③ 検証結果
(ansible) [root@localhost workspace]# ansible-playbook -i inventory.ini case1_glob_expansion.yml
PLAY [検証1 パス名展開による正規表現の破壊] *************************************************************************************************************************************************
TASK [クォートなしで正規表現をecho] *********************************************************************************************************************************************************
changed: [192.168.1.21]
TASK [実行結果とOS側の評価内容を確認] *******************************************************************************************************************************************************
ok: [192.168.1.21] => {
"msg": [
"標準出力(stdout): . .. .ansible .bash_history .bash_logout .bash_profile .bashrc .ssh",
"OSが実際に実行したコマンド(stderr): + echo . .. .ansible .bash_history .bash_logout .bash_profile .bashrc .ssh"
]
}
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ログの読み解きと考察
-
現象:
stdoutおよびstderrの結果において、Ansibleで指定した.*という文字列が消失し、代わりに.sshや.bashrcといったディレクトリ内のファイル・フォルダ名が展開されています。 -
理由:コマンド実行の直前、OS側のShellが文字列をスキャンし、
*を「カレントディレクトリ内の全ファイル名」として展開(パス名展開)したためです。set -xの出力により、echoコマンドが実行される前段階で、すでに文字列がファイルリストへ書き換えられている事実が観測できます。
テスト結果のまとめ:
クォートによる保護がない状態でAnsibleから特殊記号(*など)を渡すと、OS側のShellはそれを「コマンドの命令」として解釈します。この挙動により、Ansible側で定義した正規表現は実行直前に別の文字列へ変質し、本来の検索・抽出処理が成立しなくなります。
6-2. 【検証2】変数展開の干渉(ダブルクォートによる不完全な保護)
Ansible変数として渡した $ 記号が、OS側のダブルクォート評価(Step 2)によって、ターゲットノードの環境変数として上書きされる現象を確認します。
① 検証手順(リモートノード側)
ターゲットノード側で、環境変数 USER が定義されていることを確認します(通常、ログインユーザー名が格納されています)。
[ansibleuser@localhost ~]$ echo $USER
ansibleuser
② Ansibleによる実行テスト
実行するPlaybook (case2_variable_interference.yml)
ダブルクォート(弱評価)とシングルクォート(完全保護)の挙動を比較します。
---
- name: 検証2 変数展開の干渉
hosts: test_servers
gather_facts: false
tasks:
- name: ダブルクォート内で環境変数を参照
# OS側の評価を誘発するためダブルクォートを使用
ansible.builtin.shell: "set -x; echo \"$USER\""
register: res_double
- name: シングルクォート内で環境変数を参照
# OS側の評価を封じ込めるためシングルクォートを使用
ansible.builtin.shell: "set -x; echo '$USER'"
register: res_single
- name: 結果の比較
ansible.builtin.debug:
msg:
- "ダブルクォート(OS評価あり): {{ res_double.stdout }}"
- "シングルクォート(OS評価なし): {{ res_single.stdout }}"
実行コマンド
ansible-playbook -i inventory.ini case2_variable_interference.yml
③ 検証結果
(ansible) [root@localhost workspace]# ansible-playbook -i inventory.ini case2_variable_interference.yml
PLAY [検証2 変数展開の干渉] *****************************************************************************************************************************************************************
TASK [ダブルクォート内で環境変数を参照] *****************************************************************************************************************************************************
changed: [192.168.1.21]
TASK [シングルクォート内で環境変数を参照] ***************************************************************************************************************************************************
changed: [192.168.1.21]
TASK [結果の比較] ***************************************************************************************************************************************************************************
ok: [192.168.1.21] => {
"msg": [
"ダブルクォート(OS評価あり): ansibleuser",
"シングルクォート(OS評価なし): $USER"
]
}
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ログの読み解きと考察
-
現象:ダブルクォートを使用したタスクでは、OS側の環境変数の中身である
ansibleuserが出力されました。一方、シングルクォートを使用したタスクでは、記述した通りの$USERという文字列がそのまま出力されています。 -
理由:OS側のShell(
/bin/sh)において、ダブルクォートは「弱評価」として扱われ、内部の$記号を環境変数の参照命令として解釈するためです。これに対し、シングルクォートは「完全な保護」として機能し、内部の記号を一切評価せずにプロセスへ渡すため、変数の展開が抑止されます。
テスト結果のまとめ:
Ansibleの変数展開後に $ などの記号が残っている場合、OS側のクォートの選択によって最終的な実行結果が分かれます。「OS側の環境変数や特殊記号を評価させたくない」場合は、シングルクォートによる保護が必須 となります。この使い分けを誤ると、Ansibleから渡した意図通りの文字列が実行直前に書き換えられる原因となります。
7. まとめ:解釈の境界線によるトラブルシュート
Ansibleで文字列の変質や正規表現の動作不全が発生した際は、以下の「解釈の境界線」に基づいた切り分けが有効です。
評価の不一致に関する調査手順
-
OS側のシングルクォート(
')で変数を保護しているか
展開後の値にShellが関与(再解釈)することを防ぐための基本設定を確認します。 -
意図的にOS側で評価させる記号(
$等)が含まれているか
ダブルクォートを使用する場合、意図しない記号までOS側の変数展開やパス名展開の対象になっていないかを確認します。 -
OSが受理した最終的なコマンドラインをログで確認したか
Ansibleのデバッグ出力(-vvv)やset -xを用い、Jinja2展開およびShell評価を経た後の「実行直前の状態」を観測します。
調査における視点
文字列に関する問題は、記述ミスだけでなく「どの段階で評価(書き換え)が行われたか」を特定することです。どの記号がShellに解釈されたかを確認する必要があります。
評価ルールの整理
-
シングルクォート: OS側のShellは一切関与せず、Ansibleから渡された値がそのまま実行プロセスへ届きます。
-
ダブルクォート: OS側のShellが一部の記号(
$等)に干渉し、環境変数などの展開が行われます。 -
クォートなし: OS側のShellが文字列全体をスキャンし、パス名展開や変数展開を全面的に適用します。
これまで整理した「物理的な経路(第1〜3回)」に、今回の「論理的な解釈(第4回)」を加えることで、Ansibleにおけるshell側の動作が読めるようになります。
8. 次回予告:
手動でのログイン実行(対話型)と、Ansibleを介した実行(非対話型)では、OS側で読み込まれる設定ファイルや環境変数の範囲が異なります。
次回は、Shellがどのように実行環境(PATH等)を切り分けているのか、その仕組みを整理します。実行方式による環境の差を把握し、Ansibleからターゲットノードの環境を確実に制御する手法を確認します。
📑 連載の移動
前の記事:【Shell編】第3回 | 次の記事:【Shell編】第5回
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
9. 連載一覧:Ansibleが理解できない理由はLinuxにあった【Shell編】
| 回数とタイトル | 内容(概要) |
|---|---|
| 【Shell編】第0回:なぜShellを理解しないとAnsibleは使えないのか | AnsibleはShellを通してコマンドを実行している。Ansible → SSH → Shell → Linux の構造を理解し、なぜShell理解が必須なのかを整理する。 |
| 【Shell編】第1回:なぜAnsibleで出力が取得できないのか | Ansibleでregisterが空になる・エラーが見えない原因はstdout / stderrの違いにある。Shellの出力構造を理解することで原因を特定できる。 |
| 【Shell編】第2回:なぜ結果が消えるのか | Ansibleで実行結果が見えなくなる原因はパイプ・リダイレクトによる出力先の変化にある。どこに出力が流れたのかを追うことで原因を特定できる。 |
| 【Shell編】第3回:なぜgrepで見つからないのか | Ansibleでgrepがヒットしない原因は検索対象ではなく入力(stdin)の問題にある。Shellの入力(ストリーム)構造を理解することで原因を特定できる。 |
| 【Shell編】第4回:なぜ正規表現で壊れるのか | Ansibleで検索や置換が壊れる原因は正規表現の評価ルールにある。文字列ではなくルールとして解釈される仕組みを理解し、リテラルの境界を学ぶ。 |
| 【Shell編】第5回:なぜ環境が違うのか | 手動では動くのにAnsibleで失敗する原因は実行環境(PATH・環境変数)の差分にある。Shellの実行コンテキストの違いを理解し、環境を制御する。 |
| 【Shell編】第6回:なぜ条件分岐が失敗するのか | Ansibleのwhen条件が意図通りに動かない原因はexit codeにある。Shellの終了ステータスの判定ロジックを理解することで、正しく条件を組める。 |
| 【Shell編】第7回:なぜループがうまく動かないのか | Ansibleで繰り返し処理が期待通り動かない原因はShellのループと入力・サブシェルの干渉にある。データの扱い方を理解し、ループを安定させる。 |
| 【Shell編】第8回:なぜ途中で処理が止まるのか | Ansible実行中に意図せず処理が止まる原因はShellのエラーハンドリング(set -e 等)にある。エラー伝播の仕組みを理解し、停止条件を制御する。 |
| 【Shell編】第9回:なぜ変数が意図通りに展開されないのか | Ansibleで変数が空になる・壊れる原因はShellのクォートと展開順序にある。値の保護と展開のルールを理解し、安定したコマンドを記述する。 |
| 【Shell編】第10回:なぜ値が分割されてしまうのか | スペースや改行で意図せず値が壊れる原因はShellの単語分割(word splitting)にある。IFSなどの内部処理の本質を理解し、データの整合性を守る。 |
| 【Shell編】第11回:なぜAnsibleの挙動が読めるようになるのか | これまでの知識を統合し、AnsibleのエラーをShellの実行プロセスから逆算して分解できるようになる。実務で使える“読み方”を完成させる。 |