Help us understand the problem. What is going on with this article?

AnsibleでEC2のAmazon Linux 2にNginxをインストールする方法の検討(古いJinja2でもOK)

2019-10-04 修正:
新しいインスタンスで実行したらamazon-linux-extras installの実行中に Ansible が停止するようになってしまったので、amazon-linux-extrasコマンドの用途をリポジトリの有効化だけに変更しました。

普通にRPMパッケージをインストールする場合

普通は Ansible の yum モジュールを使えば、以下のように簡単に RPM パッケージをインストールすることができます。

roles/httpd/tasks/main.yml
- 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. 1つめのタスクで yum モジュールを使って、指定パッケージのリストを取得する
  2. 結果をregisterで指定した変数に保存する
  3. 2つめのタスクのwhenで、インストール済みパッケージを変数から抽出する
  4. インストール済みパッケージが 0件なら shell モジュールを使ってインストールのコマンドを実行する
roles/nginx/tasks/main.yml
- 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
    "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)だけのリストを取得することができました。

roles/nginx/tasks/main.yml
- 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の対象にしないなら、以下のようにもっと簡単になります。

roles/nginx/tasks/main.yml
- 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 モジュールを使ってパッケージリストを取得する場合の参考として記載します。

roles/nginx/tasks/main.yml
- 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. 1つめのタスクで yum モジュールではなく、shell モジュールで yumコマンドを実行する
  2. 「yum list installed <パッケージ名>」を実行することで、インストール済みの指定パッケージだけのリストを取得する
  3. 結果をregisterで指定した変数に保存する
  4. 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 ...
3244
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした