オープンソースのPaaSソフトウェア CloudFoundry の技術情報やイベント告知などを掲載します

2015-06-23

Yabitz を Cloud Foundry で動かす

本日の第14回「Cloud Foundry 百日行」は、Yabitzです。
社内のサーバなどホスト管理を行えるアプリケーションです。rubyで書かれており、加えてMySQL環境が必要となります。

[Yabitz]基本情報

デプロイ

手順

  • 0) 準備
  • 1) cf-mysql-releaseを利用する為のコード修正
  • 2) アプリの起動
  • 3) 動作確認

0.準備

まずはソースコードの取得をしましょう。

$ git clone https://github.com/livedoor/yabitz.git
$ cd yabitz
$ ls
config.ru  Gemfile  lib  public  README.md  scripts  spec  tmp  view

ここでデプロイに入る前に補足を少ししておきます。

  • これまでの記事にも出てきましたがMySQLなどのDBと連携が必要となるアプリではDBの初期設定が必要なものが多く存在します。
    今回のアプリもスクリプトでデータベースのスキーマを作成する必要があります。
    cf create-service で作成したMySQLのDBに初期設定を行う場合、「アプリ内から初期設定を実行する」または「DB初期設定用のアプリを利用する」などいくつかの対処がありますが、今回は「アプリ内から初期設定を実行する」方法で対処しています。機会があればその他の方法もご紹介したいと思います。
  • このアプリはユーザデータを扱う方法がpluginで提供されています。今回はホストの情報を管理するDBにユーザ情報も管理する為のpluginを利用します。
  • 今回紹介する方法でアプリをデプロイする為にはGemfile.lockファイルを bundle install で作成する必要があります。その為、下記の準備が必要になります。
    • ruby(利用したバージョン 2.1.5)
    • bundler(利用したバージョン 1.10.2)

1.cf-mysql-releaseを利用する為のコード修正

まずはアプリを停止状態で cf push してMySQLとバインドできる状態にします。

$ cf push yabitz --no-start

そしてMySQLを準備します。
なおMySQLはこれまでの記事にも何度か出てきているcf-mysql-releaseを利用します。

$ cf marketplace

service      plans                     description   
p-mysql      100mb-dev, 1gb-dev        A MySQL service for application development and testing   

$ cf create-service p-mysql 100mb-dev yabitzdb
$ cf bind-service yabitz yabitzdb
$ cf env yabitz
System-Provided:
{
 "VCAP_SERVICES": {
  "p-mysql": [
   {
    "credentials": {
     "hostname": "10.244.1.18",
     "jdbcUrl": "jdbc:mysql://10.244.1.18:3306/cf_9f086cff_c59f_4b6f_afa7_925ce372764b?user=k3i8E0ZSj6aBmlLx\u0026password=HMHe7pf0XLqw2j0c",
     "name": "cf_9f086cff_c59f_4b6f_afa7_925ce372764b",
     "password": "HMHe7pf0XLqw2j0c",
     "port": 3306,
     "uri": "mysql://k3i8E0ZSj6aBmlLx:HMHe7pf0XLqw2j0c@10.244.1.18:3306/cf_9f086cff_c59f_4b6f_afa7_925ce372764b?reconnect=true",
     "username": "k3i8E0ZSj6aBmlLx"
    },
    "label": "p-mysql",
    "name": "yabitzdb",
    "plan": "100mb-dev",
    "tags": [
     "mysql"
    ]
   }
  ]
 }
}
:

準備が終われば手元のコードの修正に入ります。

MySQLとの接続情報は上記の cf env で得た値をベタ書きしてもいいのですが、環境変数 VCAP_SERVICES(以下 環境変数) から情報を取得できるようにコードを一部書き換えます。
まず環境変数はjsonで提供される為、Gemfileに以下のように追記しjsonを利用できるようにします。

$ vi Gemfile
$ git diff
diff --git a/Gemfile b/Gemfile
index a217b6e..426bc22 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,6 +8,6 @@ gem 'haml'
 gem 'sass'
 gem 'ruby-ldap'
 gem 'rspec'
-
+gem 'json'
 gem 'passenger'
 gem 'unicorn'

