病みつきエンジニアブログ

機械学習、Python、Scala、JavaScript、などなど

保存できないけどread-onlyではないJupyter Notebookサーバーを立ち上げる

誰でも触れる、かつ、誰でもパラメータ変更できる、けど保存はしないでほしいJupyter(a.k.a IPython) Notebookサーバー作りたいことってあるじゃないですか。

例えば、社内のMySQLサーバーとかHadoop環境とかのクエリを叩けるようなJupyter Notebookを作ったとき。 誰でもクエリの内容は変えられていいけど、それが保存されると、他の人が叩くときに困る。 こんな感じ↓

f:id:yamitzky:20151204192809p:plain

configを変更する

Jupyterの場合は「~/.jupyter/jupyter_notebook_config.py」の設定ファイルに、次のような設定を加えます。

import notebook
from tornado import web

class FreezeFileContentsManager(notebook.services.contents.filemanager.FileContentsManager):
    def save(self, model, path=''):
        raise web.HTTPError(400, "You cannot save notebook on this server.")

c.NotebookApp.contents_manager_class = FreezeFileContentsManager

FreezeFileContentsManagerというふうにファイルマネージャーを自分で定義することによって、処理を乗っ取ることができます。 この場合は、saveの処理を上書きしています。

元になっているFileContentsManagerのコードはこちら。他にもいろいろoverrideすれば、処理を変えられそうです。

github.com

アウトプット変更とかだけだったら、 pre_save_hookを変更するのが良いかと思います。(参考)

AWS IAMによる権限設定のハマりどころと、効率的なデバッグ方法

Amazon Web ServiceのIAM(Identity and Access Management)は、AWSの各種サービスに対してのアクセス制御を(結構細かく)設定するためのシステムです。

ただ、いくつか掛けられる制約にも制限があり、いろいろハマるところがあったので、メモを。

シナリオ

Jenkins経由で、特定のAMIからのみ、EC2インスタンスを一時的に立ち上げたり(run-instances)、消したりしたい(terminate-instances)。
停止(stop-instances)したり再開(start-instances)したり、というライフサイクルではない。
誤って全然関係ないインスタンスを消せないように制約をつけたいし、関係ないAMIから立ちあげられないように制限したい。
ついでにインスタンスタイプにしぼりたい。
また、安全のため特定のIPからのみアクセスできるようにしたい。

このようなシナリオでも、一応それっぽい権限で設定することができます。

IAMポリシーファイルの説明

IAMのポリシーファイルは、下記のような感じです。

{
  "Version": "2012-10-17", // バージョンは2012-10-17で固定
  "Statement": [
    {
      "Effect": "Allow",  // 許可するのか拒否するのか。デフォルトは全て"Deny"なので、"Allow"を記載していく感じ
      "Action": [ "ec2:TerminateInstances" ],  // 何のアクションを許可|拒否するのか。
      "Resource": [ "arn:aws:ec2:ap-northeast-1:1234567890:instance/*" ],  // そのアクションはどのリソース(インスタンスとかセキュリティーグループとか)へアクセス可能か
      "Condition": {  // どのような条件下でのみ、このStatementが有効か
        "StringEquals": { "ec2:InstanceType": "t2.micro" }
      }
    }
  ]
}

Statementが配列なので、いっぱい増やしていく感じ。

リソース条件はアスタリスク( "*" とか "arn:aws:ec2:ap-northeast-1:1234567890:instance/*" )で指定することもできるが、リソースを絞り込めば「そのリソースにしかアクセスできない」という状態を担保できる( "arn:aws:ec2:ap-northeast-1:1234567890:instance/id-hogehoge" )。

リソース指定できるアクションに制約がある

リソース指定によってIAMに制約をかけられる・・・と思いきや、全てのアクションがリソース指定できるわけではありません

特に、DescribeInstancesなどのGET系はだいたいダメで、他にもCreateTagsもダメです。その一覧はこちら

下記のようなresource指定してしまうと、そのStatementは無効になり、その操作は許可されなくなってしまいます。 リソースに指定できるのは "*" だけです。

{
  "Effect": "Allow",
  "Action": "ec2:DescribeInstances",
  "Resource": [ "arn:aws:ec2:ap-northeast-1:1234567890:instance/*" ], // NGな例
}

アクションとリソースと条件の組み合わせに制約がある

ここに書いてある通りですが、アクションごとに指定できるリソースに種類があり、リソースごとに指定できる条件キーが決まっています。

