Browse Month: June 2016

EMR 上の Hadoop / Spark の Web UI にアクセスする方法

プライベートサブネット上に EMR 上の Hadoop / Spark の Web UI をインターネット経由でアクセスしたくなりました。

ちゃんとした方法ですと、EMR上でSparkのWeb UIにアクセスする方法 | Developers.IOにあるとおり、次の2つ選択肢があるようです。

  1. SSH トンネリング
  2. SSH トンネリング + SOCK プロキシ

どちらも SSH を利用するため、外部からは接続はできません。

あとは、AWS VPC VPN 接続を使ってプライベートサブネットに接続する選択肢が考えられますが、この場合拠点に VPN が必要です。

そのような環境はなかったので、同一プライベートサブネット上の Nginx からプロキシする方法で設定してみました。

まず試してみると、ほとんどプライベート DNS になっているため、Nginx プロキシでレスポンスボディを変更する必要がありました。

レスポンスボディを書き換えることができるモジュールを探してみたところ、openresty/replace-filter-nginx-moduleyaoweibin/ngx_http_substituions_filter_module がありました。

どちらのモジュールも試したところ、前者の replace-filter-nginx-module は複数箇所の書き換えがなぜかうまくできず、結果的には後者の  ngx_http_substitutions_filter_module を使うことになりました。

