SSHポートフォワードを利用してGPUサーバを使う

機械学習に使うGPUサーバをAWS上のPrivateSubnetに置いているので、直接JupyterNotebookやTensorBoardを使う事はできません。
そこでSSHトンネルを作って外から接続できないGPUサーバへ踏み台サーバを経由して接続できるようにします。

ローカルフォワード

ローカルフォワードは良く使われる方法で、通信を踏み台サーバ(ここではBastianサーバ)から別の宛先に転送する

設定

  • 源ポート:19999(localhostで使用するポートを記載)
    ※空いているポートであれば何でもOK
  • 送り先:10.0.0.24:19999(SSHサーバが転送する先を指定する)
    ※プライベートIPが10.0.0.24/JupyterNotebookで使うポートが19999(TensorBoardを使う時はポート:6006)
  • ローカル/自動を選択

1.png

開くをクリックしてSSH接続を開始する

手順

まずGPUインスタンスに踏み台サーバより接続してJupyterNotebookを起動する

$ ssh GPU #GPUインスタンスに接続
$ source activate TensorFlow-GPU #Anacondaの仮想環境を起動
$ jupyter notebook #JupyterNotebookを起動

以下のURLにブラウザで接続


http://localhost:19999/?token=×××××××××××××××××××××× 

これでlocalhostのポート:19999にブラウザから接続するとGPUサーバ(プライベートIP:10.0.0.24/ポート:19999)に転送されてブラウザからGPUサーバにあるJupyterNotebookやTensorBoardを使える

参考

【SSH】ポートフォワーディングを使って作業が捗る putty編
SSHポートフォワード(SSHトンネル)【ローカル・リモート・ダイナミック総集編】

続きを読む

OpenFaaSを使ってGo言語でFunctionを書いて、AWSに展開したDocker環境にデプロイするまで

OpenFaaSは聞いたことがあるでしょうか。
まだ生まれて半年ほどしか経っていませんが、GithubStar数は6000を超える勢いで成長している有望なフレームワークです。

2017年10月18日に行われたServerless Meetup Osaka #4でOpenFaaSのことを聞いたので早速試してみました。

どこまでやるかというと、
OpenFaaSのAuthorであるAlex Ellisのブログで紹介されている、OpenFaaSでGo言語の関数を動かして、さらにクラウドに展開するところまでやってみたいと思います。

今回やってみて、日本語の情報がほぼ皆無で、英語でも情報がかなり少なかったので、丁寧に手順を載せておきます。

OpenFaaSいいところ

まとめとして先に良かったことを書いておきます。

  • Dockerイメージになるなら何でも動かせる
  • とはいえ、メジャーな言語のテンプレートを用意している
  • 手軽さは既存のFunction as a Serviceと変わらない

スケール・コストなどの観点はチュートリアルでは評価しきれないので、言及しません。

Faas CLIのインストール

Docker CE 17.05以上がインストールされていることが必要です。

コマンドでさくっとインストールできます。

curl -sSL https://cli.openfaas.com | sudo sh

brewコマンドが使えるなら、

brew install faas-cli

Selection_002.png

OpenFaaSデプロイ環境をローカルに追加

Kubernetesに比べると簡単に扱えると感じたDocker Swarmにデプロイします。

まずはDocker Swarm自体の初期化して、managerとworkerを動作させます。

docker swarm init

以下のコマンドでFaaSスタックをデプロイします。

git clone https://github.com/openfaas/faas && \
  cd faas && \
  git checkout 0.6.5 && \
  ./deploy_stack.sh

このデプロイした環境はどこにいるかというと、Docker Service (Swarm Mode) として動いているので、docker service lsのコマンドで確認できます。

image.png

ローカルのデプロイ環境にはhttp://localhost:8080でアクセスできるので開くと、次のようなFunction管理ポータルが表示されます。
Linuxの場合はhttp://127.0.0.1:8080で開く必要があるかもしれません。

Selection_006.png

Go言語のインストール

