Scalaでのバッチの開発、デプロイについて

みなさん、こんにちは。アドウェイズの波切です。
最近、広告システムの開発でScala」を使い始めたので、Scalaの記事を書いてみようかと思います。

scala


「みなさんScalaでバッチプログラム(CLI)のデプロイどうやってますか?」

ScalaのWebフレームワークである「Play framework」だと、sbtからdistコマンドや、
stage
コマンドを使用することで、jarファイルや起動スクリプトをまとめることが出来るため、簡単にデプロイが行えますが、CLIにはこのような仕組みがありません。

今回の記事は、この問題をどう解決したかについてお伝えしようかと思います。

(1)Scalaで開発したコマンドラインプログラムのデプロイ

Scala言語でのデプロイは、LL言語のデプロイと違って、ソースコードをデプロイするのではなく、ビルドした成果物をデプロイする点が異なります。

「Play framework」のようにデプロイ用のコマンドがあると良いと思い調べたところ、
「sbt-native-packager」でできることが分かりました。(Playも内部的には、これを使っているようです。)

これを導入することで、簡単に開発したプログラムをサーバにデプロイ出来るようになります。

・導入方法

1. project/plugins.sbtに下記を追加する。
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.3")


2. build.sbtにenablePlugins(JavaAppPackaging)を加える。 

name := """cli"""
 
version := "1.0"
 
scalaVersion := "2.11.6"
 
// Change this to another test framework if you prefer
libraryDependencies += "org.scalatest" %% "scalatest" % "2.2.4" % "test"
 
enablePlugins(JavaAppPackaging)
 
// Uncomment to use Akka
//libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.3.11"
・使い方

プラグインの導入に成功すると、sbtからstagedistなどのコマンドが使えるようになります。
下の例は、stageコマンドですが、このコマンドを使うと、Scalaのプログラムを実行できるシェルと依存するすべてのライブラリをまとめてくれます。

 stageコマンドの実行
$ activator stage
[success] Total time: 1 s, completed 2015/06/13 20:37:17
 
$ tree target/universal/stage
target/universal/stage
├── bin
│   ├── cli
│   └── cli.bat
└── lib
    ├── default.cli-1.0.jar
    └── org.scala-lang.scala-library-2.11.6.jar
 
2 directories, 4 files
$ target/universal/stage/bin/cli
Hello, world!


この状態で「target/universal/stage」以下のディレクトリをそのまま対象となるサーバにコピーすれば、デプロイ完了という訳です。

(2)コマンドラインプログラムごとにプロジェクトを作らないといけないのか?

さて、CLIプログラムをコンパイルしてデプロイすることはできるようになりましたが、
「sbt-native-packager」では、1プロジェクトにつき、1つの実行コマンドの生成する機能しかありません。CLIのコマンドが増えた場合は、どうすれば良いのでしょうか?

1つの解決策は、コマンドが増えた時に、プロジェクトを増やすことですが、
これでは、めんどくさい(DRYじゃない)し、1コマンドごとに全ての依存ライブラリをコピーすることになるので、ディスク容量的にも無駄が大きく、あまり良い方法には思えません。

これ悩んでる人いるだろうなと思って、良い解決方法を求めてググってみたのですが、
調べた限り無いようだったので、結局ウチのチームでは、gitsvnでよく見かける、「サブコマンド方式」で実装することにしました。

サブコマンド方式での実装が負担になっては本末転倒なので、コマンドを簡単に実装出来るよう、仕組みを用意して、2ステップでコマンドを追加出来るようにしました。

下記のようなイメージです。

例 hogeサブコマンドを加える

1. HogeCommandクラスを作成 する。
package scalar.cli.commands

import scalar.cli.{Command, CLI, CommandResult}

case class HogeCommand(manager : CLI) extends Command {
  def name = "hoge"
  def description = "print hoge"

  override def execute(args : Seq[String]) = {
    println("hoge")
    CommandResult.SUCCESS
  }
}

2. メイン処理の開始時にサブコマンドを渡して実行する。
import scalar.cli._
import scalar.cli.commands.{HelpCommand, DailyReportCommand, HogeCommand, CLILauncherCommand}
 
object Main extends App {
  val status = CLI(
    subCommands = List(
      HelpCommand,
      DailyReportCommand,
      HogeCommand
    )
  ).execute(args)
 
  if (status != CommandResult.SUCCESS) {
    sys.exit(status)
  }
}

↓こんな感じでコマンドが追加されます。
$ ./cli 
Usage: cli [options] [<command>] [<args>]

  -h | --help
        show help message.
  -b | --banner
        print message and timestamp on startup and shutdown.

Currently available commands are :
  daily_report         create or update daily report
  help                 shows usage of command
  hoge                 print hoge

$ ./cli hoge
hoge


ちょうど、WebのプログラムでControllerとRouterに追加するようなイメージでサブコマンドが追加できます。ソースをおいて置くので、興味のある方は見ていただければと思います。

というわけで、CLIプログラムでの開発・デプロイの問題点も解消して、今はバリバリScalaのプログラムを開発しています。

目下、Scalaでの最大の悩みは、コンパイル速度が遅い」ことと、
Perl以上に、「That's more than one way to do it.」な言語なので、開発現場で使うべき機能の取捨選択」です。

特に後者は、便利さやコンパクトさを追求した結果、「他の人から見たらわかりづらくなってしまった」とか、そもそも「いろいろな実装手段があるので、悩む時間が長い」など生産性の面では深刻だったりします。

とはいえ、新しいモノに取り組んでいく過程では、こういったことはよくあることなので、日々チームで話し合いを重ねたり、ルールを決めたりしてやっています。

Scalaの開発でまた、トピックがあればご紹介していきたいと思います。
それでは、また!