Java製アプリを Eclipse から実行したことしかない新人に「ビルドツールとは?」を説明してみる…そして CI へ


Ant とか Gradle とか,名前は見かけるけど何に使っているのかよくわかりません (意訳)

的なことを新人から立て続けに言われたので,順を追って説明してみようと試みる.

ビルドとは: 書いたプログラムを本番環境で動作させるまで

「ビルド」という言葉をいきなり説明するのも唐突なので,そもそもプログラムコードが本番で稼働するまでの流れをざっくりと説明します.

デプロイまでに必要な作業

アプリケーションをテスト環境や本番環境で動作させるためには,おおまかに言えば以下の様な手順をを踏みます.
(自分や新人の実業務ではサーバーサイドは Java,クライアントは Java だったり TypeScript で書かれた Web だったりするので,それを想定しています.)

  • コンパイル: プログラミング言語を用いて書いたプログラムをバイトコードに変換すること.スクリプト言語なら不要.
  • 依存ライブラリの解決: プロダクトが利用しているライブラリを用意すること.コンパイル時にライブラリが必要だったり,出荷するアプリケーションにライブラリを含める必要があるからです.
  • 静的解析: ソースコードのスタイルが正確か,潜在バグを含んでいる可能性の高い記述をしていないかを確認すること.
  • テスト: テストコードを実行してテスト結果を確認すること.(普通,失敗したらここで終了.)
  • 設定ファイル解決: アプリケーションを動かす環境ごとに異なる設定 (使うポート番号とか,接続する DB 情報とか) を記述したファイルのうち,適切なものを選ぶこと.
  • パッケージング: 複数のバイトコードファイルを,1つまたは複数のファイルにまとめること.Java なら jar ファイルとか war ファイルにしたり,更にインフラの用いるスクリプトなどと併せて zip ファイルや tar.gz ファイルにまとめてしまうこともある.
  • リリース(デプロイ): 上記までの手順でまとめたファイルを実際に動作させる環境に配置して起動すること.

アプリケーションを社内のテスト環境や本番環境にリリースするたびに,これらの作業を行う必要があります.
「ビルド」という言葉の意味は文脈に依って多岐にわたるのですが,本来的な意味は「要求された実行環境で動作できる形式にアプリケーションやライブラリを組み立てる」ことです.

ビルド方法の進化

「ビルド」という言葉は,ここから派生して「定型的な作業を行うこと」という広義な意味を持つこともあるので,文脈に気をつけるようにしてください.
以下では「アプリケーションやライブラリを出荷させられる状態にするように構築すること」という狭義の意味で話を進めます.

ビルドと言っても,その実現の仕方はいろいろとあります.

Step 0: 人力ビルド

何も考えなければ,前述の作業群を誰かが行えば,アプリケーションのビルドとしては十分です.しかし,プロジェクトを運営していく中で,こんな問題に直面しないでしょうか.

  • 誰が行うのか?: 大きなプロダクトだとコンパイルにも時間が掛かるし,静的解析にも時間がかかるので,なるべくジュニアなメンバーに任せたい,となるのが「あるある」なパターン.
  • どれくらいの頻度で行うのか?: なるべくなら頻繁にリリースして,不具合が混入されたら早めに気付けるようにしたい.しかし,ジュニアなメンバーとはいえリリースを行うたびに時間をとられるのはアホくさい.
  • 手作業の不安: コンパイルのコマンドを打つ,環境ごとに分けられている設定ファイルのどれを取ってくるかを決める,zip コマンドでファイルを固める…これらの作業を手作業で行っていると,ファイルの取り違えやオプションの指定間違いが起きるのは想像に難くない.

Step 1: ビルドスクリプト

ビルド作業で実行するべきコマンドをすべてシェルスクリプトやコマンドファイルにまとめて,それを叩くだけにする,とすれば,人手に依るオペレーションから生じるリスクを回避することが出来ます.こういうシェルスクリプトやコマンドファイルを「ビルドスクリプト」とか「ビルドシェル」と呼ぶことがあります.

しかし,ビルドスクリプトは万能ではありません.
一番の欠点は,タスク間の依存関係を理解したうえで,無駄な作業を再度実行しない,という判断ができないことです.
例えば,以下の作業を考えてみましょう.

1. ビルドを回す.プロダクトコードとテストコードのコンパイルが実行されたうえで,テストが一部失敗した.

2. テスト失敗の原因を確認すると,プロダクトコードではなくテストコードに問題があったので,テストコードの記述を修正した.

3. もう一度ビルドを回す.プロダクトコードとテストコードのコンパイルが実行され,めでたくテストも全て成功した.

無駄な工程は何かというと,「プロダクトコードが書き換えられていないのにコンパイルし直している」ということです.
ここで大事なのは,

  • テストの実行はテストコードのコンパイルに依存している
  • テストコードのコンパイルはプロダクトコードのコンパイルに依存している