こちらの公式インストールガイドを参考にインストールしてください。

v1.8.3かそれ以降が必要です。
gvmでインストールしても問題ありません。

Selection_003.png

$GOPATHが設定されているか確認しておきます。
Selection_004.png

OpenFaaSプロジェクトの生成

まずはプロジェクトフォルダーを作成します。

mkdir -p $GOPATH/src/functions
cd $GOPATH/src/functions

続いてFaaS CLIを使ってプロジェクトテンプレートを生成します。
名前は参考記事通りgohashです。

faas-cli new --lang go gohash

Selection_005.png

プロジェクトの中身を確認する

gohash.ymlファイルにはテンプレートで作成されたFunctionとローカルの実行環境についての設定が書き込まれています。

gohash.yml
provider:
  name: faas
  gateway: http://localhost:8080

functions:
  gohash:
    lang: go
    handler: ./gohash
    image: gohash

gohash/handler.goにはHello Worldなコードが書かれています。

gohash/handler.go
package function

import (
    "fmt"
)

// Handle a serverless request
func Handle(req []byte) string {
    return fmt.Sprintf("Hello, Go. You said: %s", string(req))
}

そしてtemplate内には、各言語のテンプレートもありますが、Go言語のものがちゃんとあります。
main.goではOpenFaaSの仕様通り、 STDIN を受け取って、対応するFunctionを呼び出した結果を STDOUT に送るというシンプルな作りとなっています。

template/go/main.go
package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "os"

    "handler/function"
)

func main() {
    input, err := ioutil.ReadAll(os.Stdin)
    if err != nil {
        log.Fatalf("Unable to read standard input: %s", err.Error())
    }

    fmt.Println(function.Handle(input))
}

Dockerfileが各言語の環境ごとに用意されていて、ミニマルな実行環境を作成し、開発者が用意したFunctionコードをコンテナ内に格納してから最後にWatchdogを起動するという動きになっているようです。

WatchdogはGo言語で書かれた小さなHttpサーバーです。

template/go/Dockerfile
FROM golang:1.8.3-alpine3.6
# ---------- 略 -----------
WORKDIR /go/src/handler
COPY . .
# ---------- 略 -----------
CMD ["./fwatchdog"]

まずはデプロイしてみる

テンプレートを導入した時点で、環境構築が無事に完了しているか確認するために、HelloWorldのままデプロイしてみます。

faas-cli build -f gohash.yml
faas-cli deploy -f gohash.yml

BuildはDockerのイメージをダウンロードするところから始まるので、最初の実行は1分ほど待つかもしれません。

とくにエラーが表示されずに、
Selection_009.png
という風な表示がされたら成功です。

http://localhost:8080にアクセスしてgohashが追加されているか確認します。
左のFunction一覧からgohashを見つけたら適当なRequest bodyを打ち込んで INVOKE します。

Selection_007.png

これで無事にデプロイできることが確認できました。

Functionを実装していく

今回はgo1.9を使ったので、コンテナ内のgoも同じバージョンにしておきます。
今回のコードでは変更しなくても特に問題にはならないと思います。
コンテナ生成Dockerfileを書き換えてしまいます。

template/go/Dockerfile変更前
FROM golang:1.8.3-alpine3.6
template/go/Dockerfile変更後
FROM golang:1.9-alpine3.6

go言語のパッケージ管理ツールとしてdepをインストールします。

go get -u github.com/golang/dep/cmd/dep

そして、Goのデータ(Struct)からハッシュを生成するstructhashdepを使ってインストールします。
ただし、faas-cliがDockerコンテナをビルドするときに、template/go/をワーキングディレクトリとしてビルドを行うため、depの実行はこのディレクトリに移動して行う必要があります。

cd template/go/
dep init
dep ensure -add github.com/cnf/structhash

こうすると、Gopkg.lockGopkg.tomlvernder/template/go/以下に生成されます。
ちなみに、gvm環境だとdep ensureでライブラリがうまくインストールされないことがありますが、goのコードがビルドされるのはコンテナ内なので、一応そのまま進めても大丈夫です。

