チーム開発の開発環境として Docker + Vagrant を選択し続ける理由

Docker Advent Calendar 2016 の 25 日目です.
Docker アドベントカレンダーとして書いているはずだったんですが、推敲と校正を重ねているうちに Docker というよりは VM とか開発環境とかの話が色濃くなってしまい、主役のツールが Vagrant になってしまいました. 謹んでお詫び申し上げます.

僕が所属する会社の事業の一つに Web/モバイルアプリの SI + 運用があり、その際の Web/API サーバー開発は macOS + Vagrant + VirtualBox (CoreOS) + Docker を社内標準のローカル開発環境(以下、開発環境)としています.

勉強会後の懇親会やコンサルティングを提供する場で良く質問されることの一つに「Docker for Mac + Docker Compose だけでやらないのはなぜ? VM を一枚噛ませているのはなんで?」というものがあります. この質問は至極真っ当で、VM を一枚噛ませることによるホストマシンのリソース消費は無視できるような量ではないですし、さぁ開発しようと思ったときにサーバーが立ち上がるまでの時間も素の Docker 利用に比べて長くなります. 巷では「Docker for Mac のホストディレクトリマウントの I/O パフォーマンスが悪すぎて使ってられん」という悩みを見かけることが多い気がしますが、僕たちが Vagrant を使って Docker の環境に一枚 VM のラップを被せるのは他のところに主な理由があります. 本記事では、チーム開発におけるツール・スタック選択についての考え方の一つを紹介できればと思います.

(ちなみに Docker for Mac が特定の状況でパフォーマンスが悪くなるのはもちろん問題だと思いますが、実際のところチーム開発用の Docker イメージをプロトタイピングするときや one-off な docker run で各種 CLI ツールを使うとき、パフォーマンスが要求されない個人的な開発などの場合にはかなりの割合で Docker for Mac を利用しています)

チーム開発のための開発環境の原則を考える

冒頭の質問「なぜ VM?」への回答の前に、まずはチーム開発に用いる開発環境を設計/構築する際に、これまでの経験から僕が原則としていることを紹介します.

  • 開発環境構築は全てが自動化されており、インフラやサーバーに明るくないエンジニアでも自分で環境が用意できること
  • エンジニアが自身の端末で開発環境を利用するために知るべき最低限のインターフェース(=コマンドなど)が、どのプロジェクトの開発チームに参加したとしても統一されていること
  • 上記インターフェースを保ったまま、日々進化するプロビジョニングツールや世間の環境構築のベストプラクティスを社内の開発環境にインクリメンタルに取り込んでいけること
  • 上記インターフェースを保ったまま、アプリケーションの進化に合わせて開発環境内のミドルウェアやサーバー構成を柔軟に変更していけること

これらの原則は全て、アプリケーション/インフラ全ての開発をインクリメンタルに回し続けていくために必要だと考えているものです.

つまり、「なぜ VM?」への回答を一言でまとめると「アプリケーション/インフラの開発をインクリメンタルに回していけるような開発環境を目指して構築と改善を繰り返してきた結果、Vagrant + Docker になった」ということになります.
ここからは、社内の開発環境がどのような変遷を経てきたのかという話を書いていきます.

開発環境構築の自動化

2014年初頭に僕が今の会社に参加した頃、開発チームのメンバーはプロジェクトの SVN リポジトリにチェックインされている開発環境セットアップ手順が書かれたテキストファイルを見ながら各々の端末にミドルウェアなどを直接インストール、設定した上で開発を行っていました. 複数プロジェクトを担当するエンジニアの場合は上記に加えて Apache の Virtual Host 機能などを使い、ホスト名の解決は /etc/hosts に手でレコードを足すという運用でした.

社内のファイル共有サーバーにはリードエンジニアが個人利用を前提に書いていた Vagrant + Puppet のプロビジョニングスクリプトはありましたが、他のメンバーの環境で動かすためにはある種のおまじない的な書き換えが必要だったこともあり、他のメンバーには利用されていませんでした (もったいないです). そもそも僕の知る限りその Vagrant + Puppet スクリプトの存在を知るメンバーは書いた本人以外にいなかった気もします.

