ラベル sbt の投稿を表示しています。 すべての投稿を表示
ラベル sbt の投稿を表示しています。 すべての投稿を表示

2015年4月27日月曜日

Finagle+Karaf+Dockerでmicroservices

流行りなのでmicroservicesという用語を使ってみましたが、意図としては(microservicesも含む)マルチサーバー構成によるクラウド・システムを構成するサービス群がターゲットです。

この実行基盤としてFinagle、Karaf、Dockerの組合せが有力ではないかということで、試しにサンプルプロジェクトを作ってみました。このサンプルプロジェクトは、新しいクラウド・システム内サービスを作る時の雛形を想定しています。

このサンプルプロジェクトを実際に作ってみることで、Karafがどのぐらいの手間で使えるのか、ScalaやFinagleとの相性はどうなのか、といったことを試してみるのが目的です。この手のプロジェクト構築ではライブラリ間の依存関係の解決が難しいのですが、Finagleをリンクしても大丈夫な設定を発見できたので、この問題はクリアできていると思います。

以下ではクラウド・システム内サービスをサービスの粒度によらずmicroserviceと呼ぶことにします。

Apache Karaf

Apache Karafは軽量OSGiコンテナです。

Karafを導入する目的は大きく以下の3つです。

  • microserviceの環境設定共有
  • dependency hell対策
  • CamelによるEIP(Enterprise Integration Patterns)
環境設定共有

第一の目的はクラウド・システムを構成するmicroservice群の環境設定を共通化することです。

microserviceをmainメソッドを作るなどして自前でデーモン化すると各種設定もすべて自前で行わなければならなくなります。

たとえば、SLF4J+fluentedなどによるロギング、JMX(Java Management Extensions)による運用監視、JNDI(Java Naming and Directory Interface)によるディレクトリ管理といった各種設定をサービス毎に設定する必要があります。

これは(1)設定そのものが大変な手間、(2)クラウド・システム内の共通設定情報の共有の手間、(3)設定漏れの危険、(4)ハードコーディングによるカスタマイザビリティの欠如、といった問題があります。

microserviceをコンテナ内で動作させることで、各種設定はコンテナに対する設定として共通化する上記の問題を解消することができます。

この目的で導入するコンテナとして、軽量OSGiコンテナであるKarafがよいのではないか考えています。

dependency hell対策

Java VM上での開発における未解決問題の一つにdependency hellがあります。

この問題はJava 9でModule機能として解決される見込みですが、当面の対策としてはOSGiコンテナを使用するのが現実解となっています。

最近はちょっとしたライブラリをリンクしても、その裏でApache Commons、Spring、JBossといった巨大なライブラリがついてくることが多いので、思ったより切実な問題です。

CamelによるEIP

Karafを採用したボーナスのようなものですが、KarafからはCamelを簡単に使えるようになっています。(KarafはESB(Enterprise Service Bus)のServiceMixのOSGiコンテナ部を切り出して製品化したものです。)

Camelを使うと、EIPのパターンを使用して様々な通信プロトコルを使って外部サービスと連携する処理を簡単に作成することができます。またCamelにはScala DSLも用意されています。

Twitter Finagle

Twitter Finagleはmicroservices指向のRPCシステムです。

FinagleはFutureモナドによる非同期実行、耐故障性の抽象化を行っており、Monadic Programmingとの相性がよいのもFunctional Reactive Programming指向のmicroservicesの通信基盤として魅力です。

準備

Karafを使うためのベースとしてKarafの実行環境をDockerイメージ化しました。

Karaf Docker Image

Dockerイメージを作成するプロジェクトは以下になります。

Dockerfileだけの簡単なプロジェクトです。

実際にmicroserviceをインストールして使うことを想定しているので、余分な設定は行わずプレインな簡単なものにしています。

Docker Hub

このDockerイメージをDocker Hubに登録しました。

以下のようにして利用できます。

$ docker pull asami/karaf-docker

サンプル・プロジェクト