きちんと開発を進める場合はgvm linkthisdepがしっかりと使えるようにします。

それでは、受け取った文字列データからハッシュを生成するコードに変更しましょう。

gohash/handler.go
package function

import (
    "fmt"

    "github.com/cnf/structhash"
)

// S is sample struct
type S struct {
    Str string
    Num int
}

// Handle a serverless request
func Handle(req []byte) string {
    s := S{string(req), len(req)}

    hash, err := structhash.Hash(s, 1)
    if err != nil {
        panic(err)
    }
    return fmt.Sprintf("%s", hash)
}

プロジェクトのデプロイ

では、作成したコードをデプロイしてみましょう。

faas-cli build -f gohash.yml
faas-cli deploy -f gohash.yml

ポータルの実行結果はFunctionのデプロイで新しいものに置き換えたので結果が変わっています。

Selection_010.png

また、もちろんAPI URLも用意されているので、直接呼び出すこともできます。

curl -X POST -d "てすとめっせーじ" http://localhost:8080/function/gohash

Selection_011.png

テストを追加する

テンプレートのDockerfileにはgo testでテストを実施しているのですが、今のところテスト用のgoファイルは生成されないようです。

今は自分で作りましょう。

touch gohash/handler_test.go
gohash/handler_test.go
package function

import "testing"

func TestHandleReturnsCorrectResponse(t *testing.T) {  
    expected := "v1_b8dfcbb21f81a35afde754b30e3228cf"
    resp := Handle([]byte("Hello World"))

    if resp != expected {
        t.Fatalf("Expected: %v, Got: %v", expected, resp)
    }
}

テストを作ったところで、わざとgohash Functionを間違えて修正してしまいましょう。

handler.go修正前
hash, err := structhash.Hash(s, 1)
handler.go修正ミス
hash, err := structhash.Hash(s, 2)

この状態で再びビルドを実行します。

faas-cli build -f gohash.yml

ちゃんとビルド中にテストで失敗してくれます。
Selection_012.png

コードを元に戻してビルドが成功することを確認します。

DockerHubにビルドしたイメージをPUSH

リモート環境でOpenFaaSを動作させるためには、FunctionのDockerイメージをDockerHubまたは別のレジストリに登録しておく必要があります。

まずは、[DockerHubのユーザ名]/gohashとなるように、gohash.ymlを書き換えます。

gohash.yml書換前
    image: gohash
gohash.yml書換後
    image: gcoka/gohash

Docker Hubの登録が済んでいれば、

docker login

でログインし、

faas-cli push -f gohash.yml

でビルドしたイメージをPUSHします。

AWSにデプロイしてみる

AWS EC2コンソールで SSH KeyPairsを作成しておいてください。
ssh-addもお忘れなく。

ssh-add ~/.ssh/yourkey.pem

AWSにDockerをデプロイするためのテンプレートが用意されているので、ここにアクセスして、
Deploy Docker Community Edition (CE) for AWS (stable)をクリックします。
use your existing VPCを選択すると、Docker用ネットワークの設定をいろいろやらないといけなくなり、VPC内のネットワーク構築の知識が必要となるようです。

https://docs.docker.com/docker-for-aws/#docker-community-edition-ce-for-aws

以下の設定は環境に合わせて変更が必要です。

  • SSHキーにKeyPairsで作成したものを指定。

また、このテンプレートでのCloudFormation実行には、以下のCreateRoleが必要です。
正確な一覧はこちら

  • EC2 instances + Auto Scaling groups
  • IAM profiles
  • DynamoDB Tables
  • SQS Queue
  • VPC + subnets and security groups
  • ELB
  • CloudWatch Log Group

特に設定は変更していません。
デプロイを試すだけなので、Swarm ManagerとWorkerの数は1ずつにしました。

Selection_029.png

CloudFormationのStackデプロイが完了したら、デプロイ結果のOutputsタブから、Swarm Managerのインスタンスへのリンクが参照できるので、開きます。
Selection_030.png