Hadoop UI を /cluster、Spark UI を /spark、としていますが、ちょっと長くなりますが、次のようにかなり無理矢理ですが、設定することで無事うまくいきました。
なお、proxy まわりの設定は必要最低限の設定にしてあります。

    # Ganglia
    location /ganglia {
        proxy_pass http://EMR Master Public DNS:80/ganglia;
        proxy_redirect off;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # Hadoop UI
    location /cluster {
        resolver [Private DNS ※Ansible でいうと ansible_dns.nameservers[0] とかになりますね] valid=60s;
        set $backend "";
        set $my_host "";
        set $my_port "";

        if ($request_uri ~ /cluster/_/(.+)$) {
            set $backend $1;
        }

        if ($request_uri ~* ^/cluster/_/(.+?):(\d+)\/.+$) {
            set $my_host $1;
            set $my_port $2;
        }

        if ($backend != "") {
            proxy_pass http://$backend;
            break;
        }

        if ($request_uri ~* /cluster/static/(.+)$) {
            proxy_pass http://EMR Master Public DNS:8088/static/$1;
            break;
        }

        # この設定が、レスポンスボディを変更する設定です
        # Then の書き換えは必要ないですが、見栄え的に修正しました
        subs_filter "Then, " ". Then, " r;
        subs_filter "href=\"/static" "href=\"/cluster/static" r;
        subs_filter "src=\"/static" "src=\"/cluster/static" r;
        subs_filter "<a href=\"http://(.+):8088\"" "<a href=\"/cluster\"" r;
        subs_filter "<a href='http://(.+):8088/(.+)'" "<a href='/$2'" r;
        subs_filter "<a href=\"http://(.+):8088/cluster/(.+)\"" "<a href=\"/cluster/_/$1:8088/cluster/$2\"" r;

        subs_filter "<a href=\"//(.+):8042\"" "<a href=\"/cluster/_/$1:8042/node\"" r;
        subs_filter ",\"<a href='http://(.+):8042'" ",\"<a href='/cluster/_/$1:8042/node'" r;
        subs_filter ",\"<a href='http://(.+):8042/(.+)'" ",\"<a href='/cluster/_/$1:8042/$2'" r;
        subs_filter "http-equiv=\"refresh\" content=\"1; url=http://(.*)\"" "http-equiv=\"refresh\" content=\"1; url=https://$host/cluster/_/$1\"" r;

        subs_filter "<a href='http://.+:20888/(.+)'" "<a href='/spark'" r;
        subs_filter "<a href=\"http://.+:20888/(.+)\"" "<a href=\"/spark\"" r;

        subs_filter "href=\"/node\/node" "href=\"$request_uri" r;
        subs_filter "href=\"/node\/allApplications" "href=\"$request_uri/allApplications" r;
        subs_filter "href=\"/node\/allContainers" "href=\"$request_uri/allContainers" r;

        subs_filter "href=\"/jobhistory/(.+)" "href=\"/cluster/_/$my_host:$my_port/jobhistory/$1" r;
        subs_filter "href=\"/(conf)" "href=\"/cluster/_/$my_host:$my_port/$1" r;
        subs_filter "href=\"/(logs)" "href=\"/cluster/_/$my_host:$my_port/$1" r;
        subs_filter "href=\"/(stacks)" "href=\"/cluster/_/$my_host:$my_port/$1" r;
        subs_filter "href=\"/(jmx)(.+)" "href=\"/cluster/_/$my_host:$my_port/$1$2" r;

        proxy_pass http://EMR Master Public DNS:8088/cluster;
        proxy_redirect off;
     }

    location /spark {
        resolver [Private DNS ※Ansible でいうと ansible_dns.nameservers[0] とかになりますね] valid=60s;
        set $backend "";
        set $my_host "";
        set $my_port "";

        if ($request_uri ~ /spark/_/(.+)$) {
            set $backend $1;
         }

        if ($request_uri ~* ^/spark/_/(.+?):(\d+)\/.+$) {
            set $my_host $1;
            set $my_port $2;
        }

        if ($backend != "") {
            proxy_pass http://$backend;
            break;
        }

        if ($request_uri ~* /spark/static/(.+)$) {
            proxy_pass http://EMR Master Public DNS:18080/static/$1;
            break;
        }

        if ($request_uri ~* /spark/history/(.+)$) {
            proxy_pass http://EMR Master Public DNS:18080/history/$1;
            break;
        }

        # この設定が、レスポンスボディを変更する設定です
        subs_filter "href=\"/\"" "href=\"/spark\"" r;
        subs_filter "href=\"/static" "href=\"/spark/static" r;
        subs_filter "src=\"/static" "src=\"/spark/static" r;
        subs_filter "href=\"/history/application_(\w+)/(\d+)\"" "href=\"/spark/history/application_$1/$2/jobs/\"" r;
        subs_filter "?id=" "/?id=";
        subs_filter "href=\"/\?page(.+?)\"" "href=\"/spark/?page$1\"" r;

        subs_filter "<a href=\"http://(.+):8042/(.+)\"" "<a href=\"/cluster/_/$1:8042/$2\"" r;

        proxy_set_header Accept-Encoding "";

       proxy_pass http://EMR Master Public DNS:18080/spark;
       proxy_redirect off;
}

このようにして頑張って書き換えてプロキシさせてあげると、Hadoop/Spark UI をウェブ経由で手軽に見ることができます。

もちろん、上記にあげたポートは Security Group で許可してする必要があります。node も含めると、具体的には 80, 8041, 8042, 8088, 18080, 19888, 20888, ポートの接続許可が必要です。
は、EMR のクラスターごとの固有設定になりますので、僕はプライベート DNS を Route 53 に登録して、その内部ドメインを参照しています。

なお、この方法ですと、EMR がバージョンアップしたときにはその都度 UI に変更があれば subs_filter の変更が必要になります。

上の設定は EMR Release 4.6.0 Hadoop 2.72. + Spark 1.6.1 の環境で動作確認しています。

# 6/16 追記

一部 /spark の書き換えルールを修正しました。

この設定で問題がないと思ったんですが、Spark History Server どうも同じ URL なのにも関わらず初回は 302 を返すため、http 経由でもアクセス可能として、次のような設定を入れる必要がありました。
具体的には、 “https:// server_name/spark/history/application_1464686385340_0492/1/jobs/” にアクセスすると、”http://server_name/history/application_1464686385340_0492/1/jobs/” に 302 リダイレクトされます。。。

location /history {
    rewrite ^/(.*) https://server_name/spark/$1 redirect;
}

td-agent のカスタムパッケージを作った

td-agent とても便利ですよね、fluentd をインストールするのに、もっとも手軽な方法だと思って、いつも愛用しています。

さて、fluentd のプラグインが増えていくると、特に複数台のサーバに導入するとき、けっこうな時間がかかって来ました。
せっかく、omnibus-td-agent が公開されているので、これをカスタマイズしてみました。

このカスタマイズでの、公式 omnibus-td-agent からの差分はこちらになりますが、おもに次のようなカスタマイズを行っています。

  • specific_install のインストール(たまに検証用に使ったり、rubygem にないものをどうしても使いたいとき)
  • fleuntd バージョンの固定(現行バージョンは 0.12.22  なので少し古いので、あとで更新しておきたいと思います)
  • 次のプラグインたちの同梱
    • dstat
    • datadog_event
    • dogstatsd
    • elasticsearch
    • file-alternative
    • filter_typecast
    • flowcounter
    • forest
    • grep
    • multiprocess
    • record-modifier
    • record-reformer
    • sampling-filter
    • typecast
    • map(公式では label 対応していないため、独自に拡張したものを利用)
    • dogapi gem

この変更で独自に拡張したい場合は、plugin_gems.rb に追加したいプラグインを設定することで、それらのプラグインを含めたパッケージを作ることができます。

あとは、Vagrant 上でビルドして、自身の Yum リポジトリに追加して、インストールするだけです。

この変更で、自分が使っているプラグインを含めた td-agent の配布がとても簡単になりました。

GPG キーの移動方法

少し前に GitHub で GPG 署名がサポートされたため、さっそく使っているのですが、他のマシンに GPG キーを移動する方法を調べてみました。

OS X の場合は、brew install gpp2 して、gpg2 コマンドを使ったほうがよいわけですが、次のようになります。

移行元のマシンで実行

$ email=< メールアドレス >
$ name=< ファイル名 >
$ gpg2 -a --export $email > $name-public-gpg.key
$ gpg2 -a --export-secret-keys $email > $name-secret-gpg.key
$ gpg2 --export-ownertrust > $name-ownertrust-gpg.txt

移行先のマシンで実行

$ email=< メールアドレス >
$ name=< ファイル名 >
$ gpg2 --import $name-secret-gpg.key
$ gpg2 --import-ownertrust $name-ownertrust-gpg.txt
$ gpg2 --list-keys
...

こんな感じであっさりとできました。

あと、都度リポジトリで GPG の設定をするのはめんどくさいので、次のようなシェルスクリプトを書いて使っています。


#!/bin/sh

email=$1
if [ -z "$email" ]; then
email=$(git config user.email)
fi

gpg=gpg
if [ $(which gpg2) ]; then
gpg=gpg2
fi

gpg_pub_key=$($gpg --list-keys | grep -1 "< $email>"| grep -e "^pub")
gpg_pub_key_id=$(echo $gpg_pub_key | cut -d ' ' -f 2 | cut -d '/' -f 2)
if [ -z "$gpg_pub_key_id" ]; then
echo "Could not get gpg pub key - $gpg_pub_key_id"
exit 1
fi

git config --local gpg.program $gpg
git config --local user.signingkey $ggp_pub_key_id
git config --local commit.gpgsign true

このスクリプトでメールアドレスを省略された場合は、リポジトリ設定のメールアドレスを利用しているのでけっこう便利です。

コミット履歴に「Verified」と標示されるのは、少しかっこいいですよね!