サンプル・プロジェクトは以下のGitHubプロジェクトとして作成しました。

サンプル・サーバー

Finagleによるサンプル・サーバーは以下のものです。

package sample

import com.twitter.finagle.{Http, Service, ListeningServer}
import com.twitter.util.{Await, Future}
import java.net.InetSocketAddress
import org.jboss.netty.handler.codec.http._

object Server {
  val service = new Service[HttpRequest, HttpResponse] {
    def apply(req: HttpRequest): Future[HttpResponse] =
      Future.value(new DefaultHttpResponse(
        req.getProtocolVersion, HttpResponseStatus.OK))
  }

  def start(): ListeningServer = {
    Http.serve(":8080", service)
  }

  def stop(server: ListeningServer) {
    server.close()
  }

  def main(args: Array[String]) {
    val server = start()
    Await.ready(server)
  }
}
ビルド

ビルドは以下の手順で行います。

  • Karaf KAR
  • Dockerfile
  • Dockerイメージ
Karaf KAR

Karafに配備するKARファイルの作成にはkarafタスクを使用します。

$ sbt karaf

karafタスクはbuild.sbtで定義しています。

Dockerfile

KARファイルを配備したKaraf実行環境のDockerイメージを作成するためのDockerfileの作成にはdockerタスクを使用します。

$ sbt docker

dockerタスクはbuild.sbtで定義しています。

Dockerイメージ

Dockerイメージの作成は以下になります。

$ docker build -t sample-finagle-karaf-docker .

dockerタスクを実行するとDockerfileが作成されるので、このDockerfileでDockerイメージをビルドします。この時、前述したDockerイメージasami/karaf-dockerを内部的に使用します。

実行

作成したDockerイメージsample-finagle-karaf-dockerを実行してみましょう。

$ docker run -t -i --rm -p 1099:1099 -p 8101:8101 -p 44444:44444 -p 8080:8080 sample-finagle-karaf-docker

ポートは1099, 8101, 44444, 8080の4ポートを使用します。

ポート1099, 8101, 44444はKarafが使用するポートです。

ポート用途
1099JMX RMI registry
8101SSHコンソール
44444JMX RMI server

ポート8080はサンプルのFinagleサービスが使用するポートです。

確認

Dockeイメージを実行するとKarafコンテナ内で自動的にサンプルのFinagleサービスが起動されます。

curlコマンドでサンプルのFinagleサービスにアクセスすると以下の結果が得られます。無事動作していることが確認できました。