ということです.テストコードの書き換えを行ったら,テストコードのコンパイル結果が変わるはずであり,テストの実行結果はテストコードのコンパイル結果の変更の影響を受けるはずです.しかし,プロダクトコード自体は変更されていないので,プロダクトコードのコンパイル結果も変わらないはずです.
シェルスクリプトは決まりきった処理の内容を記述するというビルドスクリプトの要請には応えますが,ビルドを構成するタスク同士の依存関係や,タスクの実行要否を判断するための状態管理までは行えません.

また,アプリケーションが依存しているライブラリも自分で配置して,そのパスをビルドスクリプト内で指定する必要があります.

Step 2: ビルドツール

そこで登場するのが「ビルドツール」です.
プロダクトによって実行出来ることは変わってくるのですが,だいたい以下の様なことを可能としています.

  • ビルドを構成するタスクを定義し,それを実行する.例えば以下の様なタスクを行う.
    • ソースファイルのコンパイル
    • テストや静的解析の実行,およびその結果のレポート出力
    • リリース用アプリケーションイメージの構築
  • ビルドを構成するタスク間の依存関係を管理し,そのタスクのインプットとなる上流タスクの結果やソースコードなどの変更有無からタスクの実行有無を判断する.
  • ライブラリの依存関係を管理する.(これを行うことに特化した「パッケージマネージャー」というカテゴリに属するツールもあります.有名なのは JavaScript の npm や,Ruby の RubyGems など.)
  • タスクのインプットとなるファイルを監視し,変更があった場合に自動的にタスクを実行する. (Grunt における watch や,Gradle における continuous build など)

Step 3: 継続的インテグレーション (CI)

ここからは「ビルドツール」という話からは逸れてしまいますが,「ビルドツールを使ってここまで自動化出来たのなら,もっと自動化してプロダクトの品質を高めよう」というのが,この「継続的インテグレーション」や「継続的デリバリー」です.

継続的インテグレーション (CI) とは,文字通り「継続的」にアプリケーションのビルドを行い,自動テストを回し,常に出荷可能なアプリケーションをビルドし続けよう,というプラクティスです.「PDCA」という言葉がありますが,継続的にビルドやテストを行うことで,小さいフィードバックを頻繁に得ることでプロダクトの開発効率を上げることが狙いです.
どれくらい「継続的」かというと,「できるだけ細かく」です.次に頻度の例を上げますが,上に行けば行くほど導入の敷居が低く,下に行けば行くほど難易度は高くなりますが得られるフィードバックが大きくなります.

  • ナイトリービルド: 毎晩メンバーが帰った後,例えば 24:00 にビルドが実行される.メンバーは朝出勤すると,ビルド結果を知ることが出来る.
  • 周期的ビルド: n 時間毎など,決まった周期でビルドを行う.
  • コミット毎ビルド: コミット (プッシュ) が行われるごとにビルドを行う.

CI のサイクルが回る頻度を上げることで,デグレードなどの問題が起きた場合の原因の候補が絞られます.
しかし,ビルドに掛かる時間を短縮化させないと,そもそも CI が回らないという点で難易度が上がります.コンパイルや静的解析もさることながら,テスト駆動開発を行う文化が深まるほどテストに時間が掛かる,いわゆる「スローテスト問題」も解決する必要が出てきます.
当然,ビルドが手動だった場合には CI どころの話ではありません.
また,問題が起きた時にすぐに対処するというチームの土壌を養成する必要もあります.

Step 4: 継続的デリバリー,または継続的デプロイメント

CI が細かく回り始めると,常に最新のプロダクトを自動的に本番環境にリリースできるような仕組みを作ることも出来ます.これを「継続的デリバリー」や「継続的デプロイメント」といいます.
最新のコードをすぐに本番環境で稼働させられるため,実ユースケースからのフィードバックを迅速に得ることが出来ます.

メインブランチ (git なら master, Subversion なら trunk) を常にデプロイ可能な状態に保つこと,問題があった場合に迅速にロールバックできること,など,技術的に乗り越える課題も多いです.当然,デプロイ作業もさることながらロールバック作業も自動化されていないと,このプラクティスを実行することは出来ません.

SI 業界の携わるエンタープライズシステムだと容易に実践できるプラクティスではありませんが,ウェブ系アプリでは事例がたくさんあります.
(自分の詳しくない分野なので,記述や解釈に不備があったらごめんなさい…)

主なビルドツールを挙げてみる

ということで,主なビルドツールをざっくりと挙げてみます.自分の仕事に関わるかどうかで記述に濃淡があるかもしれませんが,ご容赦ください.
あまり詳しくないビルドツールは 良い点 / 悪い点 は記載省略しています.(調べて書いても良かったが,書くなら動かしてみてからが良かったので…)

GNU Make (あるいは,単に make と呼ぶ)

  • Profile
    • https://www.gnu.org/software/make/
    • ビルドツールの草分け的存在.
    • ビルドスクリプト名は Makefile.
    • C や C++ のビルドで今でも現役.特に UNIX のネイティブライブラリやソフトウェアを自分でビルドするときは make && make install とか,よくやる.
    • (個人的には,自分で makefile 書いたことがないので詳しいことまではよくわからない…)
  • 良い点
    • なんたって草分け.
    • 独自のビルドタスクを記述するスクリプティングが容易
  • 難点
    • マルチプラットフォーム対応に弱い.
    • 依存ライブラリの管理機構を備えていない.

