Browse Tag: aws

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;
}

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 時間ほどかかりました。

EC2 c3.large が人気らしいというお話

今年の AWS re:Invent 2013 で発表された EC2 C3 ファミリーですが、かなり c1.medium / m1.medium からの移行などで人気があるようです。

EC2_Management_Console

僕も同じく、すこし遅めの対応でインスタンスの切り替えをしようとしたところ、次のようなメッセージが表示されていまいました。確認したので、東京リージョン ap-northeast-1a で非 default VPC 環境です。AWS の中の人に聞いたところ、c3.xlarge がまだ余裕があるみたいです。ap-northeast-1c ですと多少の余裕があって c3.large は起動するようです。ap-northeast-1a 側を使っている人がけっこう多そうな感じですね!

AWS といっても、当然ながら物理的なリソースの制限があるわけなので、AWS データセンターの中の人には頑張っていただきたい所存です。

早く、c1.medium や m1.medium から切り替えたいところですね。

参考:
Amazon EC2 で M1,C1 インスタンスを使ってる人は今すぐ C3 を使い始めよう – yoshidashingo

2013/12/20 追記
もう完全に東京では、枯渇してしまった様子ですね。来年あたり復活してから、c1/m1 からの切り替え祭りを実施したいと思います。

EC2 上で AWS_DEFAULT_REGION を設定する

EC2 上で awscli を利用する場合、AWS_DEFAULT_REGION 環境変数を設定する必要があるが、/etc/profile.d/awscli.sh として、次のようなコードを書いてみた。

EC2 でない場合、ログインに時間がかかってしまうため、その場合も考慮してみた。

#!/bin/sh

if [ -x "`which aws 2> /dev/null`" ]; then
AZ=`curl --connect-timeout 3 -s http://169.254.169.254/latest/meta-data/placement/availability-zone`
if [ -n "$AZ" ]; then
export AWS_DEFAULT_REGION=`echo $AZ | cut -c 1-$((${#AZ} - 1))`
fi
complete -C aws_completer aws
fi

from gist

最近、AWS を触っているので、随時アウトプットはしていきたいですね。