Jaybanuan's Blog

どうせまた調べるハメになることをメモしていくブログ

Ansibleでの環境変数の設定(/etc/environment)が挙動不審

はじめに

AnsibleのPlaybookで/etc/environmentにプロキシ関連の環境変数(http_proxyとか)を書き込んだ際、書き込んだはずの環境変数が後続のタスクで参照できなかったので調査した。

環境

$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 20.04.1 LTS"

$ ansible --version
ansible 2.9.7
  config file = None
  configured module search path = ['/home/redj/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/redj/.local/lib/python3.8/site-packages/ansible
  executable location = /home/redj/.local/bin/ansible
  python version = 3.8.2 (default, Jul 16 2020, 14:00:26) [GCC 9.3.0]

現象

試行 1 - 環境変数が見えない

以下のPlaybookを実行すると、最後のタスクdebughttp_proxyなどが表示されてほしいが、実際には表示されない。

- hosts: target
  tasks:
    - name: /etc/environment にプロキシの環境変数を書き込み
      become: yes
      ini_file:
        path: /etc/environment
        no_extra_spaces: yes
        section: null
        option: "{{ item.key }}"
        value: "{{ item.value }}"
      loop: "{{ proxy_envs | dict2items }}"
      vars:
        proxy_envs:
          http_proxy: http://192.168.8.8:3128/
          https_proxy: http://192.168.8.8:3128/
          no_proxy: 127.0.0.1,localhost
    
    - name: 環境変数の取得
      shell: env | grep "_proxy" | cat
      register: result
      
    - name: 環境変数の表示
      debug:
        var: result.stdout_lines

実行結果は以下のようになり、プロキシ関連の環境変数が見つからないことがわかる。

$ ansible-playbook -i hosts.yml playbook.yml

PLAY [target] **********************************************************************************************************************

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

TASK [/etc/environment にプロキシの環境変数を書き込み] ********************************************************************************************
changed: [192.168.8.16] => (item={'key': 'http_proxy', 'value': 'http://192.168.8.8:3128/'})
changed: [192.168.8.16] => (item={'key': 'https_proxy', 'value': 'http://192.168.8.8:3128/'})
changed: [192.168.8.16] => (item={'key': 'no_proxy', 'value': '127.0.0.1,localhost'})

TASK [環境変数の取得] *********************************************************************************************************************
changed: [192.168.8.16]

TASK [環境変数の表示] *********************************************************************************************************************
ok: [192.168.8.16] => {
    "result.stdout_lines": []
}

PLAY RECAP *************************************************************************************************************************
192.168.8.16               : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

試行 2 - 環境変数が見える

試しにもう一度実行してみると、今回は環境変数は見えている。 /etc/environmentの更新がokになっていることから、初回の実行での環境変数の追加は成功していることが分かる。 なぜ初回の実行では環境変数が見えていないんだろうか…。

$ ansible-playbook -i hosts.yml playbook.yml

PLAY [target] **********************************************************************************************************************

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

TASK [/etc/environment にプロキシの環境変数を書き込み] ********************************************************************************************
ok: [192.168.8.16] => (item={'key': 'http_proxy', 'value': 'http://192.168.8.8:3128/'})
ok: [192.168.8.16] => (item={'key': 'https_proxy', 'value': 'http://192.168.8.8:3128/'})
ok: [192.168.8.16] => (item={'key': 'no_proxy', 'value': '127.0.0.1,localhost'})

TASK [環境変数の取得] *********************************************************************************************************************
changed: [192.168.8.16]

TASK [環境変数の表示] *********************************************************************************************************************
ok: [192.168.8.16] => {
    "result.stdout_lines": [
        "no_proxy=127.0.0.1,localhost",
        "https_proxy=http://192.168.8.8:3128/",
        "http_proxy=http://192.168.8.8:3128/"
    ]
}

PLAY RECAP *************************************************************************************************************************
192.168.8.16               : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

試行 3 - 環境変数が見える

今度はターゲットのサーバの/etc/environmentを元に戻してやり直してみる。 さらに、Playbookでbecome: yesの位置を以下のようにタスク全体にかかるように移動しておく。

- hosts: target
  become: yes
  tasks:
(以下略)

このPlaybookを実行すると、今度は初回の実行で環境変数が見つかる。 /etc/environmentの更新がchangedになっていることから、環境変数の追加が成功し、かつ後続のタスクで環境変数が見えていることが分かる。 なぜだ…。

$ ansible-playbook -i hosts.yml playbook.yml

PLAY [target] **********************************************************************************************************************

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

TASK [/etc/environment にプロキシの環境変数を書き込み] ********************************************************************************************
changed: [192.168.8.16] => (item={'key': 'http_proxy', 'value': 'http://192.168.8.8:3128/'})
changed: [192.168.8.16] => (item={'key': 'https_proxy', 'value': 'http://192.168.8.8:3128/'})
changed: [192.168.8.16] => (item={'key': 'no_proxy', 'value': '127.0.0.1,localhost'})

TASK [環境変数の取得] *********************************************************************************************************************
changed: [192.168.8.16]

TASK [環境変数の表示] *********************************************************************************************************************
ok: [192.168.8.16] => {
    "result.stdout_lines": [
        "no_proxy=127.0.0.1,localhost",
        "https_proxy=http://192.168.8.8:3128/",
        "http_proxy=http://192.168.8.8:3128/"
    ]
}

PLAY RECAP *************************************************************************************************************************
192.168.8.16               : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

解決方法

結論から言うと、Ansibleが呼び出すsshコマンドのパラメータを調整して、Multiplexing (多重化)を無効化することで解決した。

SSHのMultiplexingとは、ひとつのTCPコネクション上に複数のSSHセッションをのせる仕組みのこと。 言い換えるとコネクションを再利用する仕組みであり、これを利用すると都度接続が不要になため、sshコマンドを連発するような場合に高速化が見込める。

つまりこれはAnsibleに適した高速化の仕組みであり、ANSIBLE_SSH_ARGSという設定項目でデフォルトで有効化されている。 以下にマニュアルでのANSIBLE_SSH_ARGSの説明を抜粋する。

項目 説明
Description: If set, this will override the Ansible default ssh arguments. In particular, users may wish to raise the ControlPersist time to encourage performance. A value of 30 minutes may be appropriate. Be aware that if -o ControlPath is set in ssh_args, the control path setting is not used.
Default: -C -o ControlMaster=auto -o ControlPersist=60s
Ini Section: ssh_connection
Ini Key: ssh_args
Environment: ANSIBLE_SSH_ARGS

Multiplexingを無効化するsshのパラメータは?

sshのMultiplexingに関係するパラメータは、主に以下の3つ。 「簡単な説明」は自分なりにまとめたものなので、正確な定義はOpenSSHのマニュアルを参照のこと。

パラメータ 簡単な説明
ControlMaster yesの場合は自身がMasterであることを、noの場合はMasterではない(=クライアントである)ことを示すautoの場合はMasterがいない時には自動的に自身がMasterになる。
ControlPersist クライアントがいなくなってからMasterのコネクションを切断するまでのタイムアウト値。
ControlPath コネクションを共有するためのソケットファイルのパス。noneを指定した場合はMultiplexingを利用しない。

上の表より、sshコマンドラインパラメータとして-o ControlPath=noneを付け加えれば良い。

注意点として、ググると「Multiplexingをオフにするには-o ControlMaster=noとしておけ」みたいな情報があるが、これは「自分はクライアント」と宣言しているのであり、オフになったわけではない。

Ansibleでの設定方法 1 - ANSIBLE_SSH_ARGS

sshのパラメータの「ベース」を指定するANSIBLE_SSH_ARGSに対して-C -o ControlPath=noneを設定する。 ANSIBLE_SSH_ARGSの設定方法は、ansible.cfgで設定する方法と、環境変数で設定する方法の、2パターンある。

ansible.cfgで設定する場合は、以下のようにする。

[ssh_connection]
ssh_args=-C -o ControlPath=none

また、環境変数で設定するには、Ansibleを以下のように実行するか、あるいは~/.profileなどに当該の環境変数を定義しておく。

$ export ANSIBLE_SSH_ARGS="-C -o ControlPath=none"
$ ansible-playbook playbook.yml

Ansibleでの設定方法 2 - ansible_ssh_extra_args

ansible_ssh_extra_argsで指定したsshのパラメータは、ANSIBLE_SSH_ARGSの内容の後ろに追加されてsshに渡される。 ansible_ssh_extra_argsの設定方法は、Ansibleのコマンドラインパラメータとして設定する方法と、インベントリなどでAnsibleの変数として設定する方法の、2パターンある。

Ansibleのコマンドラインパラメータとして設定する場合は、以下のようにAnsibleを実行する。

$ ansible-playbook --ssh-extra-args="-o ControlPath=none" -i hosts.yml playbook

インベントリでAnsibleの変数として設定するには、インベントリを以下のように記述する。

devpc:
  hosts:
    "192.168.8.16":
      ansible_user: jaybanuan
      ansible_ssh_extra_args: -o ControlPath=none

Ansibleでの設定方法 - まとめ

Ansibleでのsshコマンドラインパラメータの指定場所がややこしいので、以下の表にまとめておく。 個人的には、ansible_ssh_extra_argsをAnsibleの変数としてインベントリで定義すると、接続先に柔軟に対応できるためよいと思う。

設定場所 ANSIBLE_SSH_ARGS ansible_ssh_extra_args
ansible.cfg
起動時のパラメータ
環境変数
Ansibleの変数

結局のところ原因は何?

結局はsshのMultiplexingが影響しているという事以外は分からなかった。 sshのMultiplexingに辿り着いたのは、/etc/environmentはログインしなおせば効くはずなのに効いておらず、Multiplexingはクライアントではsshの認証が省略される(のか?)ので、ログアウトしきれていないのでは、と推測したため。

参考