このエントリーをはてなブックマークに追加

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」と標示されるのは、少しかっこいいですよね!

このエントリーをはてなブックマークに追加

Packer で AMI を作るときにタイムアウトした

Packer で AMI を作るとき、Provisors で時間がかかってくるようになると、次のようなエラーメッセージが表示されて終了することがあります。
今のところ、Ansible + Serverspec を実行しているため、かなり時間がかかってきたようです。。。


...
2016/05/11 07:52:03 packer: 2016/05/11 07:52:03 Allowing 300s to complete (change with AWS_TIMEOUT_SECONDS)
2016/05/11 07:52:24 ui: ==> amazon-ebs: Creating the AMI: foo-2016-05-10T22-45-36Z
2016/05/11 07:52:25 ui: amazon-ebs: AMI: ami-xxx
2016/05/11 07:52:25 ui: ==> amazon-ebs: Waiting for AMI to become ready...
2016/05/11 07:52:25 packer: 2016/05/11 07:52:25 Waiting for state to become: available
2016/05/11 07:52:25 packer: 2016/05/11 07:52:25 Allowing 300s to complete (change with AWS_TIMEOUT_SECONDS)
2016/05/11 07:54:39 ui: ==> amazon-ebs: Modifying attributes on AMI (ami-xxx)...
2016/05/11 07:54:39 ui: amazon-ebs: Modifying: description
...

こんなときは、メッセージのとおり AWS_TIMEOUT_SECONDS 環境変数を設定してから実行するとよくなりました。
また、標準出力に表示されるログがかなり長くなってきたので、ログファイルに出力するようにしました。

今のところ、次のような環境変数を設定しています。
$packer_name というのは AMI 名、$packer_role は AMI のロール名になります。
今のところ、$packer_name の $packer_role ごとに AMI を作っています。


$ export PACKER_LOG=1
$ export PACKER_LOG_PATH="packer-$packer_name-$packer_role-$(date +%Y%m%d%H%M%S).log"
$ export AWS_TIMEOUT_SECONDS=600

Packer でビルドしているとき、途中で終了しても気づけないことがあるので、ログファイルはちゃんとみた方がよいと思います。
CI などで Packer をビルドしているときは、CI のログを見るのが良いかなと思います。

このエントリーをはてなブックマークに追加

Ubuntu 14.04 LTS から 16.04 LTS へのアップグレード方法

待望の Ubuntu 16.04 LSTS がリリースされましたね!
さっそく自サーバの Ubuntu 14.04 LTS Server をアップグレードしてみました。

Digital Ocean に詳しい解説がのっていますが、正式には 16.04.01 でアップグレードが正式に対応になることですので、アップグレードする際には自己責任でお願いします。

まずは、現行のものを最新にアップグレードして、必要なら再起動をしておきます。


$ sudo apt-get update
$ sudp apt-get upgrade

あとは、いつもどおり do-release-upgrade でアップグレードするだけです。ただし、現時点では -d オプションをつける必要があります。


$ sudo apt-get install update-manager-core
$ sudo do-release-upgrade -d

僕の環境では無事アップグレードできたのですが、SSH 接続すると、次のようなエラーになってしまいました。


Unable to negotiate with x.x.x.x: no matching key exchange method found. Their offer: curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha1

これは、~/.ssh/config に、次のような設定が入っていたからです。


KexAlgorithms diffie-hellman-group1-sha1

これを次のように修正しましたところ、接続はできました。


KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256

あとは、16.04 LTS の新機能をチェックしながら、Ubuntu 16.04 LTS を楽しみたいと思います。

また、まだ EC2 上には公式のイメージは提供されていないようですが、楽しみに待っておきたいと思います。

このエントリーをはてなブックマークに追加

Ansible on CentOS 7 ですこしハマった

CentOS 7 上で Ansible 2 系を使っていますが、EPEL にある Ansible パッケージが、現時点で最新の 2.0.2.0 にバージョンアップされていました。
さっそくこのバージョンを使ったところ、次のような JSON をパースするような処理がある場合は、エラーとなってしまいました。


- set_fact: foo_aws_access_key_id="{{ cloudformation_outputs | selectattr('OutputKey', 'equalto', 'FooKey') | map(attribute='OutputValue') | join(',') }}"

これは、取得した CloudFormation の JSON データの OUTPUTS から特にキーの値を取得するものですが、これを実行すると、次のようなエラーになります。


An exception occurred during task execution. To see the full traceback, use -vvv. The error was: TemplateRuntimeError: no test named 'equalto'
fatal: [foo]: FAILED! => {"failed": true, "msg": "Unexpected failure during module execution.", "stdout": ""}

原因を調査したところ、どうも Ansible が使っているテンプレートエンジン Jinja2 が 2.7 系のままだったことが原因みたいです。
たしかに yum 経由でインストールされる Jinja2 が 2.7 系のままでした。


sudo yum info python-jinja2.noarch
読み込んだプラグイン:fastestmirror
Loading mirror speeds from cached hostfile
* base: ftp.osuosl.org
* epel: mirror.symnds.com
* extras: mirror.confluxtech.com
* updates: mirrors.tripadvisor.com
インストール済みパッケージ
名前 : python-jinja2
アーキテクチャー : noarch
バージョン : 2.7.2
リリース : 2.el7
容量 : 3.0 M
リポジトリー : installed
提供元リポジトリー : base
要約 : General purpose template engine
URL : http://jinja.pocoo.org/
ライセンス : BSD
説明 : Jinja2 is a template engine written in pure Python. It provides a
: Django inspired non-XML syntax but supports inline expressions and an
: optional sandboxed environment.
:
: If you have any exposure to other text-based template languages, such
: as Smarty or Django, you should feel right at home with Jinja2. It's
: both designer and developer friendly by sticking to Python's
: principles and adding functionality useful for templating
: environments

