hidemium's blog

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

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ベースで細かなことするにはまだまだ難しいかもしれません。

Figで複数のDockerコンテナをまとめて起動して、Redmineを構築する

以前sameersbn/redmineというRedmineのイメージを試してみましたが、アプリケーションとMySQLでコンテナを分離した場合、複数のコンテナ起動や、DBの作成、マイグレーションの実行が個別に必要となり手間がかかっていました。

そこで今回は、マルチコンテナ構成ツールであるFigを用いて、アプリケーションとMySQLでコンテナを分離した場合でも、Redmineを一括起動するDockerfileを書いてみました。

構成

  • Ubuntu 14.04
  • Docker 1.0.0
  • Fig 0.5.2

コンテナとDockerfileの割り振り

Redmineの起動のためには、DBサーバ、アプリケーションサーバ、Webサーバが必要となります。これらを1つのコンテナの中で動かす事も出来ますが、安定性等を考え、以下のように2つのコンテナで動かす事にします。

また、Dockerfileのメンテナンス性を考え、APコンテナはRubyまでを導入したイメージと、そのイメージをベースにRedmineを導入したイメージに分離させています。Redmineの設定を変更したい場合、Rubyのインストールを省くことができます。

  • DBイメージ  : 共通設定、MySQL
  • Rubyイメージ : 共通設定、Ruby
  • APイメージ  : Redmine、Nginx、Unicorn (Rubyイメージをベースに差分ビルド)

Dockerfile

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

Dockerfileの構成

Dockerfileの構成は以下の通りです。
このほかにもコンテナに追加する定義ファイルもいくつかあります。

fig-redmine 
├──fig.yml           ... Figのコンテナ起動定義ファイル
├──mysql
│ └──Dockerfile      ... MySQL用のDockerfile
├──ruby     
│ └──Dockerfile      ... Ruby用のDockerfile 
└──redmine     
   └──Dockerfile      ... Redmine用のDockerfile 

MySQL

$ vi mysql/Dockerfile
FROM ubuntu:14.04

MAINTAINER hidemium

# install basic package
ADD sources.list /etc/apt/sources.list
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && \
    apt-get -y install \
      mysql-server \
      openssh-server \
      supervisor && \
      apt-get clean

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

# install sshd
RUN mkdir -p /root/.ssh /var/run/sshd
ADD id_rsa.pub /root/.ssh/authorized_keys
RUN chmod 700 /root/.ssh
RUN chmod 600 /root/.ssh/authorized_keys
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

# config supervisor
RUN mkdir -p /var/log/supervisor /etc/supervisor/conf.d
ADD mysqld.conf /etc/supervisor/conf.d/mysqld.conf
ADD sshd.conf /etc/supervisor/conf.d/sshd.conf