Selection_031.png

このインスタンスにSSH接続を行います。
ユーザーはdockerを指定します。

ssh docker@54.159.253.49

OpenFaaSのスタックを導入するために、Gitが必要なので、インストールします。

中身はAlpine Linuxなので、apkコマンドでパッケージをインストールします。

sshコンソール
sudo apk --update add git

ローカルでにインストールしたときと同じコマンドですが再掲。

sshコンソール
git clone https://github.com/openfaas/faas && \
  cd faas && \
  git checkout 0.6.5 && \
  ./deploy_stack.sh

ではOpenFaaSスタックが無事デプロイされているか確認します。

AWSに構築したテンプレートはインターネットからはLoadBalancerを通してアクセスできるので、LoadBalancerのURLを調べます。DNS name: に表示されているものがそれです。

Selection_034.png

URLがわかったら、ポート8080を指定してアクセスします。

Selection_035.png

うまくいっていることを確認したらAWSにデプロイします。
--gatewayオプションを使えばgohash.ymlファイルを書き換える必要がありません。

faas-cli build -f gohash.yml --gateway http://your.amazon.aws.loadbalancer.url:8080

Selection_038.png

Selection_039.png

無事にAWSで動きました。

トラブルシューティング(詰まったところ)

faas-cli buildしてもコード変更が反映されない

--no-cacheをつけることで、すべてのビルドをやり直してくれます。

faas-cli build -f gohash.yml --no-cache

structhashパッケージがvenderディレクトリにコピーされない

$GOPATHが正しく設定されているか確認してください。

gvmを使ってgoをインストールしている場合は、$GOPATH/src/functionsgvm linkthisを実行すると解決するかもしれません。

AWS CloudFormationのデプロイに失敗する

use your existing VPCのテンプレートを使っている場合は、使わないでVPCの作成もDockerテンプレートに任せてください。

AWSにデプロイしたFunctionを実行しても500: Internal Server Errorになる

Docker HubにDockerイメージをPUSHしたか確認してください。

ちゃんと動いているかわからない・・・

docker serviceとして動いているのでdockerコマンドからいくつか情報を得ることができます。

指定したFunctionのプロセス動作情報を表示
docker service ps gohash
指定したFunctionのログを表示
docker service logs gohash
gatewayのログを表示
docker service logs func_gateway

最後に

Docker SwarmはMicrosoft Azure Container Serviceもサポートしているのですが、完全なマネージドの場合OpenFaaSが必要とするDockerバージョンが足りていないようです。

今回はDocker Swarmに対してデプロイしましたが、Kubernetesもサポートしているので、そちらも試してみたいと思います。
Kubernetesの場合は、Swarmよりもホスティング対応しているクラウドがいくつかあるようなので、期待が持てますね。

情報がとても少ないので、どんどん試してどんどん情報公開していきましょう!

続きを読む

AWS CLI で Athena のクエリ実行を同期的に行う

Athena に対するクエリを AWS CLI で実行する場合、一度のコマンドでクエリ結果を取得することは出来ません。

いくつかのコマンドを実行して結果を取得するような流れになってます。

クエリ結果取得までのコマンド実行の流れ

1. クエリ実行開始 ( start-query-execution )

start-query-execution \
  --query-string <value> \
  --result-configuration <value>
  • クエリは非同期的に処理されるため時間の掛かるクエリであってもコマンドの結果はすぐに返ってくる。
  • 結果は QueryExecutionId (クエリ実行 ID) のみ。

2. クエリ実行状況の問い合わせ ( get-query-execution )

get-query-execution --query-execution-id <value>
  • start-query-execution で得られたクエリ実行 ID を渡してクエリの実行状況を問い合わせる。
  • クエリの実行ステータスを取得できる。
    • SUBMITTED: 実行キューに追加された
    • RUNNING: 実行中
    • SUCCEEDED: 実行完了した
    • CANCELLED: キャンセルされた
    • FAILED: 失敗した
  • SUCCEEDED の場合は一緒に実行結果の出力先の S3 Path (.csv) が返ってくる。

