【注意】Amazon Aurora(MySQL)でZero Downtime Patch実行時に不具合が発生するケースがある【AWS】

はじめに

先日、Amazon Aurora(MySQL)の必須アップデートの案内が来たのでアップデートの検証作業を実施しておりました。
Zero Downtime Patch(以下、ZDP)が実行され、ダウンタイム無しでアップデートが行われるはずが、不具合が発生してしまいました。

ググっても関連する情報が見つからなかったので、取り急ぎ分かっている情報について記録しておきます。

なお、対象のアップデートは下記のものになります。(Cluster Version 1.14)

Database Engine Updates 2017-08-07
http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Aurora.DatabaseEngineUpdates.20170807.html

ポイント(TL;DR)

重要なポイントは下記のとおりです。

Aurora(MySQL)に対し、DB Connection Poolを利用し、且つPrepared Statementを利用していると、Zero Downtime Patch実行時に不具合が発生する可能性が高い。
Aurora(MySQL)のアップデートを行う前に、アプリの構成・実装を調べた方が良い。

Zero Downtime Patch(ZDP)とは何か

Auroraを無停止(正確には5秒程度の遅延)でアップデートできる画期的な仕組みです。
アップデート処理中にも、クライアントからのDB接続は維持されます。

今回、Cluster Version 1.14へのアップデートとなりますが、本アップデートに関するドキュメントにも説明があります。
http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Aurora.DatabaseEngineUpdates.20170807.html

AuroraのCluster Version 1.10で初めて導入されたようです。
ZDPの内容について、下記ドキュメントにも記載があります。

Database Engine Updates 2016-12-14
http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Aurora.DatabaseEngineUpdates.20161214.html

こちらの記事も情報がまとまっていて参考になります。

Amazon Aurora のアップグレード
https://qiita.com/tonishy/items/542f7dd10cc43fd299ab

事象の内容

Aurora(MySQL)でClusterのアップデートが必要の旨の案内が来たため、AWS Management Consoleからアップデートを行いました。
AuroraのClusterアップデートは無事に完了しましたが、当該Auroraインスタンスを利用しているJavaアプリが軒並みエラーを吐き出し始めました。
その後Javaアプリを再起動し、復旧しました。

AWS Management Console上、WriterとなっているインスタンスのEventに下記のものが表示されました。
従って、Zero Downtime Patchが行われていたことは間違いありません。

The database was successfully patched with a zero downtime patch.

Javaアプリのログ上には、「Unknown prepared statement handler」といったエラーが多数出力されておりました。

事象の原因

当該JavaアプリはJDBC Connection Poolを利用し、DBとの接続を維持する構成になっています。
また、Prepared Statementを利用する実装になっております。

ZDP実行時、MySQLのクライアント(i.e. Javaアプリ)からの接続は維持されるようですが、Prepared Statementは維持されないことが原因のようでした。
時系列的に整理すると、下記のような状況になっていたようです。

  1. Javaアプリ起動時、Auroraに対して幾つかJDBC接続を確立する
  2. リクエストに応じて、JavaアプリからAuroraにクエリを発行する。この際、Prepareを発行してPrepared Statementを作る。
    Javaアプリは、同一のリクエストに対してはPrepared Statementを再利用する。
  3. AuroraのアップデートをZDPで行う。この際、JavaアプリからのJDBC接続は維持されるが、Prepared Statementは全てクリアされる。
  4. Javaアプリに対し、項番2と同様のリクエストを行う。Javaアプリは、コンパイル済みのPrepared Statementが存在しているという認識のまま、これを再利用しようとし、エラーとなる。

同一のアプリに対し、Prepared Statementを利用するもの/しないものを用意してAuroraのアップデートを実施しました。
結果、前者はエラーが発生し、後者はエラーが発生しませんでした。

なお上記の挙動は、独自に調査・検証したものとなります。
現在、AWSの公式見解・ドキュメント等の情報は見つけられておりません。
# 一応、Database Engine Updates 2016-12-14には以下の記載がありますが、これが該当するかどうかは不明です。
# Note that temporary data like that in the performance schema is reset during the upgrade process.

2017/9/26にAWSサポートへの問い合わせを行っておりますが、2017/10/19現在、回答待ちの状況です。

確認した環境・構成

上記事象は、下記構成のJavaアプリで確認しました。

  • Java 8 (Oracle JDK)
  • spring-boot-1.4.0.RELEASE
  • mybatis-spring-boot-starter 1.1.1
  • mariadb-java-client 1.5.5

併せてMyBatisGeneratorも利用しています。
特別な理由がない限り、JavaアプリからはMyBatisGeneratorで生成されたMapperとClassを利用しています。

デフォルトでは、MyBatisGeneratorではPrepared Statementを利用するMapperを生成するようです。

回避策

回避策は概ね以下の3通りになると思います。

1.Prepared Statementを利用しないよう、アプリを変更する。

ソースコード/ORマッパーの改修・設定変更を行い、Prepared Statementを利用しないようにする方法です。
おそらく、この対応は難しいケースがほとんどだと思います。

2.MySQL接続ドライバの設定で、Prepared Statementを利用しないよう変更する。 

今回のケースでは、MariaDB Connector/Jを利用しており、オプションを指定することでPrepared Statementを利用しないように変更することができました。

useServerPrepStmtsオプションをfalseにすることで実現可能です。
ちなみに、1.6.0以降のバージョンはデフォルトでfalseになっているようですので、これ以降のバージョンを利用してる場合は、本件の影響を受けません。

About MariaDB Connector/J -> Optional URL parameters -> Essential options
https://mariadb.com/kb/en/library/about-mariadb-connector-j/

3.ZDPではない、通常のアップデートを実施する

Auroraインスタンスの再起動を伴う、通常のアップデートを実行することで、本件事象を回避することが可能と考えられます(※未検証)
20~30秒程度のダウンタイムが発生します。

下記に、ZDPにならないケースについて記載がありますので、このどれかを満たせば通常のアップデートになると推測できます(※未検証)

Database Engine Updates 2017-08-07 -> Zero-Downtime Patching
http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Aurora.DatabaseEngineUpdates.20170807.html

  • Long-running queries are in progress
  • Open long-running transactions exist
  • Binary logging is enabled
  • Binary log replication is running
  • Pending parameter changes exist
  • Temporary tables are in use
  • Table locks are in use
  • Open SSL connections exist

まとめ

DBへのConnection Poolを利用し、且つPrepared Statementを利用するようアプリを実装していると、不具合が発生する可能性が高いです。

経験的な感覚ですが、JavaアプリはConnection Poolを利用しているケースが多いかと思います。
Aurora(MySQL)を利用している場合は、チェックをした方が良いかと思います。

検証作業について

古いClusterバージョンのAuroraインスタンスを作ることはできません。AWSサポートにも聞いてみましたが無理なようでした。
従って、運良く過去に構築した古いAuroraインスタンスが存在していれば検証は可能ですが、それ以外のケースでは検証手段がありません。

