Vagrant ベースのレガシーなプロジェクトをそのまま Docker で動かす

弊社の開発プロジェクトのうち、比較的新しめなものについては、開発環境が最初から Docker ベースになってきています。

ところが、それより古いプロジェクトだと、「Vagrant で起動した Linux 仮想環境に Ansible で色々インストールして使う」スタイルのものが多いです。

「Docker だったらオーバーヘッドほぼゼロなのに…」(Linux), 「Docker だったら HyperKit 上で動作する Linux カーネルが常駐してるから起動が速いのに…」(macOS)と、今のご時世ならこう思うところですが、かと言って今になってから開発環境を Docker ベースに切り替えるのもなかなか(主に工数的な面で)難しい。

そこで、**開発環境の大幅な構成変更をすることなく**(これ大事)、 Docker の効率的な処理系の恩恵だけつまみ食いできるか、試してみました。

# TL;DR

* Vagrant に [vagrant-docker](https://www.vagrantup.com/docs/docker/) というそのものズバリな provider がある!
* Linux as a container; Docker の哲学に真っ向から喧嘩を売っていくスタイル
* PID 1 となる systemd の建て方が難しい: 特権 `CAP_SYS_ADMIN` と `/sys/fs/cgroup` の扱いがミソ

# とりあえず建ててみる

作戦の根幹は、

* `/sbin/init` の他は `sshd` が動いている程度の、 Linux のシステムとしてはミニマムな状態のコンテナを作れる Dockerfile を用意
* Vagrant を介して管理
* あとは provisioner を使って煮るなり焼くなり

となります。

## ということで、動く例

[こちら](https://i-beam.org/2017/07/01/vagrant-docker-provider/)を参考にしつつ、 sshd の起動を systemd で行うようにしてみました。

```dockerfile:Dockerfile
FROM ubuntu:18.04

RUN \
  export DEBIAN_FRONTEND=noninteractive && \
  apt-get update && apt-get -y upgrade && \
  apt-get install -y --no-install-recommends \
    dbus \
    systemd-sysv \
    openssh-server \
    sudo && \
  useradd -m -s /bin/bash -G adm,sudo vagrant && \
  echo -n vagrant:vagrant | chpasswd && \
  echo 'vagrant ALL = (ALL) NOPASSWD: ALL' >/etc/sudoers.d/vagrant && \
  chmod 0440 /etc/sudoers.d/vagrant

#VOLUME [ "/sys/fs/cgroup" ]
CMD [ "/sbin/init", "--show-status" ]
```

```ruby:Vagrantfile
Vagrant.configure("2") do |config|
  # Every Vagrant development environment requires a box. You can search for
  # boxes at https://vagrantcloud.com/search.
  # NB: no use for vagrant-docker
  #config.vm.box = "base"

  # Provider-specific configuration so you can fine-tune various
  # backing providers for Vagrant. These expose provider-specific options.

  config.vm.provider 'docker' do |docker, override|
    # https://www.vagrantup.com/docs/docker/configuration.html
    docker.build_dir = '.'
    docker.has_ssh = true
    override.ssh.username = 'vagrant'
    override.ssh.password = 'vagrant'
#    docker.cmd = [ '/bin/sh', '-c', 'mkdir /run/sshd && exec /usr/sbin/sshd -D' ]
    docker.create_args = %w[
      --cap-add SYS_ADMIN
      --tmpfs /sys/fs/cgroup:rw
    ]
    override.vm.synced_folder '.', '/vagrant', docker_consistency: 'cached'
  end
end
```

以上を用意した上で、 `vagrant up` するわけですが、 `config.vm.provider` を複数記述している場合は、 `--provider` オプションを使って docker を明示的に指定します。

```shell-session
$ vagrant up --provider docker
```

## 所々の謎記述について
### Dockerfile
#### `dbus` 要らなくね?

事実上必須です。
`systemctl` コマンドが `systemd` との通信に dbus を使っているらしく、これが入っていないと `systemctl` を使った様々なトリックが使えません。

#### `python` は `apt-get install` しなくていいの?

provisioner に Ansible を使っている場合は、入れておくと便利かも。

### Vagrantfile
#### `config.vm.box` は設定しなくていいの?

結論から言うと、設定不要あるいは逆にあると邪魔、でした。
うまく使えば `Vagrantfile` の記述共通化に使えるかもしれません。

#### `docker.create_args = %w[ --cap-add SYS_ADMIN --tmpfs /sys/fs/cgroup:rw ]`

systemd は、デーモンやら何やらのリソース管理のために cgroups と言う仕組みを使いますが、そのためには `/sys/fs/cgroup` ディレクトリ以下に cgroup ファイルシステムをマウントできるだけの特権が必要です。

ところが、 Docker は*デフォルトで*特権を放棄した状態でコンテナ内のプロセスを立ち上げるため、立ち上げられた systemd は身動きが取れなくなってしまいます。

cgroup ファイルシステムを `/sys/fs/cgroup` にマウントするには、 `CAP_SYS_ADMIN` 特権があれば十分なので、　`docker create` のオプション `--cap-add` で与えています。

他に、マウントポイント用の tmpfs も必要なので、同様に `--tmpfs` オプションで用意してやります。

#### `override.vm.synced_folder '.', '/vagrant', docker_consistency: 'cached'`

素の Docker for macOS でもあった、ホスト側のディレクトリをボリュームとしてコンテナ内にマウントすると遅い問題を回避する設定です。 vagrant-docker の synced_folder も同じメカニズムを使って実現されているので、回避策も似ています。

素の Docker for macOS で[このような設定](https://qiita.com/koshigoe/items/52749db8836b4e3fbfc4#%E5%AF%BE%E7%AD%96)にすることと等価です。

# 細かな点
## 気になるネットワークまわりは…

残念ながら、 Vagrant の枠組みだけだと、実質的に `forwarded_port` しか使い物になりませんでした。

`host_ip:` 指定付きの `forwarded_port` をうまく使って `private_network` に似た構成を取ることや、 `config.vm.provider 'docker' { |docker| docker.link }` を使ってコンテナ間ネットワークを構成することは可能なので、凝った構成でなければどうにかできるとは思います。

OK: `config.vm.network 'forwarded_port'` (`host_ip:` なし・ありどちらも)
NG: `config.vm.network 'private_network'` (エラーにはならない)
NG: `config.vm.network 'public_network'` (エラーにはならない)

## synced_folder を NFS にできる?

残念ながらエラーとなりました。

```console
==> default: Machine booted and ready!
No host IP was given to the Vagrant core NFS helper. This is
an internal error that should be reported as a bug.
```

そもそも、 NFS にする動機が「vboxsf が遅すぎるのを何とかしたい」というものだったはずなので、母艦が Linux ならむしろやらない方がいいです。母艦が macOS の場合は、「osxfs と nfs どっちが速いの?」という話になると思います。

## xxxx したら何が起こる? シリーズ
### `vagrant halt` したら何が起こる?

Docker 的には、コンテナが stop 状態になります。
後で `vagrant up` すると、コンテナの内容が保たれた状態で起動します。

### `vagrant reload` したら何が起こる?

コンテナが破棄されてビルドから作り直しになります。
素の Docker と同様、キャッシュが残っていれば活用されて素早く起動します。

なお、 provisioner は、 reload のたびに毎回実行されます。
provisioner を実行すると言うことは、とどのつまりコンテナ起動後にコンテナ内部へ変更を加えることなので、当然コンテナが破棄されたら失われますから、 vagrant-docker 側で配慮してくれるのはありがたいと言えなくもないのですが、 provisioner の処理が多いと辛いところ。

特に、 `vagrant reload` を仮想マシン再起動のイディオムとして使っている場合は悲しいことになるので、多少面倒でも `vagrant halt && vagrant up` とするといいと思います。

### コンテナの中で `shutdown -h` or `shutdown -r` したら何が起こる?

Docker 的には、コンテナは running のままですが、 init 以外のプロセスが sshd を含めて全部終了した状態になります。
この状態だと、 `vagrant halt && vagrant up` で復帰させるか、 `docker exec` でコンテナ内に別のプロセスを立ち上げてあれこれすることが可能です。

ごめんなさい、あんまりいいユースケースが思いつきませんでした…

## [vagrant-lxc](https://github.com/fgrehm/vagrant-lxc) ってあったような…

macOS のことを考えなくていいなら、同じ基礎技術 cgroups を使う `vagrant-lxc` provider も選択肢になると思います。

一時期開発が停滞していましたが、久しぶりに github を見たところ、また開発が再開しているようです。