# install MySQL
RUN rm -rf /var/lib/mysql/mysql
RUN rm -rf /var/lib/apt/lists/*
ADD my.cnf /etc/mysql/my.cnf
ADD mysql-listen.cnf /etc/mysql/conf.d/mysql-listen.cnf
RUN chown -R mysql:mysql /var/lib/mysql
RUN chmod 700 /var/lib/mysql

# initialize MySQL 
RUN mysql_install_db --user mysql > /dev/null
RUN /usr/bin/mysqld_safe & \
    sleep 3 && \
    mysqladmin -u root password "root" && \
    mysql -u root -proot -e "GRANT ALL ON *.* TO 'root'@'172.17.%.%' IDENTIFIED BY '' WITH GRANT OPTION;" && \
    mysql -u root -proot -e "CREATE USER 'redmine'@'%.%.%.%' IDENTIFIED BY 'redmine';" && \
    mysql -u root -proot -e "CREATE DATABASE IF NOT EXISTS redmine_production DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;" && \
    mysql -u root -proot -e "GRANT ALL PRIVILEGES ON redmine_production.* TO 'redmine'@'%.%.%.%';" && \
    mysql -u root -proot -e "FLUSH PRIVILEGES;"

# expose ports
EXPOSE 22 3306 

# define default command.
CMD ["supervisord", "-n"]
  • MySQLのDockerfile内でRedmine用のユーザ作成、データベース作成を定義しています。

Ruby

$ vi ruby/Dockerfile
FROM ubuntu:14.04

MAINTAINER hidemium

# 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 \
    subversion \
    mercurial \
    libcurl4-openssl-dev \
    libreadline-dev \
    libssl-dev \
    libxml2-dev \
    libxslt1-dev \
    libyaml-dev \
    zlib1g-dev && \
    apt-get clean && \
    curl -O http://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz && \
    tar -zxvf ruby-2.1.2.tar.gz && \
    cd ruby-2.1.2 && \
    ./configure --disable-install-doc && \
    make && \
    make install && \
    cd .. && \
    rm -r ruby-2.1.2 ruby-2.1.2.tar.gz && \
    echo 'gem: --no-document' > /usr/local/etc/gemrc

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

# install sshd
RUN mkdir -p /root/.ssh /var/run/sshd
ADD id_rsa.pub /root/.ssh/authorized_keys
RUN chmod 700 /root/.ssh
RUN chmod 600 /root/.ssh/authorized_keys
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

# install supervisor
RUN mkdir -p /var/log/supervisor /etc/supervisor/conf.d
ADD sshd.conf /etc/supervisor/conf.d/sshd.conf

# install bundler
RUN \
  echo 'gem: --no-rdoc --no-ri' >> /.gemrc && \
  gem install bundler
  • rbenvを使ってRubyをインストールする方法もありますが、複数バージョンのRubyを使い分ける必要はないため、ソースからのビルドしています。DockerはImmutableなので、異なるバージョンのRubyを使用したい場合は、コンテナを作り直せばいいという考えです。
  • Redmineに必要なパッケージもこちらでインストールしておきます。

Redmine

$ vi redmine/Dockerfile
FROM ubuntu-ruby:14.04

MAINTAINER hidemium

ENV REDMINE_VERSION 2.5-stable
ENV REDMINE_HOME /var/redmine
ENV SETUP_DIR /app/setup
ENV RAILS_ENV production

# install basic package
RUN apt-get update && \
    apt-get -y install \
      nginx \
      imagemagick \
      libmagickcore-dev \
      libmagickwand-dev \
      libmysqlclient-dev

# install Redmine
RUN git clone -b ${REDMINE_VERSION} https://github.com/redmine/redmine.git ${REDMINE_HOME}
WORKDIR /var/redmine 
RUN echo 'gem "mysql2", "~> 0.3.11"' >> Gemfile
RUN echo 'gem "unicorn"' >> Gemfile
RUN bash -l -c 'bundle install --without development test'

# install plugins
RUN mkdir -p ${SETUP_DIR}
ADD setup/themes_install ${SETUP_DIR}/themes_install
ADD setup/plugins_install ${SETUP_DIR}/plugins_install
RUN chmod 755 ${SETUP_DIR}/*
RUN ${SETUP_DIR}/themes_install
RUN ${SETUP_DIR}/plugins_install

# add config file
ADD config/database.yml ${REDMINE_HOME}/config/database.yml
ADD config/configuration.yml ${REDMINE_HOME}/config/configuration.yml
ADD config/unicorn.rb ${REDMINE_HOME}/config/unicorn.rb
RUN mkdir ${REDMINE_HOME}/public/plugin_assets

# install nginx
ADD redmine.conf /etc/nginx/conf.d/redmine.conf
RUN rm -f /etc/nginx/sites-enabled/default
RUN ln -s /etc/nginx/sites-available/redmine /etc/nginx/sites-enabled/redmine
RUN echo "daemon off;" >> /etc/nginx/nginx.conf

# add supervisor config file 
ADD unicorn.conf /etc/supervisor/conf.d/unicorn.conf
ADD nginx.conf /etc/supervisor/conf.d/nginx.conf

# expose ports
EXPOSE 22 80

# define default command
CMD bash -l -c 'bundle install --without development test' && \
    bash -l -c 'bundle exec rake generate_secret_token' && \
    mv plugins plugins-1 && \
    bash -l -c 'bundle exec rake db:migrate RAILS_ENV=${RAILS_ENV}' && \
    mv plugins-1 plugins && \
    bash -l -c 'bundle exec rake redmine:plugins:migrate RAILS_ENV=${RAILS_ENV}' && \
    supervisord -n
  • 「FROM ubuntu-ruby:14.04」で先に作成したRubyイメージをベースにしています。
  • RedmineソースコードGitHubリポジトリから取得しています。
  • 一度bundle installすれば、Dockerのキャッシュが使われるので再実行が短縮できます。そのため、最初にbundle installを実行しています。ただし、RedmineのGemfileは、database.yml内の情報からデータベースの種類を判別し、必要なgemを選択しています。database.ymlを先に追加すると処理が失敗するため、「'gem "mysql2", "~> 0.3.11"' >> Gemfile」で追加しています。
  • テーマとプラグインのインストールは、「themes_install」と「plugins_install」というシェルから実行しています。
  • RedmineをNginx + Unicornで動作させる設定を行っています。
  • CMDでマイグレーションコマンドを定義することで、Redmineコンテナ起動時にマイグレーションを自動で行うことができます。

Dockerfileのビルド

以下のコマンドにより、Dockerfileの取得とDockerイメージのビルドを行います。

$ git clone https://github.com/hideakihal/docker-fig-redmine.git
$ cd docker-fig-redmine
$ sudo docker build -t ubuntu-mysql:14.04 mysql
$ sudo docker build -t ubuntu-ruby:14.04 ruby
$ sudo docker build -t ubuntu-redmine:14.04 redmine

Fig

Dockerイメージの用意ができたため、ここからはFigを使用方法を説明します。

インストール

FigはPythonで書かれているので、パッケージ管理をするpipをインストールしておきます。pipを使ってFigをインストールします。

$ sudo apt-get install -y python-pip python2.7-dev
$ sudo pip install -U fig 

fig.yml

複数のコンテナをどのように起動するかはfig.ymlというファイルで定義します。
fig.ymlは、以下のような構成になり、起動するDockerイメージやポート番号、コンテナ間通信の設定などが行えます。
fig.ymlのリファレンスは公式サイトにあります。

$ vi fig.yml
db:
  image: ubuntu-mysql:14.04
  ports:
    - 22
    - 3306
web:
  image: ubuntu-redmine:14.04
  ports:
    - 22
    - 80
  environment:
    - DB_HOST=db_1
  links:
    - db

上記の設定を通常のDockerコマンドで表現すると以下のようになります。
毎回以下のようなコマンドを打つのは大変なので、とても簡単できるようになります。

$ sudo docker run -d -p 22 -p 3306 --name figredmine_db_1 ubuntu-mysql:14.04
$ sudo docker run -d -p 22 -p 80 -e "DB_HOST=db_1" --link figredmine_db_1:db_1 --name figredmine_web_1 ubuntu-redmine:14.04

環境変数について

fig.yml内で、DB_HOSTという環境変数を定義しています。これは、APコンテナのdatabase.ymlにDBコンテナをホスト名を渡すために行っています。
当初は、以下のようにlinkオプションの起動でDBコンテナのIPアドレスを取得していましたが、DBコンテナ名が変わるとDockerイメージのリビルドが必要でした。

$ vi /var/redmine/config/database.yml
production:
  adapter: mysql2
  database: redmine_production
  host: <%= ENV.fetch('DB_1_PORT_3306_TCP_ADDR') %>
  port: <%= ENV.fetch('DB_1_PORT_3306_TCP_PORT') %>

Figのissueを見てみると、linkオプションにより/etc/hostsも変更されることが分かり、以下のように設定を修正しました。

$ vi /var/redmine/config/database.yml
production:
  adapter: mysql2
  database: redmine_production
  host: <%= ENV.fetch('DB_HOST') %>
  port: 3306
...

Dockerの公式ドキュメントを見てみると、コンテナのポート番号やIPをもった環境変数と/etc/hostsが追加されることが記載されています。
実際のコンテナでも確認したところ、確かにコンテナの環境変数と/etc/hostsが更新されていることが分かりました。

root@be9520e48d64:~# env
DB_1_PORT_3306_TCP_ADDR=172.17.3.117
DB_1_PORT_3306_TCP_PORT=3306
...

ホスト名が、db_1のように「コンテナ名_数字」となっていますが、これはFigの仕様で同じDockerイメージをスケールさせる場合に末尾の数字がカウントアップされていきます。今回はスケールさせないため、末尾の数字は1として表示されます。

root@be9520e48d64:~# cat /etc/hosts
172.17.3.117    db_1
172.17.3.117    figredmine_db_1
...

複数コンテナの一括起動

以下のコマンドを実行し、複数コンテナを一括起動します。

$ sudo fig up -d

起動の進捗を確認する場合は、以下のコマンドを使用できます。

$ sudo fig logs
Attaching to figredmine_db_1, figredmine_web_1
web_1 | Don't run Bundler as root. Bundler can ask for sudo if it is needed, and
web_1 | installing your bundle as root will break this application for all non-root
web_1 | users on this machine.
web_1 | Your Gemfile lists the gem vcard (~> 0.2.8) more than once.
web_1 | You should probably keep only one of them.
web_1 | While it's not a problem now, it could cause errors if you change the version of just one of them later.
web_1 | Your Gemfile lists the gem capybara (~> 2.1.0) more than once.
web_1 | You should probably keep only one of them.
web_1 | While it's not a problem now, it could cause errors if you change the version of just one of them later.
...

コンテナの起動を確認する場合、以下のコマンドを使用できます。APとDBのコンテナが起動していることが分かります。
docker psコマンドより情報がまとまってシンプルに表示できます。

$ sudo fig ps
    Name                   Command               State               Ports
---------------------------------------------------------------------------------------
figredmine_db_1    supervisord -n                Up      49177->22/tcp, 49178->3306/tcp
figredmine_web_1   /bin/sh -c bash -l -c '...    Up      49179->22/tcp, 49180->80/tcp

上記で、80番ポートにフォワードされているポート番号を確認し、ブラウザから、「http://localhost:ポート番号」にアクセスし、Redmineのログイン画面が表示されることを確認します。

本番環境で動作させる場合

fig upコマンドを再実行すると、既存のコンテナは削除され、コンテナが再作成されます。開発中は不要なコンテナの削除を自動的に行ってくれるため便利ですが、本番環境で現行のコンテナを残して、Blue-Green Deploymentを行いたい場合は不便です。また、開発環境と本番環境でコンテナの起動設定を変えたい場合などがあるかと思います。
そこで、fig upコマンドでは、-fオプションによりデフォルトパスのfig.yml以外のfig.ymlを指定することができます。

以下のように、fig-production.yml(任意)を作成します。
上記のfig.ymlとは異なり、コンテナ名の変更や、データの永続性のため共有ディレクトリの設定を追加しています。また、DB_HOSTのコンテナ名も合わせて変更します。

$ vi fig-production.yml
dbBlue:
  image: ubuntu-mysql:14.04
  ports:
    - 22
    - 3306
  volumes:
    - /path/in/host:/var/lib/mysql
webBlue:
  image: ubuntu-redmine:14.04
  ports:
    - 22
    - 80
  environment:
    - DB_HOST=dbBlue_1
  volumes:
    - /path/in/host:/var/redmine/files
  links:
    - dbBlue
  • コンテナ名は記号は不可で、英数字しか使用できないようです。

以下のコマンドを実行し、本番環境用のコンテナを起動します。

$ sudo fig -f fig-production.yml up -d

おわりに

Figによって、複雑になったdocker runコマンドを定義ファイルにまとめることができ、複数コンテナが連携するDockerfileの開発がとても楽になりました。さらに、複数コンテナのDockerfileとFigの定義ファイルを1つのフォルダで管理できるため、リポジトリもとても見やすくなったかと思います。

Figを開発したOrchard社は、Docker社は買収されたようなので、今後の展開に注目しています。

DockerでPlay Frameworkアプリケーションをデプロイする

DockerによるWebアプリのデプロイ方法をいくつか調べてみると、ビルド時間の短縮やメンテナンス性から、インフラレイヤーとアプリケーションレイヤーに分けて差分ビルドする方法が良さそうだったので、Play Frameworkアプリでデプロイを試してみました。

構成

  • Ubuntu 12.04
  • Docker 1.0.0
  • Play Framework 2.2.3

インフラレイヤー

Java及びPlay Frameworkの実行環境までを構築します。

Dockerfileの構成

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

java
├──Dockerfile
├──id_rsa.pub            ... 公開鍵
├──play.sh               ... 環境変数の定義ファイル
├──sources.list          ... ミラーサイト一覧
└──supervisord.conf    ... supervisorの定義ファイル

Dockerfile

Dockerfileを以下のように作成します。

$ cd java
$ vi Dockerfile
FROM ubuntu:12.04

MAINTAINER hidemium

# Ubuntu update
ADD sources.list /etc/apt/sources.list
RUN apt-get -y update

# ssh install
RUN apt-get -y install openssh-server
RUN apt-get -y install python-setuptools
RUN apt-get clean
RUN easy_install supervisor

RUN mkdir /var/run/sshd
RUN echo 'root:root' | chpasswd

# sshd config
ADD id_rsa.pub /root/id_rsa.pub
RUN mkdir /root/.ssh/
RUN mv /root/id_rsa.pub /root/.ssh/authorized_keys
RUN chmod 700 /root/.ssh
RUN chmod 600 /root/.ssh/authorized_keys
RUN sed -i -e '/^UsePAM\s\+yes/d' /etc/ssh/sshd_config

# config supervisor
RUN mkdir -p /var/log/supervisor
RUN mkdir -p /etc/supervisor/conf.d/
ADD supervisord.conf /etc/supervisord.conf

# Install Java
RUN apt-get install -y software-properties-common python-software-properties
RUN add-apt-repository ppa:webupd8team/java
RUN apt-get update
RUN echo "oracle-java7-installer shared/accepted-oracle-license-v1-1 boolean true" | debconf-set-selections
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get install -y oracle-java7-installer

# Install Play Framework
ENV PLAY_VERSION 2.2.3
RUN apt-get install -y unzip
RUN wget http://downloads.typesafe.com/play/$PLAY_VERSION/play-$PLAY_VERSION.zip
RUN unzip play-$PLAY_VERSION.zip
RUN mv play-$PLAY_VERSION /usr/local
ADD play.sh /etc/profile.d/play.sh
RUN sh /etc/profile.d/play.sh

# Expose ports
EXPOSE 22 9000 9999

# Define default command
CMD ["supervisord", "-n"]

Dockerfileの中で、読み込みを行っているファイルは以下の通りです。

sources.list

日本のミラーサイトを指定しています。

$ cat sources.list
deb http://jp.archive.ubuntu.com/ubuntu precise main restricted
deb-src http://jp.archive.ubuntu.com/ubuntu precise main restricted
deb http://jp.archive.ubuntu.com/ubuntu precise-updates main restricted
deb-src http://jp.archive.ubuntu.com/ubuntu precise-updates main restricted

play.sh

Java及びPlay Frameworkの環境変数を設定しています。

export JAVA_HOME=/usr/lib/jvm/java-7-oracle
export PATH=$PATH:$JAVA_HOME/bin
export CLASSPATH=.:$JAVA_HOME/jre/lib:$JAVA_HOME/lib:$JAVA_HOME/lib/tools.jar
export PLAY_HOME=/usr/local/play-2.2.3
export PATH=$PATH:$PLAY_HOME

supervisord.conf

supervisorの設定とsshdの起動を定義しています。

$ cat supervisord.conf
[unix_http_server]
file=/tmp/supervisor.sock   ; (the path to the socket file)

[supervisord]
logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB        ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10           ; (num of main logfile rotation backups;default 10)
loglevel=info                ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false               ; (start in foreground if true;default false)
minfds=1024                  ; (min. avail startup file descriptors;default 1024)
minprocs=200                 ; (min. avail process descriptors;default 200)

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket

[include]
files = /etc/supervisor/conf.d/*.conf

[program:sshd]
command=/usr/sbin/sshd -D
autostart=true
autorestart=true

Dockerイメージのビルド

以下のコマンドで、Dockerfileからビルドし、ベースとなるDockerイメージを作成します。

$ sudo docker build -t ubuntu-java:12.04 .

アプリケーションレイヤー

上記のDockerイメージをベースに、Play Frameworkのアプリケーションをコンテナに追加し、本番モードで起動します。
なお、データベースはPlay Frameworkに内蔵しているH2 databaseを使用する想定です。

Dockerfileの構成

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

play
├──Dockerfile
└──myapp           ... Play Frameworkアプリケーションを格納したディレクトリ

上記の「myapp」には、git cloneなどでデプロイするPlay Frameworkアプリケーションをコピーしておきます。手元にPlay Frameworkアプリケーションがない場合は、ダウンロードしたPlay Frameworkのパッケージ内にsampleプログラムが格納されているため、こちらを使用することも可能です。

Dockerfile

Play Frameworkは、Play Frameworkのバージョンに依存しないように、単独で起動が可能なdistと呼ばれる配布パッケージを作成することができます。*1
以下では、stage向けとdist向けのDockerfileを用意しました。

stageデプロイ用

Dockerfileを以下のように作成します。

$ cd play
$ vi Dockerfile
FROM ubuntu-java:12.04

MAINTAINER hidemium

RUN mkdir /myapp
WORKDIR /myapp
ADD myapp /myapp
RUN bash -l -c 'play clean compile stage'
RUN rm target/universal/stage/bin/*.bat

EXPOSE 22 9000
CMD target/universal/stage/bin/$(ls target/universal/stage/bin) -DapplyEvolutions.default=true & \
    supervisord -n
  • 「FROM ubuntu-java:12.04」では、インフラレイヤーの箇所で作成したDockerイメージをベースにしています。
  • 「ADD myapp /myapp」では、Play Frameworkアプリケーションをコンテナに追加します。
  • 「ENTRYPOINT target/universal/stage/bin/$(ls target/universal/stage/bin) -DapplyEvolutions.default=true」では、Play Frameworkアプリケーションを本番モードで実行します。また、Evolutionsが自動実行されるようにオプションを指定しています。
distデプロイ用

Dockerfileを以下のように作成します。
distファイル(zipファイル)は、あらかじめ解凍し、myappをいうディレクトリにリネームしておきます。

$ cd play
$ vi Dockerfile
FROM ubuntu-java:12.04

MAINTAINER hidemium

RUN mkdir /myapp
WORKDIR /myapp
ADD myapp /myapp
RUN rm bin/*.bat

EXPOSE 22 9000
CMD bin/$(ls bin) -DapplyEvolutions.default=true & \
    supervisord -n
  • 「FROM ubuntu-java:12.04」では、インフラレイヤーの箇所で作成したDockerイメージをベースにしています。
  • 「ADD myapp /myapp」では、Play Frameworkアプリケーションをコンテナに追加します。
  • 「ENTRYPOINT bin/$(ls bin) -DapplyEvolutions.default=true」では、Play Frameworkアプリケーションを本番モードで実行します。distファイルは、stage用と比べてパスがシンプルになっています。

Dockerイメージのビルド

以下のコマンドで、Dockerfileからビルドし、Play FrameworkアプリケーションのDockerイメージを作成します。Play Frameworkの実行環境をベースに差分ビルドを行っているため、ビルド時間を短縮できます。

$ sudo docker build -t ubuntu-play:12.04 .

Play Frameworkアプリケーションのデプロイ

以下のコマンドで、Play Frameworkアプリケーションのコンテナを起動します。

$ sudo docker run -d -p 22 -p 9000 ubuntu-play:12.04

Dockerコンテナの9000番ポートにフォワードしているポート番号を確認します。

$ sudo docker port <コンテナID> 9000
0.0.0.0:<ポート番号>

「http://<サーバのIPアドレス>:<ポート番号>」にアクセスし、Play Frameworkアプリケーションに接続できることを確認します。

*1:distを使用する場合は、インフラレイヤーからPlay Frameworkをインストールしている箇所を削除してしまっても問題ありません。

Fluentd + Elasticsearch + Kibanaでログを可視化する

FluetndからElasticsearchへログを転送し、Kibanaでログを可視化できるか試してみました。転送するログはSensu Serverのログを使用しました。

構成

Ubuntu 12.04: Sensu Server インストール対象
Ubuntu 12.04: Elasticsearch
※上記はDocker 1.0.0上で動作しています。

Fluentdのインストール

Fluentdをインストールします。
Fluentdの安定版であり、Rubyのインタープリンタを含んだtd-agentを使用します。また、td-agentの必須パッケージのlibssl0.9.8が入っていなかったので合わせてインストールしています。

$ curl http://packages.treasuredata.com/GPG-KEY-td-agent | apt-key add -
$ echo "deb http://packages.treasuredata.com/precise/ precise contrib" > /etc/apt/sources.list.d/treasure-data.list
$ apt-get update
$ apt-get install -y libssl0.9.8
$ apt-get install -y ntp td-agent

Fluentdのバージョンを確認します。

$ /usr/lib/fluent/ruby/bin/fluentd --version
fluentd 0.10.50

supervisorでFluentdを起動する設定を追加します。

$ vi /etc/supervisor/conf.d/td-agent.conf
[program:td-agent]
command=/etc/init.d/td-agent start
autostart=true
autorestart=true

Elasticsearchへの連携

td-agent.confに以下の内容を追加し、転送するログとElasticsearchサーバの指定を行います。
今回はSensu Serverのログを転送する設定をしています。

$ vi /etc/td-agent/td-agent.conf
<source>
  type tail
  format json
  path /var/log/sensu/server.log
  tag ubuntu.sensu.server
  pos_file /tmp/sensu.pos
</source>

<match *.sensu.*>
  type elasticsearch
  include_tag_key true
  tag_key @log_name
  host <IPアドレス>
  port 9200
  logstash_format true
  flush_interval 10s
</match>

supervisorをリロードして、Fluentdを起動します。

$ supervisorctl reload

Kibanaの確認

「http://<サーバのIPアドレス>:<ポート番号>」からKibanaにアクセスし、トップページの「Logstash Dashboard」をクリックすると、Sensu Serverのログが転送されているのが確認できます。