続きを読む

DynamoDBの予約語一覧を無理やり動的に取得してみた

DynamoDBの予約語を対処するために一覧が欲しくなったのですが、いちいちプログラム内で定義したくなかったので、AWSの予約語一覧ページから一覧を取得するサービスを作りました。

前提

  1. AWSが公開しているDynamoDB予約語一覧のWebページ( http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html )をスクレイピングして予約語一覧を抽出します。
  2. 実装はAWS Lambda (Python3)、Webサービスとして動作させるためにAPI Gatewayを利用します。

結果

https://github.com/kojiisd/dynamodb-reserved-words

DynamoDB予約語一覧ページの確認

今回は以下のページをParseしたいと思います。

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/ReservedWords.html

HTMLを見てみると、Parseしたい箇所はcodeタグで囲まれているようで、しかもこのページ内でcodeタグが出現するのは1度だけのようです。

スクリーンショット 2017-10-15 18.03.18.png

これであればすぐにパースできそうです。

HTMLの取得とParse

BeautifulSoupを利用すれば、簡単に実装ができます。BeautifulSoupのインストールは他のページでいくらでも紹介されているので、ここでは省略します。

import sys
import os


sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'lib'))
from bs4 import BeautifulSoup
import requests

URL = os.environ['TARGET_URL']

def run(event, context):

    response = requests.get(URL)
    soup = BeautifulSoup(response.content, "html.parser")

    result_tmp = soup.find("code")
    if result_tmp == None:
        return "No parsing target"

    result = result_tmp.string

    return result.split('n');

とりあえず申し訳程度にパースできない場合(AWSのページがcodeタグを使わなくなった時)を想定してハンドリングしています。

API Gatewayを定義してサービス化

コードが書けてしまえばAPI Gatewayを定義してサービス化するだけです。

設定は非常に単純なので割愛で。

スクリーンショット 2017-10-15 18.12.11_deco.png

アクセスしてみる

実際にアクセスしてみます。

