systemd-nspawnで外部からssh可能でnvidia GPUを使えるコンテナを作る

# 前書き
CUDAのバージョンが違う環境を複数用意して使い分けたい場合などにコンテナは便利である。DockerやLXCなど有名なものが幾つかあるが、最も軽量だと思われる `systemd-nspawn` を用いて、ホストOSとゲストOSが両方共Ubuntuである場合(両方DebianでもたぶんOK)にコンテナを作りゲストOSの使用メモリ量を制限する手順を解説する。デフォルトでホストとゲストがネットワークインターフェースを共有することに気づいていなかったり、無線LANインターフェースでMACVLANを使えないことを知らなかったため丸一日調べるのに時間がかかった。MACVLANなどでネットワークインターフェースをホストOSとゲストOSで分ける手順は最後の節で述べる。以下の作業はすべてroot権限で実行してください。コンテナ名は`container1`にしてあるので適当に置き換えてください。

# ホストとゲスト
コンテナ内で動作しているほうがゲストで、ハードウェアの上で直接動作しているほうがホストです。

# コンテナ環境の設定

0. (もしbtrfsを使っているなら `btrfs subvolume create /var/lib/machines/container1`)
1. `apt-get install debootstrap`
2. `debootstrap  --arch amd64 --variant=minbase --include=ubuntu-minimal,openssh-server,language-pack-en-base,language-pack-ja-base bionic /var/lib/machines/container1 http://jp.archive.ubuntu.com/ubuntu` この作業で `/var/lib/machines/container1` に最小限のUbuntu 18.04とopensshサーバーsshdがインストールされる。`bionic`の部分はホストOSと揃えたほうが無難でしょう。
3. `cp /etc/apt/sources.list /var/lib/machines/container1/etc/apt` 
4. `vi /var/lib/machines/container1/etc/ssh/sshd_config` で`Port 26000`, `PermitRootLogin yes` にする。ネットワークインターフェースがホストとゲストで共有されているから、ホストで動作する`sshd`とゲストで動作する`sshd`のポートを変えないと同時に実行できない。
5. `systemd-nspawn -M container1` をホストで実行するとゲストOSのシェルプロンプトが出るから、`passwd` コマンドで適当なルートパスワードを設定する。
6. `systemd-nspawn -b -M container1 --bind=/dev/nvidia0 --bind=/dev/nvidiactl --bind=/dev/nvidia-uvm --bind=/dev/nvidia-uvm-tools --bind=/dev/nvidia-modeset` でゲストOSを起動する。`--bind`は指定したホストOSのファイルをゲストにもアクセスさせるオプションである。また`/etc/systemd/system/systemd-nspawn@.service`の中の`DeviceAllow`の行に上記のデバイスを追加しておく。
7. ゲストOSの中で`apt-get --no-install-recommends install software-properties-common; add-apt-repository ppa:graphics-drivers/ppa; apt-get update; apt-get dist-upgrade; apt-get --no-install-recommends install nvidia-utils-???`を実行する。`???`の部分にはホストOSに入っているNVIDIAドライバのバージョン(例えば410)を入れる。これでゲストOSの中で`nvidia-smi`を使用できる。
8. 他の計算機からゲストOSに`ssh`でアクセスするには`ssh -l root -p 26000 ホストOSのホスト名`でできる。ゲストOSの終了はログインして`poweroff`または`shutdown -h now`でできる。
9. ゲストOSの`/etc/hostname` の設定をやっておいたほうが便利である
10. `/home`にコンテナ内からアクセスさせたい場合は `--bind=/home` をつければよい

# ゲストOSのメモリ制限と自動起動

