Apache Zeppelinでデータ分析を分散処理する - Part 4: Ambari on Dockerのambari-functionsを使ってみる

## このシリーズ

* [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)


## ambari-functions

　[Part 2](http://qiita.com/masato/items/911e06fd31202181f0fd)では[Ambari on Docker](https://github.com/sequenceiq/docker-ambari)の[sequenceiq/ambari](https://hub.docker.com/r/sequenceiq/ambari/)イメージからAmbariクラスターをセットアップしました。ここで利用した[ambari-functions](https://github.com/sequenceiq/docker-ambari/blob/master/ambari-functions)にはDockerとAmbariの管理に便利な関数がいくつか定義されています。リポジトリからcloneして`ambari-functions`を読み込むと`amb-*`関数が利用できるようになります。

```bash
$ git clone https://github.com/sequenceiq/docker-ambari.git
$ cd !$
$ source ambari-functions
```


## Docker操作系

### docker-ps

　`docker ps`をラップしてAmbari管理に有用な情報を1行に見やすくしてくれます。コンテナ名、IPアドレス、Dockerイメージ名が一覧できます。

```bash
$ docker-ps
/nginx 172.17.0.7 nginx-consul <nil> {[/bin/start.sh]}
/amb3 172.17.0.6 sequenceiq/ambari:2.1.2-v1 <nil> {[/start-agent]}
/amb2 172.17.0.5 sequenceiq/ambari:2.1.2-v1 <nil> {[/start-agent]}
/amb1 172.17.0.4 sequenceiq/ambari:2.1.2-v1 <nil> {[/start-agent]}
/amb-server 172.17.0.3 sequenceiq/ambari:2.1.2-v1 <nil> {[/start-server]}
/amb-consul 172.17.0.2 sequenceiq/consul:v0.5.0-v6 {[/bin/start]} {[-server -bootstrap]}
```

　現在のZeppelinは以下のように5台のコンテナで構成されています。

* Ambari Server x 1
* Ambari Worker Node x 3
* Consul x 1
* Nginx x 1


### docker-psa

　`docker ps -a`と同じです。起動していないコンテナも表示します。

```bash
$ docker-psa
/nginx 172.17.0.7 nginx-consul <nil> {[/bin/start.sh]}
/amb3 172.17.0.6 sequenceiq/ambari:2.1.2-v1 <nil> {[/start-agent]}
/amb2 172.17.0.5 sequenceiq/ambari:2.1.2-v1 <nil> {[/start-agent]}
/amb1 172.17.0.4 sequenceiq/ambari:2.1.2-v1 <nil> {[/start-agent]}
/amb-server 172.17.0.3 sequenceiq/ambari:2.1.2-v1 <nil> {[/start-server]}
/amb-consul 172.17.0.2 sequenceiq/consul:v0.5.0-v6 {[/bin/start]} {[-server -bootstrap]}
```




## Ambari操作系

### amb-start-cluster

　[Part 2](http://qiita.com/masato/items/911e06fd31202181f0fd)で`amb-start-cluster`関数を使いDockerコンテナを4つ作成しました。

```bash
$ amb-start-cluster 4
```

　関数の内部では`amb-start-first`を実行し最初の1ノードのAmbari Serverと追加でConsulのコンテナを作成します。今回の場合残り3ノードがワーカーノードになります。

```bash:amb-start-cluster
amb-start-cluster() {
  local act_cluster_size=$1
  : ${act_cluster_size:=$CLUSTER_SIZE}
  echo starting an ambari cluster with: $act_cluster_size nodes

  amb-start-first
  [ $act_cluster_size -gt 1 ] && for i in $(seq $((act_cluster_size - 1))); do
    amb-start-node $i
  done
}
```


### amb-start-first

 最初にConsulコンテナとAmbari Serverコンテナを作成します。

```bash
amb-start-first() {
  local dns_port_command=""
  if [[ "$EXPOSE_DNS" == "true" ]]; then
     dns_port_command="-p 53:$DNS_PORT/udp"
  fi
  run-command docker run -d $dns_port_command --name $CONSUL -h $CONSUL.service.consul $CONSUL_IMAGE -server -bootstrap
  sleep 5

  run-command docker run -d -e BRIDGE_IP=$(get-consul-ip) $DOCKER_OPTS --name $AMBARI_SERVER_NAME -h $AMBARI_SERVER_NAME.service.consul $IMAGE /start-server

  get-ambari-server-ip

  _consul-register-service $AMBARI_SERVER_NAME $AMBARI_SERVER_IP
  _consul-register-service ambari-8080 $AMBARI_SERVER_IP
}
```

### amb-start-node

　この関数ではAmbariワーカーノードのコンテナを作成して、Consulにサービスとして登録しています。

```bash:amb-start-node
amb-start-node() {
  get-ambari-server-ip
  : ${AMBARI_SERVER_IP:?"AMBARI_SERVER_IP is needed"}
  NUMBER=${1:?"please give a <NUMBER> parameter it will be used as node<NUMBER>"}
  if [[ $# -eq 1 ]]; then
    MORE_OPTIONS="-d"
  else
    shift
    MORE_OPTIONS="$@"
  fi

  run-command docker run $MORE_OPTIONS -e BRIDGE_IP=$(get-consul-ip) $DOCKER_OPTS --name ${NODE_PREFIX}$NUMBER -h ${NODE_PREFIX}${NUMBER}.service.consul $IMAGE /start-agent

  _consul-register-service ${NODE_PREFIX}${NUMBER} $(get-host-ip ${NODE_PREFIX}$NUMBER)
}
```

　Ambariワーカーノードのコンテナで実行される[start-agent](https://github.com/sequenceiq/docker-ambari/blob/master/ambari-server/start-agent)へ`BRIDGE_IP`環境変数としてConsulコンテナのIPアドレスを渡しています。`--net="host"`フラグをつけてコンテナを起動した場合`/etc/resolv.conf`がホストのファイルが使われるため、`--dns`フラグが利用できません。ワークアラウンドとしてコンテナのコマンドでConsulをnameserverに指定して`/etc/resolv.conf`を上書きしています。

```bash:start-agent
# --dns isn't available for: docker run --net=host
# sed -i /etc/resolf.conf fails:
# sed: cannot rename /etc/sedU9oCRy: Device or resource busy
# here comes the tempfile workaround ...
local-nameserver() {
  cat>/etc/resolv.conf<<EOF
nameserver $BRIDGE_IP
search service.consul node.dc1.consul
EOF
}
```

　amb-start-node関数を直接使い、現在3台あるワーカーノードに1台のコンテナを追加してみます。関数の中でAmbari ServerやConsulのIPアドレスを設定してくれます。`NODE_PREFIX`を引数の数字を追加した名前がコンテナ名になります。`docker-ps`関数で確認したように現在`amb3`まであるので`4`を追加します。

```bash
$ amb-start-node 4
```

　出力結果は以下です。ホスト名に`amb4.service.consul`が指定されました。

```bash
[DEBUG] docker run -d -e BRIDGE_IP=172.17.0.2 --name amb4 -h amb4.service.consul sequenceiq/ambari:2.1.2-v1 /start-agent
78353cc44a93693a331357ba15b08d4015f88d7fc509ce06ab39295a6972de3e
```

　Consulのサービスカタログにも`amb4`が登録されています。

```bash
$ amb-members | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   308  100   308    0     0   304k      0 --:--:-- --:--:-- --:--:--  300k
[
  {
    "Node": "amb-consul.service.consul",
    "Address": "172.17.0.2"
  },
  {
    "Node": "amb-server",
    "Address": "172.17.0.3"
  },
  {
    "Node": "amb1",
    "Address": "172.17.0.4"
  },
  {
    "Node": "amb2",
    "Address": "172.17.0.5"
  },
  {
    "Node": "amb3",
    "Address": "172.17.0.6"
  },
  {
    "Node": "amb4",
    "Address": "172.17.0.8"
  }
]
```

### amb-shell

　[Part 2](http://qiita.com/masato/items/911e06fd31202181f0fd)では[sequenceiq/ambari](https://hub.docker.com/r/sequenceiq/ambari/)に入っている[/tmp/ambari-shell.sh](https://github.com/sequenceiq/docker-ambari/blob/master/ambari-server/ambari-shell.sh)スクリプトを直接実行して[Ambari Shell](https://github.com/sequenceiq/ambari-shell)を起動しました。今回は[ambari-functions](https://github.com/sequenceiq/docker-ambari/blob/master/ambari-functions)に入っている`amb-shell`関数を経由して使ってみます。どちらも同じ`/tmp/ambari-shell.sh`を実行します。

```bash:amb-shell
_amb_run_shell() {
  COMMAND=$1
  : ${COMMAND:? required}
  get-ambari-server-ip
  NODES=$(docker inspect --format="{{.Config.Image}} {{.Name}}" $(docker ps -q)|grep $IMAGE|grep $NODE_PREFIX|wc -l|xargs)
  run-command docker run -it --rm -e EXPECTED_HOST_COUNT=$((NODES-1)) -e BLUEPRINT=$BLUEPRINT --link ${AMBARI_SERVER_NAME}:ambariserver --entrypoint /bin/sh $IMAGE -c $COMMAND
}

amb-shell() {
  _amb_run_shell /tmp/ambari-shell.sh
}
```

　Ambari Shellが起動しました。

```bash
[DEBUG] docker run -it --rm -e EXPECTED_HOST_COUNT=4 -e BLUEPRINT=--link amb-server:ambariserver --entrypoint /bin/sh sequenceiq/ambari:2.1.2-v1 -c /tmp/ambari-shell.sh
AMBARI_HOST=172.17.0.3
[DEBUG] waits for ambari server: 172.17.0.3 RUNNING ...

[DEBUG] waits until 4 hosts connected to server ...
[DEBUG] connected hosts: 4
    _                _                   _  ____   _            _  _
   / \    _ __ ___  | |__    __ _  _ __ (_)/ ___| | |__    ___ | || |
  / _ \  | '_ ` _ \ | '_ \  / _` || '__|| |\___ \ | '_ \  / _ \| || |
 / ___ \ | | | | | || |_) || (_| || |   | | ___) || | | ||  __/| || |
/_/   \_\|_| |_| |_||_.__/  \__,_||_|   |_||____/ |_| |_| \___||_||_|

Welcome to Ambari Shell. For command and param completion press TAB, for assistance type 'hint'.
ambari-shell>
```

　Ambariが利用可能なホストの一覧を表示します。

```bash
ambari-shell>host list
amb4.service.consul [HEALTHY] 172.17.0.8 centos6:x86_64
amb1.service.consul [HEALTHY] 172.17.0.4 centos6:x86_64
amb2.service.consul [HEALTHY] 172.17.0.5 centos6:x86_64
amb3.service.consul [HEALTHY] 172.17.0.6 centos6:x86_64
```


　残念ながらAmbari Shellを使って[ノードの追加はできません](https://github.com/sequenceiq/ambari-shell/issues/24#issuecomment-57451115)。[Cloudbreak](http://hortonworks.com/hadoop/cloudbreak/)などでも使っている[ambari-rest-client](https://github.com/sequenceiq/ambari-rest-client)の[AmbariClientMain.groovy](https://github.com/sequenceiq/ambari-rest-client/blob/master/src/main/groovy/com/sequenceiq/ambari/main/AmbariClientMain.groovy)を直接使うと可能らしいので試してみようと思います。

```groovy:AmbariClientMain.groovy
package com.sequenceiq.ambari.main

import com.sequenceiq.ambari.client.AmbariClient

class AmbariClientMain {
  public static void main(String[] args) {
    def host = 'localhost'
    def port = '8080'
    if (args.size == 2) {
      host = args[0]
      port = args[1]
    }

    AmbariClient client = new AmbariClient(host, port)
    println "\n  clusterList: \n${client.showClusterList()}"
    println "\n  hostsList: \n${client.showHostList()}"
    println "\n  tasksList: \n${client.showTaskList()}"
    println "\n  serviceList: \n${client.showServiceList()}"
    println "\n  blueprintList: \n${client.showBlueprints()}"
    println "\n  clusterBlueprint: \n${client.showClusterBlueprint()}"

  }
}
```

## Hadoop操作系

　[WebHDFS REST API](https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/WebHDFS.html)経由でHDFSの操作ができます。


### amb-create-hdfs-dir

　HDFSにディレクトリを作成する関数です。

```bash
amb-create-hdfs-dir() {
  get-ambari-server-ip
  DIR=$1
  curl -X PUT "http://$AMBARI_SERVER_IP:50070/webhdfs/v1$DIR?user.name=hdfs&op=MKDIRS" > /dev/null 2>&1
}
```

　Ambariの設定からWebHDFSのチェックを確認します。

```
HDFS > Configs > Advanced > General > WebHDFS enabled
```

<img width="512" alt="webhdfs-checked.png" src="https://qiita-image-store.s3.amazonaws.com/0/43745/84f37141-f4dc-c150-7d01-5002b96b9c6f.png">

　[2.1. Getting Correct Configuration Values for Manually-Deployed Clusters](https://docs.hortonworks.com/HDPDocuments/Ambari-2.1.2.0/bk_ambari_views_guide/content/section_get_values_non-standard_cluster_config.html)のドキュメントにWebHDFSのURIの設定場所が書いてあります。

```
HDFS > Configs > Advanced > Advanced hdfs-site > dfs.namenode.http-address
```

<img width="512" alt="webhdfs-setting.png" src="https://qiita-image-store.s3.amazonaws.com/0/43745/fa759bbf-41c6-cf15-db02-0c867aca818f.png">

　この環境では`amb1.service.consul:50070`がWebHDFSのURIに設定されていますが、`amb-create-hdfs-dir`関数の中で`AMBARI_SERVER_IP`環境変数にAmbari ServerのIPアドレスが指定されてしまいます。WebHDFSを使う場合は直接curlから実行します。

```bash
$ WEB_HDFS_IP=$(docker inspect --format="{{ .NetworkSettings.IPAddress }}" amb1)
$ curl -X PUT "http://$WEB_HDFS_IP:50070/webhdfs/v1/user/root/test?user.name=root&op=MKDIRS"
{"boolean":true}
```

　amb1コンテナに入ってHDFSにディレクトリが作成されたか確認してみます。

```bash
$ docker exec -it amb1 hadoop fs -ls
Found 3 items
drwxr-xr-x   - root hdfs          0 2015-12-10 08:45 .sparkStaging
-rw-r--r--   3 root hdfs    4610348 2015-12-10 08:49 bank-full.csv
drwxr-xr-x   - root hdfs          0 2015-12-11 18:50 test
```

　

### amb-copy-to-hdfs

　WebHDFSのAPI経由でローカルのファイルをコピーできるようです。こちらも同様に`AMBARI_SERVER_IP`環境変数を使っているのでこの環境では利用できません。

```bash
amb-copy-to-hdfs() {
  get-ambari-server-ip
  FILE_PATH=${1:?"usage: <FILE_PATH> <NEW_FILE_NAME_ON_HDFS> <HDFS_PATH>"}
  FILE_NAME=${2:?"usage: <FILE_PATH> <NEW_FILE_NAME_ON_HDFS> <HDFS_PATH>"}
  DIR=${3:?"usage: <FILE_PATH> <NEW_FILE_NAME_ON_HDFS> <HDFS_PATH>"}
  amb-create-hdfs-dir $DIR
  DATANODE=$(curl -si -X PUT "http://$AMBARI_SERVER_IP:50070/webhdfs/v1$DIR/$FILE_NAME?user.name=hdfs&op=CREATE" |grep Location | sed "s/\..*//; s@.*http://@@")
  DATANODE_IP=$(get-host-ip $DATANODE)
  curl -T $FILE_PATH "http://$DATANODE_IP:50075/webhdfs/v1$DIR/$FILE_NAME?op=CREATE&user.name=hdfs&overwrite=true&namenoderpcaddress=$AMBARI_SERVER_IP:8020"
}
```

## まとめ

　[ambari-functions](https://github.com/sequenceiq/docker-ambari/blob/master/ambari-functions)を見てきました。[Ambari Shell](https://github.com/sequenceiq/ambari-shell)でできないことやHadoopのサービスとコンポーネントの構成によっては実行できない関数があります。Dockerやクラウドへのプロビジョンングを考えると同じDockerイメージを使うHadoopk管理ツールの[Cloudbreak](http://hortonworks.com/hadoop/cloudbreak/)を選択した方が良さそうです。[Groovy](http://www.groovy-lang.org/)の[ambari-rest-client](https://github.com/sequenceiq/ambari-rest-client)がAmbariやCloudbreakで共通して利用されているクライアントライブラリでした。[AmbariClientMain.groovy](https://github.com/sequenceiq/ambari-rest-client/blob/master/src/main/groovy/com/sequenceiq/ambari/main/AmbariClientMain.groovy)のMainメソッドも直接試してみようと思います。