例えば、TerminateInstancesのアクションは、インスタンスIDに関するリソースのみ指定できます。 RunInstancesはイメージ、インスタンス、キーペア、etc。。。が指定できます。逆に、指定しないとそのリソースを使うことはできません。例えば、既存のキーペアを使ってインスタンスを作成したいなら、キーペアに対するリソース指定が必要になります。

RunInstancesのように複数のリソース指定をする場合に注意が必要なのが、複数のリソースで設定できる条件キーが異なることです。 例えば、イメージには「InstanceType」を指定できますが、キーペアには「Region」しか指定できません。 そのため、次のような設定をすると、「このキーペアにアクセスできないよ><」って言われます。

{
  "Effect": "Allow",
  "Action": "ec2:RunInstances",
  "Resource": [
    "arn:aws:ec2:ap-northeast-1:1234567890:instance/*",
    "arn:aws:ec2:ap-northeast-1:1234567890:key-pair/test-key-pair"
  ],
  "Condition": {
    "StringEquals": {
      "ec2:InstanceType": "t2.micro"
    }
  }
}

この場合は、おとなしく2つのStatementに分割します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ec2:RunInstances",
      "Resource": [
        "arn:aws:ec2:ap-northeast-1:1234567890:instance/*"
      ],
      "Condition": {
        "StringEquals": {
          "ec2:InstanceType": "t2.micro"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": "ec2:RunInstances",
      "Resource": [
        "arn:aws:ec2:ap-northeast-1:1234567890:key-pair/test-key-pair",
        // その他のリソース許可の指定
      ]
    }
  ]
}

このシナリオを「リソースタグ」でクリアするのは厳しい

AWSのブログにて言及されていますが、リソースタグを活用すれば「特定のAMIから、特定のタグを設定したインスタンスを作成し、特定のタグのインスタンスのみ削除できる」ということができそうです(まぁできないんですけど)。 作るStatementとしては、

  • 特定のAMIからのみ run-instances できるStatement
  • 特定のResourceTagを持つインスタンスのみ terminate-instances できるStatement

の2つを作る、という感じ。

しかし、 run-instances する際にResourceTagは設定できず(例えばインスタンス名とか)、 create-tags しないといけないのですが、CreateTagsアクションにはリソース条件などが設定できないのです(=全許可しかできない)。ブログでもコメント欄で突っ込まれています。

create / terminate のライフサイクルではなく、start / stop のライフサイクルであれば、この方式は良さそうです。ただ今回の場合は、インスタンスは消し去りたかったので、この方式は取れませんでした。

IP制御もできるが、IPはグローバルIP

ec2に限らず、「どこのIPからリクエストがあったか?」を条件とできる、 aws:SourceIp という条件キーもあります。指定する場合は、一番最初にDenyしておくと良いです。

{
  "Effect": "Deny",
  "Action": "*",
  "Resource": "*",
  "Condition": {
      "NotIpAddress": {
          "aws:SourceIp": "123.45.67.89"
      }
  }
}

一点注意点なのが、IPはグローバルIPとして展開されます。なので、EC2インスタンス上からのみ「俺をTerminateしてくれ」という処理をできるように制限したい場合、グローバルIPが事前にわかっていないといけません。

IAMPolicy Simulatorが使いづらい

IAM Policy Simulatorは正直使いづらいです。条件に合致しないときには、「何も一致しませんでした」としか言われません。何がダメだったのかもわからないです。

そこで、AWS CLIから実際にdry-runすると良いです。

aws ec2 run-instances --dry-run --image-id=ami-abcdefg --count=1 --instance-type t2.micro

こうすると、権限が不足してれば、エンコードされたエラーメッセージが出てきます。エラーメッセージのデコードは Management Consoleの権限不足エラーをデコードする | Developers.IO を参考に、resourceという項目を見ると良いです。このresourceに対して、適切な条件が設定されていない、ということがわかります。

zshの場合は、こんな感じで関数作るとデバッグが楽です(bashは知らん)。

function sts() {
  aws sts decode-authorization-message --encoded-message $1 | jq -r ".DecodedMessage" | jq -c ".context.resource"
}

sts "エンコードされたエラーメッセージ"

最終的に取った手段