次に手元の環境で bundle install します。これは後述のruby-buildpackの適用条件を満たす為にGemfile.lockを作成をすることが目的ですのでこちらのリポジトリのコードを使う場合は不要です。
なお、 bundle install でのエラーは自己解決していただくか、修正済みのコードをご利用ください。

$ bundle install --path vendor/bundle
$ cat Gemfile.lock
GIT
  remote: git://github.com/tagomoris/stratum.git
  revision: d763f84f3885a81bad263588f3f8e62a58d73b6f
  specs:
    stratum (0.1.0)
      mysql2-cs-bind

GEM
  remote: http://rubygems.org/
  specs:
    diff-lcs (1.2.5)
    haml (4.0.6)
      tilt
    json (1.8.3)
    kgio (2.9.3)
    mysql2 (0.3.18)
    mysql2-cs-bind (0.0.6)
      mysql2
    passenger (5.0.10)
      rack
      rake (>= 0.8.1)
    rack (1.6.2)
    rack-protection (1.5.3)
      rack
    raindrops (0.13.0)
    rake (10.4.2)
    rspec (3.3.0)
      rspec-core (~> 3.3.0)
      rspec-expectations (~> 3.3.0)
      rspec-mocks (~> 3.3.0)
    rspec-core (3.3.0)
      rspec-support (~> 3.3.0)
    rspec-expectations (3.3.0)
      diff-lcs (>= 1.2.0, < 2.0)
      rspec-support (~> 3.3.0)
    rspec-mocks (3.3.0)
      diff-lcs (>= 1.2.0, < 2.0)
      rspec-support (~> 3.3.0)
    rspec-support (3.3.0)
    ruby-ldap (0.9.17)
    sass (3.4.14)
    sinatra (1.4.6)
      rack (~> 1.4)
      rack-protection (~> 1.4)
      tilt (>= 1.3, < 3)
    tilt (2.0.1)
    unicorn (4.9.0)
      kgio (~> 2.6)
      rack
      raindrops (~> 0.7)

PLATFORMS
  ruby

DEPENDENCIES
  haml
  json
  mysql2
  mysql2-cs-bind
  passenger
  rspec
  ruby-ldap
  sass
  sinatra
  stratum!
  unicorn

BUNDLED WITH
   1.10.2
$ rm -rf vendor/bundle

cf push 後にruby-buildpackにて 再度 bundle install は実行されるので、不要となるvendor/bundleディレクトリは削除しておいて下さい。

次はMySQLの環境変数を読み込めるように3つのファイルを編集します。

  • アプリとDBの接続plugin(lib/yabitz/plugin/config_instant.rb)
    requireに先ほどGemfileに追加したjsonを加え、接続情報を環境変数から取得できるように変更します。

  • 利用するユーザデータを指定するplugin(lib/yabitz/plugin/instant_membersource.rb)
    cf create-service で作成したMySQL内のユーザデータのtableを利用する為にpluginを有効化します。

  • DBの初期設定スクリプト(scripts/db_schema.rb)
    上記のpluginにはDBの初期設定スクリプトと別に初期ユーザ登録用スクリプト(instant/register_user.rb)が準備されています。
    しかし初期ユーザ登録用スクリプトは対話型となっておりCloud Foundry環境で実行するには不向きです。
    その為、初期ユーザ登録機能もバッチ型のDBの初期設定スクリプトにまとめています。

$ vi lib/yabitz/plugin/config_instant.rb
$ vi lib/yabitz/plugin/instant_membersource.rb
$ vi scripts/db_schema.rb
$ git diff

:

diff --git a/lib/yabitz/plugin/config_instant.rb b/lib/yabitz/plugin/config_instant.rb
index 15b6462..b67cb61 100644
--- a/lib/yabitz/plugin/config_instant.rb
+++ b/lib/yabitz/plugin/config_instant.rb
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-
+require 'json'
 module Yabitz::Plugin
   module InstantConfig
     def self.plugin_type
@@ -18,14 +18,14 @@ module Yabitz::Plugin
     end
 
     DB_PARAMS = [:server, :user, :pass, :name, :port, :sock]