$ curl -X GET https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod
["", "ABORT", "ABSOLUTE", "ACTION", "ADD", "AFTER", "AGENT", "AGGREGATE", "ALL", "ALLOCATE", "ALTER", "ANALYZE", "AND", "ANY", "ARCHIVE", "ARE", "ARRAY", "AS", "ASC", "ASCII", "ASENSITIVE", "ASSERTION", "ASYMMETRIC", "AT", "ATOMIC", "ATTACH", "ATTRIBUTE", "AUTH", "AUTHORIZATION", "AUTHORIZE", "AUTO", "AVG", "BACK", "BACKUP", "BASE", "BATCH", "BEFORE", "BEGIN", "BETWEEN", "BIGINT", "BINARY", "BIT", "BLOB", "BLOCK", "BOOLEAN", "BOTH", "BREADTH", "BUCKET", "BULK", "BY", "BYTE", "CALL", "CALLED", "CALLING", "CAPACITY", "CASCADE", "CASCADED", "CASE", "CAST", "CATALOG", "CHAR", "CHARACTER", "CHECK", "CLASS", "CLOB", "CLOSE", "CLUSTER", "CLUSTERED", "CLUSTERING", "CLUSTERS", "COALESCE", "COLLATE", "COLLATION", "COLLECTION", "COLUMN", "COLUMNS", "COMBINE", "COMMENT", "COMMIT", "COMPACT", "COMPILE", "COMPRESS", "CONDITION", "CONFLICT", "CONNECT", "CONNECTION", "CONSISTENCY", "CONSISTENT", "CONSTRAINT", "CONSTRAINTS", "CONSTRUCTOR", "CONSUMED", "CONTINUE", "CONVERT", "COPY", "CORRESPONDING", "COUNT", "COUNTER", "CREATE", "CROSS", "CUBE", "CURRENT", "CURSOR", "CYCLE", "DATA", "DATABASE", "DATE", "DATETIME", "DAY", "DEALLOCATE", "DEC", "DECIMAL", "DECLARE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DEFINE", "DEFINED", "DEFINITION", "DELETE", "DELIMITED", "DEPTH", "DEREF", "DESC", "DESCRIBE", "DESCRIPTOR", "DETACH", "DETERMINISTIC", "DIAGNOSTICS", "DIRECTORIES", "DISABLE", "DISCONNECT", "DISTINCT", "DISTRIBUTE", "DO", "DOMAIN", "DOUBLE", "DROP", "DUMP", "DURATION", "DYNAMIC", "EACH", "ELEMENT", "ELSE", "ELSEIF", "EMPTY", "ENABLE", "END", "EQUAL", "EQUALS", "ERROR", "ESCAPE", "ESCAPED", "EVAL", "EVALUATE", "EXCEEDED", "EXCEPT", "EXCEPTION", "EXCEPTIONS", "EXCLUSIVE", "EXEC", "EXECUTE", "EXISTS", "EXIT", "EXPLAIN", "EXPLODE", "EXPORT", "EXPRESSION", "EXTENDED", "EXTERNAL", "EXTRACT", "FAIL", "FALSE", "FAMILY", "FETCH", "FIELDS", "FILE", "FILTER", "FILTERING", "FINAL", "FINISH", "FIRST", "FIXED", "FLATTERN", "FLOAT", "FOR", "FORCE", "FOREIGN", "FORMAT", "FORWARD", "FOUND", "FREE", "FROM", "FULL", "FUNCTION", "FUNCTIONS", "GENERAL", "GENERATE", "GET", "GLOB", "GLOBAL", "GO", "GOTO", "GRANT", "GREATER", "GROUP", "GROUPING", "HANDLER", "HASH", "HAVE", "HAVING", "HEAP", "HIDDEN", "HOLD", "HOUR", "IDENTIFIED", "IDENTITY", "IF", "IGNORE", "IMMEDIATE", "IMPORT", "IN", "INCLUDING", "INCLUSIVE", "INCREMENT", "INCREMENTAL", "INDEX", "INDEXED", "INDEXES", "INDICATOR", "INFINITE", "INITIALLY", "INLINE", "INNER", "INNTER", "INOUT", "INPUT", "INSENSITIVE", "INSERT", "INSTEAD", "INT", "INTEGER", "INTERSECT", "INTERVAL", "INTO", "INVALIDATE", "IS", "ISOLATION", "ITEM", "ITEMS", "ITERATE", "JOIN", "KEY", "KEYS", "LAG", "LANGUAGE", "LARGE", "LAST", "LATERAL", "LEAD", "LEADING", "LEAVE", "LEFT", "LENGTH", "LESS", "LEVEL", "LIKE", "LIMIT", "LIMITED", "LINES", "LIST", "LOAD", "LOCAL", "LOCALTIME", "LOCALTIMESTAMP", "LOCATION", "LOCATOR", "LOCK", "LOCKS", "LOG", "LOGED", "LONG", "LOOP", "LOWER", "MAP", "MATCH", "MATERIALIZED", "MAX", "MAXLEN", "MEMBER", "MERGE", "METHOD", "METRICS", "MIN", "MINUS", "MINUTE", "MISSING", "MOD", "MODE", "MODIFIES", "MODIFY", "MODULE", "MONTH", "MULTI", "MULTISET", "NAME", "NAMES", "NATIONAL", "NATURAL", "NCHAR", "NCLOB", "NEW", "NEXT", "NO", "NONE", "NOT", "NULL", "NULLIF", "NUMBER", "NUMERIC", "OBJECT", "OF", "OFFLINE", "OFFSET", "OLD", "ON", "ONLINE", "ONLY", "OPAQUE", "OPEN", "OPERATOR", "OPTION", "OR", "ORDER", "ORDINALITY", "OTHER", "OTHERS", "OUT", "OUTER", "OUTPUT", "OVER", "OVERLAPS", "OVERRIDE", "OWNER", "PAD", "PARALLEL", "PARAMETER", "PARAMETERS", "PARTIAL", "PARTITION", "PARTITIONED", "PARTITIONS", "PATH", "PERCENT", "PERCENTILE", "PERMISSION", "PERMISSIONS", "PIPE", "PIPELINED", "PLAN", "POOL", "POSITION", "PRECISION", "PREPARE", "PRESERVE", "PRIMARY", "PRIOR", "PRIVATE", "PRIVILEGES", "PROCEDURE", "PROCESSED", "PROJECT", "PROJECTION", "PROPERTY", "PROVISIONING", "PUBLIC", "PUT", "QUERY", "QUIT", "QUORUM", "RAISE", "RANDOM", "RANGE", "RANK", "RAW", "READ", "READS", "REAL", "REBUILD", "RECORD", "RECURSIVE", "REDUCE", "REF", "REFERENCE", "REFERENCES", "REFERENCING", "REGEXP", "REGION", "REINDEX", "RELATIVE", "RELEASE", "REMAINDER", "RENAME", "REPEAT", "REPLACE", "REQUEST", "RESET", "RESIGNAL", "RESOURCE", "RESPONSE", "RESTORE", "RESTRICT", "RESULT", "RETURN", "RETURNING", "RETURNS", "REVERSE", "REVOKE", "RIGHT", "ROLE", "ROLES", "ROLLBACK", "ROLLUP", "ROUTINE", "ROW", "ROWS", "RULE", "RULES", "SAMPLE", "SATISFIES", "SAVE", "SAVEPOINT", "SCAN", "SCHEMA", "SCOPE", "SCROLL", "SEARCH", "SECOND", "SECTION", "SEGMENT", "SEGMENTS", "SELECT", "SELF", "SEMI", "SENSITIVE", "SEPARATE", "SEQUENCE", "SERIALIZABLE", "SESSION", "SET", "SETS", "SHARD", "SHARE", "SHARED", "SHORT", "SHOW", "SIGNAL", "SIMILAR", "SIZE", "SKEWED", "SMALLINT", "SNAPSHOT", "SOME", "SOURCE", "SPACE", "SPACES", "SPARSE", "SPECIFIC", "SPECIFICTYPE", "SPLIT", "SQL", "SQLCODE", "SQLERROR", "SQLEXCEPTION", "SQLSTATE", "SQLWARNING", "START", "STATE", "STATIC", "STATUS", "STORAGE", "STORE", "STORED", "STREAM", "STRING", "STRUCT", "STYLE", "SUB", "SUBMULTISET", "SUBPARTITION", "SUBSTRING", "SUBTYPE", "SUM", "SUPER", "SYMMETRIC", "SYNONYM", "SYSTEM", "TABLE", "TABLESAMPLE", "TEMP", "TEMPORARY", "TERMINATED", "TEXT", "THAN", "THEN", "THROUGHPUT", "TIME", "TIMESTAMP", "TIMEZONE", "TINYINT", "TO", "TOKEN", "TOTAL", "TOUCH", "TRAILING", "TRANSACTION", "TRANSFORM", "TRANSLATE", "TRANSLATION", "TREAT", "TRIGGER", "TRIM", "TRUE", "TRUNCATE", "TTL", "TUPLE", "TYPE", "UNDER", "UNDO", "UNION", "UNIQUE", "UNIT", "UNKNOWN", "UNLOGGED", "UNNEST", "UNPROCESSED", "UNSIGNED", "UNTIL", "UPDATE", "UPPER", "URL", "USAGE", "USE", "USER", "USERS", "USING", "UUID", "VACUUM", "VALUE", "VALUED", "VALUES", "VARCHAR", "VARIABLE", "VARIANCE", "VARINT", "VARYING", "VIEW", "VIEWS", "VIRTUAL", "VOID", "WAIT", "WHEN", "WHENEVER", "WHERE", "WHILE", "WINDOW", "WITH", "WITHIN", "WITHOUT", "WORK", "WRAPPED", "WRITE", "YEAR", "ZONE "]

うまくできました。

まとめ

思いつきで作成してみましたが、AWSのページが閉鎖されたりHTML構造が変更されない限り、仕様変更などは気にせずに使えそうです。

あとBeautifulSoup初めて使いましたが、かなり便利。

続きを読む

Djangoの既存プロジェクトをec2にデプロイ

概要

詰まりまくったのでメモ。Dockerで開発していたのでそのままデプロイしたろ!と思っていたが意味わからなすぎて断念。(おそらく?)一般的な方法でデプロイした。もっと良い方法があれば教えて欲しい限りです。

環境

OS: Amazon Linux AMI release 2017.09
ローカル: Docker
Python: 3.6.2
Django: 1.11.5
Gunicorn: 19.7.1
Nginx: 1.12.1

AWSの設定

インスタンスの作成

AWSの設定はAmazon Web Services 基礎からのネットワーク&サーバー構築を参考にした。

AWSに登録してコンソール > EC2からインスタンスを作成する。全部デフォルト。t2microなら無料。
VPC、サブネット、ルートテーブル、ゲートウェイ、セキュリティグループやらが作成される(はず)。なかったら作ってVPCに紐付ける。sshキーをダウンロードまたは登録しておく。

ポートの開放

EC2 > セキュリティグループからポートが開放できる。セキュリティグループを選択 -> インバウンド -> 編集 -> ルールの追加で80番ポート、8000番ポート(確認用、あとで閉じる)を開く。タイプはカスタムTCP、ソースは0.0.0.0/0で良い。
ここで EC2 > インスタンス から作ったインスタンスの詳細が確認できる。右側のパブリックDNSでドメイン、IPv4パブリックIPでIPが確認できる。

nginxのインストール

AWSにsshでログイン。キーペアをダウンロードした場合は~/.sshに置いて別のキー名を指定する。

