AnsibleでのSELinuxの無効化とリブート

はじめに

CentOS7でSELinuxを無効化するPlaybookを書くのが一苦労だったので、試行錯誤の結果を記録しておく。

試した環境

Playbookを実行する側

CentOS 7.4をyum updateで最新化して、ansibleをインストールした環境。

$ uname -srvm
Linux 3.10.0-693.21.1.el7.x86_64 #1 SMP Wed Mar 7 19:03:37 UTC 2018 x86_64

$ cat /etc/redhat-release
CentOS Linux release 7.4.1708 (Core)

$ ansible-playbook --version | grep -E '^ansible-playbook'
ansible-playbook 2.4.2.0

$ python --version
Python 2.7.5

Playbookを実行される側

CentOS 7.4をインストールした直後の環境。

$ uname -srvm
Linux 3.10.0-693.el7.x86_64 #1 SMP Tue Aug 22 21:09:27 UTC 2017 x86_64

$ cat /etc/redhat-release
CentOS Linux release 7.4.1708 (Core)

$ python --version
Python 2.7.5

Playbook

ここではselinux-disablerというロールを作成した。 tasks/main.ymlは以下の通り。

- name: (1) SELinux用のPythonモジュールをインストール
  yum: name=libselinux-python state=installed

- name: (2) SELinuxの無効化
  selinux: state=disabled
  register: selinux

- name: (3) SSHのポート番号の取得
  set_fact:
    ssh_port: "{{ hostvars[inventory_hostname].ansible_port if 'ansible_port' in hostvars[inventory_hostname] else 22 }}"
  when: selinux.reboot_required

- name: (4) マシンのリブート
  shell: "sleep 2 && reboot"
  async: 1
  poll: 0
  when: selinux.reboot_required

- name: (5) マシンの停止を待ち合わせ
  local_action: wait_for host={{ inventory_hostname }} port={{ ssh_port }} state=stopped
  when: selinux.reboot_required

- name: (6) マシンの起動を待ち合わせ
  local_action: wait_for host={{ inventory_hostname }} port={{ ssh_port }} state=started
  when: selinux.reboot_required

Playbookの解説

(1) SELinux用のPythonモジュールをインストール

- name: (1) SELinux用のPythonモジュールをインストール
  yum: name=libselinux-python state=installed

モジュールselinuxを利用するためには、事前にlibselinux-pythonのインストールが必要。

(2) SELinuxの無効化

- name: (2) SELinuxの無効化
  selinux: state=disabled
  register: selinux

モジュールselinuxを利用してSELinuxを無効化。 以前は冪等性が正しく実装されていなかったのか、ググるとコマンドgetenforceの実行結果を元にモジュールselinuxを実行するかどうかを判断してるサンプルが多数引っかかる。 例えば以下のような感じ。

- name: SELinuxの状態を取得
  shell: getenforce
  changed_when: False
  register: selinux_state

- name: SELinuxの無効化
  selinux: state=disabled
  when: selinux_state.stdout != 'Disabled'

ドキュメント的には、Ansible 2.4からモジュールselinuxのreturn valuesについての説明が加わっているので、このあたりで整備されたと思われる。

なお、リブートが必要かどうかはモジュールselinuxの戻り値のreboot_requiredを調べると分かる。

(3) SSHのポート番号の取得

- name: (3) SSHのポート番号の取得
  set_fact:
    ssh_port: "{{ hostvars[inventory_hostname].ansible_port if 'ansible_port' in hostvars[inventory_hostname] else 22 }}"
  when: selinux.reboot_required

ここは、単に好き嫌いの問題。 ハードコーディングが気にならないのであれば、わざわざSSHのポート番号をひねくり出さなくても、wait_forでの待ち合わせで以下のように「port=22」にしておけばよい。

- name: マシンの起動を待ち合わせ
  local_action: wait_for host={{ inventory_hostname }} port=22 state=started

ネット上にあるサンプルでは、上記のようにwait_forの待ち合わせでSSHのポート22がハードコーディングされているものが殆どだった。 ハードコーディングが嫌いなので標準の変数がないか探したところ、ansible_portがそれに該当することが分かった。 さらに、ansible_portはhostvars経由で参照するようなので、以下のように試してみた。

- name: マシンの起動を待ち合わせ
  local_action: wait_for host={{ inventory_hostname }} port={{ hostvars[inventory_hostname].ansible_port }} state=started

しかし、この変数は以下のようにインベントリで明示的に指定している場合は使えるのだが、そうでない場合は「未定義」となり、参照箇所でエラーが発生する。

[server]
192.168.8.3 ansible_port=22

なんで適切なデフォルト値で変数を準備してくれないんだよ、と思って調べてみると、以下の報告があった。

結論は、バグではなく仕様と言い切っている。

but if you want to ensure a variable value is always available for use in templates and stuff, you'll need to set it yourself- the connection plugins don't have a way to smuggle out the value they used.

文意を崩さないように心がけつつ意訳。

もし、テンプレートでの利用か何かの都合でansible_portの値が必ず存在していてほしいのであれば、自分自身でansible_portの値を明示的に設定しておく必要がある。 なぜならば、現在のところコネクションプラグインが利用している内部的なデフォルト値を外に出す方法は準備されていないからだ。

