Apache Zeppelinでデータ分析を分散処理する - Part 5: Ambari on DockerとsbtのScalaビルド環境

　[Part 2](http://qiita.com/masato/items/911e06fd31202181f0fd)でセットアップしたAmbari on DockerとApache Zeppelinの環境にsbtを使ったビルド環境をDockerで作ってみます。ScalaのコードもZeppelinの画面上で書いて実行したほうが簡単なのですが、Sparkアプリのjarを`spark-submit`する環境も作っておきます。

## このシリーズ

* [Apache Zeppelinでデータ分析を分散処理する - Part 1: データ分析のライフサイクル](http://qiita.com/masato/items/b66930a2ab8d888df5ff)
* [Apache Zeppelinでデータ分析を分散処理する - Part 2: Ambari on DockerにZeppelinをセットアップする](http://qiita.com/masato/items/911e06fd31202181f0fd)
* [Apache Zeppelinでデータ分析を分散処理する - Part 3: ZeppelinでHiveを使えるようにする](http://qiita.com/masato/items/78a45cf9b6984552d4d9)
* [Apache Zeppelinでデータ分析を分散処理する - Part 4: Ambari on Dockerのambari-functionsを使ってみる](http://qiita.com/masato/items/b879b84486aeb7d3d6d8)


## プロジェクト

　プロジェクトのディレクトリを作成します。

```bash
$ mkdir -p ~/scala_apps/spike
$ cd !$
```

### DockerとDocker Compose

　sbtのビルド環境はDockerとDocker Compose上に構築します。コンテナは使い捨て、プロジェクトのディレクトリはDockerホストからマウントします。成果物のjarもDokcerホスト上に作成されます。ScalaのコーディングはDockerホスト上のエディタか、別のコーディング用コンテナで行う想定です。

### Dockerfile

　Dockerfileはオフィシャル[java](https://hub.docker.com/_/java/)のOpenJDK 7をベースにします。[Apache Ivy](http://ant.apache.org/ivy/)のartifactsのキャッシュディレクトリとsbtのグローバルセッティングのディレクトリをボリュームにします。この2つは実行時にDockerホストのディレクトリをマウントして永続化します。sbtを始めて実行する時にartifactsをたくさんダウンロードするのですが、これが結構時間がかかります。

```bash:Dockerfile
FROM java:openjdk-7-jdk
MAINTAINER Masato Shimizu <ma6ato@gmail.com>

ENV SBT_VERSION 0.13.9
ENV PATH ${PATH}:/usr/local/sbt/bin

WORKDIR /app

RUN wget -O- http://dl.bintray.com/sbt/native-packages/sbt/$SBT_VERSION/sbt-$SBT_VERSION.tgz | tar xz -C /usr/local

VOLUME ["/root/.ivy2", "/root/.sbt", "/app"]
```

### docker-compose.yml

　docker-compose.ymlファイルにコンテナを起動するときの設定を書いておきます。Ambari on Dockerで構成するHadoopクラスタのConsulを使ったDNSを外部から利用しています。sbtコンテナでビルドしたjarファイルを[WebHDFS](https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/WebHDFS.html)経由でPUTするときにHadoopノードの名前解決が必要になります。

```yaml:docker-compose.yml
sbt:
  build: .
  volumes:
    - .:/app
    - /temp/.sbt:/root/.sbt
    - /temp/.ivy2:/root/.ivy2
  dns:
    - $AMB_CONSUL
    - 172.17.0.1
    - 8.8.8.8
  dns_search:
    - service.consul
```

　Dockerイメージをbuidします。

```bash
$ docker-compose build
```

## Scala REPL

　Scalaのインタープリターを`sbt console`コマンドから起動してみます。`AMB_CONSUL`環境変数はREPLの起動では使いませんが、docker-compose.ymlに定義してある環境変数が見つからないと警告が出るため最初にexportしておきます。

```bash
$ export AMB_CONSUL=$(docker inspect --format="{{ .NetworkSettings.IPAddress }}" amb-consul)
$ docker-compose run --rm sbt sbt console
[info] Set current project to Sample app (in build file:/app/)
[info] Starting scala interpreter...
[info]
Welcome to Scala version 2.10.5 (OpenJDK 64-Bit Server VM, Java 1.7.0_91).
Type in expressions to have them evaluated.
Type :help for more information.

scala> :quit
```

## sbtコンテナ

　Scalaのコードを配置するディレクトリを作成します。

```bash
$ cd ~/scala_apps/spike
$ mkdir -p src/main/scala/
```

### SampleApp.scala

　SparkのLICENSEファイルの行数を数える単純なSparkアプリを書きます。

```scala:src/main/scala/SampleApp.scala
import org.apache.spark.{SparkContext, SparkConf}

object SampleApp {
  def main(args: Array[String]) {
    val conf = new SparkConf().setAppName("App")
    val sc = new SparkContext(conf)

    val src = sc.textFile("LICENSE")
    val lines = src.count()

    println("Lines %,d".format(lines))
  }
}
```

### build.sbt

　プロジェクトのビルド定義を書きます。sbtの0.13.9に合わせてScalaは2.10.5を指定します。HDP 2.3と同じSpark 1.4.1をビルド用に使います。今回は最初なので`spark-submit`するjarは[sbt assembly](https://github.com/sbt/sbt-assembly)を使ったuberjarではありません。

```scala:build.sbt
name := "Sample app"

version := "1.0"

scalaVersion := "2.10.5"

libraryDependencies += "org.apache.spark" %% "spark-core" % "1.4.1"
```


### sbt package

　プロジェクトのディレクトリはScalaのソースコードを含めて以下のようになりました。

```bash
$ tree
.
├── Dockerfile
├── build.sbt
├── docker-compose.yml
└── src
    └── main
        └── scala
            └── SampleApp.scala
```

　sbtコンテナを使ってSparkアプリをビルドします。`target/scala-2.10/sample-app_2.10-1.0.jar`ファイルがDockerホスト上に作成されました。

```bash
$ docker-compose run --rm sbt sbt package
[info] Set current project to Sample app (in build file:/app/)
[info] Packaging /app/target/scala-2.10/sample-app_2.10-1.0.jar ...
[info] Done packaging.
[success] Total time: 1 s, completed Dec 26, 2015 4:13:52 AM
```


### HDFS

　ここからはAmbari on Dockerで構成されたHadoopクラスタのコンテナを使います。HDFSのNameNodeである`amb1`ノードを使います。

　今回はrootユーザーのまま作業をしているので、HDFSの`/user`ディレクトリにユーザーのホームディレクトリがあることを確認します。

```bash
$ docker exec amb1 hadoop fs -ls /user
Found 5 items
drwxrwx---   - ambari-qa hdfs          0 2015-12-25 17:08 /user/ambari-qa
drwxr-xr-x   - hcat      hdfs          0 2015-12-25 17:10 /user/hcat
drwx------   - hive      hdfs          0 2015-12-25 17:10 /user/hive
drwxr-xr-x   - root      hdfs          0 2015-12-26 02:40 /user/root
drwxrwxr-x   - spark     hdfs          0 2015-12-25 17:09 /user/spark
```


　`/user/root`ディレクトリがなければ、以下のように`hdfs`ユーザーにスイッチして作成します。


```bash
$ su hdfs 
$ hadoop fs -mkdir /user/root
$ hadoop fs -chown root /user/root
```

　サンプルのSparkアプリで利用するLICENSEファイルはHDPの以下のディレクトリにあります。

```bash
/usr/hdp/2.3.4.0-3485/spark/LICENSE
```

　LICENSEファイルを`yarn-cluster`モードで実行するSparkアプリから読み込むためにHDFSにputしておきます。

```bash
$ docker exec amb1 hadoop fs -put /usr/hdp/2.3.4.0-3485/spark/LICENSE /user/root
$ docker exec amb1 hadoop fs -ls /user/root/LICENSE
-rw-r--r--   3 root hdfs      17356 2015-12-25 17:20 LICENSE
```

　SparkアプリのjarファイルもWebHDFS経由でputします。

```bash
$ docker-compose run --rm sbt \
  curl -L -X PUT -T /app/target/scala-2.10/sample-app_2.10-1.0.jar "http://amb1:50070/webhdfs/v1/user/root/test/sample-app_2.10-1.0.jar?user.name=root&op=CREATE&overwrite=true"
```

　HDFSに`/user/test/sample-app_2.10-1.0.jar`ファイルが作成されました。

```bash1
$ docker exec amb1 hadoop fs -ls ./test/sample-app_2.10-1.0.jar
-rwxr-xr-x   3 root hdfs       1862 2015-12-26 04:16 test/sample-app_2.10-1.0.jar
```

## spark-submit

　最後に`spark-submit`をしてHDFSにputしたSparkアプリを実行します。`--master`フラグには`yarn-cluster`を指定してYARNのアプリケーションマスターのプロセスで実行しています。

```bash
$ docker exec  amb1 \
 spark-submit \
  --class SampleApp \
  --master yarn-cluster \
  --driver-memory 1g \
  --executor-memory 1g \
  --executor-cores 1 \
  hdfs://amb1/user/root/test/sample-app_2.10-1.0.jar
...
15/12/26 02:54:18 INFO Client:
	 client token: N/A
	 diagnostics: N/A
	 ApplicationMaster host: 172.17.0.5
	 ApplicationMaster RPC port: 0
	 queue: default
	 start time: 1451098443955
	 final status: SUCCEEDED
	 tracking URL: http://amb1.service.consul:8088/proxy/application_1451063385654_0011/
	 user: root
15/12/26 02:54:18 INFO ShutdownHookManager: Shutdown hook called
15/12/26 02:54:18 INFO ShutdownHookManager: Deleting directory /tmp/spark-c30cdca1-6f2b-4432-a866-c63391e619b2
```

　[yarn long](https://hadoop.apache.org/docs/stable/hadoop-yarn/hadoop-yarn-site/YarnCommands.html#logs)コマンドからSparkアプリの実行結果を確認します。SparkのLICENSEファイルは294行ありました。

```bash
$ docker exec amb1 \
  yarn logs -applicationId application_1451063385654_0011
...
LogType:stdout
Log Upload Time:Sat Dec 26 02:54:19 +0000 2015
LogLength:10
Log Contents:
Lines 294
End of LogType:stdout
```