# ssh -i ~/.ssh/id_rsa ec2-user@(インスタンスのIP)

以下ではrootで作業する。

$ sudo su -

以下、EC2にNginx + Gunicorn + SupervisorでDjangoアプリケーションをデプロイするを参考にnginxをインストール。

nginx入れる。

$ yum install nginx

nginxを起動する。

$ nginx

nginxの自動起動設定

$ chkconfig --add nginx
$ chkconfig nginx on

自動起動設定できているか確認する。

$ chkconfig | grep nginx
nginx           0:off   1:off   2:on    3:on    4:on    5:on    6:off

また、http://(パブリックDNS)を確認してnginxが起動しているか確認する。

Djangoプロジェクトの起動

AWSにプロジェクトを送る。

scpでzipで固めたDjangoプロジェクトを送る。また送る前にrequirements.txtは用意しておく。

# pip freeze > requirements.txt
# scp -i ~/.ssh/id_rsa ~/path/to/project.zip ec2-user@yourIP:home/ec2-user/

送ったものがhome/ec2-userに落ちているはず。解凍する

$ unzip project.zip

ほんとはgitで落とせばいいんだろうけどプライベートリポジトリにしてるので新しいuser登録して新しくssh登録してってしないといけないのかな。誰か教えてください。

pythonとかを入れる

色々考えるのがめんどくさかったのでEC2サーバにPython3環境構築を参考にした。

gitとpyenv入れる。-yが全部yesって答えるオプションらしい。初めて知った。

$ yum install git -y
$ git clone https://github.com/yyuu/pyenv.git ~/.pyenv

path通す。

$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
$ source ~/.bash_profile

コマンド通るか確認。

$ pyenv -V

依存関係を入れる。

$ sudo yum install gcc zlib-devel bzip2 bzip2-devel readline readline-devel sqlite sqlite-devel openssl openssl-devel -y

本体を入れる。

$ pyenv install 3.6.2

pythonを切り替える。これはrootのpythonなので、sudo su -後でないとデフォルトのpythonに戻ってしまうので注意。

$ pyenv global 3.6.2
$ pyenv rehash
$ python --version
Python 3.6.2

Django、その他諸々のプロジェクトに必要なライブラリをインストールする。

$ pip install --upgrade -r project/requirements.txt

requirements.txtにGunicornが入ってなければGunicornを入れる。
Gunicornとはwsgiサーバーのことで、nginxとDjangoを繋ぐものみたいなイメージ。

$ pip install gunicorn

manage.pyの上でDjangoを起動させる。

$ gunicorn your_project.wsgi --bind=0.0.0.0:8000

http://(パブリックDNS):8000を確認すると、ALLOWED_HOSTSに追加してね!と出るので追加する。

/your_project/settings.py
# 中略
ALLOWED_HOSTS = ['(パブリックDNS)']
# 以下略

もう一回確認してプロジェクトが見えれば成功。

Nginxの設定の変更

再びEC2にNginx + Gunicorn + SupervisorでDjangoアプリケーションをデプロイするを丸パクリ。本当にありがとうございました
/etc/nginx.confとあるが、Amazon Linuxでは/etc/nginx/nginx.confにあった。
以下引用

/etc/nginx.confを以下の通り編集する

/etc/nginx.conf
〜中略〜

http {
   〜中略〜

   upstream app_server {
       server 127.0.0.1:8000 fail_timeout=0;
    }

   server {
        #以下4行はコメントアウト
        #listen       80 default_server;
        #listen       [::]:80 default_server;
        #server_name  localhost;
        #root         /usr/share/nginx/html;

       # 以下3行を追加
        listen    80;
        server_name     IPアドレス or ドメイン;
        client_max_body_size    4G;

       # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

       location / {
            # 以下4行を追加
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_redirect off;
            proxy_pass   http://app_server;
        }

   〜以下略〜

nginxの再起動

$ service nginx restart
Stopping nginx:                                            [  OK  ]
Starting nginx:                                            [  OK  ]

Djangoを再びGunicornで立ち上げる

$ gunicorn your_project.wsgi --bind=0.0.0.0:8000

http://(パブリックDNS)で確認できたら成功。

daemon化

今はコマンドからGunicornを起動しているだけなので、ログアウトしたら終了してしまう。そこでGunicornをデーモン化して常駐するようにしたい。
ただ色々な所を見ると、Surpervisorというツールでそれが可能なようだが、SurpervisorはPython3系に対応していないので2系の環境をもう一つ作れとのこと。んなアホな。
もうちょっと調べてみると、普通に-Dというオプションをつけるだけでデーモンとして起動できるらしい。

起動

$ gunicorn your_project.wsgi --bind=0.0.0.0:8000 -D

終了するときは

$ ps -ef | grep gunicorn
root     17419     1  0 Oct07 ?        00:00:08 /root/.pyenv/versions/3.6.2/bin/python3.6 /root/.pyenv/versions/3.6.2/bin/gunicorn your_project.wsgi --bind=0.0.0.0:8000 -D
root     17422 17419  0 Oct07 ?        00:00:01 /root/.pyenv/versions/3.6.2/bin/python3.6 /root/.pyenv/versions/3.6.2/bin/gunicorn your_project.wsgi --bind=0.0.0.0:8000 -D
root     21686 21594  0 08:05 pts/0    00:00:00 grep --color=auto gunicorn
$ kill 17419

スクリプトで起動

ただこのままだと毎回めんどくさいので、シェルスクリプトでサーバーを起動、停止するにあるスクリプトを使わせてもらう。

Flaskとかで作ったちょっとしたサーバーの起動/停止用のシェルスクリプト。
gunicornでデーモン状態にしている。
startで起動、stopで終了、restartでstop+start。

your_project.sh
#!/bin/sh
PROGNAME=`basename $0`
BASEDIR=`dirname $0`
PIDFILE=$BASEDIR/$PROGNAME.pid

start() {
  echo "Starting server..."
  cd $BASEDIR
  gunicorn flaskhello:app -p $PIDFILE -D
}

stop() {
  echo "Stopping server..."
  kill -TERM `cat $PIDFILE`
  rm -f $PIDFILE
}

usage() {
  echo "usage: $PROGNAME start|stop|restart"
}

if [ $# -lt 1 ];  then
  usage
  exit 255
fi

case $1 in
  start)
    start
    ;;
  stop)
    stop
    ;;
  restart)
    stop
    start
    ;;
esac

以下のgunicorn flaskhello:app -p $PIDFILE -D
gunicorn your_project.wsgi --bind=0.0.0.0:8000 -Dに書き換える。

$ sh your_project.sh start   #起動
$ sh your_project.sh stop    #終了
$ sh your_project.sh restart #再起動

sshを切ってもhttp://(パブリックDNS)が見えたら成功。ひとまず見えるようになった。

