Ansibleでインターネットと疎通がないマシンにpyenv-virtualenv環境をデプロイする方法


タイトル通り。
いまどきそんなに需要ないとは思いますが結構手こずったので自分メモ。
前提として、必要な資材は予め疎通がある環境でDLしてAnsible サーバに格納しておくこととします。

## 環境
サーバ、ターゲット共に

* Red Hat Enterprise Linux 7.6
* Python 2.7.5

とします。Ansible バージョンは

* Ansible 2.8.0

です。

### Playbookの中身

```yaml:roles/pyenv/tasks/main.yml
---
- name: install required packages
  yum:
    name:
      - gcc
      - openssl-devel
      - readline-devel
      - bzip2-devel
      - sqlite-devel
      - patch
      - libffi-devel

- name: install pyenv
  unarchive:
    src: pyenv.tgz
    dest: /opt
    exclude: "{{python.version}}"

- name: install pyenv-virutalenv
  unarchive:
    src: pyenv-virtualenv.tgz
    dest: /opt/pyenv/plugins

- name: create directory
  file:
    path: "{{item}}"
    state: directory
  with_items:
    - /opt/pyenv/shims
    - /opt/pyenv/versions

- name: copy python-build
  copy:
    src: "{{python.version}}"
    dest: /opt/pyenv/plugins/python-build/share/python-build/{{python.version}}

- name: copy python-build-plugin
  unarchive:
    src: pyenv_local.tgz
    dest: /tmp

- name: configure pyenv
  copy:
    src: pyenv.sh
    dest: /etc/profile.d/pyenv.sh
  register: _result

- name: load profile
  shell: /bin/bash /etc/profile.d/pyenv.sh
  when: _result.changed

- name: check installed python
  command: /bin/bash -l -c 'pyenv versions'
  register: _result
  changed_when: false
  ignore_errors: true

- name: python-{{python.version}} install
  command: /bin/bash -l -c 'python -m SimpleHTTPServer 80 & pyenv install {{python.version}}'
  args:
    chdir: /tmp
  when: python.version in _result.stdout_lines

- name: check created virtualenv
  command: /bin/bash -l -c 'pyenv versions'
  register: _result
  changed_when: false
  ignore_errors: true

- name: create virtualenv
  command: /bin/bash -l -c 'pyenv virtualenv {{python.version}} {{python.venv_name}}'
  when: python.venv_name in _result.stdout_lines

- name: copy wheel files
  unarchive:
    src: wheel.tar.gz
    dest: /tmp

- name: copy requirements file
  copy:
    src: requirements.txt
    dest: /tmp

- name: install virtualenv packages
  pip:
    requirements: /tmp/requirements.txt
    virtualenv: /opt/pyenv/versions/{{python.venv_name}}
    virtualenv_command: "/opt/pyenv/bin/pyenv virtualenv {{python.version}} {{python.venv_name}}"
    extra_args: "--only-binary=:all: --no-index --find-links=/tmp"
  register: _result

- name: pyenv rehash
  shell: /opt/pyenv/bin/pyenv rehash
  when: _result.changed
```

ややこしいとこだけ解説します。

