NGCコンテナをカスタマイズしてSingularity Image Fileを作る

# はじめに
コンピュータビジョンの研究に取り組む大学院生です。
研究に関連する実験は研究室のサーバーや産総研のサーバーABCIに接続して行っています。この時、Singularityを使って仮想環境を構築しています。Singularityとは？というお話は[こちらの記事](https://qiita.com/mkt3/items/b9f86f5ddf9eb0f43608)が詳しいのでこちらを参照していただければと思うのですが、簡単に言えば「管理者権限が無くても実行できるDockerのようなもの」というように私は認識しています。管理者権限を持っているローカルのマシンでSingularityイメージを作成して、これを管理者権限を持っていないサーバーにアップロードすることで、同じ仮想環境をサーバー上に用意することができるという感じです。

Singularityでは、DockerコンテナをSingularityイメージに変換することも可能で、今までは[NVIDIA GPU Cloud (NGC)で提供されているPyTorch用のコンテナ](https://ngc.nvidia.com/catalog/containers/nvidia:pytorch)を利用していたのですが、このコンテナではOpenCVが上手くインポートできない(依存しているパッケージがインストールされておらず、インストールには管理者権限が必要)ということが私の中で問題になり、このコンテナをベースにしてカスタマイズしたSingularityイメージを作成しようということになりました。

[NGCのユーザーガイド](https://www.nvidia.com/content/dam/en-zz/ja/Solutions/cloud/NGC-User-Guide_JA.pdf)では、Dockerfileを書くなどしてNGCコンテナをカスタマイズするやり方は載っていたのですが、Singularityで使うという今回の目的を満たすには、カスタマイズしたコンテナをDocker Hubにアップロードして、それをSingularityイメージに変換するというやや遠回り感のある工程になりそうだったため、Singularity定義ファイルを作ることでNGCコンテナをカスタマイズしたSIngularityイメージを作成するということを試みました。本記事はこの作業の備忘録になります。

## 本記事の流れ
* [1. VirtualboxとVagrantのインストール](#virtualboxとvagrantのインストール)
* [2. 仮想マシンの立ち上げとSingularityのインストール](#仮想マシンの立ち上げとsingularityのインストール)
* [3. 仮想マシンに割り当てるディスクを増やす](#仮想マシンに割り当てるディスクを増やす)
* [4. Singularity定義ファイルの作成](#singularity定義ファイルの作成)
* [5. Singularity Image Fileの作成](#singularity-image-fileの作成)

## 環境
* OS: macOS BigSur 11.2.1
* Homebrew 3.0.1

## Homebrewのインストール
Homebrewのインストールでは以前[こちらの記事](https://qiita.com/gen10nal/items/d16eeaa692871347e936#homebrew%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)で紹介しているのでこちらをご参照ください。

# VirtualboxとVagrantのインストール
MacでSingularityを動かすためにまずVirtualboxとVagrantを導入します。VirtualboxやVagrantとは？みたいな話は[こちらの記事](https://qiita.com/ozawan/items/160728f7c6b10c73b97e)が詳しいです。簡単に言うとこの2つで仮想マシンを構築します。

以下でしばらくhomebrew-caskの導入について書いてますが、homebrew-caskを新たに導入する必要がないことが判明したので、しばらく読み飛ばしてください。

~~Homebrewは導入済みだったのですが、CUIベースでないアプリケーションをインストールするにはHomebrew-caskを導入する必要があったので、まずHomebrew-caskを以下のコマンドで入れます。~~

```bash
$ brew install cask
```
~~するとCLT(CommandLineTools)が最新版じゃないと怒られてしまいました。~~

~~なので指示通りにCLTを最新版にします。~~

```bash
$ sudo rm -rf /Library/Developer/CommandLineTools
```
~~パスワードが聞かれるので入力します。その後、次のコマンド。~~

```bash
$ sudo xcode-select --install
```
~~これを実行すると突然GUIウィンドウが出てきて「インストールしますか？」みたいなことを聞かれるので「インストールする」を選ぶとインストールが始まります。~~

[Homebrew-caskのリポジトリ](https://github.com/Homebrew/homebrew-cask)によるとHomebrew-caskはHomebrewに統合されたみたいで新たにインストールする必要ないみたいです。
次のコマンドでbrewのバージョンを確認するとhomebrew-caskもちゃんと入っていることが確認できます。

```bash
$ brew -v
```
![brew_ver.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359175/fcae92d6-2619-b979-8a93-1db01eee38fd.png)
なので、brew caskでインストールしたい場合も`brew cask install`と明示的に書く必要はなく、普通に`brew install`でvitualboxやvagrantをインストールできます。

```bash
$ brew install virtualbox && \
  brew install vagrant && \
  brew install vagrant-manager
```
以下のコマンドを実行することで、これらがhomebrew-caskでインストールされていることを確認できます。

```bash
$ brew list --cask
```
このコマンドは、以前は`brew cask list`というコマンドだったようで、実行することでhomebrew-caskでインストールしたソフトウェアの一覧が表示されます。

virtulaboxとvagrantのインストールが終わったら、システム環境設定でこれらの使用を許可する必要があります。(参照：[Vagrant upでエラーが出て躓いた時の解決方法](https://qiita.com/nemui_/items/e6eb45210c173651b6ff))

# 仮想マシンの立ち上げとSingularityのインストール
今回はSingularity 3.5をインストールします(これはABCIで使えるSingularityのバージョンが3.5なので、これに合わせています)ので、[Singularity 3.5のドキュメント](https://sylabs.io/guides/3.5/admin-guide/installation.html#installation-on-windows-or-mac)に従って仮想マシンにSingularityをインストールします。

Vagrantfileという、仮想マシンの設定ファイルを保存するディレクトリを作ります。

```bash
~$ mkdir vm-singularity && \
   cd vm-singularity
```
次に、仮想マシンにSingularity 3.5をインストールします。

```bash
~/vm-singularity$ export VM=sylabs/singularity-3.5-ubuntu-bionic64 && \
                  vagrant init $VM
```
これでSingularityがインストールできたら仮想マシンを立ち上げます。
動作は確認していませんが、[後述](#仮想マシンの立ち上げとsingularityのインストール)のように、容量がデフォルトの20GBでは足りないことが明らかな時は仮想マシンを立ち上げる前に`Vagrantfile`を先に書き換えておくと、後々楽かもしれません(この辺りの話は情報提供いただければ幸いです)。

```bash
~/vm-singularity$ vagrant up && \
                  vagrant ssh
```
これでターミナルの表示が

```bash
vagrant@vagrant:~$ 
```
のようになり、仮想マシンを操作することができるようになります。
次のコマンドでSingularityが正しくインストールされているか確認することができます。

```bash
vagrant@vagrant:~$ singularity version
```
手元では`3.5.1`と表示されました。

# 仮想マシンに割り当てるディスクを増やす
デフォルトでは仮想マシンには20GBのディスクが割り当てられていましたが、これでは足りなかった(`singularity build`を実行した際に`no space left on device`というエラーが出ます)ので増設を試みます。
ディスクサイズは`df -h`コマンドで確認できます。
![スクリーンショット 2021-02-23 1.21.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359175/54f3fd59-8498-6e65-643d-6177da7977c2.png)
`/`にマウントしている`/dev/mapper/vagrant--vg-root`のSizeが19Gとなっています。今回はこれを増量します。
まずは先程作ったディレクトリ`~/vm-singularity`の直下にある`Vagrantfile`を編集します。
仮想マシンに入ってしまっている場合は一度出て、仮想マシンを停止します。

```bash
vagrant@vagrant:~$ exit
~/vm-singularity$ vagrant halt
```
次に`Vagrantfile`の編集によってディスク容量を増やせるようにする、`vagrant-disksize`というプラグインをインストールします(参照：[Vagrantfileに一行書くだけでVMのディスク容量を増やす方法](https://qiita.com/yut_h1979/items/c84c490053877beee5c1))。

```bash
~/vm-singularity$ vagrant plugin install vagrant-disksize
```
ここから`Vagrantfile`を編集します。最初に`vagrant up`をする前に`Vagrantfile`の編集をしておくと、`Vagrantfile`の編集以降の作業は省略できる可能性があります(確認はしていません)。

```bash
~/vm-singularity$ vim Vagrantfile
```
次の1行を加えます。

```vim:Vagrantfile
Vagrant.configure("2") do |config|
    config.vm.box = "sylabs/singularity-3.5-ubuntu-bionic64"
    config.disksize.size = '30GB' # <=この行を新たに追加する。 
```
ここでは30GBとしていますが、今回用いるNGCコンテナがそれなりに大きいファイルであり、実際にやってみると30GBでも足りず、再び増量することになってしまい二度手間だったので最初から50GBにしておくと良かったです(40GBでもやってみましたがそれでも足りず、50GBにしたら最後までできるようになりました)。
ただし、このやり方はディスクを増やすことはできても減らすことはできないようなので、n度手間でも少しずつ増やしていくのが良いのかもしれません。
次に仮想マシンを再び立ち上げ接続します。立ち上げ時のメッセージで

```
==> default: Resized disk: old 20480 MB, req 30720 MB new 30720 MB
==> default: You may need to resize the filesystem from within the guest.
```
という表示があり、`Vagrantfile`の内容が反映されていることが確認できます。
しかし、このままではまだ`/dev/mapper/vagrant--vg-root`に割り当てられたディスクの容量は増えていません。`df -h`コマンドで確認してみると19Gとなったままでした。
そこで[こちらの記事](https://wiki.r371.com/index.php/Vagrant%E3%81%AE%E3%83%87%E3%82%A3%E3%82%B9%E3%82%AF%E5%AE%B9%E9%87%8F%E3%81%AE%E5%A2%97%E3%82%84%E3%81%97%E6%96%B9)を参考に、以下の操作を行いました。
まず次のコマンドでディスクの状態を確認します。

```bash
vagrant@vagrant:~$ sudo fdisk -l
```
![スクリーンショット 2021-02-23 1.37.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359175/554c28fe-b271-5c62-9e9f-8c0ecdadeedc.png)
ディスク`/dev/sda`には30GB割り当てられているものの`/dev/mapper/vagrant--vg-root`には19GBしか割り当てられていないことが確認できます。
またDeviceは`/dev/sda1`の一つだけであることも確認できます。このDeviceのId`8e`は次のステップで必要になるのでデバイスの状態は予め確認しておきましょう。
次にパーティションを作成します。

```bash
vagrant@vagrant:~$ sudo fdisk /dev/sda
```
コマンド実行後に表示される画面の指示に従って次のようにコマンドを打っていきます。

```
n # パーティション作成
p # primaryを選択
2 # パーティション番号(Deviceが1つだけなのでEnterを押してもデフォルトで2が選択されます)
Enter # パーティション開始位置(デフォルトのままいきます)
Enter # パーティション終了位置(デフォルトのままいきます)
t # タイプ変更
2 # パーティション番号
8e # 拡張したいパーティションと同じになるように、先程fdiskで確認したIdの文字列
w # 保存して終了
```
実際にこれらを行った画面は以下のようになります。
![スクリーンショット 2021-02-23 1.37.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359175/e89add52-35b9-ee75-740d-e197d320c03c.png)
ディスクの状態を確認します。

```bash
vagrant@vagrant:~$ sudo fdisk -l
```
![スクリーンショット 2021-02-23 1.37.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359175/9aa10643-8693-e991-3f86-e076b4b00bc1.png)
`/dev/sda2`が新たに加わっていることが確認できたら、一度仮想マシンを再起動し、再び接続します。

```bash
vagrant@vagrant:~$ exit
~/vm-singularity$ vagrant reload
~/vm-singularity$ vagrant ssh
```
次に先程作成したパーティション`/dev/sda2`の物理ボリュームを作成します。

```bash
vagrant@vagrant:~$ sudo pvcreate /dev/sda2
```
![スクリーンショット 2021-02-23 1.40.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359175/955fd61e-b48c-dac2-c2ff-3eed063a0713.png)
物理ボリュームとは？みたいな話は[こちらの記事](https://www.atmarkit.co.jp/flinux/rensai/linuxtips/a065lvminfo.html)が参考になりましたので興味のある方は覗いてみてください。
次はボリュームグループに新しいパーティションを追加します。
この際にボリュームグループの名前が必要なので次のコマンドで確認します。

```bash
vagrant@vagrant:~$ sudo vgdisplay
```
![スクリーンショット 2021-02-23 1.47.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359175/f246ed12-1ff7-79b9-f4c8-8795f18dffbe.png)
するとVG Nameは`vagrant-vg`となっていることが確認できましたので、次のコマンドでこのボリュームグループに新しいパーティションを追加します。

```bash
vagrant@vagrant:~$ sudo vgextend vagrant-vg /dev/sda2
```
次はディスクを拡張します。拡張したいディスクは`/dev/mapper/vagrant--vg-root`であったので次のようにコマンドします。

```bash
vagrant@vagrant:~$ sudo lvextend -l +100%FREE /dev/mapper/vagrant--vg-root
```
![スクリーンショット 2021-02-23 1.49.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359175/b799ab8a-d245-fef8-6bf9-7ee2c3b08700.png)
そして最後にファイルシステムを拡張します。ファイルシステムがext3もしくはext4の場合は`resize2fs`コマンドでできますが、そうでない場合は別のコマンドを使う必要があるようです。ファイルシステムは次のコマンドで確認できます。

```bash
vagrant@vagrant:~$ mount
```
一覧がたくさん出てきますが、`/dev/mapper/vagrant--vg-root`を探すと以下のように書いてありました。

```
/dev/mapper/vagrant--vg-root on / type ext4 (rw,relatime,errors=remount-ro,data=ordered)
```
ファイルシステムがext4であることが確認できたので、こちらも先程と同様に`/dev/mapper/vagrant--vg-root`を指定して、次のコマンドを実行します。

```bash
vagrant@vagrant:~$ sudo resize2fs /dev/mapper/vagrant--vg-root
```
![スクリーンショット 2021-02-23 1.53.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359175/217f6323-8414-7503-f75f-5170ca14b260.png)
これで仮想マシンに割り当てるディスクの増量は完了しました。`df -h`コマンドで、`/dev/mapper/vagrant--vg-root`の容量が30GBに増えていることが確認できました。
![スクリーンショット 2021-02-23 1.53.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359175/00e211f9-c318-258f-dc8f-46a5b06362f8.png)


# Singularity定義ファイルの作成
定義ファイルを編集するためにまず仮想マシン内にvimをインストールします。

```bash
vagrant@vagrant:~$ sudo apt update
vagrant@vagrant:~$ sudo apt install vim
```
これで仮想マシン内でvimを使って定義ファイルを編集できるようになりました。
定義ファイルでは色々と設定することが可能ですが、ここではNGCコンテナでOpenCVを正しくインポートできるようにすることが目標なので、シンプルな定義ファイルを作成します。その他様々なオプションなどは[Singularityのドキュメント](https://sylabs.io/guides/3.5/user-guide/definition_files.html#definition-files)を参照してください。
Singularity定義ファイルを次のように編集します。

```vim:my_pytorch.def
Bootstrap: docker
From: nvcr.io/nvidia/pytorch:20.12-py3

%post
    apt update
    apt -y upgrade

    # opencvが依存しているパッケージのインストール
    apt install -y libgl1-mesa-dev

    # singularity image fileのサイズをなるべく小さくするために余計なファイルを削除
    apt autoremove -y
    apt clean
    rm -rf /usr/local/src/*
    rm -rf /var/lib/apt/lists/*

    # pipのパスが通っていなかったため絶対パスでpipコマンドを使う
    /opt/conda/bin/pip install --no-cache-dir opencv-python
```
`From`の部分でベースにするNGCコンテナを指定しています。ここでは[NGCのPyTorchコンテナ](https://ngc.nvidia.com/catalog/containers/nvidia:pytorch)で最新のバージョンである`pytorch:20.12.-py3`を選択しています。
また、余計なファイルを削除する部分は[こちらの記事](https://nykergoto.hatenablog.jp/entry/2020/07/25/%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%81%AAdockerfile%E3%82%92%E6%9B%B8%E3%81%8F%E3%81%A8%E3%81%8D%E3%81%AB%E6%B0%97%E3%82%92%E3%81%A4%E3%81%91%E3%81%A8%E3%81%8F%E3%81%A8%E8%89%AF%E3%81%84%E3%81%93)を参考にしています。

# Singularity Image Fileの作成
ようやくここまで来ました。
先程作ったSingularity定義ファイルをビルドしてSingularity Image Fileを作成します。

```bash
vagrant@vagrant:~$ sudo singularity build my_pytorch.sif my_pytorch.def
```
ビルドには結構時間がかかります。実行が終わるとカレントディレクトリに`my_pytorch.sif`ができています。
仮想マシン内の`/vagrant`は仮想マシンを作成したディレクトリ`vm-singularity`をマウントしていますので、次のようにすることで、Singularity Image Fileを仮想マシンの外に移すことができます。

```bash
vagrant@vagrant:~$ mv my_pytorch.sif /vagrant/my_pytorch.sif
```
すると`~/vm-singularity`の直下に`my_pytorch.sif`が移動しています。
これを使用したいサーバーに`scp`コマンドなどで送るとサーバーでこの仮想環境を使用できるようになります。

実際にサーバーにこの`my_pytorch.sif`を送り、ちゃんとOpenCVがインポートできるようになったことが確認できました。