Ant

  • Profile
    • http://ant.apache.org/
    • Java 向けのビルドツール.
    • ビルドファイル名のデフォルトは build.xml.
  • 良い点
    • Ant 自体も Java で動いているので,マルチプラットフォームで動作させられる.
    • Eclipse に標準で組み込まれており,インクリメンタルビルドなどがスムーズに使える.
  • 悪い点
    • XML で記述するので,ビルドファイルの可読性が低い.
    • 宣言的にビルドスクリプトを記述するので,独自の条件分岐を記述しようとすると非常に煩雑になる.
    • デフォルトではライブラリの依存解決機構を持っていない.(後に Ivy で解決されるが…)
    • 複数プロジェクトのビルドに対応していない.(ので,独自に他のプロジェクトの成果物を取ってくるようにビルドスクリプトを記述する必要がある.)

Maven

  • Profile
    • http://maven.apache.org/
    • Ant に取って代わろうとしたビルドツール.
    • pom.xml でプロジェクト構造を定義. (あまりビルドファイルと呼ばないな…)
    • なんてったってライブラリ依存解決.
    • ライブラリ依存管理機能を提供するという意味での「Maven」と,ビルドツールとしての「Maven」という文脈の違いに気をつけたほうが良い.
    • http://search.maven.org/
  • 良い点
    • ライブラリの依存管理と「セントラルリポジトリ」を編み出したこと.
    • プロジェクト構造に規約を持ち込むことで, Ant よりもビルドスクリプトの記述がすっきりしたこと.
  • 悪い点
    • 依然としてビルドファイルが XML であること.
    • やはり記述が宣言的.規約にガチガチに縛られた使い方が前提となっており,少し外れたことをしようとすると,かなり苦労する必要がある.

Gradle

  • Profile
    • http://gradle.org/
    • これまでのツールの良い所どり.
    • ビルドスクリプト名のデフォルトは build.gradle.
    • 最近の Java 開発ではだいぶデファクト・スタンダード.
    • ネイティブ (C, C++, Objective-C) のサポートもある.
      (Google Testing Framework を標準サポートしている!!)
    • Android Studio でデフォルト.
  • 良い所
    • Groovy を使ったスクリプティングでのタスク記述が可能.(Goodbye XML)
    • Maven レポジトリによるライブラリ依存解決が可能.
    • マルチプロジェクトサポート.
    • ビルドツール自体にプラグイン機構が組み込まれている.独自拡張が可能.
    • これまでのツールは各開発者が自分の環境にツールのインストールを必要としたが,Gradle はスタンドアロンで動作可能な実行ファイルを簡単に配布可能.
      メンバーやビルド環境間でのビルドツールのバージョンを揃えられる. (Gradle Wrapper)
  • 悪い点
    • DSL を詳しく理解しなければいけない.
    • 遅い…?? (個人的には ant と大して変わらないような気がしているが)

Bazel

  • Profile
    • http://bazel.io/
    • 元は Google の社内ツール.2015年3月に Alpha 版が公開され,現在 Beta 版.2016年5月に安定版リリース予定.
    • WORKSPACE というファイルと BUILD というファイルを置くらしい(?)
    • 最近 Cookpad 開発者ブログで取り上げられて少し注目を浴びている?
      http://techlife.cookpad.com/entry/introduce-bazel-build
    • 速いらしい.

Grunt

  • Profile
    • http://gruntjs.com/
    • JavaScript 向け.
    • ビルドツールというよりタスクランナーと呼ばれる.
    • ビルドスクリプト名は Gruntfile.
    • Gruntfile は JSON で記述する.

Gulp

  • Profile
    • http://gulpjs.com/
    • JavaScript 向け.
    • (僕は使ったことがない…)
    • Grunt の対抗馬…らしい.
    • ビルドスクリプト名は gulpfile.
    • Node.js との相性が良い…らしい.

SBT

  • Profile
    • http://www.scala-sbt.org/
    • Scala でのデファクトスタンダード…らしい.
    • (僕は他人のプロジェクトを言われるがままに動かすのにしか使ったことがない…)
    • ビルドスクリプト名は build.sbt

参考書籍

  • Gradle 徹底入門
    • だいぶ詳しい.
    • この記事のビルドツールの説明を書く際に,この本の説明となるべく似せないように気をつけたがだいぶ似てしまった…
      (自分なりに強調するところは強調したのだが…決してパクリではありません…)
  • Jenkins the Definitive Guide
    • 社内オンプレで CI するならデファクトは Jenkins. そのやり方も,CI とは何かの説明もあって良い本.
  • チーム開発実践入門
    • ビルドや CI も含めて,環境やビルドやリソース管理にヒューマンリソースをなるべく割かずにチームで効率よく開発を行うのにはどうしたら良いのかの方法論を紹介した本.新人にかなりオススメ.
  • 継続的インテグレーション入門
    • 最近個人的に読みたいと思っているだけ.