アメリカ的間投詞

機械学習、AWS、Webなど

フリーランス始めて1年経ったので振り返る

2019年3月末に、それまで3年間正社員として働いていたクックパッド株式会社を退職し、フリーランスとして仕事を始めた。つまり今日からフリーランス2年目が始まるということなので、ここで1年目がどうだったのかを振り返ってみる。

仕事の内容ついて

今年度あった仕事/契約については、ソフトウェア開発業務の準委任契約、つまり時間単価で受けるケースがほとんどであった。意図的に寄せたわけではなく、仕事の内容と関わり方から最適な方法を探ると多くの場合時間単価受けに落ち着いたという感じ。その他にRailsアプリの受託開発が1件、短期だが技術顧問の仕事が1件あった。

今年やった仕事は、Webスタック(AWS,Railsがメイン)の開発業務、SRE系の業務(ロギングと監視、IaC、ツール開発など)、データ分析系の業務(データを貰ってレポートするもの)、機械学習系のPoC("AI"案件)、技術顧問である。売上としてはWeb開発、SRE系が支配的であり、案件もその辺りが多いように見受けられた。SRE系というか、稼働していてかつ売上が上がっているサービスのインフラ周りの球拾い(火消しというほどではない)をする仕事が一定量あるようだった。

仕事のとり方と契約について

正社員時代から副業でお世話になっている1件のみはエージェントからの紹介で、残りはWantedlyTwitterで声をかけてもらうか、知り合いから紹介してもらい、直接出向いて仕事を貰うというやり方であった。

直接で向いて交渉するのはなかなか大変だったというか、単身で出向いて「なるべくいい仕事をするからなるべくたくさんお金をくれ」という会話をするのは、実際に開発をすることに比べれば個人的には楽しいものではなかった。「ちなみに報酬はおいくらくらいでやってくれますか?」という質問に対して、逡巡するふりをして予め想定しておいた金額を伝えるのがなかなか気まずいので、面談前に想定している単価は伝えるようにしていた。単価は仕事の内容によっても変わってしかるべきだと思うのでなかなか難しい。

単価の折り合いがつく確率は五分五分くらいであり、さらに仕事の内容とこちらから提供できる稼働時間で現実的に参入可能かどうかというのもあるので、実際にお話をしてから仕事につながる確率は30-40%くらい。それでも今年度は仕事が足りなくて困るということはなかった。

今年度は7社との契約をし、うち4社についてすでに契約を終了している。

個人事業主特有の雑務について

契約や請求書の作成、経費精算や確定申告などのことである。フリーランスになるとこの辺が大変だという印象があるのではないかと思うので実際どうだったのかについて考えてみる。

何事も経験ということで、これらの業務はとりあえず今年は外注せずに全て自分でやってみた。総じて事務処理が超苦手ということがなければ自分で出来てしまうなという印象だが、本当に正しく納税できているのか、無駄が発生していないかどうかなどプロの意見がほしいという気持ちもあるので今年は税理士さんに相談するかもしれない。

契約

読んで判子を押すだけ。特に難しいことはなかった。

請求書の作成と経費精算

請求書は月初のなるべく早い日(例えば今日)にfreeeを使って作成した。時間単価受けの仕事についてはこれを行うために日々の業務のログを取っておく必要があるが、これは Bearに作業時間と内容をかんたんにメモをしておき、手製のSpreadsheet(休憩時間が入力できる)に転記して実働時間を計算、報酬を計算して請求書を作るという具合であった。 契約の数の分だけ作る必要があるのでそこそこ時間が取られるが、月の業務の振り返りも兼ねてのんびりやって特に辛い思いはしなかった。

確定申告

freeeに入力していた売り上げや経費から青色申告に必要な書類はおおむね自動で出来上がる。家賃やふるさと納税、金融資産の運用益などがやや面倒だが、これもfreeeの指示の通りやっていけば埋まる。納税はWebから振り込みで完了する。

ので、難しいということは特になかった。本当に正しく納税できているのかはちょっと不安。

売上について

時間単価で受けた仕事の売り上げの合計は約11,000,000円であり、ここに掛けた時間は約850時間であった。よって平均の時間単価は約13,000円/時間となる。 ここにその他の受託案件の報酬約1,000,000円ほどと、株式会社ポインティ役員報酬を加えたのが今年の年収になる。

感想と今後について

今年は並行して契約している会社が多く、そのため1つの案件に掛けられる時間が少なくなりがちであった。この場合、受けられる仕事が小さく切り出せる仕事に限定されがちであり、受けたいけど受けられない仕事があったのがやや残念であった。また会社の売り上げやミッションに対して具体的な目標やそれに伴う責任感を持つという感覚は正社員のときに比べると弱くなり、なんとなくチームに入り込みづらく、一人勝手に疎外感を感じることもあった。(ただ一方でその気楽さがありがたいと感じることもあった。)

ところで、正社員時代に有給が何日あったか覚えていないが、仮に15日/年とし、土日と祝日を加えると年間の休日は135日ほどとすれば、正社員時代は (365 - 135) * 8 = 1840時間を目安に働いていた事になる(多分)。 1840 - 850 = 990時間は株式会社ポインティ の仕事に充てたことになる。この仕事のやり方を許してくれているメンバーに感謝をしつつ、とりあえず今年も同様の体制で仕事をしていく予定。

あと今年は英会話が一切活かせずちょっと勿体なかった。英会話が必要な開発案件があったらお声掛けください。

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.