$ curl http://192.168.59.103:8080 -v
* Rebuilt URL to: http://192.168.59.103:8080/
* Hostname was NOT found in DNS cache
*   Trying 192.168.59.103...
* Connected to 192.168.59.103 (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.37.1
> Host: 192.168.59.103:8080
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Length: 0
< 
* Connection #0 to host 192.168.59.103 left intact

感想

現時点だとSBTからKaraf向けのKARファイルの作成のための設定が大変なので、まだまだKarafは気軽には使えないような感じです。

ただAPPREL CLOUD級の規模になってくると設定の大変さよりも運用管理上のメリットの方が大きくなるのでそろそろ導入を考えてもよさそうです。

諸元

  • Scala 2.10.5
  • Finagle 6.25.0
  • Karaf 4.0.0.M2
  • Docker 1.3.2
  • sbt 0.13.7

2012年9月11日火曜日

scala-sbt.g8

Scala界ではプロジェクトの雛形生成器?としてgiter8が有名で、Typesafe Stack 2.0でも使われています。

Installing the Typesafe Stack 2.0.2にはscalaプロジェクトの雛形も多数用意されていますが、プレインなsbtプロジェクトとしてはscala-sbtが用意されています。

以下のようにするとsbtプロジェクトの雛形を簡単に構築できます。

$ g8 typesafehub/scala-sbt

オレ様scala-sbt

実際に自分プロジェクトで使うとプレインバニラな雛形は物足りなくなるので、各所でオレ様scala-sbtが多数作られていると思います。

ボクも自分用に作ってみたので紹介します。自分用にカスタマイズしたポイントは以下になります。(注意:個人向けなので、普通に使って便利なのかは保証の限りではありません。(笑))

プラグインの設定

sbtプラグインとして以下のものの設定を行いました。

  • ensime
  • eclipse plugin
  • conscript
  • onejar
プログラムの雛形

生成するプログラムの雛形として以下の機能を盛り込みました。ちょっとした開発ツールを作る時の土台にすることを想定しています。

  • scalaz
  • scalax.io
  • オプションのハンドリング
  • コマンドの切り分け
  • エラー処理
  • Scalatest

mainのソースコードは以下のようになります。

package com.example.command

import scala.util.control.Exception.{catching, allCatch}
import scalaz.{Resource => _, _}, Scalaz._
import scalax.io._
import scalax.file._

class Command(val args: Array[String]) {
  var verbose: Boolean = false
  var directory: Option[String] = None

  def run() {
    catching(classOf[java.io.IOException]).withApply {
      e => _error(e.getMessage)
    } apply {
      _run()
    }
  }

  private def _run() {
    _parse_options(args.toList) match {
      case "ls" :: Nil => _ls()
      case "cat" :: file :: Nil => _cat(file)
      case _ => _usage
    }
  }

  private def _error(msg: String) {
    Console.err.println(msg)
  }

  private def _parse_options(args: List[String]): List[String] = {
    _parse_options(args, Nil)
  }

  private def _parse_options(in: List[String], out: List[String]): List[String] = {
    in match {
      case Nil => out
      case "-verbose" :: rest => {
        verbose = true
        _parse_options(rest, out)
      }
      case "-dir" :: arg :: rest => {
        directory = arg.some
        _parse_options(rest, out)
      }
      case arg :: rest => _parse_options(rest, out :+ arg)
    }
  }

  private def _usage {
    println("usage: sample command args")
  }

  private def _ls() {
    for (path <- Path(directory | ".").children("[^.]*", nil)) {
      println(path.name)
    }
  }

  private def _cat(file: String) {
    println(Resource.fromFile(file).string)
  }
}

Scalatestによるテストケースは以下のようになります。

package com.example.command

import org.scalatest.WordSpec
import org.scalatest.matchers.ShouldMatchers
import org.scalatest.junit.JUnitRunner
import org.junit.runner.RunWith

@RunWith(classOf[JUnitRunner])
class CommandSpec extends WordSpec with ShouldMatchers {
  "Command" should {
    "execute" that {
      "empty arguments" in {
        val app = new Command(Array())
        app.run()
      }
      "one argument" in {
        val app = new Command(Array("ls"))
        app.run()
      }
    }
  }
}

使い方

giter8

giter8のインストール方法はトップページにありますが、ここには載っていないMacPortsでもインストールできます。ボクはMacPortsでインストールしました。

$ sudo port install giter8
scala-sbtの雛形生成

オレ様版scala-sbtの雛形生成は以下になります。

$ g8 asami/scala-sbt

以下のようにパラメタを聞いてくるので入力すると雛形が生成されます。

organization [com.example]: 

package [com.example.command]: 

name [command]: 

scala_version [2.9.2]: 

version [0.1-SNAPSHOT]: 
コンパイル&テスト

雛形が生成されるので、コンパイル&テストしてみます。

$ cd command
$ sbt test

最後に以下のようなメッセージが出て正常終了するはずです。

[info] CommandSpec:
[info] Command 
[info]   should execute that 
[info]   - empty arguments
[info]   - one argument
[info] Passed: : Total 2, Failed 0, Errors 0, Passed 2, Skipped 0
[success] Total time: 13 s ...
ensime

ensimeの環境設定は以下になります。

$ sbt "ensime generate"
eclipse

Eclipseの環境設定は以下になります。

$ sbt eclipse

これで開発はEmacs&ensime、デバッグはEclipseの最強の組合せでScalaプログラミングできる環境を簡単に整えることができます!(笑)