### pyenv のインストール及びvirtualenv の元python のインストール
```yaml
- name: install pyenv
  unarchive:
    src: pyenv.tgz
    dest: /opt
    exclude: "{{python.version}}"

- name: install pyenv-virutalenv
  unarchive:
    src: pyenv-virtualenv.tgz
    dest: /opt/pyenv/plugins
```
これは、[pyenv](https://github.com/yyuu/pyenv.git) 及び[pyenv-virtualenv](https://github.com/yyuu/pyenv-virtualenv.git)のリポジトリクローンを圧縮したやつを配布してます。
exclude しているのはpyenv-virtualenv でvirtualenv 環境を作成する際の元になるpython バージョンのインストールスクリプトです。これは、後述するpython のローカルインストールを行う為のスクリプトと差し替えを行うために除外しています。(除外しないと冪等性が担保できなくなるので)
あ、pyenv のインストールディレクトリはなんとなく/opt としてますが、そこはお好みの場所でよいです。

```yaml
- name: copy python-build
  copy:
    src: "{{python.version}}"
    dest: /opt/pyenv/plugins/python-build/share/python-build/{{python.version}}
```
これが先程書いたローカルインストールを行うためのスクリプト差し替えです。python 3.7.3 だとオリジナルはこのようになっていますが、

```shell:3.7.3
#require_gcc
prefer_openssl11
export PYTHON_BUILD_CONFIGURE_WITH_OPENSSL=1
install_package "openssl-1.1.0j" "https://www.openssl.org/source/openssl-1.1.0j.tar.gz#31bec6c203ce1a8e93d5994f4ed304c63ccf07676118b6634edded12ad1b3246" mac_openssl --if has_broken_mac_openssl
install_package "readline-8.0" "https://ftpmirror.gnu.org/readline/readline-8.0.tar.gz#e339f51971478d369f8a053a330a190781acb9864cf4c541060f12078948e461" mac_readline --if has_broken_mac_readline
if has_tar_xz_support; then
  install_package "Python-3.7.3" "https://www.python.org/ftp/python/3.7.3/Python-3.7.3.tar.xz#da60b54064d4cfcd9c26576f6df2690e62085123826cff2e667e72a91952d318" ldflags_dirs standard verify_py37 copy_python_gdb ensurepip
else
  install_package "Python-3.7.3" "https://www.python.org/ftp/python/3.7.3/Python-3.7.3.tgz#d62e3015f2f89c970ac52343976b406694931742fbde2fed8d1ce8ebb4e1f8ff" ldflags_dirs standard verify_py37 copy_python_gdb ensurepip
fi
```
差し替え版はこのようにローカルから取得するようになっています。

```shell:3.7.3
#require_gcc
prefer_openssl11
export PYTHON_BUILD_CONFIGURE_WITH_OPENSSL=1
install_package "openssl-1.1.0j" "http://localhost/openssl-1.1.0j.tar.gz#31bec6c203ce1a8e93d5994f4ed304c63ccf07676118b6634edded12ad1b3246" mac_openssl --if has_broken_mac_openssl
install_package "readline-8.0" "http://localhost/readline-8.0.tar.gz#e339f51971478d369f8a053a330a190781acb9864cf4c541060f12078948e461" mac_readline --if has_broken_mac_readline
if has_tar_xz_support; then
  install_package "Python-3.7.3" "http://localhost/Python-3.7.3.tar.xz#da60b54064d4cfcd9c26576f6df2690e62085123826cff2e667e72a91952d318" ldflags_dirs standard verify_py37 copy_python_gdb ensurepip
else
  install_package "Python-3.7.3" "http://localhost/Python-3.7.3.tgz#d62e3015f2f89c970ac52343976b406694931742fbde2fed8d1ce8ebb4e1f8ff" ldflags_dirs standard verify_py37 copy_python_gdb ensurepip
fi
```
取得するファイルは

```yaml
- name: copy python-build-plugin
  unarchive:
    src: pyenv_local.tgz
    dest: /tmp
```
予め必要なファイルを用意しておいて、ターゲットホストに配布しておきます。

```yaml
- name: check installed python
  command: /bin/bash -l -c 'pyenv versions'
  register: _result
  changed_when: false
  ignore_errors: true

- name: python-{{python.version}} install
  command: /bin/bash -l -c 'python -m SimpleHTTPServer 80 & pyenv install {{python.version}}'
  args:
    chdir: /tmp
  when: python.version in _result.stdout_lines
```
**pyenv versions** コマンドでインストール対象バージョンのpython がインストールされているか確認し、未インストールの場合のみインストールするようにします。
インストールする際はSimpleHTTPServer を使用してローカルからhttpで資材を取得します。
ここまででpyenv 環境にvirtualenv の元になるpython のインストールが完了します。

### pyenv-virtualenv のインストール及びvirtualenv 環境下で使用するパッケージのインストール

```yaml
- name: check created virtualenv
  command: /bin/bash -l -c 'pyenv versions'
  register: _result
  changed_when: false
  ignore_errors: true

- name: create virtualenv
  command: /bin/bash -l -c 'pyenv virtualenv {{python.version}} {{python.venv_name}}'
  when: python.venv_name in _result.stdout_lines
```
**pyenv versions** コマンドで作成したいvirtualenv 環境が存在するか確認し、未作成の場合のみ作成するようにします。

```yaml
- name: copy wheel files
  unarchive:
    src: wheel.tar.gz
    dest: /tmp

- name: copy requirements file
  copy:
    src: requirements.txt
    dest: /tmp
```
wheel.tar.gz 及びrequirements.txt はvirtualenv 環境で必要なパッケージを纏めたものになります。
**pip freeze** で作成すると楽です。

```yaml
- name: install virtualenv packages
  pip:
    requirements: /tmp/requirements.txt
    virtualenv: /opt/pyenv/versions/{{python.venv_name}}
    virtualenv_command: "/opt/pyenv/bin/pyenv virtualenv {{python.version}} {{python.venv_name}}"
    extra_args: "--only-binary=:all: --no-index --find-links=/tmp"
  register: _result
```
上記したrequirements.txt からvirtualenv 環境にパッケージをインストールします。
Ansible の**pip モジュール**のvirtualenv 及びvirtualenv_command オプションを指定してインストールします。

### つまったところ
pip モジュールのvirtualenv, virtualenv_command の指定が一番引っかかりました。
[ドキュメント](https://docs.ansible.com/ansible/latest/modules/pip_module.html)にはpyenv-virtualenv の場合のExample が載ってなかったので…
あ、書き直すのがめんどくさかったのでそのままですが、資材配布先に/tmp を指定しているのはよろしくないです。/tmp がクリーンアップされたら冪等にならないので。