機械学習エンジニアがAnsible入門してみた

Deep Learningの開発環境を作ったり壊したりをもっと気軽にしたいので、Ansibleで環境構築を自動化してみました。

想像していたよりもずっと簡単だったので、アプリ系・機械学習系エンジニアに「IaC怖くないよ」と伝えたいのが本記事の趣旨です。Ansibleを既に使われてる方からしたら拙い内容ですがご容赦ください。

## TL;DR

- ホスト名の設定
- aptサーバーを日本サーバーに変更
- aptコマンドでインストール
- nvidia-driverのインストール
- docker-ce + nvidia-docker2のインストール
- dockerサービスの再起動

を下記のコマンドで自動化した。

```bash
# Ansibleのインストール。最新のインストール手順は公式ページを参照すること
# cf. https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html
sudo apt update
sudo apt install software-properties-common
sudo apt-add-repository --yes --update ppa:ansible/ansible
sudo apt install ansible

# Playbookの取得
# e.g.
curl -L https://raw.githubusercontent.com/chmod644/deeplearning-setup-playbook/d7db096cbd42741f68f2461c01280f138eee3c5a/playbook.yml -o playbook.yml

# ansible-playbookを実行
ansible-playbook playbook.yml -u ansible -i localhost, -c local --ask-become-pass
```

## 感想

使ってみた感想としては、**想像してたよりずっと学習コストが低い。**Ansibleや他のIaCツールも未経験の状態でスタートして、環境構築を自動化するとこまで1日で辿りつけてしまった。

今回はマシン1台の環境構築を自動化しただけなので、構成管理というには初歩も初歩だろうけど、本当に簡単で驚いてしまった。

Ansibleのメリットは色々な記事で説明されてるので今更だが、改めて実感したので一応書いておく。

- モジュールが豊富で、基本的な作業（apt, yum, wget, systemctlなどなど）はたいてい簡単に記述できる
- 構成情報を記述するplaybookはyaml形式なので、記述方法を覚える学習コストが低いもしくはゼロ
- QiitaやStack Overflow, GitHub issueにも情報が多いので調べやすい
- Ansibleで管理される側のノードには基本的にpythonだけあればよいので事前準備が楽（エージェントレス）

## 目標

開発マシンに対して以下の作業を自動化するのを目標とした。

- ホスト名の設定
- aptサーバーを日本サーバーに変更
- aptコマンドでインストール
- nvidia-driverのインストール
- docker-ce + nvidia-docker2のインストール
- dockerサービスの再起動

※社内環境で使う際にはDNS設定やドメイン参加のような初期設定が必要だが、今回は自宅マシンで検証したのでそのあたりは自動化対象にしてない。

なお、環境構築に使ったのは開発マシン1台とした。つまりControl Node（Ansibleの実行ノード）もManaged Node（Ansibleで管理されるノード）も同一の開発マシンである。

## 手順

ユーザが行う手順は以下の通り。

1. ansible本体のインストール
2. playbook.ymlをイントラ上のサーバーからダウンロード
3. ローカルホストを指定してansibleを実行

開発マシンのスペック：

- Ubuntu 18.04
- GeForce GTX 1060
- (Ansible 2.8 ※本手順でインストールする)



### 1. Ansible本体のインストール

