hidemium's blog

日々学んだことをアウトプットする。

リポジトリ共有式Pull Requestの手順について

GitHubを使った開発の方法であるリポジトリ共有式のPull Requestの手順を整理してみました。

全体の流れ

大まかに以下のような手順になります。

  • ローカルにclone
  • ブランチ作成 (名前はissue[Issueの番号]とか※)
  • 修正完了後、Push
  • masterにプルリク作成
  • 確認したらmasterにマージ

※本家GitHub Flowでは説明的なブランチを作成するようですが。

ソースコードの取得

新たにcloneする場合

$ git clone <リポジトリ>  #ソースコードを取得

以前にcloneをしたことがある場合

$ git checkout master                     #最新のリモートリポジトリを取得
$ git branch -a                           #ブランチを確認
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
$ git pull     #手元のソースコードを最新化

開発作業の流れ

1. 以下のコマンドでブランチを作成し、 リモートにpushします。

$ git branch     現在のブランチを確認
* master
$ git checkout -b issue1               #Pull Request用ブランチを作成
$ echo "# Hello, Pull Request" > README.md     #見出しを変更
$ git add README.md                            #変更をadd
$ git commit -m "Update README.md"             #変更をcommit
$ git push origin issue1                #リモートにpush

2. リポジトリを開いて、Pull Requestsをクリックします。
3. New pull requestをクリックします。
4. ブランチを指定して、Create pull requestをクリックし、masterに対してプルリクを実行します。

Pull Requestの確認

1. 以下のコマンドでチェックアウトし、 動作を確認します。

$ git fetch                                #変更を取得
$ git branch -a                            #ブランチを確認
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
  remotes/origin/issue1
$ git checkout -b issue1 origin/issue1     #チェックアウト
#ここから動作確認

2. リポジトリを開いて、Pull Requestsをクリックします。
3. Mergeをクリックし、masterに対してマージを実行します。

ブランチの削除

マージが出来たら、もうブランチが不要になるので消しておきます。
リモートブランチの削除はプルリクエストの画面からもできます。

$ git checkout master
$ git branch -d issue1      #ローカルブランチの削除
$ git push origin :issue1   #リモートブランチの削除

マージ後にリモートブランチが消えてもgit branch -aに出てくる場合は、以下のコマンドを実行します。

$ git fetch --prune

最初に戻る

ソースコードの取得」に戻り、手元のソースコードを最新化します。

トピックブランチで作業中に、master ブランチで変更があった場合

作業中のトピックブランチにいる状態で以下のコマンドを実行します。

$ git merge master

Play Framework(Java)でEbeanを使ってManyToManyのリレーションを行う

この記事は Play framework Advent Calendar 2014の 14 日目です。

前回、Ebeanを使ってリレーションを持つテーブルの操作を試してみました。しかし、多対多のリレーションについて、更新時に関係付けしたインスタンスが削除される事象が発生し、しばらくはまってしまいました。今回は、その時に確認した内容についてまとめました。*1

構成

サンプル用のアプリについて

ソースコードhideakihal/play-ebean-relation · GitHubで公開しています。

ManyToManyを試すために以下のの2つのModelを用意しました。

  • Message・・・メッセージを管理
  • Member・・・ユーザ名を管理

models パッケージに,Message.javaとMember.javaを作成し、以下のように@ManyToManyのアノテーションを追加します。

Message.java

@ManyToMany(cascade = CascadeType.ALL)
public List<Member> members;

Member.java

@ManyToMany(mappedBy="members", cascade = CascadeType.ALL)
public List<Message> messages;

controller パッケージに,以下のようにApplication.javaとContacts.javaを作成します。

Application.java

Messageインスタンスを作成、更新するためにApplication.javaを用意します。CRUD処理で必要なcreateメソッド、updateメソッド、deleteメソッドは以下通りです。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		List<String> names = data.name;
		for (String nm : names){
			Member m = Member.findByName(nm);
			data.members.add(m);
		}
 		data.save();
public static Result update() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		List<String> names = data.name;
		for (String nm : names){
			Member m = Member.findByName(nm);
			data.members.add(m);
		}
 		data.update();