3. クエリ結果取得 ( get-query-results )

get-query-results --query-execution-id <value>
  • start-query-execution で得られたクエリ実行 ID を引数に渡してクエリの実行結果を JSON format として取得する。
  • ページング取得に対応。
  • S3 へ出力された get-query-execution の実行結果 CSV を JSON 形式で取得するだけのコマンドのため、生の CSV 形式でもよければ S3 から直接取得すればよい。

つまりスクリプトでクエリを実行して結果を取得するには…

  1. start-query-execution でクエリ実行を開始
  2. 定期的に get-query-execution を実行してクエリ実行完了を待つ
  3. 完了したら aws s3 cp もしくは get-query-results で実行結果を取得する

クエリ実行完了を待つコードを毎回書くのが地味に面倒なので関数化した

#!/bin/bash
set -eu
set -o pipefail

# 取得対象のテーブル名
readonly TABLE_NAME="sample_table_name"
# Athena のクエリ結果出力先
readonly QUERY_OUTPUT_LOCATION="s3://aws-athena-query-results/"

# 同期的に Athena Query を実行します。
#
# params:
#   $1 string クエリ出力先パス
#   $2 string 実行するSQL
# result:
#   string `aws athena query_execution_result` の結果 (json)
function athena_query_execution_sync() {
  # クエリ実行開始
  query_id=$(aws athena start-query-execution \
    --result-configuration OutputLocation="$1" \
    --query-string "${2}" | jq -r '.QueryExecutionId')

  # クエリ結果問い合わせの最大リトライ回数
  MAX_RETRY=5
  # クエリ結果問い合わせ間隔 (秒)
  FETCH_INTERVAL_SECONDS=10

  try_cnt=1
  while true ; do
    if [ ${try_cnt} -ge ${MAX_RETRY} ] ; then
      echo "Error: timeout" >&2
      exit 1
    fi

    # クエリ実行完了を待つため一定時間待機
    sleep ${FETCH_INTERVAL_SECONDS}

    # クエリの状態取得
    query_execution_result=$(aws athena get-query-execution --query-execution-id ${query_id})
    query_state=$(echo ${query_execution_result} | jq -r '.QueryExecution.Status.State')

    # クエリ実行が完了していたら正常終了させる
    if [ ${query_state} = 'SUCCEEDED' ] ; then
      echo "${query_execution_result}"
      return
    fi

    try_cnt=$(( try_cnt + 1 ))
  done
}

# 事前にパーティションをロード
sql="MSCK REPAIR TABLE ${TABLE_NAME}"
athena_query_execution_sync "${QUERY_OUTPUT_LOCATION}" "${sql}"

# クエリ実行 (SQLは適当)
sql="SELECT * FROM ${TABLE_NAME} WHERE date='2017-10-21' LIMIT 20"
query_result=$(athena_query_execution_sync "${QUERY_OUTPUT_LOCATION}" "${sql}")
result_s3_path=$(echo "${query_result}" | jq -r '.QueryExecution.ResultConfiguration.OutputLocation')

# クエリ実行結果 (CSV) を stdout へ出力
aws s3 cp "${result_s3_path}" -

続きを読む

Proxyが厳しい企業内からも利用できるJupyter NotebookをAWS上に用意する

やりたいこと

  • Deep Learning向けのAIMを、Amazon AWSのEC2で動かす
  • Proxy内からもアクセスできるように、Jupyter NotebookをPort443で動かす
  • 通常は安く動かし、必要なときだけパワフルなGPUで動かす
    • インスタンスタイプを変更したらJupyter Notebookが自動で起動
    • インスタンスタイプを変更してもIPアドレスが変わない

Machine Learning や Deep Leaning を学習していると、自分のPCではパワーが足りなかったり、複数の環境を整えるために容量が足りなくなってきたりします。また、時間がかかる処理をさせている時には、コーヒーを飲みながらサクサクとWebサーフィンして待ちたいです。