staticファイルが見つからない

ただここまで追ってもcss,jsなどstaticファイルは見つかってないはず。以下で見えるようにする。
setting.pyのSTATIC_URL、STATIC_PATHは設定済みでローカルでは見える状態とする。
詳しい解説 -> Django での static files の扱い方まとめ

staticファイルのコピー

manage.pyの上でstaticファイルを指定したディレクトリにコピー

$ python manage.py collectstatic

nginx.confをいじる

nginx.confにstaticファイルのディレクトリを登録する。

/etc/nginx/nginx.conf
server{
    # 〜中略〜
    location /static {
         alias /home/ec2-user/your_project/static;
         #settings.pyで設定したのと同じ場所を記述
    }
    # 〜以下略〜
}

パーミッション変更

ここでnginxとgunicornを再起動する。

$ sh your_project.sh stop
Stopping server...
$ service nginx restart
Stopping nginx:                                            [  OK  ]
Starting nginx:                                            [  OK  ]
$ sh your_project.sh start
Starting server...

するととpermission deniedが出た。これはec2-userにotherからの権限がないせいだった。
参考によると、このディレクトリに移動する権限が必要らしい。
/home/でec2-userに権限を与える。

$ ls -l
drwx------  5 ec2-user ec2-user 4096 Oct  6 04:19 ec2-user
$ chmod o+x ec2-user
$ ls -l
drwx-----x  5 ec2-user ec2-user 4096 Oct  6 04:19 ec2-user

gunicornを再起動すると無事にstaticファイルにアクセスできて終了。

参考

EC2にNginx + Gunicorn + SupervisorでDjangoアプリケーションをデプロイする
EC2サーバにPython3環境構築
シェルスクリプトでサーバーを起動、停止する
Django での static files の扱い方まとめ
Nginxでつくる、どシンプルな静的コンテンツサーバ

続きを読む

開発用サーバーを作る on AWS(PHP7+Nginx)

そのまんま

3分くらいでサーバーが立つ便利な世の中。
EC2起動したらsshで接続して以下コピペすればおk。
Apacheでいい人はnginxは外してくだされ。
最後の行で止まるからEnterしたったらええよ。

sudo yum update
sudo rpm -Uvh ftp://ftp.scientificlinux.org/linux/scientific/6.4/x86_64/updates/fastbugs/scl-utils-20120927-8.el6.x86_64.rpm
sudo rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
sudo yum install -y nginx php70 php70-php-fpm php70-mbstring php70-mcrypt php70-pdo php70-xml php70-fpm
sudo sed -i -e "s#/usr/share/nginx/html;#/var/www/html;#g" /etc/nginx/nginx.conf
sudo sed -i -e "s/user = apache/user = nginx/g" /etc/php-fpm.d/www.conf
sudo sed -i -e "s/group = apache/group = nginx/g" /etc/php-fpm.d/www.conf
sudo chmod 2777 /var/www -R
sudo chown -R nginx:ec2-user /var/www/html
echo "<?php phpinfo(); ?>" >> /var/www/html/index.php
sudo chkconfig php-fpm on
sudo chkconfig nginx on
sudo service php70-php-fpm start
sudo service nginx start

アクセスしてphpinfoが表示されるか確認しませう。

おまけ

EC2を日本時間に

$ sudo mv /etc/localtime /etc/_localtime
$ sudo cp /usr/share/zoneinfo/Japan /etc/localtime

さらにおまけ

AWS SDK導入

$ curl -sS https://getcomposer.org/installer | php
$ php composer.phar require aws/aws-sdk-php

もひとつおまけ

MySQLクライアントインストール

sudo yum install mysql57 php70-mysql php70-mysqlnd

by 株式会社Arrvis

続きを読む

AWS Code Commitでgitリポジトリを作る

投稿日:2017-10-04

はじめに

今回gitの環境を自分で初めて構築したのでその備忘録として記録しておきます。(これまではsvnを使用していたり、誰かの作ったgit環境で作業をしたりしていました)

前準備

前提条件とかの確認
この部分で読む意味があるかどうか判断してもらえればありがたい。

やりたいこと

  • AWS Code Commit を使用して git リポジトリの作成
  • ローカルのwindows環境からアクセス

環境

  • Amazon Linux AMI 2016.09-release
  • Windows 8.1 Pro 64bit

前提知識

必要なもの

  • AWSアカウント
  • Windowsマシン(対応するgitがインストールできればOK)

実作業

AWS CodeCommit にアクセス

AWS CodeCommit repository を作成

『リポジトリの作成』ボタンを押してリポジトリ名(100文字以内)を入れれば完了。説明は無くてもいい。
リポジトリ名も説明も後で変更可能。とはいえ、リポジトリ名を変えると色々とメンドクサイので変えない方がよい。

SSH接続

※ root アカウントではSSH接続はできません。
※ IAMUserSSHKeys ポリシー、IAMReadOnlyAccess ポリシー、適切な AWS CodeCommit 管理ポリシーを IAM ユーザーにアタッチ
詳細はこちら

  1. Git (1.7.9 以降)インストール
  2. ssh-keygen を使用してパブリック/プライベートキーペアを作成
  3. SSH パブリックキーを IAM ユーザーにアップロード
  4. SSH 設定ファイル (drive:Users<user-name>.sshconfig) を編集
Host git-codecommit.*.amazonaws.com
  User Your-IAM-SSH-Key-ID-Here
  IdentityFile ~/.ssh/Your-Private-Key-File-Name-Here

5.作業を行うコンピュータにリポジトリのクローンを作成
git clone ssh://git-codecommit.us-east-1.amazonaws.com/v1/repos/<reponame>

最後に

以上でローカルの作業環境からAWSにpushできるようになる
ここで一応費用面についても触れておく

無料範囲

  • 最初の5人のアクティブユーザー(アクセスしないユーザーは関係なし)
  • 無制限のリポジトリ
  • 50 GB のストレージ/月
  • 10,000 回の Git リクエスト/月

追加料金

  • 追加ユーザー1人:1 USD/月(以下の特典が付く)
10 GB のストレージ/月
2000 件の Git リクエスト/月
  • 0.06 USD/GB/月
  • 0.001 USD/Git リクエスト

料金の詳細はこちら

参考リンク

AWS CodeCommit とは
Git をはじめからていねいに
私家版 Git For Windowsのインストール手順
WS CodeCommit 料金

続きを読む

フロントエンド環境+CircleCI+AWS環境構築メモ

0.やること

  • WebpackベースでReact&Redux環境を構築する
  • GitHubにプッシュしたらCircleCIが自動テストを行い、S3に用意した検証環境にデプロイ
  • テスト結果をSlackに通知する

準備物

  • AWSのIAMユーザー情報
  • S3バケット
  • GitHubアカウント(BitBacketでもOK)

1.React&Redux&Webpack

