Linux
Vagrant
Docker
systemd
vagrant-docker

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

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

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

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

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


TL;DR


  • Vagrant に vagrant-docker というそのものズバリな provider がある!

  • Linux as a container; Docker の哲学に真っ向から喧嘩を売っていくスタイル

  • PID 1 となる systemd の建て方が難しい: 特権 CAP_SYS_ADMIN/sys/fs/cgroup の扱いがミソ


とりあえず建ててみる

作戦の根幹は、



  • /sbin/init の他は sshd が動いている程度の、 Linux のシステムとしてはミニマムな状態のコンテナを作れる Dockerfile を用意

  • Vagrant を介して管理

  • あとは provisioner を使って煮るなり焼くなり

となります。


ということで、動く例

こちらを参考にしつつ、 sshd の起動を systemd で行うようにしてみました。


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" ]



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 を明示的に指定します。

$ vagrant up --provider docker


所々の謎記述について


Dockerfile


dbus 要らなくね?

事実上必須です。

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


pythonapt-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 でこのような設定にすることと等価です。


細かな点


気になるネットワークまわりは…

残念ながら、 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 にできる?

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

==> 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 ってあったような…

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

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