Build Insiderオピニオン:花井志生(1)
Dockerでビルドを改善する
開発現場で役立つ記事や書籍を多数著している花井志生氏がオピニオンコラム初登場。今回は、コンテナー型の仮想実行環境「Docker」を使ってCIを行うことのメリットを考察。
Dockerを開発に使用する利点といったら、何を思い浮かべるだろうか。それは例えばDBサーバーやWebサーバーのような環境を手軽に構築できるという点かもしれないし、簡単に「やり直し」ができる動作環境として使えるという点かもしれない。あるいは多人数で開発するときに同一の環境を手軽に配布できる点を挙げる人もいるかもしれない。
Dockerは代表的なアプリケーションコンテナー技術で、現在もものすごい速度で開発が進められている。もちろんDockerの主戦場は、アプリの実行環境の提供にあるわけだが、開発環境として使用したときのメリットも大きい。今回はCI(継続的インテグレーション)サーバーのビルドの中でDockerを使用するケースを取り上げて、ビルドを行う際にDockerを使うことの利点について考えてみたい。
ビルドにおける諸問題
まずは日々のCIサーバーを用いた自動ビルドの中でどういう課題があるのかを考えてみよう。
CIサーバーのセットアップが属人化していないか?
通常、CIサーバーをセットアップしただけではビルドはできない。
例えばWebアプリのテストを自動化するケースであれば、SeleniumのテストのためにXvfb(Xサーバーをメモリ上で実現する仮想フレームバッファ)や、テスト用のWebブラウザーなどをインストールしておく必要があるだろうし、テスト実行のためにはアプリが使用するプログラミング言語の環境もセットアップしておく必要があるだろう。
もしも今稼働しているCIサーバーが壊れたときに、すぐに復旧できるだろうか? もちろん経験を積んだ開発者ならば、CIサーバーのセットアップは、Chefなどを用いてスクリプト化するだろうが、日々の開発が続く中で新たなコンポーネント(例えばDBサーバー)が必要になったときに、Chefのスクリプトをきちんと保守し、そしてその都度スクリプトが全体としてきちんと動作するかを確認していくことは、それほど容易ではない。
一度でもCIサーバーにssh
などでログインして手作業のセットアップをしてしまえば、それは「忘れられた手順」と化し、次にセットアップするときに抜けや間違いを招くことになるだろう。CIサーバー自体への追加の環境セットアップは最小限にとどめておくことが肝要である。
次に、1台のCIサーバーでありとあらゆるビルドをまかなおうとしたり、複数台のCIサーバーで負荷分散をしたりする場合に出てくる課題について考えていこう。これらはいずれもCIサーバーの資源や設定にビルドを依存させることで発生するものだ。
CIサーバーの共用資源の管理が煩わしくなっていないか?
例えば何らかのバッチアプリを開発しているとしよう。
この場合、テストを自動化する上で、DBは必須といえる。もちろんデータアクセス部分を分離して、そこをモック化してテストするとか、テストの際にはSQLiteやH2のような組み込み型のDBを使う、というやり方もないわけではない。
しかしバッチアプリは高スループットを求められることが多く、どうしてもDB固有の方言を使わざるを得ないことが多い(つまりテストのときにだけ別のDBMSを使うのが難しい)上、モック化すると肝心のSQLのテストができないのでテストのカバレッジが著しく落ち、DBのアーキテクチャに依存したデッドロックなどの事象が露見しにくくなるなど弊害が大きい。
結局、こうした方法を用いるメリットとデメリットをてんびんにかけた場合、特にバッチアプリのようなDBに極度に依存したアプリでは、少々面倒でも本番で使用するものと同じDBサーバーを用意する方が理にかなっているだろう。
しかしビルドでDBを使うとなるといろいろと面倒なことが起きる。例えば複数のビルドが、それぞれDBにアクセスする際には、どのように分離すればよいのだろうか。DDL(Data Definition Language)はアプリのバージョンによって変わっていくが、それらをどうやって同居させるのだろうか。
よくある方法はスキーマを分けることだ。しかし例えば、あるアプリがDBサーバーに対する特定のチューニング(何らかのパラメーターの変更など。例: 全文検索インデックス作成のためのモジュールの追加)を必要とする場合、そうしたチューニングを必要としない他のアプリとどうやって同居すればよいのだろうか。そして、そうしたチューニングの設定値をきちんと忘れずに本番環境まで持っていけるのだろうか。
このように、CIサーバーの資源を複数のビルドで共有しようとすると、それらの管理が煩雑になっていく。
CIサーバーが稼働しているサーバーにビルドが依存していないか?
比較的よく見かけるパターンとして、アプリがサーバー内の特定のディレクトリに存在する設定ファイルを読む造りになっていることがある。個人的には、このような構成はやめるべきだと思うが、現実問題としてこうした構成がなくなることは当分ないだろう。それはアプリのモジュール内にパラメーターとして設定を組み込むよりも、サーバーの特定ディレクトリのファイルを変更するようにした方が運用の担当者にとって「楽」だからである。
このようなケースでは、アプリの自動テストのときにもこのディレクトリが必要となるため、往々にしてCIサーバーにも同様のディレクトリが存在して、ビルド(時に行われる自動テスト)がそのディレクトリの内容に依存してしまうことが多い。
そして、あるアプリが新機能の確認のために、その設定内容を変更したことで、他の無関係の機能のテストが失敗する。悪夢である。しかも通常は、ビルドトリガーはこういった設定内容の変更とは連動していないので、気付くのがかなり後になることも多い。そうなると担当者も変更の経緯を忘れていて原因究明に手がかかる。
複数のビルドを同時実行できるか?
CIサーバーが混んでくる(同時に複数のビルドが実行されるようになる)とビルドがある確率で失敗する。こういう場合、原因の1つは資源の衝突だ。例えばWebアプリの自動受け入れテストを行う場合、ビルドの中でアプリケーションサーバーを起動して、アプリをデプロイし、Seleniumなどを用いてテストするケースが多いだろう。
そうした場合にアプリケーションサーバーのポートに固定値を用いていると、複数のビルドが同時に走ったときにポートがぶつかってしまう。あるいはスタンドアロンアプリで設定の保管にホームディレクトリ上の設定ファイルを使っている場合(Windowsならレジストリ、JavaならPreferences APIなど)、自動テストの中でこれらの設定を書き換えて、その挙動をテストしたくなるだろう。しかし、これもシステム全体で1つの資源なので、複数のテストが同時に走れば競合して予期しない結果を招くだろう。
スレーブをすぐに用意できるか?
開発が佳境にさしかかってくると、CIサーバーにはビルドが集中し、1台では全てをさばききれなくなることが多い。こういう事態に対処するため、多くのCIサーバーには、スレーブ機を構成する仕組みがある。マスター機はスレーブ機へのビルドのディスパッチに専念させて、複数のスレーブ機にビルドを分散させることで、負荷を分散できる。しかし前節に挙げたような問題があると、スレーブ機の用意が迅速に行えなくなってしまう。
また、開発には波があるので、スレーブ機はクラウド環境に置くことが望ましい。開発繁忙期にだけスレーブ機を増強して、終わったら削除してしまえば課金を抑えられる。さらにこの考えを進めて、ビルドごとにスレーブ機を作成、削除することも可能だろう。これにより本当に必要なときにだけスレーブ機を利用しながら、資源を有効活用できる。
従来、こういうケースではスレーブ機のVMイメージを用意するのが一般的だった。しかしビルドごとにVMを構築するのは、いささかオーバーヘッドが大きいし、ビルド1つに1つのVMを割り当てるというのも効率が悪い。もう少し軽量で、それでいて資源が分離され、セットアップが自動的に行える仕組みが必要だろう。
そして、Dockerを活用することで、ここまでに見てきた課題の多くが解決できるのだ。
Docker(アプリケーションコンテナー)による解決
上記に挙げた問題の多くがDockerによって解決できる。そのために、Dockerをどのように活用できるのか見てみよう。
セットアップのコード化
テストに特定の環境(例:DBサーバー)が必要ならば、ビルドの中でセットアップすればよい。ビルドに特定のプログラミング言語が必要ならば、それをビルドの中でセットアップすればよい。DockerにはComposeという仕組みがあり、アプリケーションとサーバーをセットにして構成できる。
セットアップは全て「Dockerfile」ファイルに書かれた「動くコード」なので、いつでも同じ環境を再現できる。このときの注意点は、手作業でセットアップしつつ、docker commit
コマンドでその内容をイメージに保存するというやり方を避けることだ。これをやってしまうと、そのときに行われた手順がDockerfileファイルに残らないので、後で同じ環境を再現することが困難になってしまう。セットアップが簡単になればスレーブ増設への対応も迅速に行えるし、Dockerに対応したクラウド環境を用意すれば、ビルドの単位で他のサーバーに分散実行させることも可能だ。
ビルド環境の資源分離
Dockerを用いることで、資源が分離されるためビルド間での資源の衝突について気にしなくてよくなる。テストの中でDBサーバーやWebサーバーを起動したとしてもポートが分離されるので、複数のビルドで衝突することはない。ファイルシステムも分離されるため、アプリケーションのテストの際に特定のディレクトリ内の設定ファイルを変更したとしても、それが他のビルドに影響することはない。もちろんDBサーバーなど資源消費がそれなりに大きなものが、場合によっては複数起動されることになるため、並列していくつのビルドを実行できるように設定するかはCIサーバーのスペックと相談して決定する必要がある。
考慮点
最後にDockerを用いたビルドを行う場合の考慮点について挙げておきたい。Dockerを用いたビルドは、
- Dockerを用いた環境の構築
- コンパイルやテストの実行
- 1で構築した環境の破壊
という順序で行うことになる。2や3で想定外の障害が起きて、1で生成した環境がビルド終了後にも残り続けてしまう可能性がある。このため1の段階で余計な環境を削除しておく必要がある。
また、ビルドに必要となるファイル(ソースコードなど)、出力ファイル(テストレポートなど)は、ホスト側とやりとりする必要がある。これにはDockerのボリューム機能を用いることになるが、Dockerはデフォルトではrootで動作するため、そのままでは出力ファイルがroot権限になり問題が起きることがある。docker run
コマンドの-u
オプションを使うなどして適切なユーザーに切り替えておくのがよいだろう(例: docker run -u `id -u` ...))。
OSのタイムゾーン設定は、イメージによってはUTCになっている場合があるので、必要に応じて合わせておく必要がある。
locale設定にも注意が必要だ。多くのDockerのイメージでは環境変数LANG
の値が「en_US.UTF-8」などになっているので、適切な設定に合わせておく必要があるだろう。
Dockerのコンテナーは終了しても、デフォルトでは削除されないのでdocker rm
コマンドを用いるか、docker run
コマンドの実行時に--rm
オプションを付与するなどして、削除するようにしておかないとディスクスペースや、dockerのコマンド実行にパフォーマンス上の支障が生じることがある。定期的にdocker ps -a
コマンドを実行して不要なコンテナーがないかチェックして、掃除しておくとよいだろう。
まとめ
今回はビルドの諸問題をDockerで解決する方法について考察した。最近のCIサーバーは、CircleCIやCodeshipなどDockerへの対応が進んでおり、Dockerをビルドで利用するハードルも徐々に下がっていくものと思われる。今回の内容がビルド自動化の一助となれば幸いである。
花井 志生(はない しせい)
入社当時はC/C++を用いた組み込み機器(POS)用のアプリケーション開発に携わる。
10年ほどでサーバーサイドに移り、主にJavaを使用したWebアプリケーション開発に軸足を移す。
2015年夏からクラウドを用いたソリューションのテクニカル・コンサル、PoCを生業としている。
主な著書にJava、Ruby、C言語を用いたものがある。
1. 【現在、表示中】≫ Dockerでビルドを改善する
開発現場で役立つ記事や書籍を多数著している花井志生氏がオピニオンコラム初登場。今回は、コンテナー型の仮想実行環境「Docker」を使ってCIを行うことのメリットを考察。
2. 自宅サーバーか、クラウドか ― 開発/テスト機の採用基準と最適なミックス
個人の開発/テスト機でもクラウド(調達)の方が安いといえるのだろうか? 自宅サーバー(所有)の方が割安なケースを考え、両者のメリットを生かす手法と実践手順の例を示す。