ベースとなる環境はreact-redux-starter-kitを利用しました。公式が提供しているcreate-react-appと迷いましたが、Reduxとreact-routerが設定済みということもありreact-redux-starter-kitを選択しました。

react-redux-starter-kitはreact-routerのバージョンが3系なので、開発の様子を見てアップデートしていく必要がありそうです。

Node.jsは推奨版が6系でしたが、8系でも特に問題なく動作したのとNode.jsはあっという間ににバージョンアップされるのでNodeは最新版を選択しました。

$ node -v
v8.4.0
$ npm -v
5.4.2

react-redux-starter-kitでプロジェクト作成
$ git clone https://github.com/davezuko/react-redux-starter-kit.git <PROJECT_NAME>
$ cd <PROJECT_NAME>
$ npm start

http://localhost:3000/ にアクセスしてアヒルの画像が表示されたら環境構築成功です。

webpack-dev-serverが起動しているので、ソースの変更、保存を検知して自動でリロードしてくれます。ディレクトリの構成はAtomicDesignで構成したいので実際の開発ではかなり触ります。

[参考資料]

React + ReduxのプロジェクトにAtomic DesignとImmutable.jsを使ったらいい感じになった話

アメブロ2016 ~ React/ReduxでつくるIsomorphic web app ~

2.GitHubへのプッシュを検知してCircleCIが自動テストと検証環境へのデプロイを行う

CrcleCIとGitHubを接続

CircleCIとGitHubを連携します。CircleCIの公式から「Start Building Free」→「Start With GitHub」の順に進みます。GitHubのアカウントでログインした後「Project」ページの「Add Project」をクリックします。GitHubでログインしている場合はのリポジトリをが自動的に読み込んでくれるので、利用するリポジトリを選択します。Setup Projectの設定はそのままで「StartBuilding」をクリックして完了です。

CircleCIとAWSを接続

ソースコードをAWSのS3にデプロイするので、CircleCIとAWSを連携させます。「Project」→「Setting」→「AWS Permissions」に進んでAccessKeyIdとSecretAccessKeyを入力して完了です。

circle.ymlを作成

CircleCIで行いたいことはcircle.ymlに書きます。今回はESLintの構文チェック結果とKarmaのテスト結果をTestSummaryにレポートを表示させます。

circle.yml
machine:
  node:
    version: 8.4.0
  post:
    - npm install -g npm@5
  timezone: Asia/Tokyo

dependencies:
  pre:
    - sudo pip install awscli

test:
  override:
    - ./node_modules/.bin/eslint --format=junit -o $CIRCLE_TEST_REPORTS/eslint/report.xml .
    - cross-env NODE_ENV=test karma start build/karma.config --reporters junit

deployment:
  branch: master
  commands:
    - npm run build
    - cp -Rf dist/. ./
    - aws s3 cp dist s3://<S3_BACKET_NAME>/ --recursive --acl public-read

circle.ymlの記述方法

machine:

仮想マシンの設定を記述します。デフォルトでもNodeをインストールしてくれますが、バージョンが0系だったのでフロントの環境に合わせて8系をインストールするように設定しました。postは指定したコマンドの後に実行されます。この例ではNodeをインストールした後にnpmのバージョンを上げています。逆に指定したコマンドの前に行いたい処理はpreに記述します。

dependencies:

プロジェクト固有の依存関係をインストールします。今回はaws-cliを利用するのでここでインストールしました。

test:

ここで実際のテストが走ります。package.jsonのtestを自動的に実行してくれるのですが、今回はjunitのxmlを出力してCircleCI上でレポートが見たいのでoverrideを記述してpackage.jsonのtestを上書きました。

deployment:

Webサーバにコードを展開します。今回はシングルページアプリケーションで、ビルドされたソースは静的なので、AWSのS3を選択しました。ここでリポジトリ上のソースをそのまま展開できればいいのですが、webpackでReactをビルドする必要があるので、package.jsonにか書かれているnpm run buildを走らせます。その後生成されたdistフォルダをコンテナのルートに移動させてからaws-cliのコマンドでS3にアップロードしています。branch: masterはmasterブランチにプッシュされた場合のみ実行する記述です。

[参考資料]
Configuring CircleCI

AWS S3にデプロイ

作成したバケットにStatic website hostingを有効します。
※検証環境で利用するのにBasic認証が必要でした。S3にBasic認証をかけるのには一手間必要なので今後CodeDeployを利用してEC2にデプロイしたいと思っています。

Slackと接続

通知したいSlackのWorkspaceのURLを用意します。「Project」→「Setting」から「ChatNotification」に進み、SlackのURLを入力して「Save」します。

これで準備はOKです。

3.動作チェック

適当にソースを変更してmasterブランチにプッシュします。今はmaterに直接プッシュしていますが、開発ではタスクごとのブランチにプッシュしてテストが通ったらmasterやreleaseブランチにプルリクエストを飛ばしてmaterブランチにマージ、プッシュされたらS3に展開という運用を考えています。今回はテストが成功しようが失敗しようが問答無用にS3に展開されます。

circle.ymlに書かれたプロセスが順に実行されています

task1.png

4.結果

TestSummaryに出力

task3.png

Reactがビルドされてサーバに展開

スクリーンショット 2017-09-29 22.15.29.png

Slackへの通知

スクリーンショット 2017-09-29 22.16.36.png

感想

  • CIツールそのものの環境構築を意識せず導入できるので取っ付き易い
  • 基本的な設定はpackage.jsonのscriptとcircle.ymlの記述だけで済むので設定が簡単
  • 黒画面でできる事は大体実現できる

CircleCIは出来ることが多いので、この環境をベースに試行錯誤しながらベストプラクティスを見つけたい思います:v_tone2:

続きを読む

ec2(amazon linux)にmysql5.7を導入

前提
 ・EC2のインスタンスを作成済み(yumのupdateも完了)
 ・インスタンスのセキュリティーグループでsshを許可している
 ・インスタンスにSSHに繋げる
 ・Macでターミナルを使用

手順
 1. リボジトリの追加
 2. mysqlインストール
 3. mysql起動
 4. ログイン
 5. パスワードの変更

1. リポジトリを追加 

sudo yum install http://dev.mysql.com/get/mysql57-community-release-el6-7.noarch.rpm

2. mysqlインストール

sudo yum install mysql-community-server mysql-community-devel.x86_64

 バージョンの確認

mysql --version

3. mysql起動

sudo service mysqld start

 mysql自動起動設定

sudo chkconfig mysqld on

4. ログイン

ログインできなかったorz
調べたら「/var/log/mysqld.log」に
[Note] A temporary password is generated for root@localhost: xxxxxxxxx
と初期パスワードが書いてあるのでそれを使用してログイン
※ historyに残るからpasswordはログインコマンドと一緒に書かない方が良いと思う。。。

mysql -u root -p

5. パスワードの変更