結局、うまくTerminateInstancesの条件を指定できないため、TerminateInstancesの権限を与えるのはやめました。 代わりに、次の手段を取りました。

  • EC2の作成時、shutdown時の動作を「terminate(削除)」になるようにした
  • AWS CLI経由で terminate-instances するのをやめ、EC2インスタンス内で shutdown -h now した

shutdown時の動作をterminateにするには、

aws ec2 run-instances --image-id=ami-abcdefg --count=1 --instance-type t2.micro --instance-initiated-shutdown-behavior=terminate

というように、「 --instance-initiated-shutdown-behavior=terminate 」をつけます。こうすれば、terminate-instances しなくても、shutdownするだけで同じことができます。

最終的なポリシーファイルは、下記のような感じ。思い出しながら書いてるからいろいろ間違っているかもしれんけど。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "NotIpAddress": {
          "aws:SourceIp": "123.45.67.89"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:ap-northeast-1:1234567890:instance/*",
      "Condition": {
        "StringEquals": {
          "ec2:InstanceType": "t2.micro"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": "ec2:RunInstances",
      "Resource": [
        "arn:aws:ec2:ap-northeast-1::image/ami-abcdefg",
        "arn:aws:ec2:ap-northeast-1:1234567890:security-group/*",
        "arn:aws:ec2:ap-northeast-1:1234567890:network-interface/*",
        "arn:aws:ec2:ap-northeast-1:1234567890:subnet/*",
        "arn:aws:ec2:ap-northeast-1:1234567890:volume/*",
        "arn:aws:ec2:ap-northeast-1:1234567890:key-pair/test-key-pair"
      ]
    }
  ]
}

vim-slimeで「for ... in ...」構文を便利に転送する

Vimにはvim-slimeという便利なプラグインがあり、Vim上で書いているコードを、tmuxやscreen経由で別ペインに転送することができる。

これの何が嬉しいかというと、「左はPythonソースコード」「右はPythonの実行結果」みたいな感じで、書きながら実行できる。↓

f:id:yamitzky:20151006175309p:plain

RStudioとか使ったことがある人は、便利さがイメージし易いはず。

f:id:yamitzky:20151006175521p:plain

Download and Install RStudio | RProgramming.net

for文の中だけ実行したいとき、iterableなオブジェクトの先頭を仮で入れたいケースがよくある
下記のようなコードを、

array = [1, 2, 3]
for i, x in enumerate(array):
    print(i, x)

下記のように実行したい。

i, x = enumerate(array).next()
print(x)

Vim力が足りないので、下記のような手順でとりあえず対応した。

~/.vim/after/ftplugin/python.vim を開いて、下記のような関数を追加

function! _EscapeText_python(text)
  if exists('g:slime_python_ipython') && len(split(a:text,"\n")) > 1
    return "%cpaste\n".a:text."--\n"
  else
    let no_empty_lines = substitute(a:text, '\n\s*\ze\n', "", "g")
    let stripped = substitute(substitute(a:text, '^\s*', '', ''), '\s*$', '', '')
    if stripped[:3] == 'for ' && stripped[-2:-2] == ':'
      return  substitute(stripped[4:-3], ' in ', ' = iter(', '') . ').next()
'
    else
      return substitute(no_empty_lines, "\n", "
", "g")
    end
  end
endfunction

これで、

for i, x in enumerate(array):

i, x = iter(enumerate(array)).next()  # i = 0; x = 1

で転送される、最高

TensorFlowを社内向けにざっくりLTして回帰した(+資料とか)

Googleが先日「TensorFlow」という機械学習ライブラリを発表していて、話題になっています。

さっそく今日社内で紹介LTしてきました。

「社内」のエンジニアの話で言うと、機械学習の会社ではないので、機械学習とかDeep Learningとかには深掘りして話していないです。もちろん、機械学習ライブラリとかも知らない、けど、「なんかGoogleからディープラーニングをOSSで出したって話題になっているぞ」っていう感じの人向けに話しています。

公式チュートリアルをちょっとだけ逸脱した線形回帰をやってみたサンプルもあります。

ちゃんと自動で微分できてます。

github.com

このライブラリ、結構良いなあと思うのは、Googleが使っているという実績力かなと思います。公開初日に「Googleのプロダクションに使ってるんで」の実績は、ちょっとずるい(厳密にどれくらい使ってるかはわからないけど)。

ライブラリ自体がどう良いかについては、きっと誰かがまとめてくれると思う。パフォーマンステストとかも。

個人的に気になるのは、分散環境での実行について。彼らのペーパーの中には、分散コンピューティングについていろいろ書いてあるっぽいんだけど(読んでない)、ライブラリ中に陽に機能はない気がする。有効グラフのやつなのかな。誰かチュートリアル書いてください。

流行るかどうかはわからないけど、Googleで使ってるって言われちゃうと、過不足はなさそう(か、不足があれば実装されていく、だろう)。 そろそろちゃんとDeepLearningを実装しながら勉強したいと思っていたので、今年の残りはTensorFlowやろうかなあと思っている。

dotfiles公開した+vim/zshのおすすめ設定とか

yamitzky/dotfiles

会社のPCを新調して、環境設定するタイミングだったので、いい機会だしdotfilesを公開した。 今までは自分のプライベートgitサーバーに置いてたんだけど。

やはり、公開すると、綺麗に書くようになるしいいなあという感じします。

これだけだとブログとして寂しいので、いくつか解説を。

homesickを使った

homesick という、ホームディレクトリ構築用のOSSを使いました。

/home以下にあるファイルだけシンボリックリンクが貼られる、という仕様。homeディレクトリを作らないといけないので、なんとなく不格好だけど、シンボリックリンク貼る対象が明確になってわかりやすいです。

brew-bundleはなくてもいい

rcmdnk/homebrew-file みたいな解決策があるのですが、あまりいらないかなと思います。

インストールしたいものをテキストファイル(例えば、Brewlist)に一覧に書いて、

brew install `cat Brewlist`

みたいな感じで叩いてあげると、自動でインストールできます。逆に、Brewlist的なものを出すには

brew list > Brewlist

サブ設定を分割

.zshrcや.vimrcがどうしても肥大化してしまうので、「alias.zsh」とか「plugin.vim」みたいな感じで分割して、ロードしてあげる仕様にした。

vimの設定について

気に入っているのが、

  • 移動がjkilのゆとり仕様
  • カラースキームはwombat
  • バックアップファイルは ~/.vim/tmp に吐き出す
  • Pythonの「# -- coding: utf-8 --」をコマンド化

zsh用便利関数

  • SHA1を作る関数 sha1
  • Macの通知をする関数 notify
  • 任意サイズのランダムな画像を作る randimg
  • ファイル更新があったら通知する quickwatch
  • :tabe って書くとvimが立ち上がる
  • cd..って書くと cd .. になる
  • その他、Python経由のサーバーとかJSONパースとかURLエンコードとか

便利なOSS

  • よく使うディレクトリへすぐに飛べる autojump
  • peco を使った履歴検索
  • zsh用補完の、zsh-completions
  • コマンドエラーがないかわかりやすい、zsh-syntax-highlighting

直したいところ

  • OS依存
  • オレオレソフトウェアのgithub公開

何かご意見あれば、リプライください!

そんじゃーね!

「メールの添付ファイルにパスワードかけて、別メールでパスワードを送る」に言いたいこと

(2015/8/29追記)

最初に代案だけ書いておくと、(メールで送る程度の秘匿性のものは)「Proself」みたいな別プロトコルを使う、です。
メールパスワードでは、パスワードに規約をつけることもできません。
また、この話は企業等においてのルール化の話です。


もう何年前のネタなんだろうという感じでもあるのですが、2015年現在もこの慣習はなくなっていないように感じます。 実際社会人になってからも、残念なことに一度言われたことがありますし(受け売りでしか喋れなかったんだと思いますが)、自分もやったことがあります。

そこで、いくつかのケーススタディーから「この方法に意味があるのか」そして「どういう方式がセキュアか」という話をします。

メールのアカウントが漏洩するケース

例えば、POP3アカウントのIDとパスワードが流出して、アカウントに不正ログインされてしまったと仮定します。

このケースの場合は、添付ファイルを含むメールも、添付ファイルのパスワードを含むメールも流出していることになるので、意味がありません

これらケースでセキュアにするとしたら、ZIPはメールで、パスワードは電話や手紙・口頭(=別のプロトコル)で通知する必要があります。

メールを誤送信するケース

例えば、メールを誤送信する確率が、仮に1%だとします。

そのとき、(当たり前ですが)「添付ファイルつきのメール」を誤送信する確率も、1%です。したがって、「添付ファイルそのもの」を誤送信する確率は、全く変わっていません。しかも、パスワードを送るメールアドレスをコピペしてたりすると、結局確率は変わっていないわけです。

そこで問題になるのは、ZIPファイルのパスワードに意味があるのか、という問題です。意味がないとは言わないのですが、数文字程度の英数字とか、辞書に含まれる単語とかを使ってしまうと一瞬で解読されますし(ブルートフォースアタックや辞書攻撃)、パスワードはいずれ解読されるリスクがありますので、流出した時点でアウトです。例えば、暗号化(ハッシュ化)されたデータベースであっても、漏洩するとニュースリリースを出したりします。

パスワードの解読にかかる時間を見てみると、Password Recovery Speeds というサイトでは、英文字だけの場合、Class Eのコンピュータ(Workstation)で10桁のパスワードでも、16日で解読が終わると書いてあります。

したがって、このケースで問題ないのは、解読するほど価値が大きくない情報の場合です(トイレの鍵、という比喩ですね)。

一つ補足すると、ZIPファイルのパスワードを解読されたところで、不正アクセス防止法には該当しないのではないかと思いますので、法律による保護を求めるのは厳しいと思います(判例あるのかな・・・?)。

どうするのが良いか?

“慣習”方式は、あまり意味がないということをつらつらと書いてきました。私は、この“慣習”は止めるべきであるという意見です。

なぜならば、これをセキュアな方式だと勘違いする人が一定数いて、本当に大事なデータを上記の方法で送ってしまい、漏洩するリスクを高めるからです。そして、より良い代案があるからです。

このようなやり方をちゃんとやろうとすると、記号を含むような15桁とかのパスワードを設定して、別のプロトコル(電話とか)を使ってパスワードを通知するべきだ、となります。面倒くさいですし、ルール化するのは困難であると思います。

代案というのは、例えば、Proselfのようなパッケージを使うというやり方があります。Proselfはユーザーにセキュアなパスワードを強制できますし、ZIPよりもブルートフォースの速度が落ちますし、ログも残ります。(サーバー自体が脆弱である可能性は残りますが、責任の所在は集約されます)

さらに本当にセキュアにするのであれば、中間者攻撃のリスクも懸念して、暗号化ハードディスクで授受をする、という方法もあります(笑)

まとめ

以上をまとめると、 「メールの添付ファイルにパスワードかけて、別メールでパスワードを送る」という方式に対して、まずは「あまり安全ではない」ということを認識する必要があります。

その上で、適切な方法を選択すべき、ということでした!

スパースな行列のPearson相関係数

Scipyには、ピアソン相関係数を計算するための関数、scipy.stats.pearson というものがあるのですが、残念ながらスパースな行列(scipy.sparse)には対応していません。

実際、実装を見てみると(stats.py)、

mx = x.mean()
my = y.mean()
xm, ym = x - mx, y - my

という実装があり、単純に実装すると疎行列としては効率が悪くなります。 仮にxが疎行列だったとして、x - mx は要素がほぼ -mx な密行列になってしまうからです。

これは、定義の\sum_{i=1}^{n}{\left( x_i-\bar{x} \right)\left( y_i-\bar{y} \right)} に当たりますが、式変形をすると、陽に平均を引く必要がなくなります。(Wikipediaより引用)

f:id:yamitzky:20150527150315p:plain

ということで、これをコードに直して、

import numpy as np
import scipy.sparse

def pearsons(a, b):
    if not (scipy.sparse.issparse(a) and scipy.sparse.issparse(b)):
        raise ValueError("only sparse arrays are supported")
    if a.shape != b.shape:
        raise ValueError("shape of sparse arrays must be same")
    if a.shape[0] != 1 or b.shape[0] != 1:
        raise ValueError("size of dimention 1 must be one")
    n = a.shape[1]
    a_sum = a.sum()
    b_sum = b.sum()
    nmr = n * a.multiply(b).sum() - a_sum * b_sum
    return (nmr /
            np.sqrt(n * a.multiply(a).sum() - (a_sum) ** 2) /
            np.sqrt(n * b.multiply(b).sum() - (b_sum) ** 2))

実際に検証してみると、以下のように同じ値になります。

import scipy.sparse
from scipy.stats import pearsonr

a = scipy.sparse.csr_matrix([1, 2, 1, 0, 1])
b = scipy.sparse.csr_matrix([1, 0, 1, 4, 2])

print pearsons(a, b) # -0.93250480824031368
print scipy.stats.pearsonr(a.toarray()[0], b.toarray()[0])[0] # -0.93250480824031379