public static Result delete(Long id) {
	Form<Message> f = new Form(Message.class).fill(
		Message.find.byId(id)
	);
	if (!f.hasErrors()) {
		Message data = f.get();
		if(data != null){
			data.delete();

Contacts.java

Memberインスタンスを作成、更新するためにContacts.javaを用意します。CRUD処理で必要なcreateメソッド、updateメソッド、deleteメソッドは以下通りです。

public static Result create() {
	Form<Member> f = new Form(Member.class).bindFromRequest();
	if (!f.hasErrors()) {
		Member data = f.get();
		data.save();
public static Result update() {
	Form<Member> f = new Form(Member.class).bindFromRequest();
	if (!f.hasErrors()) {
		Member data = f.get();
		data.update();
public static Result delete(Long id) {
	Form<Member> f = new Form(Member.class).fill(
		Member.find.byId(id)
	);
	if (!f.hasErrors()) {
		Member data = f.get();
		if(data != null){
			data.delete();

サンプルアプリの実行画面

実行画面は以下のようになっています。MessageとMemberに対して作成、読み込み、更新、削除ができるCRUDのアプリになります。

以下は右サイドバーのMemberをクリックした画面になります。messagesには関係付けたMessageインスタンスのIDが表示されます。

f:id:hidemium:20141214100433p:plain

Member側からユーザ名のみを登録することができ、以下はMember>+addをクリックした画面になります。

f:id:hidemium:20141214100421p:plain

以下は右サイドバーのMesssageをクリックした画面になります。memberには関係付けたMemberインスタンスの名前が表示されます。

f:id:hidemium:20141214100341p:plain

Messsage側からはMemberを複数登録できることができ、以下はMesssage>+addをクリックした画面になります。

f:id:hidemium:20141214100358p:plain

問題になったこと

Message側からの作成、更新、削除は問題ありませんでしたが、Member側からMemberインスタンスの更新を行った際に、関係付けたMessageインスタンスが削除されてしまう事象が発生しました。

Model Create Update Delete Annotation
Message @ManyToMany(cascade = CascadeType.ALL)
Member × @ManyToMany(mappedBy="members", cascade = CascadeType.ALL)

画面イメージとして、以下はMember>+editをクリックした画面になります。
以下のようにMemberのpasswordを変更して更新を行います。

f:id:hidemium:20141214113111p:plain

Memberの一覧を見ると、messagesにてMessageのidが削除されていることが分かります。

f:id:hidemium:20141214113125p:plain

原因について

当初は、cascadeやmappedByといったアノテーションの指定方法に問題があるかと思い、いくつかバリエーションを試してみましたが、問題は解決しませんでした。

次に、Memberインスタンスの更新には、Contacts.javaのupdateメソッドを使用しているため、updateメソッド注目してどこでMessageインスタンスが消失するか確認しました。
updateメソッドでは、bindFromRequestでフォームから渡された値を保管するFormインスタンスを作成し、FormインスタンスからMemberインスタンスを取得しています。

public static Result update() {
	Form<Member> f = new Form(Member.class).bindFromRequest();
	if (!f.hasErrors()) {
		Member data = f.get();
		data.update();

このMemberインスタンスの値を確認したとろ、すでにMessageインスタンスが消失した状態となっていました。つまり、bindFromRequestはフォームに入力されてpostされたものしか取得されないため、関係レコードのオブジェクトは取得できないことが原因となっていました。当初は、bindFromRequestで関係レコードのオブジェクトも取得できると勘違いしていたため原因をつかむのに時間がかかってしまいました。

解決方法について

原因は判明しましたが、すでに関係付けたMessageインスタンスをどのように取得するか悩みました。例えば、View側で関係付けたMessageインスタンスを一旦IDにし、IDの配列として渡す方法など考えましたが、あまりいい方法とも思えませんでした。最終的に、MemberのIDからfindByIdでMemberインスタンスを取得することで、更新前に関係付けたMessageインスタンスが取得することが確認できたため、以下のような実装を行いました。

Contacts.java

public static Result update() {
	Form<Member> f = new Form(Member.class).bindFromRequest();
	if (!f.hasErrors()) {
		Member data = f.get();
		Member name = Member.findById(data.id.toString());
		List<Message> arr = name.messages;
		for(Message m : arr){
			data.messages.add(m);
		}
		data.update();

Member.findById(data.id.toString())で更新前に関係付けたMessageインスタンスを取得しています。また、cascade = CascadeType.ALLを使用しているため、messages側からaddするだけで自動的に更新を行うことができます。

Member.javaのfindByIdメソッドは以下のように作成します。

Member.java

public static Member findById(String input){
	return Member.find.where().eq("id", input).findList().get(0);
}

細かな実装については、hideakihal/play-ebean-relation · GitHubを確認してみてください。

おわりに

Ebeanを使ったManyToManyのリレーションについて、更新処理が上手くできずにはまっていましたが、なんとか解決することができました。今回の問題は、Stack Overflowに似たような問題が上がっていましたが、欲しい情報が見つからず解決に時間がかかってしまいました。
今回の記事がみなさんのお役に立てられればと思います。

*1:あくまで個人的に検証した結果であるため、正しい実装ではないかもしれません。よい方法があればどなかた教えてください。

Play Framework(Java)でEbeanを使ってリレーションを持つテーブルを操作する

前回、EBeanを使ったデータベースの操作を試してみましたが、Ebeanには複数のModelを関連付けて処理する仕組みも用意されています。Modelクラスのフィールドにアノテーションを付けることで、1対1、1対多、多対多といった関係付けを行うことができます。今回は、Ebeanを使ってリレーションを持つテーブルの操作について試してみました。

構成

Modelの連携の種類について

Modelの連携には以下の種類があります。

  • OneToOne(1対1)
  • OneToMany(1対多)、ManyToOne(多対1)
  • ManyToMany(多対多)

サンプル用のModelについて

それぞれの連携を説明するために、今回以下の2つのModelを用意しました。

  • Message・・・メッセージを管理
  • Member・・・ユーザ名を管理

まずはmodels パッケージに,以下のようにMessage.javaとMember.javaを作成します。

Message.java
package models;

import java.util.*;
import javax.persistence.*;
import javax.validation.*;
import com.avaje.ebean.*;
import play.db.ebean.*;
import play.data.validation.*;

@Entity 
public class Message extends Model {

  @Id
  public Long id;
  
  @Required
  public String name;

  @Required
  public String message;
  
  @CreatedTimestamp
  public Date postdate;
	
  public static Finder<Long, Message> find = 
    new Finder<Long, Message>(Long.class, Message.class);
}
Member.java
package models;

import java.util.*;
import javax.persistence.*;
import javax.validation.*;
import com.avaje.ebean.*;
import play.db.ebean.*;
import play.data.validation.*;

@Entity
public class Member extends Model {

  @Id
  public Long id;

  @Required
  public String name;

  @CreatedTimestamp
  public Date postdate;

  public static Finder<Long, Member> find = 
    new Finder<Long, Member>(Long.class, Member.class);
}

OneToOne

OneToOneは、あるModelのエンティティ1つに対して、別のModelのエンティティ1つが対応する関係です。

Message.java

Messageインスタンスに1つのMemberインスタンスが関連するようにするには、Message.javaに以下の内容を追加します。@OneToOneというアノテーションが、OneToOneの関係を指定するものになります。

@OneToOne(cascade = CascadeType.ALL)
public Member member;

エンティティと関連付けられたエンティティに対する処理を自動で行う場合はCascadeTypeを指定します。cascade = CascadeType.ALLを追加することで、参照や更新などのすべての操作に対して処理を自動で行うことができるようになります。この設定がない場合は、操作ごとにApplication.javaに処理を書く必要があります。

Member.java

ユーザ名からMemberインスタンスを取得できるように、Member.javaにfindByIdメソッドを追加します。

public static Member findById(String input){
	return Member.find.where().eq("id", input).findList().get(0);
}

Application.java

Application.javaに以下のようにfindByIdを追加します。findByIdでMemberインスタンスを取得し、saveによりMessageインスタンスに保存するだけで関連付けをすることができます。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		data.member = Member.findById(data.name);
		data.save();
		return redirect("/");
	} else {
		return badRequest(add.render("ERROR",f));
	}
}

Evolutionを実行すると、以下のDLLが自動的に作成されます。Message.javaに追加したmemberがmember_idとして作成されていることが分かります。

create table message (
  id                        bigint not null,
  name                      varchar(255),
  message                   varchar(255),
  member_id                 bigint,
  postdate                  timestamp not null,
  constraint pk_message primary key (id))
;

OneToMany、ManyToOne

OneToManyは、あるModelのエンティティ1つに対して、別のModelの複数のエンティティが対応する関係です。ManyToOneは、逆にあるModelの複数のエンティティが、別のModelのエンティティ1つが対応します。
Memberインスタンス複数のMessageインスタンスが関連するようにするには、Message.javaとMember.javaに以下の内容を追加します。@OneToMany、@ManyToOneというアノテーションが、OneToMany、ManyToOneの関係を指定するものになります。

Message.java

@OneToMany
public Member members;

Member.java

@ManyToOne(cascade = CascadeType.ALL)
public List<Message> messages;

public static Member findById(String input){
	return Member.find.where().eq("id", input).findList().get(0);

こちらでも、エンティティと関連付けられたエンティティに対する処理を自動で行うため、cascade = CascadeType.ALLを指定しておきます。

Application.java

Application.javaに以下のようにfindByIdを追加します。findByIdでMemberインスタンスを取得し、saveによりMessageインスタンスに保存するだけで関連付けをすることができます。
Messageインスタンスの操作しかしていませんが、Memberインスタンスも自動的に反映されるため処理を追加する必要はありません。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		data.member = Member.findById(data.name);
		data.save();
		return redirect("/");
	} else {
		return badRequest(add.render("ERROR",f));
	}
}

Evolutionを実行すると、DLLが自動的に作成されますが、OneToManyやManyToOneでは変化はありません。

ManyToMany

ManyToManyは、あるModelのの複数のエンティティに対して、別のModelの複数のエンティティが対応する関係です。
複数のMemberインスタンス複数のMessageインスタンスが関連するようにするには、Message.javaとMember.javaに以下の内容を追加します。@ManyToManyというアノテーションが、ManyToManyの関係を指定するものになります。Message.javaには、複数のMemberインスタンスを格納するために、配列を追加しました。

Message.java

public List<String> name;

@ManyToMany(cascade = CascadeType.ALL)
public List<Member> members;

Member.java

@ManyToMany(mappedBy="members", cascade = CascadeType.ALL)
public List<Message> messages;

public static Member findById(String input){
	return Member.find.where().eq("id", input).findList().get(0);

こちらでも、エンティティと関連付けられたエンティティに対する処理を自動で行うため、cascade = CascadeType.ALLを指定しておきます。
また、ManyToManyではエンティティの主従関係を明示的にするためmappedByを指定しています。mappedBy="members"は、Messageが主、Memberが従であることを示しています。また、mappedByを指定しない場合、エンティティと関連付けした状態でインスタンスを削除をすると参照整合性制約違反が発生するため、自動的に処理をするには指定したほうがいいかもしれません。

Application.java

MemberのIDの配列を取得し、それらのIDからfindByIdでMemberインスタンスを取得しています。取得した複数のMemberインスタンスをmembersにaddにより追加します。saveによりMessageインスタンスに保存し関連付けをすることができます。Messageインスタンスの操作しかしていませんが、Memberインスタンスも自動的に反映されるため処理を追加する必要はありません。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
    		List<String> names = data.name;
    		for (String nm : names){
    			Member m = Member.findById(nm);
    			data.members.add(m);
    		}
		data.save();
		return redirect("/");
	} else {
		return badRequest(add.render("ERROR",f));
	}
}

Evolutionを実行すると、以下のDLLが自動的に作成されます。MessageとMemberの関係テーブルがmessage_memberとして作成されていることが分かります。

create table message_member (
  message_id                     bigint not null,
  member_id                      bigint not null,
  constraint pk_message_member primary key (message_id, member_id))
;

Play Framework(Java)でEbeanを使ってデータベースを操作する

Play FrameworkにはEbeanというORMが標準搭載されており、Ebeanを継承したModelクラスを作成することでデータベースを操作することができます。今回は、Ebeanを使ったデータベースの操作ついて試してみました。

構成

データベース接続の準備

application.confの設定

H2でデータベースを利用するには、application.confの設定を行います。confフォルダ内のapplication.confが対象となります。
application.confに以下の内容を追加します。

db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play"
ebean.default="models.*"

上の2行で、JDBCドライバと、H2のメモリ上にデータが格納されるDBインスタンス、最後の1行で、EBeanを使用する設定を行っています。

モデルの作成

Modelクラスは、play.db.ebean.Modelというクラスとして用意されています。
app/models パッケージに,play.db.ebean.Model を継承したModelクラスを作成することで、Modelクラスを使用できます。app/models パッケージに,以下のようにMessage.javaを作成します。

package models;

import java.util.*;
import javax.persistence.*;

import play.db.ebean.*;
import play.data.format.*;
import play.data.validation.*;
import play.data.validation.Constraints.*;

@Entity 
public class Message extends Model {

  @Id
  public Long id;
  
  @Constraints.Required
  public String name;

  public String mail;

  @Required
  public String message;
  
  @CreatedTimestamp
  public Date postdate;
	
  public static Finder<Long, Message> find = 
    new Finder<Long, Message>(Long.class, Message.class);
}

Modelクラスには、@がついたコードがありますが、これはJavaオブジェクトとデータベースのテーブルの間の関係を定義するアノテーションというものです。
@Entityは、このクラスがエンティティであることを示します。
@idは、記述したフィールドをプライマリキーであることを示します。
@Requiredは、入力必須項目であることを示しています。
@CreatedTimestampは、作成時の時間を格納することを示しています。

コントローラーの作成

controllersパッケージの配下にApplication.javaに以下のようなcreateメソッドを作成します。bindFromRequestのところでフォームから渡された値を保管するFormインスタンスを作成しています。FormインスタンスからMessageインスタンスを取得し、saveを呼び出すことでインスタンスが保存されます。一見インスタンスを操作しているだけですが、実際にはEBeanによりデータベースのテーブルに必要な値が保存されます。
※routersやviewについての説明は省いています。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		data.save();
		return redirect("/");
	} else {
		return badRequest(add.render("ERROR",f));
	}
}

Evolutionの実行

Play frameworkはEvolutionというマイグレーションシステムを持っており、Modelを作成し、ブラウザからアクセスすると、Evolutionを実行するか確認する画面が表示されます。Evolutionを実行すると、自動的に作成されたDLLが実行されmessageテーブルを作成してくれます。DLLはconf/evolutions/default/1.sqlに作成され、messsegeテーブルのcreateは以下のように作成されます。

create table message (
  id                        bigint not null,
  name                      varchar(255),
  mail                      varchar(255),
  message                   varchar(255),
  postdate                  timestamp not null,
  constraint pk_message primary key (id))
;

デフォルトの状態では、モデル構成が変更される度にデータベースをクリーンする動作になっているため、この機能を停止するには1.sqlで以下の箇所を削除します。

# To stop Ebean DDL generation, remove this comment and start using Evolutions

Play Framework(Java)の開発環境を構築する

Play Frameworkという、Scala/Javaで書かれたWebアプリケーションフレームワークがあります。Play Frameworkは,Ruby on Railsから影響を受けており、生産性の向上を目的として開発されています。今回は、Play Frameworkの開発環境の構築を行ってみました。

構成

Play Frameworkの特徴

Play Frameworkのインストールに入る前に、Play Frameworkの特徴についてまとめてみました。

コーディングのしやすさ、DB操作のしやすさ、開発環境の導入のしやすさなど、生産性を向上させるための工夫がいくつも取り入れられています。

インストール

Java

Java SE Downloadsにアクセスします。Java 8とJava 7がダウンロード可能ですが、今回はJava 7をダウンロードします。
Java Platform, Standard Edition>Java 7uXXにて、JDKの下のDownloadボタンをクリックします。
Java SE Development Kit 7uXXにて、「Accept License Agreement」をチェックし、「jdk-7uXX-windows-x64.exe」をクリックしてダウンロードします。

jdk-7uXX-windows-x64.exe」をクリックして、Javaをインストールします。
インストール時の設定はデフォルトのままで良いかと思います。

Javaの実行ファイルへ環境変数PATHを設定します。

C:\Program Files\Java\jdk1.7.0_55\bin;

インストール後に、コマンドプロンプトにて以下のコマンドを実行し、Javaがインストールされていることを確認します。

> java -version
java version "1.7.0_55"
Java(TM) SE Runtime Environment (build 1.7.0_55-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)

Play Framework

Play frameworkの公式サイトにアクセスします。
最新バージョンはここからダウンロードできますが、旧バージョンはこちらからダウンロードします。
「play-2.2.3.zip」をクリックしてダウンロードします。

ダウンロードした圧縮ファイルを解凍し、任意のディレクトリに配置します。

C:\Users\<your-user-name>\tools\play-2.2.3

Playの実行ファイルへ環境変数PATHを設定します。

C:\Users\<your-user-name>\tools\play-2.2.3;

インストール後に、コマンドプロンプトにて以下のコマンドを実行し、Playがインストールされていることを確認します。

> play help
       _
 _ __ | | __ _ _  _
| '_ \| |/ _' | || |
|  __/|_|\____|\__ /
|_|            |__/

play 2.2.3 built with Scala 2.10.3 (running Java 1.7.0_55), http://www.playframe
work.com
Welcome to Play 2.2.3!

These commands are available:
-----------------------------
license            Display licensing informations.
new [directory]    Create a new Play application in the specified directory.

You can also browse the complete documentation at http://www.playframework.com.

Eclipse

Pleiadesにアクセスし、Eclipseの本体と日本語化プラグインがセットになったPleiades All in Oneをダウンロードします。
Eclipse 4.3 Kepler」をクリックしてダウンロードします。

ダウンロードした圧縮ファイルを解凍し、任意のディレクトリに配置します。

C:\Users\<your-user-name>\tools\eclipse-4.3

Play frameworkアプリの起動

Play frameworkの新規アプリケーションを作成します。
play newコマンドの後に、アプリケーション名を付けて作成します。このあたりはRailsと似ています。

> cd C:\Users\<your-user-name>\dev
> play new PlayApp
       _
 _ __ | | __ _ _  _
| '_ \| |/ _' | || |
|  __/|_|\____|\__ /
|_|            |__/

play 2.2.3 built with Scala 2.10.3 (running Java 1.7.0_55), http://www.playframe
work.com

The new application will be created in C:\Users\your-user-name\dev\PlayApp

アプリケーション名を聞かれますが、変更はしないのでここはEnterキーを押しておきます。

What is the application name? [PlayApp]
> [Enterキー]

ScalaJavaのどちらで開発するか聞かれますが、今回はJavaの開発環境を構築するので2を入力して、Enterキーを押します。

Which template do you want to use for this new application?

  1             - Create a simple Scala application
  2             - Create a simple Java application

> 2 [Enterキー]
OK, application PlayApp is created.

Have fun!

アプリケーションが起動できるか確認しておきます。
また、Playのエラーが表示されるときの文字化けを防ぐために、環境変数_JAVA_OPTIONSの設定を行っておきます。

> set _JAVA_OPTIONS="-Dfile.encoding=SJIS"
> cd C:\Users\<your-user-name>\dev\PlayApp
> play run
:
--- (Running the application from SBT, auto-reloading is enabled) ---

[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000

(Server started, use Ctrl+D to stop and go back to the console...)

ブラウザから、以下のアドレスにアクセスすると、playのデフォルトページに接続することができます。

http://localhost:9000

Ctrl+Dキーでアプリケーションを停止することができます。

Eclipseプロジェクトへのインポート

Eclipseで開発する場合、Eclipseのプロジェクトに変換する必要があります。
以下のコマンドを実行して、Eclipseのプロジェクトに変換します。

> play eclipse

Eclipseを起動し、メニューのファイル>インポートをクリックします。
「既存プロジェクトをワークスペースへ」を選択して、次へをクリックします。
「ルートディレクトリの選択」に作成したアプリケーションのパスを入力します。
「プロジェクト」に作成したアプリケーションが表示されるので、完了をクリックします。
Eclipse上にアプリケーションのプロジェクトが表示されます。

DockerでKandanとHubotを動かす

チャット上のHubotを通じて運用を自動化するChatOpsという運用スタイルが注目されています。ローカルでチャット上で動くHubotを試すために、HipChatクローンのKandanとHubotを動作するDockerfileを書いてみました。

構成

  • Ubuntu 14.04
  • Docker 1.3.0
  • Kandan 1.2
  • hubot 2.4.7
  • hubot-kandan adapter 1.0

Dockerfile

Dockerfileを以下のように作成します。
ソースコードこちら@githubで公開しています。

Dockerfileの構成

Dockerfileの構成は以下の通りです。

docker-kandan-hubot
├──Dockerfile
├──hubot
│     ├──hubot-scripts.json
│     ├──hubot.conf
│     ├──hubot.sh
│     └──package.json
├──kandan
│     ├──database.yml
│     └──kandan.conf
├──sources.list
└──sshd.conf

Dockerfile

Dockerfile以外にもいくつかファイルはありますが、今回はDockerfileのみ説明します。

$ docker-kandan-hubot
$ vi Dockerfile
FROM ubuntu:14.04

MAINTAINER hideakihal

# install basic package
ADD sources.list /etc/apt/sources.list
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && \
    apt-get -y install \
    openssh-server \
    supervisor \
    build-essential \
    curl \
    unzip \
    git-core \
    ruby1.9.1-dev \
    ruby-bundler \
    libxslt-dev \
    libxml2-dev \
    libpq-dev \
    libsqlite3-dev \
    gcc \
    g++ \
    make && \
    curl -sL https://deb.nodesource.com/setup | bash - && \
    apt-get install -y nodejs && \
    apt-get clean

# root user
RUN echo 'root:root' | chpasswd

# install sshd
RUN mkdir -p /root/.ssh /var/run/sshd
RUN sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config
RUN sed -ri 's/#UsePAM no/UsePAM no/g' /etc/ssh/sshd_config
RUN sed -ri 's/PermitRootLogin without-password/PermitRootLogin yes/g' /etc/ssh/sshd_config

# hubot user
RUN useradd -m -s /bin/bash hubot 
RUN echo 'hubot:hubot' | chpasswd
RUN echo 'hubot ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/hubot
ENV HOME /home/hubot

# install Kandan
USER hubot
WORKDIR /home/hubot
RUN wget https://github.com/kandanapp/kandan/archive/v1.2.tar.gz
RUN tar xvf v1.2.tar.gz
RUN mv kandan-1.2/ kandan
RUN echo 'gem: --no-rdoc --no-ri' >> /home/hubot/.gemrc
WORKDIR /home/hubot/kandan
RUN sudo gem install execjs
RUN sudo sed -ri "s/gem 'pg'/gem 'sqlite3'/g" /home/hubot/kandan/Gemfile
RUN bundle install --without development test
ADD kandan/database.yml /home/hubot/kandan/config/database.yml
RUN RAILS_ENV=production bundle exec rake db:create db:migrate kandan:bootstrap
RUN sed -ri 's/config.serve_static_assets = false/config.serve_static_assets = true/g' \
    /home/hubot/kandan/config/environments/production.rb
RUN RAILS_ENV=production bundle exec rake assets:precompile
RUN RAILS_ENV=production bundle exec rake kandan:boot_hubot
RUN RAILS_ENV=production bundle exec rake kandan:hubot_access_key | awk '{print $6}' > hubot-key

# install Hubot
WORKDIR /home/hubot
RUN wget https://github.com/github/hubot/archive/v2.4.7.zip
RUN unzip v2.4.7.zip
WORKDIR /home/hubot/hubot-2.4.7
RUN sudo npm install -g mime@1.2.4 qs@0.4.2
RUN npm install
RUN make package
WORKDIR /home/hubot/hubot-2.4.7/hubot
RUN git clone https://github.com/kandanapp/hubot-kandan.git node_modules/hubot-kandan
RUN npm install faye
RUN npm install ntwitter
ADD hubot/package.json /home/hubot/hubot-2.4.7/hubot/package.json
ADD hubot/hubot-scripts.json /home/hubot/hubot-2.4.7/hubot/hubot-scripts.json
RUN sed -ri 's/"version":     "1.0",/"version":     "1.0.0",/g' node_modules/hubot-kandan/package.json
USER root
ADD hubot/hubot.sh /etc/profile.d/hubot.sh
RUN awk '{print "export HUBOT_KANDAN_TOKEN="$0}' /home/hubot/kandan/hubot-key >> /etc/profile.d/hubot.sh 

# add supervisor config file 
RUN mkdir -p /var/log/supervisor /etc/supervisor/conf.d
ADD sshd.conf /etc/supervisor/conf.d/sshd.conf
ADD kandan/kandan.conf /etc/supervisor/conf.d/kandan.conf
ADD hubot/hubot.conf /etc/supervisor/conf.d/hubot.conf
RUN awk '{print " HUBOT_KANDAN_TOKEN="$0}' /home/hubot/kandan/hubot-key >> /etc/supervisor/conf.d/hubot.conf 

# expose ports
EXPOSE 22 3000

# define default command
CMD supervisord -n
  • 最新のKandanだと、Hubotからうまく認証できないため、Kandan 1.2を使用します。
  • KandanのDBにはsqliteを使用しています。
  • hubotのアクセスキーは一度ファイルに出力し、supervisorの定義ファイルで環境変数を設定するために使用しています。
  • hubot-kandan adapterは、最新のhubotには対応していないため、hubot 2.4.7を使用します。

インストール

GitHubにあるソースコードを取得して、Dockerイメージをビルドします。

$ git clone https://github.com/hideakihal/docker-kandan-hubot.git
$ sudo docker build -t kandan-hubot docker-kandan-hubot

コンテナを起動します。KandanとSSHの使用ポートである3000と22を起動時に指定します。

$ sudo docker run -d -p 22 -p 3000:3000 kandan-hubot

ブラウザから、以下のアドレスにアクセスすると、Kandanのログイン画面に接続することができます。

http://<your-host-ip>:3000

f:id:hidemium:20141103012204p:plain

デフォルトでユーザ名「Admin」、パスワード「kandanappadmin」でログインできます。
ログインすると、Hubotがチャットに参加していることが確認できます。

f:id:hidemium:20141103012459p:plain

動作確認のため、「@hubot ping」や「@hubot pug me」などを投稿すると、hubotが応答してくれます。

f:id:hidemium:20141103012800p:plain

Hubotスクリプトの追加

Hubotのスクリプトを追加した場合、Hubotの再起動が必要になります。今回supervisorによりHubotを起動しているため、以下のコマンドを実行し、Hubotを再起動します。

$ ssh root@localhost -p <your-container-22port>
password:root
# supervisorctl restart hubot

おわりに

Dockerを使ってKandan上でHubotを動かす環境を作ることができました。ローカルで試してみたい場合や、Hubotのスクリプトをチャット上で検証してみたい場合などに使えるか思います。

※SlackとKandanを比較した場合、Slackの出来がいいため、どうしてもという理由がない限りSlackを使ったほうがいいようです。

ShipyardでDockerのWebUIを構築してみた

Dockerでどのようなコンテナが起動しているか、コンテナの状態を確認するためにWebUIを提供するツールがいくつか開発されています。今回は、Shipyardをインストールし、コンテナの状態がどのように確認できるのか試してみました。

構成

  • Ubuntu 14.04
  • Docker 1.3.0
  • Shipyard 2.0.3

Shipyardのインストール

Dockerデーモンの外部接続を許可する必要があるので、/etc/default/dockerのDOCKER_OPTSに以下の内容を追加します。

$ vi /etc/default/docker
DOCKER_OPTS='-H tcp://0.0.0.0:4243/ -H unix:///var/run/docker.sock'

設定を反映させるため、Dockerを再起動します。

$ service docker restart

ShipyardはRethinkDBという分散データベースを使用しているので、こちらをインストールします。上のコマンドは、RethinkDB用のデータ格納コンテナ、下のコマンドはRethinkDBコンテナを起動しています。

$ docker run -it -d --name shipyard-rethinkdb-data --entrypoint /bin/bash shipyard/rethinkdb -l
$ docker run -it -P -d --name shipyard-rethinkdb --volumes-from shipyard-rethinkdb-data shipyard/rethinkdb

Shipyardコンテナを起動します。今回はソケット通信によりホストの情報を取得するため、「-v /var/run/docker.sock:/docker.sock」を追加します。

$ docker run -it -v /var/run/docker.sock:/docker.sock -p 8080:8080 -d --name shipyard --link shipyard-rethinkdb:rethinkdb shipyard/shipyard

これらのコマンドを実行すると、DockerHubからイメージを取得して、コンテナを起動してくれます。※なお、Shipyard 2からはAgentは不要になったようです。

ブラウザから、以下のアドレスにアクセスすると、ShipyardのWebUIに接続することができます。

http://<your-host-ip>:8080

f:id:hidemium:20141026191535p:plain

デフォルトでユーザ名「admin」、パスワード「Shipyard」でログインできます。

Engineの追加

初回ログイン時はDockerのホスト情報が登録されていないので、登録を行います。
「Engines」タブをクリックして、「ADD」ボタンをクリックします。
Name、Label、CPUs、Memoryは任意の値を入力し、Addressに「unix:///docker.sock」を入力し、「ADD」ボタンをクリックします。

f:id:hidemium:20141026195815p:plain

登録ができると、以下のようにホスト情報が表示されます。

f:id:hidemium:20141026195853p:plain

Containerの操作

設定に問題がなければ、「Containers」タブをクリックすると、以下のように起動中のコンテナの一覧が表示されます。また、コンテナをUIから起動する、「Deploy」ボタンが用意されています。

f:id:hidemium:20141026195934p:plain

WebUIを提供しているshipyard/shipyardのコンテナをクリックすると、以下のように表示されます。restart、stop、destroy、scaleといったボタンが用意されています。

f:id:hidemium:20141026200105p:plain

コンテナの一覧画面に戻って、「Deploy」ボタンをクリックすると、以下のような画面になります。Dockerのイメージ名やホスト名、CPUやメモリのリソースの制限について設定することができます。

f:id:hidemium:20141026200114p:plain

Dashbordの操作

EngineとContainerで設定した値を元に、CPUやメモリのリソースの利用状況を円グラフで表示してくれます。
※Dockerが稼動しているサーバのCPU使用率やメモリ使用率をリアルタイムで表示するものではないようです。(このあたりの仕様はまだよく分かってません。)

f:id:hidemium:20141026200123p:plain

おわりに

通常コンテナの状況はCUIベースで確認していますが、複数ユーザでどのようなコンテナが起動しているか確認するにはShipyardがあると便利かと思います。ただ、WebUIベースで細かなことするにはまだまだ難しいかもしれません。