パスワードの変更をしないと処理を受け付けてくれない為、パスワードを変更
※ポリシー強め
ALTER USER root@localhost IDENTIFIED BY '新パスワード';
※下記でパスワードのポリシーを下げる事も可能
SET GLOBAL validate_password_length=4;
SET GLOBAL validate_password_policy=LOW;

新しいパスワードで再ログインして確認する!

完了♪

続きを読む

AWSのS3サービスにMavenリポジトリを構築

始めに

Apache Mavenを利用する場合、インハウスリポジトリを構築すると便利なので、これまでWebDAVが使えるWebサーバに環境構築していました。
AWSではS3に構築することが出来るので、費用面からしても断然有利なので、今回はこれを使ってみましょう。

準備環境

準備した環境は以下の通り。
Windows7 Pro
Java 1.8
Maven 3.3.3
Eclipse4.6
Spring Boot 1.5.6.RELEASE(デモ用)

AWS作業

AWSは既存のアカウントでも良いですが、今回はセキュリティ対策のため、S3にアクセス可能なアカウントを用意しました。
アクセスポリシーは、こんな感じで良いと思います。
your-bucketのところは、各自で書き換えてください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListAllMyBuckets"
            ],
            "Resource": "arn:aws:s3:::*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::your-bucket",
                "arn:aws:s3:::your-bucket/*"
            ]
        }
    ]
}

Java&Eclipse作業

適当なMavenプロジェクトを作成します。
私はSpring Bootプロジェクトで試しました。
以下は抜粋ですので、必要に応じて書き換えてください。

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <!-- (省略) -->

  <repositories>
    <repository>
      <id>s3-repos</id>
      <name>AWS S3 Repository</name>
      <url>s3://your-bucket/</url>
    </repository>
  </repositories>

  <distributionManagement>
    <repository>
      <id>aws-snapshot</id>
      <name>AWS S3 Repository</name>
      <url>s3://your-bucket/snapshot</url>
    </repository>
  </distributionManagement>

  <dependencies>
    <dependency>
  <!-- (省略) -->
    </dependency>
  </dependencies>


  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <!-- Windows環境におけるJunit実行時の文字化け対応 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <junitArtifactName>junit:junit</junitArtifactName>
          <encoding>UTF-8</encoding>
          <inputEncoding>UTF-8</inputEncoding>
          <outputEncoding>UTF-8</outputEncoding>
          <argLine>-Dfile.encoding=UTF-8</argLine>
          <!-- <skipTests>true</skipTests> -->
        </configuration>
      </plugin>
    </plugins>
    <extensions>
      <extension>
        <groupId>org.springframework.build</groupId>
        <artifactId>aws-maven</artifactId>
        <version>5.0.0.RELEASE</version>
      </extension>
    </extensions>
  </build>
</project>

あとは、Mavenコマンドで使用するユーザ設定を行います。
Eclipseの場合は、ウィンドウ→設定→Maven→ユーザー設定か、Maven実行時にユーザ設定を指定します。
Maven実行時のパラメータ設定でも良いかと思います。
S3へのアクセスにプロキシ設定が必要な場合は、プロキシサーバの設定も適宜追加します。

setting.xml
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
  <proxies>
    <proxy>
      <id>http_proxy</id>
      <active>true</active>
      <protocol>http</protocol>
      <host>xx.xx.xx.xx</host>
      <port>xx</port>
    </proxy>
    <proxy>
      <id>https_proxy</id>
      <active>true</active>
      <protocol>https</protocol>
      <host>xx.xx.xx.xx</host>
      <port>xx</port>
    </proxy>
    <proxy>
      <id>s3_proxy</id>
      <active>true</active>
      <protocol>s3</protocol>
      <host>xx.xx.xx.xx</host>
      <port>xx</port>
    </proxy>
  </proxies>
  <servers>
    <server>
      <id>aws-release</id>
      <username>アクセスキーID</username>
      <password>シークレットアクセスキー</password>
    </server>
    <server>
      <id>aws-snapshot</id>
      <username>アクセスキーID</username>
      <password>シークレットアクセスキー</password>
    </server>
  </servers>
</settings>

実行結果

mvn deplyコマンドを実行して、S3に登録を行います。
snapshotかreleaseかは、アクセスするリポジトリ名を切り替えればOKかと思います。
他にいいやり方があるとは思いますが、とりあえずこれで。

[INFO] --- maven-deploy-plugin:2.8.2:deploy (default-deploy) @ ews ---
[INFO] Uploading: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.jar
[INFO] Configuring Proxy. Proxy Host: xx.xx.xx.xx Proxy Port: xx
[INFO] Uploaded: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.jar (2000 KB at 1000.0 KB/sec)
[INFO] Uploading: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.pom
[INFO] Uploaded: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.pom (5 KB at 1.0 KB/sec)
[INFO] Downloading: s3://your-bucket/snapshot/com/example/maven-metadata.xml
[INFO] Uploading: s3://your-bucket/snapshot/com/example/maven-metadata.xml
[INFO] Uploaded: s3://your-bucket/snapshot/com/example/maven-metadata.xml (300 B at 0.1 KB/sec)

考察

インターネットで調べたものの、手順がよく分からなかったので、実際にやってみました。
他のプロジェクトからうまく引っ張れるかどうか試していませんが、jarファイルを一般公開したくないなぁって言う場合には、かなり使えるんじゃ無いでしょうか。

続きを読む

Amazon EMR 上の WebUI群 ( Hue や Zeppelin ) をSSHトンネルなしでブラウザ表示する方法

概要

EMR で構築したクラスタ上の WebUI をブラウザ表示するには SSH トンネリングを利用した接続が推奨されていますが、この方法は不便が多いため、より気軽な方法を模索しました

最終的に リバースプロキシ をおいて接続する方法 が良かったので共有します

構成

検証環境 : EMR-Release 5.2.1

SSH トンネリングによる構成

  • この図では master-public-dns-name に対してインターネット経由で SSH トンネルを作成しています
    (VPN 接続がある場合は、Local Netowark 内を通します)
  • EMR Master インスタンスのある SecurityGroup で SSH のポート(デフォルト22)を接続元指定で開け、各クライアントマシンと EMR Master インスタンス間で SSH トンネルを作成して、ポートフォワーディングによってブラウザで任意のWebUIと通信を行います

問題点

  • 各クライアント(各個人PC)マシン上で設定が必要になる(非エンジニアでは作業負荷が高い)
  • SSHで繋ぐため、クライアント(各個人PC)にEMR master への秘密鍵をばらまく必要がある(秘密鍵は隠したい)
  • 都度 SSHトンネル接続&Webブラウザアクセスの手順が面倒

Hue や Zeppelin などの分析ツールは大勢の非エンジニアが利用するケースもありえます
その場合各個人クライアントに鍵を配ったりターミナルでSSHトンネルを作成させたりする操作は煩雑でつらいものがあります

