パブリッククラウドへのデプロイの自動化
最近は、Webアプリケーションの本番環境やテスト環境として、パブリッククラウド(ここでは、EC2やAzureなどのIaaSを指すことにします)を使用する事例をよく聞きます。パブリッククラウドを使用するときの課題として、アプリケーションのデプロイの自動化があります。
デプロイの自動化の課題
オンプレミスの場合は、WebサーバとCIサーバ(CI
しかし、パブリッククラウドに上記のWebサーバ・APサーバを持っていくと、デプロイ用のインタフェースがインターネットに公開されることになり、外部からアクセスされる危険がでてきます。かといって、デプロイ用のインタフェースを外部(インターネット)からブロックすると、CIサーバからもアクセスできなくなります。
課題の解決
この問題を解決する手段はいろいろあると思いますが、手段の1つとしてSSHトンネルを使う方法があります。外部からのアクセスをブロックしつつ、SSHトンネルを使うことで、SSHでログインできるマシンにだけデプロイ用のインタフェースを公開する形にすることができます。
SSHトンネルの設定例
APサーバとCIサーバをSSHトンネルで繋ぐため、CIサーバ上で以下のコマンドを実行します。
ssh -f userfoo@apserver.example.com -L 9999:localhost:8080 -N
「-f」は、バックグランドで動作させるためのオプションです。「userfoo@apserver.example.com」は、「apserver.example.com(APサーバ)」に「userfoo」ユーザでSSHでログインするという意味です。「-L 9999:localhost:8080」は、ローカル(CIサーバ)のポート9999にアクセスすると、SSHトンネルでapserver.example.comに転送され、そこからlocalhost(APサーバ)のポート8080(デプロイ用のインタフェースのポートを想定)にアクセスするという意味になります。「-N」は、リモートでコマンドを実行しないというオプションです(トンネルするだけなのでこのオプションを付けます)。
さいごに
この方法がベストかどうかは分かりませんが、1つの手段として参考になれば幸いです。
サーバ側(Java)とクライアント側でエディタを別にする
クライアント側をJavaScriptプログラムで作成する場合は、サーバ側のJavaプログラムをEclipseで作成し、クライアント側のJavaScriptプログラムを別のエディタで作成したくなると思います(JavaScriptプログラムの作成に人気のエディタとして、JetBrainsのWebStormや、Sublime Text 2があります)。
このとき少し問題となるのが、EclipseのWTPで起動したWebサーバの挙動です。JavaScriptファイルなどの静的コンテンツが別のエディタで更新されても、Eclipseで同期させない限り(F5のリフレッシュなど)、Webサーバは古いデータを配信します(WTPの設定で何とかなるのかもしれませんが、調べきれてません)。これでは、同期させる手間がかかり作業効率が落ちます。
この問題を解決するには、WTPを使用しないでWebサーバを起動する必要があります。まず思いつくのは、Webサーバの起動ファイルを直接実行することです。しかし、この方法だと、Mavenを使ったEclipseプロジェクトの場合、JarファイルをレポジトリからWEB-INF/libにコピーしてくる手間がかかります。また、Jarの依存をMavenで追加するたびにコピーの作業が必要になるためお奨めではありません。
お奨めは、Eclipseプロジェクト内で適当なクラスを作成し、mainメソッドを作成してWebサーバのAPIを呼出しサーバを起動することです。この方法だと、Mavenで追加したJarもそのまま読込めるので手間がかかりません。さらに、変更したクラスを再ロードさせるのも簡単です。下記のコードは、JettyのAPIをmainメソッドで呼出した例です。
package jetty; import java.util.Scanner; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.webapp.WebAppContext; public class StartJetty { public static void main(String[] args) throws Exception { Server server = new Server(8080); WebAppContext context = new WebAppContext(); context.setDescriptor("WebContent/WEB-INF/web.xml"); context.setWar("WebContent"); context.setContextPath("/foo"); context.setExtraClasspath("target/classes"); server.setHandler(context); server.start(); Scanner s = new Scanner(System.in); while (true) { s.next(); context.stop(); context.start(); } } }
このmainメソッドをEclipseから実行すれば、ポート8080でWebサーバが立ち上がります。また、コードの下部で、Scannerを使用してwhileのループをさせていますが、これによって、Eclipse内のコンソールウィンドウで何らかの文字を入力してエンターキーを押下するだけで、クラスが再ロードされます。
ちょっとしたことノウハウですが、作業効率の向上に地味に貢献すると思います。
補足
今回のサンプルで使用したJettyのAPIのJarを追加するMavenの依存の記述を以下に示します。
<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-server</artifactId> <version>8.1.5.v20120716</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-servlet</artifactId> <version>8.1.5.v20120716</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-webapp</artifactId> <version>8.1.5.v20120716</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-servlets</artifactId> <version>8.1.5.v20120716</version> </dependency>
Cucumber-JVMで、同じシナリオを異なるレイヤーでテストする
以前のブログでCucumber-JVMのテストを紹介したときは、ドメインクラス(Carクラス)のオブジェクトと直接やりとりする下位のレイヤーのプログラムのテストでしたが、同じシナリオを使って上位のレイヤーのテストも可能です。
例えば、以前のブログで紹介したCarクラスの操作が、RESTのAPIで提供されているとしましょう。まずは、シナリオを再確認します。テストに使用するシナリオは、以前と同じく以下の内容です。
Feature: 車の燃料の補給 運転手は、車を走らせるために、燃料を補給する必要がある。 Scenario: 燃料の補給 Given 車のタンクには、10リットルの燃料が入っている When 運転手は、50リットルの燃料を補給した Then タンクには、60リットルの燃料が入っている
次に、Step Definitionsの実装例を以下に示します。
public class CarMaintenanceSteps { private int carId; private RestTemplate restTemplate;// 初期化の処理の記述は省略 @Given("^車のタンクには、(\\d+)リットルの燃料が入っている$") public void 車のタンクには_リットルの燃料が入っている(int arg1) throws Throwable { Car car = new Car(arg1); car = restTemplate.postForObject( "http://localhost:8080/carapp/car/add", car, Car.class); carId = car.getId(); } @When("^運転手は、(\\d+)リットルの燃料を補給した$") public void 運転手は_リットルの燃料を補給した(int arg1) throws Throwable { restTemplate.postForObject( "http://localhost:8080/carapp/car/{carId}/fuel/add", arg1, Car.class, carId); } @Then("^タンクには、(\\d+)リットルの燃料が入っている$") public void タンクには_リットルの燃料が入っている(int arg1) throws Throwable { Car car = restTemplate.getForObject( "http://localhost:8080/carapp/car/{carId}", Car.class, carId); assertEquals(car.getFuelLevel(), arg1); } }
RestTemplateは、Spring Frameworkが提供するクラスで、RESTのクライアントの機能を提供します(RestTemplateの使い方の説明は、今回の本筋ではないため割愛します。また、サーバ側の実装も本筋ではないため割愛します)。シナリオのステップの内容に合わせてURIを変更しRESTのAPIを呼出しているのが確認できます。
Webコンテナの起動
上記のStep Definitionsを実行する際は、RESTのAPIを提供するサーバを起動しておく必要があります。Javaの場合だとWebコンテナの起動が必要です。Webコンテナの起動は、テストの実行時に自動的に行えた方がよいです。
Cucumber-JVMでは、Hooksという機能を使って、シナリオの実行前と実行後に任意のメソッドを実行することができます。使い方は簡単で、シナリオの実行前に実行したいメソッドには@Beforeアノテーションを指定し、シナリオの実施後に実行したいメソッドには@Afterアノテーションを指定します(JUnitの@Beforeと@Afterとはパッケージ名が違います)。
シナリオの実行前にコンテナを起動したい場合は、@Beforeを指定したメソッドでコンテナを起動する処理を記述すれば良いです。以下にサンプルを示します。
public class HooksSample { private static Context context; @Before public void before() throws Exception { if (context == null) { Tomcat tomcat = new Tomcat(); tomcat.setPort(8080); tomcat.setBaseDir("."); tomcat.getHost().setAppBase("."); tomcat.start(); context = tomcat.addWebapp(tomcat.getHost(), "/carapp", "WebContent"); } else { context.reload(); } } }
上記のサンプルでは、TomcatのAPIを呼出してコンテナを起動しています。シナリオ毎にTomcatを起動するのは時間がかかるため、1回目のシナリオの実行時にTomcatを起動し、2回目以降はアプリケーションをリロードするだけになっています(TomcatのAPIの使い方の説明は、今回の本筋ではないため割愛します)。
さいごに
Cucumber-JVMを使って、同じシナリオを、異なるレイヤーでテストできることを紹介しました。シナリオが軸になるため、テストコードの目的が統一され、精度とメンテナンス性が向上すると思います。
EclipseでXSD/DTDファイルを登録してタグ補完
EclipseのWTPに含まれるXMLエディタは、XMLファイルを編集する際、XSDやDTDファイルの内容に従ってタグや属性の補完をしてくれます。メジャーなXMLファイル(JPAのpersistence.xmlやSpring FrameworkのBean定義ファイルなど)は、EclipseがよしなにXSDやDTDファイルを読み込んでくれるため(※1)、特に設定をしなくてもタグ補完してくれます。しかし、そうではないXMLファイルは、EclipseにXSDファイルを明示的に登録する必要があります。
※1 Eclipseが事前にXSDやDTDファイル登録していたり、schemaLocationで指定された場所からXSDファイルを読み込んだりする
登録の仕方は簡単です。Eclipseの「Window」→「Preference」でダイアログ画面を開き、「XML」→「XML Catalog」を開きます。
「XML Catalog Entries」で「User Specified Entries」を選択し「Add」を押下します。ワークペースまたはファイルシステムからXSD/DTDファイルを選択したら、「Key:」に任意の文字列を指定し(XSDファイルにtargetNamespaceが指定されていたら、その値が自動的に入力されます)、「OK」を押下します。下の画面では、「shiporder.xsd」というXSDファイルを指定して、「Key:」に「shiporder」という文字列を指定しています(shiporder.xsdの中身は、こちらのサイトのものを使用しています)。
これで登録は完了です。後は、XMLファイルを編集する時に、「Key:」で指定した文字列をネームスペースで指定(XSDファイルを登録した場合)すれば、タグ補完ができます。
タグ補完ができると、XMLファイルの編集がし易くなる利点もありますが、記述可能なタグや属性が一覧できて、設定項目の理解が深まるという利点もあります。
参考
- Using the XML Catalog:http://wiki.eclipse.org/Using_the_XML_Catalog
- An XSD Example(shiporder.xsdの中身):http://www.w3schools.com/schema/schema_example.asp
Cucumber-JVMのHelloWorld
以前のブログでCucumber-JVMについて触れました。Cucumber-JVMは面白いプロダクトだし、使い方が非常に簡単なので、今回はCucumber-JVMのHelloWorldをブログに書きます。Cucumber-JVMの概要については以前のブログをご参照下さい。では、HelloWorldの手順を書いていきます。
Mavenでライブラリを取得
pom.xmlに依存ライブラリを追加します。
<dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-picocontainer</artifactId> <version>1.1.1</version> <scope>test</scope> </dependency> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-junit</artifactId> <version>1.1.1</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency>
シナリオを記述
Gherkin(ピクルス用の小さなキュウリを意味する)と呼ばれるフォーマットでシナリオを書きます。ファイル名の拡張子は、「.feature」にし、任意のパッケージに格納します。以下にシナリオのサンプルを示します。
Feature: 車の燃料の補給 運転手は、車を走らせるために、燃料を補給する必要がある。 Scenario: 燃料の補給 Given 車のタンクには、10リットルの燃料が入っている When 運転手は、50リットルの燃料を補給した Then タンクには、60リットルの燃料が入っている
「Senario:」がシナリオの始まりの部分です。その下にステップを記述します。
ステップの先頭のキーワードの種類には、Given、When、Then、And、Butがありますが、Cucumber-JVMにとって違いは関係ないので(ステップの順番にテストコードを実行するだけ)、使い分けはシナリオを記述する人の裁量で自由に行います。
また、1つの「.feature」ファイルに、複数のシナリオ(「Senario:」)を記述することができます。ファイルは任意のパッケージに配置します。ここでは、CarMaintenance.featureというファイル名でcom.genba.carパッケージに配置することにします。このHelloWorldでは、最終的には以下のようなファイル構成になります。
実行のためのクラスを作成
テストコードを記述クラスは後ほど紹介しますが、まずは、テストを実行するためだけのクラスを作成します。フィールドもメソッドも持たず、Annotationの@RunWithでCucumber.classを指定するだけです。このクラスを実行すると、パッケージ配下(サブパッケージ含む)のシナリオ(「.feature」ファイル)とテストコードが紐付けられてテストが実行されます(明示的に「.feature」ファイルとテストコードを指定することもできます)。以下にサンプルを示します。
package com.genba.car; import org.junit.runner.RunWith; import cucumber.api.junit.Cucumber; @RunWith(Cucumber.class) public class CarTest {}
実行のためのクラス(CarTestクラス)をJUnitで実行
まだテストコードを記述してませんが、ひとまずこの状態でCarTestクラスをJUnitで実行します。すると、コンソール画面に以下のメッセージが表示されます。
You can implement missing steps with the snippets below: @Given("^車のタンクには、(\\d+)リットルの燃料が入っている$") public void 車のタンクには_リットルの燃料が入っている(int arg1) throws Throwable { // Express the Regexp above with the code you wish you had throw new PendingException(); } @When("^運転手は、(\\d+)リットルの燃料を補給した$") public void 運転手は_リットルの燃料を補給した(int arg1) throws Throwable { // Express the Regexp above with the code you wish you had throw new PendingException(); } @Then("^タンクには、(\\d+)リットルの燃料が入っている$") public void タンクには_リットルの燃料が入っている(int arg1) throws Throwable { // Express the Regexp above with the code you wish you had throw new PendingException(); }
シナリオのステップに紐づかなかったテストコードの雛形が出力されています。
テストコードの雛形の作成
Cucumber-JVMでは、シナリオのステップごとにテストメソッドを作成します。先ほどコンソール画面で表示されたテストコードは、3つのテストメソッドが含まれており、それぞれ、シナリオのステップに該当しています。このテストメソッドのことをStep Definitionと呼び、Step Definitionを定義しているテストクラス群をひとまとめにしてStep Definitions(複数形のsが付く)と呼びます。まずは、この雛形をコピペしただけのテストクラスを、実行のためのクラス(CarTestクラス)のパッケージ配下(サブパッケージ含む)に作成します。ここではCarMaintenanceStepsクラスにします。以下にサンプルを示します。
package com.genba.car; import cucumber.api.java.en.Given; import cucumber.api.java.en.Then; import cucumber.api.java.en.When; import cucumber.runtime.PendingException; public class CarMaintenanceSteps { @Given("^車のタンクには、(\\d+)リットルの燃料が入っている$") public void 車のタンクには_リットルの燃料が入っている(int arg1) throws Throwable { // Express the Regexp above with the code you wish you had throw new PendingException(); } @When("^運転手は、(\\d+)リットルの燃料を補給した$") public void 運転手は_リットルの燃料を補給した(int arg1) throws Throwable { // Express the Regexp above with the code you wish you had throw new PendingException(); } @Then("^タンクには、(\\d+)リットルの燃料が入っている$") public void タンクには_リットルの燃料が入っている(int arg1) throws Throwable { // Express the Regexp above with the code you wish you had throw new PendingException(); } }
Annotationの中の文字列は正規表現になっています。Cucumber-JVMは、ステップとStep Definitionの紐付けを正規表現のマッチで行います。アノテーションの種類も関係ありません(例えば、ステップがWhenでStep Definitionが@Givenでも、正規表現がマッチすれば紐付けられます)。ちなみに、1つのシナリオが複数のテストクラスのStep Definitionを利用することもできるし、複数のシナリオが同じStep Definitionを利用することもできます。また、1つのステップが複数のStep Definitionにマッチしてしまうとエラーになります。「(\\d+)」のところはステップで可変で設定できる数字を表し、ステップで指定した値がテストメソッドの引数に設定されます。数字のほかにも、文字列や日付なども可変にすることができます(可変の値の種類についてはこちらを参照)。
この状態で、実行のためのクラス(CarTestクラス)を実行すると、PendingExceptionが発生し、コンソール画面で未実装のStep Definitionが出力されます。以下にメッセージを示します。
cucumber.runtime.PendingException: TODO: implement me at com.genba.car.CarMaintenanceSteps.車のタンクには_リットルの燃料が入っている(CarMaintenanceSteps.java:12) at ?.Given 車のタンクには、10リットルの燃料が入っている(com\genba\car\CarMaintenance.feature:5)
Step Definitionsの作成
Step Definitionsの中身を記述します。以下にサンプルを示します。テスト対象のCarクラスも合わせて示します。
package com.genba.car; import cucumber.api.java.en.Given; import cucumber.api.java.en.Then; import cucumber.api.java.en.When; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; public class CarMaintenanceSteps { private Car car; @Given("^車のタンクには、(\\d+)リットルの燃料が入っている$") public void 車のタンクには_リットルの燃料が入っている(int arg1) throws Throwable { car = new Car(arg1); } @When("^運転手は、(\\d+)リットルの燃料を補給した$") public void 運転手は_リットルの燃料を補給した(int arg1) throws Throwable { car.addFuel(arg1); } @Then("^タンクには、(\\d+)リットルの燃料が入っている$") public void タンクには_リットルの燃料が入っている(int arg1) throws Throwable { int actualFuelLevel = car.getFuelLevel(); assertThat(actualFuelLevel, is(arg1)); } }
package com.genba.car; public class Car { private Integer fuelLevel; public Car(int initialFuelLevel) { fuelLevel = initialFuelLevel; } public void addFuel(int addedFuel) { fuelLevel = fuelLevel + addedFuel; } public int getFuelLevel() { return fuelLevel; } }
継続的デリバリのスモールスタート
継続的デリバリにはさまざまな概念やツールが関係するため、いざ導入しようとするとどこから手をつければ良いのか迷うと思います。また、最初から何もかもやろうとすると、目的を見失って、意味のない作業に追われてしまう危険性が高いです。今回のブログでは、継続的デリバリの目的とスモールスタートについて私の考えを書きます。
継続的デリバリの目的(私の解釈)
継続デリバリは、アプリケーションを常に本番リリース可能な状態にするための営みです(こちらのサイトを参考)。そして、継続デリバリの考えは継続的インテグレーションが元になっています。継続的インテグレーションは、複数の開発者が作成・変更したソースコードを頻繁に統合してビルドする営みです(こちらのサイトを参考)。継続的インテグレーションの目的は、ソースコードの統合時の問題を早期に発見することだと言えます。継続デリバリは、ソースコードの統合とビルドに加え、デプロイと実行も頻繁に行います。継続的デリバリの目的は、アプリケーションのデプロイと実行時の問題を早期に発見することだと言えます。
スモールスタート
継続的デリバリの目的は、以下の2点を実施することでの実現できると思います。
- Trunkでの開発
- 要求レベルのテストの自動化
Trunkでの開発
継続的デリバリの実施には、継続的インテグレーションの実施は必然だと言えるでしょう。そして、継続的インテグレーションを実施するには、Trunkでの開発が大事です(詳しくはこちらを参照)。Trunkでの開発とは、SCMを運用するとき、Branchを作らずにTrunkだけで開発をすることを意味します。逆に、Trunkでの開発ができれば、後はTrunkを定期的にビルドするだけで、継続的インテグレーションができることになります。
要求レベルのテストの自動化
アプリケーションがデプロイできて実行できるかをチェックするには、本番環境に近い環境にデプロイしたアプリケーションに対して、要求レベルのテストを実施するのが良いでしょう。ただし、毎回手動で実施するのは時間がかかるため、自動化させたほうが良いです。デプロイの自動化はコマンドラインツールなど何かしら手段はあるでしょうし、要求レベルのテストの自動化も、さまざまなツールが存在します(ツールの例はこちらを参照)。なお、要求レベルのテストを自動で行っても、本番リリースするには手動のテストが必要だと思うので、継続的デリバリが目指す「本番リリース可能な状態」にはならないと思いますが、デプロイ・実行時に発生しうる問題を早期に発見するには十分でしょう。
さいごに
継続的デリバリを導入する際は、何もかも盛り込むのではなく、最初は上記2点を実施するための最小限のプロセスやツールに絞り、その他の部分は徐々に肉付けしていくのが良いと思います。
保守とTrunkと継続的インテグレーション
システムの保守の現場では、保守案件(機能追加や障害対応)ごとにBranchを作成するいわゆるFeature Branchを活用した運用をよく見かけます。
しかし、継続的インテグレーションのことを考えると、この運用は好ましくありません。
継続的インテグレーションとは、複数の開発者が作成・変更したソースコードを頻繁に統合してビルドすることです。これにより、統合時の問題(主に、開発者間のコミュニケーション不足による手戻り)を早期に発見できたり、大規模なマージ作業を避けることができます。
さきほどの図の場合だと、AのブランチとBのブランチのソースコードは、それぞれが完成するまで統合されないので継続的インテグレーションが出来ません。
では、どうすればいいかというと、保守案件が複数あっても、Branchを作成せずに、原則としてTrunkだけで開発すればよいのです。
そして、Trunkのソースを頻繁にビルドすれば、継続的インテグレーションが実現できます。
このとき問題となるのが、Aの案件をリリースするときに、開発中のBの案件のソースコードが混じってしまうことです。そうなると、開発中の画面がユーザに表示されてしまったり、バグのある処理が実行される危険性があります。
この危険性を防ぐため、開発中のソースコードの機能が実行されないような工夫が必要となります。工夫の仕方はいろいろあると思いますが、良いと思うのは、開発中のソースコードの機能を設定ファイルでON/OFFすることです。設定ファイルのON/OFFで、画面のメニューの表示/非表示を切り替えたり、プログラム内にフラグを持たせてロジックを切り替えたりします(この辺りの話は、こちらのサイトが参考になります)。こうすれば、リリース時は開発中のソースコードの機能を隠すことができるし、テスト時には設定ファイルを書き換えるだけで開発中のソースコードの機能をテストできます。
さいごに
どんな保守案件もtrunkを使って開発すべきとは言いませんが、trunkで開発できる保守案件(機能のON/OFFが容易にできる案件)は多いと思います。継続的インテグレーションのメリットを考えれば、原則としてtrunkを使って開発し、継続的インテグレーションを導入するのが良いと思います。