アメリカ的間投詞

機械学習、AWS、Webなど

AWS Data Wranglerを触ってみた

つい先々週Amazon Web Services ブログで紹介されていたAWS Data Wranglerを触ってみたので、その感想などを書きます。

AWS Data WranglerAWSの各サービス上にあるデータを操作するためのPythonライブラリです(つまりサービスではない)。Python環境においてメモリ上にあるpandasのDataFrameや、PySparkのセッションで捕捉しているデータをAWSの各種リソース(S3, Redshiftなど)へとアップロードすること、またその逆の作業が行えます。

元来このような操作は各チームでライブラリを用意するか、個々のデベロッパーが都度開発することで実現されることが多かったでしょう。今回AWS Data Wranglerが提供されたことにより、そのような手間を省くのに加え、データのやり取りを行う際のベストプラクティスに沿った実装を利用できる様になることが期待できます。

基本的な使い方はリポジトリの利用例に十分に紹介されているので、S3とDataFrameをやり取りする際の細かい挙動について気になる点を1つだけ紹介します。

基本のpandas => S3 => Athena => pandas

pandasとS3のやり取りとして、実験の途中や、バッチの実行結果として生じたDataFrameをS3に保存する(したい)というケースは多いかと思います。AWS Data Wranglerはこれをサポートしています。

この結果は少々直感とは異なり、以下のようになります。

$ aws s3 ls s3://BUCKET/PREFIX/
2019-10-03 22:07:32       3436 29db73219ba048648b9ae0286c3b6747.parquet.snappy
2019-10-03 22:07:32       3457 2ada1fe72e1044a4a01d3b03c46ae558.parquet.snappy
2019-10-03 22:07:32       3436 3ea443453f7b4e37b6ceb51a06aa0c95.parquet.snappy
2019-10-03 22:07:32       3454 68a737bf6ca04d60a6cd9f16f5f3d00f.parquet.snappy
2019-10-03 22:07:32       3436 754e5d5d8f594ce7bdaf9bf42488837f.parquet.snappy
2019-10-03 22:07:32       3436 91fafdee0ea341ba8b03193b3a6bac9c.parquet.snappy
2019-10-03 22:07:32       3456 9a63221332544d5c8a8e7adbdcd6eb54.parquet.snappy
2019-10-03 22:07:32       3438 9c0b3ab83e824c74ba177f38241828d9.parquet.snappy
2019-10-03 22:07:32       3456 b55e7e25325e47a083198432bf37e5ad.parquet.snappy
2019-10-03 22:07:32       3436 d1a728e140a84d1e84572c8b54bc78a6.parquet.snappy
2019-10-03 22:07:32       3438 f39464920ee1443a84151860561b115c.parquet.snappy
2019-10-03 22:07:32       3438 fff9fe7a86644e7886b63df513599189.parquet.snappy

AWS Data WranglerはデフォルトでS3へのアップロードを実行環境のCPUのコア数分だけ並列で行います。これらのObjectのKeyは session.pandas.to_parquet の返り値として得ることができますが、別の文脈(別のプログラムやNotebook)からアクセスし直すのはやや煩雑でしょう。

代わりにAWS環境では、AWS Glueを利用して s3://BUCKET/PREFIIX/ をテーブル化し、即座にAthenaでクエリをすることが可能です。テーブルの作成はもちろん手動でも可能ですが、AWS Data Wranglerに任せることができます。

session.pandas.to_parquesttable= database= (予めAWS GlueのDatabaseを作成しておく必要があります)の引数を与えることで、AWS Glue データカタログに所望のテーブルが自動で作成されます。そして次のようにしてAthenaにクエリを発行することができます。

またAthenaで扱うデータにとってはほぼ必須となるパーティションの制御もお手の物です。DataFrameをアップロードする際に、パーティションのキーとしたい列をDataFrameに予め用意してやることで、Hive式の配置でデータを格納することができます。(このような処理を自分で実装したことが何度かあるので、ライブラリ化されて嬉しい)

$ aws s3 ls s3://BUCKET/PREFIX/
                           PRE year=2015/
                           PRE year=2016/
                           PRE year=2017/
$ aws s3 ls s3://BUCKET/PREFIX/year=2015/
                           PRE month=1/
                           PRE month=10/
                           PRE month=11/
                           PRE month=12/
                           PRE month=2/
                           PRE month=3/
                           PRE month=4/
                           PRE month=5/
                           PRE month=6/
                           PRE month=7/
                           PRE month=8/
                           PRE month=9/