-
+    mysql_dbs = JSON.parse(ENV['VCAP_SERVICES'])
     CONFIG_SET = {
       :database => {
-        :server => 'localhost',
-        :user => 'root',
-        :pass => nil,
-        :name => 'yabitz_instant',
-        :port => nil,
+        :server => mysql_dbs["p-mysql"][0]["credentials"]["hostname"],
+        :user => mysql_dbs["p-mysql"][0]["credentials"]["username"],
+        :pass => mysql_dbs["p-mysql"][0]["credentials"]["password"],
+        :name => mysql_dbs["p-mysql"][0]["credentials"]["name"],
+        :port => mysql_dbs["p-mysql"][0]["credentials"]["port"],
         :sock => nil,
       },
       :test_database => {
diff --git a/lib/yabitz/plugin/instant_membersource.rb b/lib/yabitz/plugin/instant_membersource.rb
index 5f8b9a6..6e4ddff 100644
--- a/lib/yabitz/plugin/instant_membersource.rb
+++ b/lib/yabitz/plugin/instant_membersource.rb
@@ -2,6 +2,7 @@
 
 require 'digest/sha1'
 require 'mysql2-cs-bind'
+require 'json'
 
 module Yabitz::Plugin
   module InstantMemberHandler
@@ -9,13 +10,14 @@ module Yabitz::Plugin
       [:auth, :member]
     end
     def self.plugin_priority
-      0
+      1
     end
 
-    DB_HOSTNAME = "localhost"
-    DB_USERNAME = "root"
-    DB_PASSWORD = nil
-    DATABASE_NAME = "yabitz_member_source"
+    mysql_dbs = JSON.parse(ENV['VCAP_SERVICES'])
+    DB_HOSTNAME = mysql_dbs["p-mysql"][0]["credentials"]["hostname"]
+    DB_USERNAME = mysql_dbs["p-mysql"][0]["credentials"]["username"]
+    DB_PASSWORD = mysql_dbs["p-mysql"][0]["credentials"]["password"]
+    DATABASE_NAME = mysql_dbs["p-mysql"][0]["credentials"]["name"]
     TABLE_NAME = "list"
     def self.query(sql, *args)
       result = []
diff --git a/scripts/db_schema.rb b/scripts/db_schema.rb
index a1fce02..727b511 100644
--- a/scripts/db_schema.rb
+++ b/scripts/db_schema.rb
@@ -2,6 +2,17 @@
 # -*- coding: utf-8 -*-
 
 require 'mysql2'
+require 'digest/sha1'
+
+## default user data
+TABLE_NAME = "list"
+USERNAME = "admin"
+PASSWORD = "password"
+FULLNAME = "default_adminuser"
+MAIL = ""
+BADGE = ""
+POSITION = ""
+PASSHASH = Digest::SHA1.hexdigest("#{PASSWORD}")
 
 # before require of schema.rb
 #  init.rb MUST be already loaded with $YABITZ_RUN_ON_TEST_ENVIRONMENT = true
@@ -351,6 +362,22 @@ removed     ENUM('0','1')   NOT NULL DEFAULT '0'
 ) ENGINE=InnoDB charset='utf8'
 EOSQL
 
+       sqls.push <<-EOSQL
+CREATE TABLE #{TABLE_NAME} (
+id          INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
+name        VARCHAR(64)     NOT NULL UNIQUE KEY,
+passhash    VARCHAR(40)     NOT NULL,
+fullname    VARCHAR(64)     NOT NULL,
+mailaddress VARCHAR(256)    ,
+badge       VARCHAR(16)     ,
+position    VARCHAR(16)
+) ENGINE=InnoDB charset='utf8'
+EOSQL
+
+       sqls.push <<-EOSQL
+INSERT INTO #{TABLE_NAME} SET name='#{USERNAME}',passhash='#{PASSHASH}',fullname='#{FULLNAME}',mailaddress='#{MAIL}',badge='#{BADGE}',position='#{POSITION}'
+EOSQL
+
     c = conn()
     sqls.each do |s|
       c.query(s)

2.アプリの起動

今回のアプリには複数のbuildpackを利用しています。

  • heroku-buildpack-apt
    指定のパッケージを apt-get install できる Buildpackになります。
    今回は ruby-buildpack にて実行される bundle install で発生するエラー( ruby-ldap の gem install )を回避する為に libsasl2-dev のパッケージを追加します。

  • ruby-buildpack
    rubyのアプリをデプロイする為のBuildpackです。
    このBuildpackは下記のように bundle install を実行してくれます。
    bundle install --without development:test --path vendor/bundle --binstubs vendor/bundle/bin -j4 --deployment
    ただし、Gemfile.lockが無い状態で実行すると --deployment オプションが指定されている為、エラーとなってしまいます。その為、先ほどの手順のようにGemfile.lockを事前に作成しておく必要があります。

