この記事はAnsible Blogger Advent Calendar 2018 の23日目の記事です。
はじめに
Ansibleにはshellでコマンドを実行できるshell
モジュールがあります。
そしてshell
を使うなら当然ヒアドキュメントも使いたくなりますよね。
しかし、Ansibleでヒアドキュメントを使用する場合、公式にはshell
モジュールでplaybook内に複数行書かずにscript
モジュールを使用するようにとあります。
Rather than using here documents to create multi-line scripts inside playbooks, use the script module instead.
しかし、そのためにわざわざスクリプトファイルを作成するのは非常に面倒です。管理対象のファイルは極力減らしたいものです。
そこで今回はshell
モジュールでヒアドキュメントの使用する方法について調べました。
前提
この記事ではAnsibleのバージョンはpipリポジトリの2.7.5(2018/12/16現在で最新)を使用します。
shell
モジュールでヒアドキュメントが使えない問題
shell
モジュールで普通にヒアドキュメントを使うとこうなります。
先頭に半角スペースが挿入され、さらにインデントが無視されるのでEOFが認識されていません。
---
- name: heredoc test
hosts: 127.0.0.1
connection: local
tasks:
- name: shell module
shell: |
cat <<EOF
aaa
bbb
ccc
EOF
register: result_shell
- name: debug
debug:
var: result_shell.stdout_lines
TASK [debug] *******************************************************************************************************************************************************************************************************************************************************************
ok: [127.0.0.1] => {
"result_shell.stdout_lines": [
" aaa",
" bbb",
" ccc",
" EOF"
]
}
どうすればいいの?
既に同様の問題でissueが立てられていました。
そしてshell
モジュールの引数cmd
を使うことで解決できるとあります。
---
- name: heredoc test
hosts: 127.0.0.1
connection: local
tasks:
- name: shell module + cmd args
shell:
cmd: |
cat <<EOF
aaa
bbb
ccc
EOF
register: result_shell
- name: debug
debug:
var: result_shell.stdout_lines
実行結果がこちら。たしかにインデント、EOFが認識されていることがわかります。
めでたしめでたし。。。でも
TASK [debug] *******************************************************************************************************************************************************************************************************************************************************************
ok: [127.0.0.1] => {
"result_shell.stdout_lines": [
"aaa",
" bbb",
"ccc"
]
}
でも引数cmd
って何?
shell
モジュールの引数cmd
って何者なんでしょう?実はこれ公式ドキュメントにもansible-doc shell
でも記載されていません。
cmd
を使うことで問題を解決出来るのは分かりましたが、これをどのような時に使用するのか、また使うことによる影響も分からず、理解せずに使用したくないのでソースを調べてみました。
なお、ソースはpipリポジトリの2.7.5を対象とし、調査の内容は私が個人的に調査した結果であるため、間違っている可能性もあることをご了承ください。
(もし間違っていたらご指摘いただけますと幸いですm(__)m )
ソースからshell
モジュールの引数cmd
を調べてみた
結論から言うと、
- 引数
cmd
を使用した場合、コマンドは一切加工されずにコマンドとして実行される - 引数
cmd
を使用しない場合、コマンドは半角スペース、改行で分割され、さらに半角スペースで結合されコマンドとして実行される
という流れで処理されていることが分かり、自分なりに納得できる答えが得られました。
つまり引数cmd
は
コマンドからスペース、改行を維持したまま実行したい場合に使用する
ものであると私は理解しました。
以下にソースを辿った経緯を記載します。
1. vvv
オプションで実行
まずはansible-playbook
を-vvv
オプション付きで実行し詳細情報を確認しました。すると_raw_params
という変数ですでにインデントが無視されたコマンドが格納されています。ということで_raw_params
が作成される過程を追っていきます。
changed: [127.0.0.1] => {
"changed": true,
"cmd": "cat <<EOF\n aaa\n bbb\n ccc\n EOF",
"delta": "0:00:00.012905",
"end": "2018-12-18 21:50:24.363250",
"invocation": {
"module_args": {
"_raw_params": "cat <<EOF\n aaa\n bbb\n ccc\n EOF",
"_uses_shell": true,
"argv": null,
"chdir": null,
"creates": null,
"executable": null,
"removes": null,
"stdin": null,
"warn": true
}
},
"rc": 0,
"start": "2018-12-18 21:50:24.350345",
"stderr": "",
"stderr_lines": [],
"stdout": " aaa\n bbb\n ccc\n EOF",
"stdout_lines": [
" aaa",
" bbb",
" ccc",
" EOF"
]
}
2. _raw_params
をgrepしてみる
17ファイルヒットしました。なんとか1個ずつ見られそうな数です。その中でplaybook/task.py
でcmd
をpop
してargs['_raw_params']
に格納しているコードがありました。怪しいですね。
t
さらにplaybook/task.py
の94行目の該当のコードのコメントを見てみると「cmd
は_raw_params
に相当しこれを上書きする」という旨の記載があります。
# the command/shell/script modules used to support the `cmd` arg,
# which corresponds to what we now call _raw_params, so move that
# value over to _raw_params (assuming it is empty)
if action in ('command', 'shell', 'script'):
if 'cmd' in args:
if args.get('_raw_params', '') != '':
raise AnsibleError("The 'cmd' argument cannot be used when other raw parameters are specified."
" Please put everything in one or the other place.", obj=ds)
args['_raw_params'] = args.pop('cmd')
以上のことから、cmd
に定義したコマンドはcmd
を使用しない場合と同じように_raw_params
に格納され実行されるということが分かりました。しかしこれだけでは先頭に半角スペースが追加される原因はまだわかりません。
ということでさらに_raw_params
が作成される過程を追ってみました。
3. _raw_params
が作成される過程を追ってみる
上記のplaybook/task.py
で_raw_params
はリストargs
の要素の1つなのでargs
が作成される過程を追います。
すると183行目でargs_parser.parse()
の結果が格納されています。
args_parser = ModuleArgsParser(task_ds=ds)
try:
(action, args, delegate_to) = args_parser.parse()
except AnsibleParserError as e:
args_parser.parse()
を追っていくとparsing/mod_args.py
の199行目でdict
型の場合はそのまま返されていますが、string
型の場合はparse_kv(thing, check_raw=check_raw)
の結果が格納されていることが分かります。
if isinstance(thing, dict):
# form is like: { xyz: { x: 2, y: 3 } }
args = thing
elif isinstance(thing, string_types):
# form is like: copy: src=a dest=b
check_raw = action in FREEFORM_ACTIONS
args = parse_kv(thing, check_raw=check_raw)
parse_kv
を追っていくとmodule_utils/splitter.py
の162、185行目で改行、スペースでsplit
されています。
items = args.strip().split('\n')
tokens = item.strip().split(' ')
そして最終的にmodule_utils/splitter.py
の100行目で分割した要素を半角スペース付きでjoin
していました。
# recombine the free-form params, if any were found, and assign
# them to a special option for use later by the shell/command module
if len(raw_params) > 0:
options[u'_raw_params'] = ' '.join(raw_params)
return options
以上のことから「インデントが無視され先頭に半角スペースが追加される」謎が解明されました。
おわりに
shell
モジュールでヒアドキュメントを使用する方法、そのための引数cmd
についてソースを調査した結果、その過程を紹介してみました。初めはドキュメントに記載がなく戸惑いましたが、ソースを辿ることでその意味、ユースケースを理解することが出来、ansibleとも少し仲良くなれた気がします。
また今回ソースを読んで自分なりに理解することで少し自信にもなりましたので、今後は積極的にソースを読んでいきたいと思いました。
ちなみに「半角スペースが追加される問題」はansibleのGitHubリポジトリでは修正されているような雰囲気です。
(今回使用したpipの2.7.5では未修正でした)