そのため、エンジニアは自分の開発環境が正しく動かなくなったと感じると、手順書を見ながらミドルウェアの再インストールからやり直していました. また、僕が入社した時点でチーム開発が行われていたプロジェクトは1つしかなかったため、上記のような状況が社内的に問題として扱われることはなく、また取り扱う必要性もなかったのだろうと思います. 加えてこの唯一のチーム開発プロジェクトは運用も自社で行なっていたため、本番環境の構築や運用についてもトライアンドエラーを行いながら進められる状況だったこと、アプリケーションのデプロイが完全には自動化されていなかったことも、VM やそのプロビジョニング自動化の必要性が検討されなかった理由だったのかなと今では感じています.

入社後、僕が最初に着手したのは開発環境構築の自動化です. この時のゴールは以下の4点でした.

  • 開発環境構築が完全に自動で行われ、アプリケーションが動作すること
  • 動作させるための設定ファイル書き換えやソースへの一時的なパッチ適用を必要としないこと (= 各エンジニアの開発環境間でソース差分が発生しないこと)
  • 環境構築のためのスクリプト群がバージョン管理下にあること
  • 開発環境の破棄が簡単にできること

上記のゴールを手っ取り早く実現するため、プロビジョニング済みの VirtualBox イメージを配布するというガチムチな手段も一瞬考えましたが、数百 MB にもなるイメージファイルを長期間に渡ってホストする必要があることを嫌い、この方法はお蔵入りにしました (前述の Vagrant + Puppet スクリプトが存在していなかったら最初はこの方法を採っていたかもしれません).
まずは社内への Proof-of-concept として利用する目的で Vagrant + Puppet スクリプトに手を入れて皆の環境で動くようにしました. 関係者への内容説明や導入することによるメリット、今後の展望などを説明し一定の好意的な反応を得たことで、最終的に利用するスクリプトの用意に進むことができました.

最終的には前述した Vagrant + Puppet スクリプトではなく、 Vagrant + Chef で一から書くことを選択しました. プロビジョニング時の冪等性担保の容易さや設定ファイルの可読性、ディレクトリ構造についてのルールの存在、また将来的な AWS OpsWorks との連携なども検討した結果の Chef です. 本番環境に Amazon Linux を利用することが多かったことから、VM には CentOS 6.x を採用していました.
作成したスクリプト群をリポジトリにチェックインし、チームへの配布を行ったのですが、チームは即座にこの方法に対応してくれました (おそらく1~2週間ほどで全員が Vagrant VM での開発環境を自身の端末で利用していたと思います). vagrant up という単一コマンドでちゃんとアプリケーションが動作する VM が容易に作成できること、何か問題が発生したときにエンジニア自身で Vagrantfile やプロビジョニングスクリプトを見て調査を行えること、既存の /etc/hosts 書き換え運用を踏襲しつつも vagrant-hostsupdater というプラグインを用いて書き換え自体を自動化したこと、などが全員の適応を早めることに成功した要因だと思います.

また、これと同時期に SVN リポジトリを全履歴を維持したまま Git リポジトリに移行し、アプリケーションソース内に直書きされていた多くのパラメーター値を設定ファイルや環境変数に落とし込むリファクタリングを行いました.

開発プロジェクトの増加とプロビジョニングツールの変化

開発環境構築が一定のクオリティで自動化できるようになった後、それまで1つしかなかった社内のチーム開発&運用プロジェクトが半年で3つにまで増え、既存の Vagrant + Chef を横展開する形でそれぞれの環境構築スクリプトが作成されました. vagrant-hostsupdater によって /etc/hosts に書き込まれる各プロジェクトの IP とホスト名は 192.168.xx.[project-id] [project-name].local のような形でプロジェクト間で重複しないものを利用し、エンジニアがプロジェクトごとの開発作業を進める際の開発環境面でのコンテキストスイッチを最小限に抑えることに成功しました.

そしてこの流れと並行して、プロビジョニングツールの Chef から Ansible への移行と Docker の技術検証が社内で始まります. 検証の成果が反映され始めた2015年初頭からは、新規プロジェクトについては Ansible でプロビジョニングされた Docker イメージを利用した開発環境が構築されるようになります (この時点での VM とコンテナ管理は CoreOS + systemd でした).

この構成はプロビジョニング方法や内部的な仕組みが初期の Vagrant + Chef と大きく異なり、一見するとかなりドラスティックな変更です. このような大掛かりな変更が社内的にスムーズに受け入れられたのは、初めて自動化に取り組んだとき同様に vagrant up という単一コマンドで開発環境を構築できたこと、VM 内の Web サーバーのエンドポイントが /etc/hosts に自動的に書き込まれる仕組みを引き続き利用できたことが大きく寄与していると考えています.