ということで、あまりやりたくはないですが、仕方がなく pip コマンドでアップグレードするようにしました。
けっこう Ansible も Jinaja もリリースされるバージョンに問題があることが多いのでバージョンを固定するようにしました。


$ sudo pip install --upgrade Jinja2==2.8

これで、上のような Playobook もちゃんと通るようになりました。

このエントリーをはてなブックマークに追加

はじめての Gatling

Gatling を、とある環境のベンチマークに使ってみたので、入門向けとして「はじめての Gatling」としてまとめてみました。

Gatling は、Scala でベンチマークのシナリオを書くけれど、Scala 力が低いても、かなり手軽にシナリオを記述できるところが便利ですね。

このエントリーをはてなブックマークに追加

はじめての Datadog

本番環境に Datadog を導入し始めていますが、Datadog はじめての方の向けに簡単にスライドとして作成してみました。
Datadog に関するカジュアル会があれば、そこで発表したい内容でしたが、スライドを公開しておきます。

今回は、「はじめての Datadog」ということで Datadog の入門編として作成しましたが、応用編も本番環境へ導入して運用している中で知見がたまったところで公開する予定です。

Datadog すこしクセがあるところもありますけれど、とても便利なモニタリングサービスだと思います。

このエントリーをはてなブックマークに追加

お手軽に Java プログラムを起動できる javactl の紹介

Java プログラムをコンパイルしたあと、jar なファイルを実行するためには java コマンドでいろいろな引数を設定して起動することがよくあります。

そんなとき、javactl を使うとプログラムを簡単に起動することができます。
おもな特長は、次のとおりです。

  • pip コマンドで簡単インストールすることができる
  • 起動するための設定ファイルは、YAML 形式で記述することができる

まず、インストールは、pip コマンド一発です。


$ pip install javactl

インストールすると、javactl というプログラムがインストールされます。


$ which javactl
/usr/bin/javactl
$ /usr/bin/javactl -h
Usage: javactl [options...] [args...]

Options:
--version show program's version number and exit
-h, --help show this help message and exit
--check dry-run mode
--debug debug mode

使い方は、とても簡単で設定ファイル名パスを渡すだけです。


$ javactl foo.yaml

設定ファイルの例は、こちらをみると、一目瞭然だと思います。

設定ファイルには、おもに次のような内容を記述します。

  • 対象の Java アプリケーション名 app)
  • アプリケーションの実行ユーザ名
  • 環境変数を定義したい場合は、その設定 (os.env)
  • java のヒープサイズなどのメモリ設定 (java.memory)
  • JMX がある場合、その設定 (java.jmx)
  • java の定義名 (java.env)
  • その他の Java オプション (java.option)
  • ログ(GC)ファイルの設定 (log)

Java に特化した起動パラメータをほぼすべて指定できるので Java/Scala アプリケーションをとりあえずさくっと起動したいには手軽なツールです。

僕の場合は、この javactl を使って Supervisor から Scala アプリケーションを実行するようにしています。

このエントリーをはてなブックマークに追加

Packer を使って EC2 AMI をビルドする

AWS EC2 AutoScaling するときなどには、AMI を作成しておくと、早く本番環境に投入できてとても便利です。

AMI を作成するには、packer を使うと、とても簡単にビルドすることができます。

今回は、公式の CentOS 7 の AMI をベースに、次の AMI を順番に作成してみることにしました。
Packer のサンプルテンプレートを GitHub においておきました。

  1. CentOS 7 AMI 最新版
  2. サービス共通の AMI(上の AMI をベースにして作成する)
  3. サービスのロール別の AMI(上の AMI をベースにして作成する)

CentOS 7 公式の AMI イメージ ID は、次のaws-cli コマンドで取得することが出来ます。(若干時間がかかります)

$ aws --output text ec2 describe-images \
    --owners aws-marketplace \
    --filters \
      Name=architecture,Values=x86_64\
      Name=virtualization-type,Values=hvm \
      Name=hypervisor,Values=xen \
      Name=owner-alias,Values=aws-marketplace \
      Name=root-device-name,Values=/dev/sda1 \
      Name=root-device-type,Values=ebs \
      Name=image-type,Values=machine \
      Name=state,Values=available \
      Name=description,Values="CentOS Linux 7 x86_64 HVM EBS 20150928_01" \
    --query 'Images[].ImageId'

テンプレートの変数は、すべて環境変数で渡すようになっています。

まず、CentOS 7 AMI 最新版 では、次のようなことをやっています。

  1. ルートディスク 30G、EBS 100GB にする
  2. タグを Name、ベースになった Base_AMI で設定する
  3. SELinux を無効にする(石川さんごめんなさい
  4. EPEL と必要な Ansible などをインストールする
  5. CentOS を最新にアップグレードする
  6. タイムゾーンを東京に変更する

残りのサービスごと、サービスロールごとのテンプレートは、ec2-userdata.sh というシェルスクリプトでそれぞれ必要な処理(Ansible によるプロビジョニングなど)を実行するようにします。

さらに今回は CircleCI を使っているので、CircleCI 上で特定のブランチのときのみ AMI を順番にビルドするように設定しました。
cirlce.yml に、例えば次のように記述をするとできます。

...
deployment:
  packer:
    branch: develop-packer
    commands:
      - ci-deploy-packer.sh

この例では、develop-packer ブランチに push されたとき packer を使って AMI を作成する ci-deploy-packer.sh スクリプトを実行するという意味になります。

実際のプロジェクトでは、CentOS 7 共通 1 つ、サービス共通 1 つ、ロールごと 2 つ、を直列で作成して、約 1 時間ほどかかりました。