Ansibleでシェルコマンドを実行させるときのノウハウ

  • 88
    いいね
  • 0
    コメント

基本的にAnsible側に制御を委ねることで冪等性の保持やエラーハンドリングが成されるので、やりたいtaskに対してまずはmoduleを探すべき(sedを使わずにlineinfileモジュールを使えないかとか)ではあるのだが、どうしてもmoduleで実現できない場合にシェルコマンド直書きになってしまう場合はある。 てかコマンド書いちゃうならAnsibleじゃなくてシェルスクリプトでいいじゃんって話でさ。

※追記 2017-04-05 17:00

記事執筆時点から時を経た現在の自分の見解としては、shell, command両モジュールの活用には以下の面から否定的であり、なるべくコマンド実行は行わないべきと考えています。

  • 冪等性の保証が困難になる。
  • Ansible Playbookは実現されるべきシステムの「状態」を示した「実行可能なドキュメント」という側面があるが、コマンドが記載されている場合は実現される「状態」が判別しづらくなる。
  • (冗談めかして書いてはみたが)コマンドによるデプロイを行いたいのであればシェルスクリプトでよい。Ansibleを何のために使っているのか?

commandではなくshellを使う

シェルコマンド直書きする場合によく話題になることとして、commandshellのどちらのモジュールを使うべきかというのがあるが、shellを使う方が間違いない場合が多い。相違点としては、公式ドキュメント上でcommand"It will not be processed through the shell"と表現されている通り、シェルを通すのでシェルの機能が使える点。commandが環境変数を読めない、&やパイプ、リダイレクトが使えないのに対し、shellではこれらが使用できる。

シェルを通さないのでcommandには環境に左右されず実行できるという利点はあるが、あまりそれが必要になる場面も浮かばないので、迷うようならshellだけ使っていても良いのではないかと。

※追記 2016-05-19 08:45

Ansible Docsには以下の記述があるので、汎用性や安全性を高めるのであればcommandの方が望ましい。あくまでパイプ等のシェル機能がどうしても必要な場合、あるいは楽をしたい場合はshellという位置付けの方が良さそうです。

The command module is much more secure as it’s not affected by the user’s environment.

エラーハンドリング

Ansibleはリターンコードが0以外の場合にfailedと判断するが、シェルコマンドを実行させる際にこの条件だと困ることがある。ignore_error: Trueを使ってエラーを完全に無視させることも可能だが、register:failed_when:を使った方がより細かく制御できる。

- name: check default ruby version
  shell: /home/{{ user_name }}/.rbenv/bin/rbenv version | grep {{ ruby_version }}
  register: ruby_version_check
  failed_when: ruby_version_check.rc not in [0, 1]

register:は指定した名前の変数にコマンドの実行結果をregisterする。公式ドキュメント上での記載はおそらくここになると思うものの、あまり詳しくなくてモンニョリするのだが、キーバリュー形式でいくつかの値が収まっており、.hogeと記述することで値を取り出すことができる。

  • rc => リターンコード
  • stdout => 標準出力
  • stderr => 標準エラー出力
  • stdout_lines => 標準出力を1行ずつ出力する(with_items等と組み合わせて使う模様)

上記のyamlではリターンコードを取り出して、0か1以外の場合はfailedとなるよう指定している。grepのリターンコードは0と1に限られるので事実上ignore_error:と変わらない指定になっているが、例えば0のときにfailedにもできるし、stdoutの内容を確認してfailedさせることもできるので、failed_when:を使う習慣を身に付けて良さそうに思う。

またregisterした変数はその後のtaskでも引き続き使えるので、コマンド実行結果をwhen:で条件分岐として利用して、taskの実行要否を判断させることができる。上述のyamlでやろうとしているのがまさにそれで、rbenvで指定しているデフォルトのRubyバージョンが想定と異なる場合に限りrbenv globalを実行させるよう、下記のtaskを続かせている。

- name: set default ruby version
  shell: /home/{{ user_name }}/.rbenv/bin/rbenv global {{ ruby_version }}
  when: ruby_version_check.rc == 1

becomeやsudoを使う場合の注意

taskをsudo実行させるbecome: yes(あるいはdeprecatedになっているsudo: yes)を指定してshellモジュールを実行する場合、当然ながらコマンド実行ユーザーがroot(もしくは指定されたbecomeユーザー)になるので留意する。例えば新しくファイルやディレクトリがそのコマンドの中で作られると所有者がrootになってしまったり、予期せぬ構成となることがままありうる。become: yesをズボラにplaybook内で指定しておけば全taskがsudo実行されるのでエラーが起こらなくなるが、shellモジュールを含んでいる場合はtask単位できちんとbecome: yesの要否を見極めた方が良い。