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

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

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

```Yaml: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 モジュールを使ってインストールのコマンドを実行する

```Yaml: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`に格納されます。

```Json: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`の判定条件にしています。

```Yaml:
  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）だけのリストを取得することができました。

```Yaml: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」フィルタが不要になりました。

```Yaml:
  when: installed_pkgs.results | length == 0
```

結果的に古い Jinja2 でも正しく動作するようになっただけでなく、判定条件の単純化によってコードの可読性が向上し、リポジトリを参照しないため処理時間を短縮することができました。

## 別の方法
上記の方法は、わざわざ nginx がインストール済みかを判定して`amazon-linux-extras enable nginx1.12`を実行するかどうかを決めていますが、リポジトリの有効化を`changed`の対象にしないなら、以下のようにもっと簡単になります。

```Yaml: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 モジュールを使ってパッケージリストを取得する場合の参考として記載します。

```Yaml: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`の判定条件にすることで、インストール済みの指定パッケージが存在しない場合にコマンドを実行することができます。

```Yaml:
  when: result.rc == 1
```

今回は使用しませんが、コマンドの標準出力は`stdout`要素、標準エラー出力は`stderr`要素で参照することができます。

### エラーの発生を抑止する方法
しかしこれだけでは、yum コマンドの終了ステータスが 1の場合にタスクが失敗したと判断され、Ansible が実行を中断してしまいます。
そこで終了ステータスが 1 になるのは意図した結果であることを Ansible に知らせるために、以下の定義を追加します。

```Yaml:
  failed_when: result.rc not in [0, 1]
```

これは「失敗と判定する条件」を「コマンドの終了ステータスが`[0, 1]`に含まれない場合」とする定義なので、1が失敗から除外されます。

代わりに以下の記述でも Ansible が中断することを抑止することができますが、これは「失敗しても無視して実行を継続する」という指定なので、終了ステータス 1 が失敗と判断されること自体は変わりません。

```Yaml:
  ignore_errors: true
```

今回のように 0 以外の終了ステータス値が意図した結果として明確な場合は、`failed_when`で明示的に失敗条件から除外するのが正しい方法です。

### 'changed'と判定されることを抑止する
shell モジュールでコマンドを実行すると Ansible は常に'changed'と判定してしまいますが、今回は「yum list」を使ってリストを取得しているだけなので、この判定を抑止します。以下の定義で Ansible が変化の判定をしないように指定します。

```Yaml:
  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 ...
```