リバースプロキシをおく構成

  • プロキシサーバ に対してインターネット経由で接続します
    (SSHトンネルの例と同様、VPN 接続がある場合は Local Netowark 内を通します)
  • EMR Master とは別インスタンスでプロキシサーバを稼動させます
  • プロキシサーバまでは https、プロキシサーバから EMR master は private network 内を http で通信させます
    (個別のWebUIはSSL設定しなくて良い)
  • 各クライアント(各個人PC)マシンは追加設定不要
    (ブラウザアクセスするだけでOK)

EMRとは別にプロキシサーバを構築するため追加コストがかかりますが、個別クライアントは何の設定も要らず、ブラウザアクセスするだけで、Hue や Zeppelin などお好みの WebUI を参照できるようになります

構築手順

1. リバースプロキシを配置するサーバの Security Group 設定

外部から(またはLocalNetwork内)の https リクエストを許可します
ポートは、利用したい WebUI にあわせて必要な個数分開けます
後述 しますが、ドキュメントに載っていないポートが参照される場合があるので、適宜追加します)

タイプ    : カスタム TCP ルール 
プロトコル : TCP
ポート範囲 : 8888
ソース    : <アクセス元の Public IP (またはLocalNetwork内のアクセス元 Private IP )>

2. EMR Master の Security Group 設定

EMR Master の Security Group では、リバースプロキシ (private ip) からの http リクエストを許可します

タイプ    : すべてのトラフィック
プロトコル : すべて
ポート範囲 : すべて
ソース    : (Private Network のIP範囲) 

3. DNS設定

お好みのドメイン名を決め、そのドメインへのアクセスをプロキシサーバへ誘導するよう DNSレコード を設定します

ラベル : emr.your-site.com(管理下のお好みのドメイン名)
TTL   : 任意の時間 Time To Live
クラス : IN
タイプ : A
リソース : <リバースプロキシサーバの Public IP >

Route53などであれば次のような感じです(イメージ)

4. リバースプロキシの構築

プロキシサーバ上では、特定のドメイン/ポート指定でアクセスがきた場合に、EMR Master サーバの指定ポートへ代理アクセスするよう設定します
今回は EC2 で適当なインスタンスを確保し、Nginx を使ってプロキシさせます

Nginx インストール

リバースプロキシを配置するサーバにNginx をインストールします
(環境にあわせて、安定動作するものを選んでセットアップします)
https://www.nginx.com/resources/wiki/start/topics/tutorials/install/

SSL証明書の配置

SSL証明書を配置して、Nginx で https リクエストがきた場合に参照させます
(証明書は必要に応じて入手します)

ls /path/to/cert/*
/path/to/cert/your-site-ssl-certificate.crt
/path/to/cert/your-site-ssl-key.pem

Nginx 設定例(後でまとめて書きます)

ssl on;
ssl_certificate     /path/to/your-site-ssl-certificate.crt;
ssl_certificate_key /path/to/your-site-ssl-key.pem;

コンテンツ書き換え

WebUI群の中には、リンクを生成する際に 自身の名前 master-private-dns-name を使う場合があるため、ブラウザアクセスした場合に表示されるページ内のリンクが飛べない(名前解決できない)ケースが発生します
これを回避するために、Webサーバのコンテンツ書き換え機能を使って master-private-dns-name を名前解決できるドメイン(先ほどDNS登録した管理下のお好みのドメイン)に書き換える処理をはさんでおくとストレスなく使えるようになります

Nginx 設定例(後でまとめて書きます)

sub_filter '(master-private-dns-name)' 'emr.your-site.com(管理下のお好みのドメイン名)';
sub_filter_once off;

Nginx設定

利用したいWebUIのポートに対して、それぞれプロキシ設定を追加します
この際、上記で紹介したSSL設定とコンテンツ書き換えを加えて下記のように設定します

/path/to/nginx/nginx.conf

# hue wabapp
server {
    listen 8888 ssl;
    server_name emr.your-site.com;
    access_log /path/to/proxy-your-site.com.access.log main;

    ssl on;
    ssl_certificate     /path/to/your-site-ssl-certificate.crt;
    ssl_certificate_key /path/to/your-site-ssl-key.pem;

    location / {
        proxy_pass http://<master-private-ip>:8888/;
        sub_filter 'ip-xx-xx-xx-xx.region.compute.internal' 'emr.your-site.com';
        sub_filter_once off;
    }
}

# 
server {
    listen 8080 ssl;
..............(同様)
}

..............(必要なポート分設定)

この例では 共通のドメイン emr.your-site.com に対して ポートごとに振り分けさせていますが、もし個別のWebUIごとに別ドメインを割り当てるほうがお好みであれば、そのように設定することももちろん可能です
hue.your-site.com へのアクセスで <master-private-ip>:8888 へ誘導する など)
ただしURLの書き換えルールが複雑になってしまうので、そのあたり検討する必要がでてきます

対象となる WebUI 群

設定対象となる WebUI 群のリストは、Amazon EMR のドキュメント を参照します

なお利用するWebUIによってドキュメントに載っていないポートも参照されるため、状況に応じて必要なポートを登録します
(一応個人的に必要だったものを羅列します)

Web UI Name uri
Hue master-public-dns-name:8888/
Tez UI master-public-dns-name:8080/tez-ui/
Zeppelin master-public-dns-name:8890/
Yarn Web Proxy master-public-dns-name:20888/
YARN ResourceManager master-public-dns-name:8088/
Yarn ResourceManager? master-public-dns-name:8032/
Hadoop HDFS NameNode master-public-dns-name:50070/
Yarn timeline-service Webapp master-public-dns-name:8188/
Spark HistoryServer master-public-dns-name:18080/

設定を書き終えたら、Nginxのプロセスを起動させます

5. 動作確認

設定は以上です、実際にブラウザで 個別の WebUI を開いてみます

Hue
http:// emr.your-site.com :8888/

無事に個別のWebUIが開く & 表示されてるリンクへ正しく遷移できれば、設定完了です! :thumbsup:

まとめ

Amazon EMR 上の WebUI 群へのアクセスを リバースプロキシ経由 にすることで、以下の利点が得られます

  • WebUIへアクセスする チームメンバーの負担を大幅減できる
    (クライアントマシンの設定不要で、ブラウザアクセスですぐ使える)
    (SSHトンネル設定のように接続が切れることもない)
  • SSH秘密鍵をばらまかなくてよい
    (余計なリスクを負わなくてよい)
  • リバースプロキシで SSL設定をまとめて行うことができる
    (個別WebUIでSSL対応させなくてよく、設定を一元的に行える)
  • コンテンツ書き換え処理によって、 すべてのリンクをたどれるようにできる
    (単体の WebUI でなく、複数同時に公開可能)

ぜひおためしください!

参考資料

続きを読む