そして最後に複数のBuildpackを適用する為に下記のBuildpackを利用します。  

  • heroku-buildpack-multi
    通常一つしか適用できないBuildpackを複数用いる為のBuildpackです。
    (補足)heroku-buildpack-multiは非常に便利なBuildpackですが、利用するBuildpackによってはheroku-buildpack-multiで正常に動作をしないものもあるので注意が必要です。

それぞれのBuildpackに必要なファイルを準備していきます。
まずはheroku-buildpack-aptに必要なAptfile。 cf push を実行するアプリのルートディレクトリに作成します。
ここで作成するAptfileには apt-get install したいパッケージ名を記載します。

$ echo "libsasl2-dev" > Aptfile

次にheroku-buildpack-multiに必要な.buildpacks。こちらも上記のAptfileと同じディレクトリに作成します。
ここで作成する.buildpacksには利用したいBuildpackを実行したい順番で記載します。今回はruby-buildpackで実行される bundle install の前に libsasl2-dev パッケージの追加が完了している必要があるので以下の順番になります。

$ echo -e "https://github.com/ddollar/heroku-buildpack-apt.git\nhttps://github.com/cloudfoundry/ruby-buildpack.git" > .buildpacks
$ cat .buildpacks 
https://github.com/ddollar/heroku-buildpack-apt.git
https://github.com/cloudfoundry/ruby-buildpack.git

Buildpackの準備はここまでです。

いよいよデプロイに移ります。

cf push に際し注意いただきたいのは初回と2回目以降で-cオプションに変更があります。
初回はDBの初期設定スクリプトを指定して実行する必要がありますが、2回目以降は設定済みのため不要です。
もしmanifest.ymlを作成される場合はその点を考慮して下さい。今回は作成せずに進めます。

  • 初回
$ cf push yabitz -b https://github.com/ddollar/heroku-buildpack-multi.git -c 'bundle exec ruby scripts/db_schema.rb&&bundle exec rackup config.ru --port=$PORT'
  • 2回目以降
$ cf push yabitz -b https://github.com/ddollar/heroku-buildpack-multi.git -c 'bundle exec rackup config.ru --port=$PORT'

では cf push を実行。

$ cf push yabitz -b https://github.com/ddollar/heroku-buildpack-multi.git -c 'bundle exec ruby scripts/db_schema.rb&&bundle exec rackup config.ru --port=$PORT'
:
App started


OK

App yabitz was started using this command `bundle exec ruby scripts/db_schema.rb&&bundle exec rackup config.ru --port=$PORT`

Showing health and status for app yabitz in org k-nagai / space work as k-nagai...
OK

requested state: started
instances: 1/1
usage: 256M x 1 instances
urls: yabitz.10.244.0.34.xip.io
last uploaded: Fri Jun 19 01:13:12 +0000 2015
stack: lucid64

     state     since                    cpu    memory          disk      details   
#0   running   2015-06-19 10:14:45 AM   0.0%   65.1M of 256M   0 of 1G      

これでアプリのデプロイ完了です。

3.動作確認

URL( yabitz.10.244.0.34.xip.io )にアクセスしてみましょう。
画面の上にある”ログイン”をクリックしてログイン(db_schema.rbに記載したユーザ情報)します。

あとは各情報を登録して管理をするのですが、ホスト登録を達成できるようになるまでに各リストの準備がなかなかに大変です。
たとえ登録ボタンをクリックして「Internal Server Error」が出ても「アプリケーションがうまく動いていない!」なんてことは考えずに「どこかのリストの登録が先に必要なんだ」と思って進めてください。
ブラウザの横に cf logs を表示しながら作業を進めると、どのテーブルの情報でエラーを起こしているかがわかり効率がいいです。

$ cf logs yabitz

登録出来た時は達成感が少し生まれます。資源管理は大変だ!ということですかね。
もう少しアプリの入力チェックにコードを加えればいきなりInternal Server Errorが返されることもなくなるかもしれませんが、今回はここまでです。お疲れさまでした。

今回使用したソフトウェア