つまり、SSHプラグインでは22がデフォルトで、WinRMのプラグインでは5986がデフォルトだが、これらのプラグインがデフォルト値をansible_portなどに書き出す仕組みは現状実装されていないので諦めてくれ、ということらしい。 仕方がないので、以下のようにした。

  set_fact:
    ssh_port: "{{ ansible_portが存在するならその値、存在しなければ22 }}"

最終的に、Jinja2 (Python)で書くと以下のようになった。

  set_fact:
    ssh_port: "{{ hostvars[inventory_hostname].ansible_port if 'ansible_port' in hostvars[inventory_hostname] else 22 }}"

(4) マシンのリブート

- name: (4) マシンのリブート
  shell: "sleep 2 && reboot"
  async: 1
  poll: 0
  when: selinux.reboot_required

コマンドrebootを利用してリブートする。

ただし、単純にrebootを実行するとSSHの切断が検知されてplaybookの実行に失敗する。 そのためasyncを利用して非同期実行を行う。 その際には(結果確認の?)ポーリングをオフにするために、pollに0を指定しておく。

また、rebootと同時に実行しているsleepは何らかのタイミング調整だが、詳細は把握しきれていない。 asyncの値とsleepの引数の値には何か制約がありそうな気がする。 例えば、async < sleepでなければならない、等。 試しにsleepを削除したところ、SSHの切断が検知されてplaybookの実行が失敗した。

詳細はそのうち整理しよう。

(5) マシンの停止を待ち合わせ

- name: (5) マシンの停止を待ち合わせ
  local_action: wait_for host={{ inventory_hostname }} port={{ ssh_port }} state=stopped
  when: selinux.reboot_required

SSHのポートがクローズされることで、マシンが停止したと判断している。 ググって見つかるリブートのサンプルには、起動の待ち合わせを遅延実行することで、停止の待ち合わせを省略しているものもある。 例えば、以下のように。

- name: マシンのリブート
  shell: "sleep 2 && reboot"
  async: 1
  poll: 0

- name: マシンの起動を待ち合わせ (delay=30により、30秒後から待ち合わせ開始)
  local_action: wait_for host={{ inventory_hostname }} port=22 delay=30 state=started

停止の待ち合わせが本当に必要かどうかはよく分からないが、delayの値をうまく決められないので、今回は丁寧に停止の待ち合わせをしておくことにする。

(6) マシンの起動を待ち合わせ

- name: (6) マシンの起動を待ち合わせ
  local_action: wait_for host={{ inventory_hostname }} port={{ ssh_port }} state=started
  when: selinux.reboot_required

SSHのポートがオープンされることで、マシンが起動したと判断している。

実行結果 - SELinuxが有効の場合

以下の(2)で、モジュールselinuxSELinuxを無効化している。 ただしその反映にはリブートが必要なので、後続の(3)〜(6)のリブート処理を実行している。

PLAY [server] ******************************************************************

TASK [Gathering Facts] *********************************************************
ok: [192.168.8.3]

TASK [selinux-disabler : (1) SELinux用のPythonモジュールをインストール] **********************
ok: [192.168.8.3]

TASK [selinux-disabler : (2) SELinuxの無効化] **************************************
 [WARNING]: SELinux state change will take effect next reboot
changed: [192.168.8.3]

TASK [selinux-disabler : (3) SSHのポート番号の取得] *************************************
ok: [192.168.8.3]

TASK [selinux-disabler : (4) マシンのリブート] *****************************************
changed: [192.168.8.3]

TASK [selinux-disabler : (5) マシンの停止を待ち合わせ] *************************************
ok: [192.168.8.3 -> localhost]

TASK [selinux-disabler : (6) マシンの起動を待ち合わせ] *************************************
ok: [192.168.8.3 -> localhost]

PLAY RECAP *********************************************************************
192.168.8.3                : ok=7    changed=2    unreachable=0  failed=0

実行結果 - SELinuxが無効の場合

以下の(2)で、モジュールselinuxの冪等性の検査が正しく行われて、結果がokになっている。 またリブートは不要なので、後続の(3)〜(6)のリブート処理をスキップしている。

PLAY [server] ******************************************************************

TASK [Gathering Facts] *********************************************************
ok: [192.168.8.3]

TASK [selinux-disabler : (1) SELinux用のPythonモジュールをインストール] **********************
ok: [192.168.8.3]

TASK [selinux-disabler : (2) SELinuxの無効化] **************************************
ok: [192.168.8.3]

TASK [selinux-disabler : (3) SSHのポート番号の取得] *************************************
skipping: [192.168.8.3]

TASK [selinux-disabler : (4) マシンのリブート] *****************************************
skipping: [192.168.8.3]

TASK [selinux-disabler : (5) マシンの停止を待ち合わせ] *************************************
skipping: [192.168.8.3]

TASK [selinux-disabler : (6) マシンの起動を待ち合わせ] *************************************
skipping: [192.168.8.3]

PLAY RECAP *********************************************************************
192.168.8.3                : ok=3    changed=0    unreachable=0  failed=0

その他

SELinuxを無効化した後にすぐにリブートするかどうかは要件次第なので、場合によってはハンドラにリブート処理を書いたほうがよいと思われる。