AnsibleがWindowsに接続するときのプロキシ事情


はじめに

この投稿は Ansible 3 Advent Calendar 2019 の12日目の記事になります。Ansibleと言いつつpywinrmメインの内容になってしまいました…。

プロキシ環境でAnsibleを使ってWindowsホストを操作する場合、プロキシを経由してしまうことでエラーになってしまう場合があります。この問題をプロキシ経由しないようにすることで解決しようと、pywinrmモジュールの挙動を確認したので紹介します。

前提条件

こちらの記事「AnsibleでWindowsを操作する準備をする」を参考にAnsible側, Windows側の準備を済ませている状態です。

$ ansible --version
ansible 2.9.2

$ python3 --version
Python 3.6.8
inventory
[windows]
windows-server ansible_host=192.168.1.1

[windows:vars]
ansible_user=hogeuser
ansible_password=hogepass
ansible_port=5986
ansible_connection=winrm
ansible_winrm_transport=ntlm
ansible_winrm_server_cert_validation=ignore

プロキシ経由のエラー

エラー内容
$ ansible -i inventory -m win_ping windows-server
windows-server | UNREACHABLE! => {
    "changed": false,
    "msg": "ntlm: HTTPSConnectionPool(host='192.168.1.1', port=5986): Max retries exceeded with url: /wsman (Caused by ProxyError('Cannot connect to proxy.', timeout('timed out',)))",
    "unreachable": true
}

1. Ansibleコントロールノードの環境変数no_proxyを参照

Windowsに接続するためansible_connection=winrmとした場合、pywinrmというモジュールを利用してhttp/httpsで接続されます。

pywinrmモジュールのデフォルトでは、Ansibleコントロールノードの環境変数 http_proxy, https_proxy, no_proxy を参照しにいきます。プロキシ環境でAnsible実行時にプロキシを経由させずにWindowsに繋ぎたい場合は、環境変数no_proxyに対象ホスト名 or IPアドレス(ネットワークアドレス)を設定するとよいです。

ちなみに、NO_PROXYも読み込むのですが、no_proxyのほうが優先されるため以下のように別々の設定をしていると予期せぬ動作をする可能性があります。(winrmが参照するrequestsモジュールの仕様)

http_proxy=[proxyserver]
https_proxy=[proxyserver]
no_proxy=192.168.0.0/16
NO_PROXY=192.168.0.0/16,172.16.0.0/12
# no_proxyが優先されてNO_PROXYの'172.16.0.0/12'は反映されないため、172.17.0.1などはプロキシ経由となってしまう

2. pywinrmモジュールをいじる(非推奨)

上記のように、pywinrmが環境変数 http_proxy, https_proxy を参照してしまうためプロキシ経由となってしまうのであれば、環境変数を読み込ませないようすればよいのです。

pywinrmの transport.py というファイルでrequestsモジュールを参照しており、session.trust_env = Trueの場合に環境変数を読み込むようになっています。requestモジュールの説明によると、trust_env は「実行環境の環境変数は信じてええんか?信じてええなら使わせてもらうで!」ってことみたいです。

そこで、transport.pyをsession.trust_env = Falseと書き換えてあげれば、環境変数を読み込まなくなりプロキシを経由せずにWindowsホストに繋がるようになります。

自分の環境の場合は/usr/local/lib/python3.6/site-packages/winrm/配下にtransport.pyがありました。pywinrmのバージョンでtransport.pyの実装が違うため、Ansibleで必須バージョンの0.3.0と、執筆時最新の0.4.1を記載します。

Version 0.3.0

tranport.py(pywinrm==0.3.0)
    def build_session(self):
        session = requests.Session()

        session.verify = self.server_cert_validation == 'validate'
        if session.verify and self.ca_trust_path:
                session.verify = self.ca_trust_path

        # configure proxies from HTTP/HTTPS_PROXY envvars
        #session.trust_env = True # 変更前
        session.trust_env = False # 変更後

0.3.0ではsession.trust_env = Trueという記載があるので、そこをFalseに変更するだけです。

Version 0.4.1

transport.py(pywinrm==0.4.1)
    def build_session(self):
        session = requests.Session()
        proxies = dict()

        if self.proxy is None:
            proxies['no_proxy'] = '*'
        elif self.proxy != 'legacy_requests':
            # If there was a proxy specified then use it
            proxies['http'] = self.proxy
            proxies['https'] = self.proxy

        session.trust_env = False #この行を追加

        # Merge proxy environment variables
        settings = session.merge_environment_settings(url=self.endpoint,
                      proxies=proxies, stream=None, verify=None, cert=None)

バージョン0.4.0からsession.trust_env = Trueの記載自体が無くなっています。session.trust_envはデフォルトがTrueのため、記載が無くてもAnsibleコントロールノードの環境変数が参照されます。

session.trust_env = Falseを追記してあげるとバージョン0.3.0と同じ結果となります。

結果
$ ansible -i inventory -m win_ping windows-server
windows-server | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

pywinrm変更を非推奨とした理由

あくまでも個人的な意見なのですが、pywinrmをいじるのは非推奨です。

pywinrmのバージョンが上がる度に対応が必要

上記で書いた通り、バージョン0.3.0と0.4.1でtransport.pyの実装が変わっています。今後もバージョンが上がる毎に特別対応する必要がでてきます。ちなみに、

AWX公式コンテナイメージを使いづらくなる

AWXのデプロイに公式のawx_taskコンテナを利用している場合、コンテナ内のtransport.pyを変更する必要があります。しかし、コンテナ再起動時には設定が戻ってしまいます。

まとめ

AnsibleがWindowsに接続するときのpywinrmの挙動を確認しました。結局のところ、環境変数no_proxyに対象ホストを設定しておけば良いだけなのですが、退っ引きならない事情でno_proxyに設定できない場合はpywinrmをいじってしまう手段もあります。