ホストOSの起動時にゲストOSも起動し、ゲストOSの占有実メモリ量を制限するためには以下のようにする。
1. `cp /lib/systemd/system/systemd-nspawn@.service /etc/systemd/system/systemd-nspawn@container1.service` コピー先ファイル名の＠の後にコンテナ名を入れてください
2. コピー先ファイルを編集して`ExecStart`のところから`--network-veth -U`を削除し、`--settings=false`に変更し、`--bind=/dev/nvidia0 --bind=/dev/nvidiactl --bind=/dev/nvidia-uvm --bind=/dev/nvidia-uvm-tools`を追加する。
3. '[Service]'欄のどこかに以下を追加
`DeviceAllow=/dev/nvidia0 rwm
DeviceAllow=/dev/nvidiactl rwm
DeviceAllow=/dev/nvidia-uvm rwm
DeviceAllow=/dev/nvidia-uvm-tools rwm
DeviceAllow=/dev/nvidia-modeset rwm`
4. '[Service]'欄に `Delegate=memory pids cpu io`を追加し、`MemoryHigh=3G`を追加。これでホストOSが使用する実メモリはおおよそ3GBに制限される。[MemoryHighの他にもいろいろな制限をホストOSに課すことができる](https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html#MemoryHigh=bytes)。`MemoryHigh`の制限機能を有効にするには[cgroup v1をsystemdにマウントさせない下準備](https://qiita.com/kakinaguru_zo/items/cb18bd8f7aa92c11f6bf#cgroup-v1%E3%82%92systemd%E3%81%AB%E3%83%9E%E3%82%A6%E3%83%B3%E3%83%88%E3%81%95%E3%81%9B%E3%81%AA%E3%81%84%E4%B8%8B%E6%BA%96%E5%82%99%E3%81%93%E3%81%AE%E6%89%8B%E9%A0%86%E3%81%AF%E5%BF%85%E9%A0%88)を予め実施してホストOSを起動しなおしておく必要がある。
5. ここまで行ったあとに`systemctl start systemd-nspawn@container1`でメモリ占有を制限したゲストOSを開始することが可能で、ホストOS起動時にゲストOSも自動で起動するには`systemctl enable systemd-nspawn@container1`とする。
6. ゲストOSのナイス値を低いままにするには`systemd-nspawn`の起動オプションに`--drop-capability=CAP_SYS_NICE`を追加した上で、`[Service]`欄に
`Nice=19
RestrictRealtime=true`
を追加する。
7. `--drop-capability` の引数には`CAP_MKNOD,CAP_SYS_TIME,CAP_SYS_BOOT`もあってもよいだろう

# 独立したIPアドレスをゲストOSに与える方法

## `systemd-nspawn --network-macvlan=インターフェース名` を使う方法
インターフェース名には`enp0s25`などの有線イーサネットを指定する。無線を指定すると動かない。ゲストOSの中に以下のファイルを作ればDHCPで自動的にIPアドレスをゲストOSが取得する。ゲストOSとホストOSの通信はなぜかできない。[これは設定ミスではなくてそういうものらしいです](https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/7/html/virtualization_deployment_and_administration_guide/sect-troubleshooting-common_libvirt_errors_and_troubleshooting#sect-Guest_can_reach_outside_network_but_cannot_reach_host_when_using_macvtap_interface)。

```yaml:/etc/netplan/01-macvlan.yaml
network:
  version: 2
  renderer: networkd
  ethernets:
    mac-vlan1:
      match:
        name: mv-*
      dhcp4: true
      dhcp6: true
```
このときホストとゲストの間の通信ができない問題は、ホスト側もMACVLAN経由でインターネット接続すればよい。この設定はUbuntu標準の`netplan`ではできないようだが、`systemd-networkd` を用いた設定は「[仮想環境のネットワーク接続をMACVLANとMACVTAPに切り替えてみた](http://kesaranpa.hatenablog.com/entry/2018/03/17/203309)」に説明がある

## `systemd-nspawn -n` を使う方法

* UbuntuではホストOS側で`systemd-networkd`が標準では起動されていないから、ゲストOSの起動前に`systemctl start systemd-networkd`などで起動しておく必要がある
* `10.0.0.6` のようなIPアドレスが割り振られるから、組織内の別の計算機からアクセスできない場合も多そう

## `systemd-nspawn --network-ipvlan=インターフェース名` を使う方法
* インターフェースは無線でも構わない。DHCPクライアントをゲストOS内で起動してIPアドレスを取得する方法がわからないので、例えば以下のファイルをゲストOSに置いて固定IPアドレスを割り当てる。DHCPでIPアドレスを貰えない一因としてMACアドレスをホストOSとゲストOSが共有していることがあるだろう。

```yaml:/etc/netplan/01-ipvlan.yaml
network:
  version: 2
  renderer: networkd
  ethernets:
    ip-vlan1:
      match:
        name: iv-*
      dhcp4: false
      dhcp6: false
      addresses: [192.168.1.150/24]
      gateway4: 192.168.1.1
      nameservers:
        addresses: [192.168.1.1]
```
* 上記の設定でゲストOSから例えば`apt-get update`などをできるが、ホストOSからゲストOSのIPアドレスに`ssh`してもつながらない