Kuromoji 形態素解析を使ってみた
ぶっちゃけこれ
Scala2.11.6 +SBT 0.13.x で使ってみたログ。
まずは build.sbt
libraryDependencies ++= Seq( "org.atilika.kuromoji" % "kuromoji" % "0.7.7" )
にこんなん書いて、
import org.atilika.kuromoji.{Token, Tokenizer} object SelfTokenizer { import scala.collection.JavaConversions._ val tokenizer = { val result = Tokenizer.builder() result.mode(Tokenizer.Mode.SEARCH) result.build() } def tokenize(str: String): List[Token] = this.synchronized { if (str == null || str.isEmpty) List.empty[Token] else tokenizer.tokenize(str).toList } }
とすれば完成。一応 Java のライブラリで、crawler4j と組み合わせで使うと幸せに。
Tokenizer.Mode で幾つかモードを指定できて、結果が違う。
今回のこれは、検索エンジンに特化するように、SEARCH を指定。
Token のフィールドは、「Surface(原文)」「PartOfSpeech(品詞情報)」だけ分かってれば良さそう。
形態素解析ライブラリを触ってみる【Igo】 - 謎言語使いの徒然 より手軽でいい感じ(結構最近でもメンテナンスしているので、信用できそう)
Xitrum web framework 弄ってみた
何か ScalaJP で Xitrum という単語があったので使ってみた。
まず新しいプロジェクトを作ろうか。
curl -L -o xitrum-new.zip https://github.com/xitrum-framework/xitrum-new/archive/master.zip
後は解凍して終わり。
インストールが必要ないのは楽でいいね。
中身を見れば大体分かる。
README.rst build.sbt screenshot.png ./config: akka.conf application.conf flash_socket_policy.xml logback.xml ssl_example.crt ssl_example.key xitrum.conf ./project: build.properties plugins.sbt ./public: 404.html 500.html app.css favicon.ico robots.txt whale.png ./sbt: sbt sbt-launch-0.13.5.jar sbt.bat ./script: runner runner.bat scalive scalive-1.2.jar scalive.bat ./src: main ./src/main: scala scalate ./src/main/scala: quickstart ./src/main/scala/quickstart: Boot.scala action ./src/main/scala/quickstart/action: DefaultLayout.scala Errors.scala SiteIndex.scala ./src/main/scalate: quickstart ./src/main/scalate/quickstart: action ./src/main/scalate/quickstart/action: DefaultLayout.jade NotFoundError.jade ServerError.jade SiteIndex.jade
そのままだよね。
デフォルトでは model に該当するクラスは用意されていない。好きに作ればいいのかな?
構成が殆んどプレーンな sbt なので、ScalaActiveRecord 突っ込めば色々楽ができるだろうね。
Controller
この辺を見る。
まぁファイル名を見れば分かるでしょう。
DefaultLayout.scala : Controller 規定
package quickstart.action import xitrum.Action trait DefaultLayout extends Action { override def layout = renderViewNoLayout[DefaultLayout]() }
Action クラスの継承がコントローラクラスの条件のようだ。layout
は View 名を省略したときに自動で適用される View テンプレートの選択ロジックに見える。
trait なのが気になるがまぁいい。
Errors.scala
package quickstart.action import xitrum.annotation.{Error404, Error500} @Error404 class NotFoundError extends DefaultLayout { def execute() { respondView() } } @Error500 class ServerError extends DefaultLayout { def execute() { respondView() } }
DefaultLayout を継承してコントローラを作成している。execute メソッドがコントローラ本体。この構成はぱっと見 PHP の Smarty にも見える。
コントローラクラスの定義が class である事を考えると、アクセスごとにインスタンスを生成している可能性がある?
てなことで、SiteIndex.scala の中身が
package quickstart.action import xitrum.annotation.GET import java.rmi.server.UID @GET("") class SiteIndex extends DefaultLayout { lazy val uid = new UID() def execute() { println(s"Instance UNIQUE ID: $uid") respondView() } }
で、起動(sbt run
)してログを,,,,,ってぇ!
8GB メモリのカスタム MacBookAir でまさかの OutOfMemory ... byobu + Chromeでタブ3枚でダメか…。
仕方ないので 16GB の窓8.1で再チャレンジ。
凄まじく大量の resolve... 落ちた原因はこれか。
で、起動して早速 http://localhost:8000 に...
[WARN] Caught exception java.lang.StringIndexOutOfBoundsException: String index out of range: 1 at java.lang.String.charAt(String.java:658) ~[na:1.7.0_45] at java.util.regex.Matcher.appendReplacement(Matcher.java:762) ~[na:1.7.0_45] at java.util.regex.Matcher.replaceAll(Matcher.java:906) ~[na:1.7.0_45] at java.lang.String.replaceAll(String.java:2162) ~[na:1.7.0_45] at xitrum.util.ClassFileLoader.loadClassData(ClassFileLoader.scala:39) ~[xitrum_2.11-3.14.jar:3.14] at xitrum.util.ClassFileLoader.findClass(ClassFileLoader.scala:26) ~[xitrum_2.11-3.14.jar:3.14] at xitrum.util.ClassFileLoader.loadClass(ClassFileLoader.scala:17) ~[xitrum_2.11-3.14.jar:3.14] at xitrum.handler.inbound.Dispatcher$.devDispatch(Dispatcher.scala:139) ~[xitrum_2.11-3.14.jar:3.14] at xitrum.handler.inbound.Dispatcher$.dispatch(Dispatcher.scala:68) ~[xitrum_2.11-3.14.jar:3.14] at xitrum.handler.inbound.Dispatcher.channelRead0(Dispatcher.scala:176) ~[xitrum_2.11-3.14.jar:3.14] at xitrum.handler.inbound.Dispatcher.channelRead0(Dispatcher.scala:154) ~[xitrum_2.11-3.14.jar:3.14] at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:103) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:332) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:318) [netty-all-4.0.20.Final.jar:4.0.20.Final] at xitrum.handler.inbound.MethodOverrider.channelRead0(MethodOverrider.scala:45) [xitrum_2.11-3.14.jar:3.14] at xitrum.handler.inbound.MethodOverrider.channelRead0(MethodOverrider.scala:19) [xitrum_2.11-3.14.jar:3.14] at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:103) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:332) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:318) [netty-all-4.0.20.Final.jar:4.0.20.Final] at xitrum.handler.inbound.UriParser.channelRead0(UriParser.scala:33) [xitrum_2.11-3.14.jar:3.14] at xitrum.handler.inbound.UriParser.channelRead0(UriParser.scala:16) [xitrum_2.11-3.14.jar:3.14] at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:103) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:332) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:318) [netty-all-4.0.20.Final.jar:4.0.20.Final] at xitrum.handler.inbound.WebJarsServer.channelRead0(WebJarsServer.scala:33) [xitrum_2.11-3.14.jar:3.14] at xitrum.handler.inbound.WebJarsServer.channelRead0(WebJarsServer.scala:23) [xitrum_2.11-3.14.jar:3.14] at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:103) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:332) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:318) [netty-all-4.0.20.Final.jar:4.0.20.Final] at xitrum.handler.inbound.PublicFileServer.channelRead0(PublicFileServer.scala:33) [xitrum_2.11-3.14.jar:3.14] at xitrum.handler.inbound.PublicFileServer.channelRead0(PublicFileServer.scala:23) [xitrum_2.11-3.14.jar:3.14] at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:103) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:332) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:318) [netty-all-4.0.20.Final.jar:4.0.20.Final] at xitrum.handler.inbound.BaseUrlRemover.channelRead0(BaseUrlRemover.scala:23) [xitrum_2.11-3.14.jar:3.14] at xitrum.handler.inbound.BaseUrlRemover.channelRead0(BaseUrlRemover.scala:11) [xitrum_2.11-3.14.jar:3.14] at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:103) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:332) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:318) [netty-all-4.0.20.Final.jar:4.0.20.Final] at xitrum.handler.inbound.Request2Env.sendUpstream(Request2Env.scala:283) [xitrum_2.11-3.14.jar:3.14] at xitrum.handler.inbound.Request2Env.channelRead0(Request2Env.scala:96) [xitrum_2.11-3.14.jar:3.14] at xitrum.handler.inbound.Request2Env.channelRead0(Request2Env.scala:44) [xitrum_2.11-3.14.jar:3.14] at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:103) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:332) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:318) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:163) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:332) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:318) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:787) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:125) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:507) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:464) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:378) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:350) [netty-all-4.0.20.Final.jar:4.0.20.Final] at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:116) [netty-all-4.0.20.Final.jar:4.0.20.Final] at java.lang.Thread.run(Thread.java:744) [na:1.7.0_45]
|||orz
俺が書いた部分が問題なのかと思って、修正した部分を削ったが状況は変わらず…。
ぱっと見 routing の名前解決で落ちてるのか?
てことで
package quickstart.action import xitrum.annotation.GET import java.rmi.server.UID @GET("/") class SiteIndex extends DefaultLayout { def execute() { respondView() } }
でもダメだった
org.fusesource.scalate.TemplateException: temp file rename failed
で起動失敗…2014/06/29 20:54 現在の master で発生…。
今のバージョン特有の問題なのか…?
バージョン番号は xitrum-new の 1.0-SNAPSHOT ...
SNAPSHOT だしなぁ
routing は慣れれば楽だが、class 数が爆発しそうな気がしてはいる。事実上の routing は annotation で指定している。
レスポンスの仕様は ここ 見ればわかるけど、結構綺麗だ。
テンプレートエンジンは scalate を使用。
同様に、このテンプレートの仕様を眺めていると、1アクション1テンプレート構成を徹底しているらしい。つまり、1アクションの中で複数のテンプレートを使いたければ、Routing アノテーションを付与しない複数の Action クラスを定義して、respondView[HomeAction_NormalUser]()
とか叩くらしい。
一つのアクションで複数のテンプレートを返すという挙動は設計的にどうなのとは思うけど、これはこれで冗長だなぁ。
Playframework の implicit マジックの雨霰とどっちがいいかは好みが分かれそうな。
HikariCP + ScalaActiveRecord + MySQL5.1 をやってみた
何故にメモかというと、HikariCP がそもそもサンプル無さ過ぎて地獄を見たから。
まずは基本的な ScalaActiveRecord アプリを作る
Play のサンプルアプリケーションをまずは作る。
play new HikariSample
ウィザードくらいは任せた。
で、速攻、 ScalaActiveRecord を突っ込む。
build.sbt はこれ
name := "HikariSample" version := "1.0-SNAPSHOT" libraryDependencies ++= Seq( jdbc, anorm, cache, "com.github.aselab" %% "scala-activerecord" % "0.2.3", "com.github.aselab" %% "scala-activerecord-play2" % "0.2.3", "mysql" % "mysql-connector-java" % "5.1.22" ) play.Project.playScalaSettings
そしたら、下記の様にまずは書く。
conf/play.plugins
9999:com.github.aselab.activerecord.ActiveRecordPlugin
conf/application.conf
# 下記を追加 db.activerecord.driver=com.mysql.jdbc.Driver db.activerecord.url="jdbc:mysql://localhost:3306/testdb" db.activerecord.user="root" db.activerecord.password="test" activerecord.schema=models.Tables
で、モデルは下記の通り。
package models import com.github.aselab.activerecord._ import com.github.aselab.activerecord.dsl._ case class Todo(name: String, description: String) extends ActiveRecord object Todo extends ActiveRecordCompanion[Todo] object Tables extends ActiveRecordTables { val todos = table[Todo] on(todos)(s => declare( s.description is(dbType("text")) )) }
controlllers/Application.scala
package controllers import play.api._ import play.api.mvc._ import play.api.data._ import play.api.data.Forms._ import models._ import com.github.aselab.activerecord.dsl._ object Application extends Controller { def form = Form(mapping( "name" -> nonEmptyText, "description" -> nonEmptyText )(Todo.apply)(Todo.unapply)) def index = Action { implicit request => Ok(views.html.index(form, Todo.all.toList)) } def post = Action { implicit request => val req = form.bindFromRequest() if (!req.hasErrors) { req.value.foreach(v => { v.save() }) } Ok(views.html.index(req, Todo.all.toList)) } }
conf/routes
GET / controllers.Application.index POST / controllers.Application.post GET /assets/*file controllers.Assets.at(path="/public", file)
そして、views/index.scala.html を
@(form: Form[models.Todo], todos: List[models.Todo]) @main("Welcome to Play") { @for(todo <- todos){ <div> <strong>@todo.name</strong>&nbsp; <span>@todo.description</span> </div> } @helper.form(action = routes.Application.index()) { @helper.inputText(form("name")) @helper.inputText(form("description")) <input type="submit" value="送信"/> } }
実験環境を作る
実験環境を色々するのめんどいので、Vagrant するか。Vagrant 知らない人はここ参照。*1
HikariSample ディレクトリの外で適当なディレクトリを掘って Vagrant コマンド。
vagrant init test https://github.com/2creatives/vagrant-centos/releases/download/v6.5.3/centos65-x86_64-20140116.box
デプロイめんどいから、適当に共有かけるか。
Vagrantfile 開いてごりごり
# -*- mode: ruby -*- # vi: set ft=ruby : # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! VAGRANTFILE_API_VERSION = "2" Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.box = "test" config.vm.box_url = "https://github.com/2creatives/vagrant-centos/releases/download/v6.5.3/centos65-x86_64-20140116.box" config.vm.network "private_network", ip: "192.168.33.10" config.vm.synced_folder "/Users/azalea/Documents/HikariSample", "/vagrant_data" end
で、実行環境を整えて
vagrant up vagrant ssh sudo su - yum install -y mysql-server wget yum install -y java-1.7.0-openjdk-devel service mysqld start mysql -u root create database testdb SET PASSWORD FOR root@localhost=PASSWORD('test'); exit
後は起動するだけ。
play とか sbt めんどいので、sbt dist
しておく。
cd /vagrant_data/ cd /opt cp /vagrant_data/target/universal/hikarisample-1.0-SNAPSHOT.zip ./ unzip hikarisample-1.0-SNAPSHOT.zip cd hikarisample-1.0-SNAPSHOT ./bin/hikarisample
http://192.168.33.100/ でアクセスできれば OK
HikariCP に乗り換える
HikariCP ってなんぞ?と思ったらここをクリック
要するに BoneCP がいつまで経ってもバグだらけだから嫌がって作ったそうな。
ScalaActiveRecord の Play plugin は playframwork からコネクションを拾ってるから、バックエンドが変わるくらいは問題なく動作できる。
まずは build.sbt
を編集。
name := "HikariSample" version := "1.0-SNAPSHOT" resolvers += Resolver.url("Edulify Repository", url("http://edulify.github.io/modules/releases/"))(Resolver.ivyStylePatterns) libraryDependencies ++= Seq( jdbc, anorm, cache, "com.github.aselab" %% "scala-activerecord" % "0.2.3", "com.github.aselab" %% "scala-activerecord-play2" % "0.2.3", "com.edulify" % "play-hirakicp_2.10" % "1.0.0", "mysql" % "mysql-connector-java" % "5.1.22" ) play.Project.playScalaSettings
次に application.conf でデフォルトの DBPlugin(BoneCP) を停止 & DB設定。
dbplugin=disabled # db.activerecord.driver=com.mysql.jdbc.Driver # db.activerecord.url="jdbc:mysql://localhost:3306/testdb" # db.activerecord.user="root" # db.activerecord.password="test" db.activerecord.hikaricp.file="conf/hikaricp.prod.properties"
hikaricp.prod.properties
には下記設定
dataSourceClassName=com.mysql.jdbc.jdbc2.optional.MysqlDataSource dataSource.url=jdbc:mysql://localhost:3306/testdb dataSource.user=root dataSource.password=test dataSource.cachePrepStmts=true dataSource.prepStmtCacheSize=250 dataSource.prepStmtCacheSqlLimit=2048 dataSource.useServerPrepStmts=true connectionTestQuery=SELECT 1 connectionInitSql=SELECT 1 maximumPoolSize=20
お次は conf/play.plugin
に追加。
1500:com.edulify.play.hikaricp.HikariCPPlugin
これで準備は整った。
ちなみに、maximumPoolSize はMySQL の設定にあわせておく事。 (デフォルトでは 150 あったはず)
これで dist して再度起動。
[root@vagrant-centos65 hikarisample-1.0-SNAPSHOT]# ./bin/hikarisample Play server process ID is 3020 [info] application - Loading Hikari configuration from Play configuration. [info] application - Loading from file configured by db.default.hikaricp.file that is Some(conf/hikaricp.prod.properties) [info] application - Loading Hikari configuration from conf/hikaricp.prod.properties [info] application - Properties: {dataSource.useServerPrepStmts=true, dataSource.user=root, dataSource.password=test, dataSourceClassName=com.mysql.jdbc.jdbc2.optional.MysqlDataSource, connectionTestQuery=SELECT 1, maximumPoolSize=20, connectionInitSql=SELECT 1, dataSource.prepStmtCacheSqlLimit=2048, dataSource.prepStmtCacheSize=250, dataSource.cachePrepStmts=true, dataSource.url=jdbc:mysql://localhost:3306/testdb} [error] n.s.e.Cache - Unable to set localhost. This prevents creation of a GUID. Cause was: vagrant-centos65.vagrantup.com: vagrant-centos65.vagrantup.com: Name or service not known java.net.UnknownHostException: vagrant-centos65.vagrantup.com: vagrant-centos65.vagrantup.com: Name or service not known at java.net.InetAddress.getLocalHost(InetAddress.java:1473) ~[na:1.7.0_55] at net.sf.ehcache.Cache.<clinit>(Cache.java:214) ~[net.sf.ehcache.ehcache-core-2.6.6.jar:na] at net.sf.ehcache.config.ConfigurationHelper.createCache(ConfigurationHelper.java:296) [net.sf.ehcache.ehcache-core-2.6.6.jar:na] at net.sf.ehcache.config.ConfigurationHelper.createDefaultCache(ConfigurationHelper.java:219) [net.sf.ehcache.ehcache-core-2.6.6.jar:na] at net.sf.ehcache.CacheManager.configure(CacheManager.java:722) [net.sf.ehcache.ehcache-core-2.6.6.jar:na] at net.sf.ehcache.CacheManager.doInit(CacheManager.java:439) [net.sf.ehcache.ehcache-core-2.6.6.jar:na] Caused by: java.net.UnknownHostException: vagrant-centos65.vagrantup.com: Name or service not known at java.net.Inet6AddressImpl.lookupAllHostAddr(Native Method) ~[na:1.7.0_55] at java.net.InetAddress$1.lookupAllHostAddr(InetAddress.java:901) ~[na:1.7.0_55] at java.net.InetAddress.getAddressesFromNameService(InetAddress.java:1293) ~[na:1.7.0_55] at java.net.InetAddress.getLocalHost(InetAddress.java:1469) ~[na:1.7.0_55] at net.sf.ehcache.Cache.<clinit>(Cache.java:214) ~[net.sf.ehcache.ehcache-core-2.6.6.jar:na] at net.sf.ehcache.config.ConfigurationHelper.createCache(ConfigurationHelper.java:296) [net.sf.ehcache.ehcache-core-2.6.6.jar:na] [info] application - Starting HikariCP connection pool... [info] application - database [activerecord] connected at jdbc:mysql://localhost:3306/testdb [info] play - Application started (Prod) [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000
てなことで起動した。
*1:Version若干古いけど、使い方変わらんから大目に見て
サーバに色々デプロイしようとしてハマった記録
Chef でローカル仮想マシン上に目的のサービスがデプロイできるようになったので、リモートサーバへのデプロイを実行してみた。
その時、かなり色々つまづいたのでメモ。
iptables は一時的でも切るしかない
ssh ポート以外にも何かのポートを使っているらしい。
iptables が入ってると、細かい原因もログも残らないまま「Connection refused」で悩むことになる。
しかも ssh は通る上に、knife solo cook [アカウント@][ホスト] [ホスト.json] -p [SSHポート]
を指定するものだから、SSHしか使わないと勘違いしやすい。
MySQLServer は過去データ、設定も含め消してから
http://community.opscode.com/cookbooks/mysql
yum コマンドでインストールした MySQLServer は yum remove mysql-server
で消してもDBファイルが残る。
しかも MySQL のアカウントは DB に保存されているので、Chef で MySQL を突っ込むと復活した挙句、root パスワードが一致しないとか、root@127.0.0.1
アカウントを変更できない(消してたりした場合)で先に進まなくなる。
しかもこの時出る ruby スタックにはSQLファイルの中身は表示されないので、気づくまでハマる。
カン所は STD エラーの方を眺めて見ると、sql ファイルを生成して食わせているのが分かるので、ssh で入ってファイルを眺めれば分かる。
SELinux の有無は確認しよう
入ってるかどうか分からずに chef を実行すると色々エラーを喰らわせられる。
環境を確認しないと痛い目を見る。
結論
色々いじっている環境に後から Chef を突っ込むのは色々ハマる。
大分長い間運用している状態なら、いい加減サーバを組み立て直した方が良いかもしれない。
後、CentOS の cookbook は案外 6.x 以上のものがあるので、5.x 系ならサーバ移動を考えるべきかもしれない。
まぁ何にしろ当座はこれで動かしてみようか。
Chef で CentOS に Playframework2.2.x アプリをデプロイする
CentOS5.10 で動作確認。多分 CentOS 6.x もそんな変わらない筈。
段階を踏もうか。
- Play アプリをリリース用にコンパイルする
- Chef を使ってJavaを突っ込む
- Chef でデプロイ用 Cookbook を作って配置する
- 起動スクリプトを書いてデプロイする
knife solo cook xxxx
でぶち込む
という順番。
Play アプリをリリース用にコンパイルする
これは簡単。
アプリケーションホームディレクトリで play clean dist
と叩くだけ。
すると、コンパイルした上で、配布用の zip を target/universal ディレクトリに applicationName-Version.zip
なファイルが出来上がる。
デプロイするときにバージョン名は地味に邪魔だけど、まぁこれは Chef 側の設定でどうにかする。
尚、この zip を解凍して bin/applicationName を叩くとサーバが起動する。
残念な事に、デーモン起動ではないので、終了までコントロールを奪ってくれる素敵仕様だ。
これはログ設定で標準出力には吐かず、ログにだけメッセージを吐くようにし、bin/applicationName &
で逃れる。
PID ファイルとかは Play が勝手に吐くので気にしない。
Chef を使って Java を突っ込む
まずは chef リポジトリ(kitchen)を作る。
前提:chef + knife solo + Berkshelf インストール済み。Ubuntu ホストはこっち参照
knife solo init chef
とでも叩くと、chef ディレクトリが作られて、そこに kitchen が作られる。
とりあえずは Java を突っ込もうか。
chef/Berksfile
が作られてる筈なので、これを開いて編集。
source "https://api.berkshelf.com" cookbook 'yum' cookbook 'java'
コンソール上で下記を叩く。
cd chef berks vendor cookbooks
すると、berkshelf が Java の cookbook を落としてくる。
次に、chef/nodes
ディレクトリに、インストール設定を突っ込む。
vi nodes/default.json { "java":{ "jdk_version":"7" }, "run_list":[ "recipe[java]" ] }
これで knife solo cook [user]@[server] default.json
と実行すれば、OSはともかく Java7 が入る。
詳細オプションは opscode community 見てほしい。
Chef でデプロイ用 Cookbook を作って配置する
次にデプロイ用 cookbook を作ろうか。
まず、chef ディレクトリ上で下記のコマンドを叩こう。
knife cookbook create app -o site-cookbooks/
すると、chef/site-cookbooks/app
ディレクトリが作られている筈。
このディレクトリが cookbook の一つの単位。
ここの、chef/site-cookbooks/app/files/default
ディレクトリに、最初の手順で作成した applicationName-version.zip
をデプロイしてしまおう。
で、もちろんこの「-version」の部分はバージョンアップの度に変わるので、設定ファイルに吐き出してしまう。
vi chef/site-cookbooks/app/attributes/default.rb default['app'][:version] = "0.1-Beta"
もちろん、これは前述の nodes/default.json
で下記のように書いてもいい。
{ "java":{ "jdk_version":"7" }, "app":{ "version":"0.1-Beta" }, "run_list":[ "recipe[java]", "app" ] }
次に、このファイルをデプロイするコードを書く。
参考:ChefでCookbookを作成するときのちょっとしたコツ 9選 - インフラエンジニアway - Powered by HEARTBEATS
vi chef/site-cookbooks/app/recipes.default.rb # # Cookbook Name:: app # Recipe:: default # # Copyright 2014, white-azalea.net # # All rights reserved - Do Not Redistribute # # ディレクトリは無ければ作る directory "/opt" do owner "root" group "root" mode 00600 action :create end directory "/opt/app" do owner "root" group "root" mode 00644 action :create end # node プロパティに設定が入っている fileName = "applicationName-" + node.app.version + ".zip" deployName = "/opt/app/" + fileName # cookbook_file で files 内のファイルをデプロイ # あれば何もしない cookbook_file deployName do source fileName mode "0755" action :create not_if { File.exist?(deployName) } end # 解凍、execute で shell 叩ける # 解凍済みなら README があるので、その場合は何もしない execute ("unzip " + deployName) do cwd "/opt/app/" action :run not_if { File.exist?("/opt/app/applicationName-" + node.app.version + "/README") } end
起動スクリプトを書く
これが一番難儀した。
参考:
CentOSでデーモンの起動スクリプトを書く | taichino.com
Technical Memorandum: CentOS-5 起動スクリプトのスケルトン
まずは、chef/site-cookbooks/app/templates/default/appservice.erb
を下記の内容で作成した。
#!/bin/bash # chkconfig: 345 98 20 # description: application service start script. # processname: appservice # # /etc/rc.d/init.d/appservice # # Sample application service start script. # # Source function library. . /etc/init.d/functions SERVER_PATH="/opt/uptext/applicationName-<%= node['app']['version'] %>" SERVER_BIN=$SERVER_PATH + "/bin/applicationName" PID_PATH=$SERVER_PATH + "/RUNNING_PID" start() { echo -n "Starting applicationName: " if [ -f $PID_PATH ] then echo "Server already running" else `$SERVER_BIN &` fi return 0 } stop() { echo -n "Shutting down applicationName: " kill `cat $PID_PATH` return 0 } case "$1" in start) start ;; stop) stop ;; restart) stop start ;; *) echo "Usage: appservice {start|stop|restart}" exit 1 ;; esac exit $?
基本的に erb なので分かるはず。
尚、「chkconfig」からの3行のコメントはおまじない。これを設定しないと、自動起動に登録できません。
次に、この設定をデプロイと起動、OS起動時の自動起動まで指定する。
chef/site-cookbooks/app/recipes.default.rb
に下記を追記。
# appservice を起動スクリプトディレクトリに配置 template "/etc/init.d/appservice" do group "root" owner "root" mode 0744 source "appservice.erb" end # appservice を起動 service "appservice" do action :start end # OS 起動時に appservice を起動するよう指定 service "appservice" do action :enable end
knife solo cook xxxx
でぶち込む
後は分かるはず。
CentOS5.10 の Vagrant box を作り直し
https://dl.dropboxusercontent.com/u/717203/centos5.10-final.box
これ。
名称は変わってない。
キチンと halt とかその他が走るようになってる。
6/23 追記:メンテナンスだるいので削除しました