2019-10-04 修正:
新しいインスタンスで実行したらamazon-linux-extras install
の実行中に Ansible が停止するようになってしまったので、amazon-linux-extras
コマンドの用途をリポジトリの有効化だけに変更しました。
普通にRPMパッケージをインストールする場合
普通は Ansible の yum モジュールを使えば、以下のように簡単に RPM パッケージをインストールすることができます。
- name: Install Apache HTTPD
yum:
name: httpd
state: present
state
パラメータに「present」を指定することで、指定のパッケージがまだインストールされていなければインストールを実行し、すでにインストール済みなら何もしないで正常終了します。これは OSが Amazon Linux 2 でも CentOS でも同じように動作します。
Amazon Linux 2にNginxをインストールする場合
ところが同じ方法で Amazon Linux 2 に Nginx をインストールしようとしたら、以下ようにamazon-linux-extras
コマンドを使えと表示されて失敗しました。
To use, run
# sudo amazon-linux-extras install nginx1.12
しかしメッセージのとおりにコマンドを使ってインストールを実行するには、パッケージがインストール済みかどうかを判定する処理が必要になります。
一般的な方法
以下の方法が一般的な方法のようです。
- 1つめのタスクで yum モジュールを使って、指定パッケージのリストを取得する
- 結果を
register
で指定した変数に保存する - 2つめのタスクの
when
で、インストール済みパッケージを変数から抽出する - インストール済みパッケージが 0件なら shell モジュールを使ってインストールのコマンドを実行する
- name: Get Nginx package list
yum:
list: nginx
register: pkg_list
- name: Install Nginx if not installed
shell: amazon-linux-extras install nginx1.12
when: pkg_list.results | selectattr("yumstate", "match", "installed") | list | length == 0
1つめのタスクで使用している yum モジュールのlist
パラメータにパッケージ名を指定すると、yum コマンドの「yum list <パッケージ名>」と同等の動作になります。
出力はregister
で指定した変数pkg_list
に格納され、以下のように取得したリストがresults
に格納されます。
"pkg_list": {
"changed": false,
"failed": false,
"results": [
{
"arch": "x86_64",
"envra": "1:nginx-1.12.2-2.amzn2.0.1.x86_64",
"epoch": "1",
"name": "nginx",
"release": "2.amzn2.0.1",
"repo": "amzn2extra-nginx1.12",
"version": "1.12.2",
"yumstate": "available"
},
(略)
{
"arch": "x86_64",
"envra": "1:nginx-1.12.2-2.amzn2.0.1.x86_64",
"epoch": "1",
"name": "nginx",
"release": "2.amzn2.0.1",
"repo": "installed",
"version": "1.12.2",
"yumstate": "installed"
}
]
}
リストの最初の要素はyumstate
の値が"available"
なので、まだインストールされていないリポジトリ上のパッケージ(インストール可能)です。しかし最後のパッケージはyumstate
の値が"installed"
なので、これがローカルにインストール済みのパッケージです。
2つめのタスクで「selectattr」というフィルタを使って、yumstate
が"installed"
と一致する要素だけをリストから抽出し、その件数が 0件かどうかをwhen
の判定条件にしています。
when: pkg_list.results | selectattr("yumstate", "match", "installed") | list | length == 0
AWS Cloud9で失敗
ところが Ansible のコントロールノードに AWS Cloud9 を使っていると、このタスクは失敗して以下のメッセージが表示されました。
The error was: template error while templating string: no filter named 'selectattr'.
AWS Cloud9 は RHEL6 ベースなので epel からインストールできる Jinja2 のバージョンが古く、「selectattr」フィルタが実装される前のバージョンでした。
改良した方法
そもそも「selectattr」フィルタを使っている目的は、yum モジュールで取得したパッケージリストからyumstate
の値が「installed」のパッケージを抽出するためです。しかし最初からインストール済みパッケージだけのリストを取得することができれば、後からフィルタを使って絞り込む必要がなくなります。
これは yum コマンドなら「yum list installed <パッケージ名>」を実行すれば簡単に実現できるのですが、yum モジュールの`list`パラメータに指定できる値は「installed」と<パッケージ名>が排他になっているらしく、両方を同時に指定すると意図した動作になりませんでした。
代わりに以下のように、yum モジュールにdisablerepo
パラメータを追加してすべてのリポジトリへの参照を無効化することで、ローカルにインストール済みのパッケージ(installed)だけのリストを取得することができました。
- name: Get Nginx package list from local
yum:
disablerepo: "*"
list: nginx
register: installed_pkgs
- name: Enable amzn2extra-nginx1.12 repository if nginx is not installed
shell: amazon-linux-extras enable nginx1.12
when: installed_pkgs.results | length == 0
- name: Install Nginx packages from amazon-linux-extras
yum:
name: nginx
state: present
これで2つめのタスクのwhen
では、単純にリストが 0件なら未インストールと判定できるようになったので、「selectattr」フィルタが不要になりました。
when: installed_pkgs.results | length == 0
結果的に古い Jinja2 でも正しく動作するようになっただけでなく、判定条件の単純化によってコードの可読性が向上し、リポジトリを参照しないため処理時間を短縮することができました。
別の方法
上記の方法は、わざわざ nginx がインストール済みかを判定してamazon-linux-extras enable nginx1.12
を実行するかどうかを決めていますが、リポジトリの有効化をchanged
の対象にしないなら、以下のようにもっと簡単になります。
- name: Enable amzn2extra-nginx1.12 repository
shell: amazon-linux-extras enable nginx1.12
changed_when: false
- name: Install Nginx packages from amazon-linux-extras
yum:
name: nginx
state: present
1つめのタスクは無条件に毎回実行されますが、changed_when: false
によってchanged
にならず、2つめのタスクは nginx がインストール済みかを yum モジュールが判定してくれます。
参考: 別の方法(shellモジュールとyumコマンドを使う)
以下の方法でも AWS Cloud9 の古い Jinja2 で正しく動作します。ただし Ansible に警告されてしまうので上記の方法の方が良いのですが、shell モジュールを使ってパッケージリストを取得する場合の参考として記載します。
- name: Check if Nginx is installed
shell: yum list installed nginx
register: result
failed_when: result.rc not in [0, 1]
changed_when: false
- name: Enable amzn2extra-nginx1.12 repository if nginx is not installed
shell: amazon-linux-extras enable nginx1.12
when: result.rc == 1
- name: Install Nginx packages from amazon-linux-extras
yum:
name: nginx
state: present
- 1つめのタスクで yum モジュールではなく、shell モジュールで yumコマンドを実行する
- 「yum list installed <パッケージ名>」を実行することで、インストール済みの指定パッケージだけのリストを取得する
- 結果を
register
で指定した変数に保存する - 2つめのタスクの
when
で、yum コマンドの終了ステータスが 1 なら未インストールと判定して、shell モジュールを使ってインストールコマンドを実行する
コマンド実行結果の参照方法
shell モジュールでコマンドを実行した場合、register
で指定した変数のrc
要素にコマンドの終了ステータス(リターンコード)が保存されます。実際の値は実行したコマンドに依存しますが、「yum list」の場合は 1件以上のリストを取得できれば 0(正常終了)、0件なら 1(異常終了)になります。
2つめのタスクで「終了ステータスが 1であること」をwhen
の判定条件にすることで、インストール済みの指定パッケージが存在しない場合にコマンドを実行することができます。
when: result.rc == 1
今回は使用しませんが、コマンドの標準出力はstdout
要素、標準エラー出力はstderr
要素で参照することができます。
エラーの発生を抑止する方法
しかしこれだけでは、yum コマンドの終了ステータスが 1の場合にタスクが失敗したと判断され、Ansible が実行を中断してしまいます。
そこで終了ステータスが 1 になるのは意図した結果であることを Ansible に知らせるために、以下の定義を追加します。
failed_when: result.rc not in [0, 1]
これは「失敗と判定する条件」を「コマンドの終了ステータスが[0, 1]
に含まれない場合」とする定義なので、1が失敗から除外されます。
代わりに以下の記述でも Ansible が中断することを抑止することができますが、これは「失敗しても無視して実行を継続する」という指定なので、終了ステータス 1 が失敗と判断されること自体は変わりません。
ignore_errors: true
今回のように 0 以外の終了ステータス値が意図した結果として明確な場合は、failed_when
で明示的に失敗条件から除外するのが正しい方法です。
'changed'と判定されることを抑止する
shell モジュールでコマンドを実行すると Ansible は常に'changed'と判定してしまいますが、今回は「yum list」を使ってリストを取得しているだけなので、この判定を抑止します。以下の定義で Ansible が変化の判定をしないように指定します。
changed_when: false
結果
パッケージがインストール済みの場合は何もしないでタスクが正常終了し、以下のように changed、failed ともに0件になりました。
PLAY RECAP *******************************************************************
target1 : ok=2 changed=0 unreachable=0 failed=0
ただし以下のように、「yum コマンドを実行するより yum モジュールを使うべき」という警告を抑止することはできませんでした。
TASK [nginx : Check if Nginx is installed] ***********************************
[WARNING]: Consider using the yum module rather than running yum. If you ...