[Ansibleのインストール手順](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html#latest-releases-via-apt-ubuntu) にしたがってインストールする。

### 2. playbookをダウンロード

開発マシンはGitすら入ってない想定。なのでplaybookはcurlやwgetで取得できるのが望ましいと思って1ファイルにまとめた。

```bash
curl -L https://raw.githubusercontent.com/chmod644/deeplearning-setup-playbook/d7db096cbd42741f68f2461c01280f138eee3c5a/playbook.yml -o playbook.yml
```

playbook の内容は後述する。


### 3. ローカルホストを指定して実行する

```bash
ansible-playbook playbook.yml -i localhost, -c local --ask-become-pass
```

コマンドライン引数について

- `-i localhost,` : Managed Nodeとしてlocalhostを指定している。カンマは必須なので注意。IPアドレスでもよい
- `-c local` : ssh接続せずlocalhostで、カレントユーザーまま実行する。`-u <ユーザー名>` や `remote_user` で指定されたユーザーは無視される。（Ansible標準動作ではssh接続するが、[Connection Plugins](https://docs.ansible.com/ansible/latest/plugins/connection.html)という機能でssh以外の接続もできる）
- `--ask-become-pass` : sudo権限で実行するためのパスワードを対話的に入力する。上記のように `-c local` が指定された場合は、カレントユーザーのパスワードを指定すればよい。

## Playbookの内容

playbookは[GitHubにもあげている](https://github.com/chmod644/deeplearning-setup-playbook/blob/fb521e434b80044e0e07fbaa819d9e0b97abb598/playbook.yml)。

拙い記述で恥ずかしいが、フィードバックしてもらえるのを期待して晒してみる。もっとよい書き方とか誤りがあればコメントいただきたい。

```yaml
- hosts: localhost
  # sudo権限でtasksを実行する。
  become: yes
  vars:
    # ディストリビューションの番号を 18.04 -> 1804 に置換
    ubuntu_dist_for_nvidia_cuda: "{{ ansible_distribution_version | regex_replace('\\.', '')}}"
  tasks:
    # --- ここからホスト名の設定 ---
    # ホスト名を標準入力から待ち受けている。
    - name: prompt hostname
      pause:
        prompt: "HOSTNAME [{{ ansible_fqdn }}]"
      register: hostname_input
    # ホスト名が入力されたらその値を、入力されなかったら
    # 初期ホスト名（ansible_fqdn）を変数hostnameにセットする
    - name: set default hostname
      set_fact:
        hostname: "{{ hostname_input.user_input | default(ansible_fqdn, true) }}"
    # 初期ホスト名とhostnameの内容が違う場合はホスト名を設定し直す。
    - name: set hostname
      hostname:
        name: "{{ hostname }}"
      when: hostname !=  ansible_fqdn
    # --- ここまでホスト名の設定 ---

    # aptリポジトリを日本サーバーに変更する。
    - name: set japanese apt server
      replace:
        path: /etc/apt/sources.list
        regexp: '(http:\/\/archive\.ubuntu\.com\/ubuntu\/|http:\/\/us\.archive\.ubuntu\.com\/ubuntu\/)'
        replace: 'http://jp.archive.ubuntu.com/ubuntu/'
    # 使いそうなパッケージを入れてる。
    - name: install apt packages
      apt:
        name: [git]
        state: present
        update_cache: yes

    # --- ここからNVidiaドライバのインストール ---
    - name: add nvidia-driver registory key
      apt_key:
        url: http://developer.download.nvidia.com/compute/cuda/repos/{{ ansible_distribution | lower }}{{ ubuntu_dist_for_nvidia_cuda }}/{{ ansible_architecture }}/7fa2af80.pub
        state: present
    - name: add nvidia-driver repository source
      apt_repository:
        repo: deb https://developer.download.nvidia.com/compute/cuda/repos/{{ ansible_distribution | lower }}{{ ubuntu_dist_for_nvidia_cuda }}/{{ ansible_architecture }} /
        state: present
        update_cache: no
        filename: nvidia-cuda
    - name: install nvidia-driver
      apt:
        name: [nvidia-418]
        state: present
        update_cache: yes
    # --- ここまでNVidiaドライバのインストール ---

    # --- ここからDocker+NVidia-Dockerのインストール ---
    # Dockerの依存パッケージをインストール
    - name: install docker dependencies
      apt:
        name: [apt-transport-https, ca-certificates, curl, gnupg-agent, software-properties-common]
        state: present
        update_cache: yes
    # Dockerのaptレジストリの認証キーを取得
    - name: add docker registory key
      apt_key:
        url: https://download.docker.com/linux/debian/gpg
        state: present
    # Dockerのaptレジストリを追加。/etc/apt/source.list.d/<filename>.listに追加される。
    - name: add docker repository source
      apt_repository:
        repo: deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable
        state: present
        update_cache: no
        filename: docker
    # nvidia-dockerも同じように認証キーとレジストリを追加する
    - name: add nvidia-docker repository key
      apt_key:
        url: https://nvidia.github.io/nvidia-docker/gpgkey
        state: present
    # { ansible_distribution | lower }}{{ ansible_distribution_version }} は ubuntu18.04とパースされる。
    - name: add nvidia-docker repository source
      get_url:
        url: https://nvidia.github.io/nvidia-docker/{{ ansible_distribution | lower }}{{ ansible_distribution_version }}/nvidia-docker.list
        dest: /etc/apt/sources.list.d/nvidia-docker.list
        owner: root
        group: root
        mode: 0644
    - name: install docker and nvidia-docker
      apt:
        name: ['docker-ce=5:18.09.6~3*', 'nvidia-docker2=2.0.3+docker18.09.6-3', docker-ce-cli, containerd.io]
        state: present
        update_cache: yes
      register: nvidia_docker_apt
    # Docker+NVidia-Dockerのバージョンを固定する
    - name: keep versions of docker and nvidia-docker
      dpkg_selections:
        name: "{{ item }}"
        selection: hold
      with_items: [docker-ce, nvidia-docker2]
    # --- ここまでDocker+NVidia-Dockerのインストール ---

    # DockerとNVidia-Dockerのインストールで変更があればdockerサービスを再起動する
    - name: reload docker configuration
      service: 
        name: docker
        state: reloaded
      when: nvidia_docker_apt.changed == True
    # カレントユーザーをdockerグループに追加
    - name: add user to docker group
      user:
        name: '{{ ansible_user }}'
        groups: docker
        append: yes
```

## 動作確認

Dockerコンテナでnvidia-smiを動かしてみる。

```console
$ docker run --runtime=nvidia --rm nvidia/cuda:9.0-base nvidia-smi
Tue Jun 18 18:30:59 2019       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 410.78       Driver Version: 410.78       CUDA Version: 10.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GTX 106...  Off  | 00000000:01:00.0 Off |                  N/A |
| 28%   40C    P8     6W / 120W |      0MiB /  6078MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+
```

## ハマったところ

- Connection pluginに `local` を指定するとremote_userの設定が無視されること。しばらく気づかなかった。
- replaceの正規表現の仕様は[Pythonのreライブラリ](https://docs.python.org/2/library/re.html)と同じらしい。
- たまに`apt`モジュールのところで `Failed to update apt cache ...` というエラーメッセージで落ちる。この現象は `ansible-playbook` の前に `sudo apt-get clean` を実行することで解消可能。`apt-get clean`相当のこともAnsibleで実行できるようにと[要望が出されている](https://github.com/ansible/ansible/issues/38920)がまだ実装されてない様子。
- Ansibleに直接関係ないが、nvidia-docker2のバージョンとdocker-ceのバージョンは揃える必要があるため、明確にバージョン指定してやらないといけない。
- パッケージの固定は `dpkg_selections`モジュールで行う。`apt`と違ってnameにリストが指定できないので注意。

## TODO

- [ ] Ansibleに詳しい人にレビューしてもらう
- [ ] CUDA/cuDNNのインストールを自動化 ※cuDNNのダウンロードにWeb画面上でのサインインが必要なため保留
- [ ] 社内環境でのドメイン参加・共通セットアップの自動化
- [ ] パッケージのバージョンを変数化する
- [ ] [Best Practices — Ansible Documentation](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html) に則ってリファクタする
- [ ] Ansibleの挙動確認
  - [ ] 対話的なコマンドの実行 （[expect – Executes a command and responds to prompts — Ansible Documentation](https://docs.ansible.com/ansible/latest/modules/expect_module.html)が使えるらしい）
  - [ ] Control NodeからManaged Nodesへのファイルコピー

## 参考
- [Installation Guide — Ansible Documentation](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html#latest-releases-via-apt-ubuntu)
- [[Ansible] AnsibleのPlaybookをlocalhostに対して実行する - Qiita](https://qiita.com/koara-local/items/db62c626568bc82f3bda)
- [Ansible Playbook to install nvidia-docker2](https://gist.github.com/tsuchm/bf61370ccb2fac639c811d77d0c61fa7)
- [Ansibleをはじめる人に。 - Qiita](https://qiita.com/t_nakayama0714/items/fe55ee56d6446f67113c)
- [Ansible 入門 - Qiita](https://qiita.com/ArimaRyunosuke/items/1f9d840311584d8160bc)
- [Get Docker CE for Ubuntu | Docker Documentation](https://docs.docker.com/install/linux/docker-ce/ubuntu/)
- [NVIDIA/nvidia-docker: Build and run Docker containers leveraging NVIDIA GPUs](https://github.com/NVIDIA/nvidia-docker)
- [Ansibleでパスワードをスマートに - Qiita](https://qiita.com/fukushi_yoshikazu/items/5e327103066ac80629f3)
- [apt-getの利用リポジトリを日本サーバーに変更する - Qiita](https://qiita.com/fkshom/items/53de3a9b9278cd524099)
- [ansible で prompt (入力待ち) の実現とその周辺テクニックのまとめ - Qiita](https://qiita.com/waterada/items/5f3e5d776c50f2aadec4)
- [NVIDIA/ansible-role-nvidia-docker](https://github.com/NVIDIA/ansible-role-nvidia-docker)
- [Support apt-mark hold · Issue #18889 · ansible/ansible](https://github.com/ansible/ansible/issues/18889)