UUUM攻殻機動隊

UUUMのエンジニアによる技術ブログです

Docker?Vagrant?個人PCのWebアプリケーション開発環境においての環境分離方法

nazoです。

個人PC上では、複数の開発環境が同居することは珍しくありません。その際に、複数のバージョンの同一のソフトウェアが混在することもあります。 それらを切り替える度にアンインストール・インストールを繰り返すのは非効率なので、同居させつつ自動的に切り替わるような状況が最適かと思われます。

これを実現するには様々な方法がありますが、一長一短あるため、具体的にどう違うのか見ていきたいと思います。特にmacOSを中心とした話になります。

Docker

Dockerはコンテナ型仮想化ツールで、同一環境内であたかも別の仮想環境を起動しているかのようにプロセス一式を分離させることができます。 DockerネイティブなOSを使っている場合は大体これでいいのですが、macOSやWindowsの場合はDockerは仮想マシンを通して利用することになり、TCPポートでの接続はともかく、ネイティブ同様の使用感を得るのはやや難しいです。 アプリケーション開発環境として考えると、Dockerの場合はホスト側からはコマンドをDocker経由で実行する必要があり、例えば go でエディタでの保存時に go fmt を自動で実行したい時や、フロントエンド開発でブラウザと連携したい時、gitのhookとの連動など、ホスト側のアプリケーションと連携する場合の相性はやや悪くなります。 また、環境の準備にフレームワークなどとの相性を考慮した様々なノウハウが必要になり、あまり手軽とは言えません。

VirtualBoxなどの仮想マシン(Vagrant)

最も確実に環境を分離できるものとしては、コンピューター毎仮想化してしまうことです。Vagrantを使用して、もう一つの仮想環境を作ってしまえば、確実に分離できるでしょう。 ただし、用途に対してオーバーヘッドがあまりに多く、また、Dockerで問題になるエディタ相性などは相変わらず良くないため、使い所が難しいです。環境一式が確実に起動し取扱も簡単なため、非開発者に環境を渡す時には比較的便利に利用することができます。 vimなどで仮想環境内でエディタを起動して操作する場合はVagrantだけで済むかもしれませんが、Windowsで使う場合以外では開発環境としては過剰な気がします。

direnv

そもそも私達が行いたいことはソフトウェアの複数バージョンの簡単な切り替えで、要求されているバージョン・設定のソフトウェアを同一PC内の複数プロジェクトで自在に取り扱えれば良いのです。そのためには、複数バージョンのソフトウェアをインストールしておいた上で、環境変数さえ分離できれば大体のことは分離することができます。 私は主に、direnv を使い、プロジェクト毎に環境を分離しています。好きなツールを使えばいいと思いますが、ディレクトリに入ると切り替わるというシンプルさや、設定ファイルをディレクトリ配下に置けばいいというわかりやすさ、言語に依存しないという点を重視して利用しています。

一部の言語の環境設定は最初から組み込まれていて、例えば、Python環境であれば、layout python と書くだけで、virtualenvを使用してプロジェクトディレクトリ配下にライブラリをインストールするようになります。use python [バージョン] と指定することで、pyenvでインストールしたPythonで layout python 相当の動作をするようになります。

direnv の設定ファイルの .envrc ファイルはbashスクリプトなので、普通に export 文を書いていけば自由に環境変数を上書きできます(ちなみにdirenvでは、 export PATH=...:$PATHPATH_add ... と書けるようになっています) ので、組み込みのlayoutが気に入らなければ、自由にパスを定義することもできます。OSのパッケージシステムで複数バージョンを入れている場合でも、基本的にはPATHをそこに向けるだけですぐ使えると思います。

拡張を個別に入れることができないPHPでは扱いづらさが残りますが( php-build のdefinitionsをプロジェクト毎に書いて切り替えるのがいいと思います)、大体の言語ではこれでバージョンやライブラリの環境毎の切り分けはでき、システム全体が汚染されることはなくなるのではないかと思います。

エディタをdirenv下で起動しないと、Docker同様の連携の悪さが残ってしまうのですが、例えば AtomVisual Studio Code では対応するものがあるようです。JetBrains製品はちょっとわかりかねますが、恐らく何らかの対応方法があるのではないかと思います。いずれにしても、環境変数を読み直すだけですので、Dockerほどの手間はかからないのではないかと思います。

direnvとDockerの複合

direnvで大体のケースでは済むものの、実際に複数バージョンのミドルウェアを個別にインストールしてもらうのは何かと大変です。また、ミドルウェアはエディタとの相性などを考える必要が基本的にはありません。 具体例を出すと、弊社ではElasticsearchを使用しているプロジェクトがあるのですが、Elasticsearchのインストールで時間がかかる・うまく動かないといった声が多く発生しました。

そのため、プログラミング言語以外のミドルウェアをDockerで起動し、プログラミング言語部分のみをdirenv環境下で動かす、とすると、プログラミング言語側は扱いやすく、それ以外はコマンド一発で動作するという両方のメリットが手に入ります。 ミドルウェアは基本的にポート番号によるTCP通信がほとんどなので、この方法で困ることがなく、アプリケーション側は柔軟に管理ができます。 構成がやや複雑になるというデメリットは残りますが、バランスの取れた状態と言えるかと思います。

なお、ミドルウェアだけであればVagrantで入れてもいいのですが、

  • 複数プロジェクトを同時に走らせると、仮想マシン分のオーバーヘッドがきつい
  • ほとんどのミドルウェアは開発環境ではデフォルト設定で足りる
  • ミドルウェアを途中から足すのが簡単

という点で、Dockerのほうが優れていると思います。

具体例:Railsの場合

docker-compose.yml は以下のようになります。

version: '3'
services:
  redis:
    image: redis
    ports:
      - 20001:6379
  mysql:
    image: mysql
    ports:
      - 20002:3306
    environment:
      MYSQL_ROOT_PASSWORD: "rootpassword"
      MYSQL_DATABASE: "hoge_dev"
    volumes:
      - ./docker/data/mysql:/var/lib/mysql

プロジェクトルートの docker フォルダ以下に docker でマウントする各種データを入れるようにします。

docker composeを含めたアプリケーション全体のプロセスは foreman で管理します。Procfile で以下のように書きます。

backend: docker-compose up
web: bundle exec rails s -p 20000
(他に必要なものがあれば以下に追加)

これで foreman start することで、docker compose一式も立ち上がり、アプリケーションも立ち上がります。foremanのプロセスを落とすと全部落ちるので、管理も簡単です。foremanクローンはいくつか存在するので、利用言語によって好きなものを利用すればいいと思います。 プロジェクトで使うポートの番号範囲を決めておけばポートが競合することもなく、プロジェクト毎でMySQLなどのバージョンが違っても簡単に取り扱うことができます。

まとめ

便利なものを便利なところにだけ使うという合せ技で、複数環境を快適に利用することができるようになりました。もちろん「全部Dockerでも何も困ってないよ!」というのであれば全部Dockerでいいと思いますし、仮想化を何も使わない選択肢も十分アリだと思います。技術自体を正しく理解し、チームや採用技術などに応じて柔軟に採用することが最も大切なのではないかと思います。