はじめに
この記事は、エーピーコミュニケーションズ Advent Calendar 2018 の2日目のエントリです。
最近、業務でAWS上でのサーバリプレイスを行っており、Ansibleを使った構築の自動化に取り組んでいます。
Ansible自体はそこそこ前から触っていたのですが、
仕事でガッツリ使うという機会があまりなく今回初めてその機会がありました。
しかし、いざ書いて動かしてみると思ったようには動かず、「おや?」と思うことがいくつかありました。
大抵は公式ドキュメントや書籍に書かれているような内容ですが、
読み飛ばしてしまいがちなところだったり、マイナーなケースだったりしますので、
これから使う人、久しぶりに使う人のお役に立てばと思います。
ちなみにansibleのバージョンは 2.6.3
でした。アップデート等で解消されていたらすみません。
また、質問や補足、マサカリなどあればコメント頂けると幸いです。
変数編
マジック変数と同じ名前の変数を設定してしまう
Ansibleにはマジック変数というものがあります。
インベントリ等に書いたホスト関連の情報が自動的に入ってくれるという便利な変数です。
マジック変数は以下の4つがあります。
- hostvars
- groups
- group_names
- inventory_hostname
リンクにあるドキュメントにもdo not set variables with these names.
と書かれていますが、
特に短いgroups
などは知らずに、あるいは無意識で使ってしまったりします。
ちなみにPlaybookや***_vars
でマジック変数を使ってしまっても使用したタスクでエラーが出るわけではなく、
変数としてどんな値を入れていてもマジック変数の値に上書きされて実行されてしまうため、
タスクでエラーが出て止まるということがほとんどだと思いますが、最悪の場合は意図したものとは違う値でタスクが進んでしまう、ということが起こる可能性があります。
例えばこんなPlaybookを書いたとき、
- hosts: all
gather_facts: false
vars:
- groups: hogehoge
tasks:
- name: test vars
debug:
msg: "groups is {{ groups }}"
意図している値は groups is hogehoge
ですが…
PLAY [all] *************************************************************************************************************
TASK [test vars] *******************************************************************************************************
ok: [192.168.33.100] => {
"msg": "groups is {'all': ['192.168.33.100'], 'ungrouped': [], 'vagrant': ['192.168.33.100']}"
}
PLAY RECAP *************************************************************************************************************
192.168.33.100 : ok=1 changed=0 unreachable=0 failed=0
groups=hogehoge
という値に、マジック変数が上書きされて処理されてしまいます。
回避策
変数を設定する場合はマジック変数を避ける必要があるので、マジック変数に何があるかをドキュメント等で知っておく必要があります。
この事象に限ったことではありませんが、レビューで実行前にチェックしたり、テスト環境で動作確認をするのが良さそうです。
ちなみにansible-lintでは特にエラー等は検出できませんでした。
2018/12/2 追記
同僚の @akira6592 さんに補足情報をいただきました。
記事内ではgroups
を始めとした4つのマジック変数を紹介しましたが、
ドキュメントの以下のページにマジック変数の一覧が記載されています。
https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html
Ansible2.7版のドキュメントから新規で追加されたページのようです。
思った以上にたくさんあるんですね!
今回の書き方は注意喚起の意味合いが強いですが、本来的には便利なものなのでドンドン活用していきましょう。
変数に使用できない記号(ハイフンなど)を使用してしまう
Ansibleで変数に使用できる文字・記号は以下の3種類です。
- アルファベット(大文字・小文字可) ※1文字目はアルファベットのみ可
- 数字
- アンダースコア
_
このようにAnsibleの変数名には記号はアンダースコアのみが使用可能で、ハイフン -
は使用できません。
回避策
こちらも変数名からハイフンを除外するしかないようです。
幸いアンダースコアは使用できるので、var1_var2
のようなスネークケースに置き換えたり、
大文字・小文字を別々に認識するようなのでVar1Var2
のようなキャメルケースなどで書き換えることは容易だと思います。
ちなみに
ハイフン混じりの変数(例:var1-var2
)を設定した場合、
Playbook内であれば以下のようなエラーが表示されます。
ERROR! Invalid variable name in vars specified for Play: 'var1-var2' is not a valid variable name
一方、Playbook外の 実行してみると、 debug1 taskでは変数 今度は 今度は実行結果1,2では未定義としてですが通っていたdebug1 taskでエラーが出ました。不思議。。。host_vars
, group_vars
等に記述した場合は上記のようなエラーは発生せず、未定義の変数のように処理されます。
実際に実行してみると以下のようなエラーが出ます。
- hosts: all
gather_facts: false
tasks:
- name: debug1
debug:
var: var1-var2
- name: debug2
debug:
msg: "var is {{ var1-var2 }}"
$ ansible-playbook -i inventory.ini test.yml
PLAY [all] *************************************************************************************************************
TASK [debug1] **********************************************************************************************************
ok: [192.168.33.100] => {
"var1-var2": "VARIABLE IS NOT DEFINED!"
}
TASK [debug2] **********************************************************************************************************
fatal: [192.168.33.100]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'var1' is undefined\n\nThe error appears to have been in '/path-to-playbook/test.yml': line 7, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n var: var1-var2\n - name: debug2\n ^ here\n"}
PLAY RECAP *************************************************************************************************************
192.168.33.100 : ok=1 changed=0 unreachable=0 failed=1
var1-var2
は VARIABLE IS NOT DEFINED!
として扱われていますが、
debug2 taskの'var1' is undefined
というエラーメッセージが気になります。
var1
に値を入れるとどうなるのでしょうか?$ ansible-playbook -i inventory.ini test.yml -e var1=test1
PLAY [all] *************************************************************************************************************
TASK [debug1] **********************************************************************************************************
ok: [192.168.33.100] => {
"var1-var2": "VARIABLE IS NOT DEFINED!"
}
TASK [debug2] **********************************************************************************************************
fatal: [192.168.33.100]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'var2' is undefined\n\nThe error appears to have been in '/path-to-playbook/test.yml': line 7, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n var: var1-var2\n - name: debug2\n ^ here\n"}
PLAY RECAP *************************************************************************************************************
192.168.33.100 : ok=1 changed=0 unreachable=0 failed=1
var2
が未定義になりました。var2
に値を入れると…?$ ansible-playbook -i inventory.ini test.yml -e var1=test1 -e var2=test2
PLAY [all] *************************************************************************************************************
TASK [debug1] **********************************************************************************************************
fatal: [192.168.33.100]: FAILED! => {"msg": "Unexpected templating type error occurred on ({{var1-var2}}): unsupported operand type(s) for -: 'str' and 'str'"}
PLAY RECAP *************************************************************************************************************
192.168.33.100 : ok=0 changed=0 unreachable=0 failed=1
まあ素直にAnsibleの変数にハイフンを使うのをやめよう、ということですね。
私は実行結果1のようなエラーメッセージを見て、ハイフンの問題と気づかずに結構な時間ハマってしまいました。。
変数としてansible_become: true
にするとログインユーザーに戻れない
Ansibleではbecome
やbecome_user
などのようにPlaybookにも書けるし、変数にも書ける設定があります。
Playbook毎に設定を変えたい場合もあれば、基本的には共通なので変数に書いて使いまわしたい場合もあり、
ユースケースによって様々な書き方ができるのがAnsibleの良さだと思いますが、
同じ挙動だと思って変数として書いたら若干挙動が異なるケースがあってハマりました。
例えば以下のようなPlaybookを書いた場合、
- hosts: all
gather_facts: false
become: true
tasks:
- name: root user
command: whoami
- name: login user
command: whoami
become: false
実行すると意図したように2つ目のタスクlogin user
ではログインしたvagrant
ユーザーで実行されていることがわかります。
$ ansible-playbook -i inventory.ini whoami.yml -v
Using /etc/ansible/ansible.cfg as config file
PLAY [all] *************************************************************************************************************
TASK [root user] *******************************************************************************************************
changed: [192.168.33.100] => {"changed": true, "cmd": ["whoami"], "delta": "0:00:00.004528", "end": "2018-12-01 04:57:34.454376", "rc": 0, "start": "2018-12-01 04:57:34.449848", "stderr": "", "stderr_lines": [], "stdout": "root", "stdout_lines": ["root"]}
TASK [login user] ******************************************************************************************************
changed: [192.168.33.100] => {"changed": true, "cmd": ["whoami"], "delta": "0:00:00.004071", "end": "2018-12-01 04:57:35.172791", "rc": 0, "start": "2018-12-01 04:57:35.168720", "stderr": "", "stderr_lines": [], "stdout": "vagrant", "stdout_lines": ["vagrant"]}
PLAY RECAP *************************************************************************************************************
192.168.33.100 : ok=2 changed=2 unreachable=0 failed=0
しかし、ansible_become: true
を変数として設定してしまうと、
$ ansible-playbook -i inventory.ini whoami.yml -v -e ansible_become=true
Using /etc/ansible/ansible.cfg as config file
PLAY [all] *************************************************************************************************************
TASK [root user] *******************************************************************************************************
changed: [192.168.33.100] => {"changed": true, "cmd": ["whoami"], "delta": "0:00:00.013255", "end": "2018-12-01 04:57:49.792209", "rc": 0, "start": "2018-12-01 04:57:49.778954", "stderr": "", "stderr_lines": [], "stdout": "root", "stdout_lines": ["root"]}
TASK [login user] ******************************************************************************************************
changed: [192.168.33.100] => {"changed": true, "cmd": ["whoami"], "delta": "0:00:00.005359", "end": "2018-12-01 04:57:50.544723", "rc": 0, "start": "2018-12-01 04:57:50.539364", "stderr": "", "stderr_lines": [], "stdout": "root", "stdout_lines": ["root"]}
PLAY RECAP *************************************************************************************************************
192.168.33.100 : ok=2 changed=2 unreachable=0 failed=0
2つ目のタスクlogin user
に記載したbecome: false
が効かずにroot
ユーザーで実行されてしまいました。
回避策
全てのタスクをbecome_user
(デフォルトではroot)で実行することが決まっていない限りは、
変数としてansible_become: true
は持たせずに、Playbookの記述で制御するのが良さそうです。
以下に続くモジュール系ではbecome
しているかどうかが重要な意味を持っていたため、
本件のように変数に書いたbecome
が解除されずにハマってしまいました。
モジュール編
fetchモジュールでMemoryErrorが発生することがある
ファイル転送系のモジュールではcopyモジュールがよく使われますが、
copyモジュールは Ansible実行マシン ===> リモートマシン
のようにファイルを転送するのに対し、
fetchモジュールはその逆でAnsible実行マシン <=== リモートマシン
のようにリモートマシン上のファイルを取得します。
しかし、以下のようなエラーが発生するケースがしばしば起こりました。
長々と書かれていますが、最後にあるMemoryError
が発生していることがわかります。
TASK [test fetch by root] **********************************************************************************************
fatal: [192.168.33.100]: FAILED! => {"changed": false, "module_stderr": "Shared connection to 192.168.33.100 closed.\r\n", "module_stdout": "Traceback (most recent call last):\r\n File \"/tmp/ansible_U2m0xP/ansible_module_slurp.py\", line 87, in <module>\r\n main()\r\n File \"/tmp/ansible_U2m0xP/ansible_module_slurp.py\", line 83, in main\r\n module.exit_json(content=data, source=source, encoding='base64')\r\n File \"/tmp/ansible_U2m0xP/ansible_modlib.zip/ansible/module_utils/basic.py\", line 2364, in exit_json\r\n File \"/tmp/ansible_U2m0xP/ansible_modlib.zip/ansible/module_utils/basic.py\", line 2358, in _return_formatted\r\n File \"/tmp/ansible_U2m0xP/ansible_modlib.zip/ansible/module_utils/basic.py\", line 2312, in jsonify\r\n File \"/tmp/ansible_U2m0xP/ansible_modlib.zip/ansible/module_utils/basic.py\", line 795, in jsonify\r\n File \"/usr/lib64/python2.7/json/__init__.py\", line 250, in dumps\r\n sort_keys=sort_keys, **kw).encode(obj)\r\n File \"/usr/lib64/python2.7/json/encoder.py\", line 210, in encode\r\n return ''.join(chunks)\r\nMemoryError\r\n", "msg": "MODULE FAILURE", "rc": 1}
fetchモジュールのドキュメントを見てみるとバッチリ書いてあり、状況も一致していました。 *When running fetch with
原文
become
, the slurp
module will also be used to fetch the contents of the file for determining the remote checksum. This effectively doubles the transfer size, and depending on the file size can consume all available memory on the remote or local hosts causing a MemoryError
. Due to this it is advisable to run this module without become
whenever possible.
ざっくりとまとめると、
fetchモジュールでは対象ファイルのチェックサムを確認するためにslurpモジュールというものを使っているようですが、
このslurpモジュールはインメモリでBase64のチェックサムを取得するため、対象ファイルのファイルサイズの最低でも2倍のRAMを消費する、とのことです。
さらにbecome: true
を使用していると、このRAM消費がさらに2倍になってしまう(トータル最低4倍)ようです。
それでメモリが不足してMemoryError
が発生するケースがあるんですね。
また、転送できたとしてもかなり時間がかかります。
100MB程度のファイルを仮想マシンからrsyncで取得すると1秒程度なのですが、fetchだと分単位かかりました。
チェックサムにそれだけ時間がかかっているのでしょうか。。
回避策
回避策はいくつか考えられます
-
become: false
にする -
validate_checksum
をfalse
にする - 別のモジュールを使う
become: false
にする方法ですが、
取得したいファイルによってはログインユーザではパーミッション不足になってしまうこともあります。
私の場合はワンタイムでしか使わないコードであまり考える時間を取りたくなかったので、
/tmp/
配下にコピーしてfetchし、fetchが終わったら削除する、というイケてない方法を取りました。。
validate_checksum
をfalse
なのですが、ファイルの一貫性は担保できないもののコレでいいかなと思って使ってみたのですが、
何故か同様のエラーが出てしまいました。
もしかしたらオプションが認識されていない?と思いソースを見てたのですが、
ansible-docで表示されるドキュメント情報のみで実体は別のところにあるのかわかりませんでした。。
別のモジュールとしては同じファイル操作系のモジュールであるsynchronize が使用できそうです。
このモジュールはcopy, fetchのようなファイル転送モジュールの一つで、
Linuxではおなじみのrsync
のようにディレクトリを再帰的にコピーすることができるモジュールです。
このモジュールはmode
が2通りあり、
デフォルトではpush
モードでcopyモジュールのようにAnsible実行マシン ===> リモートマシン
にデータを転送しますが、
pull
モードにするとfetchモジュールのようにAnsible実行マシン <=== リモートマシン
でデータを取得できます。
このpullモードにすることでfetchの代わりとして使えそうです。
- hosts: all
gather_facts: false
become: true
tasks:
- name: sync
synchronize:
mode: pull
src: /etc/sysconfig/ # リモートファイルパス
dest: files # 格納先(相対パスでも絶対パスでも可)
しかしsynchronizeモジュールもどんな状況でも使用できるわけではなく、
Currently, synchronize is limited to elevating permissions via
passwordless
sudo
. This is because rsync itself is connecting to the remote machine and rsync doesn’t give us a way to pass sudo credentials in.
とあるように、become
の方法としてsudo
のみでパスワード入力不要なものに限られます。
Ansibleではbecome_methodとして様々なものが使え、Linuxであればデフォルトのsudo
の他にsu
を使用する方法もありますが、
su
のみ可能でsudo
が不可能な環境ではfetchモジュールと同様にログインユーザからアクセス可能なファイルしか取得できません。
まとめ
Ansibleに限らずどんなツールにも言えることですが、
ツールの概要だけ読んで「こうやって書けば使えるんじゃない?」というイメージは掴めますが、
実際動かしてみると思ったとおりには動かない、なんてことは結構あるんだなーと感じました。
また、Ansibleを構成管理ツールとして見ると、
構築のためにファイルを送ることは多くても、ファイルを取ってくることは少ないので、fetchモジュールのような機能は若干マイナーな使い方なのかな?と思いましたが、
運用自動化ツールとして見る場合は十分使う機会はあると思いました。
自分のハマりどころが少しでも他のユーザーの方々のお役に立てば嬉しいです。