そこでAmazonのAWS上にEC2インスタンスとして環境を作ります。Lazy Girlの記事を見ると、まさにDeep Learning向けのAMIが用意されている。GPUも使えます。

A Lazy Girl’s Guide to Setting Up Jupyter on EC2 for Deep Learning

Deep Learning AMIを起動

Lazy Girlの記事に従ってセットアップし、デフォルトのPort 8888 できちんとJupyter Notebookにアクセスできることを確認する。

なお、

source src/anaconda3/bin/activate root

をするだけで、

> which jupyter
~/src/anaconda3/bin/jupyter

となったので、.bash_profile にPATHを追加するくだりは必要なかった。

Port 443 で Jupyter Notebookを起動

Portを8888から、Proxyを通過できる443へ変更します。

nano ~/.jupyter/jupyter_notebook_config.py

c.NotebookApp.port = 443

そのままubuntuユーザでjupyter notebookを起動させようとすると、443のような低い番号のポートはダメだと怒られるので、rootで起動します。

sudo jupyter notebook --allow-root

これで厚いProxyの壁に阻まれた会社内からでもJupyter Notebookへアクセスして学習を続けられます!

自動起動の設定

インスタンスタイプを変更するには、一度EC2インスタンスをStopする必要があります。
自動的にJuypter Notebookが立ち上がるようにserviceとして登録しておくと便利です。
(会社のProxyがPort 22を通してくれないので、SSHして手動で起動させられない問題も解決)

sudo nano /etc/systemd/system/jupyter.service

[Unit]
After=network.target

[Service]
ExecStart=/home/ubuntu/start_jupyter.sh

[Install]
WantedBy=default.target

sudo chmod 664 /etc/systemd/system/jupyter.service

nano /home/ubuntu/start_jupyter.sh

#!/bin/bash

source /home/ubuntu/src/anaconda3/bin/activate root
cd /home/ubuntu/notebook
jupyter notebook

chmod 744 /home/ubuntu/start_jupyter.sh

サービスはrootユーザで起動するので、ubuntuユーザからsudoで起動する場合とは挙動が違うようです。rootユーザとして起動したときに読み込まれるjupyter_notebok_config.pyファイルを用意してあげます。

sudo su
>/home/ubuntu# cd
>~# mkdir .jupyter
>~# cp /home/ubuntu/.jupyter/jupyter_notebook_config.py /root/.jupyter/
>~# exit 

サービスを登録してスタート。きちんと動いていることを確認。

~$ sudo systemctl enable jupyter.service
~$ sudo systemctl start jupyter.service
~$ sudo systemctl status jupyter.service

Elastic IP でIPを固定

EC2インスタンスを再起動すると、IPアドレスが変わってしまいます。
Elastic IPサービスを使用すると、同じIPアドレスでアクセスできるようになります。

EC2 Management Console -> Elastic IPs -> Allocate new address

なお、EC2インスタンスをSTOPしている時にはElastic IPサービスの利用料が掛かります。(EC2インスタンスにattachされている時にはElastic IPサービスの部分は無料)

N.Virginiaでの料金だと、倹約のためにEC2インスタンスをSTOPしていても月に$3.6掛かります。t2.nanoをずっと動かし続けた$4.18とほとんど変わらない。

Instance Type Monthly $
stopped 3.60
t2.nano 4.18
t2.micro 8.35

おまけ (System Shell Command)

会社のProxyは非常に厳しく、Port 22を通してくれません。SSHでサーバにアクセスしてGitHubからexampleをダウンロードしたり、必要なモジュールをインストールしたりしたい時に困ります。

でもJupyter NotebookにさえWebブラウザからアクセスできればなんとかなります。

Jupyter NotebookのPython promptでは、!ping www.bbc.co.ukのように「!」で行をスタートすればシステムシェルコマンドが打てます。

Screen Shot 2017-10-21 at 10.40.29.png

続きを読む