うーん素晴らしい。これこれ。

まとめ

プログラムやNotebookで出来上がった有益ぽいDataFrameは、ローカルのファイルシステムではなくAWS Data Wranglerを使って即クラウド、再利用する際は同じ名前(パス、database、table)で呼び出して使う。AWS Data Wranglerが提案するプラクティスはこのようなものではないでしょうか。

AWS Data Wranglerを利用することによって、これまで実験の中間データが迷子になってしまっていたような環境では、実験の再現性などの質を高めることに つながるでしょう。(一方で依然データの名前の管理、データのドキュメンテーションなど、データの管理に必要な作業はまだまだ残っていますが…)

またpandasにはもともと pandas.io という、データソースとのやり取りをサポートする機能が備わっているのですが、AWS Data WranglerAWS環境を前提とすることでより強力な機能を提供できたとも捉えられるでしょう。

最後にこれは余談ですが、クックパッドにいたころに作った、各種データソース(Redshift、MySQLPostgreSQL、S3など)からデータを読み出す作業をライブラリ化したものがあります。

最近は殆ど手を付けていなかったのですが、AWS Data Wranglerの登場によって、ほぼ完全に「しょぼい版」になってしまいましたね。さよならakagi。

今からRailsでSNSログインを実装したい人へ

TL;DR;

  • omniauth-* gemを使ったログインの実装がOpenID Connectに準拠しているとは限らない
  • OpenID Connect準拠なログインを実装しよう
  • あるいは準拠している実装を使おう(例えばAmazon Cognitoとか)
  • Deviseについては知らない

前置き: SNSログインとネット上の情報について

emailとpasswordを二度入力させている間に貴重な新規ユーザーを離脱させてしまう代わりに、LINEやFacebookの見慣れたUIとホカホカに温まった(line.meやfacebook.comの)cookieを使ってポチポチサクサクと認証に必要な情報を集めたい。

そんな要望を実現するために精一杯頑張るのもエンジニアの仕事です。(スタートアップにいればそんな要望も実装側の都合も頑張りもまるっと自分事ってこともあるでしょう。)

つい最近私にもそんな機能を提供する必要が生じ、ひとまず手法を一通り調べてみようと"rails line login"でググってみました。

すると、(シークレットセッションで検索しなおした場合の少なくとも)上位5件は、

  • omniauth-lineを使ってLINEのアクセストークンを取得し、
  • アクセストークンを使ってユーザープロフィールエンドポイント(例えば v2/profile)からuserIdを取得し、
  • 以上によりユーザーを「#{userId}さん」として認証する!(簡単!)

といった手法を紹介するものでした。

一方で、「OAuth使ってログイン実装しちゃまずいでしょ」という風のうわさも耳にします。(恥ずかしながら私の認識もその程度でした。)

このような状況で、いざ自分で、一般ユーザーに公開するサービスに、LINEログインを実装する、という必要に迫られてみるとなかなかに心細い。

そこで、OAuthだとかOIDCだとかLINEでログインだとかについて、それを認証機構としたRailsアプリケーションを一般ユーザー向けに公開するのに十分な自信を持てる程度まで調べました。以下は調べた結果得られた知識のうち、ネット上の情報とのダイバージェンスが大きいものを抜粋してまとめます。

omniauth-* gemを使った認証(≒ログイン)が安全だとは限らない

omniauthは、実装をプラグイン出来るRack middlewareで、プログラミング言語のようなものです。

冗談はさておき、omniauthはREADME.md冒頭によれば、複数の参加者(例えばあなたのサーバーとLINE)による 認証(大事) をいい感じにするフレームワークとして動作するRack middlewareです。 つまり、omniauthのstrategyというプラグイン機構を使い、異なる様々なRailsサーバー外のサービス(LINE, Facebook, GitHub等)と連携して認証を行う方法を、omniauthを使う開発者側からは幾分透過的に扱えるといったものです。(各種デベロッパーコンソールからIDとSecret持ってきてconfig/initializer/omniauth-*.rbにぶち込んでウェーイ、が出来る)