また、2015年中頃からは本番環境での Docker 運用が軌道に乗ったことを受け、数ヶ月の移行期間を置いた後に Ansible の利用が停止されました. Ansible の利用を止めた理由は話の本筋ではないため詳細は省きますが、Docker の利用により EC2 自体の複雑なプロビジョニングが必要ではなくなってきたこと、Docker イメージのレイヤーキャッシュ有効活用によるビルド速度とデプロイ速度の向上を目指したところが理由として大きかったと思います (Dockerfile だけで行けそうだよね、という感じに社内の雰囲気が変わってきたのも理由の一つ).

Docker Machine, Docker Compose

Ansible の利用停止に向けた作業が始まった2015年中頃、並行して Docker Machine, Docker Compose の技術検証を開始しました. この検証の目的はこれらのツールが Vagrant での開発環境構築を置き換えうるものかどうか、という点にありました.

結果として Docker Machine が採用されることはありませんでしたが、 Docker Compose は cloud-config を代替するものとして採用されます.

もともと CoreOS + systemd で Docker を利用した開発環境を構築した際は、cloud-config の各ユニットに docker コマンドを直接書いていました. このやり方は構築当初においては特に問題なく利用できていたのですが、以下のような変化を吸収していく過程で docker コマンドが長大なものになってしまうという問題を抱え始めます.

  • 本番環境の運用が軌道に乗ったことで前にも増して Docker を使い込むようになり、run の際に利用するオプションや設定が増えた
  • アプリケーション開発の現場で The 12-Factor App を強く意識した実装が行われるようになってきたことから、特に環境変数がこれまでと比べて多用されるようになった

Docker の採用によってアプリケーションのポータビリティがこれまで想像もしていなかったレベルまで高まった結果、本番環境のために描いた理想に引っ張られる形で開発環境へのニーズもより高いレベルに変わってきた、という感じです. (とてもいい流れです)

環境変数やコマンドをある程度構造化した形で YAML に定義できる Docker Compose は、上記のような問題をうまいこと吸収しつつ Docker コンテナのライフサイクルを管理できるツールとして、社内標準の開発環境へと取り込まれました.

現在の社内の開発環境

2016年現在、社内で構築される開発環境は(プロジェクトによって細部に差異はありますが)以下のような構成になっています.

  • Vagrant
  • VirtualBox
  • CoreOS
  • Docker Compose
  • セルフホステッドな Docker レジストリ + Amazon ECR

Docker Compose のインストールは VM のプロビジョニング時に行われ、Vagrantfile 内で指定されたバージョンの Docker Compose のバイナリが自動的に配置されるような仕組みになっています. また、Docker エンジン自体のバージョンは CoreOS のチャンネルとバージョン番号に依存するため、そちらも VM 起動時にチェックできるようにミニマムで要求する CoreOS バージョン番号を指定しています. プロジェクトによっては VM 起動時に DB のマイグレーションスクリプトを自動で流したり、依存ライブラリを解決したりといったような処理を挟むことで、コマンド一発で最低限利用できる環境を整えるという原則を維持しています.

開発環境構築自動化を目指した当初と比べると、社内のプロジェクト数は増え、チーム構成も変わりました. メインで利用するプロビジョニングツールについても、Puppet, Chef, Ansible + Docker, Docker と変化を続けてきましたし、今後も改善を続けていきます.

前述したように、複数のプロジェクトごとに異なる要件を吸収するためには、それぞれプロビジョニングスクリプトの中身を変えたり、要求する Docker のバージョンを変えたりということが発生していきます. このような状況においてもアプリケーション開発やプロジェクトの流れを止めずに新技術や新手法の検討/採用を続けていけるのは、冒頭に書いたような開発環境の原則を維持できているからだと考えています. 継続的に新技術や新手法を検討できることが、結果として本番環境の構成や管理手法をより洗練された方法に変えていき、そこで得られた知見や見つかった問題を解決するために別の技術や手法の検討につながっていく、というサイクルが作れています.

まとめ

あなたの会社の規模やチームメンバーの構成、チーム間の関係性など、プロジェクトを取り巻く条件によってベストプラクティスというのは異なると思います. もしも今、会社やチームの開発環境やその仕組みを変えていく必要があり、その指針に迷っているのであれば、冒頭に書いたような原則を自分たちのために考えるところから始めてみるのはいかがでしょうか.