例えば、LINEでログイン(omniauth-line)の場合、 - ログインしたい(例えば GET /loginに来た)ユーザーをLINEの認証エンドポイント(https://access.line.me/oauth2/v2.1/authorize)にリダイレクトし、 - 結果得られたauthorization codeを利用してアクセストークンを取得し、 - ユーザープロファイルエンドポイント(v2/profile)から取得したユーザー情報(uid, email等)を添えてログイン実行エンドポイント(例えば POST /login)にリダイレクトして - POST /loginに来た情報を使ってRailsがよしなにやる

ようなstrategyが実装してあり、ユーザーはただ『omniauth-lineを使う』という設定をするだけで、GET /login は見慣れたLINEの何かしらログインを行う雰囲気の画面にリダイレクトされ、気がつくとPOST /loginを通してユーザーのID(A1B2C3)が取得でき、それをよしなにやって(ユーザーを作ってセッションを作って…)に保存して「A1B2C3さんようこそ」となることが出来ます。便利。

だがしかし、風のうわさではこれはまずいらしい。それは単純には、

「『OAuth{,2}に基づいた 認可 手続きで得たトークンを使ってユーザープロファイルエンドポイント越しにuserIdを取得した(出来た)』という事実は『"userIdさん"として 認証 出来た」ということにはならない」

ということです。

意味が違うからダメというだけで、神経質なエンジニアだけが気にする問題だ、という認識はやや危険であり、OAuth{,2}が認可(〇〇してよいかを決める)を行うためのものである以上は、それに基づいた実装が認証に必要なスペックを満たしているという筋合いも保証もないわけです。実際にこのような認証を行っていたことで生じた脆弱性(OAuthのせいではないが)があり、それをサポートするためにOAuthの拡張が(何度か)行われました 。

どうしてもOAuthの仕組みと似たような形で認証を行いたい、ということであれば、OpenID Connectに従うべきです。これは紛れもなく認証のための標準化された仕様であり、OAuthの仕組みを利用し、その上にいくつかのスペックを追加することで認証のスペックとして成立させています。

細かい説明は参考文献に任せるとして、簡単には、OAuthによってアクセストークンを得る際にIDトークンが併せて返却され、その中身のsubという要素がユーザーの(IdP(LINE等)側でローカルに一意な、再利用されない)識別子であるということになります。そしてこれを以て(IdPへの信頼を前提として)、ユーザーを認証することが出来ます。

さてomniauthに話題を戻すと、omniauth-lineもomniauth-facebookもomniauth-twitterも、ユーザー情報エンドポイントのような場所からidとかuserIdとかを持ってきて、uid(つまりこの名前で認証をしたいということだ)として認めています。

これはやはりOpenID Connectに準拠しておらず、認証の方法としてOpenID Connectの名のもとに正しいとは言えません。

一般ユーザーに届けるサービスを作るのであれば、外部サービスを用いたログイン機能はOpenID Connectに準拠した認証を以て行うようにしましょう。

(念の為ですが、omniauth-*の実装が悪いと言いたいのではなく、これを認証機構として取り入れてしまうことがまずくないとはいえない、だからみんな自分でちゃんと気をつけよう!ということです。(しかし、omniauthが『複数の参加者による認証のフレームワーク』なのだとすれば、その下にOAuth的なstrategyがいろいろとブラブラとぶら下がっているのは如何なものか…))

準拠した実装をしよう

OpenID Connectに準拠したLINEログインをRails上で実装するのに手っ取り早いのは、OmniAuth::Strategies::OAuth2 を継承した自前のstrategyを書いてやることでしょう。

ちなみに、試しに読んだり書いたりしてみた個人的な感想では、OpenID Connectに準拠したクライアント側の実装はそんなに難しくはありません(IdP側は大変そう)。OAuth部分ができていれば、

  • Authorization Scopeにopenidを追加して、
  • アクセストークンと一緒に返ってきたid_tokenをjwtデコードして
  • jwtのsignatureを検証して
  • issとaudとexpを検証して
  • subをuidとする

というだけです(と思う…)。

準拠した実装を使おう(例えばAmazon Cognitoとか)

それも不安(あるいは面倒)ということであれば、準拠した実装を使うことにしましょう。

Amazon CognitoはOpenID Connectに準拠したIdPとの連携をサポートしています。当然Cognito自体もOpenID Connectに準拠しており、我々はCognitoを設定して、「認証して」だとか「IDをオクレ」だとかいうだけで、認証情報として認めることの出来るIDが降ってきます。

認証の実行に掛かるオーバーヘッド(トークン取ったりほどいたり)をクラウドに逃がせる利点もあるし、今どきのやり方としてはおすすめ出来ると思います。細かいやり方はどうかドキュメントにおまかせします。(力尽きた)


以上、各自頑張りましょう。

参考文献

OpenID Connectはそんなに大変かね? - OAuth.jp

OpenID Connect | OpenID

Thread Safe: The problem with OAuth for Authentication.